diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/estimate_video_cost_artcraft_seedance2p0.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/estimate_video_cost_artcraft_seedance2p0.rs index 78db3ed871..9ad29304d5 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/estimate_video_cost_artcraft_seedance2p0.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/artcraft/estimate_video_cost_artcraft_seedance2p0.rs @@ -34,6 +34,7 @@ pub (crate) fn estimate_video_cost_artcraft_seedance2p0( reference_video_urls: None, reference_audio_urls: None, use_face_blur_hack: None, + host_override: None, }; let cost_in_usd_cents = args.estimate_cost_in_usd_cents(); diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/seedance2pro/estimate_video_cost_seedance2pro_seedance2p0.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/seedance2pro/estimate_video_cost_seedance2pro_seedance2p0.rs index e5089850c7..2876d74fa4 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/seedance2pro/estimate_video_cost_seedance2pro_seedance2p0.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/cost/seedance2pro/estimate_video_cost_seedance2pro_seedance2p0.rs @@ -22,6 +22,7 @@ pub(crate) fn estimate_video_cost_seedance2pro_seedance2p0( reference_video_urls: None, reference_audio_urls: None, use_face_blur_hack: None, + host_override: None, }; let cost_in_credits = args.estimate_credits(); diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/seedance2pro/execute_seedance2pro_seedance2p0.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/seedance2pro/execute_seedance2pro_seedance2p0.rs index bb2273fe0f..c67c08c392 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/seedance2pro/execute_seedance2pro_seedance2p0.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/execute/seedance2pro/execute_seedance2pro_seedance2p0.rs @@ -44,6 +44,7 @@ pub async fn execute_seedance2pro_seedance2p0( reference_video_urls, reference_audio_urls, use_face_blur_hack: None, + host_override: None, }; let response = generate_video(args) @@ -70,6 +71,7 @@ async fn upload_to_seedance2pro( let prepare_response = prepare_file_upload(PrepareFileUploadArgs { session, extension, + host_override: None, }) .await .map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?; @@ -77,6 +79,7 @@ async fn upload_to_seedance2pro( let upload_response = upload_file(UploadFileArgs { upload_url: prepare_response.upload_url, file_bytes, + host_override: None, }) .await .map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?; diff --git a/crates/api_clients/seedance2pro_client/src/requests/generate_video/generate_video.rs b/crates/api_clients/seedance2pro_client/src/requests/generate_video/generate_video.rs index a7ac9d2a09..169e82f538 100644 --- a/crates/api_clients/seedance2pro_client/src/requests/generate_video/generate_video.rs +++ b/crates/api_clients/seedance2pro_client/src/requests/generate_video/generate_video.rs @@ -4,13 +4,12 @@ use crate::error::seedance2pro_error::Seedance2ProError; use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError; use crate::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError; use crate::requests::generate_video::request_types::*; +use crate::requests::kinovi_host::{KinoviHost, resolve_host}; use crate::utils::common_headers::FIREFOX_USER_AGENT; use log::info; use wreq::Client; use wreq_util::Emulation; -const RUN_TASK_URL: &str = "https://seedance2-pro.com/api/trpc/workflow.runTask?batch=1"; - // --- Request args --- pub struct GenerateVideoArgs<'a> { @@ -48,6 +47,9 @@ pub struct GenerateVideoArgs<'a> { /// Controls the `faceBlurMode` field: true sends "on", false sends "off", None omits it. pub use_face_blur_hack: Option, + + /// Override the default host (kinovi.ai). + pub host_override: Option, } impl std::fmt::Debug for GenerateVideoArgs<'_> { @@ -63,6 +65,7 @@ impl std::fmt::Debug for GenerateVideoArgs<'_> { .field("reference_video_urls", &self.reference_video_urls) .field("reference_audio_urls", &self.reference_audio_urls) .field("use_face_blur_hack", &self.use_face_blur_hack) + .field("host_override", &self.host_override) .finish() } } @@ -158,6 +161,9 @@ pub struct GenerateVideoResponse { // --- Implementation --- pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result { + let host = resolve_host(args.host_override.as_ref()); + let base_url = host.api_base_url(); + let run_task_url = format!("{}/api/trpc/workflow.runTask?batch=1", base_url); info!("Requesting video from Seedance2Pro: {:?}", args); @@ -240,15 +246,17 @@ pub async fn generate_video(args: GenerateVideoArgs<'_>) -> Result Self { + Self::Kinovi + } +} + +impl KinoviHost { + /// Returns the API base URL (scheme + domain, no trailing slash). + pub fn api_base_url(&self) -> &str { + match self { + Self::Kinovi => "https://kinovi.ai", + Self::Seedance2Pro => "https://seedance2-pro.com", + Self::CustomHost { api_host, .. } => api_host.as_str(), + } + } + + /// Returns the CDN base URL for uploaded/static files (no trailing slash). + pub fn cdn_base_url(&self) -> &str { + match self { + Self::Kinovi => "https://static.kinovi.ai", + Self::Seedance2Pro => "https://static.seedance2-pro.com", + Self::CustomHost { cdn_host, .. } => cdn_host.as_str(), + } + } +} + +/// Resolves an optional host override to the effective host. +pub fn resolve_host(host_override: Option<&KinoviHost>) -> &KinoviHost { + // Use a static default to avoid needing to return owned data + static DEFAULT: KinoviHost = KinoviHost::Kinovi; + host_override.unwrap_or(&DEFAULT) +} diff --git a/crates/api_clients/seedance2pro_client/src/requests/mod.rs b/crates/api_clients/seedance2pro_client/src/requests/mod.rs index c335075452..1db298b5f5 100644 --- a/crates/api_clients/seedance2pro_client/src/requests/mod.rs +++ b/crates/api_clients/seedance2pro_client/src/requests/mod.rs @@ -1,4 +1,5 @@ pub mod generate_video; +pub mod kinovi_host; pub mod poll_orders; pub mod prepare_file_upload; pub mod upload_file; diff --git a/crates/api_clients/seedance2pro_client/src/requests/poll_orders/poll_orders.rs b/crates/api_clients/seedance2pro_client/src/requests/poll_orders/poll_orders.rs index d1af7b8b42..8c97f330cd 100644 --- a/crates/api_clients/seedance2pro_client/src/requests/poll_orders/poll_orders.rs +++ b/crates/api_clients/seedance2pro_client/src/requests/poll_orders/poll_orders.rs @@ -2,6 +2,7 @@ use crate::creds::seedance2pro_session::Seedance2ProSession; use crate::error::seedance2pro_client_error::Seedance2ProClientError; use crate::error::seedance2pro_error::Seedance2ProError; use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError; +use crate::requests::kinovi_host::{KinoviHost, resolve_host}; use crate::requests::poll_orders::failure_reason::FailureReason; use crate::requests::poll_orders::request_types::*; use crate::utils::common_headers::FIREFOX_USER_AGENT; @@ -9,8 +10,6 @@ use log::info; use wreq::Client; use wreq_util::Emulation; -const GET_ORDERS_BASE_URL: &str = "https://seedance2-pro.com/api/trpc/userOrder.getOrders"; - // --- Args & response --- @@ -20,6 +19,9 @@ pub struct PollOrdersArgs<'a> { /// Optional cursor from a previous `PollOrdersResponse::next_cursor`. /// When `None`, the most recent orders are returned. pub cursor: Option, + + /// Override the default host (kinovi.ai). + pub host_override: Option, } pub struct PollOrdersResponse { @@ -97,6 +99,10 @@ pub struct OrderStatus { // --- Implementation --- pub async fn poll_orders(args: PollOrdersArgs<'_>) -> Result { + let host = resolve_host(args.host_override.as_ref()); + let base_url = host.api_base_url(); + let get_orders_url = format!("{}/api/trpc/userOrder.getOrders", base_url); + info!("Polling orders (cursor: {:?})...", args.cursor); let input_json = build_input_json(args.cursor); @@ -107,14 +113,15 @@ pub async fn poll_orders(args: PollOrdersArgs<'_>) -> Result) -> Result) -> String { match cursor { - None => r#"{"0":{"json":{"limit":30,"format":null,"direction":"forward"},"meta":{"values":{"format":["undefined"]},"v":1}}}"#.to_string(), + None => r#"{"0":{"json":{"limit":30,"format":null,"toolId":null,"direction":"forward"},"meta":{"values":{"format":["undefined"],"toolId":["undefined"]},"v":1}}}"#.to_string(), Some(c) => format!( - r#"{{"0":{{"json":{{"limit":30,"format":null,"cursor":{cursor},"direction":"forward"}},"meta":{{"values":{{"format":["undefined"]}},"v":1}}}}}}"#, + r#"{{"0":{{"json":{{"limit":30,"format":null,"toolId":null,"cursor":{cursor},"direction":"forward"}},"meta":{{"values":{{"format":["undefined"],"toolId":["undefined"]}},"v":1}}}}}}"#, cursor = c ), } @@ -220,7 +227,7 @@ mod tests { async fn test_poll_all_orders() -> AnyhowResult<()> { setup_test_logging(LevelFilter::Trace); let session = test_session()?; - let result = poll_orders(PollOrdersArgs { session: &session, cursor: None }).await?; + let result = poll_orders(PollOrdersArgs { session: &session, cursor: None, host_override: None }).await?; println!("Orders returned: {}", result.orders.len()); println!("Next cursor: {:?}", result.next_cursor); for order in &result.orders { @@ -238,7 +245,7 @@ mod tests { let session = test_session()?; // Use the cursor value returned from a prior call (e.g. 394062 from the example responses). let cursor: u64 = 394062; - let result = poll_orders(PollOrdersArgs { session: &session, cursor: Some(cursor) }).await?; + let result = poll_orders(PollOrdersArgs { session: &session, cursor: Some(cursor), host_override: None }).await?; println!("Orders returned (with cursor {}): {}", cursor, result.orders.len()); println!("Next cursor: {:?}", result.next_cursor); for order in &result.orders { @@ -261,7 +268,7 @@ mod tests { loop { page += 1; - let result = poll_orders(PollOrdersArgs { session: &session, cursor }).await?; + let result = poll_orders(PollOrdersArgs { session: &session, cursor, host_override: None }).await?; let page_count = result.orders.len(); total_orders += page_count; diff --git a/crates/api_clients/seedance2pro_client/src/requests/prepare_file_upload/prepare_file_upload.rs b/crates/api_clients/seedance2pro_client/src/requests/prepare_file_upload/prepare_file_upload.rs index 32a0b34325..7bd5a54dfd 100644 --- a/crates/api_clients/seedance2pro_client/src/requests/prepare_file_upload/prepare_file_upload.rs +++ b/crates/api_clients/seedance2pro_client/src/requests/prepare_file_upload/prepare_file_upload.rs @@ -2,6 +2,7 @@ use crate::creds::seedance2pro_session::Seedance2ProSession; use crate::error::seedance2pro_client_error::Seedance2ProClientError; use crate::error::seedance2pro_error::Seedance2ProError; use crate::error::seedance2pro_generic_api_error::Seedance2ProGenericApiError; +use crate::requests::kinovi_host::{KinoviHost, resolve_host}; use crate::requests::prepare_file_upload::request_types::*; use crate::utils::common_headers::FIREFOX_USER_AGENT; use chrono::Utc; @@ -10,8 +11,6 @@ use rand::Rng; use wreq::Client; use wreq_util::Emulation; -const SIGNED_UPLOAD_URL: &str = "https://seedance2-pro.com/api/trpc/uploads.signedUploadUrl?batch=1"; - /// Generates a material path based on the current time. /// Format: `materials/YYYYMMDD/-.` fn generate_material_path(extension: &str) -> String { @@ -28,6 +27,9 @@ pub struct PrepareFileUploadArgs<'a> { /// File extension without the dot, e.g. "png", "mp4", "mp3". pub extension: String, + + /// Override the default host (kinovi.ai). + pub host_override: Option, } pub struct PrepareFileUploadResponse { @@ -39,6 +41,10 @@ pub struct PrepareFileUploadResponse { } pub async fn prepare_file_upload(args: PrepareFileUploadArgs<'_>) -> Result { + let host = resolve_host(args.host_override.as_ref()); + let base_url = host.api_base_url(); + let signed_upload_url = format!("{}/api/trpc/uploads.signedUploadUrl?batch=1", base_url); + let material_path = generate_material_path(&args.extension); info!("Preparing file upload with path: {}", material_path); @@ -57,16 +63,17 @@ pub async fn prepare_file_upload(args: PrepareFileUploadArgs<'_>) -> Result, + + /// Override the default host (kinovi.ai). + pub host_override: Option, } pub struct UploadFileResponse { @@ -25,15 +27,18 @@ pub struct UploadFileResponse { /// Extracts the path from the R2 upload URL and builds the static public URL. /// e.g. `https://comm.….r2.cloudflarestorage.com/materials/20260219/…?X-Amz-…` -/// -> `https://static.seedance2-pro.com/materials/20260219/…` -fn build_public_url(upload_url: &str) -> Result { +/// -> `https://static.kinovi.ai/materials/20260219/…` +fn build_public_url(upload_url: &str, host: &KinoviHost) -> Result { let parsed = Url::parse(upload_url) .map_err(|err| Seedance2ProClientError::UrlParseError(err))?; let path = parsed.path(); // e.g. "/materials/20260219/1771463564512-b14bfe90.png" - Ok(format!("{}{}", STATIC_BASE_URL, path)) + Ok(format!("{}{}", host.cdn_base_url(), path)) } pub async fn upload_file(args: UploadFileArgs) -> Result { + let host = resolve_host(args.host_override.as_ref()); + let base_url = host.api_base_url(); + info!("Uploading file to: {}", args.upload_url); let client = Client::builder() @@ -41,13 +46,15 @@ pub async fn upload_file(args: UploadFileArgs) -> Result Result anyhow::Result<()> { let result = poll_orders(PollOrdersArgs { session: &session, cursor, + host_override: None, }).await .map_err(|err| anyhow!("Error polling orders on page {}: {:?}", page, err))?; diff --git a/crates/cli/production/support-tool/src/commands/seedance2pro/subcommands/find_job.rs b/crates/cli/production/support-tool/src/commands/seedance2pro/subcommands/find_job.rs index c779fb768b..44a9ae50dc 100644 --- a/crates/cli/production/support-tool/src/commands/seedance2pro/subcommands/find_job.rs +++ b/crates/cli/production/support-tool/src/commands/seedance2pro/subcommands/find_job.rs @@ -26,6 +26,7 @@ pub async fn run(state: &Seedance2ProState, args: FindJobArgs) -> anyhow::Result let result = poll_orders(PollOrdersArgs { session: &session, cursor, + host_override: None, }).await .map_err(|err| anyhow!("Error polling orders on page {}: {:?}", page, err))?; diff --git a/crates/service/job/seedance2_pro_job/src/main_loop.rs b/crates/service/job/seedance2_pro_job/src/main_loop.rs index fb834b75ba..9a48390264 100644 --- a/crates/service/job/seedance2_pro_job/src/main_loop.rs +++ b/crates/service/job/seedance2_pro_job/src/main_loop.rs @@ -49,6 +49,7 @@ async fn run_poll_iteration(deps: &JobDependencies) -> anyhow::Result<()> { let response = poll_orders(PollOrdersArgs { session: &deps.seedance2pro_session, cursor, + host_override: None, }) .await .map_err(|err| { diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen_handler.rs index 55b9d222fc..5a16cfe1f4 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen_handler.rs @@ -211,6 +211,7 @@ pub async fn seedance_2p0_multi_function_video_gen_handler( reference_video_urls, reference_audio_urls, use_face_blur_hack: None, + host_override: None, }; // --- Calculate cost and charge wallet --- @@ -453,6 +454,7 @@ async fn upload_to_seedance2pro( let prepare_result = prepare_file_upload(PrepareFileUploadArgs { session, extension, + host_override: None, }) .await .map_err(|err| { @@ -463,6 +465,7 @@ async fn upload_to_seedance2pro( let upload_result = upload_file(UploadFileArgs { upload_url: prepare_result.upload_url, file_bytes, + host_override: None, }) .await .map_err(|err| {