mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-30 15:25:57 -04:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9653a1d21 | ||
|
|
42c867cfea |
@@ -42,21 +42,6 @@ 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`)
|
||||
@@ -68,10 +53,7 @@ sub-modules can reach them via `use super::*`.
|
||||
## Imports
|
||||
|
||||
- Group imports: std, external crates, internal workspace crates, `crate::` imports
|
||||
- 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.
|
||||
- Use fully qualified paths for one-off references; `use` for repeated references
|
||||
- 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,28 +10,10 @@
|
||||
## Test Patterns
|
||||
|
||||
- Tests live in `#[cfg(test)] mod tests { ... }` at the bottom of each file
|
||||
- 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
|
||||
- Group related tests in sub-modules: `mod explicit_checks`, `mod mechanical_checks`
|
||||
- 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
|
||||
@@ -39,14 +21,6 @@ constants first, then tests, then helpers at the end of that sub-module.
|
||||
- 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
|
||||
|
||||
+22
-33
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n jobs.attempt_count,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.product_category as `product_category: enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory`,\n jobs.inference_category as `inference_category: enums::by_table::generic_inference_jobs::inference_category::InferenceCategory`,\n jobs.maybe_model_type,\n jobs.maybe_model_token,\n jobs.maybe_raw_inference_text,\n jobs.maybe_inference_args,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n\n jobs.frontend_failure_category as `maybe_frontend_failure_category: enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory`,\n jobs.failure_reason,\n\n jobs.on_success_result_entity_type as maybe_result_entity_type,\n jobs.on_success_result_entity_token as maybe_result_entity_token,\n jobs.on_success_result_batch_token as maybe_result_batch_token,\n\n model_weights.title as maybe_model_weights_title,\n tts_models.title as maybe_tts_model_title,\n voice_conversion_models.title as maybe_voice_conversion_model_title,\n\n tts_results.public_bucket_wav_audio_path as maybe_tts_public_bucket_path,\n voice_conversion_results.public_bucket_hash as maybe_voice_conversion_public_bucket_hash,\n\n media_files.public_bucket_directory_hash as maybe_media_file_public_bucket_directory_hash,\n media_files.maybe_public_bucket_prefix as maybe_media_file_public_bucket_prefix,\n media_files.maybe_public_bucket_extension as maybe_media_file_public_bucket_extension,\n\n jobs.assigned_worker as maybe_assigned_worker,\n jobs.assigned_cluster as maybe_assigned_cluster,\n\n jobs.is_keepalive_required,\n\n jobs.created_at,\n jobs.updated_at,\n\n jobs.first_started_at as maybe_first_started_at,\n jobs.successfully_completed_at as maybe_successfully_completed_at,\n\n NOW() as database_clock\n\nFROM generic_inference_jobs as jobs\n\nLEFT OUTER JOIN model_weights ON jobs.maybe_model_token = model_weights.token\nLEFT OUTER JOIN tts_models ON jobs.maybe_model_token = tts_models.token\nLEFT OUTER JOIN voice_conversion_models ON jobs.maybe_model_token = voice_conversion_models.token\n\nLEFT OUTER JOIN tts_results ON jobs.on_success_result_entity_token = tts_results.token\nLEFT OUTER JOIN voice_conversion_results ON jobs.on_success_result_entity_token = voice_conversion_results.token\nLEFT OUTER JOIN media_files ON jobs.on_success_result_entity_token = media_files.token\n\nWHERE jobs.token = ?\n ",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n jobs.attempt_count,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.product_category as `product_category: enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory`,\n jobs.inference_category as `inference_category: enums::by_table::generic_inference_jobs::inference_category::InferenceCategory`,\n jobs.maybe_model_type,\n jobs.maybe_model_token,\n jobs.maybe_raw_inference_text,\n jobs.maybe_inference_args,\n\n jobs.frontend_failure_category as `maybe_frontend_failure_category: enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory`,\n jobs.failure_reason,\n\n jobs.on_success_result_entity_type as maybe_result_entity_type,\n jobs.on_success_result_entity_token as maybe_result_entity_token,\n jobs.on_success_result_batch_token as maybe_result_batch_token,\n\n model_weights.title as maybe_model_weights_title,\n tts_models.title as maybe_tts_model_title,\n voice_conversion_models.title as maybe_voice_conversion_model_title,\n\n tts_results.public_bucket_wav_audio_path as maybe_tts_public_bucket_path,\n voice_conversion_results.public_bucket_hash as maybe_voice_conversion_public_bucket_hash,\n\n media_files.public_bucket_directory_hash as maybe_media_file_public_bucket_directory_hash,\n media_files.maybe_public_bucket_prefix as maybe_media_file_public_bucket_prefix,\n media_files.maybe_public_bucket_extension as maybe_media_file_public_bucket_extension,\n\n jobs.assigned_worker as maybe_assigned_worker,\n jobs.assigned_cluster as maybe_assigned_cluster,\n\n jobs.is_keepalive_required,\n\n jobs.created_at,\n jobs.updated_at,\n\n jobs.first_started_at as maybe_first_started_at,\n jobs.successfully_completed_at as maybe_successfully_completed_at,\n\n NOW() as database_clock\n\nFROM generic_inference_jobs as jobs\n\nLEFT OUTER JOIN model_weights ON jobs.maybe_model_token = model_weights.token\nLEFT OUTER JOIN tts_models ON jobs.maybe_model_token = tts_models.token\nLEFT OUTER JOIN voice_conversion_models ON jobs.maybe_model_token = voice_conversion_models.token\n\nLEFT OUTER JOIN tts_results ON jobs.on_success_result_entity_token = tts_results.token\nLEFT OUTER JOIN voice_conversion_results ON jobs.on_success_result_entity_token = voice_conversion_results.token\nLEFT OUTER JOIN media_files ON jobs.on_success_result_entity_token = media_files.token\n\nWHERE jobs.token = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -125,16 +125,6 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "maybe_frontend_failure_category: enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -144,7 +134,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"ordinal": 13,
|
||||
"name": "failure_reason",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -154,7 +144,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"ordinal": 14,
|
||||
"name": "maybe_result_entity_type",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -164,7 +154,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"ordinal": 15,
|
||||
"name": "maybe_result_entity_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -174,7 +164,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"ordinal": 16,
|
||||
"name": "maybe_result_batch_token",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -184,7 +174,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"ordinal": 17,
|
||||
"name": "maybe_model_weights_title",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -194,7 +184,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"ordinal": 18,
|
||||
"name": "maybe_tts_model_title",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -204,7 +194,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"ordinal": 19,
|
||||
"name": "maybe_voice_conversion_model_title",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -214,7 +204,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"ordinal": 20,
|
||||
"name": "maybe_tts_public_bucket_path",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -224,7 +214,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"ordinal": 21,
|
||||
"name": "maybe_voice_conversion_public_bucket_hash",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -234,7 +224,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"ordinal": 22,
|
||||
"name": "maybe_media_file_public_bucket_directory_hash",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -244,7 +234,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"ordinal": 23,
|
||||
"name": "maybe_media_file_public_bucket_prefix",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -254,7 +244,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"ordinal": 24,
|
||||
"name": "maybe_media_file_public_bucket_extension",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -264,7 +254,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 26,
|
||||
"ordinal": 25,
|
||||
"name": "maybe_assigned_worker",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -274,7 +264,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 27,
|
||||
"ordinal": 26,
|
||||
"name": "maybe_assigned_cluster",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
@@ -284,7 +274,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 28,
|
||||
"ordinal": 27,
|
||||
"name": "is_keepalive_required",
|
||||
"type_info": {
|
||||
"type": "Tiny",
|
||||
@@ -294,7 +284,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 29,
|
||||
"ordinal": 28,
|
||||
"name": "created_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -304,7 +294,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 30,
|
||||
"ordinal": 29,
|
||||
"name": "updated_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -314,7 +304,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 31,
|
||||
"ordinal": 30,
|
||||
"name": "maybe_first_started_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -324,7 +314,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 32,
|
||||
"ordinal": 31,
|
||||
"name": "maybe_successfully_completed_at",
|
||||
"type_info": {
|
||||
"type": "Timestamp",
|
||||
@@ -334,7 +324,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 33,
|
||||
"ordinal": 32,
|
||||
"name": "database_clock",
|
||||
"type_info": {
|
||||
"type": "Datetime",
|
||||
@@ -375,7 +365,6 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -384,5 +373,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "6948edca3f06643605695665df51cc94f897e2cb9a2d6906910d58d4d56ee73e"
|
||||
"hash": "a2c9e2f61eba666254f8432b3c50a0a7738ee3b3faa77c5281bc8e4af96159b9"
|
||||
}
|
||||
@@ -62,7 +62,6 @@ 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
|
||||
|
||||
@@ -76,4 +75,3 @@ 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
-2
@@ -11469,14 +11469,12 @@ name = "support-tool"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"artcraft_client",
|
||||
"clap 4.5.60",
|
||||
"easyenv",
|
||||
"errors",
|
||||
"log",
|
||||
"seedance2pro_client",
|
||||
"serde_json",
|
||||
"tokens",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
ALTER TABLE prompts
|
||||
DROP COLUMN maybe_quality;
|
||||
@@ -1,8 +0,0 @@
|
||||
-- noinspection SqlDialectInspectionForFile
|
||||
-- noinspection SqlNoDataSourceInspectionForFile
|
||||
-- noinspection SqlResolveForFile
|
||||
|
||||
-- Add quality parameter to prompts table.
|
||||
-- Represented by the enum `CommonQuality` (high, medium, low).
|
||||
ALTER TABLE prompts
|
||||
ADD COLUMN maybe_quality VARCHAR(16) DEFAULT NULL;
|
||||
@@ -109,7 +109,7 @@ CREATE TABLE generic_inference_jobs (
|
||||
-- If we pre-constructed a prompt, this will be copied to the resultant media generated.
|
||||
-- This is especially important for Fal-related jobs and endpoints. We need this for keeping
|
||||
-- track of all the prompt details, context images, etc.
|
||||
maybe_prompt_token VARCHAR(32) DEFAULT NULL,
|
||||
maybe_prompt_token VARCHAR(30) DEFAULT NULL,
|
||||
|
||||
-- A migration flag we can control from the backend API service which will tell
|
||||
-- the worker where to store the results. If false, GCP. If true, AWS.
|
||||
|
||||
@@ -68,10 +68,6 @@ CREATE TABLE prompts (
|
||||
-- The resolution for the prompt (eg. four_k, 720p, etc.)
|
||||
maybe_resolution VARCHAR(16) DEFAULT NULL,
|
||||
|
||||
-- Represented by the enum `CommonQuality`.
|
||||
-- The quality setting (eg. high, medium, low)
|
||||
maybe_quality VARCHAR(16) DEFAULT NULL,
|
||||
|
||||
-- The duration in seconds
|
||||
maybe_duration_seconds INT UNSIGNED DEFAULT NULL,
|
||||
|
||||
|
||||
+1
-2
@@ -15,12 +15,11 @@ body:
|
||||
type: json
|
||||
content: |
|
||||
{
|
||||
"model": "gpt_image_2",
|
||||
"model": "seedream_5_lite",
|
||||
"provider": "artcraft",
|
||||
"generation_mode": {"type": "text_to_image" },
|
||||
"aspect_ratio": "wide_sixteen_by_nine",
|
||||
"resolution": "two_k",
|
||||
"quality": "low",
|
||||
"image_batch_count": 1
|
||||
}
|
||||
order: 2000
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
$kind: http-request
|
||||
name: Batch Get Prompt
|
||||
url: "{{api_host}}/v1/prompts/prompt_8q8fwcmh93nn8pjb2szeqbxgz"
|
||||
method: GET
|
||||
headers:
|
||||
- key: Accept
|
||||
value: application/json
|
||||
- key: Session
|
||||
value: ""
|
||||
disabled: true
|
||||
order: 2171327660164107
|
||||
@@ -1,59 +0,0 @@
|
||||
# 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
|
||||
+2
-8
@@ -3,14 +3,13 @@ use utoipa::ToSchema;
|
||||
|
||||
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_provider::GenerationProvider;
|
||||
|
||||
pub const ESTIMATE_IMAGE_COST_PATH: &str = "/v1/generate/cost_estimate/image";
|
||||
|
||||
/// Request body for the image cost estimate endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EstimateImageCostRequest {
|
||||
/// The image model to estimate costs for.
|
||||
pub model: CommonImageModel,
|
||||
@@ -30,15 +29,12 @@ pub struct EstimateImageCostRequest {
|
||||
/// Optional resolution.
|
||||
pub resolution: Option<CommonResolution>,
|
||||
|
||||
// Optional quality.
|
||||
pub quality: Option<CommonQuality>,
|
||||
|
||||
/// Number of images to generate in parallel.
|
||||
pub image_batch_count: Option<u16>,
|
||||
}
|
||||
|
||||
/// Describes the type of image generation being requested.
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum GenerationMode {
|
||||
/// No input images — generate from prompt only.
|
||||
@@ -98,7 +94,6 @@ mod tests {
|
||||
generation_mode: GenerationMode::TextToImage,
|
||||
aspect_ratio: None,
|
||||
resolution: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
};
|
||||
let serialized = serde_json::to_string(&request).unwrap();
|
||||
@@ -113,7 +108,6 @@ mod tests {
|
||||
generation_mode: GenerationMode::ImageEdit { count: 2 },
|
||||
aspect_ratio: None,
|
||||
resolution: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
};
|
||||
let serialized = serde_json::to_string(&request).unwrap();
|
||||
|
||||
+1
-15
@@ -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(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct Seedance2p0MultiFunctionVideoGenRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
@@ -37,9 +37,6 @@ 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>,
|
||||
|
||||
@@ -70,17 +67,6 @@ 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,
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
use crate::common::responses::job_details::{JobDetailsLipsyncRequest, JobDetailsLivePortraitRequest};
|
||||
use crate::common::responses::media_links::MediaLinks;
|
||||
use chrono::{DateTime, Utc};
|
||||
use enums::api_safe::by_table::generic_inference_jobs::frontend_failure_category_for_api_clients::FrontendFailureCategoryForApiClients;
|
||||
use enums::api_safe::by_table::generic_inference_jobs::frontend_failure_category_for_old_clients::FrontendFailureCategoryForOldClients;
|
||||
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
use enums::api_safe::by_table::generic_inference_jobs::frontend_failure_category_for_api_clients::FrontendFailureCategoryForApiClients;
|
||||
use enums::api_safe::by_table::generic_inference_jobs::frontend_failure_category_for_old_clients::FrontendFailureCategoryForOldClients;
|
||||
use tokens::tokens::batch_generations::BatchGenerationToken;
|
||||
|
||||
pub const LIST_SESSION_JOBS_URL_PATH: &str = "/v1/jobs/session";
|
||||
|
||||
@@ -54,9 +53,6 @@ pub struct ListSessionJobsItem {
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct ListSessionRequestDetailsResponse {
|
||||
pub inference_category: InferenceCategory,
|
||||
|
||||
pub maybe_prompt_token: Option<PromptToken>,
|
||||
|
||||
pub maybe_model_type: Option<String>,
|
||||
pub maybe_model_token: Option<String>,
|
||||
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// Shared request body for both the video cost estimate and video generation endpoints.
|
||||
#[derive(Clone, Serialize, Deserialize, ToSchema, Debug)]
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||
pub struct OmniGenVideoCostAndGenerateRequest {
|
||||
/// REQUIRED (even if marked optional)
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
|
||||
-7
@@ -6,12 +6,5 @@ use utoipa::ToSchema;
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct OmniGenVideoGenerateResponse {
|
||||
pub success: bool,
|
||||
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
|
||||
/// All job tokens created by this request (including the primary).
|
||||
/// For single-job requests this will contain just one element matching
|
||||
/// `inference_job_token`. For batch requests (eg. Seedance2Pro with
|
||||
/// multiple order IDs) this will contain all of them.
|
||||
pub all_job_tokens: Vec<InferenceJobToken>,
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
use crate::prompts::get_prompt::GetPromptImageContextItem;
|
||||
use chrono::{DateTime, Utc};
|
||||
use enums::by_table::prompts::prompt_type::PromptType;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_generation_mode::CommonGenerationMode;
|
||||
use enums::common::generation::common_model_class::CommonModelClass;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
pub const BATCH_GET_PROMPTS_PATH: &str = "/v1/prompt/batch";
|
||||
|
||||
#[derive(Deserialize, ToSchema, IntoParams)]
|
||||
pub struct BatchGetPromptsQuery {
|
||||
/// Prompt tokens to look up.
|
||||
pub tokens: HashSet<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct BatchGetPromptsResponse {
|
||||
pub success: bool,
|
||||
pub prompts: Vec<BatchPromptInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct BatchPromptInfo {
|
||||
pub token: PromptToken,
|
||||
|
||||
/// The type of model used
|
||||
pub maybe_model_type: Option<CommonModelType>,
|
||||
|
||||
/// The class of model used
|
||||
pub maybe_model_class: Option<CommonModelClass>,
|
||||
|
||||
/// The service provider used
|
||||
pub maybe_generation_provider: Option<GenerationProvider>,
|
||||
|
||||
/// Positive prompt (technically optional, but usually present)
|
||||
pub maybe_positive_prompt: Option<String>,
|
||||
|
||||
/// Negative prompt (optional)
|
||||
pub maybe_negative_prompt: Option<String>,
|
||||
|
||||
/// OPTIONAL. The generation mode (eg. keyframe, reference, inpaint, etc.)
|
||||
pub maybe_generation_mode: Option<CommonGenerationMode>,
|
||||
|
||||
/// OPTIONAL. The aspect ratio (eg. square, auto, wide_three_by_two, etc.)
|
||||
pub maybe_aspect_ratio: Option<CommonAspectRatio>,
|
||||
|
||||
/// OPTIONAL. The resolution (eg. one_k, two_k, four_k, etc.)
|
||||
pub maybe_resolution: Option<CommonResolution>,
|
||||
|
||||
/// OPTIONAL. The number of outputs to generate (0-255).
|
||||
pub maybe_batch_count: Option<u8>,
|
||||
|
||||
/// OPTIONAL. Whether to generate audio.
|
||||
pub maybe_generate_audio: Option<bool>,
|
||||
|
||||
/// OPTIONAL. The duration in seconds.
|
||||
pub maybe_duration_seconds: Option<u32>,
|
||||
|
||||
/// Context images (optional)
|
||||
pub maybe_context_images: Option<Vec<GetPromptImageContextItem>>,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -27,6 +27,13 @@ pub struct GetPromptSuccessResponse {
|
||||
pub prompt: PromptInfo,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct GetPromptImageContextItem {
|
||||
pub media_token: MediaFileToken,
|
||||
pub semantic: PromptContextSemanticType,
|
||||
pub media_links: MediaLinks,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct PromptInfo {
|
||||
pub token: PromptToken,
|
||||
@@ -123,13 +130,6 @@ pub struct PromptInfo {
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct GetPromptImageContextItem {
|
||||
pub media_token: MediaFileToken,
|
||||
pub semantic: PromptContextSemanticType,
|
||||
pub media_links: MediaLinks,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct PromptInfoModeratorFields {
|
||||
/// How many milliseconds it took to run generation.
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
pub mod batch_get_prompts;
|
||||
pub mod create_prompt;
|
||||
pub mod get_prompt;
|
||||
-1
@@ -1 +0,0 @@
|
||||
pub mod omni_gen_image;
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
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,2 +1 @@
|
||||
pub mod image;
|
||||
pub mod video;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use cloudflare_errors::cloudflare_error::CloudflareError;
|
||||
use errors::AnyhowError;
|
||||
use reqwest::StatusCode;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io;
|
||||
@@ -31,12 +30,6 @@ pub enum ApiError {
|
||||
backend_hostname: Option<String>,
|
||||
},
|
||||
|
||||
/// An uncategorized bad HTTP response with status code and body.
|
||||
UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: StatusCode,
|
||||
body: String,
|
||||
},
|
||||
|
||||
/// Cloudflare errors.
|
||||
CloudflareError(CloudflareError),
|
||||
|
||||
@@ -73,8 +66,6 @@ impl Display for ApiError {
|
||||
ApiError::TooManyRequests(msg) => write!(f, "Too many requests to Storyteller backend: {}", msg),
|
||||
ApiError::InternalServerError {body, backend_hostname} =>
|
||||
write!(f, "Internal Server Error; backend hostname: {:?} ; body: {}; ", backend_hostname, body),
|
||||
ApiError::UncategorizedBadResponseWithStatusAndBody { status_code, body } =>
|
||||
write!(f, "Uncategorized bad response with status code {} and body: {}", status_code, body),
|
||||
// Server response handling errors
|
||||
ApiError::DeserializationError(error) => write!(f, "Deserialization error: {}", error),
|
||||
// Network errors
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use log::{info, warn};
|
||||
use reqwest::Client;
|
||||
use url::Url;
|
||||
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::endpoints::media_files::get_media_file::{get_media_file, GetMediaFileSuccessResponse};
|
||||
use crate::error::api_error::ApiError;
|
||||
use crate::error::client_error::ClientError;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
|
||||
pub struct DownloadMediaFileArgs<'a, P: AsRef<Path>> {
|
||||
pub media_token: &'a MediaFileToken,
|
||||
pub api_host: &'a ApiHost,
|
||||
pub download_path: DownloadPath<P>,
|
||||
}
|
||||
|
||||
pub enum DownloadPath<P: AsRef<Path>> {
|
||||
/// Write the file to this exact path.
|
||||
ExactFilename(P),
|
||||
/// Write the file into this directory, generating a filename from the CDN URL extension.
|
||||
Directory(P),
|
||||
}
|
||||
|
||||
pub struct DownloadMediaFileResult {
|
||||
/// The path the file was written to.
|
||||
pub downloaded_file_path: PathBuf,
|
||||
|
||||
/// The full media file response from the API.
|
||||
pub media_file_response: GetMediaFileSuccessResponse,
|
||||
|
||||
/// Size of downloaded file in bytes.
|
||||
pub filesize_bytes: usize,
|
||||
}
|
||||
|
||||
pub async fn download_media_file<P: AsRef<Path>>(
|
||||
args: DownloadMediaFileArgs<'_, P>,
|
||||
) -> Result<DownloadMediaFileResult, StorytellerError> {
|
||||
let DownloadMediaFileArgs {
|
||||
media_token,
|
||||
api_host,
|
||||
download_path
|
||||
} = args;
|
||||
|
||||
// 1. Fetch media file info from the API.
|
||||
let response = get_media_file(api_host, media_token).await?;
|
||||
let media_class = &response.media_file.media_class;
|
||||
let cdn_url = &response.media_file.media_links.cdn_url;
|
||||
|
||||
info!("Downloading media file {} of class {} from CDN: {}",
|
||||
media_token.as_str(), media_class.to_str(), cdn_url);
|
||||
|
||||
// 2. Determine the output file path.
|
||||
let output_path = match &download_path {
|
||||
DownloadPath::ExactFilename(path) => path.as_ref().to_path_buf(),
|
||||
DownloadPath::Directory(dir) => {
|
||||
let filename = derive_filename_from_url(cdn_url, &media_token);
|
||||
dir.as_ref().join(filename)
|
||||
}
|
||||
};
|
||||
|
||||
// 3. Download the bytes from the CDN.
|
||||
let bytes = download_bytes(cdn_url).await?;
|
||||
|
||||
// 4. Write to disk.
|
||||
let mut file = fs::File::create(&output_path)
|
||||
.map_err(|err| StorytellerError::Client(ClientError::IoError(err)))?;
|
||||
|
||||
file.write_all(&bytes)
|
||||
.map_err(|err| StorytellerError::Client(ClientError::IoError(err)))?;
|
||||
|
||||
file.flush()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::IoError(err)))?;
|
||||
|
||||
info!("Downloaded {} bytes to {:?}", bytes.len(), output_path);
|
||||
|
||||
Ok(DownloadMediaFileResult {
|
||||
downloaded_file_path: output_path,
|
||||
media_file_response: response,
|
||||
filesize_bytes: bytes.len(),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
async fn download_bytes(url: &Url) -> Result<Vec<u8>, StorytellerError> {
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::ReqwestError(err)))?;
|
||||
|
||||
let response = client.get(url.as_str())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Api(ApiError::OtherReqwestError(err)))?;
|
||||
|
||||
let status_code = response.status();
|
||||
|
||||
if !status_code.is_success() {
|
||||
let body = response.text().await.unwrap_or_else(|err| {
|
||||
warn!("Failed to retrieve response body: {}", err);
|
||||
"".to_string()
|
||||
});
|
||||
return Err(StorytellerError::Api(ApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code,
|
||||
body,
|
||||
}));
|
||||
}
|
||||
|
||||
let bytes = response.bytes()
|
||||
.await
|
||||
.map_err(|err| StorytellerError::Client(ClientError::ReqwestError(err)))?;
|
||||
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
|
||||
/// Derive a filename from the CDN URL's path extension, falling back to the media token.
|
||||
fn derive_filename_from_url(url: &Url, media_token: &MediaFileToken) -> String {
|
||||
let path = url.path();
|
||||
let extension = Path::new(path)
|
||||
.extension() // NB: without dot '.'
|
||||
.and_then(|ext| ext.to_str())
|
||||
.unwrap_or("bin");
|
||||
|
||||
format!("{}.{}", media_token.as_str(), extension)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn derive_filename_from_url_extracts_extension() {
|
||||
let url = Url::parse("https://cdn.example.com/files/abc123.png").unwrap();
|
||||
let token = MediaFileToken::new_from_str("mf_test123");
|
||||
assert_eq!(derive_filename_from_url(&url, &token), "mf_test123.png");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_filename_from_url_handles_no_extension() {
|
||||
let url = Url::parse("https://cdn.example.com/files/abc123").unwrap();
|
||||
let token = MediaFileToken::new_from_str("mf_test123");
|
||||
assert_eq!(derive_filename_from_url(&url, &token), "mf_test123.bin");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_filename_from_url_handles_query_params() {
|
||||
let url = Url::parse("https://cdn.example.com/files/abc123.mp4?token=xyz").unwrap();
|
||||
let token = MediaFileToken::new_from_str("mf_test123");
|
||||
assert_eq!(derive_filename_from_url(&url, &token), "mf_test123.mp4");
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
pub mod download_media_file;
|
||||
pub mod upload_media_file_from_file;
|
||||
@@ -17,8 +17,6 @@ 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")]
|
||||
|
||||
@@ -41,9 +41,6 @@ pub enum CommonVideoModel {
|
||||
#[serde(rename = "seedance_2p0_fast")]
|
||||
Seedance2p0Fast,
|
||||
|
||||
#[serde(rename = "happy_horse_1p0")]
|
||||
HappyHorse1p0,
|
||||
|
||||
#[serde(rename = "sora_2")]
|
||||
Sora2,
|
||||
|
||||
|
||||
@@ -15,9 +15,6 @@ 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),
|
||||
|
||||
@@ -33,7 +30,6 @@ 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,7 +1,5 @@
|
||||
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 {
|
||||
@@ -24,9 +22,6 @@ 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),
|
||||
|
||||
@@ -48,18 +43,6 @@ 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 {}
|
||||
@@ -67,9 +50,6 @@ 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)
|
||||
}
|
||||
@@ -91,18 +71,6 @@ 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-210
@@ -1,210 +0,0 @@
|
||||
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
@@ -4,7 +4,6 @@ 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;
|
||||
|
||||
-209
@@ -1,209 +0,0 @@
|
||||
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
@@ -4,7 +4,6 @@ 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;
|
||||
|
||||
-145
@@ -1,145 +0,0 @@
|
||||
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.
|
||||
}
|
||||
}
|
||||
-1
@@ -4,7 +4,6 @@ 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;
|
||||
|
||||
-175
@@ -1,175 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
-1
@@ -4,7 +4,6 @@ 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;
|
||||
|
||||
+5
-10
@@ -16,7 +16,6 @@ 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;
|
||||
@@ -30,7 +29,6 @@ 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;
|
||||
@@ -102,20 +100,18 @@ impl GenerateImageRequest {
|
||||
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),
|
||||
_ => Err(ArtcraftRouterError::UnsupportedModel(format!("{:?}", self.model))),
|
||||
CommonImageModel::QwenEdit2511Angles => plan_generate_image_artcraft_qwen_edit_2511_angles(self),
|
||||
CommonImageModel::Flux2LoraAngles => plan_generate_image_artcraft_flux_2_lora_angles(self),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,13 +123,12 @@ impl GenerateImageRequest {
|
||||
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::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),
|
||||
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),
|
||||
_ => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "provider",
|
||||
|
||||
-22
@@ -6,7 +6,6 @@ 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;
|
||||
@@ -21,7 +20,6 @@ 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;
|
||||
@@ -34,7 +32,6 @@ 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;
|
||||
@@ -49,7 +46,6 @@ 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;
|
||||
@@ -64,7 +60,6 @@ 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;
|
||||
@@ -79,7 +74,6 @@ 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;
|
||||
@@ -95,7 +89,6 @@ pub enum ImageGenerationPlan {
|
||||
ArtcraftFluxPro11Ultra(PlanArtcraftFluxPro11Ultra),
|
||||
ArtcraftGptImage1(PlanArtcraftGptImage1),
|
||||
ArtcraftGptImage1p5(PlanArtcraftGptImage1p5),
|
||||
ArtcraftGptImage2(PlanArtcraftGptImage2),
|
||||
ArtcraftNanaBanana(PlanArtcraftNanaBanana),
|
||||
ArtcraftNanaBanana2(PlanArtcraftNanaBanana2),
|
||||
ArtcraftNanaBananaPro(PlanArtcraftNanaBananaPro),
|
||||
@@ -110,7 +103,6 @@ pub enum ImageGenerationPlan {
|
||||
FalFluxPro11Ultra(PlanFalFluxPro11Ultra),
|
||||
FalGptImage1(PlanFalGptImage1),
|
||||
FalGptImage1p5(PlanFalGptImage1p5),
|
||||
FalGptImage2(PlanFalGptImage2),
|
||||
FalSeedream4(PlanFalSeedream4),
|
||||
FalSeedream4p5(PlanFalSeedream4p5),
|
||||
FalSeedream5Lite(PlanFalSeedream5Lite),
|
||||
@@ -149,10 +141,6 @@ impl ImageGenerationPlan {
|
||||
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
|
||||
@@ -209,10 +197,6 @@ impl ImageGenerationPlan {
|
||||
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
|
||||
@@ -260,9 +244,6 @@ impl ImageGenerationPlan {
|
||||
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)
|
||||
}
|
||||
@@ -305,9 +286,6 @@ impl ImageGenerationPlan {
|
||||
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,7 +4,6 @@ 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;
|
||||
|
||||
-435
@@ -1,435 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
-1
@@ -4,7 +4,6 @@ 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;
|
||||
|
||||
-535
@@ -1,535 +0,0 @@
|
||||
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 fal_client::requests::webhook::image::edit::enqueue_gpt_image_2_edit_image_webhook::{
|
||||
EnqueueGptImage2EditImageNumImages, EnqueueGptImage2EditImageQuality,
|
||||
EnqueueGptImage2EditImageSize,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_gpt_image_2_text_to_image_webhook::{
|
||||
EnqueueGptImage2TextToImageNumImages, EnqueueGptImage2TextToImageQuality,
|
||||
EnqueueGptImage2TextToImageSize,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalGptImage2NumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
/// Quality is shared between t2i and edit (Low / Medium / High).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalGptImage2Quality {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
/// GPT Image 2 supports six image sizes.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum FalGptImage2ImageSize {
|
||||
SquareHd,
|
||||
Square,
|
||||
Portrait4x3,
|
||||
Portrait16x9,
|
||||
Landscape4x3,
|
||||
Landscape16x9,
|
||||
Auto,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanFalGptImage2 {
|
||||
pub prompt: Option<String>,
|
||||
pub image_urls: Vec<String>,
|
||||
pub image_size: Option<FalGptImage2ImageSize>,
|
||||
pub quality: FalGptImage2Quality,
|
||||
pub num_images: FalGptImage2NumImages,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_fal_gpt_image_2(
|
||||
request: &GenerateImageRequest,
|
||||
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
let image_urls = 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::FalGptImage2(PlanFalGptImage2 {
|
||||
prompt: request.prompt.clone(),
|
||||
image_urls,
|
||||
image_size,
|
||||
quality,
|
||||
num_images,
|
||||
}))
|
||||
}
|
||||
|
||||
fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef>,
|
||||
) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(vec![]),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls.clone()),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_quality(quality: Option<CommonQuality>) -> FalGptImage2Quality {
|
||||
match quality {
|
||||
Some(CommonQuality::Low) => FalGptImage2Quality::Low,
|
||||
Some(CommonQuality::Medium) => FalGptImage2Quality::Medium,
|
||||
Some(CommonQuality::High) => FalGptImage2Quality::High,
|
||||
None => FalGptImage2Quality::High,
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_image_size(aspect_ratio: Option<CommonAspectRatio>) -> Option<FalGptImage2ImageSize> {
|
||||
use FalGptImage2ImageSize 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<FalGptImage2NumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FalGptImage2NumImages::One),
|
||||
2 => Ok(FalGptImage2NumImages::Two),
|
||||
3 => Ok(FalGptImage2NumImages::Three),
|
||||
4 => Ok(FalGptImage2NumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FalGptImage2NumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl FalGptImage2NumImages {
|
||||
pub fn to_t2i(self) -> EnqueueGptImage2TextToImageNumImages {
|
||||
use EnqueueGptImage2TextToImageNumImages as T;
|
||||
match self {
|
||||
Self::One => T::One,
|
||||
Self::Two => T::Two,
|
||||
Self::Three => T::Three,
|
||||
Self::Four => T::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueGptImage2EditImageNumImages {
|
||||
use EnqueueGptImage2EditImageNumImages as E;
|
||||
match self {
|
||||
Self::One => E::One,
|
||||
Self::Two => E::Two,
|
||||
Self::Three => E::Three,
|
||||
Self::Four => E::Four,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
Self::Three => 3,
|
||||
Self::Four => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FalGptImage2Quality {
|
||||
pub fn to_t2i(self) -> EnqueueGptImage2TextToImageQuality {
|
||||
use EnqueueGptImage2TextToImageQuality as T;
|
||||
match self {
|
||||
Self::Low => T::Low,
|
||||
Self::Medium => T::Medium,
|
||||
Self::High => T::High,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueGptImage2EditImageQuality {
|
||||
use EnqueueGptImage2EditImageQuality as E;
|
||||
match self {
|
||||
Self::Low => E::Low,
|
||||
Self::Medium => E::Medium,
|
||||
Self::High => E::High,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FalGptImage2ImageSize {
|
||||
pub fn to_t2i(self) -> EnqueueGptImage2TextToImageSize {
|
||||
use EnqueueGptImage2TextToImageSize as T;
|
||||
match self {
|
||||
Self::SquareHd => T::SquareHd,
|
||||
Self::Square => T::Square,
|
||||
Self::Portrait4x3 => T::Portrait4x3,
|
||||
Self::Portrait16x9 => T::Portrait16x9,
|
||||
Self::Landscape4x3 => T::Landscape4x3,
|
||||
Self::Landscape16x9 => T::Landscape16x9,
|
||||
Self::Auto => T::SquareHd,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_edit(self) -> EnqueueGptImage2EditImageSize {
|
||||
use EnqueueGptImage2EditImageSize as E;
|
||||
match self {
|
||||
Self::SquareHd => E::SquareHd,
|
||||
Self::Square => E::Square,
|
||||
Self::Portrait4x3 => E::Portrait4x3,
|
||||
Self::Portrait16x9 => E::Portrait16x9,
|
||||
Self::Landscape4x3 => E::Landscape4x3,
|
||||
Self::Landscape16x9 => E::Landscape16x9,
|
||||
Self::Auto => E::Auto,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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_empty_urls() {
|
||||
let plan = build_plan(&GenerateImageRequest { image_inputs: None, ..base_request() });
|
||||
assert!(plan.image_urls.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_image_inputs_are_extracted() {
|
||||
let urls = vec![
|
||||
"https://example.com/a.jpg".to_string(),
|
||||
"https://example.com/b.jpg".to_string(),
|
||||
];
|
||||
let plan = build_plan(&GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(urls.clone())),
|
||||
..base_request()
|
||||
});
|
||||
assert_eq!(plan.image_urls, urls);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_token_inputs_return_error() {
|
||||
let result = plan_generate_image_fal_gpt_image_2(&GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(vec![])),
|
||||
..base_request()
|
||||
});
|
||||
assert!(matches!(result, Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Quality ──
|
||||
|
||||
mod quality_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn defaults_to_high() {
|
||||
let plan = build_plan(&base_request());
|
||||
assert!(matches!(plan.quality, FalGptImage2Quality::High));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn low() {
|
||||
let plan = build_plan(&GenerateImageRequest { quality: Some(CommonQuality::Low), ..base_request() });
|
||||
assert!(matches!(plan.quality, FalGptImage2Quality::Low));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn medium() {
|
||||
let plan = build_plan(&GenerateImageRequest { quality: Some(CommonQuality::Medium), ..base_request() });
|
||||
assert!(matches!(plan.quality, FalGptImage2Quality::Medium));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high() {
|
||||
let plan = build_plan(&GenerateImageRequest { quality: Some(CommonQuality::High), ..base_request() });
|
||||
assert!(matches!(plan.quality, FalGptImage2Quality::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(FalGptImage2ImageSize::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(FalGptImage2ImageSize::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(FalGptImage2ImageSize::Square)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn square_hd() {
|
||||
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::SquareHd), ..base_request() });
|
||||
assert!(matches!(plan.image_size, Some(FalGptImage2ImageSize::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(FalGptImage2ImageSize::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(FalGptImage2ImageSize::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(FalGptImage2ImageSize::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(FalGptImage2ImageSize::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, FalGptImage2NumImages::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_fal_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_fal_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, FalGptImage2NumImages::Four));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── as_u64 ──
|
||||
|
||||
#[test]
|
||||
fn num_images_as_u64() {
|
||||
assert_eq!(FalGptImage2NumImages::One.as_u64(), 1);
|
||||
assert_eq!(FalGptImage2NumImages::Two.as_u64(), 2);
|
||||
assert_eq!(FalGptImage2NumImages::Three.as_u64(), 3);
|
||||
assert_eq!(FalGptImage2NumImages::Four.as_u64(), 4);
|
||||
}
|
||||
|
||||
// ── Conversions to fal-client enqueue enums ──
|
||||
|
||||
mod conversion_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn num_images_to_t2i() {
|
||||
use EnqueueGptImage2TextToImageNumImages as T;
|
||||
assert!(matches!(FalGptImage2NumImages::One.to_t2i(), T::One));
|
||||
assert!(matches!(FalGptImage2NumImages::Two.to_t2i(), T::Two));
|
||||
assert!(matches!(FalGptImage2NumImages::Three.to_t2i(), T::Three));
|
||||
assert!(matches!(FalGptImage2NumImages::Four.to_t2i(), T::Four));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_to_edit() {
|
||||
use EnqueueGptImage2EditImageNumImages as E;
|
||||
assert!(matches!(FalGptImage2NumImages::One.to_edit(), E::One));
|
||||
assert!(matches!(FalGptImage2NumImages::Two.to_edit(), E::Two));
|
||||
assert!(matches!(FalGptImage2NumImages::Three.to_edit(), E::Three));
|
||||
assert!(matches!(FalGptImage2NumImages::Four.to_edit(), E::Four));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_to_t2i() {
|
||||
use EnqueueGptImage2TextToImageQuality as T;
|
||||
assert!(matches!(FalGptImage2Quality::Low.to_t2i(), T::Low));
|
||||
assert!(matches!(FalGptImage2Quality::Medium.to_t2i(), T::Medium));
|
||||
assert!(matches!(FalGptImage2Quality::High.to_t2i(), T::High));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quality_to_edit() {
|
||||
use EnqueueGptImage2EditImageQuality as E;
|
||||
assert!(matches!(FalGptImage2Quality::Low.to_edit(), E::Low));
|
||||
assert!(matches!(FalGptImage2Quality::Medium.to_edit(), E::Medium));
|
||||
assert!(matches!(FalGptImage2Quality::High.to_edit(), E::High));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_to_t2i() {
|
||||
use EnqueueGptImage2TextToImageSize as T;
|
||||
assert!(matches!(FalGptImage2ImageSize::SquareHd.to_t2i(), T::SquareHd));
|
||||
assert!(matches!(FalGptImage2ImageSize::Square.to_t2i(), T::Square));
|
||||
assert!(matches!(FalGptImage2ImageSize::Portrait4x3.to_t2i(), T::Portrait4x3));
|
||||
assert!(matches!(FalGptImage2ImageSize::Portrait16x9.to_t2i(), T::Portrait16x9));
|
||||
assert!(matches!(FalGptImage2ImageSize::Landscape4x3.to_t2i(), T::Landscape4x3));
|
||||
assert!(matches!(FalGptImage2ImageSize::Landscape16x9.to_t2i(), T::Landscape16x9));
|
||||
// Auto falls back to SquareHd for t2i (which doesn't have an Auto variant)
|
||||
assert!(matches!(FalGptImage2ImageSize::Auto.to_t2i(), T::SquareHd));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_to_edit() {
|
||||
use EnqueueGptImage2EditImageSize as E;
|
||||
assert!(matches!(FalGptImage2ImageSize::SquareHd.to_edit(), E::SquareHd));
|
||||
assert!(matches!(FalGptImage2ImageSize::Square.to_edit(), E::Square));
|
||||
assert!(matches!(FalGptImage2ImageSize::Portrait4x3.to_edit(), E::Portrait4x3));
|
||||
assert!(matches!(FalGptImage2ImageSize::Portrait16x9.to_edit(), E::Portrait16x9));
|
||||
assert!(matches!(FalGptImage2ImageSize::Landscape4x3.to_edit(), E::Landscape4x3));
|
||||
assert!(matches!(FalGptImage2ImageSize::Landscape16x9.to_edit(), E::Landscape16x9));
|
||||
assert!(matches!(FalGptImage2ImageSize::Auto.to_edit(), E::Auto));
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_plan(request: &GenerateImageRequest) -> PlanFalGptImage2 {
|
||||
let ImageGenerationPlan::FalGptImage2(plan) =
|
||||
plan_generate_image_fal_gpt_image_2(request).expect("plan should succeed")
|
||||
else {
|
||||
panic!("expected FalGptImage2 variant")
|
||||
};
|
||||
plan
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -58,10 +58,10 @@ mod tests {
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
|
||||
fn estimate_usd_cents(duration_seconds: u16) -> u64 {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
model: CommonVideoModel::Kling3p0Pro,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
|
||||
+2
-2
@@ -58,10 +58,10 @@ mod tests {
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
|
||||
fn estimate_usd_cents(duration_seconds: u16) -> u64 {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
model: CommonVideoModel::Kling3p0Standard,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
|
||||
+2
-2
@@ -61,10 +61,10 @@ mod tests {
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
|
||||
fn estimate_usd_cents(duration_seconds: u16) -> u64 {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance1p5Pro,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
|
||||
+30
-126
@@ -1,7 +1,7 @@
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0BatchCount;
|
||||
use seedance2pro_client::requests::generate_video::generate_video::{KinoviAspectRatio, KinoviBatchCount, KinoviGenerateVideoRequest, KinoviModelType, KinoviOutputResolution};
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use seedance2pro_client::requests::generate_video::generate_video::{KinoviBatchCount, GenerateVideoArgs, KinoviModelType, KinoviResolution};
|
||||
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::PlanArtcraftSeedance2p0;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
@@ -16,29 +16,30 @@ pub (crate) fn estimate_video_cost_artcraft_seedance2p0(
|
||||
Seedance2p0BatchCount::Four => KinoviBatchCount::Four,
|
||||
};
|
||||
|
||||
let output_resolution = plan.resolution.map(map_common_resolution_to_kinovi);
|
||||
// TODO: Make a better client that doesn't require this.
|
||||
// A dummy session is sufficient — cost estimation does not make any network calls.
|
||||
let dummy_session = Seedance2ProSession::from_cookies_string(String::new());
|
||||
|
||||
let request = KinoviGenerateVideoRequest {
|
||||
let args = GenerateVideoArgs {
|
||||
session: &dummy_session,
|
||||
model_type: KinoviModelType::Seedance2Pro,
|
||||
prompt: String::new(),
|
||||
aspect_ratio: KinoviAspectRatio::Square1x1,
|
||||
resolution: KinoviResolution::Square1x1,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
output_resolution,
|
||||
|
||||
// TODO: This is a cost factor
|
||||
reference_video_urls: None,
|
||||
|
||||
// NB: These do not contribute to costs in the Seedance2 integration
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = request.estimate_cost_in_usd_cents();
|
||||
let cost_in_usd_cents = args.estimate_cost_in_usd_cents();
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
@@ -50,30 +51,15 @@ pub (crate) fn estimate_video_cost_artcraft_seedance2p0(
|
||||
}
|
||||
}
|
||||
|
||||
fn map_common_resolution_to_kinovi(resolution: CommonResolution) -> KinoviOutputResolution {
|
||||
match resolution {
|
||||
CommonResolution::FourEightyP => KinoviOutputResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP => KinoviOutputResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP => KinoviOutputResolution::TenEightyP,
|
||||
// For resolutions that don't map directly, default to 720p
|
||||
_ => KinoviOutputResolution::SevenTwentyP,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
|
||||
fn estimate_usd_cents(
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
resolution: Option<CommonResolution>,
|
||||
) -> u64 {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
fn estimate_usd_cents(duration_seconds: u16, video_batch_count: u16) -> u64 {
|
||||
let request = GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
@@ -84,7 +70,7 @@ mod tests {
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
@@ -99,105 +85,23 @@ mod tests {
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
}
|
||||
|
||||
// -- 720p (default / legacy pricing: 250 credits/$1) --
|
||||
|
||||
#[test]
|
||||
fn pro_720p_batch_1() {
|
||||
assert_eq!(estimate_usd_cents(4, 1, Some(CommonResolution::SevenTwentyP)), 64);
|
||||
assert_eq!(estimate_usd_cents(5, 1, Some(CommonResolution::SevenTwentyP)), 80);
|
||||
assert_eq!(estimate_usd_cents(6, 1, Some(CommonResolution::SevenTwentyP)), 96);
|
||||
assert_eq!(estimate_usd_cents(7, 1, Some(CommonResolution::SevenTwentyP)), 112);
|
||||
assert_eq!(estimate_usd_cents(15, 1, Some(CommonResolution::SevenTwentyP)), 240);
|
||||
}
|
||||
fn test_estimate_cost_usd_cents() {
|
||||
// Batch 1:
|
||||
assert_eq!(estimate_usd_cents(4, 1), 64);
|
||||
assert_eq!(estimate_usd_cents(5, 1), 80);
|
||||
assert_eq!(estimate_usd_cents(6, 1), 96);
|
||||
assert_eq!(estimate_usd_cents(7, 1), 112);
|
||||
assert_eq!(estimate_usd_cents(15, 1), 240);
|
||||
|
||||
#[test]
|
||||
fn pro_720p_batch_2() {
|
||||
assert_eq!(estimate_usd_cents(4, 2, Some(CommonResolution::SevenTwentyP)), 128);
|
||||
assert_eq!(estimate_usd_cents(5, 2, Some(CommonResolution::SevenTwentyP)), 160);
|
||||
assert_eq!(estimate_usd_cents(15, 2, Some(CommonResolution::SevenTwentyP)), 480);
|
||||
}
|
||||
// Batch 2 = 2×
|
||||
assert_eq!(estimate_usd_cents(4, 2), 128);
|
||||
assert_eq!(estimate_usd_cents(5, 2), 160);
|
||||
assert_eq!(estimate_usd_cents(15, 2), 480);
|
||||
|
||||
#[test]
|
||||
fn pro_720p_batch_4() {
|
||||
assert_eq!(estimate_usd_cents(4, 4, Some(CommonResolution::SevenTwentyP)), 256);
|
||||
assert_eq!(estimate_usd_cents(5, 4, Some(CommonResolution::SevenTwentyP)), 320);
|
||||
assert_eq!(estimate_usd_cents(15, 4, Some(CommonResolution::SevenTwentyP)), 960);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_none_resolution_same_as_720p() {
|
||||
assert_eq!(
|
||||
estimate_usd_cents(5, 1, None),
|
||||
estimate_usd_cents(5, 1, Some(CommonResolution::SevenTwentyP)),
|
||||
);
|
||||
}
|
||||
|
||||
// -- 480p (new pricing: 193 credits/$1) --
|
||||
|
||||
#[test]
|
||||
fn pro_480p_batch_1() {
|
||||
assert_eq!(estimate_usd_cents(4, 1, Some(CommonResolution::FourEightyP)), 31);
|
||||
assert_eq!(estimate_usd_cents(5, 1, Some(CommonResolution::FourEightyP)), 39);
|
||||
assert_eq!(estimate_usd_cents(10, 1, Some(CommonResolution::FourEightyP)), 78);
|
||||
assert_eq!(estimate_usd_cents(15, 1, Some(CommonResolution::FourEightyP)), 117);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_480p_batch_2() {
|
||||
assert_eq!(estimate_usd_cents(5, 2, Some(CommonResolution::FourEightyP)), 78);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_480p_batch_4() {
|
||||
assert_eq!(estimate_usd_cents(5, 4, Some(CommonResolution::FourEightyP)), 155);
|
||||
}
|
||||
|
||||
// -- 1080p (new pricing: 193 credits/$1) --
|
||||
|
||||
#[test]
|
||||
fn pro_1080p_batch_1() {
|
||||
assert_eq!(estimate_usd_cents(4, 1, Some(CommonResolution::TenEightyP)), 187);
|
||||
assert_eq!(estimate_usd_cents(5, 1, Some(CommonResolution::TenEightyP)), 233);
|
||||
assert_eq!(estimate_usd_cents(10, 1, Some(CommonResolution::TenEightyP)), 466);
|
||||
assert_eq!(estimate_usd_cents(15, 1, Some(CommonResolution::TenEightyP)), 699);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_1080p_batch_2() {
|
||||
assert_eq!(estimate_usd_cents(5, 2, Some(CommonResolution::TenEightyP)), 466);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_1080p_batch_4() {
|
||||
assert_eq!(estimate_usd_cents(5, 4, Some(CommonResolution::TenEightyP)), 933);
|
||||
}
|
||||
|
||||
// -- Relative pricing --
|
||||
|
||||
#[test]
|
||||
fn pro_480p_cheaper_than_720p_cheaper_than_1080p() {
|
||||
let c480 = estimate_usd_cents(5, 1, Some(CommonResolution::FourEightyP));
|
||||
let c720 = estimate_usd_cents(5, 1, Some(CommonResolution::SevenTwentyP));
|
||||
let c1080 = estimate_usd_cents(5, 1, Some(CommonResolution::TenEightyP));
|
||||
assert!(c480 < c720, "480p ({}) should be cheaper than 720p ({})", c480, c720);
|
||||
assert!(c720 < c1080, "720p ({}) should be cheaper than 1080p ({})", c720, c1080);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_duration() {
|
||||
let c4 = estimate_usd_cents(4, 1, Some(CommonResolution::SevenTwentyP));
|
||||
let c10 = estimate_usd_cents(10, 1, Some(CommonResolution::SevenTwentyP));
|
||||
let c15 = estimate_usd_cents(15, 1, Some(CommonResolution::SevenTwentyP));
|
||||
assert!(c4 < c10);
|
||||
assert!(c10 < c15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_batch() {
|
||||
let b1 = estimate_usd_cents(5, 1, Some(CommonResolution::TenEightyP));
|
||||
let b2 = estimate_usd_cents(5, 2, Some(CommonResolution::TenEightyP));
|
||||
let b4 = estimate_usd_cents(5, 4, Some(CommonResolution::TenEightyP));
|
||||
assert!(b1 < b2);
|
||||
assert!(b2 < b4);
|
||||
// Batch 4 = 4×
|
||||
assert_eq!(estimate_usd_cents(4, 4), 256);
|
||||
assert_eq!(estimate_usd_cents(5, 4), 320);
|
||||
assert_eq!(estimate_usd_cents(15, 4), 960);
|
||||
}
|
||||
}
|
||||
|
||||
+42
-172
@@ -1,7 +1,7 @@
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0BatchCount;
|
||||
use seedance2pro_client::requests::generate_video::generate_video::{KinoviGenerateVideoRequest, KinoviBatchCount, KinoviModelType, KinoviOutputResolution, KinoviAspectRatio};
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use seedance2pro_client::requests::generate_video::generate_video::{KinoviBatchCount, GenerateVideoArgs, KinoviModelType, KinoviResolution};
|
||||
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::PlanArtcraftSeedance2p0;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
@@ -16,29 +16,26 @@ pub(crate) fn estimate_video_cost_artcraft_seedance2p0_fast(
|
||||
Seedance2p0BatchCount::Four => KinoviBatchCount::Four,
|
||||
};
|
||||
|
||||
let output_resolution = plan.resolution.map(map_common_resolution_to_kinovi);
|
||||
let dummy_session = Seedance2ProSession::from_cookies_string(String::new());
|
||||
|
||||
let request = KinoviGenerateVideoRequest {
|
||||
model_type: KinoviModelType::Seedance2Fast,
|
||||
let args = GenerateVideoArgs {
|
||||
session: &dummy_session,
|
||||
model_type: KinoviModelType::Seedance2Fast, // <-- Fast, not Pro
|
||||
prompt: String::new(),
|
||||
aspect_ratio: KinoviAspectRatio::Square1x1,
|
||||
resolution: KinoviResolution::Square1x1,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
output_resolution,
|
||||
|
||||
// TODO: This is a cost factor
|
||||
reference_video_urls: None,
|
||||
|
||||
// NB: These do not contribute to costs in the Seedance2 integration
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = request.estimate_cost_in_usd_cents();
|
||||
let cost_in_usd_cents = args.estimate_cost_in_usd_cents();
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
@@ -50,30 +47,15 @@ pub(crate) fn estimate_video_cost_artcraft_seedance2p0_fast(
|
||||
}
|
||||
}
|
||||
|
||||
fn map_common_resolution_to_kinovi(resolution: CommonResolution) -> KinoviOutputResolution {
|
||||
match resolution {
|
||||
CommonResolution::FourEightyP => KinoviOutputResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP => KinoviOutputResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP => KinoviOutputResolution::TenEightyP,
|
||||
// For resolutions that don't map directly, default to 720p
|
||||
_ => KinoviOutputResolution::SevenTwentyP,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
|
||||
fn estimate_usd_cents(
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
resolution: Option<CommonResolution>,
|
||||
) -> u64 {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
fn estimate_usd_cents(duration_seconds: u16, video_batch_count: u16) -> u64 {
|
||||
let request = GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance2p0Fast,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
@@ -84,7 +66,7 @@ mod tests {
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
@@ -99,43 +81,8 @@ mod tests {
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
}
|
||||
|
||||
fn estimate_usd_cents_downgrade(
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
resolution: Option<CommonResolution>,
|
||||
) -> u64 {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0Fast,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
negative_prompt: None,
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
generate_audio: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
idempotency_token: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
.estimate_costs()
|
||||
.cost_in_usd_cents
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
}
|
||||
|
||||
fn estimate_pro_usd_cents(
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
resolution: Option<CommonResolution>,
|
||||
) -> u64 {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
fn estimate_pro_usd_cents(duration_seconds: u16, video_batch_count: u16) -> u64 {
|
||||
let request = GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
@@ -146,7 +93,7 @@ mod tests {
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
@@ -161,133 +108,56 @@ mod tests {
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
}
|
||||
|
||||
// -- 720p (default / legacy pricing: 220 credits/$1) --
|
||||
|
||||
#[test]
|
||||
fn fast_720p_batch_1() {
|
||||
assert_eq!(estimate_usd_cents(4, 1, Some(CommonResolution::SevenTwentyP)), 51);
|
||||
assert_eq!(estimate_usd_cents(5, 1, Some(CommonResolution::SevenTwentyP)), 64);
|
||||
assert_eq!(estimate_usd_cents(10, 1, Some(CommonResolution::SevenTwentyP)), 127);
|
||||
assert_eq!(estimate_usd_cents(15, 1, Some(CommonResolution::SevenTwentyP)), 191);
|
||||
fn cost_is_present() {
|
||||
assert!(estimate_usd_cents(5, 1) > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_720p_batch_2() {
|
||||
assert_eq!(estimate_usd_cents(5, 2, Some(CommonResolution::SevenTwentyP)), 127);
|
||||
fn cost_batch_1() {
|
||||
assert_eq!(estimate_usd_cents(4, 1), 51);
|
||||
assert_eq!(estimate_usd_cents(5, 1), 64);
|
||||
assert_eq!(estimate_usd_cents(10, 1), 127);
|
||||
assert_eq!(estimate_usd_cents(15, 1), 191);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_720p_batch_4() {
|
||||
assert_eq!(estimate_usd_cents(5, 4, Some(CommonResolution::SevenTwentyP)), 255);
|
||||
fn cost_batch_2() {
|
||||
assert_eq!(estimate_usd_cents(5, 2), 127);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_none_resolution_same_as_720p() {
|
||||
assert_eq!(
|
||||
estimate_usd_cents(5, 1, None),
|
||||
estimate_usd_cents(5, 1, Some(CommonResolution::SevenTwentyP)),
|
||||
);
|
||||
}
|
||||
|
||||
// -- 480p (new pricing: 193 credits/$1) --
|
||||
|
||||
#[test]
|
||||
fn fast_480p_batch_1() {
|
||||
assert_eq!(estimate_usd_cents(4, 1, Some(CommonResolution::FourEightyP)), 21);
|
||||
assert_eq!(estimate_usd_cents(5, 1, Some(CommonResolution::FourEightyP)), 26);
|
||||
assert_eq!(estimate_usd_cents(10, 1, Some(CommonResolution::FourEightyP)), 52);
|
||||
assert_eq!(estimate_usd_cents(15, 1, Some(CommonResolution::FourEightyP)), 78);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_480p_batch_2() {
|
||||
assert_eq!(estimate_usd_cents(5, 2, Some(CommonResolution::FourEightyP)), 52);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_480p_batch_4() {
|
||||
assert_eq!(estimate_usd_cents(5, 4, Some(CommonResolution::FourEightyP)), 104);
|
||||
}
|
||||
|
||||
// -- 1080p is not supported by Fast; ErrorOut rejects it --
|
||||
|
||||
#[test]
|
||||
fn fast_1080p_error_out_rejects() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0Fast,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
negative_prompt: None,
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: Some(CommonResolution::TenEightyP),
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
generate_audio: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
idempotency_token: None,
|
||||
};
|
||||
assert!(request.build().is_err());
|
||||
}
|
||||
|
||||
// -- 1080p rounds down to 720p with PayLessDowngrade --
|
||||
|
||||
#[test]
|
||||
fn fast_1080p_downgraded_same_as_720p() {
|
||||
for duration in [4, 5, 10, 15] {
|
||||
assert_eq!(
|
||||
estimate_usd_cents_downgrade(duration, 1, Some(CommonResolution::TenEightyP)),
|
||||
estimate_usd_cents(duration, 1, Some(CommonResolution::SevenTwentyP)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// -- Relative pricing --
|
||||
|
||||
#[test]
|
||||
fn fast_480p_cheaper_than_720p() {
|
||||
let c480 = estimate_usd_cents(5, 1, Some(CommonResolution::FourEightyP));
|
||||
let c720 = estimate_usd_cents(5, 1, Some(CommonResolution::SevenTwentyP));
|
||||
assert!(c480 < c720, "480p ({}) should be cheaper than 720p ({})", c480, c720);
|
||||
fn cost_batch_4() {
|
||||
assert_eq!(estimate_usd_cents(5, 4), 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_is_cheaper_than_pro() {
|
||||
for res in [
|
||||
Some(CommonResolution::FourEightyP),
|
||||
Some(CommonResolution::SevenTwentyP),
|
||||
] {
|
||||
for duration in [4, 5, 10, 15] {
|
||||
let fast = estimate_usd_cents(duration, 1, res);
|
||||
let pro = estimate_pro_usd_cents(duration, 1, res);
|
||||
assert!(
|
||||
fast < pro,
|
||||
"Fast ({}) should be cheaper than Pro ({}) at {}s {:?}",
|
||||
fast, pro, duration, res,
|
||||
);
|
||||
}
|
||||
for duration in [4, 5, 10, 15] {
|
||||
let fast = estimate_usd_cents(duration, 1);
|
||||
let pro = estimate_pro_usd_cents(duration, 1);
|
||||
assert!(
|
||||
fast < pro,
|
||||
"Fast ({}) should be cheaper than Pro ({}) at {}s",
|
||||
fast, pro, duration,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_duration() {
|
||||
let c4 = estimate_usd_cents(4, 1, Some(CommonResolution::SevenTwentyP));
|
||||
let c10 = estimate_usd_cents(10, 1, Some(CommonResolution::SevenTwentyP));
|
||||
let c15 = estimate_usd_cents(15, 1, Some(CommonResolution::SevenTwentyP));
|
||||
let c4 = estimate_usd_cents(4, 1);
|
||||
let c10 = estimate_usd_cents(10, 1);
|
||||
let c15 = estimate_usd_cents(15, 1);
|
||||
assert!(c4 < c10);
|
||||
assert!(c10 < c15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_batch() {
|
||||
let b1 = estimate_usd_cents(5, 1, Some(CommonResolution::SevenTwentyP));
|
||||
let b2 = estimate_usd_cents(5, 2, Some(CommonResolution::SevenTwentyP));
|
||||
let b4 = estimate_usd_cents(5, 4, Some(CommonResolution::SevenTwentyP));
|
||||
let b1 = estimate_usd_cents(5, 1);
|
||||
let b2 = estimate_usd_cents(5, 2);
|
||||
let b4 = estimate_usd_cents(5, 4);
|
||||
assert!(b1 < b2);
|
||||
assert!(b2 < b4);
|
||||
}
|
||||
|
||||
+25
-145
@@ -1,11 +1,34 @@
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use seedance2pro_client::requests::generate_video::generate_video::{GenerateVideoArgs, KinoviModelType, KinoviResolution};
|
||||
|
||||
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0::PlanSeedance2proSeedance2p0;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
pub(crate) fn estimate_video_cost_seedance2pro_seedance2p0(
|
||||
plan: &PlanSeedance2proSeedance2p0,
|
||||
) -> VideoGenerationCostEstimate {
|
||||
let cost_in_credits = plan.request.estimate_credits();
|
||||
let cost_in_usd_cents = plan.request.estimate_cost_in_usd_cents();
|
||||
// A dummy session is sufficient — cost estimation does not make any network calls.
|
||||
let dummy_session = Seedance2ProSession::from_cookies_string(String::new());
|
||||
|
||||
let args = GenerateVideoArgs {
|
||||
session: &dummy_session,
|
||||
model_type: KinoviModelType::Seedance2Pro,
|
||||
prompt: String::new(),
|
||||
resolution: KinoviResolution::Square1x1, // Resolution does not affect cost
|
||||
duration_seconds: plan.duration_seconds,
|
||||
batch_count: plan.batch_count,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
|
||||
let cost_in_credits = args.estimate_credits();
|
||||
let cost_in_usd_cents = args.estimate_cost_in_usd_cents();
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_credits as u64),
|
||||
@@ -16,146 +39,3 @@ pub(crate) fn estimate_video_cost_seedance2pro_seedance2p0(
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
|
||||
fn estimate_usd_cents(
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
resolution: Option<CommonResolution>,
|
||||
) -> u64 {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
provider: Provider::Seedance2Pro,
|
||||
prompt: None,
|
||||
negative_prompt: None,
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
generate_audio: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
idempotency_token: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
.estimate_costs()
|
||||
.cost_in_usd_cents
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
}
|
||||
|
||||
// -- 720p (default / legacy pricing: 250 credits/$1) --
|
||||
|
||||
#[test]
|
||||
fn pro_720p_batch_1() {
|
||||
assert_eq!(estimate_usd_cents(4, 1, Some(CommonResolution::SevenTwentyP)), 64);
|
||||
assert_eq!(estimate_usd_cents(5, 1, Some(CommonResolution::SevenTwentyP)), 80);
|
||||
assert_eq!(estimate_usd_cents(6, 1, Some(CommonResolution::SevenTwentyP)), 96);
|
||||
assert_eq!(estimate_usd_cents(7, 1, Some(CommonResolution::SevenTwentyP)), 112);
|
||||
assert_eq!(estimate_usd_cents(10, 1, Some(CommonResolution::SevenTwentyP)), 160);
|
||||
assert_eq!(estimate_usd_cents(15, 1, Some(CommonResolution::SevenTwentyP)), 240);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_720p_batch_2() {
|
||||
assert_eq!(estimate_usd_cents(4, 2, Some(CommonResolution::SevenTwentyP)), 128);
|
||||
assert_eq!(estimate_usd_cents(5, 2, Some(CommonResolution::SevenTwentyP)), 160);
|
||||
assert_eq!(estimate_usd_cents(15, 2, Some(CommonResolution::SevenTwentyP)), 480);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_720p_batch_4() {
|
||||
assert_eq!(estimate_usd_cents(4, 4, Some(CommonResolution::SevenTwentyP)), 256);
|
||||
assert_eq!(estimate_usd_cents(5, 4, Some(CommonResolution::SevenTwentyP)), 320);
|
||||
assert_eq!(estimate_usd_cents(15, 4, Some(CommonResolution::SevenTwentyP)), 960);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_none_resolution_same_as_720p() {
|
||||
assert_eq!(
|
||||
estimate_usd_cents(5, 1, None),
|
||||
estimate_usd_cents(5, 1, Some(CommonResolution::SevenTwentyP)),
|
||||
);
|
||||
}
|
||||
|
||||
// -- 480p (new pricing: 193 credits/$1) --
|
||||
|
||||
#[test]
|
||||
fn pro_480p_batch_1() {
|
||||
assert_eq!(estimate_usd_cents(4, 1, Some(CommonResolution::FourEightyP)), 31);
|
||||
assert_eq!(estimate_usd_cents(5, 1, Some(CommonResolution::FourEightyP)), 39);
|
||||
assert_eq!(estimate_usd_cents(10, 1, Some(CommonResolution::FourEightyP)), 78);
|
||||
assert_eq!(estimate_usd_cents(15, 1, Some(CommonResolution::FourEightyP)), 117);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_480p_batch_2() {
|
||||
assert_eq!(estimate_usd_cents(5, 2, Some(CommonResolution::FourEightyP)), 78);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_480p_batch_4() {
|
||||
assert_eq!(estimate_usd_cents(5, 4, Some(CommonResolution::FourEightyP)), 155);
|
||||
}
|
||||
|
||||
// -- 1080p (new pricing: 193 credits/$1) --
|
||||
|
||||
#[test]
|
||||
fn pro_1080p_batch_1() {
|
||||
assert_eq!(estimate_usd_cents(4, 1, Some(CommonResolution::TenEightyP)), 187);
|
||||
assert_eq!(estimate_usd_cents(5, 1, Some(CommonResolution::TenEightyP)), 233);
|
||||
assert_eq!(estimate_usd_cents(10, 1, Some(CommonResolution::TenEightyP)), 466);
|
||||
assert_eq!(estimate_usd_cents(15, 1, Some(CommonResolution::TenEightyP)), 699);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_1080p_batch_2() {
|
||||
assert_eq!(estimate_usd_cents(5, 2, Some(CommonResolution::TenEightyP)), 466);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_1080p_batch_4() {
|
||||
assert_eq!(estimate_usd_cents(5, 4, Some(CommonResolution::TenEightyP)), 933);
|
||||
}
|
||||
|
||||
// -- Relative pricing --
|
||||
|
||||
#[test]
|
||||
fn pro_480p_cheaper_than_720p_cheaper_than_1080p() {
|
||||
let c480 = estimate_usd_cents(5, 1, Some(CommonResolution::FourEightyP));
|
||||
let c720 = estimate_usd_cents(5, 1, Some(CommonResolution::SevenTwentyP));
|
||||
let c1080 = estimate_usd_cents(5, 1, Some(CommonResolution::TenEightyP));
|
||||
assert!(c480 < c720, "480p ({}) should be cheaper than 720p ({})", c480, c720);
|
||||
assert!(c720 < c1080, "720p ({}) should be cheaper than 1080p ({})", c720, c1080);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_duration() {
|
||||
let c4 = estimate_usd_cents(4, 1, Some(CommonResolution::SevenTwentyP));
|
||||
let c10 = estimate_usd_cents(10, 1, Some(CommonResolution::SevenTwentyP));
|
||||
let c15 = estimate_usd_cents(15, 1, Some(CommonResolution::SevenTwentyP));
|
||||
assert!(c4 < c10);
|
||||
assert!(c10 < c15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_batch() {
|
||||
let b1 = estimate_usd_cents(5, 1, Some(CommonResolution::TenEightyP));
|
||||
let b2 = estimate_usd_cents(5, 2, Some(CommonResolution::TenEightyP));
|
||||
let b4 = estimate_usd_cents(5, 4, Some(CommonResolution::TenEightyP));
|
||||
assert!(b1 < b2);
|
||||
assert!(b2 < b4);
|
||||
}
|
||||
}
|
||||
|
||||
+64
-146
@@ -1,11 +1,34 @@
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
use seedance2pro_client::requests::generate_video::generate_video::{GenerateVideoArgs, KinoviModelType, KinoviResolution};
|
||||
|
||||
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0_fast::PlanSeedance2proSeedance2p0Fast;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
pub(crate) fn estimate_video_cost_seedance2pro_seedance2p0_fast(
|
||||
plan: &PlanSeedance2proSeedance2p0Fast,
|
||||
) -> VideoGenerationCostEstimate {
|
||||
let cost_in_credits = plan.request.estimate_credits();
|
||||
let cost_in_usd_cents = plan.request.estimate_cost_in_usd_cents();
|
||||
// A dummy session is sufficient — cost estimation does not make any network calls.
|
||||
let dummy_session = Seedance2ProSession::from_cookies_string(String::new());
|
||||
|
||||
let args = GenerateVideoArgs {
|
||||
session: &dummy_session,
|
||||
model_type: KinoviModelType::Seedance2Fast, // <-- Fast, not Pro
|
||||
prompt: String::new(),
|
||||
resolution: KinoviResolution::Square1x1, // Resolution does not affect cost
|
||||
duration_seconds: plan.duration_seconds,
|
||||
batch_count: plan.batch_count,
|
||||
start_frame_url: None,
|
||||
end_frame_url: None,
|
||||
reference_image_urls: None,
|
||||
reference_video_urls: None,
|
||||
reference_audio_urls: None,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
};
|
||||
|
||||
let cost_in_credits = args.estimate_credits();
|
||||
let cost_in_usd_cents = args.estimate_cost_in_usd_cents();
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_credits as u64),
|
||||
@@ -19,18 +42,13 @@ pub(crate) fn estimate_video_cost_seedance2pro_seedance2p0_fast(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
|
||||
fn estimate_usd_cents(
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
resolution: Option<CommonResolution>,
|
||||
) -> u64 {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
fn estimate_usd_cents(duration_seconds: u16, video_batch_count: u16) -> u64 {
|
||||
let request = GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance2p0Fast,
|
||||
provider: Provider::Seedance2Pro,
|
||||
prompt: None,
|
||||
@@ -41,7 +59,7 @@ mod tests {
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
@@ -56,12 +74,36 @@ mod tests {
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
}
|
||||
|
||||
fn estimate_pro_usd_cents(
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
resolution: Option<CommonResolution>,
|
||||
) -> u64 {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
#[test]
|
||||
fn cost_is_present() {
|
||||
let cents = estimate_usd_cents(5, 1);
|
||||
assert!(cents > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_batch_1() {
|
||||
// Seedance2Fast: 28 credits/sec, 220 credits/$1 (with rounding)
|
||||
assert_eq!(estimate_usd_cents(4, 1), 51);
|
||||
assert_eq!(estimate_usd_cents(5, 1), 64);
|
||||
assert_eq!(estimate_usd_cents(10, 1), 127);
|
||||
assert_eq!(estimate_usd_cents(15, 1), 191);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_batch_2() {
|
||||
assert_eq!(estimate_usd_cents(5, 2), 127);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_batch_4() {
|
||||
assert_eq!(estimate_usd_cents(5, 4), 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_is_cheaper_than_pro() {
|
||||
let fast = estimate_usd_cents(5, 1);
|
||||
|
||||
let pro_request = GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
provider: Provider::Seedance2Pro,
|
||||
prompt: None,
|
||||
@@ -72,140 +114,16 @@ mod tests {
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
generate_audio: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
idempotency_token: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
.estimate_costs()
|
||||
.cost_in_usd_cents
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
}
|
||||
let pro = pro_request.build().unwrap().estimate_costs().cost_in_usd_cents.unwrap();
|
||||
|
||||
// -- 720p (default / legacy pricing: 220 credits/$1) --
|
||||
|
||||
#[test]
|
||||
fn fast_720p_batch_1() {
|
||||
assert_eq!(estimate_usd_cents(4, 1, Some(CommonResolution::SevenTwentyP)), 51);
|
||||
assert_eq!(estimate_usd_cents(5, 1, Some(CommonResolution::SevenTwentyP)), 64);
|
||||
assert_eq!(estimate_usd_cents(10, 1, Some(CommonResolution::SevenTwentyP)), 127);
|
||||
assert_eq!(estimate_usd_cents(15, 1, Some(CommonResolution::SevenTwentyP)), 191);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_720p_batch_2() {
|
||||
assert_eq!(estimate_usd_cents(5, 2, Some(CommonResolution::SevenTwentyP)), 127);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_720p_batch_4() {
|
||||
assert_eq!(estimate_usd_cents(5, 4, Some(CommonResolution::SevenTwentyP)), 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_none_resolution_same_as_720p() {
|
||||
assert_eq!(
|
||||
estimate_usd_cents(5, 1, None),
|
||||
estimate_usd_cents(5, 1, Some(CommonResolution::SevenTwentyP)),
|
||||
);
|
||||
}
|
||||
|
||||
// -- 480p (new pricing: 193 credits/$1) --
|
||||
|
||||
#[test]
|
||||
fn fast_480p_batch_1() {
|
||||
assert_eq!(estimate_usd_cents(4, 1, Some(CommonResolution::FourEightyP)), 21);
|
||||
assert_eq!(estimate_usd_cents(5, 1, Some(CommonResolution::FourEightyP)), 26);
|
||||
assert_eq!(estimate_usd_cents(10, 1, Some(CommonResolution::FourEightyP)), 52);
|
||||
assert_eq!(estimate_usd_cents(15, 1, Some(CommonResolution::FourEightyP)), 78);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_480p_batch_2() {
|
||||
assert_eq!(estimate_usd_cents(5, 2, Some(CommonResolution::FourEightyP)), 52);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_480p_batch_4() {
|
||||
assert_eq!(estimate_usd_cents(5, 4, Some(CommonResolution::FourEightyP)), 104);
|
||||
}
|
||||
|
||||
// -- 1080p maps to 720p for Fast (Kinovi doesn't support 1080p fast) --
|
||||
|
||||
#[test]
|
||||
fn fast_1080p_same_as_720p() {
|
||||
for duration in [4, 5, 10, 15] {
|
||||
assert_eq!(
|
||||
estimate_usd_cents(duration, 1, Some(CommonResolution::TenEightyP)),
|
||||
estimate_usd_cents(duration, 1, Some(CommonResolution::SevenTwentyP)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_1080p_batch_2() {
|
||||
assert_eq!(
|
||||
estimate_usd_cents(5, 2, Some(CommonResolution::TenEightyP)),
|
||||
estimate_usd_cents(5, 2, Some(CommonResolution::SevenTwentyP)),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_1080p_batch_4() {
|
||||
assert_eq!(
|
||||
estimate_usd_cents(5, 4, Some(CommonResolution::TenEightyP)),
|
||||
estimate_usd_cents(5, 4, Some(CommonResolution::SevenTwentyP)),
|
||||
);
|
||||
}
|
||||
|
||||
// -- Relative pricing --
|
||||
|
||||
#[test]
|
||||
fn fast_480p_cheaper_than_720p() {
|
||||
let c480 = estimate_usd_cents(5, 1, Some(CommonResolution::FourEightyP));
|
||||
let c720 = estimate_usd_cents(5, 1, Some(CommonResolution::SevenTwentyP));
|
||||
assert!(c480 < c720, "480p ({}) should be cheaper than 720p ({})", c480, c720);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fast_is_cheaper_than_pro() {
|
||||
for res in [
|
||||
Some(CommonResolution::FourEightyP),
|
||||
Some(CommonResolution::SevenTwentyP),
|
||||
Some(CommonResolution::TenEightyP),
|
||||
] {
|
||||
for duration in [4, 5, 10, 15] {
|
||||
let fast = estimate_usd_cents(duration, 1, res);
|
||||
let pro = estimate_pro_usd_cents(duration, 1, res);
|
||||
assert!(
|
||||
fast < pro,
|
||||
"Fast ({}) should be cheaper than Pro ({}) at {}s {:?}",
|
||||
fast, pro, duration, res,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_duration() {
|
||||
let c4 = estimate_usd_cents(4, 1, Some(CommonResolution::SevenTwentyP));
|
||||
let c10 = estimate_usd_cents(10, 1, Some(CommonResolution::SevenTwentyP));
|
||||
let c15 = estimate_usd_cents(15, 1, Some(CommonResolution::SevenTwentyP));
|
||||
assert!(c4 < c10);
|
||||
assert!(c10 < c15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_batch() {
|
||||
let b1 = estimate_usd_cents(5, 1, Some(CommonResolution::TenEightyP));
|
||||
let b2 = estimate_usd_cents(5, 2, Some(CommonResolution::TenEightyP));
|
||||
let b4 = estimate_usd_cents(5, 4, Some(CommonResolution::TenEightyP));
|
||||
assert!(b1 < b2);
|
||||
assert!(b2 < b4);
|
||||
assert!(fast < pro, "Fast ({}) should be cheaper than Pro ({})", fast, pro);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-14
@@ -5,23 +5,13 @@ use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraf
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
ArtcraftVideoResponsePayload, GenerateVideoResponse,
|
||||
};
|
||||
use crate::api::common_resolution::CommonResolution as CommonResolutionRouter;
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{
|
||||
Seedance2p0MultiFunctionVideoGenRequest, Seedance2p0OutputResolution,
|
||||
};
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0MultiFunctionVideoGenRequest;
|
||||
use artcraft_client::endpoints::generate::video::multi_function::seedance_2p0_multi_function_video_gen::seedance_2p0_multi_function_video_gen;
|
||||
|
||||
pub async fn execute_artcraft_seedance2p0(
|
||||
plan: &PlanArtcraftSeedance2p0,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let output_resolution = plan.resolution.map(|r| match r {
|
||||
CommonResolutionRouter::FourEightyP => Seedance2p0OutputResolution::FourEightyP,
|
||||
CommonResolutionRouter::SevenTwentyP => Seedance2p0OutputResolution::SevenTwentyP,
|
||||
CommonResolutionRouter::TenEightyP => Seedance2p0OutputResolution::TenEightyP,
|
||||
_ => Seedance2p0OutputResolution::SevenTwentyP,
|
||||
});
|
||||
|
||||
let request = Seedance2p0MultiFunctionVideoGenRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
prompt: plan.prompt.clone(),
|
||||
@@ -32,7 +22,6 @@ pub async fn execute_artcraft_seedance2p0(
|
||||
reference_audio_media_tokens: plan.reference_audio.clone(),
|
||||
reference_character_tokens: plan.reference_characters.clone(),
|
||||
aspect_ratio: plan.aspect_ratio,
|
||||
output_resolution,
|
||||
duration_seconds: plan.duration_seconds,
|
||||
batch_count: Some(plan.batch_count),
|
||||
};
|
||||
@@ -61,7 +50,7 @@ mod tests {
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_text_to_video_seedance_2p0() {
|
||||
let client = get_artcraft_client();
|
||||
let request = crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder {
|
||||
let request = crate::generate::generate_video::generate_video_request::GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
video_batch_count: Some(1),
|
||||
prompt: Some("a cat walking through a cyberpunk city at night".to_string()),
|
||||
@@ -84,7 +73,7 @@ mod tests {
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_text_to_video_seedance_2p0_batch_two() {
|
||||
let client = get_artcraft_client();
|
||||
let request = crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder {
|
||||
let request = crate::generate::generate_video::generate_video_request::GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
video_batch_count: Some(2),
|
||||
prompt: Some("a dog surfing a wave, cinematic".to_string()),
|
||||
|
||||
+12
-183
@@ -1,4 +1,3 @@
|
||||
use crate::api::common_resolution::CommonResolution as CommonResolutionRouter;
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
@@ -6,11 +5,9 @@ use crate::generate::generate_video::generate_video_response::{
|
||||
ArtcraftVideoResponsePayload, GenerateVideoResponse,
|
||||
};
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::PlanArtcraftSeedance2p0;
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{Seedance2p0AspectRatio, Seedance2p0BatchCount};
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::omni_gen::generate::video::omni_gen_video::omni_gen_video_generate;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
|
||||
/// Execute Seedance 2.0 Fast via the Artcraft omni-gen video endpoint.
|
||||
@@ -22,45 +19,34 @@ pub async fn execute_artcraft_seedance2p0_fast(
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let aspect_ratio = plan.aspect_ratio.map(|ar| match ar {
|
||||
Seedance2p0AspectRatio::Landscape16x9 => CommonAspectRatio::WideSixteenByNine,
|
||||
Seedance2p0AspectRatio::Portrait9x16 => CommonAspectRatio::TallNineBySixteen,
|
||||
Seedance2p0AspectRatio::Square1x1 => CommonAspectRatio::Square,
|
||||
Seedance2p0AspectRatio::Standard4x3 => CommonAspectRatio::WideFourByThree,
|
||||
Seedance2p0AspectRatio::Portrait3x4 => CommonAspectRatio::TallThreeByFour,
|
||||
});
|
||||
|
||||
let resolution = plan.resolution.map(|r| match r {
|
||||
CommonResolutionRouter::FourEightyP => CommonResolution::FourEightyP,
|
||||
CommonResolutionRouter::SevenTwentyP => CommonResolution::SevenTwentyP,
|
||||
CommonResolutionRouter::TenEightyP => CommonResolution::TenEightyP,
|
||||
CommonResolutionRouter::HalfK => CommonResolution::HalfK,
|
||||
CommonResolutionRouter::OneK => CommonResolution::OneK,
|
||||
CommonResolutionRouter::TwoK => CommonResolution::TwoK,
|
||||
CommonResolutionRouter::ThreeK => CommonResolution::ThreeK,
|
||||
CommonResolutionRouter::FourK => CommonResolution::FourK,
|
||||
artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0AspectRatio::Landscape16x9 => CommonAspectRatio::WideSixteenByNine,
|
||||
artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0AspectRatio::Portrait9x16 => CommonAspectRatio::TallNineBySixteen,
|
||||
artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0AspectRatio::Square1x1 => CommonAspectRatio::Square,
|
||||
artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0AspectRatio::Standard4x3 => CommonAspectRatio::WideFourByThree,
|
||||
artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0AspectRatio::Portrait3x4 => CommonAspectRatio::TallThreeByFour,
|
||||
});
|
||||
|
||||
let request = OmniGenVideoCostAndGenerateRequest {
|
||||
idempotency_token: Some(plan.idempotency_token.clone()),
|
||||
model: Some(CommonVideoModel::Seedance2p0Fast),
|
||||
prompt: plan.prompt.clone(),
|
||||
negative_prompt: None,
|
||||
start_frame_image_media_token: plan.start_frame.clone(),
|
||||
end_frame_image_media_token: plan.end_frame.clone(),
|
||||
reference_image_media_tokens: plan.reference_images.clone(),
|
||||
reference_video_media_tokens: plan.reference_videos.clone(),
|
||||
reference_audio_media_tokens: plan.reference_audio.clone(),
|
||||
reference_character_tokens: plan.reference_characters.clone(),
|
||||
resolution,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio,
|
||||
quality: None,
|
||||
duration_seconds: plan.duration_seconds.map(|d| d as u16),
|
||||
video_batch_count: Some(match plan.batch_count {
|
||||
Seedance2p0BatchCount::One => 1,
|
||||
Seedance2p0BatchCount::Two => 2,
|
||||
Seedance2p0BatchCount::Four => 4,
|
||||
artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0BatchCount::One => 1,
|
||||
artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0BatchCount::Two => 2,
|
||||
artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0BatchCount::Four => 4,
|
||||
}),
|
||||
quality: None,
|
||||
generate_audio: None,
|
||||
negative_prompt: None,
|
||||
};
|
||||
|
||||
let response = omni_gen_video_generate(
|
||||
@@ -77,160 +63,3 @@ pub async fn execute_artcraft_seedance2p0_fast(
|
||||
all_inference_job_tokens: all_tokens,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use crate::test_helpers::{base_video_request, get_artcraft_client};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn fast_request() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::Seedance2p0Fast,
|
||||
prompt: Some("a corgi walking through a cyberpunk city at night".to_string()),
|
||||
..base_video_request()
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_text_to_video_fast_720p_wide() {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution: Some(CommonResolution::SevenTwentyP),
|
||||
video_batch_count: Some(1),
|
||||
..fast_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_video(&client).await;
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_video 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_video_fast_480p_square() {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
resolution: Some(CommonResolution::FourEightyP),
|
||||
video_batch_count: Some(1),
|
||||
..fast_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_video(&client).await;
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_video request failed");
|
||||
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
|
||||
println!("Job token: {:?}", payload.inference_job_token);
|
||||
assert_eq!(1, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_image_to_video_fast_with_start_keyframe() {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
start_frame: Some(ImageRef::MediaFileToken(
|
||||
MediaFileToken::new("mf_test_keyframe_placeholder".to_string()),
|
||||
)),
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution: Some(CommonResolution::SevenTwentyP),
|
||||
video_batch_count: Some(1),
|
||||
..fast_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_video(&client).await;
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_video request failed");
|
||||
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
|
||||
println!("Job token: {:?}", payload.inference_job_token);
|
||||
assert_eq!(1, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_text_to_video_fast_batch_2() {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
|
||||
resolution: Some(CommonResolution::SevenTwentyP),
|
||||
video_batch_count: Some(2),
|
||||
..fast_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_video(&client).await;
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_video request failed");
|
||||
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
|
||||
println!("Job tokens ({} total):", payload.all_inference_job_tokens.len());
|
||||
for token in &payload.all_inference_job_tokens {
|
||||
println!(" {:?}", token);
|
||||
}
|
||||
assert_eq!(1, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_text_to_video_fast_with_reference_images() {
|
||||
let client = get_artcraft_client();
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
reference_images: Some(crate::api::image_list_ref::ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_test_ref_image_placeholder".to_string()),
|
||||
])),
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution: Some(CommonResolution::SevenTwentyP),
|
||||
video_batch_count: Some(1),
|
||||
..fast_request()
|
||||
};
|
||||
|
||||
let plan = request.build().unwrap();
|
||||
let result = plan.generate_video(&client).await;
|
||||
println!("Result: {:?}", result);
|
||||
let response = result.expect("generate_video request failed");
|
||||
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
|
||||
println!("Job token: {:?}", payload.inference_job_token);
|
||||
assert_eq!(1, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_includes_resolution() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
resolution: Some(CommonResolution::FourEightyP),
|
||||
..fast_request()
|
||||
};
|
||||
let plan = request.build().unwrap();
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0Fast(p) = plan else { panic!("wrong variant") };
|
||||
assert!(matches!(p.resolution, Some(CommonResolution::FourEightyP)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_includes_characters() {
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
reference_character_tokens: Some(CharacterListRef::CharacterTokens(vec![
|
||||
CharacterToken::new("char_test123".to_string()),
|
||||
])),
|
||||
..fast_request()
|
||||
};
|
||||
let plan = request.build().unwrap();
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0Fast(p) = plan else { panic!("wrong variant") };
|
||||
assert!(p.reference_characters.is_some());
|
||||
assert_eq!(p.reference_characters.unwrap().len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
+18
-16
@@ -7,7 +7,7 @@ use crate::generate::generate_video::generate_video_response::{
|
||||
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0::PlanSeedance2proSeedance2p0;
|
||||
use crate::utils::download_file::download_file;
|
||||
use seedance2pro_client::requests::generate_video::generate_video::{
|
||||
generate_video, GenerateVideoArgs,
|
||||
generate_video, GenerateVideoArgs, KinoviModelType,
|
||||
};
|
||||
use seedance2pro_client::requests::prepare_file_upload::prepare_file_upload::{
|
||||
prepare_file_upload, PrepareFileUploadArgs,
|
||||
@@ -26,23 +26,27 @@ pub async fn execute_seedance2pro_seedance2p0(
|
||||
let session = &seedance2pro_client.session;
|
||||
|
||||
// Upload media files to seedance2pro CDN
|
||||
let start_frame_url = upload_optional_url(session, plan.request.start_frame_url.as_deref()).await?;
|
||||
let end_frame_url = upload_optional_url(session, plan.request.end_frame_url.as_deref()).await?;
|
||||
let reference_image_urls = upload_optional_url_list(session, plan.request.reference_image_urls.as_deref()).await?;
|
||||
let reference_video_urls = upload_optional_url_list(session, plan.request.reference_video_urls.as_deref()).await?;
|
||||
let reference_audio_urls = upload_optional_url_list(session, plan.request.reference_audio_urls.as_deref()).await?;
|
||||
|
||||
let mut request = plan.request.clone();
|
||||
request.start_frame_url = start_frame_url;
|
||||
request.end_frame_url = end_frame_url;
|
||||
request.reference_image_urls = reference_image_urls;
|
||||
request.reference_video_urls = reference_video_urls;
|
||||
request.reference_audio_urls = reference_audio_urls;
|
||||
let start_frame_url = upload_optional_url(session, plan.start_frame_url.as_deref()).await?;
|
||||
let end_frame_url = upload_optional_url(session, plan.end_frame_url.as_deref()).await?;
|
||||
let reference_image_urls = upload_optional_url_list(session, plan.reference_image_urls.as_deref()).await?;
|
||||
let reference_video_urls = upload_optional_url_list(session, plan.reference_video_urls.as_deref()).await?;
|
||||
let reference_audio_urls = upload_optional_url_list(session, plan.reference_audio_urls.as_deref()).await?;
|
||||
|
||||
let args = GenerateVideoArgs {
|
||||
session,
|
||||
model_type: KinoviModelType::Seedance2Pro,
|
||||
prompt: plan.prompt.clone().unwrap_or_default(),
|
||||
resolution: plan.resolution,
|
||||
duration_seconds: plan.duration_seconds,
|
||||
batch_count: plan.batch_count,
|
||||
start_frame_url,
|
||||
end_frame_url,
|
||||
reference_image_urls,
|
||||
reference_video_urls,
|
||||
reference_audio_urls,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
request,
|
||||
};
|
||||
|
||||
let response = generate_video(args)
|
||||
@@ -52,8 +56,6 @@ pub async fn execute_seedance2pro_seedance2p0(
|
||||
Ok(GenerateVideoResponse::Seedance2Pro(Seedance2proVideoResponsePayload {
|
||||
order_id: response.order_id,
|
||||
task_id: response.task_id,
|
||||
maybe_order_ids: response.order_ids,
|
||||
maybe_task_ids: response.task_ids,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
+21
-19
@@ -1,15 +1,15 @@
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_video::execute::seedance2pro::execute_seedance2pro_seedance2p0::{
|
||||
upload_optional_url, upload_optional_url_list,
|
||||
};
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
GenerateVideoResponse, Seedance2proVideoResponsePayload,
|
||||
};
|
||||
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0_fast::PlanSeedance2proSeedance2p0Fast;
|
||||
use crate::generate::generate_video::execute::seedance2pro::execute_seedance2pro_seedance2p0::{
|
||||
upload_optional_url, upload_optional_url_list,
|
||||
};
|
||||
use seedance2pro_client::requests::generate_video::generate_video::{
|
||||
generate_video, GenerateVideoArgs,
|
||||
generate_video, GenerateVideoArgs, KinoviModelType,
|
||||
};
|
||||
|
||||
/// Execute Seedance 2.0 Fast via the Seedance2Pro/Kinovi provider.
|
||||
@@ -23,23 +23,27 @@ pub async fn execute_seedance2pro_seedance2p0_fast(
|
||||
let session = &seedance2pro_client.session;
|
||||
|
||||
// Upload media files to seedance2pro CDN
|
||||
let start_frame_url = upload_optional_url(session, plan.request.start_frame_url.as_deref()).await?;
|
||||
let end_frame_url = upload_optional_url(session, plan.request.end_frame_url.as_deref()).await?;
|
||||
let reference_image_urls = upload_optional_url_list(session, plan.request.reference_image_urls.as_deref()).await?;
|
||||
let reference_video_urls = upload_optional_url_list(session, plan.request.reference_video_urls.as_deref()).await?;
|
||||
let reference_audio_urls = upload_optional_url_list(session, plan.request.reference_audio_urls.as_deref()).await?;
|
||||
|
||||
let mut request = plan.request.clone();
|
||||
request.start_frame_url = start_frame_url;
|
||||
request.end_frame_url = end_frame_url;
|
||||
request.reference_image_urls = reference_image_urls;
|
||||
request.reference_video_urls = reference_video_urls;
|
||||
request.reference_audio_urls = reference_audio_urls;
|
||||
let start_frame_url = upload_optional_url(session, plan.start_frame_url.as_deref()).await?;
|
||||
let end_frame_url = upload_optional_url(session, plan.end_frame_url.as_deref()).await?;
|
||||
let reference_image_urls = upload_optional_url_list(session, plan.reference_image_urls.as_deref()).await?;
|
||||
let reference_video_urls = upload_optional_url_list(session, plan.reference_video_urls.as_deref()).await?;
|
||||
let reference_audio_urls = upload_optional_url_list(session, plan.reference_audio_urls.as_deref()).await?;
|
||||
|
||||
let args = GenerateVideoArgs {
|
||||
session,
|
||||
model_type: KinoviModelType::Seedance2Fast, // <-- Fast, not Pro
|
||||
prompt: plan.prompt.clone().unwrap_or_default(),
|
||||
resolution: plan.resolution,
|
||||
duration_seconds: plan.duration_seconds,
|
||||
batch_count: plan.batch_count,
|
||||
start_frame_url,
|
||||
end_frame_url,
|
||||
reference_image_urls,
|
||||
reference_video_urls,
|
||||
reference_audio_urls,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
host_override: None,
|
||||
request,
|
||||
};
|
||||
|
||||
let response = generate_video(args)
|
||||
@@ -49,7 +53,5 @@ pub async fn execute_seedance2pro_seedance2p0_fast(
|
||||
Ok(GenerateVideoResponse::Seedance2Pro(Seedance2proVideoResponsePayload {
|
||||
order_id: response.order_id,
|
||||
task_id: response.task_id,
|
||||
maybe_order_ids: response.order_ids,
|
||||
maybe_task_ids: response.task_ids,
|
||||
}))
|
||||
}
|
||||
|
||||
+5
-68
@@ -9,17 +9,17 @@ use crate::api::provider::Provider;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_pro::plan_generate_video_artcraft_kling3p0_pro;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_standard::plan_generate_video_artcraft_kling3p0_standard;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling_1_6_pro::plan_generate_video_artcraft_kling_1_6_pro;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling_2_1_master::plan_generate_video_artcraft_kling_2_1_master;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling_2_1_pro::plan_generate_video_artcraft_kling_2_1_pro;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling_2_5_turbo_pro::plan_generate_video_artcraft_kling_2_5_turbo_pro;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling_2_6_pro::plan_generate_video_artcraft_kling_2_6_pro;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_pro::plan_generate_video_artcraft_kling3p0_pro;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling3p0_standard::plan_generate_video_artcraft_kling3p0_standard;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance_1_0_lite::plan_generate_video_artcraft_seedance_1_0_lite;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance1p5_pro::plan_generate_video_artcraft_seedance1p5_pro;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::plan_generate_video_artcraft_seedance2p0;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0_fast::plan_generate_video_artcraft_seedance2p0_fast;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance_1_0_lite::plan_generate_video_artcraft_seedance_1_0_lite;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_sora_2::plan_generate_video_artcraft_sora_2;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_sora_2_pro::plan_generate_video_artcraft_sora_2_pro;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_veo_2::plan_generate_video_artcraft_veo_2;
|
||||
@@ -47,19 +47,12 @@ use crate::generate::generate_video::plan::muapi::plan_generate_video_muapi_seed
|
||||
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0::plan_generate_video_seedance2pro_seedance2p0;
|
||||
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0_fast::plan_generate_video_seedance2pro_seedance2p0_fast;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::happy_horse_1p0::build::build_artcraft_happy_horse_1p0;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0::build::build_artcraft_seedance_2p0;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast::build::build_artcraft_seedance_2p0_fast;
|
||||
use crate::generate::generate_video_v2::providers::kinovi::happy_horse_1p0::build::build_kinovi_happy_horse_1p0;
|
||||
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0::build::build_kinovi_seedance_2p0;
|
||||
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::build::build_kinovi_seedance_2p0_fast;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
|
||||
/// Plan to either (1) generate a video or (2) determine how much it costs to generate that video.
|
||||
/// This works across multiple providers by shaping a generic "GenerateVideoRequest" into a provider-specific plan.
|
||||
/// That plan can then be used to return a cost estimate for that given provider or return a struct that can be used to send a real generation request.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GenerateVideoRequestBuilder {
|
||||
pub struct GenerateVideoRequest {
|
||||
/// Which model to use.
|
||||
pub model: CommonVideoModel,
|
||||
|
||||
@@ -116,57 +109,7 @@ pub struct GenerateVideoRequestBuilder {
|
||||
pub idempotency_token: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for GenerateVideoRequestBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
model: CommonVideoModel::Seedance2p0,
|
||||
provider: Provider::Artcraft,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
prompt: None,
|
||||
negative_prompt: None,
|
||||
start_frame: None,
|
||||
end_frame: None,
|
||||
reference_images: None,
|
||||
reference_videos: None,
|
||||
reference_audio: None,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
duration_seconds: None,
|
||||
video_batch_count: None,
|
||||
generate_audio: None,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateVideoRequestBuilder {
|
||||
|
||||
pub fn use_new_builder(&self) -> bool {
|
||||
match (self.provider, self.model) {
|
||||
(Provider::Artcraft, CommonVideoModel::HappyHorse1p0) => true,
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0) => true,
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0Fast) => true,
|
||||
(Provider::Seedance2Pro, CommonVideoModel::HappyHorse1p0) => true,
|
||||
(Provider::Seedance2Pro, CommonVideoModel::Seedance2p0) => true,
|
||||
(Provider::Seedance2Pro, CommonVideoModel::Seedance2p0Fast) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
// New builder (transitional state)
|
||||
pub fn build2(self) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
match (self.provider, self.model) {
|
||||
(Provider::Artcraft, CommonVideoModel::HappyHorse1p0) => build_artcraft_happy_horse_1p0(self),
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0) => build_artcraft_seedance_2p0(self),
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0Fast) => build_artcraft_seedance_2p0_fast(self),
|
||||
(Provider::Seedance2Pro, CommonVideoModel::HappyHorse1p0) => build_kinovi_happy_horse_1p0(self),
|
||||
(Provider::Seedance2Pro, CommonVideoModel::Seedance2p0) => build_kinovi_seedance_2p0(self),
|
||||
(Provider::Seedance2Pro, CommonVideoModel::Seedance2p0Fast) => build_kinovi_seedance_2p0_fast(self),
|
||||
_ => self.unsupported_provider_and_model(),
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateVideoRequest {
|
||||
/// Read the video generation request, construct a plan, then yield a means to execute it.
|
||||
pub fn build(&self) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
match self.provider {
|
||||
@@ -245,12 +188,6 @@ impl GenerateVideoRequestBuilder {
|
||||
))
|
||||
}
|
||||
|
||||
fn unsupported_provider_and_model(&self) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
Err(ArtcraftRouterError::UnsupportedProviderAndModelForNewApi(
|
||||
format!("Video generation for model `{:?}` is not supported for provider {:?} using the new API", self.model, self.provider)
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_or_generate_idempotency_token(&self) -> String {
|
||||
self.idempotency_token.clone()
|
||||
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
|
||||
-2
@@ -16,8 +16,6 @@ pub struct MuapiVideoResponsePayload {
|
||||
pub struct Seedance2proVideoResponsePayload {
|
||||
pub order_id: String,
|
||||
pub task_id: String,
|
||||
pub maybe_order_ids: Option<Vec<String>>,
|
||||
pub maybe_task_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pub mod cost;
|
||||
pub mod execute;
|
||||
pub mod generate_video_request_builder;
|
||||
pub mod generate_video_request;
|
||||
pub mod generate_video_response;
|
||||
pub mod plan;
|
||||
pub mod video_generation_cost_estimate;
|
||||
|
||||
+12
-12
@@ -3,7 +3,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::kling_3p0_pro_multi_function_video_gen::{
|
||||
Kling3p0ProMultiFunctionVideoGenAspectRatio,
|
||||
@@ -24,7 +24,7 @@ pub struct PlanArtcraftKling3p0Pro {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_kling3p0_pro(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
@@ -166,11 +166,11 @@ mod tests {
|
||||
use crate::api::provider::Provider;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
|
||||
fn base_request() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
fn base_request() -> GenerateVideoRequest {
|
||||
GenerateVideoRequest {
|
||||
model: CommonVideoModel::Kling3p0Pro,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space".to_string()),
|
||||
@@ -193,7 +193,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_16x9() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
..base_request()
|
||||
};
|
||||
@@ -203,7 +203,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_9x16() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
|
||||
..base_request()
|
||||
};
|
||||
@@ -213,7 +213,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_square() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
..base_request()
|
||||
};
|
||||
@@ -223,7 +223,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_error_out_on_unsupported() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideThreeByTwo),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_request()
|
||||
@@ -238,7 +238,7 @@ mod tests {
|
||||
#[test]
|
||||
fn duration_valid_range() {
|
||||
for d in 3..=15 {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
duration_seconds: Some(d),
|
||||
..base_request()
|
||||
};
|
||||
@@ -249,7 +249,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn duration_out_of_range_error_out() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
duration_seconds: Some(16),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_request()
|
||||
@@ -263,7 +263,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn url_image_ref_returns_error() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
start_frame: Some(ImageRef::Url("https://example.com/image.jpg".to_string())),
|
||||
..base_request()
|
||||
};
|
||||
|
||||
+12
-12
@@ -3,7 +3,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::kling_3p0_standard_multi_function_video_gen::{
|
||||
Kling3p0StandardMultiFunctionVideoGenAspectRatio,
|
||||
@@ -24,7 +24,7 @@ pub struct PlanArtcraftKling3p0Standard {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_kling3p0_standard(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
@@ -166,11 +166,11 @@ mod tests {
|
||||
use crate::api::provider::Provider;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
|
||||
fn base_request() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
fn base_request() -> GenerateVideoRequest {
|
||||
GenerateVideoRequest {
|
||||
model: CommonVideoModel::Kling3p0Standard,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space".to_string()),
|
||||
@@ -193,7 +193,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_16x9() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
..base_request()
|
||||
};
|
||||
@@ -203,7 +203,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_9x16() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
|
||||
..base_request()
|
||||
};
|
||||
@@ -213,7 +213,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_square() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
..base_request()
|
||||
};
|
||||
@@ -223,7 +223,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_error_out_on_unsupported() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideThreeByTwo),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_request()
|
||||
@@ -238,7 +238,7 @@ mod tests {
|
||||
#[test]
|
||||
fn duration_valid_range() {
|
||||
for d in 3..=15 {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
duration_seconds: Some(d),
|
||||
..base_request()
|
||||
};
|
||||
@@ -249,7 +249,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn duration_out_of_range_error_out() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
duration_seconds: Some(16),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_request()
|
||||
@@ -263,7 +263,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn url_image_ref_returns_error() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
start_frame: Some(ImageRef::Url("https://example.com/image.jpg".to_string())),
|
||||
..base_request()
|
||||
};
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::generate_kling_1_6_pro_image_to_video::{
|
||||
GenerateKling16ProAspectRatio, GenerateKling16ProDuration,
|
||||
@@ -21,7 +21,7 @@ pub struct PlanArtcraftKling16Pro {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_kling_1_6_pro(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::generate_kling_2_1_master_image_to_video::{
|
||||
GenerateKling21MasterAspectRatio, GenerateKling21MasterDuration,
|
||||
@@ -20,7 +20,7 @@ pub struct PlanArtcraftKling21Master {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_kling_2_1_master(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::generate_kling_2_1_pro_image_to_video::{
|
||||
GenerateKling21ProAspectRatio, GenerateKling21ProDuration,
|
||||
@@ -21,7 +21,7 @@ pub struct PlanArtcraftKling21Pro {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_kling_2_1_pro(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::kling_2_5_turbo_multi_function_video_gen::{
|
||||
Kling2p5TurboProMultiFunctionVideoGenAspectRatio, Kling2p5TurboProMultiFunctionVideoGenDuration,
|
||||
@@ -22,7 +22,7 @@ pub struct PlanArtcraftKling2p5TurboPro {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_kling_2_5_turbo_pro(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::kling_2_6_multi_function_video_gen::{
|
||||
Kling2p6ProMultiFunctionVideoGenAspectRatio, Kling2p6ProMultiFunctionVideoGenDuration,
|
||||
@@ -22,7 +22,7 @@ pub struct PlanArtcraftKling2p6Pro {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_kling_2_6_pro(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+12
-12
@@ -3,7 +3,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_1p5_pro_multi_function_video_gen::{
|
||||
Seedance1p5ProMultiFunctionVideoGenAspectRatio,
|
||||
@@ -25,7 +25,7 @@ pub struct PlanArtcraftSeedance1p5Pro {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_seedance1p5_pro(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
@@ -184,11 +184,11 @@ mod tests {
|
||||
use crate::api::provider::Provider;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
|
||||
fn base_seedance_1p5_pro_request() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
fn base_seedance_1p5_pro_request() -> GenerateVideoRequest {
|
||||
GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance1p5Pro,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space".to_string()),
|
||||
@@ -211,7 +211,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_16x9() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
..base_seedance_1p5_pro_request()
|
||||
};
|
||||
@@ -221,7 +221,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_9x16() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
|
||||
..base_seedance_1p5_pro_request()
|
||||
};
|
||||
@@ -231,7 +231,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_21x9() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideTwentyOneByNine),
|
||||
..base_seedance_1p5_pro_request()
|
||||
};
|
||||
@@ -241,7 +241,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_error_out_on_unsupported() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideThreeByTwo),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_seedance_1p5_pro_request()
|
||||
@@ -256,7 +256,7 @@ mod tests {
|
||||
#[test]
|
||||
fn duration_valid_range() {
|
||||
for d in 4..=12 {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
duration_seconds: Some(d),
|
||||
..base_seedance_1p5_pro_request()
|
||||
};
|
||||
@@ -267,7 +267,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn duration_out_of_range_error_out() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
duration_seconds: Some(13),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_seedance_1p5_pro_request()
|
||||
@@ -281,7 +281,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn url_image_ref_accepted_for_cost_path() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
start_frame: Some(ImageRef::Url("https://example.com/image.jpg".to_string())),
|
||||
..base_seedance_1p5_pro_request()
|
||||
};
|
||||
|
||||
+14
-57
@@ -1,13 +1,12 @@
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{
|
||||
Seedance2p0AspectRatio, Seedance2p0BatchCount,
|
||||
@@ -25,14 +24,13 @@ pub struct PlanArtcraftSeedance2p0 {
|
||||
pub reference_audio: Option<Vec<MediaFileToken>>,
|
||||
pub reference_characters: Option<Vec<CharacterToken>>,
|
||||
pub aspect_ratio: Option<Seedance2p0AspectRatio>,
|
||||
pub resolution: Option<CommonResolution>,
|
||||
pub duration_seconds: Option<u8>,
|
||||
pub batch_count: Seedance2p0BatchCount,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_seedance2p0(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
@@ -43,7 +41,6 @@ pub fn plan_generate_video_artcraft_seedance2p0(
|
||||
let reference_audio = resolve_audio_list_ref(request.reference_audio.clone())?;
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let resolution = plan_output_resolution(request.resolution, strategy)?;
|
||||
let batch_count = plan_batch_count(request.video_batch_count, strategy)?;
|
||||
let duration_seconds = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
@@ -56,7 +53,6 @@ pub fn plan_generate_video_artcraft_seedance2p0(
|
||||
reference_audio,
|
||||
reference_characters: resolve_character_list_ref(request.reference_character_tokens.clone()),
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
@@ -232,45 +228,6 @@ fn plan_duration(
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance 2.0 Pro supports output resolutions: 480p, 720p, 1080p.
|
||||
fn plan_output_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<CommonResolution>, ArtcraftRouterError> {
|
||||
match resolution {
|
||||
None => Ok(None),
|
||||
|
||||
// Direct mappings
|
||||
Some(CommonResolution::FourEightyP)
|
||||
| Some(CommonResolution::SevenTwentyP)
|
||||
| Some(CommonResolution::TenEightyP) => Ok(resolution),
|
||||
|
||||
// Mismatches
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
|
||||
// HalfK → 480p (up); OneK/TwoK/ThreeK/FourK → 1080p (max)
|
||||
Ok(Some(match unsupported {
|
||||
CommonResolution::HalfK => CommonResolution::FourEightyP,
|
||||
_ => CommonResolution::TenEightyP,
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
// HalfK → 480p (min); OneK/TwoK/ThreeK/FourK → 1080p (closest below)
|
||||
Ok(Some(match unsupported {
|
||||
CommonResolution::HalfK => CommonResolution::FourEightyP,
|
||||
_ => CommonResolution::TenEightyP,
|
||||
}))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -286,7 +243,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_16x9() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
..base_video_request()
|
||||
};
|
||||
@@ -296,7 +253,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_9x16() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
|
||||
..base_video_request()
|
||||
};
|
||||
@@ -306,7 +263,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_direct_square() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
..base_video_request()
|
||||
};
|
||||
@@ -321,7 +278,7 @@ mod tests {
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideThreeByTwo),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_video_request()
|
||||
@@ -336,7 +293,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_error_out_on_unsupported() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideThreeByTwo),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
..base_video_request()
|
||||
@@ -355,7 +312,7 @@ mod tests {
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
] {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
video_batch_count: Some(0),
|
||||
request_mismatch_mitigation_strategy: strategy,
|
||||
..base_video_request()
|
||||
@@ -370,22 +327,22 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn batch_count_direct_mapping() {
|
||||
let req = GenerateVideoRequestBuilder { video_batch_count: Some(1), ..base_video_request() };
|
||||
let req = GenerateVideoRequest { video_batch_count: Some(1), ..base_video_request() };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = req.build().unwrap() else { panic!("wrong variant") };
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::One));
|
||||
|
||||
let req = GenerateVideoRequestBuilder { video_batch_count: Some(2), ..base_video_request() };
|
||||
let req = GenerateVideoRequest { video_batch_count: Some(2), ..base_video_request() };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = req.build().unwrap() else { panic!("wrong variant") };
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::Two));
|
||||
|
||||
let req = GenerateVideoRequestBuilder { video_batch_count: Some(4), ..base_video_request() };
|
||||
let req = GenerateVideoRequest { video_batch_count: Some(4), ..base_video_request() };
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0(plan) = req.build().unwrap() else { panic!("wrong variant") };
|
||||
assert!(matches!(plan.batch_count, Seedance2p0BatchCount::Four));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_three_upgrade_rounds_to_four() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
video_batch_count: Some(3),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_video_request()
|
||||
@@ -396,7 +353,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn batch_count_three_downgrade_rounds_to_two() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
video_batch_count: Some(3),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
|
||||
..base_video_request()
|
||||
@@ -407,7 +364,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn url_image_ref_returns_error() {
|
||||
let request = GenerateVideoRequestBuilder {
|
||||
let request = GenerateVideoRequest {
|
||||
start_frame: Some(ImageRef::Url("https://example.com/image.jpg".to_string())),
|
||||
..base_video_request()
|
||||
};
|
||||
|
||||
+13
-54
@@ -1,13 +1,12 @@
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::PlanArtcraftSeedance2p0;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{
|
||||
@@ -17,7 +16,7 @@ use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
pub fn plan_generate_video_artcraft_seedance2p0_fast(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
@@ -28,7 +27,6 @@ pub fn plan_generate_video_artcraft_seedance2p0_fast(
|
||||
let reference_audio = resolve_audio_list_ref(request.reference_audio.clone())?;
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let resolution = plan_output_resolution(request.resolution, strategy)?;
|
||||
let batch_count = plan_batch_count(request.video_batch_count, strategy)?;
|
||||
let duration_seconds = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
@@ -41,7 +39,6 @@ pub fn plan_generate_video_artcraft_seedance2p0_fast(
|
||||
reference_audio,
|
||||
reference_characters: resolve_character_list_ref(request.reference_character_tokens.clone()),
|
||||
aspect_ratio,
|
||||
resolution,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
@@ -202,44 +199,6 @@ fn plan_duration(
|
||||
}
|
||||
}
|
||||
|
||||
// Seedance 2.0 Fast supports output resolutions: 480p and 720p only.
|
||||
fn plan_output_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<CommonResolution>, ArtcraftRouterError> {
|
||||
match resolution {
|
||||
None => Ok(None),
|
||||
|
||||
// Direct mappings
|
||||
Some(CommonResolution::FourEightyP)
|
||||
| Some(CommonResolution::SevenTwentyP) => Ok(resolution),
|
||||
|
||||
// Mismatches
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
|
||||
// HalfK → 480p (up); TenEightyP/OneK+ → 720p (max)
|
||||
Ok(Some(match unsupported {
|
||||
CommonResolution::HalfK => CommonResolution::FourEightyP,
|
||||
_ => CommonResolution::SevenTwentyP,
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
// HalfK → 480p (min); TenEightyP/OneK+ → 720p (closest below)
|
||||
Ok(Some(match unsupported {
|
||||
CommonResolution::HalfK => CommonResolution::FourEightyP,
|
||||
_ => CommonResolution::SevenTwentyP,
|
||||
}))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
@@ -249,14 +208,14 @@ mod tests {
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{
|
||||
Seedance2p0AspectRatio, Seedance2p0BatchCount,
|
||||
};
|
||||
|
||||
fn base_request() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
fn base_request() -> GenerateVideoRequest {
|
||||
GenerateVideoRequest {
|
||||
model: CommonVideoModel::Seedance2p0Fast,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space".to_string()),
|
||||
@@ -295,7 +254,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_16x9() {
|
||||
let req = GenerateVideoRequestBuilder {
|
||||
let req = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
..base_request()
|
||||
};
|
||||
@@ -306,7 +265,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_9x16() {
|
||||
let req = GenerateVideoRequestBuilder {
|
||||
let req = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
|
||||
..base_request()
|
||||
};
|
||||
@@ -317,7 +276,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn aspect_ratio_square() {
|
||||
let req = GenerateVideoRequestBuilder {
|
||||
let req = GenerateVideoRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
..base_request()
|
||||
};
|
||||
@@ -336,7 +295,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn batch_count_2() {
|
||||
let req = GenerateVideoRequestBuilder { video_batch_count: Some(2), ..base_request() };
|
||||
let req = GenerateVideoRequest { video_batch_count: Some(2), ..base_request() };
|
||||
let plan = req.build().unwrap();
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0Fast(p) = plan else { panic!("wrong variant") };
|
||||
assert!(matches!(p.batch_count, Seedance2p0BatchCount::Two));
|
||||
@@ -344,7 +303,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn batch_count_4() {
|
||||
let req = GenerateVideoRequestBuilder { video_batch_count: Some(4), ..base_request() };
|
||||
let req = GenerateVideoRequest { video_batch_count: Some(4), ..base_request() };
|
||||
let plan = req.build().unwrap();
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0Fast(p) = plan else { panic!("wrong variant") };
|
||||
assert!(matches!(p.batch_count, Seedance2p0BatchCount::Four));
|
||||
@@ -352,7 +311,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn duration_in_range() {
|
||||
let req = GenerateVideoRequestBuilder { duration_seconds: Some(10), ..base_request() };
|
||||
let req = GenerateVideoRequest { duration_seconds: Some(10), ..base_request() };
|
||||
let plan = req.build().unwrap();
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0Fast(p) = plan else { panic!("wrong variant") };
|
||||
assert_eq!(p.duration_seconds, Some(10));
|
||||
@@ -360,7 +319,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max() {
|
||||
let req = GenerateVideoRequestBuilder { duration_seconds: Some(99), ..base_request() };
|
||||
let req = GenerateVideoRequest { duration_seconds: Some(99), ..base_request() };
|
||||
let plan = req.build().unwrap();
|
||||
let VideoGenerationPlan::ArtcraftSeedance2p0Fast(p) = plan else { panic!("wrong variant") };
|
||||
assert_eq!(p.duration_seconds, Some(15));
|
||||
@@ -368,7 +327,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn url_image_ref_returns_error() {
|
||||
let req = GenerateVideoRequestBuilder {
|
||||
let req = GenerateVideoRequest {
|
||||
start_frame: Some(ImageRef::Url("https://example.com/image.jpg".to_string())),
|
||||
..base_request()
|
||||
};
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::generate_seedance_1_0_lite_image_to_video::{
|
||||
GenerateSeedance10LiteAspectRatio, GenerateSeedance10LiteDuration,
|
||||
@@ -24,7 +24,7 @@ pub struct PlanArtcraftSeedance10Lite {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_seedance_1_0_lite(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::sora_2_multi_function_video_gen::{
|
||||
Sora2MultiFunctionVideoGenAspectRatio, Sora2MultiFunctionVideoGenDuration,
|
||||
@@ -23,7 +23,7 @@ pub struct PlanArtcraftSora2 {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_sora_2(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::sora_2_pro_multi_function_video_gen::{
|
||||
Sora2ProMultiFunctionVideoGenAspectRatio, Sora2ProMultiFunctionVideoGenDuration,
|
||||
@@ -23,7 +23,7 @@ pub struct PlanArtcraftSora2Pro {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_sora_2_pro(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::generate_veo_2_image_to_video::{
|
||||
GenerateVeo2AspectRatio, GenerateVeo2Duration,
|
||||
@@ -23,7 +23,7 @@ pub struct PlanArtcraftVeo2 {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_veo_2(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::generate_veo_3_image_to_video::{
|
||||
GenerateVeo3AspectRatio, GenerateVeo3Duration, GenerateVeo3Resolution,
|
||||
@@ -29,7 +29,7 @@ pub struct PlanArtcraftVeo3 {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_veo_3(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::generate_veo_3_fast_image_to_video::{
|
||||
GenerateVeo3FastAspectRatio, GenerateVeo3FastDuration, GenerateVeo3FastResolution,
|
||||
@@ -23,7 +23,7 @@ pub struct PlanArtcraftVeo3Fast {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_veo_3_fast(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::veo_3p1_multi_function_video_gen::{
|
||||
Veo3p1MultiFunctionVideoGenAspectRatio, Veo3p1MultiFunctionVideoGenDuration,
|
||||
@@ -26,14 +26,14 @@ pub struct PlanArtcraftVeo3p1 {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_veo_3p1(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let plan = build_plan_artcraft_veo_3p1(request)?;
|
||||
Ok(VideoGenerationPlan::ArtcraftVeo3p1(plan))
|
||||
}
|
||||
|
||||
pub(crate) fn build_plan_artcraft_veo_3p1(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<PlanArtcraftVeo3p1, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use artcraft_api_defs::generate::video::multi_function::veo_3p1_fast_multi_function_video_gen::{
|
||||
Veo3p1FastMultiFunctionVideoGenAspectRatio, Veo3p1FastMultiFunctionVideoGenDuration,
|
||||
@@ -26,7 +26,7 @@ pub struct PlanArtcraftVeo3p1Fast {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_artcraft_veo_3p1_fast(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use fal_client::requests::webhook::video::image::enqueue_kling_v1p6_pro_image_to_video_webhook::{
|
||||
Kling1p6ProAspectRatio, Kling1p6ProDuration,
|
||||
@@ -19,7 +19,7 @@ pub struct PlanFalKling16Pro {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_fal_kling_1_6_pro(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_kling_1_6_pro::require_url;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use fal_client::requests::webhook::video::image::enqueue_kling_v2p1_master_image_to_video_webhook::{
|
||||
@@ -18,7 +18,7 @@ pub struct PlanFalKling21Master {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_fal_kling_2_1_master(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_kling_1_6_pro::{optional_url, require_url};
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use fal_client::requests::webhook::video::image::enqueue_kling_v2p1_pro_image_to_video_webhook::{
|
||||
@@ -19,7 +19,7 @@ pub struct PlanFalKling21Pro {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_fal_kling_2_1_pro(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_kling_1_6_pro::optional_url;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
|
||||
@@ -38,7 +38,7 @@ pub struct PlanFalKling2p5TurboPro {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_fal_kling_2_5_turbo_pro(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_kling_1_6_pro::optional_url;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
|
||||
@@ -36,7 +36,7 @@ pub struct PlanFalKling2p6Pro {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_fal_kling_2_6_pro(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@ use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_kling_1_6_pro::optional_url;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
|
||||
@@ -37,7 +37,7 @@ pub struct PlanFalKling3p0Pro {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_fal_kling_3p0_pro(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let inner = build_kling_3p0_plan(request, "Kling 3.0 Pro")?;
|
||||
Ok(VideoGenerationPlan::FalKling3p0Pro(PlanFalKling3p0Pro {
|
||||
@@ -60,7 +60,7 @@ pub(crate) struct Kling3p0Common {
|
||||
}
|
||||
|
||||
pub(crate) fn build_kling_3p0_plan(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
_model_label: &'static str,
|
||||
) -> Result<Kling3p0Common, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_kling_3p0_pro::{
|
||||
build_kling_3p0_plan, FalKling3p0AspectRatio, FalKling3p0Duration, FalKling3p0Mode,
|
||||
};
|
||||
@@ -16,7 +16,7 @@ pub struct PlanFalKling3p0Standard {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_fal_kling_3p0_standard(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let inner = build_kling_3p0_plan(request, "Kling 3.0 Standard")?;
|
||||
Ok(VideoGenerationPlan::FalKling3p0Standard(PlanFalKling3p0Standard {
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ use crate::api::common_resolution::CommonResolution;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_kling_1_6_pro::{optional_url, require_url};
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use fal_client::requests::webhook::video::image::enqueue_seedance_1_lite_image_to_video_webhook::{
|
||||
@@ -21,7 +21,7 @@ pub struct PlanFalSeedance10Lite {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_fal_seedance_1_0_lite(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ use crate::api::common_resolution::CommonResolution;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_kling_1_6_pro::optional_url;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
|
||||
@@ -58,7 +58,7 @@ pub struct PlanFalSeedance1p5Pro {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_fal_seedance_1p5_pro(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ use crate::api::common_resolution::CommonResolution;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_kling_1_6_pro::optional_url;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
|
||||
@@ -44,7 +44,7 @@ pub struct PlanFalSora2 {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_fal_sora_2(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ use crate::api::common_resolution::CommonResolution;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_kling_1_6_pro::optional_url;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
|
||||
@@ -44,7 +44,7 @@ pub struct PlanFalSora2Pro {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_fal_sora_2_pro(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_2_image_to_video_webhook::{
|
||||
Veo2AspectRatio, Veo2Duration,
|
||||
@@ -29,7 +29,7 @@ pub struct PlanFalVeo2 {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_fal_veo_2(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_3_image_to_video_webhook::{
|
||||
Veo3I2vAspectRatio, Veo3I2vDuration, Veo3I2vResolution,
|
||||
@@ -70,7 +70,7 @@ pub struct PlanFalVeo3 {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_fal_veo_3(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use fal_client::requests::webhook::video::image::enqueue_veo_3_fast_image_to_video_webhook::{
|
||||
@@ -21,7 +21,7 @@ pub struct PlanFalVeo3Fast {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_fal_veo_3_fast(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ use crate::api::image_ref::ImageRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
|
||||
/// Three-way internal selection of which Fal Veo 3.1 endpoint to call.
|
||||
@@ -49,14 +49,14 @@ pub struct PlanFalVeo3p1 {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_fal_veo_3p1(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let plan = build_plan_fal_veo_3p1(request, "Veo 3.1")?;
|
||||
Ok(VideoGenerationPlan::FalVeo3p1(plan))
|
||||
}
|
||||
|
||||
pub(crate) fn build_plan_fal_veo_3p1(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
model_label: &'static str,
|
||||
) -> Result<PlanFalVeo3p1, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::plan::fal::plan_generate_video_fal_veo_3p1::{
|
||||
build_plan_fal_veo_3p1, PlanFalVeo3p1,
|
||||
};
|
||||
@@ -14,7 +14,7 @@ pub struct PlanFalVeo3p1Fast {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_fal_veo_3p1_fast(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let inner = build_plan_fal_veo_3p1(request, "Veo 3.1 Fast")?;
|
||||
Ok(VideoGenerationPlan::FalVeo3p1Fast(PlanFalVeo3p1Fast { inner }))
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ 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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use muapi_client::requests::seedance_2p0_image_to_video::seedance_2p0_image_to_video::{
|
||||
AspectRatio as I2vAspectRatio,
|
||||
@@ -41,7 +41,7 @@ pub struct PlanMuapiSeedance2p0 {
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_muapi_seedance2p0(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
|
||||
+38
-52
@@ -6,22 +6,25 @@ use crate::api::video_list_ref::VideoListRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use seedance2pro_client::requests::generate_video::generate_video::{
|
||||
KinoviGenerateVideoRequest as SeedanceGenerateVideoRequest,
|
||||
KinoviBatchCount, KinoviModelType, KinoviOutputResolution, KinoviAspectRatio,
|
||||
};
|
||||
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use seedance2pro_client::requests::generate_video::generate_video::{KinoviBatchCount, KinoviResolution};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanSeedance2proSeedance2p0 {
|
||||
pub request: SeedanceGenerateVideoRequest,
|
||||
pub prompt: Option<String>,
|
||||
pub start_frame_url: Option<String>,
|
||||
pub end_frame_url: Option<String>,
|
||||
pub reference_image_urls: Option<Vec<String>>,
|
||||
pub reference_video_urls: Option<Vec<String>>,
|
||||
pub reference_audio_urls: Option<Vec<String>>,
|
||||
pub resolution: KinoviResolution,
|
||||
pub duration_seconds: u8,
|
||||
pub batch_count: KinoviBatchCount,
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_seedance2pro_seedance2p0(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
@@ -31,40 +34,23 @@ pub fn plan_generate_video_seedance2pro_seedance2p0(
|
||||
let reference_video_urls = resolve_video_list_ref_urls(request.reference_videos.clone())?;
|
||||
let reference_audio_urls = resolve_audio_list_ref_urls(request.reference_audio.clone())?;
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let resolution = plan_resolution(request.aspect_ratio, strategy)?;
|
||||
let batch_count = plan_batch_count(request.video_batch_count, strategy)?;
|
||||
let duration_seconds = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
let output_resolution = request.resolution.map(map_common_resolution_to_kinovi);
|
||||
|
||||
Ok(VideoGenerationPlan::Seedance2proSeedance2p0(PlanSeedance2proSeedance2p0 {
|
||||
request: SeedanceGenerateVideoRequest {
|
||||
model_type: KinoviModelType::Seedance2Pro,
|
||||
prompt: request.prompt.clone().unwrap_or_default(),
|
||||
aspect_ratio,
|
||||
output_resolution,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
start_frame_url,
|
||||
end_frame_url,
|
||||
reference_image_urls,
|
||||
reference_video_urls,
|
||||
reference_audio_urls,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
},
|
||||
prompt: request.prompt.clone(),
|
||||
start_frame_url,
|
||||
end_frame_url,
|
||||
reference_image_urls,
|
||||
reference_video_urls,
|
||||
reference_audio_urls,
|
||||
resolution,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
}))
|
||||
}
|
||||
|
||||
fn map_common_resolution_to_kinovi(resolution: CommonResolution) -> KinoviOutputResolution {
|
||||
match resolution {
|
||||
CommonResolution::FourEightyP => KinoviOutputResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP => KinoviOutputResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP => KinoviOutputResolution::TenEightyP,
|
||||
_ => KinoviOutputResolution::SevenTwentyP,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_image_ref_url(
|
||||
image_ref: Option<ImageRef>,
|
||||
) -> Result<Option<String>, ArtcraftRouterError> {
|
||||
@@ -118,29 +104,29 @@ fn resolve_audio_list_ref_urls(
|
||||
//
|
||||
// All supported ratios cost the same, so PayMoreUpgrade and PayLessDowngrade both
|
||||
// select the nearest match rather than rounding in a specific direction.
|
||||
fn plan_aspect_ratio(
|
||||
fn plan_resolution(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<KinoviAspectRatio, ArtcraftRouterError> {
|
||||
) -> Result<KinoviResolution, ArtcraftRouterError> {
|
||||
match aspect_ratio {
|
||||
// No preference or auto — default to landscape
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(KinoviAspectRatio::Landscape16x9),
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(KinoviResolution::Landscape16x9),
|
||||
|
||||
// Direct mappings
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
|
||||
Ok(KinoviAspectRatio::Landscape16x9)
|
||||
Ok(KinoviResolution::Landscape16x9)
|
||||
}
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
|
||||
Ok(KinoviAspectRatio::Portrait9x16)
|
||||
Ok(KinoviResolution::Portrait9x16)
|
||||
}
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
|
||||
Ok(KinoviAspectRatio::Square1x1)
|
||||
Ok(KinoviResolution::Square1x1)
|
||||
}
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(KinoviAspectRatio::Standard4x3),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(KinoviAspectRatio::Portrait3x4),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(KinoviResolution::Standard4x3),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(KinoviResolution::Portrait3x4),
|
||||
|
||||
// Mismatches — apply strategy
|
||||
Some(unsupported) => match strategy {
|
||||
@@ -159,15 +145,15 @@ fn plan_aspect_ratio(
|
||||
}
|
||||
|
||||
/// Pick the nearest supported resolution by AR value (width / height).
|
||||
fn nearest_resolution(aspect_ratio: CommonAspectRatio) -> KinoviAspectRatio {
|
||||
fn nearest_resolution(aspect_ratio: CommonAspectRatio) -> KinoviResolution {
|
||||
match aspect_ratio {
|
||||
CommonAspectRatio::WideFiveByFour => KinoviAspectRatio::Standard4x3, // 1.25, nearest 1.33
|
||||
CommonAspectRatio::WideThreeByTwo => KinoviAspectRatio::Standard4x3, // 1.50, nearest 1.33
|
||||
CommonAspectRatio::WideTwentyOneByNine => KinoviAspectRatio::Landscape16x9, // 2.33, nearest 1.78
|
||||
CommonAspectRatio::TallFourByFive => KinoviAspectRatio::Portrait3x4, // 0.80, nearest 0.75
|
||||
CommonAspectRatio::TallTwoByThree => KinoviAspectRatio::Portrait3x4, // 0.67, nearest 0.75
|
||||
CommonAspectRatio::TallNineByTwentyOne => KinoviAspectRatio::Portrait9x16, // 0.43, nearest 0.56
|
||||
_ => KinoviAspectRatio::Square1x1,
|
||||
CommonAspectRatio::WideFiveByFour => KinoviResolution::Standard4x3, // 1.25, nearest 1.33
|
||||
CommonAspectRatio::WideThreeByTwo => KinoviResolution::Standard4x3, // 1.50, nearest 1.33
|
||||
CommonAspectRatio::WideTwentyOneByNine => KinoviResolution::Landscape16x9, // 2.33, nearest 1.78
|
||||
CommonAspectRatio::TallFourByFive => KinoviResolution::Portrait3x4, // 0.80, nearest 0.75
|
||||
CommonAspectRatio::TallTwoByThree => KinoviResolution::Portrait3x4, // 0.67, nearest 0.75
|
||||
CommonAspectRatio::TallNineByTwentyOne => KinoviResolution::Portrait9x16, // 0.43, nearest 0.56
|
||||
_ => KinoviResolution::Square1x1,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+38
-52
@@ -6,22 +6,25 @@ use crate::api::video_list_ref::VideoListRef;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use seedance2pro_client::requests::generate_video::generate_video::{
|
||||
KinoviGenerateVideoRequest as SeedanceGenerateVideoRequest,
|
||||
KinoviBatchCount, KinoviModelType, KinoviOutputResolution, KinoviAspectRatio,
|
||||
};
|
||||
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use seedance2pro_client::requests::generate_video::generate_video::{KinoviBatchCount, KinoviResolution};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanSeedance2proSeedance2p0Fast {
|
||||
pub request: SeedanceGenerateVideoRequest,
|
||||
pub prompt: Option<String>,
|
||||
pub start_frame_url: Option<String>,
|
||||
pub end_frame_url: Option<String>,
|
||||
pub reference_image_urls: Option<Vec<String>>,
|
||||
pub reference_video_urls: Option<Vec<String>>,
|
||||
pub reference_audio_urls: Option<Vec<String>>,
|
||||
pub resolution: KinoviResolution,
|
||||
pub duration_seconds: u8,
|
||||
pub batch_count: KinoviBatchCount,
|
||||
}
|
||||
|
||||
pub fn plan_generate_video_seedance2pro_seedance2p0_fast(
|
||||
request: &GenerateVideoRequestBuilder,
|
||||
request: &GenerateVideoRequest,
|
||||
) -> Result<VideoGenerationPlan, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
@@ -31,40 +34,23 @@ pub fn plan_generate_video_seedance2pro_seedance2p0_fast(
|
||||
let reference_video_urls = resolve_video_list_ref_urls(request.reference_videos.clone())?;
|
||||
let reference_audio_urls = resolve_audio_list_ref_urls(request.reference_audio.clone())?;
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
|
||||
let resolution = plan_resolution(request.aspect_ratio, strategy)?;
|
||||
let batch_count = plan_batch_count(request.video_batch_count, strategy)?;
|
||||
let duration_seconds = plan_duration(request.duration_seconds, strategy)?;
|
||||
|
||||
let output_resolution = request.resolution.map(map_common_resolution_to_kinovi);
|
||||
|
||||
Ok(VideoGenerationPlan::Seedance2proSeedance2p0Fast(PlanSeedance2proSeedance2p0Fast {
|
||||
request: SeedanceGenerateVideoRequest {
|
||||
model_type: KinoviModelType::Seedance2Fast,
|
||||
prompt: request.prompt.clone().unwrap_or_default(),
|
||||
aspect_ratio,
|
||||
output_resolution,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
start_frame_url,
|
||||
end_frame_url,
|
||||
reference_image_urls,
|
||||
reference_video_urls,
|
||||
reference_audio_urls,
|
||||
character_ids: None,
|
||||
use_face_blur_hack: None,
|
||||
},
|
||||
prompt: request.prompt.clone(),
|
||||
start_frame_url,
|
||||
end_frame_url,
|
||||
reference_image_urls,
|
||||
reference_video_urls,
|
||||
reference_audio_urls,
|
||||
resolution,
|
||||
duration_seconds,
|
||||
batch_count,
|
||||
}))
|
||||
}
|
||||
|
||||
fn map_common_resolution_to_kinovi(resolution: CommonResolution) -> KinoviOutputResolution {
|
||||
match resolution {
|
||||
CommonResolution::FourEightyP => KinoviOutputResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP => KinoviOutputResolution::SevenTwentyP,
|
||||
CommonResolution::TenEightyP => KinoviOutputResolution::SevenTwentyP, // NB: Kinovi doesn't have 1080p fast currently
|
||||
_ => KinoviOutputResolution::SevenTwentyP,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_image_ref_url(
|
||||
image_ref: Option<ImageRef>,
|
||||
) -> Result<Option<String>, ArtcraftRouterError> {
|
||||
@@ -113,27 +99,27 @@ fn resolve_audio_list_ref_urls(
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
fn plan_resolution(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<KinoviAspectRatio, ArtcraftRouterError> {
|
||||
) -> Result<KinoviResolution, ArtcraftRouterError> {
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(KinoviAspectRatio::Landscape16x9),
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(KinoviResolution::Landscape16x9),
|
||||
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
|
||||
Ok(KinoviAspectRatio::Landscape16x9)
|
||||
Ok(KinoviResolution::Landscape16x9)
|
||||
}
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
|
||||
Ok(KinoviAspectRatio::Portrait9x16)
|
||||
Ok(KinoviResolution::Portrait9x16)
|
||||
}
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
|
||||
Ok(KinoviAspectRatio::Square1x1)
|
||||
Ok(KinoviResolution::Square1x1)
|
||||
}
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(KinoviAspectRatio::Standard4x3),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(KinoviAspectRatio::Portrait3x4),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(KinoviResolution::Standard4x3),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(KinoviResolution::Portrait3x4),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
@@ -150,15 +136,15 @@ fn plan_aspect_ratio(
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_resolution(aspect_ratio: CommonAspectRatio) -> KinoviAspectRatio {
|
||||
fn nearest_resolution(aspect_ratio: CommonAspectRatio) -> KinoviResolution {
|
||||
match aspect_ratio {
|
||||
CommonAspectRatio::WideFiveByFour => KinoviAspectRatio::Standard4x3,
|
||||
CommonAspectRatio::WideThreeByTwo => KinoviAspectRatio::Standard4x3,
|
||||
CommonAspectRatio::WideTwentyOneByNine => KinoviAspectRatio::Landscape16x9,
|
||||
CommonAspectRatio::TallFourByFive => KinoviAspectRatio::Portrait3x4,
|
||||
CommonAspectRatio::TallTwoByThree => KinoviAspectRatio::Portrait3x4,
|
||||
CommonAspectRatio::TallNineByTwentyOne => KinoviAspectRatio::Portrait9x16,
|
||||
_ => KinoviAspectRatio::Square1x1,
|
||||
CommonAspectRatio::WideFiveByFour => KinoviResolution::Standard4x3,
|
||||
CommonAspectRatio::WideThreeByTwo => KinoviResolution::Standard4x3,
|
||||
CommonAspectRatio::WideTwentyOneByNine => KinoviResolution::Landscape16x9,
|
||||
CommonAspectRatio::TallFourByFive => KinoviResolution::Portrait3x4,
|
||||
CommonAspectRatio::TallTwoByThree => KinoviResolution::Portrait3x4,
|
||||
CommonAspectRatio::TallNineByTwentyOne => KinoviResolution::Portrait9x16,
|
||||
_ => KinoviResolution::Square1x1,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/// The result of estimating the cost of a video generation plan.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug)]
|
||||
pub struct VideoGenerationCostEstimate {
|
||||
pub cost_in_credits: Option<u64>,
|
||||
pub cost_in_usd_cents: Option<u64>,
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
# generate_video_v2
|
||||
|
||||
This module implements the v2 video generation pipeline. It replaces the v1 system
|
||||
(in `generate_video/`) with a two-phase approach that separates request planning from
|
||||
execution, allowing cost estimation and media uploads to happen at the right time.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
```
|
||||
GenerateVideoRequestBuilder
|
||||
│
|
||||
▼ build2()
|
||||
VideoGenerationDraftOrRequest
|
||||
│
|
||||
├─ Draft(VideoGenerationDraftRequest)
|
||||
│ │
|
||||
│ ▼ estimate_cost() ← cost estimate from draft state
|
||||
│ │
|
||||
│ ▼ finalize(context) ← uploads media, resolves tokens
|
||||
│ │
|
||||
│ ▼
|
||||
│ VideoGenerationRequest
|
||||
│ │
|
||||
│ ▼ estimate_cost() ← cost estimate from finalized request
|
||||
│ │
|
||||
│ ▼ send_request() ← fires the API call
|
||||
│ │
|
||||
│ ▼
|
||||
│ GenerateVideoResponse
|
||||
│
|
||||
└─ Request(VideoGenerationRequest) ← skips draft phase (used by Artcraft provider)
|
||||
```
|
||||
|
||||
### Key types
|
||||
|
||||
- **`GenerateVideoRequestBuilder`** — generic builder with provider/model/prompt/resolution/etc.
|
||||
Located in `generate_video/generate_video_request_builder.rs`. The `use_new_builder()` method
|
||||
gates which provider+model combos use the v2 path. `build2()` dispatches to the correct
|
||||
provider's build function.
|
||||
|
||||
- **`VideoGenerationDraftOrRequest`** — the output of `build2()`. Either `Draft` (for providers
|
||||
that need media upload/resolution, like Kinovi) or `Request` (for providers that work with
|
||||
media file tokens directly, like Artcraft).
|
||||
|
||||
- **`VideoGenerationDraftRequest`** — enum with one variant per provider+model. Holds
|
||||
materialized settings (resolution, duration, batch count) plus unresolved media references.
|
||||
Can estimate cost without network calls.
|
||||
|
||||
- **`VideoGenerationDraftContext`** — context needed during `finalize()`: the provider client,
|
||||
media file token-to-URL map, and character token-to-ID map.
|
||||
|
||||
- **`VideoGenerationRequest`** — enum with one variant per provider+model. Holds a fully
|
||||
materialized request ready to send. All media URLs are resolved and uploaded.
|
||||
|
||||
### Cost estimation
|
||||
|
||||
Cost can be estimated at two stages:
|
||||
1. **From draft** — before `finalize()`. Uses the planned resolution/duration/batch count.
|
||||
Good for showing the user a price before committing.
|
||||
2. **From request** — after `finalize()`. Uses the final materialized request. Should match
|
||||
the draft estimate for the same parameters.
|
||||
|
||||
## Module layout
|
||||
|
||||
```
|
||||
generate_video_v2/
|
||||
├── mod.rs
|
||||
├── video_generation_draft.rs ← VideoGenerationDraftRequest enum
|
||||
├── video_generation_draft_context.rs ← context for finalize()
|
||||
├── video_generation_draft_or_request.rs
|
||||
├── video_generation_request.rs ← VideoGenerationRequest enum
|
||||
└── providers/
|
||||
├── artcraft/ ← Artcraft provider (uses media file tokens directly)
|
||||
│ ├── seedance_2p0/ ← Seedance 2.0 Pro model (skips draft, returns Request directly)
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── build.rs ← builder → Request conversion + plan helpers
|
||||
│ │ ├── cost.rs ← standalone cost estimation (independent of seedance2pro_client)
|
||||
│ │ └── request.rs ← request state + send() via Artcraft multi-function API
|
||||
│ └── seedance_2p0_fast/ ← Seedance 2.0 Fast model (skips draft, 480p/720p only)
|
||||
│ ├── mod.rs
|
||||
│ ├── build.rs ← builder → Request conversion + plan helpers (no 1080p)
|
||||
│ ├── cost.rs ← standalone cost estimation with Fast-specific pricing
|
||||
│ └── request.rs ← request state + send() via Artcraft omni-gen API
|
||||
└── kinovi/ ← Kinovi/Seedance2Pro provider
|
||||
├── mod.rs
|
||||
├── resolve.rs ← shared: media token resolution, upload helpers
|
||||
├── upload.rs ← shared: download + re-upload to Seedance2Pro CDN
|
||||
├── seedance_2p0/ ← Seedance 2.0 Pro model
|
||||
│ ├── mod.rs
|
||||
│ ├── build.rs ← builder → draft conversion + plan helpers
|
||||
│ ├── cost.rs ← cost estimation state
|
||||
│ ├── draft.rs ← draft state + finalize (to_request)
|
||||
│ └── request.rs ← request state + send()
|
||||
└── seedance_2p0_fast/ ← Seedance 2.0 Fast model
|
||||
├── mod.rs
|
||||
├── build.rs
|
||||
├── cost.rs
|
||||
├── draft.rs
|
||||
└── request.rs
|
||||
```
|
||||
|
||||
## Adding a new provider or model
|
||||
|
||||
1. **Create the module** under `providers/{provider}/{model}/` with these files:
|
||||
- `mod.rs` — declares sub-modules
|
||||
- `build.rs` — `build_{provider}_{model}(builder) -> VideoGenerationDraftOrRequest`.
|
||||
Contains `plan_*` helpers for aspect ratio, resolution, batch count, duration.
|
||||
- `draft.rs` — draft state struct with `to_request()` async method for finalization.
|
||||
**Not needed** if the provider uses media tokens directly (no upload/resolution needed);
|
||||
in that case `build.rs` returns `VideoGenerationDraftOrRequest::Request(...)` directly.
|
||||
- `cost.rs` — cost state with `from_request()` and `estimate_cost()`. Add `from_draft()`
|
||||
if the provider uses the draft phase.
|
||||
- `request.rs` — request state with `send()` async method.
|
||||
|
||||
2. **Register the module** in the parent provider's `mod.rs`.
|
||||
|
||||
3. **Add enum variants** in:
|
||||
- `video_generation_draft.rs` — `VideoGenerationDraftRequest` enum + `estimate_cost` + `finalize`
|
||||
(skip if provider has no draft phase)
|
||||
- `video_generation_request.rs` — `VideoGenerationRequest` enum + `estimate_cost` + `send_request`
|
||||
|
||||
4. **Wire into the builder** in `generate_video_request_builder.rs`:
|
||||
- Add the `(Provider, Model)` pair to `use_new_builder()` returning `true`
|
||||
- Add the dispatch arm in `build2()`
|
||||
|
||||
5. **Tests**: each `build.rs` and `cost.rs` should have comprehensive tests. `request.rs`
|
||||
should have `#[ignore]` live API tests for manual verification.
|
||||
@@ -1,5 +0,0 @@
|
||||
pub mod providers;
|
||||
pub mod video_generation_request;
|
||||
pub mod video_generation_draft;
|
||||
pub mod video_generation_draft_or_request;
|
||||
pub mod video_generation_draft_context;
|
||||
-330
@@ -1,330 +0,0 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
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_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::resolve::{
|
||||
resolve_audio_list_ref, resolve_character_list_ref, resolve_image_list_ref,
|
||||
resolve_image_ref, resolve_video_list_ref,
|
||||
};
|
||||
use crate::generate::generate_video_v2::providers::artcraft::happy_horse_1p0::request::ArtcraftHappyHorse1p0RequestState;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
pub fn build_artcraft_happy_horse_1p0(mut builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy)?;
|
||||
let resolution = plan_output_resolution(builder.resolution.take(), strategy)?;
|
||||
let batch_count = plan_batch_count(builder.video_batch_count.take(), strategy)?;
|
||||
let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?;
|
||||
let prompt = builder.prompt.take();
|
||||
|
||||
let start_frame = resolve_image_ref(builder.start_frame.take())?;
|
||||
let end_frame = resolve_image_ref(builder.end_frame.take())?;
|
||||
let reference_images = resolve_image_list_ref(builder.reference_images.take())?;
|
||||
let reference_videos = resolve_video_list_ref(builder.reference_videos.take())?;
|
||||
let reference_audio = resolve_audio_list_ref(builder.reference_audio.take())?;
|
||||
let reference_characters = resolve_character_list_ref(builder.reference_character_tokens.take());
|
||||
let idempotency_token = builder.get_or_generate_idempotency_token();
|
||||
|
||||
let request = OmniGenVideoCostAndGenerateRequest {
|
||||
model: Some(CommonVideoModelEnum::HappyHorse1p0),
|
||||
idempotency_token: Some(idempotency_token),
|
||||
prompt,
|
||||
start_frame_image_media_token: start_frame,
|
||||
end_frame_image_media_token: end_frame,
|
||||
reference_image_media_tokens: reference_images,
|
||||
reference_video_media_tokens: reference_videos,
|
||||
reference_audio_media_tokens: reference_audio,
|
||||
reference_character_tokens: reference_characters,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
duration_seconds: duration_seconds.map(|d| d as u16),
|
||||
video_batch_count: Some(batch_count),
|
||||
negative_prompt: None,
|
||||
generate_audio: None,
|
||||
quality: None,
|
||||
};
|
||||
|
||||
let state = ArtcraftHappyHorse1p0RequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftHappyHorse1p0(state)))
|
||||
}
|
||||
|
||||
// ── Plan helpers ──
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<CommonAspectRatioEnum>, ArtcraftRouterError> {
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
|
||||
Ok(Some(CommonAspectRatioEnum::WideSixteenByNine))
|
||||
}
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
|
||||
Ok(Some(CommonAspectRatioEnum::TallNineBySixteen))
|
||||
}
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
|
||||
Ok(Some(CommonAspectRatioEnum::Square))
|
||||
}
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(CommonAspectRatioEnum::WideFourByThree)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(CommonAspectRatioEnum::TallThreeByFour)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(nearest_aspect_ratio(unsupported)))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> CommonAspectRatioEnum {
|
||||
match aspect_ratio {
|
||||
CommonAspectRatio::WideFiveByFour => CommonAspectRatioEnum::WideFourByThree,
|
||||
CommonAspectRatio::WideThreeByTwo => CommonAspectRatioEnum::WideFourByThree,
|
||||
CommonAspectRatio::WideTwentyOneByNine => CommonAspectRatioEnum::WideSixteenByNine,
|
||||
CommonAspectRatio::TallFourByFive => CommonAspectRatioEnum::TallThreeByFour,
|
||||
CommonAspectRatio::TallTwoByThree => CommonAspectRatioEnum::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineByTwentyOne => CommonAspectRatioEnum::TallNineBySixteen,
|
||||
_ => CommonAspectRatioEnum::Square,
|
||||
}
|
||||
}
|
||||
|
||||
// Happy Horse supports 720p and 1080p only (no 480p).
|
||||
fn plan_output_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<CommonResolutionEnum>, ArtcraftRouterError> {
|
||||
match resolution {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonResolution::SevenTwentyP) => Ok(Some(CommonResolutionEnum::SevenTwentyP)),
|
||||
Some(CommonResolution::TenEightyP) => Ok(Some(CommonResolutionEnum::TenEightyP)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "resolution",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
|
||||
Ok(Some(CommonResolutionEnum::TenEightyP))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(Some(CommonResolutionEnum::SevenTwentyP))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_batch_count(
|
||||
video_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<u16, ArtcraftRouterError> {
|
||||
let count = video_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 | 2 | 4 => Ok(count),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "video_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(4),
|
||||
RequestMismatchMitigationStrategy::PayLessDowngrade => {
|
||||
Ok(if count < 4 { 2 } else { 4 })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_duration(
|
||||
duration_seconds: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<u8>, ArtcraftRouterError> {
|
||||
const MIN: u16 = 4;
|
||||
const MAX: u16 = 15;
|
||||
match duration_seconds {
|
||||
None => Ok(None),
|
||||
Some(d) if d >= MIN && d <= MAX => Ok(Some(d as u8)),
|
||||
Some(d) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "duration_seconds",
|
||||
value: format!("{}", d),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(d.clamp(MIN, MAX) as u8)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
mod field_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn model_is_happy_horse_1p0() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(matches!(req.request.model, Some(CommonVideoModelEnum::HappyHorse1p0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.prompt = Some("test".to_string()); }));
|
||||
assert_eq!(req.request.prompt, Some("test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(10); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(99); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); }));
|
||||
assert_eq!(req.request.video_batch_count, Some(4));
|
||||
}
|
||||
}
|
||||
|
||||
mod resolution_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn res_720p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::SevenTwentyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::TenEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::TenEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(req.request.resolution.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_480p_upgrades() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::FourEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::TenEightyP));
|
||||
}
|
||||
}
|
||||
|
||||
mod media_token_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn start_frame_token_passed_through() {
|
||||
let token = MediaFileToken::new("mf_start".to_string());
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.start_frame = Some(ImageRef::MediaFileToken(token.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.start_frame_image_media_token, Some(token));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_start_frame_rejected() {
|
||||
let result = build_artcraft_happy_horse_1p0(GenerateVideoRequestBuilder {
|
||||
start_frame: Some(ImageRef::Url("https://example.com".to_string())),
|
||||
..base_builder()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_image_tokens_passed_through() {
|
||||
let tokens = vec![MediaFileToken::new("mf_a".to_string()), MediaFileToken::new("mf_b".to_string())];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_image_media_tokens, Some(tokens));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_tokens_passed_through() {
|
||||
let tokens = vec![CharacterToken::new("char_a".to_string()), CharacterToken::new("char_b".to_string())];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_character_tokens, Some(tokens));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: crate::api::common_video_model::CommonVideoModel::HappyHorse1p0,
|
||||
provider: Provider::Artcraft,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
|
||||
let mut builder = base_builder();
|
||||
f(&mut builder);
|
||||
builder
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftHappyHorse1p0RequestState {
|
||||
let result = build_artcraft_happy_horse_1p0(builder).expect("build should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(
|
||||
VideoGenerationRequest::ArtcraftHappyHorse1p0(state)
|
||||
) => state,
|
||||
_ => panic!("expected ArtcraftHappyHorse1p0 request"),
|
||||
}
|
||||
}
|
||||
}
|
||||
-277
@@ -1,277 +0,0 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::happy_horse_1p0::request::ArtcraftHappyHorse1p0RequestState;
|
||||
|
||||
// ── Pricing ──
|
||||
//
|
||||
// Happy Horse 1.0 upstream credit pricing:
|
||||
//
|
||||
// | Resolution | Credits/sec |
|
||||
// |------------|-------------|
|
||||
// | 720p | 33 |
|
||||
// | 1080p | 66 |
|
||||
//
|
||||
// Default resolution (None) is 720p.
|
||||
// Batch count multiplies the total cost.
|
||||
// Credit package: 22,000 credits for $114 (~192.98 credits/$1, rounded to 193).
|
||||
//
|
||||
// ArtCraft credits: 100 credits = $1.00, so ArtCraft credits = USD cents.
|
||||
// We compute USD cents from upstream credits ÷ credits_per_dollar × 100.
|
||||
|
||||
const UPSTREAM_CREDITS_PER_SECOND_720P: u32 = 33;
|
||||
const UPSTREAM_CREDITS_PER_SECOND_1080P: u32 = 66;
|
||||
const UPSTREAM_CREDITS_PER_DOLLAR: f64 = 193.0;
|
||||
|
||||
pub struct ArtcraftHappyHorse1p0CostState {
|
||||
pub resolution: CommonResolution,
|
||||
pub duration_seconds: u16,
|
||||
pub batch_count: u16,
|
||||
}
|
||||
|
||||
impl ArtcraftHappyHorse1p0CostState {
|
||||
pub fn from_request(request: &ArtcraftHappyHorse1p0RequestState) -> Self {
|
||||
let resolution = request.request.resolution
|
||||
.unwrap_or(CommonResolution::SevenTwentyP);
|
||||
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
|
||||
let batch_count = request.request.video_batch_count.unwrap_or(1);
|
||||
|
||||
Self { resolution, duration_seconds, batch_count }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
let credits_per_second = match self.resolution {
|
||||
CommonResolution::TenEightyP => UPSTREAM_CREDITS_PER_SECOND_1080P,
|
||||
_ => UPSTREAM_CREDITS_PER_SECOND_720P,
|
||||
};
|
||||
|
||||
let upstream_credits = credits_per_second as u64
|
||||
* self.duration_seconds as u64
|
||||
* self.batch_count as u64;
|
||||
|
||||
let usd_cents = (upstream_credits as f64 / UPSTREAM_CREDITS_PER_DOLLAR * 100.0).round() as u64;
|
||||
|
||||
// ArtCraft credits = USD cents.
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(usd_cents),
|
||||
cost_in_usd_cents: Some(usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
|
||||
// ── 720p pricing (33 upstream credits/sec) ──
|
||||
|
||||
mod pricing_720p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_1() {
|
||||
// 33 credits/sec; upstream_credits / 193 * 100
|
||||
// (NB: duration 3s is clamped to 4s by the builder)
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 4, 1), 68);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1), 85);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 10, 1), 171);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 15, 1), 256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_2() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 2), 171);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_4() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 4), 342);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_defaults_to_720p() {
|
||||
assert_eq!(cost_cents(None, 5, 1), cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1));
|
||||
}
|
||||
}
|
||||
|
||||
// ── 1080p pricing (66 upstream credits/sec) ──
|
||||
|
||||
mod pricing_1080p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_1() {
|
||||
// 66 credits/sec; upstream_credits / 193 * 100
|
||||
// (NB: duration 3s is clamped to 4s by the builder)
|
||||
assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 4, 1), 137);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 5, 1), 171);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 10, 1), 342);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 15, 1), 513);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_2() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 5, 2), 342);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_4() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 5, 4), 684);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Relative pricing ──
|
||||
|
||||
mod relative_pricing {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_720p_cheaper_than_1080p() {
|
||||
let c720 = cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1);
|
||||
let c1080 = cost_cents(Some(CommonResolution::TenEightyP), 5, 1);
|
||||
assert!(c720 < c1080);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn r1080p_approximately_double_720p() {
|
||||
// 1080p upstream credits are exactly 2× 720p, but USD cents may differ
|
||||
// by ±1 due to rounding (each is rounded independently).
|
||||
for dur in [4u16, 5, 10, 15] {
|
||||
let c720 = cost_cents(Some(CommonResolution::SevenTwentyP), dur, 1);
|
||||
let c1080 = cost_cents(Some(CommonResolution::TenEightyP), dur, 1);
|
||||
let expected = c720 * 2;
|
||||
assert!(
|
||||
c1080 >= expected - 1 && c1080 <= expected + 1,
|
||||
"1080p ({}) should be ~2× 720p ({}) at {}s", c1080, c720, dur,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_duration() {
|
||||
let c4 = cost_cents(None, 4, 1);
|
||||
let c10 = cost_cents(None, 10, 1);
|
||||
let c15 = cost_cents(None, 15, 1);
|
||||
assert!(c4 < c10);
|
||||
assert!(c10 < c15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_batch() {
|
||||
let b1 = cost_cents(None, 5, 1);
|
||||
let b2 = cost_cents(None, 5, 2);
|
||||
let b4 = cost_cents(None, 5, 4);
|
||||
assert!(b1 < b2);
|
||||
assert!(b2 < b4);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Credits equal cents ──
|
||||
|
||||
mod credits_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn credits_equal_usd_cents() {
|
||||
let resolutions = [
|
||||
Some(CommonResolution::SevenTwentyP),
|
||||
Some(CommonResolution::TenEightyP),
|
||||
None,
|
||||
];
|
||||
for res in resolutions {
|
||||
for dur in [4, 5, 10, 15] {
|
||||
for batch in [1, 2, 4] {
|
||||
let cost = build_cost(res, dur, batch);
|
||||
assert_eq!(
|
||||
cost.cost_in_credits, cost.cost_in_usd_cents,
|
||||
"credits should equal cents for res={:?} dur={}s batch={}",
|
||||
res, dur, batch,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cross-check with Kinovi ──
|
||||
|
||||
mod cross_check_with_kinovi {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn artcraft_matches_kinovi_all_combos() {
|
||||
let resolutions = [
|
||||
Some(CommonResolution::SevenTwentyP),
|
||||
None,
|
||||
Some(CommonResolution::TenEightyP),
|
||||
];
|
||||
let durations: [u16; 4] = [4, 5, 10, 15];
|
||||
let batches: [u16; 3] = [1, 2, 4];
|
||||
|
||||
for res in &resolutions {
|
||||
for dur in &durations {
|
||||
for batch in &batches {
|
||||
let artcraft_cost = build_cost(*res, *dur, *batch);
|
||||
|
||||
let kinovi = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::HappyHorse1p0,
|
||||
provider: Provider::Seedance2Pro,
|
||||
resolution: *res,
|
||||
duration_seconds: Some(*dur),
|
||||
video_batch_count: Some(*batch),
|
||||
..Default::default()
|
||||
};
|
||||
let kinovi_cost = kinovi.build2()
|
||||
.expect("kinovi build2")
|
||||
.estimate_cost()
|
||||
.expect("kinovi estimate_cost");
|
||||
|
||||
assert_eq!(
|
||||
artcraft_cost.cost_in_usd_cents, kinovi_cost.cost_in_usd_cents,
|
||||
"USD cents mismatch: res={:?} dur={}s batch={}",
|
||||
res, dur, batch,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn build_cost(
|
||||
resolution: Option<CommonResolution>,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
) -> crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::HappyHorse1p0,
|
||||
provider: Provider::Artcraft,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
..Default::default()
|
||||
};
|
||||
builder.build2()
|
||||
.expect("build2 should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate_cost should succeed")
|
||||
}
|
||||
|
||||
fn cost_cents(
|
||||
resolution: Option<CommonResolution>,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
) -> u64 {
|
||||
build_cost(resolution, duration_seconds, video_batch_count)
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user