mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-28 20:42:50 -04:00
Compare commits
47
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa5fad440f | ||
|
|
bc226fb361 | ||
|
|
7817f3af70 | ||
|
|
cf1f4eb8c8 | ||
|
|
2e6ccf0d00 | ||
|
|
a560541b62 | ||
|
|
df4e300119 | ||
|
|
9bd3726d7a | ||
|
|
fc0f4b7f5c | ||
|
|
94fdd6cb72 | ||
|
|
0d235bfe27 | ||
|
|
6883b64276 | ||
|
|
2c9a78b802 | ||
|
|
6542834336 | ||
|
|
6a2719c742 | ||
|
|
38219a4abf | ||
|
|
3fc002d793 | ||
|
|
c0c17b27b5 | ||
|
|
b68b8f7d71 | ||
|
|
3d9b4be7ab | ||
|
|
489390c881 | ||
|
|
f5338611ac | ||
|
|
11fc5746ec | ||
|
|
47383daba8 | ||
|
|
9166985418 | ||
|
|
8fc488551f | ||
|
|
3a9cebc1b8 | ||
|
|
72a2d956b9 | ||
|
|
62a02c07df | ||
|
|
ec991185c6 | ||
|
|
fc3423d05e | ||
|
|
98cd19bd40 | ||
|
|
94d9de09cc | ||
|
|
7d788bff62 | ||
|
|
d0e2ad9269 | ||
|
|
6d9401c1f1 | ||
|
|
49e49efa3c | ||
|
|
d51c001835 | ||
|
|
f94fc5bf07 | ||
|
|
f397ad825c | ||
|
|
7eea797883 | ||
|
|
73856fca85 | ||
|
|
3d27c620e4 | ||
|
|
062b23d3a0 | ||
|
|
129fc3d593 | ||
|
|
ad40798a12 | ||
|
|
3db8e66204 |
@@ -42,6 +42,21 @@ then the story, then the fine print.
|
||||
- A method that calls another method in the same file should appear *above* the method
|
||||
it calls. This lets readers follow the call chain downward.
|
||||
|
||||
### Ordering within test modules
|
||||
|
||||
Put test cases first, helpers last. A reader opening a test file should immediately see
|
||||
*what* is being tested before *how* the test machinery works.
|
||||
|
||||
1. **Imports**
|
||||
2. **Constants** (test IDs, URLs, fixture data) — small context that tests reference
|
||||
3. **Test functions** or **sub-modules** grouped by category
|
||||
4. **Helper functions** (builders, pipeline runners, assertions)
|
||||
|
||||
Group related tests into sub-modules (`mod pricing_tests { ... }`) when a category has 2+
|
||||
tests. Don't create a sub-module for a single test. Keep nesting to two levels max
|
||||
(`mod tests { mod category { ... } }`). Shared helpers go in the parent module so
|
||||
sub-modules can reach them via `use super::*`.
|
||||
|
||||
## Naming
|
||||
|
||||
- Handler functions: `{verb}_{noun}_handler` (e.g. `get_health_check_handler`, `login_handler`)
|
||||
@@ -53,7 +68,10 @@ then the story, then the fine print.
|
||||
## Imports
|
||||
|
||||
- Group imports: std, external crates, internal workspace crates, `crate::` imports
|
||||
- Use fully qualified paths for one-off references; `use` for repeated references
|
||||
- Prefer `use` imports over inline fully-qualified paths. Inline qualification is acceptable
|
||||
only for genuine one-off references where adding an import would be noise, or for types
|
||||
that collide with std prelude names (`Result`, `Option`, `Error`). If you reference a symbol
|
||||
more than once, always import it.
|
||||
- Prefer specific imports over wildcards, except in `api_doc.rs` and test modules
|
||||
- When two crates export a type with the same name, alias with a suffix describing the source crate:
|
||||
`use enums::...::CommonResolution as CommonResolutionEnum;`
|
||||
|
||||
@@ -10,10 +10,28 @@
|
||||
## Test Patterns
|
||||
|
||||
- Tests live in `#[cfg(test)] mod tests { ... }` at the bottom of each file
|
||||
- Group related tests in sub-modules: `mod explicit_checks`, `mod mechanical_checks`
|
||||
- Group related tests into sub-modules by category (e.g. `mod pricing_tests`, `mod resolution_tests`)
|
||||
- Only create a sub-module when it has 2+ tests — don't wrap a single test in its own module
|
||||
- Keep nesting shallow: `mod tests { mod category { ... } }` is fine; a third level is rarely needed
|
||||
- Shared helpers used by multiple sub-modules belong in the parent `mod tests`, not duplicated in each sub-module
|
||||
- Enum tests should cover: serialization, deserialization, `to_str`/`from_str` round-trip, variant count
|
||||
- Password/crypto tests: be mindful of computation cost, don't add too many bcrypt test cases
|
||||
|
||||
## Test File Layout
|
||||
|
||||
Put test cases first, helpers last. Engineers should see the actual tests before scrolling
|
||||
to understand helper plumbing.
|
||||
|
||||
Within a `mod tests { ... }` block:
|
||||
|
||||
1. **Imports** at the top
|
||||
2. **Constants** (test data IDs, URLs, etc.) — small context that tests reference
|
||||
3. **Test functions** (`#[test]` / `#[tokio::test]`) grouped by category
|
||||
4. **Helper functions** (builders, pipeline runners, etc.) at the very bottom
|
||||
|
||||
If tests are grouped into sub-modules, the same rule applies within each sub-module:
|
||||
constants first, then tests, then helpers at the end of that sub-module.
|
||||
|
||||
## What to Test
|
||||
|
||||
- New error types and their mappings
|
||||
@@ -21,6 +39,14 @@
|
||||
- Hash/deduplication functions with known inputs
|
||||
- Edge cases in parsers (empty input, missing fields, malformed data)
|
||||
|
||||
## Imports in Tests
|
||||
|
||||
- Import test fixtures and constants with `use`, not inline fully-qualified paths.
|
||||
Write `use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;` at the top,
|
||||
then reference `JUNO_AT_LAKE_IMAGE_URL` in test bodies.
|
||||
- Same rule applies to enum variants used in match arms — import them, don't write
|
||||
`crate::some::deep::module::MyEnum::Variant` inline.
|
||||
|
||||
## What Not to Test
|
||||
|
||||
- Don't add integration tests that require a running database unless explicitly asked
|
||||
|
||||
@@ -62,6 +62,7 @@ artcraft/
|
||||
- A mix of wreq and reqwest for Rust HTTP clients
|
||||
- Never use `println!` or `eprintln!` outside of tests; use `log` crate macros instead
|
||||
- When two crates export the same type name, alias imports with a suffix: `use foo::Bar as BarFoo;`
|
||||
- Prefer `use` imports over inline fully-qualified paths; only qualify inline for true one-offs or std collisions
|
||||
- TypeScript with Nx, React, Vite, Zustand, and Three.js
|
||||
- Use two spaces for indentation
|
||||
|
||||
@@ -75,3 +76,4 @@ Organize for top-to-bottom reading. Important things first, details later.
|
||||
- **In impl blocks**: constructors first, then public methods, then private helpers
|
||||
- Private helpers go *below* the methods that call them
|
||||
- Among helpers: meatier logic above leaf-level formatters
|
||||
- **In test modules**: constants first, then test cases (grouped into sub-modules when 2+), then helper functions last
|
||||
|
||||
Generated
+1
@@ -14793,6 +14793,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"test_data",
|
||||
"tokio",
|
||||
"uuid 1.22.0",
|
||||
"workspace-build-acceleration",
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ body:
|
||||
type: json
|
||||
content: |
|
||||
{
|
||||
"user_token": "user_jg7ng4kdt2x5y", // newuser946
|
||||
"user_token": "user_rtdhen9kz73kx", //
|
||||
"payments_namespace": "artcraft"
|
||||
}
|
||||
order: 4100053395280358
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Rust Workspace
|
||||
|
||||
All Rust code lives under `crates/`. The workspace is defined in `Cargo.toml` at the repo root.
|
||||
|
||||
## Code Style
|
||||
|
||||
- Two spaces for indentation (not four)
|
||||
- No minimum supported Rust version; use latest stable features freely
|
||||
- Never use `println!` or `eprintln!` outside of tests; use `log` crate macros instead
|
||||
- Use `anyhow::Result` / `AnyhowResult` for fallible functions in application code
|
||||
- Fields that are optional use the `maybe_` prefix: `maybe_creator_user_token`
|
||||
- When two crates export the same type name, alias with a suffix: `use foo::Bar as BarFoo;`
|
||||
- Prefer `use` imports over inline fully-qualified paths; only qualify inline for true one-offs or std prelude collisions (`Result`, `Option`, `Error`)
|
||||
|
||||
## File Layout
|
||||
|
||||
Organize for top-to-bottom reading. Important things first, details later.
|
||||
|
||||
1. **Constants** at the top (after imports)
|
||||
2. **Primary public type** (the main struct/enum the file defines)
|
||||
3. **Supporting types** below the primary type
|
||||
4. **API types** in order: Request, Response, Error
|
||||
5. **`impl` blocks** after their type definitions
|
||||
- Constructors first, then public methods, then private helpers
|
||||
- Callers above callees — follow the call chain downward
|
||||
|
||||
## Test Layout
|
||||
|
||||
Within `#[cfg(test)] mod tests { ... }`:
|
||||
|
||||
1. **Imports**
|
||||
2. **Constants** (test IDs, URLs, fixture data) — small context that tests reference
|
||||
3. **Test functions or sub-modules** grouped by category
|
||||
4. **Helper functions** (builders, pipeline runners, assertions)
|
||||
|
||||
Constants provide small context up front. Test cases come next so engineers see *what* is
|
||||
tested before *how* the helpers work.
|
||||
|
||||
Group related tests into sub-modules (`mod pricing_tests { ... }`) when a category has 2+
|
||||
tests. Don't wrap a single test in its own module. Keep nesting to two levels max. Shared
|
||||
helpers go in the parent `mod tests` so sub-modules can `use super::*`.
|
||||
|
||||
## Building
|
||||
|
||||
- Most crates: `cargo check -p {crate_name}` or `cargo test -p {crate_name}`
|
||||
- Crates using SQLx: `SQLX_OFFLINE=true cargo check -p {crate_name}`
|
||||
- `storyteller-web` and `mysql_queries` require `SQLX_OFFLINE=true`
|
||||
- `storyteller-web` has ~370 pre-existing warnings — check the last few lines for actual errors
|
||||
|
||||
## Key Crates
|
||||
|
||||
- `storyteller_web` — main HTTP API monolith (actix-web)
|
||||
- `artcraft` (desktop) — Tauri desktop app
|
||||
- `artcraft_router` — provider routing for image/video generation (Artcraft, Fal, Seedance2Pro, Muapi)
|
||||
- `artcraft_api_defs` — shared HTTP API type definitions
|
||||
- `seedance2pro_client` — HTTP client for the Kinovi/Seedance2Pro video generation service
|
||||
- `enums` — database-backed enums stored as VARCHAR fields
|
||||
- `tokens` — primary key identifiers with Stripe-like prefixes (e.g. `user_`, `mf_`)
|
||||
- `mysql_queries` — SQLx MySQL queries for the backend
|
||||
+15
-1
@@ -7,7 +7,7 @@ use utoipa::ToSchema;
|
||||
pub const SEEDANCE_2P0_MULTI_FUNCTION_VIDEO_GEN_PATH: &str = "/v1/generate/video/multi_function/seedance_2p0";
|
||||
|
||||
/// Seedance 2.0 multi-function video generation (text-to-video, image-to-video, and reference-based).
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct Seedance2p0MultiFunctionVideoGenRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
@@ -37,6 +37,9 @@ pub struct Seedance2p0MultiFunctionVideoGenRequest {
|
||||
/// Video aspect ratio / resolution.
|
||||
pub aspect_ratio: Option<Seedance2p0AspectRatio>,
|
||||
|
||||
/// Output resolution quality tier (480p, 720p, 1080p).
|
||||
pub output_resolution: Option<Seedance2p0OutputResolution>,
|
||||
|
||||
/// Duration in seconds (4–15).
|
||||
pub duration_seconds: Option<u8>,
|
||||
|
||||
@@ -67,6 +70,17 @@ pub enum Seedance2p0BatchCount {
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Seedance2p0OutputResolution {
|
||||
/// 480p
|
||||
FourEightyP,
|
||||
/// 720p (default)
|
||||
SevenTwentyP,
|
||||
/// 1080p
|
||||
TenEightyP,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct Seedance2p0MultiFunctionVideoGenResponse {
|
||||
pub success: bool,
|
||||
|
||||
+5
@@ -2,6 +2,7 @@ use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_image_model::CommonImageModel;
|
||||
use enums::common::generation::common_quality::CommonQuality;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
@@ -56,6 +57,9 @@ pub struct OmniGenImageProviderModelDetails {
|
||||
pub struct OmniGenImageModelDetails {
|
||||
pub model: CommonImageModel,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub model_creator: Option<ModelCreator>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub full_name: Option<String>,
|
||||
|
||||
@@ -129,6 +133,7 @@ impl Default for OmniGenImageModelDetails {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
model_creator: None,
|
||||
full_name: None,
|
||||
text_prompt_supported: None,
|
||||
text_prompt_max_length: None,
|
||||
|
||||
+5
@@ -2,6 +2,7 @@ use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_quality::CommonQuality;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use enums::common::generation::model_creator::ModelCreator;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
@@ -65,6 +66,9 @@ pub struct OmniGenVideoModelDetails {
|
||||
|
||||
pub model: CommonVideoModel,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub model_creator: Option<ModelCreator>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub full_name: Option<String>,
|
||||
|
||||
@@ -172,6 +176,7 @@ impl Default for OmniGenVideoModelDetails {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: DEFAULT_VIDEO_MODEL,
|
||||
model_creator: None,
|
||||
full_name: None,
|
||||
text_prompt_supported: None,
|
||||
text_prompt_max_length: None,
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod omni_gen_image;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
use artcraft_api_defs::omni_gen::generate_response::omni_gen_image_generate_response::OmniGenImageGenerateResponse;
|
||||
|
||||
pub const OMNI_GEN_IMAGE_GENERATE_PATH: &str = "/v1/omni_gen/generate/image";
|
||||
|
||||
pub async fn omni_gen_image_generate(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: OmniGenImageCostAndGenerateRequest,
|
||||
) -> Result<OmniGenImageGenerateResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
OMNI_GEN_IMAGE_GENERATE_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod image;
|
||||
pub mod video;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum AudioListRef<'a> {
|
||||
MediaFileTokens(&'a Vec<MediaFileToken>),
|
||||
Urls(&'a Vec<String>),
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum AudioListRef {
|
||||
MediaFileTokens(Vec<MediaFileToken>),
|
||||
Urls(Vec<String>),
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
|
||||
/// Forward-compatible character reference list.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum CharacterListRef<'a> {
|
||||
CharacterTokens(&'a Vec<CharacterToken>),
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CharacterListRef {
|
||||
CharacterTokens(Vec<CharacterToken>),
|
||||
|
||||
// In the future, we may have other identifiers for characters.
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ pub enum CommonImageModel {
|
||||
GptImage1,
|
||||
#[serde(rename = "gpt_image_1p5")]
|
||||
GptImage1p5,
|
||||
#[serde(rename = "gpt_image_2")]
|
||||
GptImage2,
|
||||
#[serde(rename = "nano_banana")]
|
||||
NanoBanana,
|
||||
#[serde(rename = "nano_banana_2")]
|
||||
|
||||
@@ -38,6 +38,9 @@ pub enum CommonVideoModel {
|
||||
#[serde(rename = "seedance_2p0")]
|
||||
Seedance2p0,
|
||||
|
||||
#[serde(rename = "seedance_2p0_fast")]
|
||||
Seedance2p0Fast,
|
||||
|
||||
#[serde(rename = "sora_2")]
|
||||
Sora2,
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ImageListRef<'a> {
|
||||
MediaFileTokens(&'a Vec<MediaFileToken>),
|
||||
Urls(&'a Vec<String>),
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ImageListRef {
|
||||
MediaFileTokens(Vec<MediaFileToken>),
|
||||
Urls(Vec<String>),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ImageRef<'a> {
|
||||
MediaFileToken(&'a MediaFileToken),
|
||||
Url(&'a str),
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ImageRef {
|
||||
MediaFileToken(MediaFileToken),
|
||||
Url(String),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum VideoListRef<'a> {
|
||||
MediaFileTokens(&'a Vec<MediaFileToken>),
|
||||
Urls(&'a Vec<String>),
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum VideoListRef {
|
||||
MediaFileTokens(Vec<MediaFileToken>),
|
||||
Urls(Vec<String>),
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ pub enum ArtcraftRouterError {
|
||||
/// The requested model is not yet supported by the router.
|
||||
UnsupportedModel(String),
|
||||
|
||||
/// [Temporary during migration] The requested model/provider is not yet supported by the router.
|
||||
UnsupportedProviderAndModelForNewApi(String),
|
||||
|
||||
/// Invalid or missing input arguments.
|
||||
InvalidInput(String),
|
||||
|
||||
@@ -30,6 +33,7 @@ impl Display for ArtcraftRouterError {
|
||||
Self::Client(e) => write!(f, "Client error: {}", e),
|
||||
Self::Download(e) => write!(f, "Download error: {}", e),
|
||||
Self::UnsupportedModel(model) => write!(f, "Unsupported model: {}", model),
|
||||
Self::UnsupportedProviderAndModelForNewApi(msg) => write!(f, "Unsupported provider/model (for new API during migration): {}", msg),
|
||||
Self::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
|
||||
Self::Provider(e) => write!(f, "Provider error: {}", e),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ClientType {
|
||||
@@ -22,6 +24,9 @@ impl Display for ClientType {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ClientError {
|
||||
/// A RouterClient was required but not provided on the draft context.
|
||||
RouterClientNotProvided,
|
||||
|
||||
/// The requested client is not configured on the RouterClient.
|
||||
ClientNotConfigured(ClientType),
|
||||
|
||||
@@ -43,6 +48,18 @@ pub enum ClientError {
|
||||
|
||||
/// Seedance2Pro only accepts URLs for media inputs, not media tokens.
|
||||
Seedance2ProOnlySupportsUrls,
|
||||
|
||||
/// The pre-dispatch context of media file token to URL map was not supplied.
|
||||
MediaFileToUrlMapNotProvided,
|
||||
|
||||
/// A media file token was not found in the provided media-file-to-URL map.
|
||||
MediaFileTokenNotFoundInMap { token: MediaFileToken },
|
||||
|
||||
/// The pre-dispatch context of character token to ID map was not supplied.
|
||||
CharacterTokenToKinoviCharacterIdNotProvided,
|
||||
|
||||
/// A character token was not found in the provided character-token-to-id map
|
||||
CharacterTokenNotFoundInMap { token: CharacterToken },
|
||||
}
|
||||
|
||||
impl Error for ClientError {}
|
||||
@@ -50,6 +67,9 @@ impl Error for ClientError {}
|
||||
impl Display for ClientError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::RouterClientNotProvided => {
|
||||
write!(f, "A RouterClient is required but was not provided on the draft context")
|
||||
}
|
||||
Self::ClientNotConfigured(client_type) => {
|
||||
write!(f, "{} client is not configured on the RouterClient", client_type)
|
||||
}
|
||||
@@ -71,6 +91,18 @@ impl Display for ClientError {
|
||||
Self::Seedance2ProOnlySupportsUrls => {
|
||||
write!(f, "Seedance2Pro only supports URLs for media inputs; resolve media tokens to URLs before calling this provider")
|
||||
}
|
||||
Self::MediaFileToUrlMapNotProvided => {
|
||||
write!(f, "Media file to URL map was not provided")
|
||||
}
|
||||
Self::MediaFileTokenNotFoundInMap { token } => {
|
||||
write!(f, "Media file token '{}' was not found in the provided URL map", token.as_str())
|
||||
}
|
||||
Self::CharacterTokenToKinoviCharacterIdNotProvided => {
|
||||
write!(f, "Character token to Kinovi character ID map was not provided")
|
||||
}
|
||||
Self::CharacterTokenNotFoundInMap { token } => {
|
||||
write!(f, "Character token '{}' was not found in the provided character-token-to-id map", token.as_str())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_dev::PlanArtcraftFlux1Dev;
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_flux_1_dev(
|
||||
plan: &PlanArtcraftFlux1Dev<'_>,
|
||||
plan: &PlanArtcraftFlux1Dev,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.02/image (2 cents). 1 credit = 1 USD cent.
|
||||
let cost_per_image: u64 = 2;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_schnell::PlanArtcraftFlux1Schnell;
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_flux_1_schnell(
|
||||
_plan: &PlanArtcraftFlux1Schnell<'_>,
|
||||
_plan: &PlanArtcraftFlux1Schnell,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: not available
|
||||
ImageGenerationCostEstimate {
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::PlanArtcraftFlux2LoraAngles;
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_flux_2_lora_angles(
|
||||
plan: &PlanArtcraftFlux2LoraAngles<'_>,
|
||||
plan: &PlanArtcraftFlux2LoraAngles,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent.
|
||||
let cost_per_image: u64 = 4;
|
||||
@@ -43,7 +43,7 @@ mod tests {
|
||||
model: CommonImageModel::Flux2LoraAngles,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1::PlanArtcraftFluxPro11;
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_flux_pro_1p1(
|
||||
plan: &PlanArtcraftFluxPro11<'_>,
|
||||
plan: &PlanArtcraftFluxPro11,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Fal pricing: $0.04 per megapixel, billed by rounding up to the nearest
|
||||
// megapixel. The default image_size values are ~1MP, so 4 cents per image.
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1_ultra::PlanArtcraftFluxPro11Ultra;
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_flux_pro_1p1_ultra(
|
||||
plan: &PlanArtcraftFluxPro11Ultra<'_>,
|
||||
plan: &PlanArtcraftFluxPro11Ultra,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.06/image (6 cents). 1 credit = 1 USD cent.
|
||||
let cost_per_image: u64 = 6;
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
};
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_gpt_image_1(
|
||||
plan: &PlanArtcraftGptImage1<'_>,
|
||||
plan: &PlanArtcraftGptImage1,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing per the fal-ai GPT Image 1 docs, adapted for our artcraft billing:
|
||||
//
|
||||
@@ -62,7 +62,7 @@ mod tests {
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn base() -> GenerateImageRequest<'static> {
|
||||
fn base() -> GenerateImageRequest {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::GptImage1,
|
||||
provider: Provider::Artcraft,
|
||||
@@ -109,7 +109,7 @@ mod tests {
|
||||
quality,
|
||||
aspect_ratio,
|
||||
image_batch_count: Some(batch),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base()
|
||||
};
|
||||
request.build().unwrap().estimate_costs()
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1p5::PlanArtcraftGptImage1p5;
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_gpt_image_1p5(
|
||||
plan: &PlanArtcraftGptImage1p5<'_>,
|
||||
plan: &PlanArtcraftGptImage1p5,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Per fal docs (fal-ai/gpt-image-1.5 and fal-ai/gpt-image-1.5/edit):
|
||||
//
|
||||
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_2::{
|
||||
ArtcraftGptImage2ImageSize, ArtcraftGptImage2Quality, PlanArtcraftGptImage2,
|
||||
};
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_gpt_image_2(
|
||||
plan: &PlanArtcraftGptImage2,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Same cost table as the Fal GPT Image 2 estimator.
|
||||
//
|
||||
// Output image cost (per output image), rounded up to whole cents:
|
||||
// Low: all sizes → 1¢
|
||||
// Medium: landscape_4_3/portrait_4_3 → 4¢
|
||||
// landscape_16_9/portrait_16_9 → 4¢
|
||||
// square → 6¢
|
||||
// square_hd → 6¢
|
||||
// High: landscape_4_3/portrait_4_3 → 15¢
|
||||
// landscape_16_9/portrait_16_9 → 16¢
|
||||
// square → 22¢
|
||||
// square_hd → 23¢
|
||||
//
|
||||
use ArtcraftGptImage2ImageSize as S;
|
||||
|
||||
let cost_per_image: u64 = match (plan.quality, plan.image_size) {
|
||||
// Low quality
|
||||
(ArtcraftGptImage2Quality::Low, _) => 1,
|
||||
|
||||
// Medium quality
|
||||
(ArtcraftGptImage2Quality::Medium, None | Some(S::Square) | Some(S::SquareHd)) => 6,
|
||||
(ArtcraftGptImage2Quality::Medium, Some(S::Landscape4x3) | Some(S::Portrait4x3)) => 4,
|
||||
(ArtcraftGptImage2Quality::Medium, Some(S::Landscape16x9) | Some(S::Portrait16x9)) => 4,
|
||||
(ArtcraftGptImage2Quality::Medium, Some(S::Auto)) => 6,
|
||||
|
||||
// High quality
|
||||
(ArtcraftGptImage2Quality::High, None | Some(S::Square)) => 22,
|
||||
(ArtcraftGptImage2Quality::High, Some(S::SquareHd)) => 23,
|
||||
(ArtcraftGptImage2Quality::High, Some(S::Landscape4x3) | Some(S::Portrait4x3)) => 15,
|
||||
(ArtcraftGptImage2Quality::High, Some(S::Landscape16x9) | Some(S::Portrait16x9)) => 16,
|
||||
(ArtcraftGptImage2Quality::High, Some(S::Auto)) => 23,
|
||||
};
|
||||
let cost_in_usd_cents = cost_per_image * plan.num_images.as_u64();
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_2::ArtcraftGptImage2NumImages;
|
||||
|
||||
fn plan(
|
||||
quality: ArtcraftGptImage2Quality,
|
||||
image_size: Option<ArtcraftGptImage2ImageSize>,
|
||||
num_images: ArtcraftGptImage2NumImages,
|
||||
) -> PlanArtcraftGptImage2 {
|
||||
PlanArtcraftGptImage2 {
|
||||
prompt: Some("p".to_string()),
|
||||
image_inputs: None,
|
||||
image_size,
|
||||
quality,
|
||||
num_images,
|
||||
idempotency_token: "test".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cents(
|
||||
quality: ArtcraftGptImage2Quality,
|
||||
image_size: Option<ArtcraftGptImage2ImageSize>,
|
||||
num_images: ArtcraftGptImage2NumImages,
|
||||
) -> u64 {
|
||||
estimate_image_cost_artcraft_gpt_image_2(&plan(quality, image_size, num_images))
|
||||
.cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
// -- Low quality (1c for all sizes) -------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn low_square() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::Low, Some(ArtcraftGptImage2ImageSize::Square), ArtcraftGptImage2NumImages::One), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_square_hd() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::Low, Some(ArtcraftGptImage2ImageSize::SquareHd), ArtcraftGptImage2NumImages::One), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_unset() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::Low, None, ArtcraftGptImage2NumImages::One), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_landscape_4x3() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::Low, Some(ArtcraftGptImage2ImageSize::Landscape4x3), ArtcraftGptImage2NumImages::One), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_four() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::Low, None, ArtcraftGptImage2NumImages::Four), 4);
|
||||
}
|
||||
|
||||
// -- Medium quality -----------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn medium_square() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::Medium, Some(ArtcraftGptImage2ImageSize::Square), ArtcraftGptImage2NumImages::One), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_square_hd() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::Medium, Some(ArtcraftGptImage2ImageSize::SquareHd), ArtcraftGptImage2NumImages::One), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_unset() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::Medium, None, ArtcraftGptImage2NumImages::One), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_landscape_4x3() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::Medium, Some(ArtcraftGptImage2ImageSize::Landscape4x3), ArtcraftGptImage2NumImages::One), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_portrait_4x3() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::Medium, Some(ArtcraftGptImage2ImageSize::Portrait4x3), ArtcraftGptImage2NumImages::One), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_landscape_16x9() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::Medium, Some(ArtcraftGptImage2ImageSize::Landscape16x9), ArtcraftGptImage2NumImages::One), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_portrait_16x9() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::Medium, Some(ArtcraftGptImage2ImageSize::Portrait16x9), ArtcraftGptImage2NumImages::One), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_square_four() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::Medium, Some(ArtcraftGptImage2ImageSize::Square), ArtcraftGptImage2NumImages::Four), 24);
|
||||
}
|
||||
|
||||
// -- High quality -------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn high_square() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::High, Some(ArtcraftGptImage2ImageSize::Square), ArtcraftGptImage2NumImages::One), 22);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_square_hd() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::High, Some(ArtcraftGptImage2ImageSize::SquareHd), ArtcraftGptImage2NumImages::One), 23);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_unset() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::High, None, ArtcraftGptImage2NumImages::One), 22);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_landscape_4x3() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::High, Some(ArtcraftGptImage2ImageSize::Landscape4x3), ArtcraftGptImage2NumImages::One), 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_portrait_4x3() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::High, Some(ArtcraftGptImage2ImageSize::Portrait4x3), ArtcraftGptImage2NumImages::One), 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_landscape_16x9() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::High, Some(ArtcraftGptImage2ImageSize::Landscape16x9), ArtcraftGptImage2NumImages::One), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_portrait_16x9() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::High, Some(ArtcraftGptImage2ImageSize::Portrait16x9), ArtcraftGptImage2NumImages::One), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_square_four() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::High, Some(ArtcraftGptImage2ImageSize::Square), ArtcraftGptImage2NumImages::Four), 88);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_square_hd_four() {
|
||||
assert_eq!(cents(ArtcraftGptImage2Quality::High, Some(ArtcraftGptImage2ImageSize::SquareHd), ArtcraftGptImage2NumImages::Four), 92);
|
||||
}
|
||||
|
||||
// -- Metadata flags -----------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn metadata_flags_are_default() {
|
||||
let estimate = estimate_image_cost_artcraft_gpt_image_2(&plan(
|
||||
ArtcraftGptImage2Quality::Medium, None, ArtcraftGptImage2NumImages::One,
|
||||
));
|
||||
assert!(!estimate.is_free);
|
||||
assert!(!estimate.is_unlimited);
|
||||
assert!(!estimate.is_rate_limited);
|
||||
assert!(!estimate.has_watermark);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana::PlanArtcraftNanaBanana;
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_nano_banana(
|
||||
plan: &PlanArtcraftNanaBanana<'_>,
|
||||
plan: &PlanArtcraftNanaBanana,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent.
|
||||
let cost_per_image: u64 = 4;
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_2::PlanArtcraftNanaBanana2;
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_nano_banana_2(
|
||||
plan: &PlanArtcraftNanaBanana2<'_>,
|
||||
plan: &PlanArtcraftNanaBanana2,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing (USD cents per image), scaled off the 1K base cost of $0.08:
|
||||
// 0.5K → $0.06 (0.75×)
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_pro::PlanArtcraftNanaBananaPro;
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_nano_banana_pro(
|
||||
plan: &PlanArtcraftNanaBananaPro<'_>,
|
||||
plan: &PlanArtcraftNanaBananaPro,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.15/image at 1K or 2K resolution, $0.30/image at 4K.
|
||||
// (Stored as USD cents.)
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::PlanArtcraftQwenEdit2511Angles;
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_qwen_edit_2511_angles(
|
||||
plan: &PlanArtcraftQwenEdit2511Angles<'_>,
|
||||
plan: &PlanArtcraftQwenEdit2511Angles,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent.
|
||||
let cost_per_image: u64 = 4;
|
||||
@@ -43,7 +43,7 @@ mod tests {
|
||||
model: CommonImageModel::QwenEdit2511Angles,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::PlanArtcraftSeedream4;
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_seedream_4(
|
||||
plan: &PlanArtcraftSeedream4<'_>,
|
||||
plan: &PlanArtcraftSeedream4,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.03/image (3 cents). 1 credit = 1 USD cent.
|
||||
let cost_per_image: u64 = 3;
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::PlanArtcraftSeedream4p5;
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_seedream_4p5(
|
||||
plan: &PlanArtcraftSeedream4p5<'_>,
|
||||
plan: &PlanArtcraftSeedream4p5,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent.
|
||||
let cost_per_image: u64 = 4;
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::PlanArtcraftSeedream5Lite;
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_seedream_5_lite(
|
||||
plan: &PlanArtcraftSeedream5Lite<'_>,
|
||||
plan: &PlanArtcraftSeedream5Lite,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent.
|
||||
let cost_per_image: u64 = 4;
|
||||
|
||||
+1
@@ -4,6 +4,7 @@ pub mod estimate_image_cost_artcraft_flux_pro_1p1;
|
||||
pub mod estimate_image_cost_artcraft_flux_pro_1p1_ultra;
|
||||
pub mod estimate_image_cost_artcraft_gpt_image_1;
|
||||
pub mod estimate_image_cost_artcraft_gpt_image_1p5;
|
||||
pub mod estimate_image_cost_artcraft_gpt_image_2;
|
||||
pub mod estimate_image_cost_artcraft_nano_banana;
|
||||
pub mod estimate_image_cost_artcraft_nano_banana_2;
|
||||
pub mod estimate_image_cost_artcraft_nano_banana_pro;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_dev::PlanFalFlux1Dev;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_flux_1_dev(
|
||||
plan: &PlanFalFlux1Dev<'_>,
|
||||
plan: &PlanFalFlux1Dev,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.025 per megapixel, billed by rounding up to nearest megapixel.
|
||||
// Default image sizes are ~1MP (1024x1024), so ~$0.025/image = 3 cents rounded up.
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_schnell::PlanFalFlux1Schnell;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_flux_1_schnell(
|
||||
plan: &PlanFalFlux1Schnell<'_>,
|
||||
plan: &PlanFalFlux1Schnell,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.003 per megapixel, billed by rounding up to nearest megapixel.
|
||||
// Default image sizes are ~1MP (1024x1024), so ~$0.003/image = 1 cent rounded up.
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1::PlanFalFluxPro11;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_flux_pro_1p1(
|
||||
plan: &PlanFalFluxPro11<'_>,
|
||||
plan: &PlanFalFluxPro11,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.04 per megapixel, billed by rounding up to nearest megapixel.
|
||||
// Default image sizes are ~1MP (1024x1024), so ~$0.04/image = 4 cents.
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1_ultra::PlanFalFluxPro11Ultra;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_flux_pro_1p1_ultra(
|
||||
plan: &PlanFalFluxPro11Ultra<'_>,
|
||||
plan: &PlanFalFluxPro11Ultra,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.06 per image.
|
||||
let cost_per_image: u64 = 6;
|
||||
|
||||
+5
-5
@@ -4,7 +4,7 @@ use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_imag
|
||||
};
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_gpt_image_1(
|
||||
plan: &PlanFalGptImage1<'_>,
|
||||
plan: &PlanFalGptImage1,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Per fal docs (fal-ai/gpt-image-1/{text-to-image,edit-image}):
|
||||
//
|
||||
@@ -59,9 +59,9 @@ mod tests {
|
||||
quality: FalGptImage1Quality,
|
||||
image_size: Option<FalGptImage1ImageSize>,
|
||||
num_images: FalGptImage1NumImages,
|
||||
) -> PlanFalGptImage1<'static> {
|
||||
) -> PlanFalGptImage1 {
|
||||
PlanFalGptImage1 {
|
||||
prompt: Some("p"),
|
||||
prompt: Some("p".to_string()),
|
||||
image_urls: vec![],
|
||||
image_size,
|
||||
quality,
|
||||
@@ -74,12 +74,12 @@ mod tests {
|
||||
image_size: Option<FalGptImage1ImageSize>,
|
||||
num_images: FalGptImage1NumImages,
|
||||
num_input_images: usize,
|
||||
) -> PlanFalGptImage1<'static> {
|
||||
) -> PlanFalGptImage1 {
|
||||
let image_urls = (0..num_input_images)
|
||||
.map(|i| format!("https://fake.example.com/img{}.png", i))
|
||||
.collect();
|
||||
PlanFalGptImage1 {
|
||||
prompt: Some("p"),
|
||||
prompt: Some("p".to_string()),
|
||||
image_urls,
|
||||
image_size,
|
||||
quality,
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_imag
|
||||
};
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_gpt_image_1p5(
|
||||
plan: &PlanFalGptImage1p5<'_>,
|
||||
plan: &PlanFalGptImage1p5,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Per fal docs (fal-ai/gpt-image-1.5 and fal-ai/gpt-image-1.5/edit):
|
||||
//
|
||||
@@ -50,9 +50,9 @@ mod tests {
|
||||
quality: FalGptImage1p5Quality,
|
||||
image_size: Option<FalGptImage1p5ImageSize>,
|
||||
num_images: FalGptImage1p5NumImages,
|
||||
) -> PlanFalGptImage1p5<'static> {
|
||||
) -> PlanFalGptImage1p5 {
|
||||
PlanFalGptImage1p5 {
|
||||
prompt: Some("p"),
|
||||
prompt: Some("p".to_string()),
|
||||
image_urls: vec![],
|
||||
image_size,
|
||||
quality,
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_2::{
|
||||
FalGptImage2ImageSize, FalGptImage2Quality, PlanFalGptImage2,
|
||||
};
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_gpt_image_2(
|
||||
plan: &PlanFalGptImage2,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Per fal docs (fal-ai/gpt-image-2 and fal-ai/gpt-image-2/edit):
|
||||
//
|
||||
// Output image cost (per output image), rounded up to whole cents:
|
||||
// Low: all sizes → 1¢
|
||||
// Medium: landscape_4_3/portrait_4_3 → 4¢
|
||||
// landscape_16_9/portrait_16_9 → 4¢
|
||||
// square → 6¢
|
||||
// square_hd → 6¢
|
||||
// High: landscape_4_3/portrait_4_3 → 15¢
|
||||
// landscape_16_9/portrait_16_9 → 16¢
|
||||
// square → 22¢
|
||||
// square_hd → 23¢
|
||||
//
|
||||
use FalGptImage2ImageSize as S;
|
||||
|
||||
let cost_per_image: u64 = match (plan.quality, plan.image_size) {
|
||||
// Low quality
|
||||
(FalGptImage2Quality::Low, _) => 1,
|
||||
|
||||
// Medium quality
|
||||
(FalGptImage2Quality::Medium, None | Some(S::Square) | Some(S::SquareHd)) => 6,
|
||||
(FalGptImage2Quality::Medium, Some(S::Landscape4x3) | Some(S::Portrait4x3)) => 4,
|
||||
(FalGptImage2Quality::Medium, Some(S::Landscape16x9) | Some(S::Portrait16x9)) => 4,
|
||||
(FalGptImage2Quality::Medium, Some(S::Auto)) => 6, // TODO: Not checked
|
||||
|
||||
// High quality
|
||||
(FalGptImage2Quality::High, None | Some(S::Square)) => 22,
|
||||
(FalGptImage2Quality::High, Some(S::SquareHd)) => 23,
|
||||
(FalGptImage2Quality::High, Some(S::Landscape4x3) | Some(S::Portrait4x3)) => 15,
|
||||
(FalGptImage2Quality::High, Some(S::Landscape16x9) | Some(S::Portrait16x9)) => 16,
|
||||
(FalGptImage2Quality::High, Some(S::Auto)) => 23, // TODO: Not checked
|
||||
};
|
||||
let cost_in_usd_cents = cost_per_image * plan.num_images.as_u64();
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_2::FalGptImage2NumImages;
|
||||
|
||||
fn plan(
|
||||
quality: FalGptImage2Quality,
|
||||
image_size: Option<FalGptImage2ImageSize>,
|
||||
num_images: FalGptImage2NumImages,
|
||||
) -> PlanFalGptImage2 {
|
||||
PlanFalGptImage2 {
|
||||
prompt: Some("p".to_string()),
|
||||
image_urls: vec![],
|
||||
image_size,
|
||||
quality,
|
||||
num_images,
|
||||
}
|
||||
}
|
||||
|
||||
fn cents(
|
||||
quality: FalGptImage2Quality,
|
||||
image_size: Option<FalGptImage2ImageSize>,
|
||||
num_images: FalGptImage2NumImages,
|
||||
) -> u64 {
|
||||
estimate_image_cost_fal_gpt_image_2(&plan(quality, image_size, num_images))
|
||||
.cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
// -- Low quality (1c for all sizes) -------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn low_square() {
|
||||
assert_eq!(cents(FalGptImage2Quality::Low, Some(FalGptImage2ImageSize::Square), FalGptImage2NumImages::One), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_square_hd() {
|
||||
assert_eq!(cents(FalGptImage2Quality::Low, Some(FalGptImage2ImageSize::SquareHd), FalGptImage2NumImages::One), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_unset() {
|
||||
assert_eq!(cents(FalGptImage2Quality::Low, None, FalGptImage2NumImages::One), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_landscape_4x3() {
|
||||
assert_eq!(cents(FalGptImage2Quality::Low, Some(FalGptImage2ImageSize::Landscape4x3), FalGptImage2NumImages::One), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low_four() {
|
||||
assert_eq!(cents(FalGptImage2Quality::Low, None, FalGptImage2NumImages::Four), 4);
|
||||
}
|
||||
|
||||
// -- Medium quality -----------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn medium_square() {
|
||||
assert_eq!(cents(FalGptImage2Quality::Medium, Some(FalGptImage2ImageSize::Square), FalGptImage2NumImages::One), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_square_hd() {
|
||||
assert_eq!(cents(FalGptImage2Quality::Medium, Some(FalGptImage2ImageSize::SquareHd), FalGptImage2NumImages::One), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_unset() {
|
||||
assert_eq!(cents(FalGptImage2Quality::Medium, None, FalGptImage2NumImages::One), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_landscape_4x3() {
|
||||
assert_eq!(cents(FalGptImage2Quality::Medium, Some(FalGptImage2ImageSize::Landscape4x3), FalGptImage2NumImages::One), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_portrait_4x3() {
|
||||
assert_eq!(cents(FalGptImage2Quality::Medium, Some(FalGptImage2ImageSize::Portrait4x3), FalGptImage2NumImages::One), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_landscape_16x9() {
|
||||
assert_eq!(cents(FalGptImage2Quality::Medium, Some(FalGptImage2ImageSize::Landscape16x9), FalGptImage2NumImages::One), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_portrait_16x9() {
|
||||
assert_eq!(cents(FalGptImage2Quality::Medium, Some(FalGptImage2ImageSize::Portrait16x9), FalGptImage2NumImages::One), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium_square_four() {
|
||||
assert_eq!(cents(FalGptImage2Quality::Medium, Some(FalGptImage2ImageSize::Square), FalGptImage2NumImages::Four), 24);
|
||||
}
|
||||
|
||||
// -- High quality -------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn high_square() {
|
||||
assert_eq!(cents(FalGptImage2Quality::High, Some(FalGptImage2ImageSize::Square), FalGptImage2NumImages::One), 22);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_square_hd() {
|
||||
assert_eq!(cents(FalGptImage2Quality::High, Some(FalGptImage2ImageSize::SquareHd), FalGptImage2NumImages::One), 23);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_unset() {
|
||||
assert_eq!(cents(FalGptImage2Quality::High, None, FalGptImage2NumImages::One), 22);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_landscape_4x3() {
|
||||
assert_eq!(cents(FalGptImage2Quality::High, Some(FalGptImage2ImageSize::Landscape4x3), FalGptImage2NumImages::One), 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_portrait_4x3() {
|
||||
assert_eq!(cents(FalGptImage2Quality::High, Some(FalGptImage2ImageSize::Portrait4x3), FalGptImage2NumImages::One), 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_landscape_16x9() {
|
||||
assert_eq!(cents(FalGptImage2Quality::High, Some(FalGptImage2ImageSize::Landscape16x9), FalGptImage2NumImages::One), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_portrait_16x9() {
|
||||
assert_eq!(cents(FalGptImage2Quality::High, Some(FalGptImage2ImageSize::Portrait16x9), FalGptImage2NumImages::One), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_square_four() {
|
||||
assert_eq!(cents(FalGptImage2Quality::High, Some(FalGptImage2ImageSize::Square), FalGptImage2NumImages::Four), 88);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_square_hd_four() {
|
||||
assert_eq!(cents(FalGptImage2Quality::High, Some(FalGptImage2ImageSize::SquareHd), FalGptImage2NumImages::Four), 92);
|
||||
}
|
||||
|
||||
// -- Metadata flags -----------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn metadata_flags_are_default() {
|
||||
let estimate = estimate_image_cost_fal_gpt_image_2(&plan(
|
||||
FalGptImage2Quality::Medium, None, FalGptImage2NumImages::One,
|
||||
));
|
||||
assert!(!estimate.is_free);
|
||||
assert!(!estimate.is_unlimited);
|
||||
assert!(!estimate.is_rate_limited);
|
||||
assert!(!estimate.has_watermark);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_ban
|
||||
};
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_nano_banana(
|
||||
plan: &PlanFalNanoBanana<'_>,
|
||||
plan: &PlanFalNanoBanana,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.039/image (no resolution multiplier — Gemini 25 Flash has no resolution option).
|
||||
// Rounded to 4 cents per image.
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_ban
|
||||
};
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_nano_banana_2(
|
||||
plan: &PlanFalNanaBanana2<'_>,
|
||||
plan: &PlanFalNanaBanana2,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.08/image base (1K). Multipliers: 0.5K = 0.75x, 2K = 1.5x, 4K = 2x.
|
||||
// (Stored as USD cents.)
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_ban
|
||||
};
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_nano_banana_pro(
|
||||
plan: &PlanFalNanaBananaPro<'_>,
|
||||
plan: &PlanFalNanaBananaPro,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.15/image at 1K or 2K resolution, $0.30/image at 4K.
|
||||
// (Stored as USD cents.)
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4::PlanFalSeedream4;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_seedream_4(
|
||||
plan: &PlanFalSeedream4<'_>,
|
||||
plan: &PlanFalSeedream4,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.03 per image.
|
||||
let cost_per_image: u64 = 3;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4p5::PlanFalSeedream4p5;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_seedream_4p5(
|
||||
plan: &PlanFalSeedream4p5<'_>,
|
||||
plan: &PlanFalSeedream4p5,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.04 per image.
|
||||
let cost_per_image: u64 = 4;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_5_lite::PlanFalSeedream5Lite;
|
||||
|
||||
pub(crate) fn estimate_image_cost_fal_seedream_5_lite(
|
||||
plan: &PlanFalSeedream5Lite<'_>,
|
||||
plan: &PlanFalSeedream5Lite,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.035 per image — round up to 4 cents.
|
||||
let cost_per_image: u64 = 4;
|
||||
|
||||
+1
@@ -4,6 +4,7 @@ pub(crate) mod estimate_image_cost_fal_flux_pro_1p1;
|
||||
pub(crate) mod estimate_image_cost_fal_flux_pro_1p1_ultra;
|
||||
pub(crate) mod estimate_image_cost_fal_gpt_image_1;
|
||||
pub(crate) mod estimate_image_cost_fal_gpt_image_1p5;
|
||||
pub(crate) mod estimate_image_cost_fal_gpt_image_2;
|
||||
pub(crate) mod estimate_image_cost_fal_seedream_4;
|
||||
pub(crate) mod estimate_image_cost_fal_seedream_4p5;
|
||||
pub(crate) mod estimate_image_cost_fal_seedream_5_lite;
|
||||
|
||||
+5
-5
@@ -9,12 +9,12 @@ use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image:
|
||||
use artcraft_client::endpoints::generate::image::text::generate_flux_1_dev_text_to_image::generate_flux_1_dev_text_to_image;
|
||||
|
||||
pub async fn execute_artcraft_flux_1_dev(
|
||||
plan: &PlanArtcraftFlux1Dev<'_>,
|
||||
plan: &PlanArtcraftFlux1Dev,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let request = GenerateFlux1DevTextToImageRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
prompt: plan.prompt.clone(),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
num_images: Some(plan.num_images),
|
||||
};
|
||||
@@ -49,7 +49,7 @@ mod tests {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night".to_string()),
|
||||
..base_flux_1_dev_image_request()
|
||||
};
|
||||
|
||||
@@ -72,7 +72,7 @@ mod tests {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
quality: None,
|
||||
image_batch_count: Some(4),
|
||||
prompt: Some("a dog surfing a wave, cinematic"),
|
||||
prompt: Some("a dog surfing a wave, cinematic".to_string()),
|
||||
..base_flux_1_dev_image_request()
|
||||
};
|
||||
|
||||
@@ -98,7 +98,7 @@ mod tests {
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a bear walking through an abandoned desert city at night"),
|
||||
prompt: Some("a bear walking through an abandoned desert city at night".to_string()),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_flux_1_dev_image_request()
|
||||
};
|
||||
|
||||
+5
-5
@@ -9,12 +9,12 @@ use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_im
|
||||
use artcraft_client::endpoints::generate::image::text::generate_flux_1_schnell_text_to_image::generate_flux_1_schnell_text_to_image;
|
||||
|
||||
pub async fn execute_artcraft_flux_1_schnell(
|
||||
plan: &PlanArtcraftFlux1Schnell<'_>,
|
||||
plan: &PlanArtcraftFlux1Schnell,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let request = GenerateFlux1SchnellTextToImageRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
prompt: plan.prompt.clone(),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
num_images: Some(plan.num_images),
|
||||
};
|
||||
@@ -48,7 +48,7 @@ mod tests {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a man and woman walking through a cyberpunk city at night"),
|
||||
prompt: Some("a man and woman walking through a cyberpunk city at night".to_string()),
|
||||
..base_flux_1_schnell_image_request()
|
||||
};
|
||||
|
||||
@@ -71,7 +71,7 @@ mod tests {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
quality: None,
|
||||
image_batch_count: Some(4),
|
||||
prompt: Some("a dog surfing a wave, cinematic"),
|
||||
prompt: Some("a dog surfing a wave, cinematic".to_string()),
|
||||
..base_flux_1_schnell_image_request()
|
||||
};
|
||||
|
||||
@@ -97,7 +97,7 @@ mod tests {
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a man and woman walking through a futuristic city at evening"),
|
||||
prompt: Some("a man and woman walking through a futuristic city at evening".to_string()),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_flux_1_schnell_image_request()
|
||||
};
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::Flu
|
||||
use artcraft_client::endpoints::generate::image::angle::flux_2_lora_edit_image_angle::flux_2_lora_edit_image_angle;
|
||||
|
||||
pub async fn execute_artcraft_flux_2_lora_angles(
|
||||
plan: &PlanArtcraftFlux2LoraAngles<'_>,
|
||||
plan: &PlanArtcraftFlux2LoraAngles,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let request = Flux2LoraEditImageAngleRequest {
|
||||
@@ -53,7 +53,7 @@ mod tests {
|
||||
aspect_ratio: Some(CommonAspectRatio::SquareHd),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
horizontal_angle: Some(30.0),
|
||||
vertical_angle: Some(0.0),
|
||||
zoom: Some(1.0),
|
||||
|
||||
+5
-5
@@ -9,12 +9,12 @@ use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image
|
||||
use artcraft_client::endpoints::generate::image::text::generate_flux_pro_11_text_to_image::generate_flux_pro_11_text_to_image;
|
||||
|
||||
pub async fn execute_artcraft_flux_pro_1p1(
|
||||
plan: &PlanArtcraftFluxPro11<'_>,
|
||||
plan: &PlanArtcraftFluxPro11,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let request = GenerateFluxPro11TextToImageRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
prompt: plan.prompt.clone(),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
num_images: Some(plan.num_images),
|
||||
};
|
||||
@@ -49,7 +49,7 @@ mod tests {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night".to_string()),
|
||||
..base_flux_pro_1p1_image_request()
|
||||
};
|
||||
|
||||
@@ -72,7 +72,7 @@ mod tests {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
quality: None,
|
||||
image_batch_count: Some(4),
|
||||
prompt: Some("a dog surfing a wave, cinematic"),
|
||||
prompt: Some("a dog surfing a wave, cinematic".to_string()),
|
||||
..base_flux_pro_1p1_image_request()
|
||||
};
|
||||
|
||||
@@ -98,7 +98,7 @@ mod tests {
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a bear walking through an abandoned desert city at night"),
|
||||
prompt: Some("a bear walking through an abandoned desert city at night".to_string()),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_flux_pro_1p1_image_request()
|
||||
};
|
||||
|
||||
+5
-5
@@ -9,12 +9,12 @@ use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to
|
||||
use artcraft_client::endpoints::generate::image::text::generate_flux_pro_11_ultra_text_to_image::generate_flux_pro_11_ultra_text_to_image;
|
||||
|
||||
pub async fn execute_artcraft_flux_pro_1p1_ultra(
|
||||
plan: &PlanArtcraftFluxPro11Ultra<'_>,
|
||||
plan: &PlanArtcraftFluxPro11Ultra,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let request = GenerateFluxPro11UltraTextToImageRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
prompt: plan.prompt.clone(),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
num_images: Some(plan.num_images),
|
||||
};
|
||||
@@ -49,7 +49,7 @@ mod tests {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night".to_string()),
|
||||
..base_flux_pro_1p1_ultra_image_request()
|
||||
};
|
||||
|
||||
@@ -72,7 +72,7 @@ mod tests {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
quality: None,
|
||||
image_batch_count: Some(4),
|
||||
prompt: Some("a dog surfing a wave, cinematic"),
|
||||
prompt: Some("a dog surfing a wave, cinematic".to_string()),
|
||||
..base_flux_pro_1p1_ultra_image_request()
|
||||
};
|
||||
|
||||
@@ -98,7 +98,7 @@ mod tests {
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a t-rex walking through an abandoned desert city at night"),
|
||||
prompt: Some("a t-rex walking through an abandoned desert city at night".to_string()),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_flux_pro_1p1_ultra_image_request()
|
||||
};
|
||||
|
||||
+8
-8
@@ -14,14 +14,14 @@ use artcraft_client::endpoints::generate::image::text::generate_gpt_image_1_text
|
||||
/// text-to-image, one for image-edit), so we dispatch on whether image refs
|
||||
/// are present rather than calling a single multi-function endpoint.
|
||||
pub async fn execute_artcraft_gpt_image_1(
|
||||
plan: &PlanArtcraftGptImage1<'_>,
|
||||
plan: &PlanArtcraftGptImage1,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let inference_job_token = match plan.image_inputs {
|
||||
let inference_job_token = match plan.image_inputs.clone() {
|
||||
Some(image_inputs) => {
|
||||
let request = GptImage1EditImageRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
prompt: plan.prompt.clone(),
|
||||
image_media_tokens: Some(image_inputs.to_owned()),
|
||||
image_size: plan.image_size.map(|s| s.to_edit()),
|
||||
num_images: Some(plan.num_images.to_edit()),
|
||||
@@ -39,7 +39,7 @@ pub async fn execute_artcraft_gpt_image_1(
|
||||
None => {
|
||||
let request = GenerateGptImage1TextToImageRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
prompt: plan.prompt.clone(),
|
||||
image_size: plan.image_size.map(|s| s.to_t2i()),
|
||||
num_images: Some(plan.num_images.to_t2i()),
|
||||
image_quality: Some(plan.quality.to_t2i()),
|
||||
@@ -89,7 +89,7 @@ mod tests {
|
||||
fn build_edit_image_plan_smoke() {
|
||||
let tokens = vec![MediaFileToken::new_from_str("mf_test00000000000000000000000000")];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_gpt_image_1_image_request()
|
||||
};
|
||||
let plan = request.build().expect("plan should build");
|
||||
@@ -106,7 +106,7 @@ mod tests {
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a horse walking through a cyberpunk city at night"),
|
||||
prompt: Some("a horse walking through a cyberpunk city at night".to_string()),
|
||||
..base_gpt_image_1_image_request()
|
||||
};
|
||||
|
||||
@@ -129,8 +129,8 @@ mod tests {
|
||||
];
|
||||
|
||||
let request = GenerateImageRequest {
|
||||
prompt: Some("Change the background to a desert with cacti"),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
prompt: Some("Change the background to a desert with cacti".to_string()),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
image_batch_count: Some(1),
|
||||
..base_gpt_image_1_image_request()
|
||||
|
||||
+9
-9
@@ -9,13 +9,13 @@ use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_func
|
||||
use artcraft_client::endpoints::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen_image::gpt_image_1p5_multi_function_image_gen;
|
||||
|
||||
pub async fn execute_artcraft_gpt_image_1p5(
|
||||
plan: &PlanArtcraftGptImage1p5<'_>,
|
||||
plan: &PlanArtcraftGptImage1p5,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let request = GptImage1p5MultiFunctionImageGenRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
image_media_tokens: plan.image_inputs.map(|tokens| tokens.to_owned()),
|
||||
prompt: plan.prompt.clone(),
|
||||
image_media_tokens: plan.image_inputs.clone(),
|
||||
mask_image_token: None,
|
||||
num_images: Some(plan.num_images),
|
||||
image_size: plan.image_size,
|
||||
@@ -56,7 +56,7 @@ mod tests {
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a horse walking through a cyberpunk city at night"),
|
||||
prompt: Some("a horse walking through a cyberpunk city at night".to_string()),
|
||||
..base_gpt_image_1p5_image_request()
|
||||
};
|
||||
|
||||
@@ -81,8 +81,8 @@ mod tests {
|
||||
];
|
||||
|
||||
let request = GenerateImageRequest {
|
||||
prompt: Some("Change the background to a desert with cacti"),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
prompt: Some("Change the background to a desert with cacti".to_string()),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto),
|
||||
image_batch_count: Some(1),
|
||||
..base_gpt_image_1p5_image_request()
|
||||
@@ -109,7 +109,7 @@ mod tests {
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(ar),
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a bear walking through a abandoned desert city at night"),
|
||||
prompt: Some("a bear walking through a abandoned desert city at night".to_string()),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_gpt_image_1p5_image_request()
|
||||
};
|
||||
@@ -140,8 +140,8 @@ mod tests {
|
||||
for ar in CommonAspectRatio::iter() {
|
||||
println!("--- image-edit aspect ratio: {:?} ---", ar);
|
||||
let request = GenerateImageRequest {
|
||||
prompt: Some("Change the background to an alien world"),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
prompt: Some("Change the background to an alien world".to_string()),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
|
||||
aspect_ratio: Some(ar),
|
||||
image_batch_count: Some(1),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
ArtcraftImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_2::PlanArtcraftGptImage2;
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::omni_gen::generate::image::omni_gen_image::omni_gen_image_generate;
|
||||
use enums::common::generation::common_image_model::CommonImageModel;
|
||||
|
||||
/// Execute GPT Image 2 via the Artcraft omni-gen image endpoint.
|
||||
pub async fn execute_artcraft_gpt_image_2(
|
||||
plan: &PlanArtcraftGptImage2,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let request = OmniGenImageCostAndGenerateRequest {
|
||||
idempotency_token: Some(plan.idempotency_token.clone()),
|
||||
model: Some(CommonImageModel::GptImage2),
|
||||
prompt: plan.prompt.clone(),
|
||||
image_media_tokens: plan.image_inputs.clone(),
|
||||
resolution: None,
|
||||
aspect_ratio: plan.aspect_ratio_enum(),
|
||||
quality: Some(plan.quality_enum()),
|
||||
image_batch_count: Some(plan.num_images_u16()),
|
||||
adjust_horizontal_angle: None,
|
||||
adjust_vertical_angle: None,
|
||||
adjust_zoom: None,
|
||||
};
|
||||
|
||||
let response = omni_gen_image_generate(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
|
||||
inference_job_token: response.inference_job_token,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_quality::CommonQuality;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::{get_artcraft_client, get_artcraft_client_for_env};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn base_gpt_image_2_request() -> GenerateImageRequest {
|
||||
GenerateImageRequest {
|
||||
model: crate::api::common_image_model::CommonImageModel::GptImage2,
|
||||
provider: crate::api::provider::Provider::Artcraft,
|
||||
prompt: Some("a t-rex walking through a cyberpunk city at night".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_text_to_image_gpt_image_2() {
|
||||
let client = get_artcraft_client_for_env(ApiHost::Localhost { port: 12345 });
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
image_batch_count: Some(1),
|
||||
..base_gpt_image_2_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_image(&client).await;
|
||||
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_image request failed");
|
||||
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
|
||||
println!("Job token: {:?}", payload.inference_job_token);
|
||||
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_edit_image_gpt_image_2() {
|
||||
let client = get_artcraft_client();
|
||||
|
||||
let image_tokens = vec![
|
||||
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
|
||||
];
|
||||
|
||||
let request = GenerateImageRequest {
|
||||
prompt: Some("Change the background to a desert with cacti".to_string()),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens)),
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto),
|
||||
image_batch_count: Some(1),
|
||||
..base_gpt_image_2_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_image(&client).await;
|
||||
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_image request failed");
|
||||
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
|
||||
println!("Job token: {:?}", payload.inference_job_token);
|
||||
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_text_to_image_gpt_image_2_high_quality() {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
quality: Some(CommonQuality::High),
|
||||
image_batch_count: Some(1),
|
||||
..base_gpt_image_2_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_image(&client).await;
|
||||
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_image request failed");
|
||||
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
|
||||
println!("Job token: {:?}", payload.inference_job_token);
|
||||
|
||||
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
|
||||
}
|
||||
}
|
||||
+9
-9
@@ -9,13 +9,13 @@ use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_functi
|
||||
use artcraft_client::endpoints::generate::image::multi_function::nano_banana_multi_function_image_gen_image::nano_banana_multi_function_image_gen;
|
||||
|
||||
pub async fn execute_artcraft_nano_banana(
|
||||
plan: &PlanArtcraftNanaBanana<'_>,
|
||||
plan: &PlanArtcraftNanaBanana,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let request = NanoBananaMultiFunctionImageGenRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
image_media_tokens: plan.image_inputs.map(|tokens| tokens.to_owned()),
|
||||
prompt: plan.prompt.clone(),
|
||||
image_media_tokens: plan.image_inputs.clone(),
|
||||
num_images: Some(plan.num_images),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
};
|
||||
@@ -52,7 +52,7 @@ mod tests {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night".to_string()),
|
||||
..base_nano_banana_image_request()
|
||||
};
|
||||
|
||||
@@ -77,8 +77,8 @@ mod tests {
|
||||
];
|
||||
|
||||
let request = GenerateImageRequest {
|
||||
prompt: Some("Change the background to a sunny beach"),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
prompt: Some("Change the background to a sunny beach".to_string()),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
@@ -107,7 +107,7 @@ mod tests {
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a shiba walking through a cyberpunk city at night"),
|
||||
prompt: Some("a shiba walking through a cyberpunk city at night".to_string()),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_nano_banana_image_request()
|
||||
};
|
||||
@@ -138,8 +138,8 @@ mod tests {
|
||||
for ar in CommonAspectRatio::iter() {
|
||||
println!("--- image-edit aspect ratio: {:?} ---", ar);
|
||||
let request = GenerateImageRequest {
|
||||
prompt: Some("Change the background to a sunny beach"),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
prompt: Some("Change the background to a sunny beach".to_string()),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
|
||||
+4
-4
@@ -9,13 +9,13 @@ use artcraft_api_defs::generate::image::multi_function::nano_banana_2_multi_func
|
||||
use artcraft_client::endpoints::generate::image::multi_function::nano_banana_2_multi_function_image_gen_image::nano_banana_2_multi_function_image_gen;
|
||||
|
||||
pub async fn execute_artcraft_nano_banana_2(
|
||||
plan: &PlanArtcraftNanaBanana2<'_>,
|
||||
plan: &PlanArtcraftNanaBanana2,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let request = NanaBanana2MultiFunctionImageGenRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
image_media_tokens: plan.image_inputs.map(|tokens| tokens.to_owned()),
|
||||
prompt: plan.prompt.clone(),
|
||||
image_media_tokens: plan.image_inputs.clone(),
|
||||
num_images: Some(plan.num_images),
|
||||
resolution: plan.resolution,
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
@@ -51,7 +51,7 @@ mod tests {
|
||||
resolution: Some(CommonResolution::OneK),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night".to_string()),
|
||||
..base_nano_banana_2_image_request()
|
||||
};
|
||||
|
||||
|
||||
+7
-7
@@ -9,13 +9,13 @@ use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_fu
|
||||
use artcraft_client::endpoints::generate::image::multi_function::nano_banana_pro_multi_function_image_gen_image::nano_banana_pro_multi_function_image_gen;
|
||||
|
||||
pub async fn execute_artcraft_nano_banana_pro(
|
||||
plan: &PlanArtcraftNanaBananaPro<'_>,
|
||||
plan: &PlanArtcraftNanaBananaPro,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let request = NanoBananaProMultiFunctionImageGenRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
image_media_tokens: plan.image_inputs.map(|tokens| tokens.to_owned()),
|
||||
prompt: plan.prompt.clone(),
|
||||
image_media_tokens: plan.image_inputs.clone(),
|
||||
num_images: Some(plan.num_images),
|
||||
resolution: plan.resolution,
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
@@ -53,7 +53,7 @@ mod tests {
|
||||
resolution: Some(CommonResolution::OneK),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night".to_string()),
|
||||
..base_image_request()
|
||||
};
|
||||
|
||||
@@ -77,7 +77,7 @@ mod tests {
|
||||
resolution: Some(CommonResolution::OneK),
|
||||
quality: None,
|
||||
image_batch_count: Some(4),
|
||||
prompt: Some("a dog surfing a wave, cinematic"),
|
||||
prompt: Some("a dog surfing a wave, cinematic".to_string()),
|
||||
..base_image_request()
|
||||
};
|
||||
|
||||
@@ -104,8 +104,8 @@ mod tests {
|
||||
];
|
||||
|
||||
let request = GenerateImageRequest {
|
||||
prompt: Some("Add the man in the first image into the fantasy scene in the second image. Keep his hoodie and attire, but match the vibe of the fantasy scene."),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
prompt: Some("Add the man in the first image into the fantasy scene in the second image. Keep his hoodie and attire, but match the vibe of the fantasy scene.".to_string()),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto), // edit mode: preserve source dimensions
|
||||
resolution: Some(CommonResolution::OneK),
|
||||
quality: None,
|
||||
|
||||
+3
-3
@@ -9,7 +9,7 @@ use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::
|
||||
use artcraft_client::endpoints::generate::image::angle::qwen_edit_2511_edit_image_angle::qwen_edit_2511_edit_image_angle;
|
||||
|
||||
pub async fn execute_artcraft_qwen_edit_2511_angles(
|
||||
plan: &PlanArtcraftQwenEdit2511Angles<'_>,
|
||||
plan: &PlanArtcraftQwenEdit2511Angles,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let request = QwenEdit2511EditImageAngleRequest {
|
||||
@@ -18,7 +18,7 @@ pub async fn execute_artcraft_qwen_edit_2511_angles(
|
||||
horizontal_angle: plan.horizontal_angle,
|
||||
vertical_angle: plan.vertical_angle,
|
||||
zoom: plan.zoom,
|
||||
additional_prompt: plan.prompt.map(|p| p.to_string()),
|
||||
additional_prompt: plan.prompt.clone(),
|
||||
num_images: Some(plan.num_images),
|
||||
image_size: plan.image_size,
|
||||
};
|
||||
@@ -55,7 +55,7 @@ mod tests {
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: None,
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
horizontal_angle: Some(30.0),
|
||||
vertical_angle: Some(0.0),
|
||||
zoom: Some(1.0),
|
||||
|
||||
+9
-9
@@ -9,13 +9,13 @@ use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_mu
|
||||
use artcraft_client::endpoints::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen_image::bytedance_seedream_v4_multi_function_image_gen;
|
||||
|
||||
pub async fn execute_artcraft_seedream_4(
|
||||
plan: &PlanArtcraftSeedream4<'_>,
|
||||
plan: &PlanArtcraftSeedream4,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let request = BytedanceSeedreamV4MultiFunctionImageGenRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
image_media_tokens: plan.image_inputs.map(|tokens| tokens.to_owned()),
|
||||
prompt: plan.prompt.clone(),
|
||||
image_media_tokens: plan.image_inputs.clone(),
|
||||
num_images: Some(plan.num_images),
|
||||
image_size: plan.image_size,
|
||||
max_images: None,
|
||||
@@ -53,7 +53,7 @@ mod tests {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night"),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night".to_string()),
|
||||
..base_seedream_4_image_request()
|
||||
};
|
||||
|
||||
@@ -78,8 +78,8 @@ mod tests {
|
||||
];
|
||||
|
||||
let request = GenerateImageRequest {
|
||||
prompt: Some("Change the background to a sunny beach"),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
prompt: Some("Change the background to a sunny beach".to_string()),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
@@ -108,7 +108,7 @@ mod tests {
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a shiba walking through a cyberpunk city at night"),
|
||||
prompt: Some("a shiba walking through a cyberpunk city at night".to_string()),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_seedream_4_image_request()
|
||||
};
|
||||
@@ -139,8 +139,8 @@ mod tests {
|
||||
for ar in CommonAspectRatio::iter() {
|
||||
println!("--- image-edit aspect ratio: {:?} ---", ar);
|
||||
let request = GenerateImageRequest {
|
||||
prompt: Some("Change the background to a sunny beach"),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
prompt: Some("Change the background to a sunny beach".to_string()),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
|
||||
+9
-9
@@ -9,13 +9,13 @@ use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_
|
||||
use artcraft_client::endpoints::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen_image::bytedance_seedream_v4p5_multi_function_image_gen;
|
||||
|
||||
pub async fn execute_artcraft_seedream_4p5(
|
||||
plan: &PlanArtcraftSeedream4p5<'_>,
|
||||
plan: &PlanArtcraftSeedream4p5,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let request = BytedanceSeedreamV4p5MultiFunctionImageGenRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
image_media_tokens: plan.image_inputs.map(|tokens| tokens.to_owned()),
|
||||
prompt: plan.prompt.clone(),
|
||||
image_media_tokens: plan.image_inputs.clone(),
|
||||
num_images: Some(plan.num_images),
|
||||
image_size: plan.image_size,
|
||||
max_images: None,
|
||||
@@ -53,7 +53,7 @@ mod tests {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a corgi walking through a cyberpunk city at night"),
|
||||
prompt: Some("a corgi walking through a cyberpunk city at night".to_string()),
|
||||
..base_seedream_4p5_image_request()
|
||||
};
|
||||
|
||||
@@ -78,8 +78,8 @@ mod tests {
|
||||
];
|
||||
|
||||
let request = GenerateImageRequest {
|
||||
prompt: Some("Change the background to a spooky haunted house"),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
prompt: Some("Change the background to a spooky haunted house".to_string()),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto2k),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
@@ -108,7 +108,7 @@ mod tests {
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a corgi walking through a cyberpunk city at night"),
|
||||
prompt: Some("a corgi walking through a cyberpunk city at night".to_string()),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_seedream_4p5_image_request()
|
||||
};
|
||||
@@ -139,8 +139,8 @@ mod tests {
|
||||
for ar in CommonAspectRatio::iter() {
|
||||
println!("--- image-edit aspect ratio: {:?} ---", ar);
|
||||
let request = GenerateImageRequest {
|
||||
prompt: Some("Change the background to a spooky haunted house"),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
|
||||
prompt: Some("Change the background to a spooky haunted house".to_string()),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
|
||||
aspect_ratio: Some(ar),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
|
||||
+4
-4
@@ -9,13 +9,13 @@ use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lit
|
||||
use artcraft_client::endpoints::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen_image::bytedance_seedream_5_lite_multi_function_image_gen;
|
||||
|
||||
pub async fn execute_artcraft_seedream_5_lite(
|
||||
plan: &PlanArtcraftSeedream5Lite<'_>,
|
||||
plan: &PlanArtcraftSeedream5Lite,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let request = BytedanceSeedream5LiteMultiFunctionImageGenRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
image_media_tokens: plan.image_inputs.map(|tokens| tokens.to_owned()),
|
||||
prompt: plan.prompt.clone(),
|
||||
image_media_tokens: plan.image_inputs.clone(),
|
||||
num_images: Some(plan.num_images),
|
||||
image_size: plan.image_size,
|
||||
max_images: None,
|
||||
@@ -49,7 +49,7 @@ mod tests {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a corgi walking through a cyberpunk city at night"),
|
||||
prompt: Some("a corgi walking through a cyberpunk city at night".to_string()),
|
||||
..base_seedream_5_lite_image_request()
|
||||
};
|
||||
|
||||
|
||||
+1
@@ -4,6 +4,7 @@ pub mod generate_image_artcraft_flux_pro_1p1;
|
||||
pub mod generate_image_artcraft_flux_pro_1p1_ultra;
|
||||
pub mod generate_image_artcraft_gpt_image_1;
|
||||
pub mod generate_image_artcraft_gpt_image_1p5;
|
||||
pub mod generate_image_artcraft_gpt_image_2;
|
||||
pub mod generate_image_artcraft_nano_banana;
|
||||
pub mod generate_image_artcraft_nano_banana_2;
|
||||
pub mod generate_image_artcraft_nano_banana_pro;
|
||||
|
||||
+3
-3
@@ -13,13 +13,13 @@ use fal_client::requests::webhook::image::text::enqueue_flux_1_dev_text_to_image
|
||||
};
|
||||
|
||||
pub async fn execute_fal_flux_1_dev(
|
||||
plan: &PlanFalFlux1Dev<'_>,
|
||||
plan: &PlanFalFlux1Dev,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if let Some(image_url) = &plan.maybe_image_url {
|
||||
// Image-to-image mode
|
||||
let args = Flux1DevEditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
image_url: image_url.clone(),
|
||||
num_images: plan.num_images.to_edit(),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
@@ -31,7 +31,7 @@ pub async fn execute_fal_flux_1_dev(
|
||||
} else {
|
||||
// Text-to-image mode
|
||||
let args = Flux1DevArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
num_images: plan.num_images.to_t2i(),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ use fal_client::requests::webhook::image::text::enqueue_flux_1_schnell_text_to_i
|
||||
};
|
||||
|
||||
pub async fn execute_fal_flux_1_schnell(
|
||||
plan: &PlanFalFlux1Schnell<'_>,
|
||||
plan: &PlanFalFlux1Schnell,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if let Some(image_url) = &plan.maybe_image_url {
|
||||
@@ -31,7 +31,7 @@ pub async fn execute_fal_flux_1_schnell(
|
||||
} else {
|
||||
// Text-to-image mode
|
||||
let args = Flux1SchnellArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
aspect_ratio: plan.t2i_aspect_ratio,
|
||||
num_images: plan.num_images.to_t2i(),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
|
||||
+2
-2
@@ -10,11 +10,11 @@ use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_text_to_imag
|
||||
};
|
||||
|
||||
pub async fn execute_fal_flux_pro_1p1(
|
||||
plan: &PlanFalFluxPro11<'_>,
|
||||
plan: &PlanFalFluxPro11,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let args = FluxPro11Args {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
num_images: plan.num_images.to_fal(),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
|
||||
+2
-2
@@ -10,11 +10,11 @@ use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_ultra_text_t
|
||||
};
|
||||
|
||||
pub async fn execute_fal_flux_pro_1p1_ultra(
|
||||
plan: &PlanFalFluxPro11Ultra<'_>,
|
||||
plan: &PlanFalFluxPro11Ultra,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let args = FluxPro11UltraArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
num_images: plan.num_images.to_fal(),
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
|
||||
+9
-9
@@ -13,12 +13,12 @@ use fal_client::requests::webhook::image::text::enqueue_gpt_image_1_text_to_imag
|
||||
};
|
||||
|
||||
pub async fn execute_fal_gpt_image_1(
|
||||
plan: &PlanFalGptImage1<'_>,
|
||||
plan: &PlanFalGptImage1,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
let args = EnqueueGptImage1TextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
num_images: plan.num_images.to_t2i(),
|
||||
image_size: plan.image_size.map(|s| s.to_t2i()),
|
||||
quality: Some(plan.quality.to_t2i()),
|
||||
@@ -32,7 +32,7 @@ pub async fn execute_fal_gpt_image_1(
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
let args = EnqueueGptImage1EditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: plan.num_images.to_edit(),
|
||||
mask_image_url: None,
|
||||
@@ -67,11 +67,11 @@ mod tests {
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::get_fal_client;
|
||||
|
||||
fn base_fal_request() -> GenerateImageRequest<'static> {
|
||||
fn base_fal_request() -> GenerateImageRequest {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::GptImage1,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a cat in space"),
|
||||
prompt: Some("a cat in space".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
@@ -106,7 +106,7 @@ mod tests {
|
||||
fn build_edit_image_plan_smoke() {
|
||||
let urls = vec!["https://example.com/img.jpg".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
image_inputs: Some(ImageListRef::Urls(urls.clone())),
|
||||
..base_fal_request()
|
||||
};
|
||||
let plan = request.build().expect("plan should build");
|
||||
@@ -123,7 +123,7 @@ mod tests {
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a horse walking through a cyberpunk city at night"),
|
||||
prompt: Some("a horse walking through a cyberpunk city at night".to_string()),
|
||||
..base_fal_request()
|
||||
};
|
||||
|
||||
@@ -143,10 +143,10 @@ mod tests {
|
||||
TREX_SKELETON_IMAGE_URL.to_string(),
|
||||
];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
image_inputs: Some(ImageListRef::Urls(urls.clone())),
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("change the background to a desert"),
|
||||
prompt: Some("change the background to a desert".to_string()),
|
||||
..base_fal_request()
|
||||
};
|
||||
|
||||
|
||||
+3
-3
@@ -13,12 +13,12 @@ use fal_client::requests::webhook::image::text::enqueue_gpt_image_1p5_text_to_im
|
||||
};
|
||||
|
||||
pub async fn execute_fal_gpt_image_1p5(
|
||||
plan: &PlanFalGptImage1p5<'_>,
|
||||
plan: &PlanFalGptImage1p5,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
let args = EnqueueGptImage1p5TextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
num_images: plan.num_images.to_t2i(),
|
||||
image_size: plan.image_size.map(|s| s.to_t2i()),
|
||||
background: None,
|
||||
@@ -32,7 +32,7 @@ pub async fn execute_fal_gpt_image_1p5(
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
let args = EnqueueGptImage1p5EditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: plan.num_images.to_edit(),
|
||||
mask_image_url: None,
|
||||
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
FalImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_2::PlanFalGptImage2;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_2_edit_image_webhook::{
|
||||
enqueue_gpt_image_2_edit_image_webhook, EnqueueGptImage2EditImageArgs,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_gpt_image_2_text_to_image_webhook::{
|
||||
enqueue_gpt_image_2_text_to_image_webhook, EnqueueGptImage2TextToImageArgs,
|
||||
};
|
||||
|
||||
pub async fn execute_fal_gpt_image_2(
|
||||
plan: &PlanFalGptImage2,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
let args = EnqueueGptImage2TextToImageArgs {
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
num_images: plan.num_images.to_t2i(),
|
||||
image_size: plan.image_size.map(|s| s.to_t2i()),
|
||||
quality: Some(plan.quality.to_t2i()),
|
||||
output_format: None,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_gpt_image_2_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
let args = EnqueueGptImage2EditImageArgs {
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: plan.num_images.to_edit(),
|
||||
mask_url: None,
|
||||
image_size: plan.image_size.map(|s| s.to_edit()),
|
||||
quality: Some(plan.quality.to_edit()),
|
||||
output_format: None,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
api_key: &fal_client.api_key,
|
||||
};
|
||||
enqueue_gpt_image_2_edit_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
};
|
||||
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use test_data::web::image_urls::{GHOST_IMAGE_URL, JUNO_AT_LAKE_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::get_fal_client;
|
||||
|
||||
// ── Build smoke tests ──
|
||||
|
||||
mod build_smoke_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn text_to_image_plan() {
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
image_batch_count: Some(1),
|
||||
..base_request()
|
||||
};
|
||||
let plan = request.build().expect("plan should build");
|
||||
let ImageGenerationPlan::FalGptImage2(plan) = plan else {
|
||||
panic!("expected FalGptImage2")
|
||||
};
|
||||
assert!(plan.image_urls.is_empty(), "text mode must have no image_urls");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_image_plan() {
|
||||
let urls = vec![
|
||||
JUNO_AT_LAKE_IMAGE_URL.to_string(),
|
||||
GHOST_IMAGE_URL.to_string(),
|
||||
];
|
||||
let request = GenerateImageRequest {
|
||||
prompt: Some("dog running from a scary ghost".to_string()),
|
||||
image_inputs: Some(ImageListRef::Urls(urls.clone())),
|
||||
..base_request()
|
||||
};
|
||||
let plan = request.build().expect("plan should build");
|
||||
let ImageGenerationPlan::FalGptImage2(plan) = plan else {
|
||||
panic!("expected FalGptImage2")
|
||||
};
|
||||
assert_eq!(plan.image_urls, urls);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Live API tests ──
|
||||
|
||||
mod live_api_tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real fal API request and incurs cost
|
||||
async fn text_to_image() {
|
||||
let client = get_fal_client();
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a horse walking through a cyberpunk city at night".to_string()),
|
||||
..base_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_image(&client).await;
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_image request failed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
println!("Request id: {:?}", payload.request_id);
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real fal API request and incurs cost
|
||||
async fn edit_image() {
|
||||
let client = get_fal_client();
|
||||
let request = GenerateImageRequest {
|
||||
prompt: Some("dog running from a scary ghost".to_string()),
|
||||
image_inputs: Some(ImageListRef::Urls(vec![
|
||||
JUNO_AT_LAKE_IMAGE_URL.to_string(),
|
||||
GHOST_IMAGE_URL.to_string(),
|
||||
])),
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
image_batch_count: Some(1),
|
||||
..base_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_image(&client).await;
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_image request failed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
println!("Request id: {:?}", payload.request_id);
|
||||
assert_eq!(1, 2, "Inspect output above");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_request() -> GenerateImageRequest {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::GptImage2,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a cat in space".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -17,13 +17,13 @@ use fal_client::requests::webhook::image::text::enqueue_gemini_25_flash_text_to_
|
||||
};
|
||||
|
||||
pub async fn execute_fal_nano_banana(
|
||||
plan: &PlanFalNanoBanana<'_>,
|
||||
plan: &PlanFalNanoBanana,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
// Text-to-image mode
|
||||
let args = Gemini25FlashTextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
num_images: to_t2i_num_images(plan.num_images),
|
||||
aspect_ratio: plan.t2i_aspect_ratio,
|
||||
webhook_url: fal_client.webhook_url.as_str(),
|
||||
@@ -35,7 +35,7 @@ pub async fn execute_fal_nano_banana(
|
||||
} else {
|
||||
// Image-edit mode
|
||||
let args = Gemini25FlashEditArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: to_edit_num_images(plan.num_images),
|
||||
aspect_ratio: plan.edit_aspect_ratio,
|
||||
|
||||
+3
-3
@@ -17,13 +17,13 @@ use fal_client::requests::webhook::image::text::enqueue_nano_banana_2_text_to_im
|
||||
};
|
||||
|
||||
pub async fn execute_fal_nano_banana_2(
|
||||
plan: &PlanFalNanaBanana2<'_>,
|
||||
plan: &PlanFalNanaBanana2,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
// Text-to-image mode
|
||||
let args = EnqueueNanoBanana2TextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
num_images: to_t2i_num_images(plan.num_images),
|
||||
resolution: plan.resolution.map(to_t2i_resolution),
|
||||
aspect_ratio: plan.t2i_aspect_ratio,
|
||||
@@ -36,7 +36,7 @@ pub async fn execute_fal_nano_banana_2(
|
||||
} else {
|
||||
// Image-edit mode
|
||||
let args = EnqueueNanoBanana2EditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: to_edit_num_images(plan.num_images),
|
||||
resolution: plan.resolution.map(to_edit_resolution),
|
||||
|
||||
+7
-7
@@ -16,13 +16,13 @@ use fal_client::requests::webhook::image::text::enqueue_nano_banana_pro_text_to_
|
||||
EnqueueNanoBananaProTextToImageNumImages, EnqueueNanoBananaProTextToImageResolution,
|
||||
};
|
||||
pub async fn execute_fal_nano_banana_pro(
|
||||
plan: &PlanFalNanaBananaPro<'_>,
|
||||
plan: &PlanFalNanaBananaPro,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
// Text-to-image mode
|
||||
let args = EnqueueNanoBananaProTextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
num_images: to_t2i_num_images(plan.num_images),
|
||||
resolution: plan.resolution.map(to_t2i_resolution),
|
||||
aspect_ratio: plan.t2i_aspect_ratio,
|
||||
@@ -35,7 +35,7 @@ pub async fn execute_fal_nano_banana_pro(
|
||||
} else {
|
||||
// Image-edit mode
|
||||
let args = EnqueueNanoBananaProEditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: to_edit_num_images(plan.num_images),
|
||||
resolution: plan.resolution.map(to_edit_resolution),
|
||||
@@ -107,7 +107,7 @@ mod tests {
|
||||
resolution: Some(CommonResolution::TwoK),
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
prompt: Some("a cyberpunk city skyline at dusk, neon lights reflecting on rain-soaked streets"),
|
||||
prompt: Some("a cyberpunk city skyline at dusk, neon lights reflecting on rain-soaked streets".to_string()),
|
||||
..base_fal_image_request()
|
||||
};
|
||||
|
||||
@@ -132,7 +132,7 @@ mod tests {
|
||||
resolution: Some(CommonResolution::OneK),
|
||||
quality: None,
|
||||
image_batch_count: Some(4),
|
||||
prompt: Some("a golden retriever surfing a wave, cinematic, 4K"),
|
||||
prompt: Some("a golden retriever surfing a wave, cinematic, 4K".to_string()),
|
||||
..base_fal_image_request()
|
||||
};
|
||||
|
||||
@@ -159,8 +159,8 @@ mod tests {
|
||||
];
|
||||
|
||||
let request = GenerateImageRequest {
|
||||
prompt: Some("Add the ghost from the first image hovering above the T-Rex skeleton in the second image, make it look spooky but friendly"),
|
||||
image_inputs: Some(ImageListRef::Urls(&image_urls)),
|
||||
prompt: Some("Add the ghost from the first image hovering above the T-Rex skeleton in the second image, make it look spooky but friendly".to_string()),
|
||||
image_inputs: Some(ImageListRef::Urls(image_urls.clone())),
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto), // edit mode: preserve source dimensions
|
||||
resolution: Some(CommonResolution::TwoK),
|
||||
quality: None,
|
||||
|
||||
+3
-3
@@ -13,12 +13,12 @@ use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4_te
|
||||
};
|
||||
|
||||
pub async fn execute_fal_seedream_4(
|
||||
plan: &PlanFalSeedream4<'_>,
|
||||
plan: &PlanFalSeedream4,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
let args = EnqueueBytedanceSeedreamV4TextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
num_images: Some(plan.num_images.to_t2i()),
|
||||
max_images: None,
|
||||
image_size: plan.image_size.map(|s| s.to_t2i()),
|
||||
@@ -30,7 +30,7 @@ pub async fn execute_fal_seedream_4(
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
let args = EnqueueBytedanceSeedreamV4EditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: Some(plan.num_images.to_edit()),
|
||||
max_images: None,
|
||||
|
||||
+3
-3
@@ -13,12 +13,12 @@ use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4p5_
|
||||
};
|
||||
|
||||
pub async fn execute_fal_seedream_4p5(
|
||||
plan: &PlanFalSeedream4p5<'_>,
|
||||
plan: &PlanFalSeedream4p5,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
let args = EnqueueBytedanceSeedreamV4p5TextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
num_images: Some(plan.num_images.to_t2i()),
|
||||
max_images: None,
|
||||
image_size: plan.image_size.map(|s| s.to_t2i()),
|
||||
@@ -30,7 +30,7 @@ pub async fn execute_fal_seedream_4p5(
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
let args = EnqueueBytedanceSeedreamV4p5EditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: Some(plan.num_images.to_edit()),
|
||||
max_images: None,
|
||||
|
||||
+3
-3
@@ -13,12 +13,12 @@ use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v5_li
|
||||
};
|
||||
|
||||
pub async fn execute_fal_seedream_5_lite(
|
||||
plan: &PlanFalSeedream5Lite<'_>,
|
||||
plan: &PlanFalSeedream5Lite,
|
||||
fal_client: &RouterFalClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let webhook_response = if plan.image_urls.is_empty() {
|
||||
let args = EnqueueBytedanceSeedreamV5LiteTextToImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
num_images: Some(plan.num_images.to_t2i()),
|
||||
max_images: None,
|
||||
image_size: plan.image_size.map(|s| s.to_t2i()),
|
||||
@@ -30,7 +30,7 @@ pub async fn execute_fal_seedream_5_lite(
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
|
||||
} else {
|
||||
let args = EnqueueBytedanceSeedreamV5LiteEditImageArgs {
|
||||
prompt: plan.prompt.unwrap_or(""),
|
||||
prompt: plan.prompt.as_deref().unwrap_or(""),
|
||||
image_urls: plan.image_urls.clone(),
|
||||
num_images: Some(plan.num_images.to_edit()),
|
||||
max_images: None,
|
||||
|
||||
+1
@@ -4,6 +4,7 @@ pub mod generate_image_fal_flux_pro_1p1;
|
||||
pub mod generate_image_fal_flux_pro_1p1_ultra;
|
||||
pub mod generate_image_fal_gpt_image_1;
|
||||
pub mod generate_image_fal_gpt_image_1p5;
|
||||
pub mod generate_image_fal_gpt_image_2;
|
||||
pub mod generate_image_fal_seedream_4;
|
||||
pub mod generate_image_fal_seedream_4p5;
|
||||
pub mod generate_image_fal_seedream_5_lite;
|
||||
|
||||
+20
-15
@@ -16,6 +16,7 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1_ultra::plan_generate_image_artcraft_flux_pro_1p1_ultra;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1::plan_generate_image_artcraft_gpt_image_1;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1p5::plan_generate_image_artcraft_gpt_image_1p5;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_2::plan_generate_image_artcraft_gpt_image_2;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana::plan_generate_image_artcraft_nano_banana;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_2::plan_generate_image_artcraft_nano_banana_2;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_pro::plan_generate_image_artcraft_nano_banana_pro;
|
||||
@@ -29,6 +30,7 @@ use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1_ultra::plan_generate_image_fal_flux_pro_1p1_ultra;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1::plan_generate_image_fal_gpt_image_1;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1p5::plan_generate_image_fal_gpt_image_1p5;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_2::plan_generate_image_fal_gpt_image_2;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4::plan_generate_image_fal_seedream_4;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4p5::plan_generate_image_fal_seedream_4p5;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_5_lite::plan_generate_image_fal_seedream_5_lite;
|
||||
@@ -37,7 +39,7 @@ use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_ban
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::plan_generate_image_fal_nano_banana_pro;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateImageRequest<'a> {
|
||||
pub struct GenerateImageRequest {
|
||||
/// Which model to use.
|
||||
pub model: CommonImageModel,
|
||||
|
||||
@@ -45,12 +47,12 @@ pub struct GenerateImageRequest<'a> {
|
||||
pub provider: Provider,
|
||||
|
||||
/// The prompt for the image generation.
|
||||
pub prompt: Option<&'a str>,
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Input images for image editing.
|
||||
/// If present, we're doing image editing (image-to-image).
|
||||
/// If absent, we're doing text-to-image.
|
||||
pub image_inputs: Option<ImageListRef<'a>>,
|
||||
pub image_inputs: Option<ImageListRef>,
|
||||
|
||||
/// The resolution to use.
|
||||
pub resolution: Option<CommonResolution>,
|
||||
@@ -83,12 +85,12 @@ pub struct GenerateImageRequest<'a> {
|
||||
|
||||
/// Some providers support idempotency.
|
||||
/// If not supplied, we'll generate one for the required providers.
|
||||
pub idempotency_token: Option<&'a str>,
|
||||
pub idempotency_token: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> GenerateImageRequest<'a> {
|
||||
impl GenerateImageRequest {
|
||||
/// Read the image generation request, construct a plan, then yield a means to execute it.
|
||||
pub fn build(&self) -> Result<ImageGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
pub fn build(&self) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
match self.provider {
|
||||
Provider::Artcraft => self.build_artcraft(),
|
||||
Provider::Fal => self.build_fal(),
|
||||
@@ -96,26 +98,28 @@ impl<'a> GenerateImageRequest<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_artcraft(&self) -> Result<ImageGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
fn build_artcraft(&self) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
match self.model {
|
||||
CommonImageModel::Flux1Dev => plan_generate_image_artcraft_flux_1_dev(self),
|
||||
CommonImageModel::Flux1Schnell => plan_generate_image_artcraft_flux_1_schnell(self),
|
||||
CommonImageModel::Flux2LoraAngles => plan_generate_image_artcraft_flux_2_lora_angles(self),
|
||||
CommonImageModel::FluxPro11 => plan_generate_image_artcraft_flux_pro_1p1(self),
|
||||
CommonImageModel::FluxPro11Ultra => plan_generate_image_artcraft_flux_pro_1p1_ultra(self),
|
||||
CommonImageModel::GptImage1 => plan_generate_image_artcraft_gpt_image_1(self),
|
||||
CommonImageModel::GptImage1p5 => plan_generate_image_artcraft_gpt_image_1p5(self),
|
||||
CommonImageModel::GptImage2 => plan_generate_image_artcraft_gpt_image_2(self),
|
||||
CommonImageModel::NanoBanana => plan_generate_image_artcraft_nano_banana(self),
|
||||
CommonImageModel::NanoBanana2 => plan_generate_image_artcraft_nano_banana_2(self),
|
||||
CommonImageModel::NanoBananaPro => plan_generate_image_artcraft_nano_banana_pro(self),
|
||||
CommonImageModel::QwenEdit2511Angles => plan_generate_image_artcraft_qwen_edit_2511_angles(self),
|
||||
CommonImageModel::Seedream4 => plan_generate_image_artcraft_seedream_4(self),
|
||||
CommonImageModel::Seedream4p5 => plan_generate_image_artcraft_seedream_4p5(self),
|
||||
CommonImageModel::Seedream5Lite => plan_generate_image_artcraft_seedream_5_lite(self),
|
||||
CommonImageModel::QwenEdit2511Angles => plan_generate_image_artcraft_qwen_edit_2511_angles(self),
|
||||
CommonImageModel::Flux2LoraAngles => plan_generate_image_artcraft_flux_2_lora_angles(self),
|
||||
_ => Err(ArtcraftRouterError::UnsupportedModel(format!("{:?}", self.model))),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_fal(&self) -> Result<ImageGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
fn build_fal(&self) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
match self.model {
|
||||
CommonImageModel::Flux1Dev => plan_generate_image_fal_flux_1_dev(self),
|
||||
CommonImageModel::Flux1Schnell => plan_generate_image_fal_flux_1_schnell(self),
|
||||
@@ -123,12 +127,13 @@ impl<'a> GenerateImageRequest<'a> {
|
||||
CommonImageModel::FluxPro11Ultra => plan_generate_image_fal_flux_pro_1p1_ultra(self),
|
||||
CommonImageModel::GptImage1 => plan_generate_image_fal_gpt_image_1(self),
|
||||
CommonImageModel::GptImage1p5 => plan_generate_image_fal_gpt_image_1p5(self),
|
||||
CommonImageModel::Seedream4 => plan_generate_image_fal_seedream_4(self),
|
||||
CommonImageModel::Seedream4p5 => plan_generate_image_fal_seedream_4p5(self),
|
||||
CommonImageModel::Seedream5Lite => plan_generate_image_fal_seedream_5_lite(self),
|
||||
CommonImageModel::GptImage2 => plan_generate_image_fal_gpt_image_2(self),
|
||||
CommonImageModel::NanoBanana => plan_generate_image_fal_nano_banana(self),
|
||||
CommonImageModel::NanoBanana2 => plan_generate_image_fal_nano_banana_2(self),
|
||||
CommonImageModel::NanoBananaPro => plan_generate_image_fal_nano_banana_pro(self),
|
||||
CommonImageModel::Seedream4 => plan_generate_image_fal_seedream_4(self),
|
||||
CommonImageModel::Seedream4p5 => plan_generate_image_fal_seedream_4p5(self),
|
||||
CommonImageModel::Seedream5Lite => plan_generate_image_fal_seedream_5_lite(self),
|
||||
_ => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "provider",
|
||||
@@ -138,14 +143,14 @@ impl<'a> GenerateImageRequest<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn unsupported_provider(&self) -> Result<ImageGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
fn unsupported_provider(&self) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
Err(ArtcraftRouterError::UnsupportedModel(
|
||||
format!("Image generation for model `{:?}` is not supported for provider {:?}", self.model, self.provider)
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_or_generate_idempotency_token(&self) -> String {
|
||||
self.idempotency_token.map(|t| t.to_string())
|
||||
self.idempotency_token.clone()
|
||||
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
+50
-28
@@ -6,6 +6,7 @@ use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraf
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_pro_1p1_ultra::estimate_image_cost_artcraft_flux_pro_1p1_ultra;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_gpt_image_1::estimate_image_cost_artcraft_gpt_image_1;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_gpt_image_1p5::estimate_image_cost_artcraft_gpt_image_1p5;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_gpt_image_2::estimate_image_cost_artcraft_gpt_image_2;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_nano_banana::estimate_image_cost_artcraft_nano_banana;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_nano_banana_2::estimate_image_cost_artcraft_nano_banana_2;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_nano_banana_pro::estimate_image_cost_artcraft_nano_banana_pro;
|
||||
@@ -20,6 +21,7 @@ use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_flux_pro
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_flux_pro_1p1_ultra::estimate_image_cost_fal_flux_pro_1p1_ultra;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_gpt_image_1::estimate_image_cost_fal_gpt_image_1;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_gpt_image_1p5::estimate_image_cost_fal_gpt_image_1p5;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_gpt_image_2::estimate_image_cost_fal_gpt_image_2;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_seedream_4::estimate_image_cost_fal_seedream_4;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_seedream_4p5::estimate_image_cost_fal_seedream_4p5;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_seedream_5_lite::estimate_image_cost_fal_seedream_5_lite;
|
||||
@@ -32,6 +34,7 @@ use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_pro_1p1_ultra::execute_artcraft_flux_pro_1p1_ultra;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_gpt_image_1::execute_artcraft_gpt_image_1;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_gpt_image_1p5::execute_artcraft_gpt_image_1p5;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_gpt_image_2::execute_artcraft_gpt_image_2;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_nano_banana::execute_artcraft_nano_banana;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_nano_banana_2::execute_artcraft_nano_banana_2;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_nano_banana_pro::execute_artcraft_nano_banana_pro;
|
||||
@@ -46,6 +49,7 @@ use crate::generate::generate_image::execute::fal::generate_image_fal_flux_pro_1
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_flux_pro_1p1_ultra::execute_fal_flux_pro_1p1_ultra;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_gpt_image_1::execute_fal_gpt_image_1;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_gpt_image_1p5::execute_fal_gpt_image_1p5;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_gpt_image_2::execute_fal_gpt_image_2;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_seedream_4::execute_fal_seedream_4;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_seedream_4p5::execute_fal_seedream_4p5;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_seedream_5_lite::execute_fal_seedream_5_lite;
|
||||
@@ -60,6 +64,7 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1_ultra::PlanArtcraftFluxPro11Ultra;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1::PlanArtcraftGptImage1;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1p5::PlanArtcraftGptImage1p5;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_2::PlanArtcraftGptImage2;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana::PlanArtcraftNanaBanana;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_2::PlanArtcraftNanaBanana2;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_pro::PlanArtcraftNanaBananaPro;
|
||||
@@ -74,6 +79,7 @@ use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1_ultra::PlanFalFluxPro11Ultra;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1::PlanFalGptImage1;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1p5::PlanFalGptImage1p5;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_2::PlanFalGptImage2;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4::PlanFalSeedream4;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4p5::PlanFalSeedream4p5;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_5_lite::PlanFalSeedream5Lite;
|
||||
@@ -82,36 +88,38 @@ use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_ban
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::PlanFalNanaBananaPro;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ImageGenerationPlan<'a> {
|
||||
ArtcraftFlux1Dev(PlanArtcraftFlux1Dev<'a>),
|
||||
ArtcraftFlux1Schnell(PlanArtcraftFlux1Schnell<'a>),
|
||||
ArtcraftFluxPro11(PlanArtcraftFluxPro11<'a>),
|
||||
ArtcraftFluxPro11Ultra(PlanArtcraftFluxPro11Ultra<'a>),
|
||||
ArtcraftGptImage1(PlanArtcraftGptImage1<'a>),
|
||||
ArtcraftGptImage1p5(PlanArtcraftGptImage1p5<'a>),
|
||||
ArtcraftNanaBanana(PlanArtcraftNanaBanana<'a>),
|
||||
ArtcraftNanaBanana2(PlanArtcraftNanaBanana2<'a>),
|
||||
ArtcraftNanaBananaPro(PlanArtcraftNanaBananaPro<'a>),
|
||||
ArtcraftSeedream4(PlanArtcraftSeedream4<'a>),
|
||||
ArtcraftSeedream4p5(PlanArtcraftSeedream4p5<'a>),
|
||||
ArtcraftSeedream5Lite(PlanArtcraftSeedream5Lite<'a>),
|
||||
ArtcraftQwenEdit2511Angles(PlanArtcraftQwenEdit2511Angles<'a>),
|
||||
ArtcraftFlux2LoraAngles(PlanArtcraftFlux2LoraAngles<'a>),
|
||||
FalFlux1Dev(PlanFalFlux1Dev<'a>),
|
||||
FalFlux1Schnell(PlanFalFlux1Schnell<'a>),
|
||||
FalFluxPro11(PlanFalFluxPro11<'a>),
|
||||
FalFluxPro11Ultra(PlanFalFluxPro11Ultra<'a>),
|
||||
FalGptImage1(PlanFalGptImage1<'a>),
|
||||
FalGptImage1p5(PlanFalGptImage1p5<'a>),
|
||||
FalSeedream4(PlanFalSeedream4<'a>),
|
||||
FalSeedream4p5(PlanFalSeedream4p5<'a>),
|
||||
FalSeedream5Lite(PlanFalSeedream5Lite<'a>),
|
||||
FalNanoBanana(PlanFalNanoBanana<'a>),
|
||||
FalNanaBanana2(PlanFalNanaBanana2<'a>),
|
||||
FalNanaBananaPro(PlanFalNanaBananaPro<'a>),
|
||||
pub enum ImageGenerationPlan {
|
||||
ArtcraftFlux1Dev(PlanArtcraftFlux1Dev),
|
||||
ArtcraftFlux1Schnell(PlanArtcraftFlux1Schnell),
|
||||
ArtcraftFluxPro11(PlanArtcraftFluxPro11),
|
||||
ArtcraftFluxPro11Ultra(PlanArtcraftFluxPro11Ultra),
|
||||
ArtcraftGptImage1(PlanArtcraftGptImage1),
|
||||
ArtcraftGptImage1p5(PlanArtcraftGptImage1p5),
|
||||
ArtcraftGptImage2(PlanArtcraftGptImage2),
|
||||
ArtcraftNanaBanana(PlanArtcraftNanaBanana),
|
||||
ArtcraftNanaBanana2(PlanArtcraftNanaBanana2),
|
||||
ArtcraftNanaBananaPro(PlanArtcraftNanaBananaPro),
|
||||
ArtcraftSeedream4(PlanArtcraftSeedream4),
|
||||
ArtcraftSeedream4p5(PlanArtcraftSeedream4p5),
|
||||
ArtcraftSeedream5Lite(PlanArtcraftSeedream5Lite),
|
||||
ArtcraftQwenEdit2511Angles(PlanArtcraftQwenEdit2511Angles),
|
||||
ArtcraftFlux2LoraAngles(PlanArtcraftFlux2LoraAngles),
|
||||
FalFlux1Dev(PlanFalFlux1Dev),
|
||||
FalFlux1Schnell(PlanFalFlux1Schnell),
|
||||
FalFluxPro11(PlanFalFluxPro11),
|
||||
FalFluxPro11Ultra(PlanFalFluxPro11Ultra),
|
||||
FalGptImage1(PlanFalGptImage1),
|
||||
FalGptImage1p5(PlanFalGptImage1p5),
|
||||
FalGptImage2(PlanFalGptImage2),
|
||||
FalSeedream4(PlanFalSeedream4),
|
||||
FalSeedream4p5(PlanFalSeedream4p5),
|
||||
FalSeedream5Lite(PlanFalSeedream5Lite),
|
||||
FalNanoBanana(PlanFalNanoBanana),
|
||||
FalNanaBanana2(PlanFalNanaBanana2),
|
||||
FalNanaBananaPro(PlanFalNanaBananaPro),
|
||||
}
|
||||
|
||||
impl<'a> ImageGenerationPlan<'a> {
|
||||
impl ImageGenerationPlan {
|
||||
pub async fn generate_image(
|
||||
&self,
|
||||
client: &RouterClient,
|
||||
@@ -141,6 +149,10 @@ impl<'a> ImageGenerationPlan<'a> {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_gpt_image_1p5(plan, artcraft_client).await
|
||||
}
|
||||
ImageGenerationPlan::ArtcraftGptImage2(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_gpt_image_2(plan, artcraft_client).await
|
||||
}
|
||||
ImageGenerationPlan::ArtcraftNanaBanana(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_nano_banana(plan, artcraft_client).await
|
||||
@@ -197,6 +209,10 @@ impl<'a> ImageGenerationPlan<'a> {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_gpt_image_1p5(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalGptImage2(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_gpt_image_2(plan, fal_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalSeedream4(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_seedream_4(plan, fal_client).await
|
||||
@@ -244,6 +260,9 @@ impl<'a> ImageGenerationPlan<'a> {
|
||||
ImageGenerationPlan::ArtcraftGptImage1p5(plan) => {
|
||||
estimate_image_cost_artcraft_gpt_image_1p5(plan)
|
||||
}
|
||||
ImageGenerationPlan::ArtcraftGptImage2(plan) => {
|
||||
estimate_image_cost_artcraft_gpt_image_2(plan)
|
||||
}
|
||||
ImageGenerationPlan::ArtcraftNanaBanana(plan) => {
|
||||
estimate_image_cost_artcraft_nano_banana(plan)
|
||||
}
|
||||
@@ -286,6 +305,9 @@ impl<'a> ImageGenerationPlan<'a> {
|
||||
ImageGenerationPlan::FalGptImage1p5(plan) => {
|
||||
estimate_image_cost_fal_gpt_image_1p5(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalGptImage2(plan) => {
|
||||
estimate_image_cost_fal_gpt_image_2(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalSeedream4(plan) => {
|
||||
estimate_image_cost_fal_seedream_4(plan)
|
||||
}
|
||||
|
||||
+1
@@ -4,6 +4,7 @@ pub mod plan_generate_image_artcraft_flux_pro_1p1;
|
||||
pub mod plan_generate_image_artcraft_flux_pro_1p1_ultra;
|
||||
pub mod plan_generate_image_artcraft_gpt_image_1;
|
||||
pub mod plan_generate_image_artcraft_gpt_image_1p5;
|
||||
pub mod plan_generate_image_artcraft_gpt_image_2;
|
||||
pub mod plan_generate_image_artcraft_nano_banana;
|
||||
pub mod plan_generate_image_artcraft_nano_banana_2;
|
||||
pub mod plan_generate_image_artcraft_nano_banana_pro;
|
||||
|
||||
+9
-9
@@ -10,16 +10,16 @@ use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image:
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftFlux1Dev<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub struct PlanArtcraftFlux1Dev {
|
||||
pub prompt: Option<String>,
|
||||
pub aspect_ratio: Option<GenerateFlux1DevTextToImageAspectRatio>,
|
||||
pub num_images: GenerateFlux1DevTextToImageNumImages,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_flux_1_dev<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
pub fn plan_generate_image_artcraft_flux_1_dev(
|
||||
request: &GenerateImageRequest,
|
||||
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
// Flux 1 Dev is text-to-image only. Abort if image inputs are provided and
|
||||
// the caller has opted into strict mode.
|
||||
if request.image_inputs.is_some() {
|
||||
@@ -37,7 +37,7 @@ pub fn plan_generate_image_artcraft_flux_1_dev<'a>(
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::ArtcraftFlux1Dev(PlanArtcraftFlux1Dev {
|
||||
prompt: request.prompt,
|
||||
prompt: request.prompt.clone(),
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
@@ -147,7 +147,7 @@ mod tests {
|
||||
fn image_inputs_with_abort_returns_error() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::AbortGeneration),
|
||||
..base_flux_1_dev_image_request()
|
||||
};
|
||||
@@ -162,7 +162,7 @@ mod tests {
|
||||
fn image_inputs_with_generate_anyway_succeeds() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::GenerateAnyway),
|
||||
..base_flux_1_dev_image_request()
|
||||
};
|
||||
@@ -173,7 +173,7 @@ mod tests {
|
||||
fn image_inputs_with_none_strategy_succeeds() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
generation_mode_mismatch_strategy: None,
|
||||
..base_flux_1_dev_image_request()
|
||||
};
|
||||
|
||||
+9
-9
@@ -10,16 +10,16 @@ use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_im
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftFlux1Schnell<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub struct PlanArtcraftFlux1Schnell {
|
||||
pub prompt: Option<String>,
|
||||
pub aspect_ratio: Option<GenerateFlux1SchnellTextToImageAspectRatio>,
|
||||
pub num_images: GenerateFlux1SchnellTextToImageNumImages,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_flux_1_schnell<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
pub fn plan_generate_image_artcraft_flux_1_schnell(
|
||||
request: &GenerateImageRequest,
|
||||
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
// Flux 1 Schnell is text-to-image only. Abort if image inputs are provided and
|
||||
// the caller has opted into strict mode.
|
||||
if request.image_inputs.is_some() {
|
||||
@@ -37,7 +37,7 @@ pub fn plan_generate_image_artcraft_flux_1_schnell<'a>(
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::ArtcraftFlux1Schnell(PlanArtcraftFlux1Schnell {
|
||||
prompt: request.prompt,
|
||||
prompt: request.prompt.clone(),
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
@@ -147,7 +147,7 @@ mod tests {
|
||||
fn image_inputs_with_abort_returns_error() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::AbortGeneration),
|
||||
..base_flux_1_schnell_image_request()
|
||||
};
|
||||
@@ -162,7 +162,7 @@ mod tests {
|
||||
fn image_inputs_with_generate_anyway_succeeds() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::GenerateAnyway),
|
||||
..base_flux_1_schnell_image_request()
|
||||
};
|
||||
@@ -173,7 +173,7 @@ mod tests {
|
||||
fn image_inputs_with_none_strategy_succeeds() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
generation_mode_mismatch_strategy: None,
|
||||
..base_flux_1_schnell_image_request()
|
||||
};
|
||||
|
||||
+22
-22
@@ -12,8 +12,8 @@ use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::{
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftFlux2LoraAngles<'a> {
|
||||
pub image_input: &'a MediaFileToken,
|
||||
pub struct PlanArtcraftFlux2LoraAngles {
|
||||
pub image_input: MediaFileToken,
|
||||
pub horizontal_angle: Option<f64>,
|
||||
pub vertical_angle: Option<f64>,
|
||||
pub zoom: Option<f64>,
|
||||
@@ -22,13 +22,13 @@ pub struct PlanArtcraftFlux2LoraAngles<'a> {
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_flux_2_lora_angles<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
pub fn plan_generate_image_artcraft_flux_2_lora_angles(
|
||||
request: &GenerateImageRequest,
|
||||
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Angle models require exactly one input image.
|
||||
let image_input = resolve_single_image_input(request.image_inputs)?;
|
||||
let image_input = resolve_single_image_input(request.image_inputs.clone())?;
|
||||
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
@@ -43,9 +43,9 @@ pub fn plan_generate_image_artcraft_flux_2_lora_angles<'a>(
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_single_image_input<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<&'a MediaFileToken, ArtcraftRouterError> {
|
||||
fn resolve_single_image_input(
|
||||
image_list_ref: Option<ImageListRef>,
|
||||
) -> Result<MediaFileToken, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
@@ -54,13 +54,13 @@ fn resolve_single_image_input<'a>(
|
||||
}))
|
||||
}
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => {
|
||||
if tokens.len() != 1 {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: format!("Angle models require exactly one input image, got {}", tokens.len()),
|
||||
}));
|
||||
if let Some(token) = tokens.first() {
|
||||
return Ok(token.clone());
|
||||
}
|
||||
Ok(&tokens[0])
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: format!("Angle models require exactly one input image, got {}", tokens.len()),
|
||||
}))
|
||||
}
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
@@ -165,7 +165,7 @@ mod tests {
|
||||
fn rejects_urls() {
|
||||
let urls = vec!["https://example.com/img.png".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
image_inputs: Some(ImageListRef::Urls(urls.clone())),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
@@ -176,7 +176,7 @@ mod tests {
|
||||
fn accepts_single_media_token() {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
@@ -187,7 +187,7 @@ mod tests {
|
||||
fn rejects_multiple_media_tokens() {
|
||||
let tokens = vec![make_token(), make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
@@ -199,7 +199,7 @@ mod tests {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: None,
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
|
||||
@@ -211,7 +211,7 @@ mod tests {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
|
||||
@@ -226,7 +226,7 @@ mod tests {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(count),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
|
||||
@@ -244,7 +244,7 @@ mod tests {
|
||||
horizontal_angle: Some(45.0),
|
||||
vertical_angle: Some(-30.0),
|
||||
zoom: Some(1.5),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
|
||||
|
||||
+9
-9
@@ -10,16 +10,16 @@ use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftFluxPro11<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub struct PlanArtcraftFluxPro11 {
|
||||
pub prompt: Option<String>,
|
||||
pub aspect_ratio: Option<GenerateFluxPro11TextToImageAspectRatio>,
|
||||
pub num_images: GenerateFluxPro11TextToImageNumImages,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_flux_pro_1p1<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
pub fn plan_generate_image_artcraft_flux_pro_1p1(
|
||||
request: &GenerateImageRequest,
|
||||
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
// Flux Pro 1.1 is text-to-image only. Abort if image inputs are provided and
|
||||
// the caller has opted into strict mode.
|
||||
if request.image_inputs.is_some() {
|
||||
@@ -37,7 +37,7 @@ pub fn plan_generate_image_artcraft_flux_pro_1p1<'a>(
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::ArtcraftFluxPro11(PlanArtcraftFluxPro11 {
|
||||
prompt: request.prompt,
|
||||
prompt: request.prompt.clone(),
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
@@ -147,7 +147,7 @@ mod tests {
|
||||
fn image_inputs_with_abort_returns_error() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::AbortGeneration),
|
||||
..base_flux_pro_1p1_image_request()
|
||||
};
|
||||
@@ -162,7 +162,7 @@ mod tests {
|
||||
fn image_inputs_with_generate_anyway_succeeds() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::GenerateAnyway),
|
||||
..base_flux_pro_1p1_image_request()
|
||||
};
|
||||
@@ -173,7 +173,7 @@ mod tests {
|
||||
fn image_inputs_with_none_strategy_succeeds() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
generation_mode_mismatch_strategy: None,
|
||||
..base_flux_pro_1p1_image_request()
|
||||
};
|
||||
|
||||
+9
-9
@@ -10,16 +10,16 @@ use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftFluxPro11Ultra<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub struct PlanArtcraftFluxPro11Ultra {
|
||||
pub prompt: Option<String>,
|
||||
pub aspect_ratio: Option<GenerateFluxPro11UltraTextToImageAspectRatio>,
|
||||
pub num_images: GenerateFluxPro11UltraTextToImageNumImages,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_flux_pro_1p1_ultra<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
pub fn plan_generate_image_artcraft_flux_pro_1p1_ultra(
|
||||
request: &GenerateImageRequest,
|
||||
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
// Flux Pro 1.1 Ultra is text-to-image only. Abort if image inputs are provided and
|
||||
// the caller has opted into strict mode.
|
||||
if request.image_inputs.is_some() {
|
||||
@@ -37,7 +37,7 @@ pub fn plan_generate_image_artcraft_flux_pro_1p1_ultra<'a>(
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::ArtcraftFluxPro11Ultra(PlanArtcraftFluxPro11Ultra {
|
||||
prompt: request.prompt,
|
||||
prompt: request.prompt.clone(),
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
@@ -150,7 +150,7 @@ mod tests {
|
||||
fn image_inputs_with_abort_returns_error() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::AbortGeneration),
|
||||
..base_flux_pro_1p1_ultra_image_request()
|
||||
};
|
||||
@@ -165,7 +165,7 @@ mod tests {
|
||||
fn image_inputs_with_generate_anyway_succeeds() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::GenerateAnyway),
|
||||
..base_flux_pro_1p1_ultra_image_request()
|
||||
};
|
||||
@@ -176,7 +176,7 @@ mod tests {
|
||||
fn image_inputs_with_none_strategy_succeeds() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
generation_mode_mismatch_strategy: None,
|
||||
..base_flux_pro_1p1_ultra_image_request()
|
||||
};
|
||||
|
||||
+16
-16
@@ -43,10 +43,10 @@ pub enum ArtcraftGptImage1NumImages {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftGptImage1<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub struct PlanArtcraftGptImage1 {
|
||||
pub prompt: Option<String>,
|
||||
/// Input images for image editing. None means text-to-image mode.
|
||||
pub image_inputs: Option<&'a Vec<MediaFileToken>>,
|
||||
pub image_inputs: Option<Vec<MediaFileToken>>,
|
||||
/// Number of input images (0 for text-to-image). This is counted from either
|
||||
/// media tokens or hydrated URLs, so the cost estimator can use it without
|
||||
/// needing to inspect image_inputs (which is None for URL-form inputs).
|
||||
@@ -59,19 +59,19 @@ pub struct PlanArtcraftGptImage1<'a> {
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_gpt_image_1<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
pub fn plan_generate_image_artcraft_gpt_image_1(
|
||||
request: &GenerateImageRequest,
|
||||
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs)?;
|
||||
let num_input_images = count_input_images(request.image_inputs);
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs.clone())?;
|
||||
let num_input_images = count_input_images(request.image_inputs.clone());
|
||||
let image_size = plan_image_size(request.aspect_ratio);
|
||||
let quality = plan_quality(request.quality);
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::ArtcraftGptImage1(PlanArtcraftGptImage1 {
|
||||
prompt: request.prompt,
|
||||
prompt: request.prompt.clone(),
|
||||
image_inputs,
|
||||
num_input_images,
|
||||
image_size,
|
||||
@@ -81,9 +81,9 @@ pub fn plan_generate_image_artcraft_gpt_image_1<'a>(
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef>,
|
||||
) -> Result<Option<Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
@@ -95,7 +95,7 @@ fn resolve_image_list_ref<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
fn count_input_images(image_inputs: Option<ImageListRef<'_>>) -> u64 {
|
||||
fn count_input_images(image_inputs: Option<ImageListRef>) -> u64 {
|
||||
match image_inputs {
|
||||
None => 0,
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => tokens.len() as u64,
|
||||
@@ -435,7 +435,7 @@ mod tests {
|
||||
fn media_token_image_inputs_is_edit_mode() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_gpt_image_1_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
|
||||
@@ -451,7 +451,7 @@ mod tests {
|
||||
// cost only depends on quality + size + num_images.
|
||||
let urls = vec!["https://example.com/image.jpg".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
image_inputs: Some(ImageListRef::Urls(urls.clone())),
|
||||
..base_gpt_image_1_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
|
||||
@@ -474,7 +474,7 @@ mod tests {
|
||||
#[test]
|
||||
fn idempotency_token_passthrough() {
|
||||
let token = "11111111-1111-1111-1111-111111111111";
|
||||
let request = GenerateImageRequest { idempotency_token: Some(token), ..base_gpt_image_1_image_request() };
|
||||
let request = GenerateImageRequest { idempotency_token: Some(token.to_string()), ..base_gpt_image_1_image_request() };
|
||||
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
|
||||
panic!("expected ArtcraftGptImage1")
|
||||
};
|
||||
|
||||
+13
-13
@@ -13,10 +13,10 @@ use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_func
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftGptImage1p5<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub struct PlanArtcraftGptImage1p5 {
|
||||
pub prompt: Option<String>,
|
||||
/// Input images for image editing. None means text-to-image mode.
|
||||
pub image_inputs: Option<&'a Vec<MediaFileToken>>,
|
||||
pub image_inputs: Option<Vec<MediaFileToken>>,
|
||||
pub image_size: Option<GptImage1p5MultiFunctionImageGenSize>,
|
||||
/// Quality defaults to Medium when not specified in the request.
|
||||
pub quality: GptImage1p5MultiFunctionImageGenQuality,
|
||||
@@ -24,18 +24,18 @@ pub struct PlanArtcraftGptImage1p5<'a> {
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_gpt_image_1p5<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
pub fn plan_generate_image_artcraft_gpt_image_1p5(
|
||||
request: &GenerateImageRequest,
|
||||
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs)?;
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs.clone())?;
|
||||
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
|
||||
let quality = plan_quality(request.quality);
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::ArtcraftGptImage1p5(PlanArtcraftGptImage1p5 {
|
||||
prompt: request.prompt,
|
||||
prompt: request.prompt.clone(),
|
||||
image_inputs,
|
||||
image_size,
|
||||
quality,
|
||||
@@ -44,9 +44,9 @@ pub fn plan_generate_image_artcraft_gpt_image_1p5<'a>(
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef>,
|
||||
) -> Result<Option<Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
@@ -293,7 +293,7 @@ mod tests {
|
||||
fn url_image_inputs_are_accepted_for_cost_path() {
|
||||
let urls = vec!["https://example.com/image.jpg".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
image_inputs: Some(ImageListRef::Urls(urls.clone())),
|
||||
..base_gpt_image_1p5_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else {
|
||||
@@ -313,7 +313,7 @@ mod tests {
|
||||
fn media_token_image_inputs_is_edit_mode() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_gpt_image_1p5_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
|
||||
|
||||
+435
@@ -0,0 +1,435 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_quality::CommonQuality;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
|
||||
use enums::common::generation::common_quality::CommonQuality as CommonQualityEnum;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// GPT Image 2 supports six image sizes.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ArtcraftGptImage2ImageSize {
|
||||
SquareHd,
|
||||
Square,
|
||||
Portrait4x3,
|
||||
Portrait16x9,
|
||||
Landscape4x3,
|
||||
Landscape16x9,
|
||||
Auto,
|
||||
}
|
||||
|
||||
/// Quality: Low / Medium / High.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ArtcraftGptImage2Quality {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ArtcraftGptImage2NumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftGptImage2 {
|
||||
pub prompt: Option<String>,
|
||||
pub image_inputs: Option<Vec<MediaFileToken>>,
|
||||
pub image_size: Option<ArtcraftGptImage2ImageSize>,
|
||||
pub quality: ArtcraftGptImage2Quality,
|
||||
pub num_images: ArtcraftGptImage2NumImages,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_gpt_image_2(
|
||||
request: &GenerateImageRequest,
|
||||
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs.clone())?;
|
||||
let image_size = plan_image_size(request.aspect_ratio);
|
||||
let quality = plan_quality(request.quality);
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::ArtcraftGptImage2(PlanArtcraftGptImage2 {
|
||||
prompt: request.prompt.clone(),
|
||||
image_inputs,
|
||||
image_size,
|
||||
quality,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
}))
|
||||
}
|
||||
|
||||
impl PlanArtcraftGptImage2 {
|
||||
/// Map the plan's aspect ratio to the `CommonAspectRatioEnum` used by the
|
||||
/// omni-gen request.
|
||||
pub fn aspect_ratio_enum(&self) -> Option<CommonAspectRatioEnum> {
|
||||
self.image_size.map(|s| match s {
|
||||
ArtcraftGptImage2ImageSize::SquareHd => CommonAspectRatioEnum::SquareHd,
|
||||
ArtcraftGptImage2ImageSize::Square => CommonAspectRatioEnum::Square,
|
||||
ArtcraftGptImage2ImageSize::Portrait4x3 => CommonAspectRatioEnum::TallThreeByFour,
|
||||
ArtcraftGptImage2ImageSize::Portrait16x9 => CommonAspectRatioEnum::TallNineBySixteen,
|
||||
ArtcraftGptImage2ImageSize::Landscape4x3 => CommonAspectRatioEnum::WideFourByThree,
|
||||
ArtcraftGptImage2ImageSize::Landscape16x9 => CommonAspectRatioEnum::WideSixteenByNine,
|
||||
ArtcraftGptImage2ImageSize::Auto => CommonAspectRatioEnum::Auto,
|
||||
})
|
||||
}
|
||||
|
||||
/// Map the plan's quality to the `CommonQualityEnum` used by the omni-gen
|
||||
/// request.
|
||||
pub fn quality_enum(&self) -> CommonQualityEnum {
|
||||
match self.quality {
|
||||
ArtcraftGptImage2Quality::Low => CommonQualityEnum::Low,
|
||||
ArtcraftGptImage2Quality::Medium => CommonQualityEnum::Medium,
|
||||
ArtcraftGptImage2Quality::High => CommonQualityEnum::High,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn num_images_u16(&self) -> u16 {
|
||||
match self.num_images {
|
||||
ArtcraftGptImage2NumImages::One => 1,
|
||||
ArtcraftGptImage2NumImages::Two => 2,
|
||||
ArtcraftGptImage2NumImages::Three => 3,
|
||||
ArtcraftGptImage2NumImages::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ArtcraftGptImage2NumImages {
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef>,
|
||||
) -> Result<Option<Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(ImageListRef::Urls(_)) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_quality(quality: Option<CommonQuality>) -> ArtcraftGptImage2Quality {
|
||||
match quality {
|
||||
Some(CommonQuality::Low) => ArtcraftGptImage2Quality::Low,
|
||||
Some(CommonQuality::Medium) => ArtcraftGptImage2Quality::Medium,
|
||||
Some(CommonQuality::High) => ArtcraftGptImage2Quality::High,
|
||||
None => ArtcraftGptImage2Quality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_image_size(aspect_ratio: Option<CommonAspectRatio>) -> Option<ArtcraftGptImage2ImageSize> {
|
||||
use ArtcraftGptImage2ImageSize as S;
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Some(S::Auto),
|
||||
|
||||
Some(CommonAspectRatio::Square) => Some(S::Square),
|
||||
Some(CommonAspectRatio::SquareHd) => Some(S::SquareHd),
|
||||
|
||||
Some(CommonAspectRatio::WideFourByThree)
|
||||
| Some(CommonAspectRatio::WideFiveByFour) => Some(S::Landscape4x3),
|
||||
|
||||
Some(CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(CommonAspectRatio::WideSixteenByNine)
|
||||
| Some(CommonAspectRatio::WideTwentyOneByNine)
|
||||
| Some(CommonAspectRatio::Wide) => Some(S::Landscape16x9),
|
||||
|
||||
Some(CommonAspectRatio::TallThreeByFour)
|
||||
| Some(CommonAspectRatio::TallFourByFive) => Some(S::Portrait4x3),
|
||||
|
||||
Some(CommonAspectRatio::TallTwoByThree)
|
||||
| Some(CommonAspectRatio::TallNineBySixteen)
|
||||
| Some(CommonAspectRatio::TallNineByTwentyOne)
|
||||
| Some(CommonAspectRatio::Tall) => Some(S::Portrait16x9),
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<ArtcraftGptImage2NumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(ArtcraftGptImage2NumImages::One),
|
||||
2 => Ok(ArtcraftGptImage2NumImages::Two),
|
||||
3 => Ok(ArtcraftGptImage2NumImages::Three),
|
||||
4 => Ok(ArtcraftGptImage2NumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(ArtcraftGptImage2NumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::provider::Provider;
|
||||
|
||||
// ── Mode detection ──
|
||||
|
||||
mod mode_detection {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn no_image_inputs_yields_none() {
|
||||
let plan = build_plan(&GenerateImageRequest { image_inputs: None, ..base_request() });
|
||||
assert!(plan.image_inputs.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_token_inputs_are_extracted() {
|
||||
let tokens = vec![
|
||||
MediaFileToken::new_from_str("mf_test_a"),
|
||||
MediaFileToken::new_from_str("mf_test_b"),
|
||||
];
|
||||
let plan = build_plan(&GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_request()
|
||||
});
|
||||
assert!(plan.image_inputs.is_some());
|
||||
assert_eq!(plan.image_inputs.unwrap().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_inputs_are_accepted_for_cost_path() {
|
||||
let urls = vec!["https://example.com/a.jpg".to_string()];
|
||||
let plan = build_plan(&GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(urls)),
|
||||
..base_request()
|
||||
});
|
||||
assert!(plan.image_inputs.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Quality ──
|
||||
|
||||
mod quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn defaults_to_high() {
|
||||
let plan = build_plan(&base_request());
|
||||
assert!(matches!(plan.quality, ArtcraftGptImage2Quality::High));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low() {
|
||||
let plan = build_plan(&GenerateImageRequest { quality: Some(CommonQuality::Low), ..base_request() });
|
||||
assert!(matches!(plan.quality, ArtcraftGptImage2Quality::Low));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium() {
|
||||
let plan = build_plan(&GenerateImageRequest { quality: Some(CommonQuality::Medium), ..base_request() });
|
||||
assert!(matches!(plan.quality, ArtcraftGptImage2Quality::Medium));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high() {
|
||||
let plan = build_plan(&GenerateImageRequest { quality: Some(CommonQuality::High), ..base_request() });
|
||||
assert!(matches!(plan.quality, ArtcraftGptImage2Quality::High));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Image size mapping ──
|
||||
|
||||
mod image_size_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn none_yields_auto() {
|
||||
let plan = build_plan(&GenerateImageRequest { aspect_ratio: None, ..base_request() });
|
||||
assert!(matches!(plan.image_size, Some(ArtcraftGptImage2ImageSize::Auto)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_variants_yield_auto() {
|
||||
for ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
|
||||
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(ar), ..base_request() });
|
||||
assert!(matches!(plan.image_size, Some(ArtcraftGptImage2ImageSize::Auto)), "expected Auto for {:?}", ar);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn square() {
|
||||
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::Square), ..base_request() });
|
||||
assert!(matches!(plan.image_size, Some(ArtcraftGptImage2ImageSize::Square)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn square_hd() {
|
||||
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::SquareHd), ..base_request() });
|
||||
assert!(matches!(plan.image_size, Some(ArtcraftGptImage2ImageSize::SquareHd)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn landscape_4x3_variants() {
|
||||
for ar in [CommonAspectRatio::WideFourByThree, CommonAspectRatio::WideFiveByFour] {
|
||||
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(ar), ..base_request() });
|
||||
assert!(matches!(plan.image_size, Some(ArtcraftGptImage2ImageSize::Landscape4x3)), "expected Landscape4x3 for {:?}", ar);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn landscape_16x9_variants() {
|
||||
for ar in [
|
||||
CommonAspectRatio::WideThreeByTwo,
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideTwentyOneByNine,
|
||||
CommonAspectRatio::Wide,
|
||||
] {
|
||||
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(ar), ..base_request() });
|
||||
assert!(matches!(plan.image_size, Some(ArtcraftGptImage2ImageSize::Landscape16x9)), "expected Landscape16x9 for {:?}", ar);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portrait_4x3_variants() {
|
||||
for ar in [CommonAspectRatio::TallThreeByFour, CommonAspectRatio::TallFourByFive] {
|
||||
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(ar), ..base_request() });
|
||||
assert!(matches!(plan.image_size, Some(ArtcraftGptImage2ImageSize::Portrait4x3)), "expected Portrait4x3 for {:?}", ar);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portrait_16x9_variants() {
|
||||
for ar in [
|
||||
CommonAspectRatio::TallTwoByThree,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
CommonAspectRatio::TallNineByTwentyOne,
|
||||
CommonAspectRatio::Tall,
|
||||
] {
|
||||
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(ar), ..base_request() });
|
||||
assert!(matches!(plan.image_size, Some(ArtcraftGptImage2ImageSize::Portrait16x9)), "expected Portrait16x9 for {:?}", ar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Num images ──
|
||||
|
||||
mod num_images_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_is_one() {
|
||||
let plan = build_plan(&GenerateImageRequest { image_batch_count: None, ..base_request() });
|
||||
assert!(matches!(plan.num_images, ArtcraftGptImage2NumImages::One));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_mapping() {
|
||||
for (count, expected) in [(1, 1), (2, 2), (3, 3), (4, 4)] {
|
||||
let plan = build_plan(&GenerateImageRequest { image_batch_count: Some(count), ..base_request() });
|
||||
assert_eq!(plan.num_images.as_u64(), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_is_always_error() {
|
||||
for strategy in [
|
||||
RequestMismatchMitigationStrategy::ErrorOut,
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let result = plan_generate_image_artcraft_gpt_image_2(&GenerateImageRequest {
|
||||
image_batch_count: Some(0),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_request()
|
||||
});
|
||||
assert!(matches!(result, Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_range_error_out() {
|
||||
let result = plan_generate_image_artcraft_gpt_image_2(&GenerateImageRequest {
|
||||
image_batch_count: Some(5),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_request()
|
||||
});
|
||||
assert!(matches!(result, Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_range_clamps_to_four() {
|
||||
for strategy in [
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let plan = build_plan(&GenerateImageRequest {
|
||||
image_batch_count: Some(9),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_request()
|
||||
});
|
||||
assert!(matches!(plan.num_images, ArtcraftGptImage2NumImages::Four));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── as_u64 ──
|
||||
|
||||
#[test]
|
||||
fn num_images_as_u64() {
|
||||
assert_eq!(ArtcraftGptImage2NumImages::One.as_u64(), 1);
|
||||
assert_eq!(ArtcraftGptImage2NumImages::Two.as_u64(), 2);
|
||||
assert_eq!(ArtcraftGptImage2NumImages::Three.as_u64(), 3);
|
||||
assert_eq!(ArtcraftGptImage2NumImages::Four.as_u64(), 4);
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_request() -> GenerateImageRequest {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::GptImage2,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_plan(request: &GenerateImageRequest) -> PlanArtcraftGptImage2 {
|
||||
let ImageGenerationPlan::ArtcraftGptImage2(plan) =
|
||||
plan_generate_image_artcraft_gpt_image_2(request).expect("plan should succeed")
|
||||
else {
|
||||
panic!("expected ArtcraftGptImage2 variant")
|
||||
};
|
||||
plan
|
||||
}
|
||||
}
|
||||
+14
-14
@@ -11,27 +11,27 @@ use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_functi
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftNanaBanana<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub struct PlanArtcraftNanaBanana {
|
||||
pub prompt: Option<String>,
|
||||
/// Input images for image editing. None means text-to-image mode.
|
||||
pub image_inputs: Option<&'a Vec<MediaFileToken>>,
|
||||
pub image_inputs: Option<Vec<MediaFileToken>>,
|
||||
pub aspect_ratio: Option<NanoBananaMultiFunctionImageGenAspectRatio>,
|
||||
pub num_images: NanoBananaMultiFunctionImageGenNumImages,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_nano_banana<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
pub fn plan_generate_image_artcraft_nano_banana(
|
||||
request: &GenerateImageRequest,
|
||||
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let is_edit_mode = request.image_inputs.is_some();
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs)?;
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs.clone())?;
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, is_edit_mode, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::ArtcraftNanaBanana(PlanArtcraftNanaBanana {
|
||||
prompt: request.prompt,
|
||||
prompt: request.prompt.clone(),
|
||||
image_inputs,
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
@@ -39,9 +39,9 @@ pub fn plan_generate_image_artcraft_nano_banana<'a>(
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef>,
|
||||
) -> Result<Option<Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
@@ -222,7 +222,7 @@ mod tests {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(auto_ar),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_nano_banana_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") };
|
||||
@@ -369,7 +369,7 @@ mod tests {
|
||||
// cost only depends on num_images + is_edit_mode.
|
||||
let urls = vec!["https://example.com/image.jpg".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
image_inputs: Some(ImageListRef::Urls(urls.clone())),
|
||||
..base_nano_banana_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else {
|
||||
@@ -391,7 +391,7 @@ mod tests {
|
||||
fn media_token_image_inputs_is_edit_mode() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_nano_banana_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") };
|
||||
|
||||
+12
-12
@@ -13,29 +13,29 @@ use artcraft_api_defs::generate::image::multi_function::nano_banana_2_multi_func
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftNanaBanana2<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub struct PlanArtcraftNanaBanana2 {
|
||||
pub prompt: Option<String>,
|
||||
/// Input images for image editing. None means text-to-image mode.
|
||||
pub image_inputs: Option<&'a Vec<MediaFileToken>>,
|
||||
pub image_inputs: Option<Vec<MediaFileToken>>,
|
||||
pub aspect_ratio: Option<NanaBanana2MultiFunctionImageGenAspectRatio>,
|
||||
pub resolution: Option<NanaBanana2MultiFunctionImageGenImageResolution>,
|
||||
pub num_images: NanaBanana2MultiFunctionImageGenNumImages,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_nano_banana_2<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
pub fn plan_generate_image_artcraft_nano_banana_2(
|
||||
request: &GenerateImageRequest,
|
||||
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let is_edit_mode = request.image_inputs.is_some();
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs)?;
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs.clone())?;
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, is_edit_mode, strategy)?;
|
||||
let resolution = plan_resolution(request.resolution, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::ArtcraftNanaBanana2(PlanArtcraftNanaBanana2 {
|
||||
prompt: request.prompt,
|
||||
prompt: request.prompt.clone(),
|
||||
image_inputs,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
@@ -44,9 +44,9 @@ pub fn plan_generate_image_artcraft_nano_banana_2<'a>(
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef>,
|
||||
) -> Result<Option<Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
@@ -211,7 +211,7 @@ mod tests {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(auto_ar),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_nano_banana_2_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") };
|
||||
|
||||
+14
-14
@@ -13,29 +13,29 @@ use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_fu
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftNanaBananaPro<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub struct PlanArtcraftNanaBananaPro {
|
||||
pub prompt: Option<String>,
|
||||
/// Input images for image editing. None means text-to-image mode.
|
||||
pub image_inputs: Option<&'a Vec<MediaFileToken>>,
|
||||
pub image_inputs: Option<Vec<MediaFileToken>>,
|
||||
pub aspect_ratio: Option<NanoBananaProMultiFunctionImageGenAspectRatio>,
|
||||
pub resolution: Option<NanoBananaProMultiFunctionImageGenImageResolution>,
|
||||
pub num_images: NanoBananaProMultiFunctionImageGenNumImages,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_nano_banana_pro<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
pub fn plan_generate_image_artcraft_nano_banana_pro(
|
||||
request: &GenerateImageRequest,
|
||||
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let is_edit_mode = request.image_inputs.is_some();
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs)?;
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs.clone())?;
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, is_edit_mode, strategy)?;
|
||||
let resolution = plan_resolution(request.resolution, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::ArtcraftNanaBananaPro(PlanArtcraftNanaBananaPro {
|
||||
prompt: request.prompt,
|
||||
prompt: request.prompt.clone(),
|
||||
image_inputs,
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
@@ -44,9 +44,9 @@ pub fn plan_generate_image_artcraft_nano_banana_pro<'a>(
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef>,
|
||||
) -> Result<Option<Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
@@ -249,7 +249,7 @@ mod tests {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(auto_ar),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
|
||||
@@ -432,7 +432,7 @@ mod tests {
|
||||
// cost only depends on num_images + resolution + is_edit_mode.
|
||||
let urls = vec!["https://example.com/image.jpg".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
image_inputs: Some(ImageListRef::Urls(urls.clone())),
|
||||
..base_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else {
|
||||
@@ -452,7 +452,7 @@ mod tests {
|
||||
fn media_token_image_inputs_is_edit_mode() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
|
||||
|
||||
+24
-24
@@ -12,9 +12,9 @@ use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftQwenEdit2511Angles<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub image_input: &'a MediaFileToken,
|
||||
pub struct PlanArtcraftQwenEdit2511Angles {
|
||||
pub prompt: Option<String>,
|
||||
pub image_input: MediaFileToken,
|
||||
pub horizontal_angle: Option<f64>,
|
||||
pub vertical_angle: Option<f64>,
|
||||
pub zoom: Option<f64>,
|
||||
@@ -23,18 +23,18 @@ pub struct PlanArtcraftQwenEdit2511Angles<'a> {
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_qwen_edit_2511_angles<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
pub fn plan_generate_image_artcraft_qwen_edit_2511_angles(
|
||||
request: &GenerateImageRequest,
|
||||
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Angle models require exactly one input image.
|
||||
let image_input = resolve_single_image_input(request.image_inputs)?;
|
||||
let image_input = resolve_single_image_input(request.image_inputs.clone())?;
|
||||
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::ArtcraftQwenEdit2511Angles(PlanArtcraftQwenEdit2511Angles {
|
||||
prompt: request.prompt,
|
||||
prompt: request.prompt.clone(),
|
||||
image_input,
|
||||
horizontal_angle: request.horizontal_angle,
|
||||
vertical_angle: request.vertical_angle,
|
||||
@@ -45,9 +45,9 @@ pub fn plan_generate_image_artcraft_qwen_edit_2511_angles<'a>(
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_single_image_input<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<&'a MediaFileToken, ArtcraftRouterError> {
|
||||
fn resolve_single_image_input(
|
||||
image_list_ref: Option<ImageListRef>,
|
||||
) -> Result<MediaFileToken, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
@@ -56,13 +56,13 @@ fn resolve_single_image_input<'a>(
|
||||
}))
|
||||
}
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => {
|
||||
if tokens.len() != 1 {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: format!("Angle models require exactly one input image, got {}", tokens.len()),
|
||||
}));
|
||||
if let Some(token) = tokens.first() {
|
||||
return Ok(token.clone());
|
||||
}
|
||||
Ok(&tokens[0])
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: format!("Angle models require exactly one input image, got {}", tokens.len()),
|
||||
}))
|
||||
}
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
@@ -167,7 +167,7 @@ mod tests {
|
||||
fn rejects_urls() {
|
||||
let urls = vec!["https://example.com/img.png".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
image_inputs: Some(ImageListRef::Urls(urls.clone())),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
@@ -178,7 +178,7 @@ mod tests {
|
||||
fn accepts_single_media_token() {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
@@ -189,7 +189,7 @@ mod tests {
|
||||
fn rejects_multiple_media_tokens() {
|
||||
let tokens = vec![make_token(), make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
@@ -201,7 +201,7 @@ mod tests {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: None,
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) = request.build().unwrap() else { panic!("expected ArtcraftQwenEdit2511Angles") };
|
||||
@@ -213,7 +213,7 @@ mod tests {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) = request.build().unwrap() else { panic!("expected ArtcraftQwenEdit2511Angles") };
|
||||
@@ -228,7 +228,7 @@ mod tests {
|
||||
let request = GenerateImageRequest {
|
||||
quality: None,
|
||||
image_batch_count: Some(count),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) = request.build().unwrap() else { panic!("expected ArtcraftQwenEdit2511Angles") };
|
||||
@@ -246,7 +246,7 @@ mod tests {
|
||||
horizontal_angle: Some(45.0),
|
||||
vertical_angle: Some(-30.0),
|
||||
zoom: Some(1.5),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) = request.build().unwrap() else { panic!("expected ArtcraftQwenEdit2511Angles") };
|
||||
|
||||
+14
-14
@@ -12,27 +12,27 @@ use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_mu
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftSeedream4<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub struct PlanArtcraftSeedream4 {
|
||||
pub prompt: Option<String>,
|
||||
/// Input images for image editing. None means text-to-image mode.
|
||||
pub image_inputs: Option<&'a Vec<MediaFileToken>>,
|
||||
pub image_inputs: Option<Vec<MediaFileToken>>,
|
||||
pub image_size: Option<BytedanceSeedreamV4MultiFunctionImageGenImageSize>,
|
||||
pub num_images: BytedanceSeedreamV4MultiFunctionImageGenNumImages,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_seedream_4<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
pub fn plan_generate_image_artcraft_seedream_4(
|
||||
request: &GenerateImageRequest,
|
||||
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let is_edit_mode = request.image_inputs.is_some();
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs)?;
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs.clone())?;
|
||||
let image_size = plan_image_size(request.aspect_ratio, is_edit_mode, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::ArtcraftSeedream4(PlanArtcraftSeedream4 {
|
||||
prompt: request.prompt,
|
||||
prompt: request.prompt.clone(),
|
||||
image_inputs,
|
||||
image_size,
|
||||
num_images,
|
||||
@@ -40,9 +40,9 @@ pub fn plan_generate_image_artcraft_seedream_4<'a>(
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef>,
|
||||
) -> Result<Option<Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
@@ -220,7 +220,7 @@ mod tests {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_seedream_4_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
|
||||
@@ -355,7 +355,7 @@ mod tests {
|
||||
fn url_image_inputs_are_accepted_for_cost_path() {
|
||||
let urls = vec!["https://example.com/image.jpg".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
image_inputs: Some(ImageListRef::Urls(urls.clone())),
|
||||
..base_seedream_4_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else {
|
||||
@@ -375,7 +375,7 @@ mod tests {
|
||||
fn media_token_image_inputs_is_edit_mode() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_seedream_4_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
|
||||
|
||||
+14
-14
@@ -12,27 +12,27 @@ use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftSeedream4p5<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub struct PlanArtcraftSeedream4p5 {
|
||||
pub prompt: Option<String>,
|
||||
/// Input images for image editing. None means text-to-image mode.
|
||||
pub image_inputs: Option<&'a Vec<MediaFileToken>>,
|
||||
pub image_inputs: Option<Vec<MediaFileToken>>,
|
||||
pub image_size: Option<BytedanceSeedreamV4p5MultiFunctionImageGenImageSize>,
|
||||
pub num_images: BytedanceSeedreamV4p5MultiFunctionImageGenNumImages,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_seedream_4p5<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
pub fn plan_generate_image_artcraft_seedream_4p5(
|
||||
request: &GenerateImageRequest,
|
||||
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let is_edit_mode = request.image_inputs.is_some();
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs)?;
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs.clone())?;
|
||||
let image_size = plan_image_size(request.aspect_ratio, is_edit_mode, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::ArtcraftSeedream4p5(PlanArtcraftSeedream4p5 {
|
||||
prompt: request.prompt,
|
||||
prompt: request.prompt.clone(),
|
||||
image_inputs,
|
||||
image_size,
|
||||
num_images,
|
||||
@@ -40,9 +40,9 @@ pub fn plan_generate_image_artcraft_seedream_4p5<'a>(
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef>,
|
||||
) -> Result<Option<Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
@@ -221,7 +221,7 @@ mod tests {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_seedream_4p5_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
|
||||
@@ -356,7 +356,7 @@ mod tests {
|
||||
fn url_image_inputs_are_accepted_for_cost_path() {
|
||||
let urls = vec!["https://example.com/image.jpg".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
image_inputs: Some(ImageListRef::Urls(urls.clone())),
|
||||
..base_seedream_4p5_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else {
|
||||
@@ -376,7 +376,7 @@ mod tests {
|
||||
fn media_token_image_inputs_is_edit_mode() {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_seedream_4p5_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
|
||||
|
||||
+12
-12
@@ -12,27 +12,27 @@ use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lit
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftSeedream5Lite<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub struct PlanArtcraftSeedream5Lite {
|
||||
pub prompt: Option<String>,
|
||||
/// Input images for image editing. None means text-to-image mode.
|
||||
pub image_inputs: Option<&'a Vec<MediaFileToken>>,
|
||||
pub image_inputs: Option<Vec<MediaFileToken>>,
|
||||
pub image_size: Option<BytedanceSeedream5LiteMultiFunctionImageGenImageSize>,
|
||||
pub num_images: BytedanceSeedream5LiteMultiFunctionImageGenNumImages,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_seedream_5_lite<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
|
||||
pub fn plan_generate_image_artcraft_seedream_5_lite(
|
||||
request: &GenerateImageRequest,
|
||||
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
let is_edit_mode = request.image_inputs.is_some();
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs)?;
|
||||
let image_inputs = resolve_image_list_ref(request.image_inputs.clone())?;
|
||||
let image_size = plan_image_size(request.aspect_ratio, is_edit_mode, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(ImageGenerationPlan::ArtcraftSeedream5Lite(PlanArtcraftSeedream5Lite {
|
||||
prompt: request.prompt,
|
||||
prompt: request.prompt.clone(),
|
||||
image_inputs,
|
||||
image_size,
|
||||
num_images,
|
||||
@@ -40,9 +40,9 @@ pub fn plan_generate_image_artcraft_seedream_5_lite<'a>(
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef>,
|
||||
) -> Result<Option<Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
@@ -167,7 +167,7 @@ mod tests {
|
||||
let tokens = vec![];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
|
||||
..base_seedream_5_lite_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftSeedream5Lite(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream5Lite") };
|
||||
|
||||
+1
@@ -4,6 +4,7 @@ pub mod plan_generate_image_fal_flux_pro_1p1;
|
||||
pub mod plan_generate_image_fal_flux_pro_1p1_ultra;
|
||||
pub mod plan_generate_image_fal_gpt_image_1;
|
||||
pub mod plan_generate_image_fal_gpt_image_1p5;
|
||||
pub mod plan_generate_image_fal_gpt_image_2;
|
||||
pub mod plan_generate_image_fal_seedream_4;
|
||||
pub mod plan_generate_image_fal_seedream_4p5;
|
||||
pub mod plan_generate_image_fal_seedream_5_lite;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user