seedance2pro -> kinovi domain name migration (#1181)

This commit is contained in:
Brandon Thomas authored and GitHub committed 2026-03-19 13:49:43 -04:00
1 parent 72bf6738e1
commit 58d2665f0e
13 files changed
+136 -30

No files matched your search

@@ -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();
@@ -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();
@@ -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)))?;
@@ -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<bool>,
/// Override the default host (kinovi.ai).
pub host_override: Option<KinoviHost>,
}
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<GenerateVideoResponse, Seedance2ProError> {
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<GenerateVideo
.build()
.map_err(|err| Seedance2ProClientError::WreqClientError(err))?;
let response = client.post(RUN_TASK_URL)
let referer = format!("{}/", base_url);
let response = client.post(&run_task_url)
.header("User-Agent", FIREFOX_USER_AGENT)
.header("Accept", "*/*")
.header("Accept-Language", "en-US,en;q=0.9")
.header("Accept-Encoding", "gzip, deflate, br, zstd")
.header("Referer", "https://seedance2-pro.com/")
.header("Referer", &referer)
.header("Content-Type", "application/json")
.header("x-trpc-source", "client")
.header("Origin", "https://seedance2-pro.com")
.header("Origin", base_url)
.header("Connection", "keep-alive")
.header("Cookie", cookie)
.header("Sec-Fetch-Dest", "empty")
@@ -331,6 +339,7 @@ mod tests {
reference_video_urls: None,
reference_audio_urls: None,
use_face_blur_hack: None,
host_override: None,
}
}
@@ -396,6 +405,7 @@ mod tests {
reference_video_urls: None,
reference_audio_urls: None,
use_face_blur_hack: None,
host_override: None,
};
let result = generate_video(args).await?;
println!("Task ID: {}", result.task_id);
@@ -423,6 +433,7 @@ mod tests {
reference_video_urls: None,
reference_audio_urls: None,
use_face_blur_hack: None,
host_override: None,
};
let result = generate_video(args).await?;
println!("Task ID: {}", result.task_id);
@@ -452,6 +463,7 @@ mod tests {
reference_video_urls: None,
reference_audio_urls: None,
use_face_blur_hack: None,
host_override: None,
};
let result = generate_video(args).await?;
println!("Task ID: {}", result.task_id);
@@ -480,6 +492,7 @@ mod tests {
]),
reference_audio_urls: None,
use_face_blur_hack: None,
host_override: None,
};
let result = generate_video(args).await?;
println!("Task ID: {}", result.task_id);
@@ -510,6 +523,7 @@ mod tests {
]),
reference_audio_urls: None,
use_face_blur_hack: None,
host_override: None,
};
let result = generate_video(args).await?;
println!("Task ID: {}", result.task_id);
@@ -530,6 +544,7 @@ mod tests {
let prepare_args = PrepareFileUploadArgs {
session: &session,
extension: "mp4".to_string(),
host_override: None,
};
let prepare_result = prepare_file_upload(prepare_args).await?;
println!("Upload URL: {}", prepare_result.upload_url);
@@ -542,6 +557,7 @@ mod tests {
let upload_args = UploadFileArgs {
upload_url: prepare_result.upload_url,
file_bytes,
host_override: None,
};
let result = upload_file(upload_args).await?;
println!("Public URL: {}", result.public_url);
@@ -560,6 +576,7 @@ mod tests {
]),
reference_audio_urls: None,
use_face_blur_hack: None,
host_override: None,
};
let result = generate_video(args).await?;
println!("Task ID: {}", result.task_id);
@@ -0,0 +1,50 @@
/// The host to target for API requests.
///
/// The service was originally at `seedance2-pro.com` but migrated to `kinovi.ai`.
/// This enum allows configuring which host to use.
#[derive(Debug, Clone)]
pub enum KinoviHost {
/// https://kinovi.ai (current default)
Kinovi,
/// https://seedance2-pro.com (legacy)
Seedance2Pro,
/// Custom hosts for API and CDN.
/// Must include the URL scheme but no trailing slash.
/// e.g. api_host: "https://example.com", cdn_host: "http://static.example.com:1234"
CustomHost { api_host: String, cdn_host: String },
}
impl Default for KinoviHost {
fn default() -> 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)
}
@@ -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;
@@ -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<u64>,
/// Override the default host (kinovi.ai).
pub host_override: Option<KinoviHost>,
}
pub struct PollOrdersResponse {
@@ -97,6 +99,10 @@ pub struct OrderStatus {
// --- Implementation ---
pub async fn poll_orders(args: PollOrdersArgs<'_>) -> Result<PollOrdersResponse, Seedance2ProError> {
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<PollOrdersResponse,
.map_err(|err| Seedance2ProClientError::WreqClientError(err))?;
let cookie = args.session.cookies.as_str();
let referer = format!("{}/app/gallery", base_url);
let request = client.get(GET_ORDERS_BASE_URL)
let request = client.get(&get_orders_url)
.query(&[("batch", "1"), ("input", input_json.as_str())])
.header("User-Agent", FIREFOX_USER_AGENT)
.header("Accept", "*/*")
.header("Accept-Language", "en-US,en;q=0.9")
.header("Accept-Encoding", "gzip, deflate, br, zstd")
.header("Referer", "https://seedance2-pro.com/app/gallery")
.header("Referer", &referer)
.header("content-type", "application/json")
.header("x-trpc-source", "client")
.header("Connection", "keep-alive")
@@ -193,9 +200,9 @@ pub async fn poll_orders(args: PollOrdersArgs<'_>) -> Result<PollOrdersResponse,
/// When `cursor` is `Some`, it is included in the JSON payload.
fn build_input_json(cursor: Option<u64>) -> 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;
@@ -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/<unix_millis>-<random_hex>.<extension>`
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<KinoviHost>,
}
pub struct PrepareFileUploadResponse {
@@ -39,6 +41,10 @@ pub struct PrepareFileUploadResponse {
}
pub async fn prepare_file_upload(args: PrepareFileUploadArgs<'_>) -> Result<PrepareFileUploadResponse, Seedance2ProError> {
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<Prep
};
let cookie = args.session.cookies.as_str();
let referer = format!("{}/", base_url);
let response = client.post(SIGNED_UPLOAD_URL)
let response = client.post(&signed_upload_url)
.header("User-Agent", FIREFOX_USER_AGENT)
.header("Accept", "*/*")
.header("Accept-Language", "en-US,en;q=0.9")
.header("Accept-Encoding", "gzip, deflate, br, zstd")
.header("Referer", "https://seedance2-pro.com/")
.header("Referer", &referer)
.header("Content-Type", "application/json")
.header("x-trpc-source", "client")
.header("Origin", "https://seedance2-pro.com")
.header("Origin", base_url)
.header("Connection", "keep-alive")
.header("Cookie", cookie)
.header("Sec-Fetch-Dest", "empty")
@@ -130,12 +137,13 @@ mod tests {
let args = PrepareFileUploadArgs {
session: &session,
extension: "png".to_string(),
host_override: None,
};
let result = prepare_file_upload(args).await?;
println!("Upload URL: {}", result.upload_url);
println!("Material path: {}", result.material_path);
assert!(!result.upload_url.is_empty());
assert!(result.upload_url.contains("cloudflarestorage.com"));
assert!(result.upload_url.contains("cloudflarestorage.com") || result.upload_url.contains("kinovi"));
assert!(result.material_path.starts_with("materials/"));
assert!(result.material_path.ends_with(".png"));
@@ -152,6 +160,7 @@ mod tests {
let args = PrepareFileUploadArgs {
session: &session,
extension: "mp4".to_string(),
host_override: None,
};
let result = prepare_file_upload(args).await?;
println!("Upload URL: {}", result.upload_url);
@@ -1,20 +1,22 @@
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::utils::common_headers::FIREFOX_USER_AGENT;
use log::info;
use url::Url;
use wreq::Client;
use wreq_util::Emulation;
const STATIC_BASE_URL: &str = "https://static.seedance2-pro.com";
pub struct UploadFileArgs {
/// The signed upload URL returned by `prepare_file_upload`.
pub upload_url: String,
/// The raw file bytes to upload.
pub file_bytes: Vec<u8>,
/// Override the default host (kinovi.ai).
pub host_override: Option<KinoviHost>,
}
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<String, Seedance2ProError> {
/// -> `https://static.kinovi.ai/materials/20260219/…`
fn build_public_url(upload_url: &str, host: &KinoviHost) -> Result<String, Seedance2ProError> {
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<UploadFileResponse, Seedance2ProError> {
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<UploadFileResponse, See
.build()
.map_err(|err| Seedance2ProClientError::WreqClientError(err))?;
let referer = format!("{}/", base_url);
let response = client.put(&args.upload_url)
.header("User-Agent", FIREFOX_USER_AGENT)
.header("Accept", "*/*")
.header("Accept-Language", "en-US,en;q=0.9")
.header("Accept-Encoding", "gzip, deflate, br, zstd")
.header("Referer", "https://seedance2-pro.com/")
.header("Origin", "https://seedance2-pro.com")
.header("Referer", &referer)
.header("Origin", base_url)
.header("Connection", "keep-alive")
.header("Sec-Fetch-Dest", "empty")
.header("Sec-Fetch-Mode", "cors")
@@ -73,7 +80,7 @@ pub async fn upload_file(args: UploadFileArgs) -> Result<UploadFileResponse, See
}.into());
}
let public_url = build_public_url(&args.upload_url)?;
let public_url = build_public_url(&args.upload_url, host)?;
info!("Public URL: {}", public_url);
@@ -104,6 +111,7 @@ mod tests {
let prepare_args = PrepareFileUploadArgs {
session: &session,
extension: "jpg".to_string(),
host_override: None,
};
let prepare_result = prepare_file_upload(prepare_args).await?;
println!("Upload URL: {}", prepare_result.upload_url);
@@ -116,11 +124,12 @@ mod tests {
let upload_args = UploadFileArgs {
upload_url: prepare_result.upload_url,
file_bytes,
host_override: None,
};
let result = upload_file(upload_args).await?;
println!("Public URL: {}", result.public_url);
assert!(result.public_url.starts_with("https://static.seedance2-pro.com/materials/"));
assert!(result.public_url.starts_with("https://static.kinovi.ai/materials/"));
assert_eq!(1, 2); // NB: Intentional failure to check the response.
Ok(())
@@ -137,6 +146,7 @@ mod tests {
let prepare_args = PrepareFileUploadArgs {
session: &session,
extension: "mp4".to_string(),
host_override: None,
};
let prepare_result = prepare_file_upload(prepare_args).await?;
println!("Upload URL: {}", prepare_result.upload_url);
@@ -149,11 +159,12 @@ mod tests {
let upload_args = UploadFileArgs {
upload_url: prepare_result.upload_url,
file_bytes,
host_override: None,
};
let result = upload_file(upload_args).await?;
println!("Public URL: {}", result.public_url);
assert!(result.public_url.starts_with("https://static.seedance2-pro.com/materials/"));
assert!(result.public_url.starts_with("https://static.kinovi.ai/materials/"));
assert_eq!(1, 2); // NB: Intentional failure to check the response.
Ok(())
@@ -24,6 +24,7 @@ pub async fn run(state: &Seedance2ProState) -> 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))?;
@@ -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))?;
@@ -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| {
@@ -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| {