mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-28 04:22:51 -04:00
Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c3138d70f | ||
|
|
bf188682ba | ||
|
|
d3acef8467 | ||
|
|
167a7f996e | ||
|
|
7fca050c56 | ||
|
|
e9316bcdb5 | ||
|
|
40a2e8a514 | ||
|
|
02ab54bc8c | ||
|
|
ff268c4e4e | ||
|
|
f97def83e1 | ||
|
|
9e9156cd32 | ||
|
|
c7b817ac3f | ||
|
|
66afce8889 | ||
|
|
f7e7c132bc | ||
|
|
cfa14769cd | ||
|
|
56d1649a9f | ||
|
|
e070028bf1 | ||
|
|
52fdf237ad | ||
|
|
15d286135a | ||
|
|
dc1845becb | ||
|
|
a654e9ada1 | ||
|
|
2843916852 |
@@ -6,6 +6,7 @@
|
||||
.env-elasticsearch-secrets
|
||||
.env-migration-tool-secrets
|
||||
.env-secrets
|
||||
.env-support-tool-secrets
|
||||
_develop/tools/.secrets
|
||||
_develop/tools/db_secrets.toml
|
||||
crates/service/web/storyteller_web/config/storyteller-web.development-secrets.env
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\n INSERT INTO media_files\n SET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n is_user_upload = ?,\n is_intermediate_system_file = ?,\n\n origin_category = ?, \n origin_product_category = ?, \n maybe_origin_model_type = ?, \n maybe_origin_model_token = ?, \n maybe_origin_filename = ?,\n\n is_batch_generated = ?,\n maybe_batch_token = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n maybe_duration_millis = ?,\n maybe_audio_encoding = ?, \n maybe_video_encoding = ?, \n maybe_frame_width = ?, \n maybe_frame_height = ?,\n maybe_prompt_token = ?,\n checksum_sha2 = ?,\n \n maybe_engine_category = ?,\n\n maybe_title = ?,\n maybe_text_transcript = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n public_bucket_directory_hash = ?, \n maybe_public_bucket_prefix = ?, \n maybe_public_bucket_extension = ?, \n\n maybe_creator_user_token = ?, \n maybe_creator_anonymous_visitor_token = ?, \n\n creator_ip_address = ?, \n creator_set_visibility = ?, \n\n maybe_creator_file_synthetic_id = ?, \n maybe_creator_category_synthetic_id = ?,\n\n extra_file_modification_info = ?,\n\n maybe_mod_user_token = ?, \n is_generated_on_prem = ?, \n generated_by_worker = ?, \n generated_by_cluster = ?\n ",
|
||||
"query": "\n INSERT INTO media_files\n SET\n token = ?,\n\n media_class = ?,\n media_type = ?,\n\n is_user_upload = ?,\n is_intermediate_system_file = ?,\n\n origin_category = ?, \n origin_product_category = ?, \n maybe_origin_model_type = ?, \n maybe_origin_model_token = ?, \n maybe_origin_filename = ?,\n\n is_batch_generated = ?,\n maybe_batch_token = ?,\n\n maybe_mime_type = ?,\n file_size_bytes = ?,\n maybe_duration_millis = ?,\n maybe_audio_encoding = ?, \n maybe_video_encoding = ?, \n maybe_frame_width = ?, \n maybe_frame_height = ?,\n maybe_prompt_token = ?,\n checksum_sha2 = ?,\n \n maybe_engine_category = ?,\n\n maybe_title = ?,\n maybe_text_transcript = ?,\n\n maybe_scene_source_media_file_token = ?,\n\n public_bucket_directory_hash = ?, \n maybe_public_bucket_prefix = ?, \n maybe_public_bucket_extension = ?, \n\n maybe_creator_user_token = ?, \n maybe_creator_anonymous_visitor_token = ?, \n\n creator_ip_address = ?, \n creator_set_visibility = ?, \n\n maybe_creator_file_synthetic_id = ?, \n maybe_creator_category_synthetic_id = ?,\n\n extra_file_modification_info = ?,\n\n maybe_cover_image_media_file_token = ?,\n\n maybe_mod_user_token = ?,\n is_generated_on_prem = ?,\n generated_by_worker = ?,\n generated_by_cluster = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 39
|
||||
"Right": 40
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b4470e607c005421dd8504b9f1d695080a581936ac6ce00b6f437b2764e741be"
|
||||
"hash": "87d5511700985daaabfc9f2972f018d56b6672c07be082daa7c1a782400cb18d"
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `operation_id`,\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 jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | UNIQUE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "operation_id",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 256
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "maybe_creator_user_token: tokens::tokens::users::UserToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "MULTIPLE_KEY | BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "creator_ip_address",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
|
||||
"char_set": 224,
|
||||
"max_size": 160
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "creator_set_visibility: enums::common::visibility::Visibility",
|
||||
"type_info": {
|
||||
"type": "String",
|
||||
"flags": "NOT_NULL | BINARY | ENUM",
|
||||
"char_set": 224,
|
||||
"max_size": 28
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "maybe_prompt_token: tokens::tokens::prompts::PromptToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken",
|
||||
"type_info": {
|
||||
"type": "VarString",
|
||||
"flags": "BINARY",
|
||||
"char_set": 224,
|
||||
"max_size": 128
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d56a26a7300495fac9741e77e390f16402e2273dbab9a15d7c17eb1f0da7a1b1"
|
||||
}
|
||||
Generated
+68
-20
@@ -578,9 +578,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.7"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
@@ -797,6 +797,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"uuid 1.18.1",
|
||||
"workspace-build-acceleration",
|
||||
"world_labs_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2224,9 +2225,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.13"
|
||||
version = "4.5.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc"
|
||||
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -2234,21 +2235,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.13"
|
||||
version = "4.5.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99"
|
||||
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex 0.7.4",
|
||||
"clap_lex 1.0.0",
|
||||
"strsim 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.13"
|
||||
version = "4.5.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
|
||||
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -2267,9 +2268,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.4"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||
checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831"
|
||||
|
||||
[[package]]
|
||||
name = "cloud_storage"
|
||||
@@ -3067,7 +3068,7 @@ version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap 4.5.13",
|
||||
"clap 4.5.60",
|
||||
"datetimes",
|
||||
"dotenv",
|
||||
"easyenv",
|
||||
@@ -3102,7 +3103,7 @@ name = "db-cleanup"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"bucket_paths",
|
||||
"clap 4.5.13",
|
||||
"clap 4.5.60",
|
||||
"dotenv",
|
||||
"easyenv",
|
||||
"enums",
|
||||
@@ -3262,7 +3263,7 @@ name = "dev-database-seed"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"bucket_paths",
|
||||
"clap 4.5.13",
|
||||
"clap 4.5.60",
|
||||
"cloud_storage",
|
||||
"config",
|
||||
"easyenv",
|
||||
@@ -3288,7 +3289,7 @@ name = "dev-upload-media-file"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"bucket_paths",
|
||||
"clap 4.5.13",
|
||||
"clap 4.5.60",
|
||||
"cloud_storage",
|
||||
"config",
|
||||
"easyenv",
|
||||
@@ -3762,7 +3763,7 @@ name = "elasticsearch-cli"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"bucket_paths",
|
||||
"clap 4.5.13",
|
||||
"clap 4.5.60",
|
||||
"config",
|
||||
"easyenv",
|
||||
"elasticsearch",
|
||||
@@ -6743,7 +6744,7 @@ dependencies = [
|
||||
name = "lru-delete-models"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"clap 4.5.13",
|
||||
"clap 4.5.60",
|
||||
"errors",
|
||||
"logging",
|
||||
"walkdir",
|
||||
@@ -7053,7 +7054,7 @@ name = "migration-tool"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"bucket_paths",
|
||||
"clap 4.5.13",
|
||||
"clap 4.5.60",
|
||||
"cloud_storage",
|
||||
"config",
|
||||
"easyenv",
|
||||
@@ -10315,7 +10316,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
name = "scrape-test"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"clap 4.5.13",
|
||||
"clap 4.5.60",
|
||||
"errors",
|
||||
"tokio",
|
||||
"web_scrapers",
|
||||
@@ -11603,6 +11604,7 @@ dependencies = [
|
||||
"utoipa-swagger-ui",
|
||||
"videos",
|
||||
"workspace-build-acceleration",
|
||||
"world_labs_api",
|
||||
"wreq",
|
||||
"zip 0.6.6",
|
||||
]
|
||||
@@ -11747,6 +11749,20 @@ dependencies = [
|
||||
"rsa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "support-tool"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.5.60",
|
||||
"easyenv",
|
||||
"errors",
|
||||
"log",
|
||||
"seedance2pro",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swift-rs"
|
||||
version = "1.0.7"
|
||||
@@ -14912,7 +14928,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"cc",
|
||||
"chrono",
|
||||
"clap 4.5.13",
|
||||
"clap 4.5.60",
|
||||
"cookie 0.18.1",
|
||||
"core-foundation-sys",
|
||||
"crossbeam-utils",
|
||||
@@ -15020,6 +15036,7 @@ dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.28.0",
|
||||
"tokio",
|
||||
"uuid 1.18.1",
|
||||
"workspace-build-acceleration",
|
||||
@@ -15064,6 +15081,37 @@ dependencies = [
|
||||
"wreq-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "worldlabs-job"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"actix-web",
|
||||
"actix_helpers",
|
||||
"anyhow",
|
||||
"bootstrap",
|
||||
"bucket_paths",
|
||||
"cloud_storage",
|
||||
"concurrency",
|
||||
"config",
|
||||
"easyenv",
|
||||
"enums",
|
||||
"errors",
|
||||
"hashing",
|
||||
"jobs_common",
|
||||
"log",
|
||||
"mysql_queries",
|
||||
"reqwest 0.12.23",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"server_environment",
|
||||
"sqlx",
|
||||
"tokens",
|
||||
"tokio",
|
||||
"workspace-build-acceleration",
|
||||
"world_labs_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wreq"
|
||||
version = "6.0.0-rc.28"
|
||||
|
||||
@@ -33,6 +33,7 @@ members = [
|
||||
"crates/cli/production/kubectl-pod-cleanup",
|
||||
"crates/cli/production/lru-delete-models",
|
||||
"crates/cli/production/migration-tool",
|
||||
"crates/cli/production/support-tool",
|
||||
|
||||
# Desktop apps
|
||||
"crates/desktop/artcraft",
|
||||
@@ -120,6 +121,7 @@ members = [
|
||||
"crates/service/job/es-update-job",
|
||||
"crates/service/job/inference_job",
|
||||
"crates/service/job/seedance2_pro_job",
|
||||
"crates/service/job/worldlabs_job",
|
||||
"crates/service/job/tts_download_job",
|
||||
"crates/service/job/tts_inference_job",
|
||||
"crates/service/job/w2l_download_job",
|
||||
@@ -221,6 +223,7 @@ component_traits = { path = "crates/service/plugins/component_traits" }
|
||||
cookie_store = { path = "crates/lib/cookie_store" }
|
||||
cookies = { path = "crates/lib/cookies" }
|
||||
crockford = { path = "crates/lib/data/crockford" }
|
||||
easyenv = { path = "crates/lib/easyenv" }
|
||||
enums = { path = "crates/schema/public/enums" }
|
||||
enums_public = { path = "crates/schema/public/enums_public" }
|
||||
errors = { path = "crates/lib/errors" }
|
||||
@@ -270,6 +273,7 @@ base64 = "0.22.1"
|
||||
bstr = "1.12.1"
|
||||
bytes = "1.10.1"
|
||||
chrono = { version = "0.4.41", features = ["serde"] }
|
||||
clap = { version = "4.5.60", features = ["derive"] }
|
||||
cookie = "0.18.1"
|
||||
env = "1.0.1"
|
||||
env_logger = "0.11.9"
|
||||
|
||||
@@ -143,6 +143,12 @@ RUN SQLX_OFFLINE=true \
|
||||
--release \
|
||||
--bin seedance2-pro-job
|
||||
|
||||
RUN SQLX_OFFLINE=true \
|
||||
LD_LIBRARY_PATH=/usr/lib:${LD_LIBRARY_PATH} \
|
||||
$HOME/.cargo/bin/cargo build \
|
||||
--release \
|
||||
--bin worldlabs-job
|
||||
|
||||
RUN SQLX_OFFLINE=true \
|
||||
LD_LIBRARY_PATH=/usr/lib:${LD_LIBRARY_PATH} \
|
||||
$HOME/.cargo/bin/cargo build \
|
||||
@@ -203,6 +209,7 @@ RUN echo -n ${GIT_SHA} > GIT_SHA
|
||||
# Copy all the binaries (except those that need a GPU):
|
||||
COPY --from=builder /tmp/target/release/storyteller-web /
|
||||
COPY --from=builder /tmp/target/release/seedance2-pro-job /
|
||||
COPY --from=builder /tmp/target/release/worldlabs-job /
|
||||
COPY --from=builder /tmp/target/release/dummy-service /
|
||||
COPY --from=builder /tmp/target/release/analytics-job /
|
||||
COPY --from=builder /tmp/target/release/email-sender-job /
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use enums::common::generation::common_splat_model::CommonSplatModel;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
|
||||
pub const ESTIMATE_SPLAT_COST_PATH: &str = "/v1/generate/cost_estimate/splat";
|
||||
|
||||
/// Request body for the splat cost estimate endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EstimateSplatCostRequest {
|
||||
/// The splat model to estimate costs for.
|
||||
pub model: CommonSplatModel,
|
||||
|
||||
/// The provider to route the generation through.
|
||||
pub provider: GenerationProvider,
|
||||
|
||||
/// Whether the request includes a reference image.
|
||||
pub has_reference_image: Option<bool>,
|
||||
}
|
||||
|
||||
/// Response body for the splat cost estimate endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EstimateSplatCostResponse {
|
||||
pub success: bool,
|
||||
|
||||
/// Estimated cost in credits.
|
||||
pub cost_in_credits: Option<u64>,
|
||||
|
||||
/// Estimated cost in USD cents.
|
||||
pub cost_in_usd_cents: Option<u64>,
|
||||
|
||||
/// Whether the generation is free for this user/plan.
|
||||
pub is_free: bool,
|
||||
|
||||
/// Whether the user has unlimited generations.
|
||||
pub is_unlimited: bool,
|
||||
|
||||
/// Whether the user is rate limited.
|
||||
pub is_rate_limited: bool,
|
||||
|
||||
/// Whether the output will have a watermark.
|
||||
pub has_watermark: bool,
|
||||
}
|
||||
|
||||
/// Error response for the splat cost estimate endpoint.
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct EstimateSplatCostError {
|
||||
pub success: bool,
|
||||
pub error_type: EstimateSplatCostErrorType,
|
||||
pub error_message: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EstimateSplatCostErrorType {
|
||||
InvalidProviderForModel,
|
||||
InvalidInput,
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod estimate_image_cost;
|
||||
pub mod estimate_splat_cost;
|
||||
pub mod estimate_video_cost;
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const FLUX_2_LORA_EDIT_IMAGE_ANGLE_PATH: &str = "/v1/generate/image/angle/flux_2_lora";
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||
pub struct Flux2LoraEditImageAngleRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// The image to edit with angle adjustment.
|
||||
pub image_media_token: MediaFileToken,
|
||||
|
||||
/// Horizontal rotation angle in degrees.
|
||||
pub horizontal_angle: Option<f64>,
|
||||
|
||||
/// Vertical rotation angle in degrees.
|
||||
pub vertical_angle: Option<f64>,
|
||||
|
||||
/// Zoom level.
|
||||
pub zoom: Option<f64>,
|
||||
|
||||
/// Number of images to generate. Default is one.
|
||||
pub num_images: Option<Flux2LoraEditImageAngleNumImages>,
|
||||
|
||||
/// Output image size. Default is square_hd.
|
||||
pub image_size: Option<Flux2LoraEditImageAngleImageSize>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Flux2LoraEditImageAngleNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Flux2LoraEditImageAngleImageSize {
|
||||
Square,
|
||||
SquareHd,
|
||||
PortraitFourThree,
|
||||
PortraitSixteenNine,
|
||||
LandscapeFourThree,
|
||||
LandscapeSixteenNine,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct Flux2LoraEditImageAngleResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod flux_2_lora_edit_image_angle;
|
||||
pub mod qwen_edit_2511_edit_image_angle;
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const QWEN_EDIT_2511_EDIT_IMAGE_ANGLE_PATH: &str = "/v1/generate/image/angle/qwen_edit_2511";
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||
pub struct QwenEdit2511EditImageAngleRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// The image to edit with angle adjustment.
|
||||
pub image_media_token: MediaFileToken,
|
||||
|
||||
/// Horizontal rotation angle in degrees.
|
||||
pub horizontal_angle: Option<f64>,
|
||||
|
||||
/// Vertical rotation angle in degrees.
|
||||
pub vertical_angle: Option<f64>,
|
||||
|
||||
/// Zoom level.
|
||||
pub zoom: Option<f64>,
|
||||
|
||||
/// Additional text prompt to guide the generation.
|
||||
pub additional_prompt: Option<String>,
|
||||
|
||||
/// Number of images to generate. Default is one.
|
||||
pub num_images: Option<QwenEdit2511EditImageAngleNumImages>,
|
||||
|
||||
/// Output image size. Default is square_hd.
|
||||
pub image_size: Option<QwenEdit2511EditImageAngleImageSize>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum QwenEdit2511EditImageAngleNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum QwenEdit2511EditImageAngleImageSize {
|
||||
Square,
|
||||
SquareHd,
|
||||
PortraitFourThree,
|
||||
PortraitSixteenNine,
|
||||
LandscapeFourThree,
|
||||
LandscapeSixteenNine,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct QwenEdit2511EditImageAngleResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod angle;
|
||||
pub mod bg_removal;
|
||||
pub mod edit;
|
||||
pub mod inpaint;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod cost_estimate;
|
||||
pub mod image;
|
||||
pub mod object;
|
||||
pub mod splat;
|
||||
pub mod video;
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const GENERATE_WORLDLABS_MARBLE_0P1_MINI_SPLAT_URL_PATH: &str = "/v1/generate/splat/worldlabs_marble_0p1_mini";
|
||||
|
||||
/// World Labs Marble 0.1-mini Splat Generation
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateWorldlabsMarble0p1MiniSplatRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// Optional image to use as input for world generation.
|
||||
pub image_media_file_token: Option<MediaFileToken>,
|
||||
|
||||
/// Optional text prompt for world generation.
|
||||
pub prompt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateWorldlabsMarble0p1MiniSplatResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub const GENERATE_WORLDLABS_MARBLE_0P1_PLUS_SPLAT_URL_PATH: &str = "/v1/generate/splat/worldlabs_marble_0p1_plus";
|
||||
|
||||
/// World Labs Marble 0.1-plus Splat Generation
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateWorldlabsMarble0p1PlusSplatRequest {
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
pub uuid_idempotency_token: String,
|
||||
|
||||
/// Optional image to use as input for world generation.
|
||||
pub image_media_file_token: Option<MediaFileToken>,
|
||||
|
||||
/// Optional text prompt for world generation.
|
||||
pub prompt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema)]
|
||||
pub struct GenerateWorldlabsMarble0p1PlusSplatResponse {
|
||||
pub success: bool,
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod generate_worldlabs_marble_0p1_mini_splat;
|
||||
pub mod generate_worldlabs_marble_0p1_plus_splat;
|
||||
+1
@@ -1,2 +1,3 @@
|
||||
pub mod image;
|
||||
pub mod splat;
|
||||
pub mod video;
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
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::generate::cost_estimate::estimate_splat_cost::{
|
||||
EstimateSplatCostRequest, EstimateSplatCostResponse, ESTIMATE_SPLAT_COST_PATH,
|
||||
};
|
||||
|
||||
pub async fn estimate_splat_cost(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: EstimateSplatCostRequest,
|
||||
) -> Result<EstimateSplatCostResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
ESTIMATE_SPLAT_COST_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod estimate_splat_cost;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::{
|
||||
Flux2LoraEditImageAngleRequest, Flux2LoraEditImageAngleResponse,
|
||||
FLUX_2_LORA_EDIT_IMAGE_ANGLE_PATH,
|
||||
};
|
||||
|
||||
pub async fn flux_2_lora_edit_image_angle(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: Flux2LoraEditImageAngleRequest,
|
||||
) -> Result<Flux2LoraEditImageAngleResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
FLUX_2_LORA_EDIT_IMAGE_ANGLE_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod flux_2_lora_edit_image_angle;
|
||||
pub mod qwen_edit_2511_edit_image_angle;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use crate::error::storyteller_error::StorytellerError;
|
||||
use crate::utils::api_host::ApiHost;
|
||||
use crate::utils::basic_json_post_request::basic_json_post_request;
|
||||
use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::{
|
||||
QwenEdit2511EditImageAngleRequest, QwenEdit2511EditImageAngleResponse,
|
||||
QWEN_EDIT_2511_EDIT_IMAGE_ANGLE_PATH,
|
||||
};
|
||||
|
||||
pub async fn qwen_edit_2511_edit_image_angle(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: QwenEdit2511EditImageAngleRequest,
|
||||
) -> Result<QwenEdit2511EditImageAngleResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
QWEN_EDIT_2511_EDIT_IMAGE_ANGLE_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod angle;
|
||||
pub mod bg_removal;
|
||||
pub mod edit;
|
||||
pub mod inpaint;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod cost_estimate;
|
||||
pub mod image;
|
||||
pub mod object;
|
||||
pub mod splat;
|
||||
pub mod video;
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
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::generate::splat::generate_worldlabs_marble_0p1_mini_splat::{GenerateWorldlabsMarble0p1MiniSplatRequest, GenerateWorldlabsMarble0p1MiniSplatResponse, GENERATE_WORLDLABS_MARBLE_0P1_MINI_SPLAT_URL_PATH};
|
||||
|
||||
pub async fn generate_worldlabs_marble_0p1_mini_splat(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateWorldlabsMarble0p1MiniSplatRequest,
|
||||
) -> Result<GenerateWorldlabsMarble0p1MiniSplatResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_WORLDLABS_MARBLE_0P1_MINI_SPLAT_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
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::generate::splat::generate_worldlabs_marble_0p1_plus_splat::{GenerateWorldlabsMarble0p1PlusSplatRequest, GenerateWorldlabsMarble0p1PlusSplatResponse, GENERATE_WORLDLABS_MARBLE_0P1_PLUS_SPLAT_URL_PATH};
|
||||
|
||||
pub async fn generate_worldlabs_marble_0p1_plus_splat(
|
||||
api_host: &ApiHost,
|
||||
maybe_creds: Option<&StorytellerCredentialSet>,
|
||||
request: GenerateWorldlabsMarble0p1PlusSplatRequest,
|
||||
) -> Result<GenerateWorldlabsMarble0p1PlusSplatResponse, StorytellerError> {
|
||||
Ok(basic_json_post_request(
|
||||
api_host,
|
||||
GENERATE_WORLDLABS_MARBLE_0P1_PLUS_SPLAT_URL_PATH,
|
||||
maybe_creds,
|
||||
request,
|
||||
).await?)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod generate_worldlabs_marble_0p1_mini_splat;
|
||||
pub mod generate_worldlabs_marble_0p1_plus_splat;
|
||||
@@ -17,10 +17,11 @@ workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
artcraft_api_defs.workspace = true
|
||||
artcraft_client.workspace = true
|
||||
fal_client.workspace = true
|
||||
seedance2pro.workspace = true
|
||||
artcraft_client.workspace = true
|
||||
tokens.workspace = true
|
||||
world_labs_api.workspace = true
|
||||
|
||||
# External
|
||||
anyhow.workspace = true
|
||||
|
||||
@@ -27,4 +27,8 @@ pub enum CommonImageModel {
|
||||
Seedream4p5,
|
||||
#[serde(rename = "seedream_5_lite")]
|
||||
Seedream5Lite,
|
||||
#[serde(rename = "qwen_edit_2511_angles")]
|
||||
QwenEdit2511Angles,
|
||||
#[serde(rename = "flux_2_lora_angles")]
|
||||
Flux2LoraAngles,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Common splat models supported by the router.
|
||||
/// Not all models are available through all providers.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommonSplatModel {
|
||||
#[serde(rename = "marble_0p1_mini")]
|
||||
Marble0p1Mini,
|
||||
|
||||
#[serde(rename = "marble_0p1_plus")]
|
||||
Marble0p1Plus,
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod common_aspect_ratio;
|
||||
pub mod common_image_model;
|
||||
pub mod common_resolution;
|
||||
pub mod common_splat_model;
|
||||
pub mod common_video_model;
|
||||
pub mod image_list_ref;
|
||||
pub mod image_ref;
|
||||
|
||||
+3
@@ -47,6 +47,9 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
+3
@@ -35,6 +35,9 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
let estimate = request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::Flux2LoraEditImageAngleNumImages;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::PlanArtcraftFlux2LoraAngles;
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_flux_2_lora_angles(
|
||||
plan: &PlanArtcraftFlux2LoraAngles<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent.
|
||||
let cost_per_image: u64 = 4;
|
||||
|
||||
let num_images: u64 = match plan.num_images {
|
||||
Flux2LoraEditImageAngleNumImages::One => 1,
|
||||
Flux2LoraEditImageAngleNumImages::Two => 2,
|
||||
Flux2LoraEditImageAngleNumImages::Three => 3,
|
||||
Flux2LoraEditImageAngleNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = cost_per_image * num_images;
|
||||
|
||||
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 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 tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
|
||||
let tokens = vec![MediaFileToken::new_from_str("test_token")];
|
||||
let request = GenerateImageRequest {
|
||||
model: CommonImageModel::Flux2LoraAngles,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
.estimate_costs()
|
||||
.cost_in_usd_cents
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_usd_cents() {
|
||||
// $0.04/image = 4 cents each
|
||||
assert_eq!(estimate_usd_cents(1), 4);
|
||||
assert_eq!(estimate_usd_cents(2), 8);
|
||||
assert_eq!(estimate_usd_cents(3), 12);
|
||||
assert_eq!(estimate_usd_cents(4), 16);
|
||||
}
|
||||
}
|
||||
+3
@@ -47,6 +47,9 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
+3
@@ -47,6 +47,9 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
+3
@@ -86,6 +86,9 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
+3
@@ -47,6 +47,9 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
+3
@@ -55,6 +55,9 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
+3
@@ -54,6 +54,9 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::QwenEdit2511EditImageAngleNumImages;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::PlanArtcraftQwenEdit2511Angles;
|
||||
|
||||
pub(crate) fn estimate_image_cost_artcraft_qwen_edit_2511_angles(
|
||||
plan: &PlanArtcraftQwenEdit2511Angles<'_>,
|
||||
) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent.
|
||||
let cost_per_image: u64 = 4;
|
||||
|
||||
let num_images: u64 = match plan.num_images {
|
||||
QwenEdit2511EditImageAngleNumImages::One => 1,
|
||||
QwenEdit2511EditImageAngleNumImages::Two => 2,
|
||||
QwenEdit2511EditImageAngleNumImages::Three => 3,
|
||||
QwenEdit2511EditImageAngleNumImages::Four => 4,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = cost_per_image * num_images;
|
||||
|
||||
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 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 tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
|
||||
let tokens = vec![MediaFileToken::new_from_str("test_token")];
|
||||
let request = GenerateImageRequest {
|
||||
model: CommonImageModel::QwenEdit2511Angles,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
.estimate_costs()
|
||||
.cost_in_usd_cents
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_usd_cents() {
|
||||
// $0.04/image = 4 cents each
|
||||
assert_eq!(estimate_usd_cents(1), 4);
|
||||
assert_eq!(estimate_usd_cents(2), 8);
|
||||
assert_eq!(estimate_usd_cents(3), 12);
|
||||
assert_eq!(estimate_usd_cents(4), 16);
|
||||
}
|
||||
}
|
||||
+3
@@ -47,6 +47,9 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
+3
@@ -47,6 +47,9 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
+3
@@ -47,6 +47,9 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
+2
@@ -9,3 +9,5 @@ pub mod estimate_image_cost_artcraft_nano_banana_pro;
|
||||
pub mod estimate_image_cost_artcraft_seedream_4;
|
||||
pub mod estimate_image_cost_artcraft_seedream_4p5;
|
||||
pub mod estimate_image_cost_artcraft_seedream_5_lite;
|
||||
pub mod estimate_image_cost_artcraft_qwen_edit_2511_angles;
|
||||
pub mod estimate_image_cost_artcraft_flux_2_lora_angles;
|
||||
|
||||
+3
@@ -52,6 +52,9 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
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_flux_2_lora_angles::PlanArtcraftFlux2LoraAngles;
|
||||
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::Flux2LoraEditImageAngleRequest;
|
||||
use artcraft_client::endpoints::generate::image::angle::flux_2_lora_edit_image_angle::flux_2_lora_edit_image_angle;
|
||||
|
||||
pub async fn execute_artcraft_flux_2_lora_angles(
|
||||
plan: &PlanArtcraftFlux2LoraAngles<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let request = Flux2LoraEditImageAngleRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
image_media_token: plan.image_input.clone(),
|
||||
horizontal_angle: plan.horizontal_angle,
|
||||
vertical_angle: plan.vertical_angle,
|
||||
zoom: plan.zoom,
|
||||
num_images: Some(plan.num_images),
|
||||
image_size: plan.image_size,
|
||||
};
|
||||
|
||||
let response = flux_2_lora_edit_image_angle(
|
||||
&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 crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::{base_flux_2_lora_angles_image_request, get_artcraft_client};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_angle_edit_flux_2_lora() {
|
||||
let client = get_artcraft_client();
|
||||
let tokens = vec![MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8")];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::SquareHd),
|
||||
image_batch_count: Some(1),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
horizontal_angle: Some(30.0),
|
||||
vertical_angle: Some(0.0),
|
||||
zoom: Some(1.0),
|
||||
..base_flux_2_lora_angles_image_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.
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
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_qwen_edit_2511_angles::PlanArtcraftQwenEdit2511Angles;
|
||||
use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::QwenEdit2511EditImageAngleRequest;
|
||||
use artcraft_client::endpoints::generate::image::angle::qwen_edit_2511_edit_image_angle::qwen_edit_2511_edit_image_angle;
|
||||
|
||||
pub async fn execute_artcraft_qwen_edit_2511_angles(
|
||||
plan: &PlanArtcraftQwenEdit2511Angles<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let request = QwenEdit2511EditImageAngleRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
image_media_token: plan.image_input.clone(),
|
||||
horizontal_angle: plan.horizontal_angle,
|
||||
vertical_angle: plan.vertical_angle,
|
||||
zoom: plan.zoom,
|
||||
additional_prompt: plan.prompt.map(|p| p.to_string()),
|
||||
num_images: Some(plan.num_images),
|
||||
image_size: plan.image_size,
|
||||
};
|
||||
|
||||
let response = qwen_edit_2511_edit_image_angle(
|
||||
&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 crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::{base_qwen_edit_2511_angles_image_request, get_artcraft_client};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // manually run — fires a real API request and incurs cost
|
||||
async fn test_angle_edit_qwen_edit_2511() {
|
||||
let client = get_artcraft_client();
|
||||
let tokens = vec![MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8")];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::SquareHd),
|
||||
image_batch_count: Some(1),
|
||||
prompt: None,
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
horizontal_angle: Some(30.0),
|
||||
vertical_angle: Some(0.0),
|
||||
zoom: Some(1.0),
|
||||
..base_qwen_edit_2511_angles_image_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.
|
||||
}
|
||||
}
|
||||
+2
@@ -9,3 +9,5 @@ pub mod generate_image_artcraft_nano_banana_pro;
|
||||
pub mod generate_image_artcraft_seedream_4;
|
||||
pub mod generate_image_artcraft_seedream_4p5;
|
||||
pub mod generate_image_artcraft_seedream_5_lite;
|
||||
pub mod generate_image_artcraft_qwen_edit_2511_angles;
|
||||
pub mod generate_image_artcraft_flux_2_lora_angles;
|
||||
|
||||
+20
-1
@@ -19,6 +19,8 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::plan_generate_image_artcraft_seedream_4;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::plan_generate_image_artcraft_seedream_4p5;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::plan_generate_image_artcraft_seedream_5_lite;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::plan_generate_image_artcraft_qwen_edit_2511_angles;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::plan_generate_image_artcraft_flux_2_lora_angles;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::plan_generate_image_fal_nano_banana_pro;
|
||||
|
||||
pub struct GenerateImageRequest<'a> {
|
||||
@@ -45,6 +47,15 @@ pub struct GenerateImageRequest<'a> {
|
||||
/// How many images to generate.
|
||||
pub image_batch_count: Option<u16>,
|
||||
|
||||
/// Only for angle manipulation models.
|
||||
pub horizontal_angle: Option<f64>,
|
||||
|
||||
/// Only for angle manipulation models.
|
||||
pub vertical_angle: Option<f64>,
|
||||
|
||||
/// Only for angle manipulation models.
|
||||
pub zoom: Option<f64>,
|
||||
|
||||
/// If the request is a mismatch with the (model/provider), how to mitigate it.
|
||||
pub request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy,
|
||||
|
||||
@@ -96,6 +107,12 @@ impl<'a> GenerateImageRequest<'a> {
|
||||
CommonImageModel::Seedream5Lite => {
|
||||
plan_generate_image_artcraft_seedream_5_lite(self).map(ImageGenerationPlan::ArtcraftSeedream5Lite)
|
||||
}
|
||||
CommonImageModel::QwenEdit2511Angles => {
|
||||
plan_generate_image_artcraft_qwen_edit_2511_angles(self).map(ImageGenerationPlan::ArtcraftQwenEdit2511Angles)
|
||||
}
|
||||
CommonImageModel::Flux2LoraAngles => {
|
||||
plan_generate_image_artcraft_flux_2_lora_angles(self).map(ImageGenerationPlan::ArtcraftFlux2LoraAngles)
|
||||
}
|
||||
},
|
||||
Provider::Fal => match self.model {
|
||||
CommonImageModel::NanaBananaPro => {
|
||||
@@ -110,7 +127,9 @@ impl<'a> GenerateImageRequest<'a> {
|
||||
| CommonImageModel::NanaBanana2
|
||||
| CommonImageModel::Seedream4
|
||||
| CommonImageModel::Seedream4p5
|
||||
| CommonImageModel::Seedream5Lite => {
|
||||
| CommonImageModel::Seedream5Lite
|
||||
| CommonImageModel::QwenEdit2511Angles
|
||||
| CommonImageModel::Flux2LoraAngles => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "provider",
|
||||
value: format!("{:?} is only available on the Artcraft provider", self.model),
|
||||
|
||||
+22
@@ -11,6 +11,8 @@ use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraf
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_4::estimate_image_cost_artcraft_seedream_4;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_4p5::estimate_image_cost_artcraft_seedream_4p5;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_5_lite::estimate_image_cost_artcraft_seedream_5_lite;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_qwen_edit_2511_angles::estimate_image_cost_artcraft_qwen_edit_2511_angles;
|
||||
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_2_lora_angles::estimate_image_cost_artcraft_flux_2_lora_angles;
|
||||
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_nano_banana_pro::estimate_image_cost_fal_nano_banana_pro;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_1_dev::execute_artcraft_flux_1_dev;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_1_schnell::execute_artcraft_flux_1_schnell;
|
||||
@@ -23,6 +25,8 @@ use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_4::execute_artcraft_seedream_4;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_4p5::execute_artcraft_seedream_4p5;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_5_lite::execute_artcraft_seedream_5_lite;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_qwen_edit_2511_angles::execute_artcraft_qwen_edit_2511_angles;
|
||||
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_2_lora_angles::execute_artcraft_flux_2_lora_angles;
|
||||
use crate::generate::generate_image::execute::fal::generate_image_fal_nano_banana_pro::execute_fal_nano_banana_pro;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
@@ -37,6 +41,8 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::PlanArtcraftSeedream4;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::PlanArtcraftSeedream4p5;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::PlanArtcraftSeedream5Lite;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::PlanArtcraftQwenEdit2511Angles;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::PlanArtcraftFlux2LoraAngles;
|
||||
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::PlanFalNanaBananaPro;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -52,6 +58,8 @@ pub enum ImageGenerationPlan<'a> {
|
||||
ArtcraftSeedream4(PlanArtcraftSeedream4<'a>),
|
||||
ArtcraftSeedream4p5(PlanArtcraftSeedream4p5<'a>),
|
||||
ArtcraftSeedream5Lite(PlanArtcraftSeedream5Lite<'a>),
|
||||
ArtcraftQwenEdit2511Angles(PlanArtcraftQwenEdit2511Angles<'a>),
|
||||
ArtcraftFlux2LoraAngles(PlanArtcraftFlux2LoraAngles<'a>),
|
||||
FalNanaBananaPro(PlanFalNanaBananaPro<'a>),
|
||||
}
|
||||
|
||||
@@ -105,6 +113,14 @@ impl<'a> ImageGenerationPlan<'a> {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_seedream_5_lite(plan, artcraft_client).await
|
||||
}
|
||||
ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_qwen_edit_2511_angles(plan, artcraft_client).await
|
||||
}
|
||||
ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_flux_2_lora_angles(plan, artcraft_client).await
|
||||
}
|
||||
ImageGenerationPlan::FalNanaBananaPro(plan) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
execute_fal_nano_banana_pro(plan, fal_client).await
|
||||
@@ -147,6 +163,12 @@ impl<'a> ImageGenerationPlan<'a> {
|
||||
ImageGenerationPlan::ArtcraftSeedream5Lite(plan) => {
|
||||
estimate_image_cost_artcraft_seedream_5_lite(plan)
|
||||
}
|
||||
ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) => {
|
||||
estimate_image_cost_artcraft_qwen_edit_2511_angles(plan)
|
||||
}
|
||||
ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) => {
|
||||
estimate_image_cost_artcraft_flux_2_lora_angles(plan)
|
||||
}
|
||||
ImageGenerationPlan::FalNanaBananaPro(plan) => {
|
||||
estimate_image_cost_fal_nano_banana_pro(plan)
|
||||
}
|
||||
|
||||
+2
@@ -9,3 +9,5 @@ pub mod plan_generate_image_artcraft_nano_banana_pro;
|
||||
pub mod plan_generate_image_artcraft_seedream_4;
|
||||
pub mod plan_generate_image_artcraft_seedream_4p5;
|
||||
pub mod plan_generate_image_artcraft_seedream_5_lite;
|
||||
pub mod plan_generate_image_artcraft_qwen_edit_2511_angles;
|
||||
pub mod plan_generate_image_artcraft_flux_2_lora_angles;
|
||||
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
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 artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::{
|
||||
Flux2LoraEditImageAngleImageSize,
|
||||
Flux2LoraEditImageAngleNumImages,
|
||||
};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftFlux2LoraAngles<'a> {
|
||||
pub image_input: &'a MediaFileToken,
|
||||
pub horizontal_angle: Option<f64>,
|
||||
pub vertical_angle: Option<f64>,
|
||||
pub zoom: Option<f64>,
|
||||
pub image_size: Option<Flux2LoraEditImageAngleImageSize>,
|
||||
pub num_images: Flux2LoraEditImageAngleNumImages,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_flux_2_lora_angles<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<PlanArtcraftFlux2LoraAngles<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Angle models require exactly one input image.
|
||||
let image_input = resolve_single_image_input(request.image_inputs)?;
|
||||
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftFlux2LoraAngles {
|
||||
image_input,
|
||||
horizontal_angle: request.horizontal_angle,
|
||||
vertical_angle: request.vertical_angle,
|
||||
zoom: request.zoom,
|
||||
image_size,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_single_image_input<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<&'a MediaFileToken, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: "Angle models require exactly one input image".to_string(),
|
||||
}))
|
||||
}
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => {
|
||||
if tokens.len() != 1 {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: format!("Angle models require exactly one input image, got {}", tokens.len()),
|
||||
}));
|
||||
}
|
||||
Ok(&tokens[0])
|
||||
}
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flux 2 LoRA supported image sizes:
|
||||
// Square, SquareHd
|
||||
// PortraitFourThree, PortraitSixteenNine
|
||||
// LandscapeFourThree, LandscapeSixteenNine
|
||||
fn plan_image_size(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Flux2LoraEditImageAngleImageSize>, ArtcraftRouterError> {
|
||||
use Flux2LoraEditImageAngleImageSize as S;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto4k) => {
|
||||
Ok(Some(S::SquareHd))
|
||||
}
|
||||
|
||||
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
|
||||
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
|
||||
|
||||
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
|
||||
|
||||
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
|
||||
|
||||
Some(unsupported @ CommonAspectRatio::WideFiveByFour)
|
||||
| Some(unsupported @ CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine)
|
||||
| Some(unsupported @ CommonAspectRatio::TallFourByFive)
|
||||
| Some(unsupported @ CommonAspectRatio::TallTwoByThree)
|
||||
| Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(S::SquareHd)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Flux2LoraEditImageAngleNumImages, ArtcraftRouterError> {
|
||||
use Flux2LoraEditImageAngleNumImages as N;
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(N::One),
|
||||
2 => Ok(N::Two),
|
||||
3 => Ok(N::Three),
|
||||
4 => Ok(N::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(N::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::base_flux_2_lora_angles_image_request;
|
||||
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::{
|
||||
Flux2LoraEditImageAngleImageSize as S,
|
||||
Flux2LoraEditImageAngleNumImages as N,
|
||||
};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn make_token() -> MediaFileToken {
|
||||
MediaFileToken::new_from_str("test_token_123")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requires_image_input() {
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: None,
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_urls() {
|
||||
let urls = vec!["https://example.com/img.png".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_single_media_token() {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_multiple_media_tokens() {
|
||||
let tokens = vec![make_token(), make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_none_is_none() {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: None,
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
|
||||
assert!(plan.image_size.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_square_maps_directly() {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
|
||||
assert!(matches!(plan.image_size, Some(S::Square)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_direct_mapping() {
|
||||
let tokens = vec![make_token()];
|
||||
let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)];
|
||||
for (count, expected) in cases {
|
||||
let request = GenerateImageRequest {
|
||||
image_batch_count: Some(count),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
|
||||
assert!(
|
||||
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
|
||||
"expected {:?} for count {}", expected, count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn angle_fields_are_passed_through() {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
horizontal_angle: Some(45.0),
|
||||
vertical_angle: Some(-30.0),
|
||||
zoom: Some(1.5),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_flux_2_lora_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
|
||||
assert_eq!(plan.horizontal_angle, Some(45.0));
|
||||
assert_eq!(plan.vertical_angle, Some(-30.0));
|
||||
assert_eq!(plan.zoom, Some(1.5));
|
||||
}
|
||||
}
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
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 artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::{
|
||||
QwenEdit2511EditImageAngleImageSize,
|
||||
QwenEdit2511EditImageAngleNumImages,
|
||||
};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftQwenEdit2511Angles<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub image_input: &'a MediaFileToken,
|
||||
pub horizontal_angle: Option<f64>,
|
||||
pub vertical_angle: Option<f64>,
|
||||
pub zoom: Option<f64>,
|
||||
pub image_size: Option<QwenEdit2511EditImageAngleImageSize>,
|
||||
pub num_images: QwenEdit2511EditImageAngleNumImages,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_image_artcraft_qwen_edit_2511_angles<'a>(
|
||||
request: &'a GenerateImageRequest<'a>,
|
||||
) -> Result<PlanArtcraftQwenEdit2511Angles<'a>, ArtcraftRouterError> {
|
||||
let strategy = request.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Angle models require exactly one input image.
|
||||
let image_input = resolve_single_image_input(request.image_inputs)?;
|
||||
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(request.image_batch_count, strategy)?;
|
||||
|
||||
Ok(PlanArtcraftQwenEdit2511Angles {
|
||||
prompt: request.prompt,
|
||||
image_input,
|
||||
horizontal_angle: request.horizontal_angle,
|
||||
vertical_angle: request.vertical_angle,
|
||||
zoom: request.zoom,
|
||||
image_size,
|
||||
num_images,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_single_image_input<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<&'a MediaFileToken, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: "Angle models require exactly one input image".to_string(),
|
||||
}))
|
||||
}
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => {
|
||||
if tokens.len() != 1 {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: format!("Angle models require exactly one input image, got {}", tokens.len()),
|
||||
}));
|
||||
}
|
||||
Ok(&tokens[0])
|
||||
}
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Qwen Edit 2511 supported image sizes:
|
||||
// Square, SquareHd
|
||||
// PortraitFourThree, PortraitSixteenNine
|
||||
// LandscapeFourThree, LandscapeSixteenNine
|
||||
fn plan_image_size(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<QwenEdit2511EditImageAngleImageSize>, ArtcraftRouterError> {
|
||||
use QwenEdit2511EditImageAngleImageSize as S;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto4k) => {
|
||||
Ok(Some(S::SquareHd))
|
||||
}
|
||||
|
||||
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
|
||||
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
|
||||
|
||||
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
|
||||
|
||||
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
|
||||
|
||||
Some(unsupported @ CommonAspectRatio::WideFiveByFour)
|
||||
| Some(unsupported @ CommonAspectRatio::WideThreeByTwo)
|
||||
| Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine)
|
||||
| Some(unsupported @ CommonAspectRatio::TallFourByFive)
|
||||
| Some(unsupported @ CommonAspectRatio::TallTwoByThree)
|
||||
| Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(S::SquareHd)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<QwenEdit2511EditImageAngleNumImages, ArtcraftRouterError> {
|
||||
use QwenEdit2511EditImageAngleNumImages as N;
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(N::One),
|
||||
2 => Ok(N::Two),
|
||||
3 => Ok(N::Three),
|
||||
4 => Ok(N::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(N::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::test_helpers::base_qwen_edit_2511_angles_image_request;
|
||||
use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::{
|
||||
QwenEdit2511EditImageAngleImageSize as S,
|
||||
QwenEdit2511EditImageAngleNumImages as N,
|
||||
};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn make_token() -> MediaFileToken {
|
||||
MediaFileToken::new_from_str("test_token_123")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requires_image_input() {
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: None,
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_urls() {
|
||||
let urls = vec!["https://example.com/img.png".to_string()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::Urls(&urls)),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_single_media_token() {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_multiple_media_tokens() {
|
||||
let tokens = vec![make_token(), make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let result = request.build();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_none_is_none() {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: None,
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) = request.build().unwrap() else { panic!("expected ArtcraftQwenEdit2511Angles") };
|
||||
assert!(plan.image_size.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_size_square_maps_directly() {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
aspect_ratio: Some(CommonAspectRatio::Square),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) = request.build().unwrap() else { panic!("expected ArtcraftQwenEdit2511Angles") };
|
||||
assert!(matches!(plan.image_size, Some(S::Square)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_images_direct_mapping() {
|
||||
let tokens = vec![make_token()];
|
||||
let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)];
|
||||
for (count, expected) in cases {
|
||||
let request = GenerateImageRequest {
|
||||
image_batch_count: Some(count),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) = request.build().unwrap() else { panic!("expected ArtcraftQwenEdit2511Angles") };
|
||||
assert!(
|
||||
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
|
||||
"expected {:?} for count {}", expected, count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn angle_fields_are_passed_through() {
|
||||
let tokens = vec![make_token()];
|
||||
let request = GenerateImageRequest {
|
||||
horizontal_angle: Some(45.0),
|
||||
vertical_angle: Some(-30.0),
|
||||
zoom: Some(1.5),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
|
||||
..base_qwen_edit_2511_angles_image_request()
|
||||
};
|
||||
let ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) = request.build().unwrap() else { panic!("expected ArtcraftQwenEdit2511Angles") };
|
||||
assert_eq!(plan.horizontal_angle, Some(45.0));
|
||||
assert_eq!(plan.vertical_angle, Some(-30.0));
|
||||
assert_eq!(plan.zoom, Some(1.5));
|
||||
}
|
||||
}
|
||||
+3
@@ -254,6 +254,9 @@ mod tests {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_mini::PlanArtcraftMarble0p1Mini;
|
||||
use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate;
|
||||
use world_labs_api::api::api_types::world_labs_model::WorldLabsModel;
|
||||
use world_labs_api::pricing::check_pricing::{calculate_cost, InputType};
|
||||
|
||||
pub(crate) fn estimate_splat_cost_artcraft_marble_0p1_mini(
|
||||
plan: &PlanArtcraftMarble0p1Mini<'_>,
|
||||
) -> SplatGenerationCostEstimate {
|
||||
let input_type = if plan.reference_image.is_some() {
|
||||
InputType::ImageNonPanorama
|
||||
} else {
|
||||
InputType::Text
|
||||
};
|
||||
|
||||
let cost = calculate_cost(WorldLabsModel::Marble0p1Mini, input_type);
|
||||
let cost_in_usd_cents = cost.us_dollar_cents as u64;
|
||||
|
||||
SplatGenerationCostEstimate {
|
||||
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 crate::api::common_splat_model::CommonSplatModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::generate::generate_splat::generate_splat_request::GenerateSplatRequest;
|
||||
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn estimate_usd_cents(prompt: Option<&str>, image_tokens: Option<&Vec<MediaFileToken>>) -> u64 {
|
||||
let request = GenerateSplatRequest {
|
||||
model: CommonSplatModel::Marble0p1Mini,
|
||||
provider: Provider::Artcraft,
|
||||
prompt,
|
||||
reference_images: image_tokens.map(crate::api::image_list_ref::ImageListRef::MediaFileTokens),
|
||||
idempotency_token: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
.estimate_costs()
|
||||
.cost_in_usd_cents
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_text_only() {
|
||||
let cost = estimate_usd_cents(Some("a room"), None);
|
||||
assert_eq!(cost, 18); // 230 credits → 18 cents
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_no_image_no_prompt() {
|
||||
let cost = estimate_usd_cents(None, None);
|
||||
assert_eq!(cost, 18);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_image() {
|
||||
let tokens = vec![MediaFileToken("test_token".to_string())];
|
||||
let cost = estimate_usd_cents(None, Some(&tokens));
|
||||
assert_eq!(cost, 18); // 230 credits → 18 cents (same as text for mini)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_image_with_prompt() {
|
||||
let tokens = vec![MediaFileToken("test_token".to_string())];
|
||||
let cost = estimate_usd_cents(Some("a cozy room"), Some(&tokens));
|
||||
assert_eq!(cost, 18); // 230 credits → 18 cents
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_plus::PlanArtcraftMarble0p1Plus;
|
||||
use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate;
|
||||
use world_labs_api::api::api_types::world_labs_model::WorldLabsModel;
|
||||
use world_labs_api::pricing::check_pricing::{calculate_cost, InputType};
|
||||
|
||||
pub(crate) fn estimate_splat_cost_artcraft_marble_0p1_plus(
|
||||
plan: &PlanArtcraftMarble0p1Plus<'_>,
|
||||
) -> SplatGenerationCostEstimate {
|
||||
let input_type = if plan.reference_image.is_some() {
|
||||
InputType::ImageNonPanorama
|
||||
} else {
|
||||
InputType::Text
|
||||
};
|
||||
|
||||
let cost = calculate_cost(WorldLabsModel::Marble0p1Plus, input_type);
|
||||
let cost_in_usd_cents = cost.us_dollar_cents as u64;
|
||||
|
||||
SplatGenerationCostEstimate {
|
||||
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 crate::api::common_splat_model::CommonSplatModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::generate::generate_splat::generate_splat_request::GenerateSplatRequest;
|
||||
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn estimate_usd_cents(prompt: Option<&str>, image_tokens: Option<&Vec<MediaFileToken>>) -> u64 {
|
||||
let request = GenerateSplatRequest {
|
||||
model: CommonSplatModel::Marble0p1Plus,
|
||||
provider: Provider::Artcraft,
|
||||
prompt,
|
||||
reference_images: image_tokens.map(crate::api::image_list_ref::ImageListRef::MediaFileTokens),
|
||||
idempotency_token: None,
|
||||
};
|
||||
request.build()
|
||||
.expect("build should succeed")
|
||||
.estimate_costs()
|
||||
.cost_in_usd_cents
|
||||
.expect("cost_in_usd_cents should be present")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_text_only() {
|
||||
let cost = estimate_usd_cents(Some("a room"), None);
|
||||
assert_eq!(cost, 126); // 1580 credits → 126 cents
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_no_image_no_prompt() {
|
||||
let cost = estimate_usd_cents(None, None);
|
||||
assert_eq!(cost, 126);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_image() {
|
||||
let tokens = vec![MediaFileToken("test_token".to_string())];
|
||||
let cost = estimate_usd_cents(None, Some(&tokens));
|
||||
assert_eq!(cost, 126); // 1580 credits → 126 cents (same as text for plus)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_estimate_cost_image_with_prompt() {
|
||||
let tokens = vec![MediaFileToken("test_token".to_string())];
|
||||
let cost = estimate_usd_cents(Some("a cozy room"), Some(&tokens));
|
||||
assert_eq!(cost, 126); // 1580 credits → 126 cents
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
pub mod estimate_splat_cost_artcraft_marble_0p1_mini;
|
||||
pub mod estimate_splat_cost_artcraft_marble_0p1_plus;
|
||||
@@ -0,0 +1 @@
|
||||
pub(crate) mod artcraft;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_mini::PlanArtcraftMarble0p1Mini;
|
||||
use crate::generate::generate_splat::generate_splat_response::{
|
||||
ArtcraftSplatResponsePayload, GenerateSplatResponse,
|
||||
};
|
||||
use artcraft_api_defs::generate::splat::generate_worldlabs_marble_0p1_mini_splat::GenerateWorldlabsMarble0p1MiniSplatRequest;
|
||||
use artcraft_client::endpoints::generate::splat::generate_worldlabs_marble_0p1_mini_splat::generate_worldlabs_marble_0p1_mini_splat;
|
||||
|
||||
pub async fn execute_artcraft_marble_0p1_mini(
|
||||
plan: &PlanArtcraftMarble0p1Mini<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateSplatResponse, ArtcraftRouterError> {
|
||||
let request = GenerateWorldlabsMarble0p1MiniSplatRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
image_media_file_token: plan.reference_image.cloned(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
};
|
||||
|
||||
let response = generate_worldlabs_marble_0p1_mini_splat(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateSplatResponse::Artcraft(ArtcraftSplatResponsePayload {
|
||||
inference_job_token: response.inference_job_token,
|
||||
}))
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_plus::PlanArtcraftMarble0p1Plus;
|
||||
use crate::generate::generate_splat::generate_splat_response::{
|
||||
ArtcraftSplatResponsePayload, GenerateSplatResponse,
|
||||
};
|
||||
use artcraft_api_defs::generate::splat::generate_worldlabs_marble_0p1_plus_splat::GenerateWorldlabsMarble0p1PlusSplatRequest;
|
||||
use artcraft_client::endpoints::generate::splat::generate_worldlabs_marble_0p1_plus_splat::generate_worldlabs_marble_0p1_plus_splat;
|
||||
|
||||
pub async fn execute_artcraft_marble_0p1_plus(
|
||||
plan: &PlanArtcraftMarble0p1Plus<'_>,
|
||||
artcraft_client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateSplatResponse, ArtcraftRouterError> {
|
||||
let request = GenerateWorldlabsMarble0p1PlusSplatRequest {
|
||||
uuid_idempotency_token: plan.idempotency_token.clone(),
|
||||
image_media_file_token: plan.reference_image.cloned(),
|
||||
prompt: plan.prompt.map(|p| p.to_string()),
|
||||
};
|
||||
|
||||
let response = generate_worldlabs_marble_0p1_plus_splat(
|
||||
&artcraft_client.api_host,
|
||||
Some(&artcraft_client.credentials),
|
||||
request,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateSplatResponse::Artcraft(ArtcraftSplatResponsePayload {
|
||||
inference_job_token: response.inference_job_token,
|
||||
}))
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
pub mod generate_splat_artcraft_marble_0p1_mini;
|
||||
pub mod generate_splat_artcraft_marble_0p1_plus;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod artcraft;
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
use crate::api::common_splat_model::CommonSplatModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_mini::plan_generate_splat_artcraft_marble_0p1_mini;
|
||||
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_plus::plan_generate_splat_artcraft_marble_0p1_plus;
|
||||
use crate::generate::generate_splat::splat_generation_plan::SplatGenerationPlan;
|
||||
|
||||
pub struct GenerateSplatRequest<'a> {
|
||||
/// Which model to use.
|
||||
pub model: CommonSplatModel,
|
||||
|
||||
/// Which provider to use.
|
||||
pub provider: Provider,
|
||||
|
||||
/// The prompt for splat generation (optional).
|
||||
pub prompt: Option<&'a str>,
|
||||
|
||||
/// Reference images (optional).
|
||||
pub reference_images: Option<ImageListRef<'a>>,
|
||||
|
||||
/// Some providers support idempotency.
|
||||
/// If not supplied, we'll generate one for the required providers.
|
||||
pub idempotency_token: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> GenerateSplatRequest<'a> {
|
||||
/// Read the splat generation request, construct a plan, then yield a means to execute it.
|
||||
pub fn build(&self) -> Result<SplatGenerationPlan<'_>, ArtcraftRouterError> {
|
||||
match self.provider {
|
||||
Provider::Artcraft => match self.model {
|
||||
CommonSplatModel::Marble0p1Mini => {
|
||||
plan_generate_splat_artcraft_marble_0p1_mini(self).map(SplatGenerationPlan::ArtcraftMarble0p1Mini)
|
||||
}
|
||||
CommonSplatModel::Marble0p1Plus => {
|
||||
plan_generate_splat_artcraft_marble_0p1_plus(self).map(SplatGenerationPlan::ArtcraftMarble0p1Plus)
|
||||
}
|
||||
},
|
||||
Provider::Fal => Err(ArtcraftRouterError::UnsupportedModel(
|
||||
format!("Splat generation via Fal is not yet supported (model: {:?})", self.model)
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_or_generate_idempotency_token(&self) -> String {
|
||||
self.idempotency_token.map(|t| t.to_string())
|
||||
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSplatResponsePayload {
|
||||
pub inference_job_token: InferenceJobToken,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum GenerateSplatResponse {
|
||||
Artcraft(ArtcraftSplatResponsePayload),
|
||||
}
|
||||
|
||||
impl GenerateSplatResponse {
|
||||
pub fn get_artcraft_payload(&self) -> Option<ArtcraftSplatResponsePayload> {
|
||||
match self {
|
||||
Self::Artcraft(p) => Some(p.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod cost;
|
||||
pub mod execute;
|
||||
pub mod generate_splat_request;
|
||||
pub mod generate_splat_response;
|
||||
pub mod plan;
|
||||
pub mod splat_generation_cost_estimate;
|
||||
pub mod splat_generation_plan;
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
pub mod plan_generate_splat_artcraft_marble_0p1_mini;
|
||||
pub mod plan_generate_splat_artcraft_marble_0p1_plus;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_splat::generate_splat_request::GenerateSplatRequest;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftMarble0p1Mini<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub reference_image: Option<&'a MediaFileToken>,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_splat_artcraft_marble_0p1_mini<'a>(
|
||||
request: &'a GenerateSplatRequest<'a>,
|
||||
) -> Result<PlanArtcraftMarble0p1Mini<'a>, ArtcraftRouterError> {
|
||||
let reference_image = resolve_single_image_ref(request.reference_images)?;
|
||||
|
||||
Ok(PlanArtcraftMarble0p1Mini {
|
||||
prompt: request.prompt,
|
||||
reference_image,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_single_image_ref<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<Option<&'a MediaFileToken>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => {
|
||||
Ok(tokens.first())
|
||||
}
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_splat::generate_splat_request::GenerateSplatRequest;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlanArtcraftMarble0p1Plus<'a> {
|
||||
pub prompt: Option<&'a str>,
|
||||
pub reference_image: Option<&'a MediaFileToken>,
|
||||
pub idempotency_token: String,
|
||||
}
|
||||
|
||||
pub fn plan_generate_splat_artcraft_marble_0p1_plus<'a>(
|
||||
request: &'a GenerateSplatRequest<'a>,
|
||||
) -> Result<PlanArtcraftMarble0p1Plus<'a>, ArtcraftRouterError> {
|
||||
let reference_image = resolve_single_image_ref(request.reference_images)?;
|
||||
|
||||
Ok(PlanArtcraftMarble0p1Plus {
|
||||
prompt: request.prompt,
|
||||
reference_image,
|
||||
idempotency_token: request.get_or_generate_idempotency_token(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_single_image_ref<'a>(
|
||||
image_list_ref: Option<ImageListRef<'a>>,
|
||||
) -> Result<Option<&'a MediaFileToken>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => {
|
||||
Ok(tokens.first())
|
||||
}
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod artcraft;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/// The result of estimating the cost of a splat generation plan.
|
||||
pub struct SplatGenerationCostEstimate {
|
||||
pub cost_in_credits: Option<u64>,
|
||||
pub cost_in_usd_cents: Option<u64>,
|
||||
pub is_free: bool,
|
||||
pub is_unlimited: bool,
|
||||
pub is_rate_limited: bool,
|
||||
pub has_watermark: bool,
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_splat::cost::artcraft::estimate_splat_cost_artcraft_marble_0p1_mini::estimate_splat_cost_artcraft_marble_0p1_mini;
|
||||
use crate::generate::generate_splat::cost::artcraft::estimate_splat_cost_artcraft_marble_0p1_plus::estimate_splat_cost_artcraft_marble_0p1_plus;
|
||||
use crate::generate::generate_splat::execute::artcraft::generate_splat_artcraft_marble_0p1_mini::execute_artcraft_marble_0p1_mini;
|
||||
use crate::generate::generate_splat::execute::artcraft::generate_splat_artcraft_marble_0p1_plus::execute_artcraft_marble_0p1_plus;
|
||||
use crate::generate::generate_splat::generate_splat_response::GenerateSplatResponse;
|
||||
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_mini::PlanArtcraftMarble0p1Mini;
|
||||
use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_plus::PlanArtcraftMarble0p1Plus;
|
||||
use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SplatGenerationPlan<'a> {
|
||||
ArtcraftMarble0p1Mini(PlanArtcraftMarble0p1Mini<'a>),
|
||||
ArtcraftMarble0p1Plus(PlanArtcraftMarble0p1Plus<'a>),
|
||||
}
|
||||
|
||||
impl<'a> SplatGenerationPlan<'a> {
|
||||
pub async fn generate_splat(
|
||||
&self,
|
||||
client: &RouterClient,
|
||||
) -> Result<GenerateSplatResponse, ArtcraftRouterError> {
|
||||
match self {
|
||||
SplatGenerationPlan::ArtcraftMarble0p1Mini(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_marble_0p1_mini(plan, artcraft_client).await
|
||||
}
|
||||
SplatGenerationPlan::ArtcraftMarble0p1Plus(plan) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
execute_artcraft_marble_0p1_plus(plan, artcraft_client).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_costs(&self) -> SplatGenerationCostEstimate {
|
||||
match self {
|
||||
SplatGenerationPlan::ArtcraftMarble0p1Mini(plan) => {
|
||||
estimate_splat_cost_artcraft_marble_0p1_mini(plan)
|
||||
}
|
||||
SplatGenerationPlan::ArtcraftMarble0p1Plus(plan) => {
|
||||
estimate_splat_cost_artcraft_marble_0p1_plus(plan)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod generate_image;
|
||||
pub mod generate_splat;
|
||||
pub mod generate_video;
|
||||
|
||||
@@ -33,6 +33,9 @@ pub fn base_image_request() -> GenerateImageRequest<'static> {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +59,9 @@ pub fn base_fal_image_request() -> GenerateImageRequest<'static> {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +77,9 @@ pub fn base_seedream_4_image_request() -> GenerateImageRequest<'static> {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +95,9 @@ pub fn base_seedream_4p5_image_request() -> GenerateImageRequest<'static> {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +113,9 @@ pub fn base_seedream_5_lite_image_request() -> GenerateImageRequest<'static> {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +131,9 @@ pub fn base_nano_banana_2_image_request() -> GenerateImageRequest<'static> {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +149,9 @@ pub fn base_nano_banana_image_request() -> GenerateImageRequest<'static> {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +167,9 @@ pub fn base_gpt_image_1p5_image_request() -> GenerateImageRequest<'static> {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +185,9 @@ pub fn base_flux_1_dev_image_request() -> GenerateImageRequest<'static> {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +203,9 @@ pub fn base_flux_1_schnell_image_request() -> GenerateImageRequest<'static> {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +221,45 @@ pub fn base_flux_pro_1p1_image_request() -> GenerateImageRequest<'static> {
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn base_qwen_edit_2511_angles_image_request() -> GenerateImageRequest<'static> {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::QwenEdit2511Angles,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: 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,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn base_flux_2_lora_angles_image_request() -> GenerateImageRequest<'static> {
|
||||
GenerateImageRequest {
|
||||
model: CommonImageModel::Flux2LoraAngles,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,6 +275,9 @@ pub fn base_flux_pro_1p1_ultra_image_request() -> GenerateImageRequest<'static>
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
use fal::request::FalRequest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct Flux2LoraEditImageAngleInput {
|
||||
/// The URL of the image to adjust camera angle for.
|
||||
pub image_urls: Vec<String>,
|
||||
|
||||
/// Horizontal rotation angle around the object in degrees.
|
||||
/// 0°=front view, 90°=right side, 180°=back view, 270°=left side.
|
||||
/// Default: 0
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub horizontal_angle: Option<f64>,
|
||||
|
||||
/// Vertical camera angle in degrees.
|
||||
/// 0°=eye-level shot, 30°=elevated shot, 60°=high-angle shot.
|
||||
/// Default: 0
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub vertical_angle: Option<f64>,
|
||||
|
||||
/// Camera zoom/distance. 0=wide shot, 5=medium shot, 10=close-up.
|
||||
/// Default: 5
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub zoom: Option<f64>,
|
||||
|
||||
/// The strength of the multiple angles effect.
|
||||
/// Default: 1, range: 0-2
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub lora_scale: Option<f64>,
|
||||
|
||||
/// The size of the generated image.
|
||||
/// Options: square_hd, square, portrait_4_3, portrait_16_9, landscape_4_3, landscape_16_9
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_size: Option<String>,
|
||||
|
||||
/// The CFG (Classifier Free Guidance) scale.
|
||||
/// Default: 2.5, range: 0-20
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub guidance_scale: Option<f64>,
|
||||
|
||||
/// The number of inference steps to perform.
|
||||
/// Default: 40, range: 1-50
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_inference_steps: Option<u32>,
|
||||
|
||||
/// Random seed for reproducibility.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub seed: Option<u64>,
|
||||
|
||||
/// Number of images to generate.
|
||||
/// Default: 1, max: 4
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_images: Option<u8>,
|
||||
|
||||
/// Whether to enable the safety checker.
|
||||
/// Default: true
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enable_safety_checker: Option<bool>,
|
||||
|
||||
/// Output format: png, jpeg, webp.
|
||||
/// Default: "png"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_format: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Flux2LoraEditImageAngleFile {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Flux2LoraEditImageAngleOutput {
|
||||
pub images: Vec<Flux2LoraEditImageAngleFile>,
|
||||
pub seed: u64,
|
||||
pub prompt: Option<String>,
|
||||
}
|
||||
|
||||
pub fn http_flux_2_lora_edit_image_angle(
|
||||
params: Flux2LoraEditImageAngleInput,
|
||||
) -> FalRequest<Flux2LoraEditImageAngleInput, Flux2LoraEditImageAngleOutput> {
|
||||
FalRequest::new("fal-ai/flux-2-lora-gallery/multiple-angles", params)
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
use fal::request::FalRequest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
||||
pub struct QwenEdit2511EditImageAngleInput {
|
||||
/// The URL of the image to adjust camera angle for.
|
||||
pub image_urls: Vec<String>,
|
||||
|
||||
/// Horizontal rotation angle around the object in degrees. 0°=front view.
|
||||
/// Default: 0
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub horizontal_angle: Option<f64>,
|
||||
|
||||
/// Vertical camera angle in degrees. -30°=low-angle shot, 30°=high-angle shot.
|
||||
/// Default: 0
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub vertical_angle: Option<f64>,
|
||||
|
||||
/// Camera zoom/distance. 0=wide shot, 5=medium shot, 10=close-up.
|
||||
/// Default: 5
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub zoom: Option<f64>,
|
||||
|
||||
/// Additional text to append to the automatically generated prompt.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub additional_prompt: Option<String>,
|
||||
|
||||
/// The scale factor for the LoRA model. Controls the strength of the effect.
|
||||
/// Default: 1
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub lora_scale: Option<f64>,
|
||||
|
||||
/// The size of the generated image.
|
||||
/// Options: square_hd, square, portrait_4_3, portrait_16_9, landscape_4_3, landscape_16_9
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub image_size: Option<String>,
|
||||
|
||||
/// The CFG (Classifier Free Guidance) scale.
|
||||
/// Default: 4.5
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub guidance_scale: Option<f64>,
|
||||
|
||||
/// The number of inference steps to perform.
|
||||
/// Default: 28
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_inference_steps: Option<u32>,
|
||||
|
||||
/// The negative prompt for the generation.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub negative_prompt: Option<String>,
|
||||
|
||||
/// Random seed for reproducibility.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub seed: Option<u64>,
|
||||
|
||||
/// Number of images to generate.
|
||||
/// Default: 1
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub num_images: Option<u8>,
|
||||
|
||||
/// Whether to enable the safety checker.
|
||||
/// Default: true
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enable_safety_checker: Option<bool>,
|
||||
|
||||
/// Output format: png, jpeg, webp.
|
||||
/// Default: "png"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_format: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct QwenEdit2511EditImageAngleFile {
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct QwenEdit2511EditImageAngleOutput {
|
||||
pub images: Vec<QwenEdit2511EditImageAngleFile>,
|
||||
pub seed: u64,
|
||||
pub prompt: Option<String>,
|
||||
}
|
||||
|
||||
pub fn http_qwen_edit_2511_edit_image_angle(
|
||||
params: QwenEdit2511EditImageAngleInput,
|
||||
) -> FalRequest<QwenEdit2511EditImageAngleInput, QwenEdit2511EditImageAngleOutput> {
|
||||
FalRequest::new("fal-ai/qwen-image-edit-2511-multiple-angles", params)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod http_flux_2_lora_edit_image_angle;
|
||||
pub mod http_qwen_edit_2511_edit_image_angle;
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod angle;
|
||||
pub mod http_nano_banana_2_edit_image;
|
||||
pub mod http_nano_banana_2_text_to_image;
|
||||
pub mod http_seedream_5_edit_image;
|
||||
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::http::image::angle::http_flux_2_lora_edit_image_angle::{http_flux_2_lora_edit_image_angle, Flux2LoraEditImageAngleInput};
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueFlux2LoraEditImageAngleArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub image_urls: Vec<String>,
|
||||
|
||||
// Camera parameters
|
||||
pub horizontal_angle: Option<f64>,
|
||||
pub vertical_angle: Option<f64>,
|
||||
pub zoom: Option<f64>,
|
||||
|
||||
// Optional args
|
||||
pub num_images: Option<EnqueueFlux2LoraAngleNumImages>,
|
||||
pub image_size: Option<EnqueueFlux2LoraAngleImageSize>,
|
||||
pub lora_scale: Option<f64>,
|
||||
pub guidance_scale: Option<f64>,
|
||||
pub num_inference_steps: Option<u32>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueFlux2LoraAngleNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueFlux2LoraAngleImageSize {
|
||||
Square,
|
||||
SquareHd,
|
||||
PortraitFourThree,
|
||||
PortraitSixteenNine,
|
||||
LandscapeFourThree,
|
||||
LandscapeSixteenNine,
|
||||
}
|
||||
|
||||
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueFlux2LoraEditImageAngleArgs<'_, U> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Pricing: $0.021 per megapixel.
|
||||
// For a 1024x1024 image (~1 MP), that's ~2 cents per image.
|
||||
let unit_cost = 2;
|
||||
let cost = match self.num_images {
|
||||
None => unit_cost,
|
||||
Some(EnqueueFlux2LoraAngleNumImages::One) => unit_cost,
|
||||
Some(EnqueueFlux2LoraAngleNumImages::Two) => unit_cost * 2,
|
||||
Some(EnqueueFlux2LoraAngleNumImages::Three) => unit_cost * 3,
|
||||
Some(EnqueueFlux2LoraAngleNumImages::Four) => unit_cost * 4,
|
||||
};
|
||||
cost as UsdCents
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn enqueue_flux_2_lora_edit_image_angle_webhook<R: IntoUrl>(
|
||||
args: EnqueueFlux2LoraEditImageAngleArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let num_images = args.num_images
|
||||
.map(|n| match n {
|
||||
EnqueueFlux2LoraAngleNumImages::One => 1,
|
||||
EnqueueFlux2LoraAngleNumImages::Two => 2,
|
||||
EnqueueFlux2LoraAngleNumImages::Three => 3,
|
||||
EnqueueFlux2LoraAngleNumImages::Four => 4,
|
||||
});
|
||||
|
||||
let image_size = args.image_size
|
||||
.map(|s| match s {
|
||||
EnqueueFlux2LoraAngleImageSize::Square => "square",
|
||||
EnqueueFlux2LoraAngleImageSize::SquareHd => "square_hd",
|
||||
EnqueueFlux2LoraAngleImageSize::PortraitFourThree => "portrait_4_3",
|
||||
EnqueueFlux2LoraAngleImageSize::PortraitSixteenNine => "portrait_16_9",
|
||||
EnqueueFlux2LoraAngleImageSize::LandscapeFourThree => "landscape_4_3",
|
||||
EnqueueFlux2LoraAngleImageSize::LandscapeSixteenNine => "landscape_16_9",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let request = Flux2LoraEditImageAngleInput {
|
||||
image_urls: args.image_urls,
|
||||
horizontal_angle: args.horizontal_angle,
|
||||
vertical_angle: args.vertical_angle,
|
||||
zoom: args.zoom,
|
||||
lora_scale: args.lora_scale,
|
||||
image_size,
|
||||
guidance_scale: args.guidance_scale,
|
||||
num_inference_steps: args.num_inference_steps,
|
||||
num_images,
|
||||
// Constants
|
||||
enable_safety_checker: Some(false),
|
||||
output_format: Some("png".to_string()),
|
||||
// Unused
|
||||
seed: None,
|
||||
};
|
||||
|
||||
let result = http_flux_2_lora_edit_image_angle(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
result.map_err(|err| classify_fal_error(err))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::image::angle::enqueue_flux_2_lora_edit_image_angle_webhook::{enqueue_flux_2_lora_edit_image_angle_webhook, EnqueueFlux2LoraEditImageAngleArgs, EnqueueFlux2LoraAngleNumImages, EnqueueFlux2LoraAngleImageSize};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_single() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueFlux2LoraEditImageAngleArgs {
|
||||
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
|
||||
horizontal_angle: Some(45.0),
|
||||
vertical_angle: Some(15.0),
|
||||
zoom: Some(5.0),
|
||||
num_images: Some(EnqueueFlux2LoraAngleNumImages::One),
|
||||
image_size: Some(EnqueueFlux2LoraAngleImageSize::SquareHd),
|
||||
lora_scale: None,
|
||||
guidance_scale: None,
|
||||
num_inference_steps: None,
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let _result = enqueue_flux_2_lora_edit_image_angle_webhook(args).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enqueues 27 angle combinations: 3 horizontal × 3 vertical × 3 zoom levels.
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_angle_grid() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let horizontal_angles: [f64; 3] = [-45.0, 0.0, 45.0];
|
||||
let vertical_angles: [f64; 3] = [-30.0, 0.0, 30.0];
|
||||
let zoom_levels: [f64; 3] = [0.0, 1.0, 3.0];
|
||||
|
||||
for &h in &horizontal_angles {
|
||||
for &v in &vertical_angles {
|
||||
for &z in &zoom_levels {
|
||||
let args = EnqueueFlux2LoraEditImageAngleArgs {
|
||||
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
|
||||
horizontal_angle: Some(h),
|
||||
vertical_angle: Some(v),
|
||||
zoom: Some(z),
|
||||
num_images: Some(EnqueueFlux2LoraAngleNumImages::One),
|
||||
image_size: Some(EnqueueFlux2LoraAngleImageSize::SquareHd),
|
||||
lora_scale: None,
|
||||
guidance_scale: None,
|
||||
num_inference_steps: None,
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let _result = enqueue_flux_2_lora_edit_image_angle_webhook(args).await?;
|
||||
println!("Enqueued: h={}, v={}, z={}", h, v, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::error::classify_fal_error::classify_fal_error;
|
||||
use crate::error::fal_error_plus::FalErrorPlus;
|
||||
use crate::requests::http::image::angle::http_qwen_edit_2511_edit_image_angle::{http_qwen_edit_2511_edit_image_angle, QwenEdit2511EditImageAngleInput};
|
||||
use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents};
|
||||
use fal::webhook::WebhookResponse;
|
||||
use reqwest::IntoUrl;
|
||||
|
||||
pub struct EnqueueQwenEdit2511EditImageAngleArgs<'a, R: IntoUrl> {
|
||||
// Request required
|
||||
pub image_urls: Vec<String>,
|
||||
|
||||
// Camera parameters
|
||||
pub horizontal_angle: Option<f64>,
|
||||
pub vertical_angle: Option<f64>,
|
||||
pub zoom: Option<f64>,
|
||||
|
||||
// Optional args
|
||||
pub additional_prompt: Option<&'a str>,
|
||||
pub num_images: Option<EnqueueQwenEdit2511AngleNumImages>,
|
||||
pub image_size: Option<EnqueueQwenEdit2511AngleImageSize>,
|
||||
pub lora_scale: Option<f64>,
|
||||
pub guidance_scale: Option<f64>,
|
||||
pub num_inference_steps: Option<u32>,
|
||||
|
||||
// Fulfillment
|
||||
pub webhook_url: R,
|
||||
pub api_key: &'a FalApiKey,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueQwenEdit2511AngleNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum EnqueueQwenEdit2511AngleImageSize {
|
||||
Square,
|
||||
SquareHd,
|
||||
PortraitFourThree,
|
||||
PortraitSixteenNine,
|
||||
LandscapeFourThree,
|
||||
LandscapeSixteenNine,
|
||||
}
|
||||
|
||||
impl <U: IntoUrl> FalRequestCostCalculator for EnqueueQwenEdit2511EditImageAngleArgs<'_, U> {
|
||||
fn calculate_cost_in_cents(&self) -> UsdCents {
|
||||
// Pricing: $0.035 per megapixel.
|
||||
// For a 1024x1024 image (~1 MP), that's ~4 cents per image.
|
||||
let unit_cost = 4;
|
||||
let cost = match self.num_images {
|
||||
None => unit_cost,
|
||||
Some(EnqueueQwenEdit2511AngleNumImages::One) => unit_cost,
|
||||
Some(EnqueueQwenEdit2511AngleNumImages::Two) => unit_cost * 2,
|
||||
Some(EnqueueQwenEdit2511AngleNumImages::Three) => unit_cost * 3,
|
||||
Some(EnqueueQwenEdit2511AngleNumImages::Four) => unit_cost * 4,
|
||||
};
|
||||
cost as UsdCents
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn enqueue_qwen_edit_2511_edit_image_angle_webhook<R: IntoUrl>(
|
||||
args: EnqueueQwenEdit2511EditImageAngleArgs<'_, R>
|
||||
) -> Result<WebhookResponse, FalErrorPlus> {
|
||||
|
||||
let num_images = args.num_images
|
||||
.map(|n| match n {
|
||||
EnqueueQwenEdit2511AngleNumImages::One => 1,
|
||||
EnqueueQwenEdit2511AngleNumImages::Two => 2,
|
||||
EnqueueQwenEdit2511AngleNumImages::Three => 3,
|
||||
EnqueueQwenEdit2511AngleNumImages::Four => 4,
|
||||
});
|
||||
|
||||
let image_size = args.image_size
|
||||
.map(|s| match s {
|
||||
EnqueueQwenEdit2511AngleImageSize::Square => "square",
|
||||
EnqueueQwenEdit2511AngleImageSize::SquareHd => "square_hd",
|
||||
EnqueueQwenEdit2511AngleImageSize::PortraitFourThree => "portrait_4_3",
|
||||
EnqueueQwenEdit2511AngleImageSize::PortraitSixteenNine => "portrait_16_9",
|
||||
EnqueueQwenEdit2511AngleImageSize::LandscapeFourThree => "landscape_4_3",
|
||||
EnqueueQwenEdit2511AngleImageSize::LandscapeSixteenNine => "landscape_16_9",
|
||||
})
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let request = QwenEdit2511EditImageAngleInput {
|
||||
image_urls: args.image_urls,
|
||||
horizontal_angle: args.horizontal_angle,
|
||||
vertical_angle: args.vertical_angle,
|
||||
zoom: args.zoom,
|
||||
additional_prompt: args.additional_prompt.map(|s| s.to_string()),
|
||||
lora_scale: args.lora_scale,
|
||||
image_size,
|
||||
guidance_scale: args.guidance_scale,
|
||||
num_inference_steps: args.num_inference_steps,
|
||||
num_images,
|
||||
// Constants
|
||||
enable_safety_checker: Some(false),
|
||||
output_format: Some("png".to_string()),
|
||||
// Unused
|
||||
negative_prompt: None,
|
||||
seed: None,
|
||||
};
|
||||
|
||||
let result = http_qwen_edit_2511_edit_image_angle(request)
|
||||
.with_api_key(&args.api_key.0)
|
||||
.queue_webhook(args.webhook_url)
|
||||
.await;
|
||||
|
||||
result.map_err(|err| classify_fal_error(err))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::creds::fal_api_key::FalApiKey;
|
||||
use crate::requests::webhook::image::angle::enqueue_qwen_edit_2511_edit_image_angle_webhook::{enqueue_qwen_edit_2511_edit_image_angle_webhook, EnqueueQwenEdit2511EditImageAngleArgs, EnqueueQwenEdit2511AngleNumImages, EnqueueQwenEdit2511AngleImageSize};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_single() -> AnyhowResult<()> {
|
||||
// XXX: Don't commit secrets!
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let args = EnqueueQwenEdit2511EditImageAngleArgs {
|
||||
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
|
||||
horizontal_angle: Some(45.0),
|
||||
vertical_angle: Some(15.0),
|
||||
zoom: Some(5.0),
|
||||
additional_prompt: Some("cinematic lighting"),
|
||||
num_images: Some(EnqueueQwenEdit2511AngleNumImages::One),
|
||||
image_size: Some(EnqueueQwenEdit2511AngleImageSize::SquareHd),
|
||||
lora_scale: None,
|
||||
guidance_scale: None,
|
||||
num_inference_steps: None,
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let _result = enqueue_qwen_edit_2511_edit_image_angle_webhook(args).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enqueues 9 angle combinations: 3 horizontal × 3 vertical × 3 zoom levels.
|
||||
/// Each combination generates one image, for a total of 27 enqueued requests.
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_angle_grid() -> AnyhowResult<()> {
|
||||
let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?;
|
||||
let api_key = FalApiKey::from_str(&secret);
|
||||
|
||||
let horizontal_angles: [f64; 3] = [-45.0, 0.0, 45.0];
|
||||
let vertical_angles: [f64; 3] = [-30.0, 0.0, 30.0];
|
||||
let zoom_levels: [f64; 3] = [0.0, 1.0, 3.0];
|
||||
|
||||
for &h in &horizontal_angles {
|
||||
for &v in &vertical_angles {
|
||||
for &z in &zoom_levels {
|
||||
let args = EnqueueQwenEdit2511EditImageAngleArgs {
|
||||
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
|
||||
horizontal_angle: Some(h),
|
||||
vertical_angle: Some(v),
|
||||
zoom: Some(z),
|
||||
additional_prompt: None,
|
||||
num_images: Some(EnqueueQwenEdit2511AngleNumImages::One),
|
||||
image_size: Some(EnqueueQwenEdit2511AngleImageSize::SquareHd),
|
||||
lora_scale: None,
|
||||
guidance_scale: None,
|
||||
num_inference_steps: None,
|
||||
api_key: &api_key,
|
||||
webhook_url: "https://example.com/webhook",
|
||||
};
|
||||
|
||||
let _result = enqueue_qwen_edit_2511_edit_image_angle_webhook(args).await?;
|
||||
println!("Enqueued: h={}, v={}, z={}", h, v, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod enqueue_flux_2_lora_edit_image_angle_webhook;
|
||||
pub mod enqueue_qwen_edit_2511_edit_image_angle_webhook;
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod angle;
|
||||
pub mod edit;
|
||||
pub mod infill;
|
||||
pub mod remove_background_rembg_webhook;
|
||||
|
||||
@@ -33,3 +33,4 @@ filesys.workspace = true
|
||||
env_logger.workspace = true
|
||||
env.workspace = true
|
||||
chrono.workspace = true
|
||||
strum.workspace = true
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod media_asset_id;
|
||||
pub mod operation_id;
|
||||
pub mod world_id;
|
||||
pub mod world_labs_model;
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Type for World IDs.
|
||||
/// These are used to refer to worlds
|
||||
/// These appear to be bare UUIDs.
|
||||
/// Strongly-typed World ID
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WorldObjectId(pub String);
|
||||
|
||||
impl WorldObjectId {
|
||||
pub fn new() -> Self {
|
||||
let uuid = Uuid::new_v4().to_string();
|
||||
Self(uuid)
|
||||
}
|
||||
pub struct WorldId(pub String);
|
||||
|
||||
impl WorldId {
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
Self(s.to_string())
|
||||
}
|
||||
@@ -19,8 +10,4 @@ impl WorldObjectId {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/// The available World Labs generation models.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum WorldLabsModel {
|
||||
/// `Marble 0.1-mini` is good for quick drafts
|
||||
/// Generation time: 30-45 seconds
|
||||
/// Cost: 150-330 credits
|
||||
Marble0p1Mini,
|
||||
|
||||
/// `Marble 0.1-plus` is best for final renders
|
||||
/// Generation time: ~5 minutes,
|
||||
/// Cost: 1500-1600 credits
|
||||
Marble0p1Plus,
|
||||
}
|
||||
|
||||
impl WorldLabsModel {
|
||||
/// Returns the official API name string used in HTTP requests.
|
||||
pub fn get_model_api_name_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Marble0p1Mini => "Marble 0.1-mini",
|
||||
Self::Marble0p1Plus => "Marble 0.1-plus",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::api::api_types::world_id::WorldId;
|
||||
use crate::api::requests::delete_world::http_request::RawResponse;
|
||||
use crate::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use crate::error::filter_world_labs_http_error::filter_world_labs_http_error;
|
||||
@@ -11,12 +12,12 @@ const BASE_URL: &str = "https://api.worldlabs.ai/marble/v1/worlds";
|
||||
|
||||
pub struct DeleteWorldArgs<'a> {
|
||||
pub creds: &'a WorldLabsApiCreds,
|
||||
pub world_id: &'a str,
|
||||
pub world_id: &'a WorldId,
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
pub struct DeleteWorldResponse {
|
||||
pub world_id: String,
|
||||
pub world_id: WorldId,
|
||||
pub deleted: bool,
|
||||
}
|
||||
|
||||
@@ -26,7 +27,7 @@ pub struct DeleteWorldResponse {
|
||||
pub async fn delete_world(args: DeleteWorldArgs<'_>) -> Result<DeleteWorldResponse, WorldLabsError> {
|
||||
let client = Client::new();
|
||||
|
||||
let url = format!("{}/{}", BASE_URL, args.world_id);
|
||||
let url = format!("{}/{}", BASE_URL, args.world_id.as_str());
|
||||
|
||||
debug!("Requesting URL: {}", url);
|
||||
|
||||
@@ -65,7 +66,7 @@ pub async fn delete_world(args: DeleteWorldArgs<'_>) -> Result<DeleteWorldRespon
|
||||
.map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
|
||||
|
||||
Ok(DeleteWorldResponse {
|
||||
world_id: raw.world_id,
|
||||
world_id: WorldId(raw.world_id),
|
||||
deleted: raw.deleted,
|
||||
})
|
||||
}
|
||||
@@ -85,15 +86,15 @@ mod tests {
|
||||
let creds = get_test_api_key().unwrap();
|
||||
|
||||
// Use a known world_id to delete
|
||||
let world_id = "REPLACE_WITH_REAL_ID";
|
||||
let world_id = WorldId::from_str("REPLACE_WITH_REAL_ID");
|
||||
|
||||
let response = delete_world(DeleteWorldArgs {
|
||||
creds: &creds,
|
||||
world_id,
|
||||
world_id: &world_id,
|
||||
request_timeout: None,
|
||||
}).await.unwrap();
|
||||
|
||||
println!("World ID: {}", response.world_id);
|
||||
println!("World ID: {}", response.world_id.as_str());
|
||||
println!("Deleted: {}", response.deleted);
|
||||
|
||||
assert_eq!(1, 2);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::api::api_types::operation_id::OperationId;
|
||||
use crate::api::api_types::world_labs_model::WorldLabsModel;
|
||||
use crate::api::requests::generate_world::http_request::{RawRequest, RawResponse, WorldPrompt, Permission};
|
||||
use crate::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use crate::error::filter_world_labs_http_error::filter_world_labs_http_error;
|
||||
@@ -14,7 +15,7 @@ pub struct GenerateWorldArgs<'a> {
|
||||
pub creds: &'a WorldLabsApiCreds,
|
||||
pub world_prompt: WorldPrompt,
|
||||
pub display_name: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub model: WorldLabsModel,
|
||||
pub seed: Option<u32>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub permission: Option<Permission>,
|
||||
@@ -35,7 +36,7 @@ pub async fn generate_world(args: GenerateWorldArgs<'_>) -> Result<GenerateWorld
|
||||
let payload = RawRequest {
|
||||
world_prompt: args.world_prompt,
|
||||
display_name: args.display_name,
|
||||
model: args.model,
|
||||
model: Some(args.model.get_model_api_name_str().to_string()),
|
||||
seed: args.seed,
|
||||
tags: args.tags,
|
||||
permission: args.permission,
|
||||
@@ -107,7 +108,7 @@ mod tests {
|
||||
disable_recaption: None,
|
||||
},
|
||||
display_name: Some("Test World".to_string()),
|
||||
model: Some("Marble 0.1-mini".to_string()),
|
||||
model: WorldLabsModel::Marble0p1Mini,
|
||||
seed: None,
|
||||
tags: None,
|
||||
permission: None,
|
||||
@@ -138,7 +139,7 @@ mod tests {
|
||||
disable_recaption: None,
|
||||
},
|
||||
display_name: None,
|
||||
model: Some("Marble 0.1-mini".to_string()),
|
||||
model: WorldLabsModel::Marble0p1Mini,
|
||||
seed: None,
|
||||
tags: None,
|
||||
permission: None,
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
pub mod generate_world;
|
||||
pub(crate) mod http_request;
|
||||
pub mod http_request;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::api::api_types::operation_id::OperationId;
|
||||
use crate::api::requests::get_operation::http_request::RawResponse;
|
||||
use crate::api::api_types::world_id::WorldId;
|
||||
use crate::api::requests::get_operation::http_request::{RawResponse, RawWorld};
|
||||
use crate::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use crate::error::filter_world_labs_http_error::filter_world_labs_http_error;
|
||||
use crate::error::world_labs_error::WorldLabsError;
|
||||
use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError;
|
||||
use log::{debug, error};
|
||||
use log::{debug, error, warn};
|
||||
use std::time::Duration;
|
||||
use wreq::Client;
|
||||
|
||||
@@ -24,8 +25,8 @@ pub struct GetOperationResponse {
|
||||
pub expires_at: Option<String>,
|
||||
pub error: Option<OperationError>,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
/// When done=true and no error, contains the World object.
|
||||
pub response: Option<serde_json::Value>,
|
||||
/// Structured World object, present when done=true and no error.
|
||||
pub world: Option<World>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -34,10 +35,47 @@ pub struct OperationError {
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
/// A completed World returned from an operation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct World {
|
||||
pub world_id: WorldId,
|
||||
pub display_name: Option<String>,
|
||||
pub world_marble_url: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub assets: Option<WorldAssets>,
|
||||
}
|
||||
|
||||
/// Assets associated with a completed World.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorldAssets {
|
||||
pub caption: Option<String>,
|
||||
pub thumbnail_url: Option<String>,
|
||||
pub pano_url: Option<String>,
|
||||
pub collider_mesh_url: Option<String>,
|
||||
pub splats: Option<SplatAssets>,
|
||||
}
|
||||
|
||||
/// Splat (3D Gaussian) assets for a World.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SplatAssets {
|
||||
/// Low-res 100k splat URL
|
||||
pub spz_url_100k: Option<String>,
|
||||
/// Medium-res 500k splat URL
|
||||
pub spz_url_500k: Option<String>,
|
||||
/// Full resolution splat URL
|
||||
pub spz_url_full_res: Option<String>,
|
||||
pub metric_scale_factor: Option<f64>,
|
||||
pub ground_plane_offset: Option<f64>,
|
||||
}
|
||||
|
||||
/// GET /marble/v1/operations/{operation_id}
|
||||
///
|
||||
/// Poll the status of an async operation.
|
||||
/// When `done` is true, `response` contains the World object.
|
||||
/// When `done` is true, `world` contains the structured World object.
|
||||
pub async fn get_operation(args: GetOperationArgs<'_>) -> Result<GetOperationResponse, WorldLabsError> {
|
||||
let client = Client::new();
|
||||
|
||||
@@ -79,6 +117,16 @@ pub async fn get_operation(args: GetOperationArgs<'_>) -> Result<GetOperationRes
|
||||
let raw: RawResponse = serde_json::from_str(&response_body)
|
||||
.map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
|
||||
|
||||
let world = raw.response.and_then(|raw_world| {
|
||||
match try_parse_world(raw_world) {
|
||||
Some(w) => Some(w),
|
||||
None => {
|
||||
warn!("Operation response present but missing world_id — skipping world parse");
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(GetOperationResponse {
|
||||
operation_id: raw.operation_id,
|
||||
done: raw.done.unwrap_or(false),
|
||||
@@ -90,7 +138,46 @@ pub async fn get_operation(args: GetOperationArgs<'_>) -> Result<GetOperationRes
|
||||
message: e.message,
|
||||
}),
|
||||
metadata: raw.metadata,
|
||||
response: raw.response,
|
||||
world,
|
||||
})
|
||||
}
|
||||
|
||||
/// Try to parse a RawWorld into a strongly-typed World.
|
||||
/// Returns None if world_id is missing (not a valid World).
|
||||
fn try_parse_world(raw: RawWorld) -> Option<World> {
|
||||
let world_id = raw.world_id?;
|
||||
|
||||
let assets = raw.assets.map(|a| {
|
||||
let splats = a.splats.map(|s| {
|
||||
let semantics = s.semantics_metadata;
|
||||
SplatAssets {
|
||||
spz_url_100k: s.spz_urls.as_ref().and_then(|u| u.low.clone()),
|
||||
spz_url_500k: s.spz_urls.as_ref().and_then(|u| u.medium.clone()),
|
||||
spz_url_full_res: s.spz_urls.as_ref().and_then(|u| u.full_res.clone()),
|
||||
metric_scale_factor: semantics.as_ref().and_then(|m| m.metric_scale_factor),
|
||||
ground_plane_offset: semantics.as_ref().and_then(|m| m.ground_plane_offset),
|
||||
}
|
||||
});
|
||||
|
||||
WorldAssets {
|
||||
caption: a.caption,
|
||||
thumbnail_url: a.thumbnail_url,
|
||||
pano_url: a.imagery.and_then(|i| i.pano_url),
|
||||
collider_mesh_url: a.mesh.and_then(|m| m.collider_mesh_url),
|
||||
splats,
|
||||
}
|
||||
});
|
||||
|
||||
Some(World {
|
||||
world_id: WorldId(world_id),
|
||||
display_name: raw.display_name,
|
||||
world_marble_url: raw.world_marble_url,
|
||||
created_at: raw.created_at,
|
||||
updated_at: raw.updated_at,
|
||||
model: raw.model,
|
||||
status: raw.status,
|
||||
tags: raw.tags,
|
||||
assets,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -108,7 +195,6 @@ mod tests {
|
||||
|
||||
let creds = get_test_api_key().unwrap();
|
||||
|
||||
// Use a known operation_id from a previous generate_world call
|
||||
let operation_id = OperationId("1fab3bf1-05a1-4929-907e-c6df07c539e2".to_string());
|
||||
|
||||
let response = get_operation(GetOperationArgs {
|
||||
@@ -120,9 +206,46 @@ mod tests {
|
||||
println!("Operation ID: {}", response.operation_id);
|
||||
println!("Done: {}", response.done);
|
||||
println!("Created at: {:?}", response.created_at);
|
||||
println!("Updated at: {:?}", response.updated_at);
|
||||
println!("Expires at: {:?}", response.expires_at);
|
||||
println!("Error: {:?}", response.error);
|
||||
println!("Metadata: {:?}", response.metadata);
|
||||
println!("Response: {:?}", response.response);
|
||||
|
||||
match &response.world {
|
||||
None => println!("World: None (not yet complete)"),
|
||||
Some(world) => {
|
||||
println!("World:");
|
||||
println!(" World ID: {}", world.world_id.as_str());
|
||||
println!(" Display name: {:?}", world.display_name);
|
||||
println!(" Marble URL: {:?}", world.world_marble_url);
|
||||
println!(" Created at: {:?}", world.created_at);
|
||||
println!(" Updated at: {:?}", world.updated_at);
|
||||
println!(" Model: {:?}", world.model);
|
||||
println!(" Status: {:?}", world.status);
|
||||
println!(" Tags: {:?}", world.tags);
|
||||
match &world.assets {
|
||||
None => println!(" Assets: None"),
|
||||
Some(assets) => {
|
||||
println!(" Assets:");
|
||||
println!(" Caption: {:?}", assets.caption);
|
||||
println!(" Thumbnail URL: {:?}", assets.thumbnail_url);
|
||||
println!(" Pano URL: {:?}", assets.pano_url);
|
||||
println!(" Collider mesh URL: {:?}", assets.collider_mesh_url);
|
||||
match &assets.splats {
|
||||
None => println!(" Splats: None"),
|
||||
Some(splats) => {
|
||||
println!(" Splats:");
|
||||
println!(" SPZ 100k: {:?}", splats.spz_url_100k);
|
||||
println!(" SPZ 500k: {:?}", splats.spz_url_500k);
|
||||
println!(" SPZ full res: {:?}", splats.spz_url_full_res);
|
||||
println!(" Metric scale factor: {:?}", splats.metric_scale_factor);
|
||||
println!(" Ground plane offset: {:?}", splats.ground_plane_offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(1, 2);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ pub(crate) struct RawResponse {
|
||||
pub expires_at: Option<String>,
|
||||
pub error: Option<RawOperationError>,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
pub response: Option<serde_json::Value>,
|
||||
pub response: Option<RawWorld>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
@@ -17,3 +17,66 @@ pub(crate) struct RawOperationError {
|
||||
pub code: Option<i32>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
/// The World object embedded in `response` when the operation completes.
|
||||
/// All fields are optional to tolerate partial/unexpected shapes.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct RawWorld {
|
||||
pub world_id: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
pub world_marble_url: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub assets: Option<RawWorldAssets>,
|
||||
pub world_prompt: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct RawWorldAssets {
|
||||
pub caption: Option<String>,
|
||||
pub thumbnail_url: Option<String>,
|
||||
pub imagery: Option<RawImagery>,
|
||||
pub mesh: Option<RawMesh>,
|
||||
pub splats: Option<RawSplats>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct RawImagery {
|
||||
pub pano_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct RawMesh {
|
||||
pub collider_mesh_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct RawSplats {
|
||||
pub spz_urls: Option<RawSpzUrls>,
|
||||
pub semantics_metadata: Option<RawSemanticsMetadata>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct RawSemanticsMetadata {
|
||||
pub metric_scale_factor: Option<f64>,
|
||||
pub ground_plane_offset: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct RawSpzUrls {
|
||||
#[serde(rename = "100k")]
|
||||
pub low: Option<String>,
|
||||
#[serde(rename = "500k")]
|
||||
pub medium: Option<String>,
|
||||
pub full_res: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::api::api_types::world_id::WorldId;
|
||||
use crate::api::requests::get_world::http_request::RawResponse;
|
||||
use crate::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use crate::error::filter_world_labs_http_error::filter_world_labs_http_error;
|
||||
@@ -11,12 +12,12 @@ const BASE_URL: &str = "https://api.worldlabs.ai/marble/v1/worlds";
|
||||
|
||||
pub struct GetWorldArgs<'a> {
|
||||
pub creds: &'a WorldLabsApiCreds,
|
||||
pub world_id: &'a str,
|
||||
pub world_id: &'a WorldId,
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
pub struct GetWorldResponse {
|
||||
pub world_id: String,
|
||||
pub world_id: WorldId,
|
||||
pub display_name: Option<String>,
|
||||
pub world_marble_url: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
@@ -45,7 +46,7 @@ pub struct WorldAssets {
|
||||
pub async fn get_world(args: GetWorldArgs<'_>) -> Result<GetWorldResponse, WorldLabsError> {
|
||||
let client = Client::new();
|
||||
|
||||
let url = format!("{}/{}", BASE_URL, args.world_id);
|
||||
let url = format!("{}/{}", BASE_URL, args.world_id.as_str());
|
||||
|
||||
debug!("Requesting URL: {}", url);
|
||||
|
||||
@@ -97,7 +98,7 @@ pub async fn get_world(args: GetWorldArgs<'_>) -> Result<GetWorldResponse, World
|
||||
});
|
||||
|
||||
Ok(GetWorldResponse {
|
||||
world_id: raw.world_id,
|
||||
world_id: WorldId(raw.world_id),
|
||||
display_name: raw.display_name,
|
||||
world_marble_url: raw.world_marble_url,
|
||||
created_at: raw.created_at,
|
||||
@@ -125,16 +126,16 @@ mod tests {
|
||||
let creds = get_test_api_key().unwrap();
|
||||
|
||||
// Use a known world_id from a previous generation
|
||||
let world_id = "0048d009-1c7a-4e13-9881-07e9b6ff32e1";
|
||||
//let world_id = "c8a89fb1-f8b4-44c6-ae50-525f7205c65f";
|
||||
let world_id = WorldId::from_str("0048d009-1c7a-4e13-9881-07e9b6ff32e1");
|
||||
//let world_id = WorldId::from_str("c8a89fb1-f8b4-44c6-ae50-525f7205c65f");
|
||||
|
||||
let response = get_world(GetWorldArgs {
|
||||
creds: &creds,
|
||||
world_id,
|
||||
world_id: &world_id,
|
||||
request_timeout: None,
|
||||
}).await.unwrap();
|
||||
|
||||
println!("World ID: {}", response.world_id);
|
||||
println!("World ID: {}", response.world_id.as_str());
|
||||
println!("Display name: {:?}", response.display_name);
|
||||
println!("Marble URL: {:?}", response.world_marble_url);
|
||||
println!("Created at: {:?}", response.created_at);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::api::api_types::world_id::WorldId;
|
||||
use crate::api::requests::list_worlds::http_request::{RawRequest, RawResponse};
|
||||
use crate::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use crate::error::filter_world_labs_http_error::filter_world_labs_http_error;
|
||||
@@ -36,7 +37,7 @@ pub struct ListWorldsResponse {
|
||||
}
|
||||
|
||||
pub struct WorldSummary {
|
||||
pub world_id: String,
|
||||
pub world_id: WorldId,
|
||||
pub display_name: Option<String>,
|
||||
pub world_marble_url: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
@@ -104,7 +105,7 @@ pub async fn list_worlds(args: ListWorldsArgs<'_>) -> Result<ListWorldsResponse,
|
||||
.map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
|
||||
|
||||
let worlds = raw.worlds.into_iter().map(|w| WorldSummary {
|
||||
world_id: w.world_id,
|
||||
world_id: WorldId(w.world_id),
|
||||
display_name: w.display_name,
|
||||
world_marble_url: w.world_marble_url,
|
||||
created_at: w.created_at,
|
||||
@@ -154,7 +155,7 @@ mod tests {
|
||||
|
||||
for (i, world) in response.worlds.iter().enumerate() {
|
||||
println!("--- World {} ---", i + 1);
|
||||
println!(" World ID: {}", world.world_id);
|
||||
println!(" World ID: {}", world.world_id.as_str());
|
||||
println!(" Display name: {:?}", world.display_name);
|
||||
println!(" Marble URL: {:?}", world.world_marble_url);
|
||||
println!(" Created at: {:?}", world.created_at);
|
||||
|
||||
@@ -5,4 +5,5 @@ pub (crate) mod test_utils;
|
||||
pub mod api;
|
||||
pub mod credentials;
|
||||
pub mod error;
|
||||
pub mod pricing;
|
||||
pub mod recipes;
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
use crate::api::api_types::world_labs_model::WorldLabsModel;
|
||||
|
||||
/// The type of input used to generate a world.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(strum::EnumIter, strum::Display))]
|
||||
pub enum InputType {
|
||||
Text,
|
||||
ImageNonPanorama,
|
||||
ImagePanorama,
|
||||
MultiImage,
|
||||
Video,
|
||||
}
|
||||
|
||||
/// Cost breakdown for a World Labs generation request.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct WorldLabsCost {
|
||||
pub worldlabs_credits: u32,
|
||||
pub us_dollar_cents: u32,
|
||||
}
|
||||
|
||||
/// Calculate the cost of a World Labs generation request.
|
||||
pub fn calculate_cost(model: WorldLabsModel, input_type: InputType) -> WorldLabsCost {
|
||||
let worldlabs_credits = credits_for(model, input_type);
|
||||
let us_dollar_cents = credits_to_us_dollar_cents(worldlabs_credits);
|
||||
WorldLabsCost { worldlabs_credits, us_dollar_cents }
|
||||
}
|
||||
|
||||
/// Returns the credit cost for a given model and input type,
|
||||
/// based on the official pricing table at https://docs.worldlabs.ai/api/pricing
|
||||
fn credits_for(model: WorldLabsModel, input_type: InputType) -> u32 {
|
||||
match (model, input_type) {
|
||||
// Marble 0.1-mini
|
||||
(WorldLabsModel::Marble0p1Mini, InputType::ImagePanorama) => 150,
|
||||
(WorldLabsModel::Marble0p1Mini, InputType::Text) => 230,
|
||||
(WorldLabsModel::Marble0p1Mini, InputType::ImageNonPanorama) => 230,
|
||||
(WorldLabsModel::Marble0p1Mini, InputType::MultiImage) => 250,
|
||||
(WorldLabsModel::Marble0p1Mini, InputType::Video) => 250,
|
||||
|
||||
// Marble 0.1-plus
|
||||
(WorldLabsModel::Marble0p1Plus, InputType::ImagePanorama) => 1500,
|
||||
(WorldLabsModel::Marble0p1Plus, InputType::Text) => 1580,
|
||||
(WorldLabsModel::Marble0p1Plus, InputType::ImageNonPanorama) => 1580,
|
||||
(WorldLabsModel::Marble0p1Plus, InputType::MultiImage) => 1600,
|
||||
(WorldLabsModel::Marble0p1Plus, InputType::Video) => 1600,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts credits to US dollar cents.
|
||||
/// Rate: $1.00 USD per 1,250 credits → 100 cents per 1,250 credits.
|
||||
fn credits_to_us_dollar_cents(credits: u32) -> u32 {
|
||||
// (credits * 100) / 1250 = (credits * 4) / 50 = (credits * 2) / 25
|
||||
// Use u64 to avoid overflow on large values.
|
||||
((credits as u64 * 100) / 1250) as u32
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
#[test]
|
||||
fn test_pricing_table() {
|
||||
let models = [
|
||||
WorldLabsModel::Marble0p1Mini,
|
||||
WorldLabsModel::Marble0p1Plus,
|
||||
];
|
||||
|
||||
println!("\n{:<20} {:<20} {:>10} {:>12}", "Model", "Input Type", "Credits", "USD Cents");
|
||||
println!("{}", "-".repeat(65));
|
||||
|
||||
for model in &models {
|
||||
for input_type in InputType::iter() {
|
||||
let cost = calculate_cost(*model, input_type);
|
||||
println!(
|
||||
"{:<20} {:<20} {:>10} {:>12}",
|
||||
model.get_model_api_name_str(),
|
||||
input_type,
|
||||
cost.worldlabs_credits,
|
||||
cost.us_dollar_cents,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mini_credits() {
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Mini, InputType::Text).worldlabs_credits, 230);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Mini, InputType::ImageNonPanorama).worldlabs_credits, 230);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Mini, InputType::ImagePanorama).worldlabs_credits, 150);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Mini, InputType::MultiImage).worldlabs_credits, 250);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Mini, InputType::Video).worldlabs_credits, 250);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plus_credits() {
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Plus, InputType::Text).worldlabs_credits, 1580);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Plus, InputType::ImageNonPanorama).worldlabs_credits, 1580);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Plus, InputType::ImagePanorama).worldlabs_credits, 1500);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Plus, InputType::MultiImage).worldlabs_credits, 1600);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Plus, InputType::Video).worldlabs_credits, 1600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dollar_conversion() {
|
||||
// 1250 credits = $1.00 = 100 cents
|
||||
assert_eq!(credits_to_us_dollar_cents(1250), 100);
|
||||
// 150 credits = $0.12 = 12 cents
|
||||
assert_eq!(credits_to_us_dollar_cents(150), 12);
|
||||
// 230 credits = $0.184 = 18 cents (truncated)
|
||||
assert_eq!(credits_to_us_dollar_cents(230), 18);
|
||||
// 1580 credits = $1.264 = 126 cents (truncated)
|
||||
assert_eq!(credits_to_us_dollar_cents(1580), 126);
|
||||
// 1600 credits = $1.28 = 128 cents
|
||||
assert_eq!(credits_to_us_dollar_cents(1600), 128);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_combinations_have_costs() {
|
||||
let models = [
|
||||
WorldLabsModel::Marble0p1Mini,
|
||||
WorldLabsModel::Marble0p1Plus,
|
||||
];
|
||||
|
||||
for model in &models {
|
||||
for input_type in InputType::iter() {
|
||||
let cost = calculate_cost(*model, input_type);
|
||||
assert!(cost.worldlabs_credits > 0, "credits should be > 0 for {:?} {:?}", model, input_type);
|
||||
assert!(cost.us_dollar_cents > 0, "dollar cents should be > 0 for {:?} {:?}", model, input_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod check_pricing;
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
use crate::api::api_types::media_asset_id::MediaAssetId;
|
||||
use crate::api::api_types::operation_id::OperationId;
|
||||
use crate::api::api_types::world_labs_model::WorldLabsModel;
|
||||
use crate::api::requests::generate_world::generate_world::{generate_world, GenerateWorldArgs};
|
||||
use crate::api::requests::generate_world::http_request::{ContentReference, WorldPrompt};
|
||||
use crate::api::requests::prepare_upload::prepare_upload::{prepare_upload, PrepareUploadArgs, MediaAssetKind};
|
||||
@@ -16,7 +17,7 @@ pub struct UploadImageAndCreateWorldArgs<'a> {
|
||||
pub creds: &'a WorldLabsApiCreds,
|
||||
pub file: FileBytesOrPath,
|
||||
pub text_prompt: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub model: WorldLabsModel,
|
||||
pub individual_request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
@@ -145,7 +146,7 @@ mod tests {
|
||||
individual_request_timeout: None,
|
||||
file: FileBytesOrPath::Bytes(file_bytes),
|
||||
text_prompt: None,
|
||||
model: None,
|
||||
model: WorldLabsModel::Marble0p1Plus,
|
||||
}).await.unwrap();
|
||||
|
||||
println!("Operation ID: {}", results.operation_id.as_str());
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "support-tool"
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
|
||||
[[bin]]
|
||||
name = "support-tool"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
easyenv.workspace = true
|
||||
|
||||
# Internal
|
||||
errors.workspace = true
|
||||
seedance2pro.workspace = true
|
||||
|
||||
# External
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
log.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
@@ -0,0 +1,27 @@
|
||||
pub mod seedance2pro;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "support-tool", about = "Production support CLI")]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: TopLevelCommand,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum TopLevelCommand {
|
||||
/// Seedance2 Pro support commands
|
||||
Seedance2pro {
|
||||
#[command(subcommand)]
|
||||
command: seedance2pro::Seedance2proCommand,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn run(cli: Cli) -> anyhow::Result<()> {
|
||||
match cli.command {
|
||||
TopLevelCommand::Seedance2pro { command } => {
|
||||
seedance2pro::run(command).await
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use anyhow::anyhow;
|
||||
use clap::Subcommand;
|
||||
|
||||
use super::state::Seedance2ProState;
|
||||
use super::subcommands;
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum Seedance2proCommand {
|
||||
/// Find a job by its order ID across all pages
|
||||
Findjob(subcommands::findjob::FindjobArgs),
|
||||
}
|
||||
|
||||
pub async fn run(command: Seedance2proCommand) -> anyhow::Result<()> {
|
||||
let cookies = easyenv::get_env_string_required("SEEDANCE2PRO_COOKIES")
|
||||
.map_err(|err| anyhow!("Missing SEEDANCE2PRO_COOKIES env var: {:?}", err))?;
|
||||
|
||||
let state = Seedance2ProState { cookies };
|
||||
|
||||
match command {
|
||||
Seedance2proCommand::Findjob(args) => subcommands::findjob::run(&state, args).await,
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user