From 084e1f68b8043436ef3d86ffd759dfdd760b96c8 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 16 May 2026 11:52:25 -0400 Subject: [PATCH] seedance global (#1527) --- ...754c3aee92dbf217288c9a7eb925004e3d259.json | 12 + Cargo.lock | 2 + .../artcraft/artcraft_router/Cargo.toml | 1 + .../src/api/common_video_model.rs | 6 + .../artcraft_router/src/api/provider.rs | 1 + .../artcraft_router/src/client/mod.rs | 1 + .../src/client/multi_router_client.rs | 8 + .../src/client/multi_router_client_builder.rs | 11 +- .../src/client/router_client.rs | 10 + .../src/client/router_gmicloud_client.rs | 11 + .../src/errors/client_error.rs | 2 + .../src/errors/provider_error.rs | 9 + .../generate_video_request_builder.rs | 8 +- .../generate_video/generate_video_response.rs | 13 + .../providers/gmicloud/mod.rs | 2 + .../gmicloud/seedance_2p0_fast_g/build.rs | 323 ++++++++++++++++ .../gmicloud/seedance_2p0_fast_g/cost.rs | 119 ++++++ .../gmicloud/seedance_2p0_fast_g/mod.rs | 3 + .../gmicloud/seedance_2p0_fast_g/request.rs | 100 +++++ .../gmicloud/seedance_2p0_g/build.rs | 355 ++++++++++++++++++ .../providers/gmicloud/seedance_2p0_g/cost.rs | 132 +++++++ .../providers/gmicloud/seedance_2p0_g/mod.rs | 3 + .../gmicloud/seedance_2p0_g/request.rs | 98 +++++ .../generate_video_v2/providers/mod.rs | 1 + .../video_generation_request.rs | 18 + .../src/creds/gmicloud_api_key.rs | 1 + .../api/video/seedance_2_0_260128/api.rs | 4 +- .../api/video/seedance_2_0_fast_260128/api.rs | 4 +- ...sert_generic_inference_job_for_gmicloud.rs | 119 ++++++ .../queries/generic_inference/gmicloud/mod.rs | 1 + .../src/queries/generic_inference/mod.rs | 1 + .../inference_job_external_third_party.rs | 12 +- .../inference_job_product_category.rs | 13 +- .../inference_job_type.rs | 12 +- .../common/generation/common_model_type.rs | 22 +- .../common/generation/common_video_model.rs | 16 + crates/service/web/storyteller_web/CLAUDE.md | 59 +++ crates/service/web/storyteller_web/Cargo.toml | 1 + .../src/configs/omni_gen/video_models.rs | 79 ++++ .../video/estimate_video_cost_handler.rs | 2 + .../video/helpers/build_router_client.rs | 6 + .../insert_db_job/insert_gmicloud_job.rs | 45 +++ .../generate/video/insert_db_job/mod.rs | 1 + .../video/omni_gen_video_generate_handler.rs | 24 ++ .../video/pipeline_v2/run_pipeline_v2.rs | 44 ++- .../service/web/storyteller_web/src/main.rs | 7 +- .../storyteller_web/src/state/server_state.rs | 8 + 47 files changed, 1704 insertions(+), 26 deletions(-) create mode 100644 .sqlx/query-99dae5c5f3b3a8e9ce105bff9a9754c3aee92dbf217288c9a7eb925004e3d259.json create mode 100644 crates/api_clients/artcraft/artcraft_router/src/client/router_gmicloud_client.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_fast_g/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_fast_g/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_fast_g/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_fast_g/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_g/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_g/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_g/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_g/request.rs create mode 100644 crates/schema/database/mysql_queries/src/queries/generic_inference/gmicloud/insert_generic_inference_job_for_gmicloud.rs create mode 100644 crates/schema/database/mysql_queries/src/queries/generic_inference/gmicloud/mod.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/insert_db_job/insert_gmicloud_job.rs diff --git a/.sqlx/query-99dae5c5f3b3a8e9ce105bff9a9754c3aee92dbf217288c9a7eb925004e3d259.json b/.sqlx/query-99dae5c5f3b3a8e9ce105bff9a9754c3aee92dbf217288c9a7eb925004e3d259.json new file mode 100644 index 0000000000..ad6c1fa7b9 --- /dev/null +++ b/.sqlx/query-99dae5c5f3b3a8e9ce105bff9a9754c3aee92dbf217288c9a7eb925004e3d259.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n\n maybe_prompt_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = NULL,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n maybe_debug_log_event_token = ?,\n\n frontend_failure_category = NULL,\n failure_reason = NULL,\n\n status = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 14 + }, + "nullable": [] + }, + "hash": "99dae5c5f3b3a8e9ce105bff9a9754c3aee92dbf217288c9a7eb925004e3d259" +} diff --git a/Cargo.lock b/Cargo.lock index 8374526d7d..5f3b30bbee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -773,6 +773,7 @@ dependencies = [ "artcraft_client", "enums", "fal_client", + "gmicloud_client", "log", "muapi_client", "reqwest 0.12.23", @@ -11295,6 +11296,7 @@ dependencies = [ "futures 0.1.31", "futures 0.3.32", "futures-util", + "gmicloud_client", "google_sign_in", "hashing", "hostname 0.4.0", diff --git a/crates/api_clients/artcraft/artcraft_router/Cargo.toml b/crates/api_clients/artcraft/artcraft_router/Cargo.toml index 11a86e0d0f..d30f6ee0ff 100644 --- a/crates/api_clients/artcraft/artcraft_router/Cargo.toml +++ b/crates/api_clients/artcraft/artcraft_router/Cargo.toml @@ -20,6 +20,7 @@ artcraft_api_defs.workspace = true enums.workspace = true artcraft_client.workspace = true fal_client.workspace = true +gmicloud_client.workspace = true muapi_client.workspace = true seedance2pro_client.workspace = true tokens.workspace = true diff --git a/crates/api_clients/artcraft/artcraft_router/src/api/common_video_model.rs b/crates/api_clients/artcraft/artcraft_router/src/api/common_video_model.rs index aad0a8c1c5..8c2b404514 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/api/common_video_model.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/api/common_video_model.rs @@ -41,6 +41,12 @@ pub enum CommonVideoModel { #[serde(rename = "seedance_2p0_fast")] Seedance2p0Fast, + #[serde(rename = "seedance_2p0_g")] + Seedance2p0Global, + + #[serde(rename = "seedance_2p0_fast_g")] + Seedance2p0FastGlobal, + #[serde(rename = "happy_horse_1p0")] HappyHorse1p0, diff --git a/crates/api_clients/artcraft/artcraft_router/src/api/provider.rs b/crates/api_clients/artcraft/artcraft_router/src/api/provider.rs index e1389c56a4..15d0d970f3 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/api/provider.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/api/provider.rs @@ -6,6 +6,7 @@ use serde_derive::{Deserialize, Serialize}; pub enum Provider { Artcraft, Fal, + GmiCloud, Muapi, Seedance2Pro, } diff --git a/crates/api_clients/artcraft/artcraft_router/src/client/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/client/mod.rs index 4fac64c689..1d0c3c6b85 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/client/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/client/mod.rs @@ -6,5 +6,6 @@ pub mod router_artcraft_client; pub mod router_client; pub mod router_fal_client; pub mod router_fal_webhook_optional_client; +pub mod router_gmicloud_client; pub mod router_muapi_client; pub mod router_seedance2pro_client; diff --git a/crates/api_clients/artcraft/artcraft_router/src/client/multi_router_client.rs b/crates/api_clients/artcraft/artcraft_router/src/client/multi_router_client.rs index c1db20f3fc..ced2d9ff57 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/client/multi_router_client.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/client/multi_router_client.rs @@ -1,6 +1,7 @@ use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::client::router_fal_client::RouterFalClient; use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient; +use crate::client::router_gmicloud_client::RouterGmiCloudClient; use crate::client::router_muapi_client::RouterMuapiClient; use crate::client::router_seedance2pro_client::RouterSeedance2ProClient; use crate::errors::client_error::{ClientError, ClientType}; @@ -9,6 +10,7 @@ pub struct MultiRouterClient { pub(crate) artcraft_client: Option, pub(crate) fal_client: Option, pub(crate) fal_webhook_optional_client: Option, + pub(crate) gmicloud_client: Option, pub(crate) muapi_client: Option, pub(crate) seedance2pro_client: Option, } @@ -23,11 +25,17 @@ impl MultiRouterClient { self.fal_client.as_ref() .ok_or(ClientError::ClientNotConfigured(ClientType::Fal)) } + pub fn get_fal_webhook_optional_client_ref(&self) -> Result<&RouterFalWebhookOptionalClient, ClientError> { self.fal_webhook_optional_client.as_ref() .ok_or(ClientError::ClientNotConfigured(ClientType::FalWebhookOptional)) } + pub fn get_gmicloud_client_ref(&self) -> Result<&RouterGmiCloudClient, ClientError> { + self.gmicloud_client.as_ref() + .ok_or(ClientError::ClientNotConfigured(ClientType::GmiCloud)) + } + pub fn get_muapi_client_ref(&self) -> Result<&RouterMuapiClient, ClientError> { self.muapi_client.as_ref() .ok_or(ClientError::ClientNotConfigured(ClientType::Muapi)) diff --git a/crates/api_clients/artcraft/artcraft_router/src/client/multi_router_client_builder.rs b/crates/api_clients/artcraft/artcraft_router/src/client/multi_router_client_builder.rs index 9d0c516429..e410b9530e 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/client/multi_router_client_builder.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/client/multi_router_client_builder.rs @@ -2,6 +2,7 @@ use crate::client::multi_router_client::MultiRouterClient; use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::client::router_fal_client::RouterFalClient; use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient; +use crate::client::router_gmicloud_client::RouterGmiCloudClient; use crate::client::router_muapi_client::RouterMuapiClient; use crate::client::router_seedance2pro_client::RouterSeedance2ProClient; @@ -9,6 +10,7 @@ pub struct MultiRouterClientBuilder { artcraft_client: Option, fal_client: Option, fal_webhook_optional_client: Option, + gmicloud_client: Option, muapi_client: Option, seedance2pro_client: Option, } @@ -19,6 +21,7 @@ impl MultiRouterClientBuilder { artcraft_client: None, fal_client: None, fal_webhook_optional_client: None, + gmicloud_client: None, muapi_client: None, seedance2pro_client: None, } @@ -33,12 +36,17 @@ impl MultiRouterClientBuilder { self.fal_client = Some(client); self } - + pub fn set_fal_webhook_optional_client(mut self, client: RouterFalWebhookOptionalClient) -> Self { self.fal_webhook_optional_client = Some(client); self } + pub fn set_gmicloud_client(mut self, client: RouterGmiCloudClient) -> Self { + self.gmicloud_client = Some(client); + self + } + pub fn set_muapi_client(mut self, client: RouterMuapiClient) -> Self { self.muapi_client = Some(client); self @@ -54,6 +62,7 @@ impl MultiRouterClientBuilder { artcraft_client: self.artcraft_client, fal_client: self.fal_client, fal_webhook_optional_client: self.fal_webhook_optional_client, + gmicloud_client: self.gmicloud_client, muapi_client: self.muapi_client, seedance2pro_client: self.seedance2pro_client, } diff --git a/crates/api_clients/artcraft/artcraft_router/src/client/router_client.rs b/crates/api_clients/artcraft/artcraft_router/src/client/router_client.rs index fb3cc04751..53207960d2 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/client/router_client.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/client/router_client.rs @@ -2,6 +2,7 @@ use crate::client::multi_router_client::MultiRouterClient; use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::client::router_fal_client::RouterFalClient; use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient; +use crate::client::router_gmicloud_client::RouterGmiCloudClient; use crate::client::router_muapi_client::RouterMuapiClient; use crate::client::router_seedance2pro_client::RouterSeedance2ProClient; use crate::errors::client_error::{ClientError, ClientType}; @@ -11,6 +12,7 @@ pub enum RouterClient { Artcraft(RouterArtcraftClient), Fal(RouterFalClient), FalWebhookOptional(RouterFalWebhookOptionalClient), + GmiCloud(RouterGmiCloudClient), Muapi(RouterMuapiClient), Seedance2Pro(RouterSeedance2ProClient), } @@ -40,6 +42,14 @@ impl RouterClient { } } + pub fn get_gmicloud_client_ref(&self) -> Result<&RouterGmiCloudClient, ClientError> { + match self { + RouterClient::GmiCloud(client) => Ok(client), + RouterClient::Multi(multi) => multi.get_gmicloud_client_ref(), + _ => Err(ClientError::ClientNotConfigured(ClientType::GmiCloud)), + } + } + pub fn get_muapi_client_ref(&self) -> Result<&RouterMuapiClient, ClientError> { match self { RouterClient::Muapi(client) => Ok(client), diff --git a/crates/api_clients/artcraft/artcraft_router/src/client/router_gmicloud_client.rs b/crates/api_clients/artcraft/artcraft_router/src/client/router_gmicloud_client.rs new file mode 100644 index 0000000000..8660ffd2c1 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/client/router_gmicloud_client.rs @@ -0,0 +1,11 @@ +use gmicloud_client::creds::gmicloud_api_key::GmiCloudApiKey; + +pub struct RouterGmiCloudClient { + pub(crate) api_key: GmiCloudApiKey, +} + +impl RouterGmiCloudClient { + pub fn new(api_key: GmiCloudApiKey) -> Self { + RouterGmiCloudClient { api_key } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/errors/client_error.rs b/crates/api_clients/artcraft/artcraft_router/src/errors/client_error.rs index e8323acefc..88da3cd101 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/errors/client_error.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/errors/client_error.rs @@ -8,6 +8,7 @@ pub enum ClientType { Artcraft, Fal, FalWebhookOptional, + GmiCloud, Muapi, Seedance2Pro, } @@ -18,6 +19,7 @@ impl Display for ClientType { Self::Artcraft => write!(f, "Artcraft"), Self::Fal => write!(f, "Fal"), Self::FalWebhookOptional => write!(f, "FalWebhookOptional"), + Self::GmiCloud => write!(f, "GmiCloud"), Self::Muapi => write!(f, "Muapi"), Self::Seedance2Pro => write!(f, "Seedance2Pro"), } diff --git a/crates/api_clients/artcraft/artcraft_router/src/errors/provider_error.rs b/crates/api_clients/artcraft/artcraft_router/src/errors/provider_error.rs index 774d893926..96e9d05b7f 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/errors/provider_error.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/errors/provider_error.rs @@ -2,6 +2,7 @@ use std::error::Error; use std::fmt::{Display, Formatter}; use artcraft_client::error::storyteller_error::StorytellerError; use fal_client::error::fal_error_plus::FalErrorPlus; +use gmicloud_client::error::gmicloud_error::GmiCloudError; use muapi_client::error::muapi_error::MuapiError; use seedance2pro_client::error::seedance2pro_error::Seedance2ProError; @@ -9,6 +10,7 @@ use seedance2pro_client::error::seedance2pro_error::Seedance2ProError; pub enum ProviderError { Storyteller(StorytellerError), Fal(FalErrorPlus), + GmiCloud(GmiCloudError), Muapi(MuapiError), Seedance2Pro(Seedance2ProError), } @@ -20,6 +22,7 @@ impl Display for ProviderError { match self { Self::Storyteller(e) => write!(f, "Storyteller provider error: {}", e), Self::Fal(e) => write!(f, "Fal provider error: {}", e), + Self::GmiCloud(e) => write!(f, "GmiCloud provider error: {}", e), Self::Muapi(e) => write!(f, "Muapi provider error: {}", e), Self::Seedance2Pro(e) => write!(f, "Seedance2Pro provider error: {}", e), } @@ -38,6 +41,12 @@ impl From for ProviderError { } } +impl From for ProviderError { + fn from(error: GmiCloudError) -> Self { + Self::GmiCloud(error) + } +} + impl From for ProviderError { fn from(error: MuapiError) -> Self { Self::Muapi(error) diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request_builder.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request_builder.rs index 0c033f8713..d445cbdd71 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request_builder.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request_builder.rs @@ -52,6 +52,8 @@ use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0::build use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast::build::build_artcraft_seedance_2p0_fast; use crate::generate::generate_video_v2::providers::kinovi::happy_horse_1p0::build::build_kinovi_happy_horse_1p0; use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0::build::build_kinovi_seedance_2p0; +use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::build::build_gmicloud_seedance_2p0_g; +use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::build::build_gmicloud_seedance_2p0_fast_g; use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::build::build_kinovi_seedance_2p0_fast; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; @@ -147,6 +149,8 @@ impl GenerateVideoRequestBuilder { (Provider::Artcraft, CommonVideoModel::HappyHorse1p0) => true, (Provider::Artcraft, CommonVideoModel::Seedance2p0) => true, (Provider::Artcraft, CommonVideoModel::Seedance2p0Fast) => true, + (Provider::GmiCloud, CommonVideoModel::Seedance2p0Global) => true, + (Provider::GmiCloud, CommonVideoModel::Seedance2p0FastGlobal) => true, (Provider::Seedance2Pro, CommonVideoModel::HappyHorse1p0) => true, (Provider::Seedance2Pro, CommonVideoModel::Seedance2p0) => true, (Provider::Seedance2Pro, CommonVideoModel::Seedance2p0Fast) => true, @@ -160,6 +164,8 @@ impl GenerateVideoRequestBuilder { (Provider::Artcraft, CommonVideoModel::HappyHorse1p0) => build_artcraft_happy_horse_1p0(self), (Provider::Artcraft, CommonVideoModel::Seedance2p0) => build_artcraft_seedance_2p0(self), (Provider::Artcraft, CommonVideoModel::Seedance2p0Fast) => build_artcraft_seedance_2p0_fast(self), + (Provider::GmiCloud, CommonVideoModel::Seedance2p0Global) => build_gmicloud_seedance_2p0_g(self), + (Provider::GmiCloud, CommonVideoModel::Seedance2p0FastGlobal) => build_gmicloud_seedance_2p0_fast_g(self), (Provider::Seedance2Pro, CommonVideoModel::HappyHorse1p0) => build_kinovi_happy_horse_1p0(self), (Provider::Seedance2Pro, CommonVideoModel::Seedance2p0) => build_kinovi_seedance_2p0(self), (Provider::Seedance2Pro, CommonVideoModel::Seedance2p0Fast) => build_kinovi_seedance_2p0_fast(self), @@ -172,9 +178,9 @@ impl GenerateVideoRequestBuilder { match self.provider { Provider::Artcraft => self.build_artcraft(), Provider::Fal => self.build_fal(), + Provider::GmiCloud => self.unsupported_provider(), // GmiCloud uses build2() only Provider::Muapi => self.build_muapi(), Provider::Seedance2Pro => self.build_seedance2pro(), - _ => self.unsupported_provider(), } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_response.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_response.rs index f8c9839841..80b82fbda9 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_response.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_response.rs @@ -34,9 +34,15 @@ pub struct FalVideoResponsePayload { pub maybe_outbound_request: Option>, } +#[derive(Clone, Debug)] +pub struct GmiCloudVideoResponsePayload { + pub request_id: String, +} + #[derive(Clone, Debug)] pub enum GenerateVideoResponse { Artcraft(ArtcraftVideoResponsePayload), + GmiCloud(GmiCloudVideoResponsePayload), Muapi(MuapiVideoResponsePayload), Seedance2Pro(Seedance2proVideoResponsePayload), Fal(FalVideoResponsePayload), @@ -64,6 +70,13 @@ impl GenerateVideoResponse { } } + pub fn get_gmicloud_payload(&self) -> Option { + match self { + Self::GmiCloud(p) => Some(p.clone()), + _ => None, + } + } + pub fn get_fal_payload(&self) -> Option { match self { Self::Fal(p) => Some(p.clone()), diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/mod.rs new file mode 100644 index 0000000000..f76c64a320 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/mod.rs @@ -0,0 +1,2 @@ +pub mod seedance_2p0_g; +pub mod seedance_2p0_fast_g; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_fast_g/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_fast_g/build.rs new file mode 100644 index 0000000000..a8136fa987 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_fast_g/build.rs @@ -0,0 +1,323 @@ +use gmicloud_client::requests::api::video::seedance_2_0_fast_260128::api::{ + Seedance20FastRatio, Seedance20FastRequest, Seedance20FastResolution, +}; + +use crate::api::audio_list_ref::AudioListRef; +use crate::api::common_aspect_ratio::CommonAspectRatio; +use crate::api::common_resolution::CommonResolution; +use crate::api::image_list_ref::ImageListRef; +use crate::api::image_ref::ImageRef; +use crate::api::video_list_ref::VideoListRef; +use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::client_error::ClientError; +use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; +use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::request::GmiCloudSeedance2p0FastGRequestState; +use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; +use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; + +pub fn build_gmicloud_seedance_2p0_fast_g( + mut builder: GenerateVideoRequestBuilder, +) -> Result { + let strategy = builder.request_mismatch_mitigation_strategy; + + let ratio = plan_ratio(builder.aspect_ratio.take(), strategy)?; + let resolution = plan_resolution(builder.resolution.take(), strategy)?; + let duration = builder.duration_seconds.take().map(|d| (d as u8).clamp(4, 15)); + let prompt = builder.prompt.take().unwrap_or_default(); + + let first_frame = resolve_url(builder.start_frame.take())?; + let last_frame = resolve_url(builder.end_frame.take())?; + let reference_images = resolve_url_list_from_images(builder.reference_images.take())?; + let reference_videos = resolve_url_list_from_videos(builder.reference_videos.take())?; + let reference_audios = resolve_url_list_from_audios(builder.reference_audio.take())?; + + let request = Seedance20FastRequest { + prompt, + duration, + resolution, + ratio, + seed: None, + watermark: Some(false), + generate_audio: Some(true), + web_search: None, + first_frame, + last_frame, + reference_images, + reference_videos, + reference_audios, + reference_asset_ids: None, + }; + + let state = GmiCloudSeedance2p0FastGRequestState { request }; + Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::GmiCloudSeedance2p0FastG(state))) +} + +fn plan_ratio( + aspect_ratio: Option, + _strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + match aspect_ratio { + None | Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) + | Some(CommonAspectRatio::Auto3k) | Some(CommonAspectRatio::Auto4k) => Ok(None), + Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(Seedance20FastRatio::Landscape16x9)), + Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(Seedance20FastRatio::Portrait9x16)), + Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(Seedance20FastRatio::Square)), + Some(CommonAspectRatio::WideFourByThree) => Ok(Some(Seedance20FastRatio::Standard4x3)), + Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(Seedance20FastRatio::Portrait3x4)), + Some(CommonAspectRatio::WideTwentyOneByNine) | Some(CommonAspectRatio::TallNineByTwentyOne) => { + Ok(Some(Seedance20FastRatio::UltraWide21x9)) + } + Some(CommonAspectRatio::WideThreeByTwo) | Some(CommonAspectRatio::WideFiveByFour) + | Some(CommonAspectRatio::TallFourByFive) | Some(CommonAspectRatio::TallTwoByThree) => { + Ok(Some(Seedance20FastRatio::Adaptive)) + } + } +} + +fn plan_resolution( + resolution: Option, + _strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + match resolution { + None => Ok(None), + Some(CommonResolution::FourEightyP) => Ok(Some(Seedance20FastResolution::FourEightyP)), + Some(CommonResolution::SevenTwentyP) => Ok(Some(Seedance20FastResolution::SevenTwentyP)), + // Fast model doesn't support 1080p — fall back to 720p + Some(CommonResolution::TenEightyP) => Ok(Some(Seedance20FastResolution::SevenTwentyP)), + Some(CommonResolution::HalfK) | Some(CommonResolution::OneK) => Ok(Some(Seedance20FastResolution::FourEightyP)), + Some(CommonResolution::TwoK) | Some(CommonResolution::ThreeK) | Some(CommonResolution::FourK) => { + Ok(Some(Seedance20FastResolution::SevenTwentyP)) + } + } +} + +fn resolve_url(image_ref: Option) -> Result, ArtcraftRouterError> { + match image_ref { + None => Ok(None), + Some(ImageRef::Url(url)) => Ok(Some(url)), + Some(ImageRef::MediaFileToken(_)) => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "start_frame/end_frame", + value: "GmiCloud only supports image URLs, not media file tokens".to_string(), + })) + } + } +} + +fn resolve_url_list_from_images( + list_ref: Option, +) -> Result>, ArtcraftRouterError> { + match list_ref { + None => Ok(None), + Some(ImageListRef::Urls(urls)) => Ok(Some(urls)), + Some(ImageListRef::MediaFileTokens(_)) => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "reference_images", + value: "GmiCloud only supports image URLs, not media file tokens".to_string(), + })) + } + } +} + +fn resolve_url_list_from_videos( + list_ref: Option, +) -> Result>, ArtcraftRouterError> { + match list_ref { + None => Ok(None), + Some(VideoListRef::Urls(urls)) => Ok(Some(urls)), + Some(VideoListRef::MediaFileTokens(_)) => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "reference_videos", + value: "GmiCloud only supports video URLs, not media file tokens".to_string(), + })) + } + } +} + +#[cfg(test)] +mod tests { + use gmicloud_client::requests::api::video::seedance_2_0_fast_260128::api::{ + Seedance20FastRatio, Seedance20FastResolution, + }; + use tokens::tokens::media_files::MediaFileToken; + + use crate::api::common_aspect_ratio::CommonAspectRatio; + use crate::api::common_resolution::CommonResolution; + use crate::api::common_video_model::CommonVideoModel; + use crate::api::image_list_ref::ImageListRef; + use crate::api::image_ref::ImageRef; + use crate::api::provider::Provider; + use crate::api::video_list_ref::VideoListRef; + use crate::api::audio_list_ref::AudioListRef; + use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; + use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; + + use super::*; + + mod field_conversions { + use super::*; + + #[test] + fn prompt_passed_through() { + let req = unwrap_request(make_builder(|b| { b.prompt = Some("fast test".to_string()); })); + assert_eq!(req.request.prompt, "fast test"); + } + + #[test] + fn duration_passed_through() { + let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(8); })); + assert_eq!(req.request.duration, Some(8)); + } + + #[test] + fn duration_clamped_to_min() { + let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(2); })); + assert_eq!(req.request.duration, Some(4)); + } + + #[test] + fn duration_clamped_to_max() { + let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(30); })); + assert_eq!(req.request.duration, Some(15)); + } + } + + mod resolution_tests { + use super::*; + + #[test] + fn res_480p() { + let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::FourEightyP); })); + assert_eq!(req.request.resolution, Some(Seedance20FastResolution::FourEightyP)); + } + + #[test] + fn res_720p() { + let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::SevenTwentyP); })); + assert_eq!(req.request.resolution, Some(Seedance20FastResolution::SevenTwentyP)); + } + + #[test] + fn res_1080p_falls_back_to_720p() { + let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::TenEightyP); })); + assert_eq!(req.request.resolution, Some(Seedance20FastResolution::SevenTwentyP)); + } + + #[test] + fn none_stays_none() { + let req = unwrap_request(make_builder(|_| {})); + assert!(req.request.resolution.is_none()); + } + } + + mod aspect_ratio_tests { + use super::*; + + #[test] + fn landscape_16x9() { + let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(CommonAspectRatio::WideSixteenByNine); })); + assert_eq!(req.request.ratio, Some(Seedance20FastRatio::Landscape16x9)); + } + + #[test] + fn square() { + let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(CommonAspectRatio::Square); })); + assert_eq!(req.request.ratio, Some(Seedance20FastRatio::Square)); + } + + #[test] + fn auto_maps_to_none() { + let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(CommonAspectRatio::Auto); })); + assert_eq!(req.request.ratio, None); + } + } + + mod media_url_tests { + use super::*; + + #[test] + fn start_frame_url_passed_through() { + let req = unwrap_request(make_builder(|b| { + b.start_frame = Some(ImageRef::Url("https://example.com/img.png".to_string())); + })); + assert_eq!(req.request.first_frame, Some("https://example.com/img.png".to_string())); + } + + #[test] + fn media_file_token_rejected() { + let result = build_gmicloud_seedance_2p0_fast_g(GenerateVideoRequestBuilder { + start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_x".to_string()))), + ..base_builder() + }); + assert!(result.is_err()); + } + + #[test] + fn reference_image_urls_passed_through() { + let urls = vec!["https://example.com/ref.png".to_string()]; + let req = unwrap_request(make_builder(|b| { + b.reference_images = Some(ImageListRef::Urls(urls.clone())); + })); + assert_eq!(req.request.reference_images, Some(urls)); + } + + #[test] + fn reference_video_urls_passed_through() { + let urls = vec!["https://example.com/ref.mp4".to_string()]; + let req = unwrap_request(make_builder(|b| { + b.reference_videos = Some(VideoListRef::Urls(urls.clone())); + })); + assert_eq!(req.request.reference_videos, Some(urls)); + } + + #[test] + fn reference_audio_urls_passed_through() { + let urls = vec!["https://example.com/ref.wav".to_string()]; + let req = unwrap_request(make_builder(|b| { + b.reference_audio = Some(AudioListRef::Urls(urls.clone())); + })); + assert_eq!(req.request.reference_audios, Some(urls)); + } + } + + fn base_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Seedance2p0FastGlobal, + provider: Provider::GmiCloud, + duration_seconds: Some(5), + video_batch_count: Some(1), + ..Default::default() + } + } + + fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder { + let mut builder = base_builder(); + f(&mut builder); + builder + } + + fn unwrap_request(builder: GenerateVideoRequestBuilder) -> GmiCloudSeedance2p0FastGRequestState { + let result = build_gmicloud_seedance_2p0_fast_g(builder).expect("build should succeed"); + match result { + VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::GmiCloudSeedance2p0FastG(state)) => state, + _ => panic!("expected GmiCloudSeedance2p0FastG request"), + } + } +} + +fn resolve_url_list_from_audios( + list_ref: Option, +) -> Result>, ArtcraftRouterError> { + match list_ref { + None => Ok(None), + Some(AudioListRef::Urls(urls)) => Ok(Some(urls)), + Some(AudioListRef::MediaFileTokens(_)) => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "reference_audios", + value: "GmiCloud only supports audio URLs, not media file tokens".to_string(), + })) + } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_fast_g/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_fast_g/cost.rs new file mode 100644 index 0000000000..c6694bf3f8 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_fast_g/cost.rs @@ -0,0 +1,119 @@ +use gmicloud_client::traits::gmicloud_request_cost_calculator_trait::GmiCloudRequestCostCalculator; + +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; +use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::request::GmiCloudSeedance2p0FastGRequestState; + +pub struct GmiCloudSeedance2p0FastGCostState { + request: GmiCloudSeedance2p0FastGRequestState, +} + +impl GmiCloudSeedance2p0FastGCostState { + pub fn from_request(request: &GmiCloudSeedance2p0FastGRequestState) -> Self { + Self { request: request.clone() } + } + + pub fn estimate_cost(&self) -> VideoGenerationCostEstimate { + let cost_in_usd_cents = self.request.request.calculate_cost_in_cents(); + + VideoGenerationCostEstimate { + cost_in_credits: None, + 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_resolution::CommonResolution; + use crate::api::common_video_model::CommonVideoModel; + use crate::api::provider::Provider; + use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + + mod pricing_720p { + use super::*; + + #[test] + fn default_5s() { + // 720p default: 3.1 tenths/s * 5 = 15.5 → ceil = 16 → div_ceil(10) = 2¢ + assert_eq!(cost_cents(None, 5), 2); + } + + #[test] + fn default_10s() { + assert_eq!(cost_cents(None, 10), 4); + } + + #[test] + fn default_15s() { + assert_eq!(cost_cents(None, 15), 5); + } + } + + mod pricing_480p { + use super::*; + + #[test] + fn p480_5s() { + assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5), 1); + } + + #[test] + fn p480_10s() { + assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 10), 1); + } + + #[test] + fn p480_15s() { + assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 15), 2); + } + } + + mod relative_pricing { + use super::*; + + #[test] + fn higher_resolution_costs_more() { + let c480 = cost_cents(Some(CommonResolution::FourEightyP), 10); + let c720 = cost_cents(Some(CommonResolution::SevenTwentyP), 10); + assert!(c480 < c720, "480p ({c480}) should be < 720p ({c720})"); + } + + #[test] + fn fast_cheaper_than_standard() { + let fast = cost_cents(Some(CommonResolution::SevenTwentyP), 10); + let standard = { + let builder = GenerateVideoRequestBuilder { + model: CommonVideoModel::Seedance2p0Global, + provider: Provider::GmiCloud, + resolution: Some(CommonResolution::SevenTwentyP), + duration_seconds: Some(10), + video_batch_count: Some(1), + ..Default::default() + }; + builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap() + }; + assert!(fast < standard, "Fast ({fast}¢) should be < Standard ({standard}¢)"); + } + } + + fn cost_cents(resolution: Option, duration_seconds: u16) -> u64 { + let builder = GenerateVideoRequestBuilder { + model: CommonVideoModel::Seedance2p0FastGlobal, + provider: Provider::GmiCloud, + resolution, + duration_seconds: Some(duration_seconds), + video_batch_count: Some(1), + ..Default::default() + }; + builder.build2() + .expect("build2 should succeed") + .estimate_cost() + .expect("estimate_cost should succeed") + .cost_in_usd_cents + .unwrap() + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_fast_g/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_fast_g/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_fast_g/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_fast_g/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_fast_g/request.rs new file mode 100644 index 0000000000..958765c1e0 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_fast_g/request.rs @@ -0,0 +1,100 @@ +use gmicloud_client::requests::api::video::seedance_2_0_fast_260128::api::Seedance20FastRequest; + +use crate::client::router_gmicloud_client::RouterGmiCloudClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::generate_video_response::{ + GenerateVideoResponse, GmiCloudVideoResponsePayload, +}; + +#[derive(Clone, Debug)] +pub struct GmiCloudSeedance2p0FastGRequestState { + pub request: Seedance20FastRequest, +} + +impl GmiCloudSeedance2p0FastGRequestState { + pub async fn send(&self, client: &RouterGmiCloudClient) -> Result { + let response = self.request.send_request(&client.api_key) + .await + .map_err(|err| ArtcraftRouterError::Provider(ProviderError::GmiCloud(err)))?; + + Ok(GenerateVideoResponse::GmiCloud(GmiCloudVideoResponsePayload { + request_id: response.request_id, + })) + } +} + +#[cfg(test)] +mod tests { + use crate::api::common_aspect_ratio::CommonAspectRatio; + use crate::api::common_resolution::CommonResolution; + use crate::api::common_video_model::CommonVideoModel; + use crate::api::image_ref::ImageRef; + use crate::api::provider::Provider; + use crate::client::router_client::RouterClient; + use crate::client::router_gmicloud_client::RouterGmiCloudClient; + use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; + use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; + use gmicloud_client::creds::gmicloud_api_key::GmiCloudApiKey; + use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL; + + #[tokio::test] + #[ignore] // requires real API key, incurs costs + async fn test_text_to_video_720p() { + let response = run_pipeline(GenerateVideoRequestBuilder { + prompt: Some("A shiba inu playing in autumn leaves.".to_string()), + aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + resolution: Some(CommonResolution::SevenTwentyP), + duration_seconds: Some(5), + ..gmicloud_fast_builder() + }).await; + println!("response: {:?}", response); + assert!(matches!(response, GenerateVideoResponse::GmiCloud(_))); + } + + #[tokio::test] + #[ignore] // requires real API key, incurs costs + async fn test_image_to_video_480p() { + let response = run_pipeline(GenerateVideoRequestBuilder { + prompt: Some("The dog starts running and splashing in the lake.".to_string()), + start_frame: Some(ImageRef::Url(JUNO_AT_LAKE_IMAGE_URL.to_string())), + resolution: Some(CommonResolution::FourEightyP), + duration_seconds: Some(5), + ..gmicloud_fast_builder() + }).await; + println!("response: {:?}", response); + assert!(matches!(response, GenerateVideoResponse::GmiCloud(_))); + } + + fn gmicloud_fast_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Seedance2p0FastGlobal, + provider: Provider::GmiCloud, + video_batch_count: Some(1), + ..Default::default() + } + } + + fn get_gmicloud_client() -> RouterClient { + let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/gmicloud_api_key.txt") + .expect("Failed to read GmiCloud API key"); + let api_key = GmiCloudApiKey::from_str(&secret); + RouterClient::GmiCloud(RouterGmiCloudClient::new(api_key)) + } + + async fn run_pipeline(builder: GenerateVideoRequestBuilder) -> GenerateVideoResponse { + let client = get_gmicloud_client(); + let draft_or_request = builder.build2().expect("build2 should succeed"); + let request = match draft_or_request { + VideoGenerationDraftOrRequest::Request(r) => r, + _ => panic!("expected Request variant (GmiCloud skips draft)"), + }; + let response = request.send_request(&client).await.expect("send_request should succeed"); + match &response { + GenerateVideoResponse::GmiCloud(p) => println!("request_id={}", p.request_id), + other => println!("response: {:?}", other), + } + response + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_g/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_g/build.rs new file mode 100644 index 0000000000..daf1849c1d --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_g/build.rs @@ -0,0 +1,355 @@ +use gmicloud_client::requests::api::video::seedance_2_0_260128::api::{ + Seedance20Ratio, Seedance20Request, Seedance20Resolution, +}; + +use crate::api::audio_list_ref::AudioListRef; +use crate::api::common_aspect_ratio::CommonAspectRatio; +use crate::api::common_resolution::CommonResolution; +use crate::api::image_list_ref::ImageListRef; +use crate::api::image_ref::ImageRef; +use crate::api::video_list_ref::VideoListRef; +use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::client_error::ClientError; +use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; +use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::request::GmiCloudSeedance2p0GRequestState; +use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; +use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; + +pub fn build_gmicloud_seedance_2p0_g( + mut builder: GenerateVideoRequestBuilder, +) -> Result { + let strategy = builder.request_mismatch_mitigation_strategy; + + let ratio = plan_ratio(builder.aspect_ratio.take(), strategy)?; + let resolution = plan_resolution(builder.resolution.take(), strategy)?; + let duration = builder.duration_seconds.take().map(|d| (d as u8).clamp(4, 15)); + let prompt = builder.prompt.take().unwrap_or_default(); + + let first_frame = resolve_url(builder.start_frame.take())?; + let last_frame = resolve_url(builder.end_frame.take())?; + let reference_images = resolve_url_list_from_images(builder.reference_images.take())?; + let reference_videos = resolve_url_list_from_videos(builder.reference_videos.take())?; + let reference_audios = resolve_url_list_from_audios(builder.reference_audio.take())?; + + let request = Seedance20Request { + prompt, + duration, + resolution, + ratio, + seed: None, + watermark: Some(false), + generate_audio: Some(true), + web_search: None, + first_frame, + last_frame, + reference_images, + reference_videos, + reference_audios, + reference_asset_ids: None, + }; + + let state = GmiCloudSeedance2p0GRequestState { request }; + Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::GmiCloudSeedance2p0G(state))) +} + +fn plan_ratio( + aspect_ratio: Option, + _strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + match aspect_ratio { + None | Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) + | Some(CommonAspectRatio::Auto3k) | Some(CommonAspectRatio::Auto4k) => Ok(None), + Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(Seedance20Ratio::Landscape16x9)), + Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(Seedance20Ratio::Portrait9x16)), + Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(Seedance20Ratio::Square)), + Some(CommonAspectRatio::WideFourByThree) => Ok(Some(Seedance20Ratio::Standard4x3)), + Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(Seedance20Ratio::Portrait3x4)), + Some(CommonAspectRatio::WideTwentyOneByNine) | Some(CommonAspectRatio::TallNineByTwentyOne) => { + Ok(Some(Seedance20Ratio::UltraWide21x9)) + } + Some(CommonAspectRatio::WideThreeByTwo) | Some(CommonAspectRatio::WideFiveByFour) + | Some(CommonAspectRatio::TallFourByFive) | Some(CommonAspectRatio::TallTwoByThree) => { + Ok(Some(Seedance20Ratio::Adaptive)) + } + } +} + +fn plan_resolution( + resolution: Option, + _strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + match resolution { + None => Ok(None), + Some(CommonResolution::FourEightyP) => Ok(Some(Seedance20Resolution::FourEightyP)), + Some(CommonResolution::SevenTwentyP) => Ok(Some(Seedance20Resolution::SevenTwentyP)), + Some(CommonResolution::TenEightyP) => Ok(Some(Seedance20Resolution::TenEightyP)), + Some(CommonResolution::HalfK) | Some(CommonResolution::OneK) => Ok(Some(Seedance20Resolution::FourEightyP)), + Some(CommonResolution::TwoK) | Some(CommonResolution::ThreeK) | Some(CommonResolution::FourK) => { + Ok(Some(Seedance20Resolution::TenEightyP)) + } + } +} + +fn resolve_url(image_ref: Option) -> Result, ArtcraftRouterError> { + match image_ref { + None => Ok(None), + Some(ImageRef::Url(url)) => Ok(Some(url)), + Some(ImageRef::MediaFileToken(_)) => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "start_frame/end_frame", + value: "GmiCloud only supports image URLs, not media file tokens".to_string(), + })) + } + } +} + +fn resolve_url_list_from_images( + list_ref: Option, +) -> Result>, ArtcraftRouterError> { + match list_ref { + None => Ok(None), + Some(ImageListRef::Urls(urls)) => Ok(Some(urls)), + Some(ImageListRef::MediaFileTokens(_)) => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "reference_images", + value: "GmiCloud only supports image URLs, not media file tokens".to_string(), + })) + } + } +} + +fn resolve_url_list_from_videos( + list_ref: Option, +) -> Result>, ArtcraftRouterError> { + match list_ref { + None => Ok(None), + Some(VideoListRef::Urls(urls)) => Ok(Some(urls)), + Some(VideoListRef::MediaFileTokens(_)) => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "reference_videos", + value: "GmiCloud only supports video URLs, not media file tokens".to_string(), + })) + } + } +} + +#[cfg(test)] +mod tests { + use gmicloud_client::requests::api::video::seedance_2_0_260128::api::{ + Seedance20Ratio, Seedance20Resolution, + }; + use tokens::tokens::media_files::MediaFileToken; + + use crate::api::common_aspect_ratio::CommonAspectRatio; + use crate::api::common_resolution::CommonResolution; + use crate::api::common_video_model::CommonVideoModel; + use crate::api::image_list_ref::ImageListRef; + use crate::api::image_ref::ImageRef; + use crate::api::provider::Provider; + use crate::api::video_list_ref::VideoListRef; + use crate::api::audio_list_ref::AudioListRef; + use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; + use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; + + use super::*; + + mod field_conversions { + use super::*; + + #[test] + fn prompt_passed_through() { + let req = unwrap_request(make_builder(|b| { b.prompt = Some("test prompt".to_string()); })); + assert_eq!(req.request.prompt, "test prompt"); + } + + #[test] + fn duration_passed_through() { + let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(10); })); + assert_eq!(req.request.duration, Some(10)); + } + + #[test] + fn duration_clamped_to_min() { + let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(1); })); + assert_eq!(req.request.duration, Some(4)); + } + + #[test] + fn duration_clamped_to_max() { + let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(99); })); + assert_eq!(req.request.duration, Some(15)); + } + + #[test] + fn duration_none_stays_none() { + let req = unwrap_request(make_builder(|b| { b.duration_seconds = None; })); + assert_eq!(req.request.duration, None); + } + } + + mod aspect_ratio_tests { + use super::*; + + #[test] + fn landscape_16x9() { + let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(CommonAspectRatio::WideSixteenByNine); })); + assert_eq!(req.request.ratio, Some(Seedance20Ratio::Landscape16x9)); + } + + #[test] + fn portrait_9x16() { + let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(CommonAspectRatio::TallNineBySixteen); })); + assert_eq!(req.request.ratio, Some(Seedance20Ratio::Portrait9x16)); + } + + #[test] + fn square() { + let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(CommonAspectRatio::Square); })); + assert_eq!(req.request.ratio, Some(Seedance20Ratio::Square)); + } + + #[test] + fn four_by_three() { + let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(CommonAspectRatio::WideFourByThree); })); + assert_eq!(req.request.ratio, Some(Seedance20Ratio::Standard4x3)); + } + + #[test] + fn unsupported_maps_to_adaptive() { + let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(CommonAspectRatio::WideThreeByTwo); })); + assert_eq!(req.request.ratio, Some(Seedance20Ratio::Adaptive)); + } + + #[test] + fn auto_maps_to_none() { + let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(CommonAspectRatio::Auto); })); + assert_eq!(req.request.ratio, None); + } + } + + mod resolution_tests { + use super::*; + + #[test] + fn res_480p() { + let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::FourEightyP); })); + assert_eq!(req.request.resolution, Some(Seedance20Resolution::FourEightyP)); + } + + #[test] + fn res_720p() { + let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::SevenTwentyP); })); + assert_eq!(req.request.resolution, Some(Seedance20Resolution::SevenTwentyP)); + } + + #[test] + fn res_1080p() { + let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::TenEightyP); })); + assert_eq!(req.request.resolution, Some(Seedance20Resolution::TenEightyP)); + } + + #[test] + fn none_stays_none() { + let req = unwrap_request(make_builder(|_| {})); + assert!(req.request.resolution.is_none()); + } + } + + mod media_url_tests { + use super::*; + + #[test] + fn start_frame_url_passed_through() { + let req = unwrap_request(make_builder(|b| { + b.start_frame = Some(ImageRef::Url("https://example.com/start.png".to_string())); + })); + assert_eq!(req.request.first_frame, Some("https://example.com/start.png".to_string())); + } + + #[test] + fn media_file_token_rejected_for_start_frame() { + let result = build_gmicloud_seedance_2p0_g(GenerateVideoRequestBuilder { + start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_test".to_string()))), + ..base_builder() + }); + assert!(result.is_err()); + } + + #[test] + fn reference_image_urls_passed_through() { + let urls = vec!["https://example.com/a.png".to_string(), "https://example.com/b.png".to_string()]; + let req = unwrap_request(make_builder(|b| { + b.reference_images = Some(ImageListRef::Urls(urls.clone())); + })); + assert_eq!(req.request.reference_images, Some(urls)); + } + + #[test] + fn reference_image_tokens_rejected() { + let result = build_gmicloud_seedance_2p0_g(GenerateVideoRequestBuilder { + reference_images: Some(ImageListRef::MediaFileTokens(vec![MediaFileToken::new("mf_a".to_string())])), + ..base_builder() + }); + assert!(result.is_err()); + } + + #[test] + fn reference_video_urls_passed_through() { + let urls = vec!["https://example.com/v.mp4".to_string()]; + let req = unwrap_request(make_builder(|b| { + b.reference_videos = Some(VideoListRef::Urls(urls.clone())); + })); + assert_eq!(req.request.reference_videos, Some(urls)); + } + + #[test] + fn reference_audio_urls_passed_through() { + let urls = vec!["https://example.com/a.wav".to_string()]; + let req = unwrap_request(make_builder(|b| { + b.reference_audio = Some(AudioListRef::Urls(urls.clone())); + })); + assert_eq!(req.request.reference_audios, Some(urls)); + } + } + + fn base_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Seedance2p0Global, + provider: Provider::GmiCloud, + duration_seconds: Some(5), + video_batch_count: Some(1), + ..Default::default() + } + } + + fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder { + let mut builder = base_builder(); + f(&mut builder); + builder + } + + fn unwrap_request(builder: GenerateVideoRequestBuilder) -> GmiCloudSeedance2p0GRequestState { + let result = build_gmicloud_seedance_2p0_g(builder).expect("build should succeed"); + match result { + VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::GmiCloudSeedance2p0G(state)) => state, + _ => panic!("expected GmiCloudSeedance2p0G request"), + } + } +} + +fn resolve_url_list_from_audios( + list_ref: Option, +) -> Result>, ArtcraftRouterError> { + match list_ref { + None => Ok(None), + Some(AudioListRef::Urls(urls)) => Ok(Some(urls)), + Some(AudioListRef::MediaFileTokens(_)) => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "reference_audios", + value: "GmiCloud only supports audio URLs, not media file tokens".to_string(), + })) + } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_g/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_g/cost.rs new file mode 100644 index 0000000000..f9ad77c05a --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_g/cost.rs @@ -0,0 +1,132 @@ +use gmicloud_client::traits::gmicloud_request_cost_calculator_trait::GmiCloudRequestCostCalculator; + +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; +use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::request::GmiCloudSeedance2p0GRequestState; + +pub struct GmiCloudSeedance2p0GCostState { + request: GmiCloudSeedance2p0GRequestState, +} + +impl GmiCloudSeedance2p0GCostState { + pub fn from_request(request: &GmiCloudSeedance2p0GRequestState) -> Self { + Self { request: request.clone() } + } + + pub fn estimate_cost(&self) -> VideoGenerationCostEstimate { + let cost_in_usd_cents = self.request.request.calculate_cost_in_cents(); + + VideoGenerationCostEstimate { + cost_in_credits: None, + 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_resolution::CommonResolution; + use crate::api::common_video_model::CommonVideoModel; + use crate::api::provider::Provider; + use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + + mod pricing_720p { + use super::*; + + #[test] + fn default_5s() { + // 720p default: 5.2 tenths/s * 5 = 26 → ceil = 26 → div_ceil(10) = 3¢ + assert_eq!(cost_cents(None, 5), 3); + } + + #[test] + fn default_10s() { + assert_eq!(cost_cents(None, 10), 6); + } + + #[test] + fn explicit_720p_5s() { + assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5), 3); + } + } + + mod pricing_480p { + use super::*; + + #[test] + fn p480_5s() { + assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5), 2); + } + + #[test] + fn p480_10s() { + assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 10), 3); + } + + #[test] + fn p480_15s() { + assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 15), 4); + } + } + + mod pricing_1080p { + use super::*; + + #[test] + fn p1080_5s() { + assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 5), 6); + } + + #[test] + fn p1080_10s() { + assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 10), 12); + } + + #[test] + fn p1080_15s() { + assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 15), 18); + } + } + + mod relative_pricing { + use super::*; + + #[test] + fn higher_resolution_costs_more() { + let c480 = cost_cents(Some(CommonResolution::FourEightyP), 10); + let c720 = cost_cents(Some(CommonResolution::SevenTwentyP), 10); + let c1080 = cost_cents(Some(CommonResolution::TenEightyP), 10); + assert!(c480 < c720, "480p ({c480}) should be < 720p ({c720})"); + assert!(c720 < c1080, "720p ({c720}) should be < 1080p ({c1080})"); + } + + #[test] + fn longer_duration_costs_more() { + let c5 = cost_cents(None, 5); + let c10 = cost_cents(None, 10); + let c15 = cost_cents(None, 15); + assert!(c5 < c10); + assert!(c10 < c15); + } + } + + fn cost_cents(resolution: Option, duration_seconds: u16) -> u64 { + let builder = GenerateVideoRequestBuilder { + model: CommonVideoModel::Seedance2p0Global, + provider: Provider::GmiCloud, + resolution, + duration_seconds: Some(duration_seconds), + video_batch_count: Some(1), + ..Default::default() + }; + builder.build2() + .expect("build2 should succeed") + .estimate_cost() + .expect("estimate_cost should succeed") + .cost_in_usd_cents + .unwrap() + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_g/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_g/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_g/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_g/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_g/request.rs new file mode 100644 index 0000000000..3b8b761d78 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/gmicloud/seedance_2p0_g/request.rs @@ -0,0 +1,98 @@ +use gmicloud_client::requests::api::video::seedance_2_0_260128::api::Seedance20Request; + +use crate::client::router_gmicloud_client::RouterGmiCloudClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::generate_video_response::{ + GenerateVideoResponse, GmiCloudVideoResponsePayload, +}; + +#[derive(Clone, Debug)] +pub struct GmiCloudSeedance2p0GRequestState { + pub request: Seedance20Request, +} + +impl GmiCloudSeedance2p0GRequestState { + pub async fn send(&self, client: &RouterGmiCloudClient) -> Result { + let response = self.request.send_request(&client.api_key) + .await + .map_err(|err| ArtcraftRouterError::Provider(ProviderError::GmiCloud(err)))?; + + Ok(GenerateVideoResponse::GmiCloud(GmiCloudVideoResponsePayload { + request_id: response.request_id, + })) + } +} + +#[cfg(test)] +mod tests { + use crate::api::common_aspect_ratio::CommonAspectRatio; + use crate::api::common_resolution::CommonResolution; + use crate::api::common_video_model::CommonVideoModel; + use crate::api::image_ref::ImageRef; + use crate::api::provider::Provider; + use crate::client::router_client::RouterClient; + use crate::client::router_gmicloud_client::RouterGmiCloudClient; + use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; + use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; + use gmicloud_client::creds::gmicloud_api_key::GmiCloudApiKey; + use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL; + + #[tokio::test] + #[ignore] // requires real API key, incurs costs + async fn test_text_to_video_720p() { + let response = run_pipeline(GenerateVideoRequestBuilder { + prompt: Some("A corgi running through a field of wildflowers at sunset.".to_string()), + aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + resolution: Some(CommonResolution::SevenTwentyP), + duration_seconds: Some(5), + ..gmicloud_builder() + }).await; + assert!(matches!(response, GenerateVideoResponse::GmiCloud(_))); + } + + #[tokio::test] + #[ignore] // requires real API key, incurs costs + async fn test_image_to_video_480p() { + let response = run_pipeline(GenerateVideoRequestBuilder { + prompt: Some("The dog starts running and splashing in the lake.".to_string()), + start_frame: Some(ImageRef::Url(JUNO_AT_LAKE_IMAGE_URL.to_string())), + resolution: Some(CommonResolution::FourEightyP), + duration_seconds: Some(5), + ..gmicloud_builder() + }).await; + assert!(matches!(response, GenerateVideoResponse::GmiCloud(_))); + } + + fn gmicloud_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Seedance2p0Global, + provider: Provider::GmiCloud, + video_batch_count: Some(1), + ..Default::default() + } + } + + fn get_gmicloud_client() -> RouterClient { + let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/gmicloud_api_key.txt") + .expect("Failed to read GmiCloud API key"); + let api_key = GmiCloudApiKey::from_str(&secret); + RouterClient::GmiCloud(RouterGmiCloudClient::new(api_key)) + } + + async fn run_pipeline(builder: GenerateVideoRequestBuilder) -> GenerateVideoResponse { + let client = get_gmicloud_client(); + let draft_or_request = builder.build2().expect("build2 should succeed"); + let request = match draft_or_request { + VideoGenerationDraftOrRequest::Request(r) => r, + _ => panic!("expected Request variant (GmiCloud skips draft)"), + }; + let response = request.send_request(&client).await.expect("send_request should succeed"); + match &response { + GenerateVideoResponse::GmiCloud(p) => println!("request_id={}", p.request_id), + other => println!("response: {:?}", other), + } + response + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/mod.rs index 6fe4c6b3b5..56e6b0e6dc 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/mod.rs @@ -1,2 +1,3 @@ pub mod artcraft; +pub mod gmicloud; pub mod kinovi; \ No newline at end of file diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/video_generation_request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/video_generation_request.rs index 18a1151417..b1e67d0b05 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/video_generation_request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/video_generation_request.rs @@ -9,6 +9,10 @@ use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0::cost: use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0::request::ArtcraftSeedance2p0RequestState; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast::cost::ArtcraftSeedance2p0FastCostState; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast::request::ArtcraftSeedance2p0FastRequestState; +use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::cost::GmiCloudSeedance2p0GCostState; +use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::request::GmiCloudSeedance2p0GRequestState; +use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::cost::GmiCloudSeedance2p0FastGCostState; +use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::request::GmiCloudSeedance2p0FastGRequestState; use crate::generate::generate_video_v2::providers::kinovi::happy_horse_1p0::cost::KinoviHappyHorse1p0CostState; use crate::generate::generate_video_v2::providers::kinovi::happy_horse_1p0::request::KinoviHappyHorse1p0RequestState; use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0::cost::KinoviSeedance2p0CostState; @@ -21,6 +25,8 @@ pub enum VideoGenerationRequest { ArtcraftHappyHorse1p0(ArtcraftHappyHorse1p0RequestState), ArtcraftSeedance2p0(ArtcraftSeedance2p0RequestState), ArtcraftSeedance2p0Fast(ArtcraftSeedance2p0FastRequestState), + GmiCloudSeedance2p0G(GmiCloudSeedance2p0GRequestState), + GmiCloudSeedance2p0FastG(GmiCloudSeedance2p0FastGRequestState), KinoviHappyHorse1p0(KinoviHappyHorse1p0RequestState), KinoviSeedance2p0(KinoviSeedance2p0RequestState), KinoviSeedance2p0Fast(KinoviSeedance2p0FastRequestState), @@ -33,6 +39,8 @@ impl VideoGenerationRequest { Self::ArtcraftHappyHorse1p0(_) => Provider::Artcraft, Self::ArtcraftSeedance2p0(_) => Provider::Artcraft, Self::ArtcraftSeedance2p0Fast(_) => Provider::Artcraft, + Self::GmiCloudSeedance2p0G(_) => Provider::GmiCloud, + Self::GmiCloudSeedance2p0FastG(_) => Provider::GmiCloud, Self::KinoviHappyHorse1p0(_) => Provider::Seedance2Pro, Self::KinoviSeedance2p0(_) => Provider::Seedance2Pro, Self::KinoviSeedance2p0Fast(_) => Provider::Seedance2Pro, @@ -45,6 +53,8 @@ impl VideoGenerationRequest { VideoGenerationRequest::ArtcraftHappyHorse1p0(request) => Ok(ArtcraftHappyHorse1p0CostState::from_request(request).estimate_cost()), VideoGenerationRequest::ArtcraftSeedance2p0(request) => Ok(ArtcraftSeedance2p0CostState::from_request(request).estimate_cost()), VideoGenerationRequest::ArtcraftSeedance2p0Fast(request) => Ok(ArtcraftSeedance2p0FastCostState::from_request(request).estimate_cost()), + VideoGenerationRequest::GmiCloudSeedance2p0G(request) => Ok(GmiCloudSeedance2p0GCostState::from_request(request).estimate_cost()), + VideoGenerationRequest::GmiCloudSeedance2p0FastG(request) => Ok(GmiCloudSeedance2p0FastGCostState::from_request(request).estimate_cost()), VideoGenerationRequest::KinoviHappyHorse1p0(request) => Ok(KinoviHappyHorse1p0CostState::from_request(request).estimate_cost()), VideoGenerationRequest::KinoviSeedance2p0(request) => Ok(KinoviSeedance2p0CostState::from_request(request).estimate_cost()), VideoGenerationRequest::KinoviSeedance2p0Fast(request) => Ok(KinoviSeedance2p0FastCostState::from_request(request).estimate_cost()), @@ -67,6 +77,14 @@ impl VideoGenerationRequest { let client_ref = client.get_artcraft_client_ref()?; request.send(client_ref).await }, + VideoGenerationRequest::GmiCloudSeedance2p0G(request) => { + let client_ref = client.get_gmicloud_client_ref()?; + request.send(client_ref).await + }, + VideoGenerationRequest::GmiCloudSeedance2p0FastG(request) => { + let client_ref = client.get_gmicloud_client_ref()?; + request.send(client_ref).await + }, VideoGenerationRequest::KinoviHappyHorse1p0(request) => { let client_ref = client.get_seedance2pro_client_ref()?; request.send(client_ref).await diff --git a/crates/api_clients/gmicloud_client/src/creds/gmicloud_api_key.rs b/crates/api_clients/gmicloud_client/src/creds/gmicloud_api_key.rs index 0290ecb003..d1c30426b0 100644 --- a/crates/api_clients/gmicloud_client/src/creds/gmicloud_api_key.rs +++ b/crates/api_clients/gmicloud_client/src/creds/gmicloud_api_key.rs @@ -1,4 +1,5 @@ /// A GmiCloud API key used for Bearer token authentication. +#[derive(Clone)] pub struct GmiCloudApiKey(pub String); impl GmiCloudApiKey { diff --git a/crates/api_clients/gmicloud_client/src/requests/api/video/seedance_2_0_260128/api.rs b/crates/api_clients/gmicloud_client/src/requests/api/video/seedance_2_0_260128/api.rs index 2c0ad063be..1d19a29dc5 100644 --- a/crates/api_clients/gmicloud_client/src/requests/api/video/seedance_2_0_260128/api.rs +++ b/crates/api_clients/gmicloud_client/src/requests/api/video/seedance_2_0_260128/api.rs @@ -53,14 +53,14 @@ pub struct Seedance20Request { pub reference_asset_ids: Option>, } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq)] pub enum Seedance20Resolution { FourEightyP, SevenTwentyP, TenEightyP, } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq)] pub enum Seedance20Ratio { /// 16:9 (landscape) Landscape16x9, diff --git a/crates/api_clients/gmicloud_client/src/requests/api/video/seedance_2_0_fast_260128/api.rs b/crates/api_clients/gmicloud_client/src/requests/api/video/seedance_2_0_fast_260128/api.rs index cdea27c197..6b2eed5e38 100644 --- a/crates/api_clients/gmicloud_client/src/requests/api/video/seedance_2_0_fast_260128/api.rs +++ b/crates/api_clients/gmicloud_client/src/requests/api/video/seedance_2_0_fast_260128/api.rs @@ -53,13 +53,13 @@ pub struct Seedance20FastRequest { pub reference_asset_ids: Option>, } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq)] pub enum Seedance20FastResolution { FourEightyP, SevenTwentyP, } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq)] pub enum Seedance20FastRatio { /// 16:9 (landscape) Landscape16x9, diff --git a/crates/schema/database/mysql_queries/src/queries/generic_inference/gmicloud/insert_generic_inference_job_for_gmicloud.rs b/crates/schema/database/mysql_queries/src/queries/generic_inference/gmicloud/insert_generic_inference_job_for_gmicloud.rs new file mode 100644 index 0000000000..af079a7d9a --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/generic_inference/gmicloud/insert_generic_inference_job_for_gmicloud.rs @@ -0,0 +1,119 @@ +use log::info; +use sqlx::{Executor, MySql}; +use std::marker::PhantomData; + +use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory; +use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty; +use enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory; +use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType; +use enums::common::job_status_plus::JobStatusPlus; +use enums::common::visibility::Visibility; +use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken; +use tokens::tokens::prompts::PromptToken; +use tokens::tokens::users::UserToken; + +use crate::errors::database_query_error::DatabaseQueryError; + +pub struct InsertGmiCloudInferenceJobArgs<'e, 'c, E> +where + E: 'e + Executor<'c, Database = MySql>, +{ + pub apriori_job_token: &'e InferenceJobToken, + pub uuid_idempotency_token: &'e str, + pub external_request_id: &'e str, + pub maybe_prompt_token: Option<&'e PromptToken>, + pub maybe_creator_user_token: Option<&'e UserToken>, + pub maybe_avt_token: Option<&'e AnonymousVisitorTrackingToken>, + pub creator_ip_address: &'e str, + pub maybe_debug_log_event_token: Option<&'e DebugLogEventToken>, + pub mysql_executor: E, + pub phantom: PhantomData<&'c E>, +} + +pub async fn insert_generic_inference_job_for_gmicloud<'e, 'c: 'e, E>( + args: InsertGmiCloudInferenceJobArgs<'e, 'c, E>, +) -> Result +where + E: 'e + Executor<'c, Database = MySql>, +{ + let query = sqlx::query!( + r#" +INSERT INTO generic_inference_jobs +SET + token = ?, + uuid_idempotency_token = ?, + + job_type = ?, + + maybe_external_third_party = ?, + maybe_external_third_party_id = ?, + + product_category = ?, + inference_category = ?, + + maybe_model_type = NULL, + maybe_model_token = NULL, + + maybe_input_source_token = NULL, + maybe_input_source_token_type = NULL, + + maybe_download_url = NULL, + maybe_cover_image_media_file_token = NULL, + + maybe_prompt_token = ?, + + maybe_raw_inference_text = NULL, + + maybe_inference_args = NULL, + + maybe_creator_user_token = ?, + maybe_creator_anonymous_visitor_token = ?, + creator_ip_address = ?, + creator_set_visibility = ?, + + priority_level = 0, + is_keepalive_required = FALSE, + max_duration_seconds = 0, + + is_debug_request = FALSE, + maybe_routing_tag = NULL, + + maybe_debug_log_event_token = ?, + + frontend_failure_category = NULL, + failure_reason = NULL, + + status = ? + "#, + args.apriori_job_token.as_str(), + args.uuid_idempotency_token, + InferenceJobType::GmiCloudQueue.to_str(), + InferenceJobExternalThirdParty::GmiCloud.to_str(), + args.external_request_id, + InferenceJobProductCategory::GmiCloudVideo.to_str(), + InferenceCategory::VideoGeneration.to_str(), + args.maybe_prompt_token.map(|t| t.to_string()), + args.maybe_creator_user_token.map(|t| t.to_string()), + args.maybe_avt_token.map(|t| t.to_string()), + args.creator_ip_address, + Visibility::Public.to_str(), + args.maybe_debug_log_event_token.map(|t| t.as_str()), + JobStatusPlus::Pending.to_str(), + ); + + let query_result = query.execute(args.mysql_executor).await; + + let record_id = match query_result { + Err(err) => return Err(DatabaseQueryError::from(err)), + Ok(res) => res.last_insert_id(), + }; + + info!( + "Insert generic inference job for GmiCloud: {} with record ID {}", + args.apriori_job_token, record_id + ); + + Ok(args.apriori_job_token.clone()) +} diff --git a/crates/schema/database/mysql_queries/src/queries/generic_inference/gmicloud/mod.rs b/crates/schema/database/mysql_queries/src/queries/generic_inference/gmicloud/mod.rs new file mode 100644 index 0000000000..ac61f12ef8 --- /dev/null +++ b/crates/schema/database/mysql_queries/src/queries/generic_inference/gmicloud/mod.rs @@ -0,0 +1 @@ +pub mod insert_generic_inference_job_for_gmicloud; diff --git a/crates/schema/database/mysql_queries/src/queries/generic_inference/mod.rs b/crates/schema/database/mysql_queries/src/queries/generic_inference/mod.rs index a60d53adac..422862351a 100644 --- a/crates/schema/database/mysql_queries/src/queries/generic_inference/mod.rs +++ b/crates/schema/database/mysql_queries/src/queries/generic_inference/mod.rs @@ -1,5 +1,6 @@ pub mod beeble; pub mod fal; +pub mod gmicloud; pub mod job; pub mod moderation; pub mod seedance2pro; diff --git a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_external_third_party.rs b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_external_third_party.rs index 6cf1a2351b..6c104da8fb 100644 --- a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_external_third_party.rs +++ b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_external_third_party.rs @@ -21,6 +21,10 @@ pub enum InferenceJobExternalThirdParty { #[default] Fal, + /// GmiCloud jobs + #[serde(rename = "gmicloud")] + GmiCloud, + /// Seedance 2 Pro jobs #[serde(rename = "seedance2pro")] Seedance2Pro, @@ -40,6 +44,7 @@ impl InferenceJobExternalThirdParty { match self { Self::Beeble => "beeble", Self::Fal => "fal", + Self::GmiCloud => "gmicloud", Self::Seedance2Pro => "seedance2pro", Self::Worldlabs => "worldlabs", } @@ -49,6 +54,7 @@ impl InferenceJobExternalThirdParty { match value { "beeble" => Ok(Self::Beeble), "fal" => Ok(Self::Fal), + "gmicloud" => Ok(Self::GmiCloud), "seedance2pro" => Ok(Self::Seedance2Pro), "worldlabs" => Ok(Self::Worldlabs), _ => Err(format!("invalid value: {:?}", value)), @@ -61,6 +67,7 @@ impl InferenceJobExternalThirdParty { BTreeSet::from([ Self::Beeble, Self::Fal, + Self::GmiCloud, Self::Seedance2Pro, Self::Worldlabs, ]) @@ -79,6 +86,7 @@ mod tests { fn test_serialization() { assert_serialization(InferenceJobExternalThirdParty::Beeble, "beeble"); assert_serialization(InferenceJobExternalThirdParty::Fal, "fal"); + assert_serialization(InferenceJobExternalThirdParty::GmiCloud, "gmicloud"); assert_serialization(InferenceJobExternalThirdParty::Seedance2Pro, "seedance2pro"); assert_serialization(InferenceJobExternalThirdParty::Worldlabs, "worldlabs"); } @@ -87,6 +95,7 @@ mod tests { fn to_str() { assert_eq!(InferenceJobExternalThirdParty::Beeble.to_str(), "beeble"); assert_eq!(InferenceJobExternalThirdParty::Fal.to_str(), "fal"); + assert_eq!(InferenceJobExternalThirdParty::GmiCloud.to_str(), "gmicloud"); assert_eq!(InferenceJobExternalThirdParty::Seedance2Pro.to_str(), "seedance2pro"); assert_eq!(InferenceJobExternalThirdParty::Worldlabs.to_str(), "worldlabs"); } @@ -95,6 +104,7 @@ mod tests { fn from_str() { assert_eq!(InferenceJobExternalThirdParty::from_str("beeble").unwrap(), InferenceJobExternalThirdParty::Beeble); assert_eq!(InferenceJobExternalThirdParty::from_str("fal").unwrap(), InferenceJobExternalThirdParty::Fal); + assert_eq!(InferenceJobExternalThirdParty::from_str("gmicloud").unwrap(), InferenceJobExternalThirdParty::GmiCloud); assert_eq!(InferenceJobExternalThirdParty::from_str("seedance2pro").unwrap(), InferenceJobExternalThirdParty::Seedance2Pro); assert_eq!(InferenceJobExternalThirdParty::from_str("worldlabs").unwrap(), InferenceJobExternalThirdParty::Worldlabs); } @@ -102,7 +112,7 @@ mod tests { #[test] fn all_variants() { // Static check - const EXPECTED_COUNT : usize = 4; + const EXPECTED_COUNT : usize = 5; assert_eq!(InferenceJobExternalThirdParty::all_variants().len(), EXPECTED_COUNT); assert_eq!(InferenceJobExternalThirdParty::iter().len(), EXPECTED_COUNT); diff --git a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_product_category.rs b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_product_category.rs index 755a888238..1e4ab1bb29 100644 --- a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_product_category.rs +++ b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_product_category.rs @@ -30,6 +30,11 @@ pub enum InferenceJobProductCategory { FalObject, FalBgRemoval, + // =============== GMICLOUD =============== + + #[serde(rename = "gmicloud_video")] + GmiCloudVideo, + // =============== BEEBLE =============== #[serde(rename = "beeble_video")] @@ -144,6 +149,7 @@ impl InferenceJobProductCategory { Self::FalVideo => "fal_video", Self::FalObject => "fal_object", Self::FalBgRemoval => "fal_bg_removal", + Self::GmiCloudVideo => "gmicloud_video", Self::BeebleVideo => "beeble_video", Self::Seedance2ProVideo => "seedance2pro_video", Self::Seedance2ProCharacter => "seedance2pro_character", @@ -180,6 +186,7 @@ impl InferenceJobProductCategory { "fal_video" => Ok(Self::FalVideo), "fal_object" => Ok(Self::FalObject), "fal_bg_removal" => Ok(Self::FalBgRemoval), + "gmicloud_video" => Ok(Self::GmiCloudVideo), "beeble_video" => Ok(Self::BeebleVideo), "seedance2pro_video" => Ok(Self::Seedance2ProVideo), "seedance2pro_character" => Ok(Self::Seedance2ProCharacter), @@ -219,6 +226,7 @@ impl InferenceJobProductCategory { Self::FalVideo, Self::FalObject, Self::FalBgRemoval, + Self::GmiCloudVideo, Self::BeebleVideo, Self::Seedance2ProVideo, Self::Seedance2ProCharacter, @@ -264,6 +272,7 @@ mod tests { assert_serialization(InferenceJobProductCategory::FalVideo, "fal_video"); assert_serialization(InferenceJobProductCategory::FalObject, "fal_object"); assert_serialization(InferenceJobProductCategory::FalBgRemoval, "fal_bg_removal"); + assert_serialization(InferenceJobProductCategory::GmiCloudVideo, "gmicloud_video"); assert_serialization(InferenceJobProductCategory::BeebleVideo, "beeble_video"); assert_serialization(InferenceJobProductCategory::Seedance2ProVideo, "seedance2pro_video"); assert_serialization(InferenceJobProductCategory::Seedance2ProCharacter, "seedance2pro_character"); @@ -299,6 +308,7 @@ mod tests { assert_eq!(InferenceJobProductCategory::FalVideo.to_str(), "fal_video"); assert_eq!(InferenceJobProductCategory::FalObject.to_str(), "fal_object"); assert_eq!(InferenceJobProductCategory::FalBgRemoval.to_str(), "fal_bg_removal"); + assert_eq!(InferenceJobProductCategory::GmiCloudVideo.to_str(), "gmicloud_video"); assert_eq!(InferenceJobProductCategory::BeebleVideo.to_str(), "beeble_video"); assert_eq!(InferenceJobProductCategory::Seedance2ProVideo.to_str(), "seedance2pro_video"); assert_eq!(InferenceJobProductCategory::Seedance2ProCharacter.to_str(), "seedance2pro_character"); @@ -333,6 +343,7 @@ mod tests { assert_eq!(InferenceJobProductCategory::from_str("fal_video").unwrap(), InferenceJobProductCategory::FalVideo); assert_eq!(InferenceJobProductCategory::from_str("fal_object").unwrap(), InferenceJobProductCategory::FalObject); assert_eq!(InferenceJobProductCategory::from_str("fal_bg_removal").unwrap(), InferenceJobProductCategory::FalBgRemoval); + assert_eq!(InferenceJobProductCategory::from_str("gmicloud_video").unwrap(), InferenceJobProductCategory::GmiCloudVideo); assert_eq!(InferenceJobProductCategory::from_str("beeble_video").unwrap(), InferenceJobProductCategory::BeebleVideo); assert_eq!(InferenceJobProductCategory::from_str("seedance2pro_video").unwrap(), InferenceJobProductCategory::Seedance2ProVideo); assert_eq!(InferenceJobProductCategory::from_str("seedance2pro_character").unwrap(), InferenceJobProductCategory::Seedance2ProCharacter); @@ -363,7 +374,7 @@ mod tests { #[test] fn all_variants() { // Static check - const EXPECTED_COUNT : usize = 31; + const EXPECTED_COUNT : usize = 32; assert_eq!(InferenceJobProductCategory::all_variants().len(), EXPECTED_COUNT); assert_eq!(InferenceJobProductCategory::iter().len(), EXPECTED_COUNT); diff --git a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_type.rs b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_type.rs index cb74bdead9..3be7528c86 100644 --- a/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_type.rs +++ b/crates/schema/public/enums/src/by_table/generic_inference_jobs/inference_job_type.rs @@ -19,6 +19,10 @@ pub enum InferenceJobType { /// Instead, we receive web hook updates from Fal. FalQueue, + /// GmiCloud jobs. We poll for results. + #[serde(rename = "gmicloud_queue")] + GmiCloudQueue, + /// Seedance 2 Pro jobs. We poll for results. #[serde(rename = "seedance2pro_queue")] Seedance2ProQueue, @@ -126,6 +130,7 @@ impl InferenceJobType { pub fn to_str(&self) -> &'static str { match self { Self::FalQueue => "fal_queue", + Self::GmiCloudQueue => "gmicloud_queue", Self::BeebleQueue => "beeble_queue", Self::Seedance2ProQueue => "seedance2pro_queue", Self::Seedance2ProCharacter => "seedance2pro_character", @@ -156,6 +161,7 @@ impl InferenceJobType { pub fn from_str(value: &str) -> Result { match value { "fal_queue" => Ok(Self::FalQueue), + "gmicloud_queue" => Ok(Self::GmiCloudQueue), "beeble_queue" => Ok(Self::BeebleQueue), "seedance2pro_queue" => Ok(Self::Seedance2ProQueue), "seedance2pro_character" => Ok(Self::Seedance2ProCharacter), @@ -189,6 +195,7 @@ impl InferenceJobType { // NB: BTreeSet::from() isn't const, but not worth using LazyStatic, etc. BTreeSet::from([ Self::FalQueue, + Self::GmiCloudQueue, Self::BeebleQueue, Self::Seedance2ProQueue, Self::Seedance2ProCharacter, @@ -233,6 +240,7 @@ mod tests { #[test] fn test_serialization() { assert_serialization(InferenceJobType::FalQueue, "fal_queue"); + assert_serialization(InferenceJobType::GmiCloudQueue, "gmicloud_queue"); assert_serialization(InferenceJobType::BeebleQueue, "beeble_queue"); assert_serialization(InferenceJobType::Seedance2ProQueue, "seedance2pro_queue"); assert_serialization(InferenceJobType::Seedance2ProCharacter, "seedance2pro_character"); @@ -262,6 +270,7 @@ mod tests { #[test] fn to_str() { assert_eq!(InferenceJobType::FalQueue.to_str(), "fal_queue"); + assert_eq!(InferenceJobType::GmiCloudQueue.to_str(), "gmicloud_queue"); assert_eq!(InferenceJobType::BeebleQueue.to_str(), "beeble_queue"); assert_eq!(InferenceJobType::Seedance2ProQueue.to_str(), "seedance2pro_queue"); assert_eq!(InferenceJobType::Seedance2ProCharacter.to_str(), "seedance2pro_character"); @@ -291,6 +300,7 @@ mod tests { #[test] fn from_str() { assert_eq!(InferenceJobType::from_str("fal_queue").unwrap(), InferenceJobType::FalQueue); + assert_eq!(InferenceJobType::from_str("gmicloud_queue").unwrap(), InferenceJobType::GmiCloudQueue); assert_eq!(InferenceJobType::from_str("beeble_queue").unwrap(), InferenceJobType::BeebleQueue); assert_eq!(InferenceJobType::from_str("seedance2pro_queue").unwrap(), InferenceJobType::Seedance2ProQueue); assert_eq!(InferenceJobType::from_str("seedance2pro_character").unwrap(), InferenceJobType::Seedance2ProCharacter); @@ -320,7 +330,7 @@ mod tests { #[test] fn all_variants() { // Static check - const EXPECTED_COUNT : usize = 25; + const EXPECTED_COUNT : usize = 26; assert_eq!(InferenceJobType::all_variants().len(), EXPECTED_COUNT); assert_eq!(InferenceJobType::iter().len(), EXPECTED_COUNT); diff --git a/crates/schema/public/enums/src/common/generation/common_model_type.rs b/crates/schema/public/enums/src/common/generation/common_model_type.rs index 1d25d14f93..92bee40238 100644 --- a/crates/schema/public/enums/src/common/generation/common_model_type.rs +++ b/crates/schema/public/enums/src/common/generation/common_model_type.rs @@ -130,6 +130,10 @@ pub enum CommonModelType { Seedance2p0, #[serde(rename = "seedance_2p0_fast")] Seedance2p0Fast, + #[serde(rename = "seedance_2p0_g")] + Seedance2p0Global, + #[serde(rename = "seedance_2p0_fast_g")] + Seedance2p0FastGlobal, #[serde(rename = "sora_2")] Sora2, #[serde(rename = "sora_2_pro")] @@ -219,6 +223,8 @@ impl CommonModelType { Self::Seedance1p5Pro => "seedance_1p5_pro", Self::Seedance2p0 => "seedance_2p0", Self::Seedance2p0Fast => "seedance_2p0_fast", + Self::Seedance2p0Global => "seedance_2p0_g", + Self::Seedance2p0FastGlobal => "seedance_2p0_fast_g", Self::Sora2 => "sora_2", Self::Sora2Pro => "sora_2_pro", Self::Veo2 => "veo_2", @@ -289,6 +295,8 @@ impl CommonModelType { "seedance_1p5_pro" => Ok(Self::Seedance1p5Pro), "seedance_2p0" => Ok(Self::Seedance2p0), "seedance_2p0_fast" => Ok(Self::Seedance2p0Fast), + "seedance_2p0_g" => Ok(Self::Seedance2p0Global), + "seedance_2p0_fast_g" => Ok(Self::Seedance2p0FastGlobal), "sora_2" => Ok(Self::Sora2), "sora_2_pro" => Ok(Self::Sora2Pro), "veo_2" => Ok(Self::Veo2), @@ -363,6 +371,8 @@ impl CommonModelType { Self::Seedance1p5Pro, Self::Seedance2p0, Self::Seedance2p0Fast, + Self::Seedance2p0Global, + Self::Seedance2p0FastGlobal, Self::Sora2, Self::Sora2Pro, Self::Veo2, @@ -434,6 +444,8 @@ impl CommonModelType { Self::Seedance1p5Pro => CommonModelClass::Video, Self::Seedance2p0 => CommonModelClass::Video, Self::Seedance2p0Fast => CommonModelClass::Video, + Self::Seedance2p0Global => CommonModelClass::Video, + Self::Seedance2p0FastGlobal => CommonModelClass::Video, Self::Sora2 => CommonModelClass::Video, Self::Sora2Pro => CommonModelClass::Video, Self::Veo2 => CommonModelClass::Video, @@ -514,6 +526,8 @@ mod tests { assert_serialization(CommonModelType::Seedance1p5Pro, "seedance_1p5_pro"); assert_serialization(CommonModelType::Seedance2p0, "seedance_2p0"); assert_serialization(CommonModelType::Seedance2p0Fast, "seedance_2p0_fast"); + assert_serialization(CommonModelType::Seedance2p0Global, "seedance_2p0_g"); + assert_serialization(CommonModelType::Seedance2p0FastGlobal, "seedance_2p0_fast_g"); assert_serialization(CommonModelType::Sora2, "sora_2"); assert_serialization(CommonModelType::Sora2Pro, "sora_2_pro"); assert_serialization(CommonModelType::Veo2, "veo_2"); @@ -581,6 +595,8 @@ mod tests { assert_eq!(CommonModelType::Seedance1p5Pro.to_str(), "seedance_1p5_pro"); assert_eq!(CommonModelType::Seedance2p0.to_str(), "seedance_2p0"); assert_eq!(CommonModelType::Seedance2p0Fast.to_str(), "seedance_2p0_fast"); + assert_eq!(CommonModelType::Seedance2p0Global.to_str(), "seedance_2p0_g"); + assert_eq!(CommonModelType::Seedance2p0FastGlobal.to_str(), "seedance_2p0_fast_g"); assert_eq!(CommonModelType::Sora2.to_str(), "sora_2"); assert_eq!(CommonModelType::Sora2Pro.to_str(), "sora_2_pro"); assert_eq!(CommonModelType::Veo2.to_str(), "veo_2"); @@ -648,6 +664,8 @@ mod tests { assert_eq!(CommonModelType::from_str("seedance_1p5_pro").unwrap(), CommonModelType::Seedance1p5Pro); assert_eq!(CommonModelType::from_str("seedance_2p0").unwrap(), CommonModelType::Seedance2p0); assert_eq!(CommonModelType::from_str("seedance_2p0_fast").unwrap(), CommonModelType::Seedance2p0Fast); + assert_eq!(CommonModelType::from_str("seedance_2p0_g").unwrap(), CommonModelType::Seedance2p0Global); + assert_eq!(CommonModelType::from_str("seedance_2p0_fast_g").unwrap(), CommonModelType::Seedance2p0FastGlobal); assert_eq!(CommonModelType::from_str("sora_2").unwrap(), CommonModelType::Sora2); assert_eq!(CommonModelType::from_str("sora_2_pro").unwrap(), CommonModelType::Sora2Pro); assert_eq!(CommonModelType::from_str("veo_2").unwrap(), CommonModelType::Veo2); @@ -669,7 +687,7 @@ mod tests { #[test] fn all_variants() { let mut variants = CommonModelType::all_variants(); - assert_eq!(variants.len(), 58); + assert_eq!(variants.len(), 60); // Image models assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Dev)); assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Schnell)); @@ -717,6 +735,8 @@ mod tests { assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance1p5Pro)); assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0)); assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0Fast)); + assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0Global)); + assert_eq!(variants.pop_first(), Some(CommonModelType::Seedance2p0FastGlobal)); assert_eq!(variants.pop_first(), Some(CommonModelType::Sora2)); assert_eq!(variants.pop_first(), Some(CommonModelType::Sora2Pro)); assert_eq!(variants.pop_first(), Some(CommonModelType::Veo2)); diff --git a/crates/schema/public/enums/src/common/generation/common_video_model.rs b/crates/schema/public/enums/src/common/generation/common_video_model.rs index 0a64ad6234..1cacd7ec54 100644 --- a/crates/schema/public/enums/src/common/generation/common_video_model.rs +++ b/crates/schema/public/enums/src/common/generation/common_video_model.rs @@ -44,6 +44,12 @@ pub enum CommonVideoModel { #[serde(rename = "seedance_2p0_fast")] Seedance2p0Fast, + #[serde(rename = "seedance_2p0_g")] + Seedance2p0Global, + + #[serde(rename = "seedance_2p0_fast_g")] + Seedance2p0FastGlobal, + #[serde(rename = "sora_2")] Sora2, @@ -83,6 +89,8 @@ impl CommonVideoModel { Self::Seedance1p5Pro => CommonModelType::Seedance1p5Pro, Self::Seedance2p0 => CommonModelType::Seedance2p0, Self::Seedance2p0Fast => CommonModelType::Seedance2p0Fast, + Self::Seedance2p0Global => CommonModelType::Seedance2p0Global, + Self::Seedance2p0FastGlobal => CommonModelType::Seedance2p0FastGlobal, Self::Sora2 => CommonModelType::Sora2, Self::Sora2Pro => CommonModelType::Sora2Pro, Self::Veo2 => CommonModelType::Veo2, @@ -115,6 +123,8 @@ mod tests { assert_serialization(CommonVideoModel::Seedance1p5Pro, "seedance_1p5_pro"); assert_serialization(CommonVideoModel::Seedance2p0, "seedance_2p0"); assert_serialization(CommonVideoModel::Seedance2p0Fast, "seedance_2p0_fast"); + assert_serialization(CommonVideoModel::Seedance2p0Global, "seedance_2p0_g"); + assert_serialization(CommonVideoModel::Seedance2p0FastGlobal, "seedance_2p0_fast_g"); assert_serialization(CommonVideoModel::Sora2, "sora_2"); assert_serialization(CommonVideoModel::Sora2Pro, "sora_2_pro"); assert_serialization(CommonVideoModel::Veo2, "veo_2"); @@ -140,6 +150,8 @@ mod tests { ("seedance_1p5_pro", CommonVideoModel::Seedance1p5Pro), ("seedance_2p0", CommonVideoModel::Seedance2p0), ("seedance_2p0_fast", CommonVideoModel::Seedance2p0Fast), + ("seedance_2p0_g", CommonVideoModel::Seedance2p0Global), + ("seedance_2p0_fast_g", CommonVideoModel::Seedance2p0FastGlobal), ("sora_2", CommonVideoModel::Sora2), ("sora_2_pro", CommonVideoModel::Sora2Pro), ("veo_2", CommonVideoModel::Veo2), @@ -172,6 +184,8 @@ mod tests { CommonVideoModel::Seedance1p5Pro, CommonVideoModel::Seedance2p0, CommonVideoModel::Seedance2p0Fast, + CommonVideoModel::Seedance2p0Global, + CommonVideoModel::Seedance2p0FastGlobal, CommonVideoModel::Sora2, CommonVideoModel::Sora2Pro, CommonVideoModel::Veo2, @@ -203,6 +217,8 @@ mod tests { (CommonVideoModel::Seedance1p5Pro, CommonModelType::Seedance1p5Pro), (CommonVideoModel::Seedance2p0, CommonModelType::Seedance2p0), (CommonVideoModel::Seedance2p0Fast, CommonModelType::Seedance2p0Fast), + (CommonVideoModel::Seedance2p0Global, CommonModelType::Seedance2p0Global), + (CommonVideoModel::Seedance2p0FastGlobal, CommonModelType::Seedance2p0FastGlobal), (CommonVideoModel::Sora2, CommonModelType::Sora2), (CommonVideoModel::Sora2Pro, CommonModelType::Sora2Pro), (CommonVideoModel::Veo2, CommonModelType::Veo2), diff --git a/crates/service/web/storyteller_web/CLAUDE.md b/crates/service/web/storyteller_web/CLAUDE.md index 4cfc19b8d8..9fc47c4e87 100644 --- a/crates/service/web/storyteller_web/CLAUDE.md +++ b/crates/service/web/storyteller_web/CLAUDE.md @@ -36,6 +36,65 @@ which reuses an existing DB connection. When adding new request/response/error types, add them to `src/docs/api_doc.rs` in the schemas section (alphabetically sorted). Types from `artcraft_api_defs` need explicit imports. +## Running locally + +Environment variables are auto-loaded by Rust at startup from these files: + +- `.env` (repo root) +- `crates/service/web/storyteller_web/config/storyteller-web.common.env` +- `crates/service/web/storyteller_web/config/storyteller-web.development.env` +- `crates/service/web/storyteller_web/config/storyteller-web.development-secrets.env` + +**Do NOT** use `SQLX_OFFLINE=true` — the server needs live DB access. + +```bash +# From the repo root, just run: +cargo run --bin storyteller-web +``` + +The server binds to `0.0.0.0:12345` by default. Compilation takes ~3 minutes on first build. + +## Testing endpoints with curl + +```bash +# 1. Log in to get session cookies: +curl -s -c /tmp/cookies.txt -X POST http://localhost:12345/v1/login \ + -H "Content-Type: application/json" \ + -d '{"username_or_email":"newuser4","password":"newuser4"}' + +# 2. Generate video (GmiCloud Seedance 2.0 Fast Global): +# Requires a UUID-format idempotency_token. +curl -s -b /tmp/cookies.txt -X POST http://localhost:12345/v1/omni_gen/generate/video \ + -H "Content-Type: application/json" \ + -d '{ + "model": "seedance_2p0_fast_g", + "prompt": "a corgi running through a field", + "duration_seconds": 5, + "aspect_ratio": "wide_sixteen_by_nine", + "idempotency_token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + }' + +# 3. Generate video (GmiCloud Seedance 2.0 Global, 720p): +curl -s -b /tmp/cookies.txt -X POST http://localhost:12345/v1/omni_gen/generate/video \ + -H "Content-Type: application/json" \ + -d '{ + "model": "seedance_2p0_g", + "prompt": "a corgi running through a field", + "duration_seconds": 5, + "resolution": "seven_twenty_p", + "aspect_ratio": "wide_sixteen_by_nine", + "idempotency_token": "b2c3d4e5-f6a7-8901-bcde-f12345678901" + }' +``` + +A successful response returns 200 with `{"success": true, "inference_job_token": "jinf_..."}`. + +**Notes:** +- `idempotency_token` must be UUID format (32 hex chars or 36 with dashes) +- `resolution` uses enum names: `four_eighty_p`, `seven_twenty_p`, `ten_eighty_p` +- `aspect_ratio` uses enum names: `wide_sixteen_by_nine`, `tall_nine_by_sixteen`, `square`, etc. +- Use a new `idempotency_token` for each request to avoid duplicate detection + ## Pager Integration - The `Pager` is available on `ServerState` as `server_state.pager` diff --git a/crates/service/web/storyteller_web/Cargo.toml b/crates/service/web/storyteller_web/Cargo.toml index ad879b10f8..09e6840755 100644 --- a/crates/service/web/storyteller_web/Cargo.toml +++ b/crates/service/web/storyteller_web/Cargo.toml @@ -54,6 +54,7 @@ beeble_client.workspace = true error_utils.workspace = true errors.workspace = true fal_client.workspace = true +gmicloud_client.workspace = true filesys.workspace = true google_sign_in = { path = "../../../api_clients/google_sign_in" } hashing = { path = "../../../lib/files/hashing" } diff --git a/crates/service/web/storyteller_web/src/configs/omni_gen/video_models.rs b/crates/service/web/storyteller_web/src/configs/omni_gen/video_models.rs index bb8c4c42c7..104ae1a45e 100644 --- a/crates/service/web/storyteller_web/src/configs/omni_gen/video_models.rs +++ b/crates/service/web/storyteller_web/src/configs/omni_gen/video_models.rs @@ -155,6 +155,85 @@ fn build_omni_gen_video_models() -> Vec { ..Default::default() }); +// models.push(OmniGenVideoModelDetails { +// model: CommonVideoModel::Seedance2p0Global, +// model_creator: Some(ModelCreator::Bytedance), +// full_name: Some("Seedance 2.0 (Global)".to_string()), +// text_prompt_supported: Some(true), +// starting_keyframe_supported: Some(true), +// ending_keyframe_supported: Some(true), +// image_references_supported: Some(true), +// image_references_max: Some(9), +// audio_references_supported: Some(true), +// audio_references_max: Some(3), +// audio_references_max_total_duration_seconds: Some(15), +// video_references_supported: Some(true), +// video_references_max: Some(3), +// video_references_max_total_duration_seconds: Some(15), +// character_references_supported: Some(true), +// character_references_max: Some(9), +// aspect_ratio_options: Some(vec![ +// CommonAspectRatio::WideTwentyOneByNine, +// CommonAspectRatio::WideSixteenByNine, +// CommonAspectRatio::WideFourByThree, +// CommonAspectRatio::Square, +// CommonAspectRatio::TallThreeByFour, +// CommonAspectRatio::TallNineBySixteen, +// ]), +// aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine), +// resolution_options: Some(vec![ +// CommonResolution::FourEightyP, +// CommonResolution::SevenTwentyP, +// CommonResolution::TenEightyP, +// ]), +// resolution_default: Some(CommonResolution::SevenTwentyP), +// batch_size_options: Some(vec![1, 2, 4]), +// batch_size_default: Some(1), +// duration_seconds_min: Some(4), +// duration_seconds_max: Some(15), +// duration_seconds_default: Some(5), +// ..Default::default() +// }); +// +// models.push(OmniGenVideoModelDetails { +// model: CommonVideoModel::Seedance2p0FastGlobal, +// model_creator: Some(ModelCreator::Bytedance), +// full_name: Some("Seedance 2.0 Fast (Global)".to_string()), +// text_prompt_supported: Some(true), +// starting_keyframe_supported: Some(true), +// ending_keyframe_supported: Some(true), +// image_references_supported: Some(true), +// image_references_max: Some(9), +// audio_references_supported: Some(true), +// audio_references_max: Some(3), +// audio_references_max_total_duration_seconds: Some(15), +// video_references_supported: Some(true), +// video_references_max: Some(3), +// video_references_max_total_duration_seconds: Some(15), +// character_references_supported: Some(true), +// character_references_max: Some(9), +// aspect_ratio_options: Some(vec![ +// CommonAspectRatio::WideTwentyOneByNine, +// CommonAspectRatio::WideSixteenByNine, +// CommonAspectRatio::WideFourByThree, +// CommonAspectRatio::Square, +// CommonAspectRatio::TallThreeByFour, +// CommonAspectRatio::TallNineBySixteen, +// ]), +// aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine), +// resolution_options: Some(vec![ +// CommonResolution::FourEightyP, +// CommonResolution::SevenTwentyP, +// ]), +// resolution_default: Some(CommonResolution::SevenTwentyP), +// batch_size_options: Some(vec![1, 2, 4]), +// batch_size_default: Some(1), +// duration_seconds_min: Some(4), +// duration_seconds_max: Some(15), +// duration_seconds_default: Some(5), +// ..Default::default() +// }); + // TODO(bt,2026-04-10): Veo 2 image-to-video doesn't support aspect ratio models.push(OmniGenVideoModelDetails { is_disabled: Some(true), // TODO: Temporarily disable diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/video/estimate_video_cost_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/video/estimate_video_cost_handler.rs index 3fa758cacf..7a89620b4c 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/video/estimate_video_cost_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/video/estimate_video_cost_handler.rs @@ -141,6 +141,8 @@ fn map_video_model(model: CommonVideoModel) -> Result RouterVideoModel::Seedance1p5Pro, CommonVideoModel::Seedance2p0 => RouterVideoModel::Seedance2p0, CommonVideoModel::Seedance2p0Fast => RouterVideoModel::Seedance2p0Fast, + CommonVideoModel::Seedance2p0Global => RouterVideoModel::Seedance2p0Global, + CommonVideoModel::Seedance2p0FastGlobal => RouterVideoModel::Seedance2p0FastGlobal, CommonVideoModel::HappyHorse1p0 => RouterVideoModel::HappyHorse1p0, CommonVideoModel::Sora2 => RouterVideoModel::Sora2, CommonVideoModel::Sora2Pro => RouterVideoModel::Sora2Pro, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/helpers/build_router_client.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/helpers/build_router_client.rs index b3d6d64c6a..ebeaf476eb 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/helpers/build_router_client.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/helpers/build_router_client.rs @@ -3,6 +3,7 @@ use artcraft_router::api::provider::Provider; use artcraft_router::client::router_client::RouterClient; use artcraft_router::client::router_fal_client::RouterFalClient; +use artcraft_router::client::router_gmicloud_client::RouterGmiCloudClient; use artcraft_router::client::router_seedance2pro_client::RouterSeedance2ProClient; use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession; @@ -27,6 +28,11 @@ pub fn build_router_client( ); Ok(RouterClient::Fal(fal_client)) } + Provider::GmiCloud => { + Ok(RouterClient::GmiCloud(RouterGmiCloudClient::new( + server_state.gmicloud.api_key.clone(), + ))) + } other => { Err(AdvancedCommonWebError::server_error_with_message( &format!("Unsupported provider for video generation: {:?}", other), diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/insert_db_job/insert_gmicloud_job.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/insert_db_job/insert_gmicloud_job.rs new file mode 100644 index 0000000000..af8dfe0d35 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/insert_db_job/insert_gmicloud_job.rs @@ -0,0 +1,45 @@ +use log::warn; + +use mysql_queries::queries::generic_inference::gmicloud::insert_generic_inference_job_for_gmicloud::{ + insert_generic_inference_job_for_gmicloud, InsertGmiCloudInferenceJobArgs, +}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; + +use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; + +use super::shared_job_args::SharedJobArgs; + +pub struct InsertGmiCloudJobArgs<'a, 'tx> { + pub external_request_id: &'a str, + pub shared: SharedJobArgs<'a, 'tx>, +} + +pub async fn insert_gmicloud_job(args: InsertGmiCloudJobArgs<'_, '_>) -> Result { + let InsertGmiCloudJobArgs { + external_request_id, + shared, + } = args; + + let db_result = insert_generic_inference_job_for_gmicloud( + InsertGmiCloudInferenceJobArgs { + apriori_job_token: shared.apriori_job_token, + uuid_idempotency_token: shared.idempotency_token, + external_request_id, + maybe_prompt_token: shared.maybe_prompt_token, + maybe_creator_user_token: Some(shared.user_token), + maybe_avt_token: shared.maybe_avt_token, + creator_ip_address: shared.ip_address, + maybe_debug_log_event_token: shared.maybe_debug_log_event_token, + mysql_executor: &mut **shared.transaction, + phantom: Default::default(), + } + ).await; + + match db_result { + Ok(token) => Ok(token), + Err(err) => { + warn!("Error inserting GmiCloud inference job: {:?}", err); + Err(AdvancedCommonWebError::from_error(err)) + } + } +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/insert_db_job/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/insert_db_job/mod.rs index 751cbf87e7..c54f8ae75d 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/insert_db_job/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/insert_db_job/mod.rs @@ -1,3 +1,4 @@ pub mod insert_fal_job; +pub mod insert_gmicloud_job; pub mod insert_seedance2pro_jobs; pub mod shared_job_args; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs index fc7bc42780..cc74308ff8 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs @@ -33,6 +33,7 @@ use crate::http_server::common_responses::advanced_common_web_error::AdvancedCom use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test; use crate::http_server::endpoints::omni_gen::generate::video::helpers::hydrate_router_request::hydrate_to_router_request; use crate::http_server::endpoints::omni_gen::generate::video::insert_db_job::insert_fal_job::{insert_fal_job, InsertFalJobArgs}; +use crate::http_server::endpoints::omni_gen::generate::video::insert_db_job::insert_gmicloud_job::{insert_gmicloud_job, InsertGmiCloudJobArgs}; use crate::http_server::endpoints::omni_gen::generate::video::insert_db_job::insert_seedance2pro_jobs::{insert_seedance2pro_jobs, InsertSeedance2proJobsArgs}; use crate::http_server::endpoints::omni_gen::generate::video::insert_db_job::shared_job_args::SharedJobArgs; use crate::http_server::endpoints::omni_gen::generate::video::pipeline_v1::run_pipeline_v1::{run_pipeline_v1, RunPipelineV1Args}; @@ -186,6 +187,8 @@ pub async fn omni_gen_video_generate_handler( Some(CommonVideoModel::HappyHorse1p0) => true, Some(CommonVideoModel::Seedance2p0) => true, Some(CommonVideoModel::Seedance2p0Fast) => true, + Some(CommonVideoModel::Seedance2p0Global) => true, + Some(CommonVideoModel::Seedance2p0FastGlobal) => true, _ => false, }; @@ -377,6 +380,27 @@ pub async fn omni_gen_video_generate_handler( vec![payload.inference_job_token.clone()], ) } + GenerateVideoResponse::GmiCloud(payload) => { + info!("Inserting GmiCloud job with token: {:?}", pipeline_result.billing.apriori_job_token); + let token = insert_gmicloud_job(InsertGmiCloudJobArgs { + external_request_id: &payload.request_id, + shared: SharedJobArgs { + apriori_job_token: &pipeline_result.billing.apriori_job_token, + idempotency_token: &idempotency_token, + user_token, + maybe_avt_token: maybe_avt_token.as_ref(), + maybe_prompt_token: prompt_token.as_ref(), + maybe_debug_log_event_token: Some(&debug_log_event_token), + ip_address: &ip_address, + transaction: &mut transaction, + }, + }).await?; + + ( + token.clone(), + vec![token], + ) + } other => { error!("Unexpected generation response variant: {:?}", other); return Err(AdvancedCommonWebError::server_error_with_message("Unexpected generation response")); diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/pipeline_v2/run_pipeline_v2.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/pipeline_v2/run_pipeline_v2.rs index b1848d1b1b..97837e7216 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/pipeline_v2/run_pipeline_v2.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/pipeline_v2/run_pipeline_v2.rs @@ -42,6 +42,8 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result Provider::Seedance2Pro, CommonVideoModel::Seedance2p0 => Provider::Seedance2Pro, CommonVideoModel::Seedance2p0Fast => Provider::Seedance2Pro, + CommonVideoModel::Seedance2p0Global => Provider::GmiCloud, + CommonVideoModel::Seedance2p0FastGlobal => Provider::GmiCloud, _ => Provider::Fal, }; @@ -55,22 +57,34 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result AnyhowResult<()> { let resend_api_key = easyenv::get_env_string_required("RESEND_API_KEY")?; + let gmicloud_api_key = easyenv::get_env_string_required("GMICLOUD_API_KEY")?; + let worldlabs_api_key = easyenv::get_env_string_required("WORLDLABS_API_KEY")?; let startup_time = Utc::now(); @@ -485,6 +487,9 @@ async fn main() -> AnyhowResult<()> { api_key: fal_api_key, webhook_url: fal_webhook_url, }, + gmicloud: GmiCloudData { + api_key: gmicloud_client::creds::gmicloud_api_key::GmiCloudApiKey::new(gmicloud_api_key), + }, seedance2pro: Seedance2ProData { cookies: easyenv::get_env_string_required("SEEDANCE2PRO_COOKIES")?, cookies_whitelist: easyenv::get_env_string_required("SEEDANCE2PRO_WHITELIST_COOKIES")?, diff --git a/crates/service/web/storyteller_web/src/state/server_state.rs b/crates/service/web/storyteller_web/src/state/server_state.rs index f2babed120..625a119825 100644 --- a/crates/service/web/storyteller_web/src/state/server_state.rs +++ b/crates/service/web/storyteller_web/src/state/server_state.rs @@ -96,6 +96,8 @@ pub struct ServerState { pub fal: FalData, + pub gmicloud: GmiCloudData, + pub beeble: BeebleData, pub seedance2pro: Seedance2ProData, @@ -324,6 +326,12 @@ pub struct FalData { pub webhook_url: String, } +/// GmiCloud integration +#[derive(Clone)] +pub struct GmiCloudData { + pub api_key: gmicloud_client::creds::gmicloud_api_key::GmiCloudApiKey, +} + /// Beeble SwitchX integration #[derive(Clone)] pub struct BeebleData {