Compare commits

...
Author SHA1 Message Date
Hanashi a1ada10817 remove comment 2026-08-01 10:47:12 -04:00
Hanashi c44128e0ad material upload 2026-08-01 10:46:28 -04:00
hanashi a71a4b9613 seedance2p5 (#1825) 2026-07-31 18:12:27 -04:00
hanashi 9737f0ef88 rename client (#1824) 2026-07-31 15:10:30 -04:00
280 changed files with 3968 additions and 1452 deletions
Generated
+27 -27
View File
@@ -757,9 +757,9 @@ dependencies = [
"fal_client",
"gmicloud_client",
"grok_api_client",
"kinovi_web_client",
"log",
"reqwest 0.12.23",
"seedance2pro_client",
"serde",
"serde_derive",
"serde_json",
@@ -6359,6 +6359,29 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "kinovi_web_client"
version = "0.0.1"
dependencies = [
"chrono",
"cloudflare_errors",
"env",
"env_logger 0.11.9",
"errors",
"log",
"rand 0.9.2",
"serde",
"serde_derive",
"serde_json",
"test_data",
"test_utils",
"tokio",
"url",
"workspace-build-acceleration",
"wreq",
"wreq-util",
]
[[package]]
name = "kubectl-pod-cleanup"
version = "0.0.1"
@@ -10519,6 +10542,7 @@ dependencies = [
"errors",
"hashing",
"jobs_common",
"kinovi_web_client",
"log",
"mysql_queries",
"pager",
@@ -10526,7 +10550,6 @@ dependencies = [
"reqwest 0.12.23",
"rootly_client",
"rootly_config",
"seedance2pro_client",
"serde",
"serde_derive",
"serde_json",
@@ -10538,29 +10561,6 @@ dependencies = [
"workspace-build-acceleration",
]
[[package]]
name = "seedance2pro_client"
version = "0.0.1"
dependencies = [
"chrono",
"cloudflare_errors",
"env",
"env_logger 0.11.9",
"errors",
"log",
"rand 0.9.2",
"serde",
"serde_derive",
"serde_json",
"test_data",
"test_utils",
"tokio",
"url",
"workspace-build-acceleration",
"wreq",
"wreq-util",
]
[[package]]
name = "selectors"
version = "0.24.0"
@@ -11632,6 +11632,7 @@ dependencies = [
"images",
"infer 0.16.0",
"jwt_signer",
"kinovi_web_client",
"language-tags",
"lexical-sort",
"limitation",
@@ -11663,7 +11664,6 @@ dependencies = [
"rootly_config",
"rust-s3",
"rustls 0.23.36",
"seedance2pro_client",
"serde",
"serde_derive",
"serde_json",
@@ -11867,8 +11867,8 @@ dependencies = [
"easyenv",
"enums",
"errors",
"kinovi_web_client",
"log",
"seedance2pro_client",
"serde_json",
"tokens",
"tokio",
+2 -2
View File
@@ -27,7 +27,7 @@ members = [
"crates/api_clients/openart_client",
"crates/api_clients/rootly_client",
"crates/api_clients/runwayml_client",
"crates/api_clients/seedance2pro_client",
"crates/api_clients/kinovi_web_client",
"crates/api_clients/thumbnail_generator",
"crates/api_clients/worldlabs_api_client",
"crates/api_clients/worldlabs_consumer_client",
@@ -285,7 +285,7 @@ mysql_queries = { path = "crates/schema/database/mysql_queries" }
pager = { path = "crates/lib/pager" }
openai_sora_client = { path = "crates/api_clients/openai_sora_client" }
primitives = { path = "crates/lib/data/primitives" }
seedance2pro_client = { path = "crates/api_clients/seedance2pro_client" }
kinovi_web_client = { path = "crates/api_clients/kinovi_web_client" }
server_environment = { path = "crates/lib/server_environment" }
sha1_hash = { path = "crates/lib/sha1_hash" }
shared_service_components = { path = "crates/lib/shared_service_components" }
+2 -2
View File
@@ -51,9 +51,9 @@ helpers go in the parent `mod tests` so sub-modules can `use super::*`.
- `storyteller_web` — main HTTP API monolith (actix-web)
- `artcraft` (desktop) — Tauri desktop app
- `artcraft_router` — provider routing for image/video generation (Artcraft, Fal, Seedance2Pro, Muapi)
- `artcraft_router` — provider routing for image/video generation (Artcraft, Fal, KinoviWeb, Muapi)
- `artcraft_api_defs` — shared HTTP API type definitions
- `seedance2pro_client` — HTTP client for the Kinovi/Seedance2Pro video generation service
- `kinovi_web_client` — HTTP client for the Kinovi/KinoviWeb video generation service
- `enums` — database-backed enums stored as VARCHAR fields
- `tokens` — primary key identifiers with Stripe-like prefixes (e.g. `user_`, `mf_`)
- `mysql_queries` — SQLx MySQL queries for the backend
@@ -11,7 +11,7 @@ pub struct OmniGenVideoGenerateResponse {
/// All job tokens created by this request (including the primary).
/// For single-job requests this will contain just one element matching
/// `inference_job_token`. For batch requests (eg. Seedance2Pro with
/// `inference_job_token`. For batch requests (eg. KinoviWeb with
/// multiple order IDs) this will contain all of them.
pub all_job_tokens: Vec<InferenceJobToken>,
}
@@ -22,7 +22,7 @@ artcraft_client.workspace = true
fal_client.workspace = true
gmicloud_client.workspace = true
grok_api_client.workspace = true
seedance2pro_client.workspace = true
kinovi_web_client.workspace = true
tokens.workspace = true
url_utils.workspace = true
worldlabs_api_client.workspace = true
@@ -8,6 +8,6 @@ pub enum RouterProvider {
Fal,
GmiCloud,
GrokApi,
Seedance2Pro,
KinoviWeb,
WorldLabs,
}
@@ -74,6 +74,9 @@ pub enum RouterVideoModel {
#[serde(rename = "seedance_2p0_bpu_mini")]
Seedance2p0BytePlusUltraMini,
#[serde(rename = "seedance_2p5_preview")]
Seedance2p5Preview,
#[serde(rename = "happy_horse_1p0")]
HappyHorse1p0,
@@ -133,6 +136,11 @@ mod tests {
assert_serde_round_trip(RouterVideoModel::Seedance2p0BytePlusUltraMini, "seedance_2p0_bpu_mini");
}
#[test]
fn seedance_2p5_preview_serializes() {
assert_serde_round_trip(RouterVideoModel::Seedance2p5Preview, "seedance_2p5_preview");
}
#[test]
fn veo_3p1_lite_and_vidu_variants_serialize() {
// NB: These strings must match `CommonVideoModel` — the two enums convert
@@ -7,5 +7,5 @@ pub mod router_client;
pub mod router_fal_client;
pub mod router_gmicloud_client;
pub mod router_grok_api_client;
pub mod router_seedance2pro_client;
pub mod router_kinovi_web_client;
pub mod router_worldlabs_client;
@@ -2,7 +2,7 @@ use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_fal_client::RouterFalClient;
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
use crate::client::router_grok_api_client::RouterGrokApiClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_worldlabs_client::RouterWorldLabsClient;
use crate::errors::client_error::{ClientError, ClientType};
@@ -11,7 +11,7 @@ pub struct MultiRouterClient {
pub(crate) fal_client: Option<RouterFalClient>,
pub(crate) gmicloud_client: Option<RouterGmiCloudClient>,
pub(crate) grok_api_client: Option<RouterGrokApiClient>,
pub(crate) seedance2pro_client: Option<RouterSeedance2ProClient>,
pub(crate) kinovi_web_client: Option<RouterKinoviWebClient>,
pub(crate) worldlabs_client: Option<RouterWorldLabsClient>,
}
@@ -36,9 +36,9 @@ impl MultiRouterClient {
.ok_or(ClientError::ClientNotConfigured(ClientType::GrokApi))
}
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ClientError> {
self.seedance2pro_client.as_ref()
.ok_or(ClientError::ClientNotConfigured(ClientType::Seedance2Pro))
pub fn get_kinovi_web_client_ref(&self) -> Result<&RouterKinoviWebClient, ClientError> {
self.kinovi_web_client.as_ref()
.ok_or(ClientError::ClientNotConfigured(ClientType::KinoviWeb))
}
pub fn get_worldlabs_client_ref(&self) -> Result<&RouterWorldLabsClient, ClientError> {
@@ -3,7 +3,7 @@ use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_fal_client::RouterFalClient;
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
use crate::client::router_grok_api_client::RouterGrokApiClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_worldlabs_client::RouterWorldLabsClient;
pub struct MultiRouterClientBuilder {
@@ -11,7 +11,7 @@ pub struct MultiRouterClientBuilder {
fal_client: Option<RouterFalClient>,
gmicloud_client: Option<RouterGmiCloudClient>,
grok_api_client: Option<RouterGrokApiClient>,
seedance2pro_client: Option<RouterSeedance2ProClient>,
kinovi_web_client: Option<RouterKinoviWebClient>,
worldlabs_client: Option<RouterWorldLabsClient>,
}
@@ -22,7 +22,7 @@ impl MultiRouterClientBuilder {
fal_client: None,
gmicloud_client: None,
grok_api_client: None,
seedance2pro_client: None,
kinovi_web_client: None,
worldlabs_client: None,
}
}
@@ -47,8 +47,8 @@ impl MultiRouterClientBuilder {
self
}
pub fn set_seedance2pro_client(mut self, client: RouterSeedance2ProClient) -> Self {
self.seedance2pro_client = Some(client);
pub fn set_kinovi_web_client(mut self, client: RouterKinoviWebClient) -> Self {
self.kinovi_web_client = Some(client);
self
}
@@ -63,7 +63,7 @@ impl MultiRouterClientBuilder {
fal_client: self.fal_client,
gmicloud_client: self.gmicloud_client,
grok_api_client: self.grok_api_client,
seedance2pro_client: self.seedance2pro_client,
kinovi_web_client: self.kinovi_web_client,
worldlabs_client: self.worldlabs_client,
}
}
@@ -3,7 +3,7 @@ use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_fal_client::RouterFalClient;
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
use crate::client::router_grok_api_client::RouterGrokApiClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::client::router_worldlabs_client::RouterWorldLabsClient;
use crate::errors::client_error::{ClientError, ClientType};
@@ -13,7 +13,7 @@ pub enum RouterClient {
Fal(RouterFalClient),
GmiCloud(RouterGmiCloudClient),
GrokApi(RouterGrokApiClient),
Seedance2Pro(RouterSeedance2ProClient),
KinoviWeb(RouterKinoviWebClient),
WorldLabs(RouterWorldLabsClient),
}
@@ -50,11 +50,11 @@ impl RouterClient {
}
}
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ClientError> {
pub fn get_kinovi_web_client_ref(&self) -> Result<&RouterKinoviWebClient, ClientError> {
match self {
RouterClient::Seedance2Pro(client) => Ok(client),
RouterClient::Multi(multi) => multi.get_seedance2pro_client_ref(),
_ => Err(ClientError::ClientNotConfigured(ClientType::Seedance2Pro)),
RouterClient::KinoviWeb(client) => Ok(client),
RouterClient::Multi(multi) => multi.get_kinovi_web_client_ref(),
_ => Err(ClientError::ClientNotConfigured(ClientType::KinoviWeb)),
}
}
@@ -0,0 +1,11 @@
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
pub struct RouterKinoviWebClient {
pub(crate) session: KinoviWebSession,
}
impl RouterKinoviWebClient {
pub fn new(session: KinoviWebSession) -> Self {
RouterKinoviWebClient { session }
}
}
@@ -1,11 +0,0 @@
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
pub struct RouterSeedance2ProClient {
pub(crate) session: Seedance2ProSession,
}
impl RouterSeedance2ProClient {
pub fn new(session: Seedance2ProSession) -> Self {
RouterSeedance2ProClient { session }
}
}
@@ -2,8 +2,8 @@ use crate::errors::client_error::ClientError;
use crate::errors::download_error::DownloadError;
use crate::errors::provider_error::ProviderError;
use fal_client::error::fal_error_plus::FalErrorPlus;
use seedance2pro_client::error::seedance2pro_error::Seedance2ProError;
use seedance2pro_client::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError;
use kinovi_web_client::error::kinovi_web_error::KinoviWebError;
use kinovi_web_client::error::kinovi_web_specific_api_error::KinoviWebSpecificApiError;
use std::error::Error;
use std::fmt::{Display, Formatter};
use artcraft_client::error::api_error::ApiError;
@@ -65,7 +65,7 @@ impl From<ProviderError> for ArtcraftRouterError {
fn from(error: ProviderError) -> Self {
let is_billing_error = match &error {
ProviderError::Fal(FalErrorPlus::FalBillingError(_)) => true,
ProviderError::Seedance2Pro(Seedance2ProError::ApiSpecific(Seedance2ProSpecificApiError::BillingError { .. })) => true,
ProviderError::KinoviWeb(KinoviWebError::ApiSpecific(KinoviWebSpecificApiError::BillingError { .. })) => true,
ProviderError::Storyteller(StorytellerError::Api(ApiError::PaymentRequired(_))) => true,
_ => false,
};
@@ -9,7 +9,7 @@ pub enum ClientType {
Fal,
GmiCloud,
GrokApi,
Seedance2Pro,
KinoviWeb,
WorldLabs,
}
@@ -20,7 +20,7 @@ impl Display for ClientType {
Self::Fal => write!(f, "Fal"),
Self::GmiCloud => write!(f, "GmiCloud"),
Self::GrokApi => write!(f, "GrokApi"),
Self::Seedance2Pro => write!(f, "Seedance2Pro"),
Self::KinoviWeb => write!(f, "KinoviWeb"),
Self::WorldLabs => write!(f, "WorldLabs"),
}
}
@@ -47,8 +47,8 @@ pub enum ClientError {
/// Fal only accepts image URLs for image inputs, not media tokens.
FalOnlySupportsUrls,
/// Seedance2Pro only accepts URLs for media inputs, not media tokens.
Seedance2ProOnlySupportsUrls,
/// KinoviWeb only accepts URLs for media inputs, not media tokens.
KinoviWebOnlySupportsUrls,
/// The Fal endpoint requires a webhook URL but the caller built the client
/// in polling/queue mode (no webhook URL). Returned by webhook-only
@@ -91,8 +91,8 @@ impl Display for ClientError {
Self::FalOnlySupportsUrls => {
write!(f, "Fal only supports image URLs for image inputs, not media tokens")
}
Self::Seedance2ProOnlySupportsUrls => {
write!(f, "Seedance2Pro only supports URLs for media inputs; resolve media tokens to URLs before calling this provider")
Self::KinoviWebOnlySupportsUrls => {
write!(f, "KinoviWeb only supports URLs for media inputs; resolve media tokens to URLs before calling this provider")
}
Self::WebhookUrlRequired => {
write!(f, "This Fal endpoint only supports webhook dispatch; the caller built RouterFalClient in polling-only mode (no webhook URL)")
@@ -4,7 +4,7 @@ use artcraft_client::error::storyteller_error::StorytellerError;
use fal_client::error::fal_error_plus::FalErrorPlus;
use gmicloud_client::error::gmicloud_error::GmiCloudError;
use grok_api_client::error::grok_error::GrokError;
use seedance2pro_client::error::seedance2pro_error::Seedance2ProError;
use kinovi_web_client::error::kinovi_web_error::KinoviWebError;
use worldlabs_api_client::error::world_labs_error::WorldLabsError;
#[derive(Debug)]
@@ -13,7 +13,7 @@ pub enum ProviderError {
Fal(FalErrorPlus),
GmiCloud(GmiCloudError),
Grok(GrokError),
Seedance2Pro(Seedance2ProError),
KinoviWeb(KinoviWebError),
WorldLabs(WorldLabsError),
}
@@ -26,7 +26,7 @@ impl Display for ProviderError {
Self::Fal(e) => write!(f, "Fal provider error: {}", e),
Self::GmiCloud(e) => write!(f, "GmiCloud provider error: {}", e),
Self::Grok(e) => write!(f, "Grok provider error: {}", e),
Self::Seedance2Pro(e) => write!(f, "Seedance2Pro provider error: {}", e),
Self::KinoviWeb(e) => write!(f, "KinoviWeb provider error: {}", e),
Self::WorldLabs(e) => write!(f, "WorldLabs provider error: {}", e),
}
}
@@ -56,9 +56,9 @@ impl From<GrokError> for ProviderError {
}
}
impl From<Seedance2ProError> for ProviderError {
fn from(error: Seedance2ProError) -> Self {
Self::Seedance2Pro(error)
impl From<KinoviWebError> for ProviderError {
fn from(error: KinoviWebError) -> Self {
Self::KinoviWeb(error)
}
}
@@ -26,8 +26,8 @@ impl AudioGenerationDraftRequest {
pub fn get_provider(&self) -> RouterProvider {
match self {
Self::KinoviSunoRemix(_) => RouterProvider::Seedance2Pro,
Self::KinoviSunoSample(_) => RouterProvider::Seedance2Pro,
Self::KinoviSunoRemix(_) => RouterProvider::KinoviWeb,
Self::KinoviSunoSample(_) => RouterProvider::KinoviWeb,
}
}
@@ -4,7 +4,7 @@ use std::fmt::Debug;
use tokens::tokens::media_files::MediaFileToken;
use crate::client::router_client::RouterClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
@@ -19,9 +19,9 @@ pub struct AudioGenerationDraftContext<'a> {
}
impl <'a> AudioGenerationDraftContext<'a> {
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ArtcraftRouterError> {
pub fn get_kinovi_web_client_ref(&self) -> Result<&RouterKinoviWebClient, ArtcraftRouterError> {
let client = self.client.ok_or(ArtcraftRouterError::Client(ClientError::RouterClientNotProvided))?;
client.get_seedance2pro_client_ref()
client.get_kinovi_web_client_ref()
.map_err(|err| ArtcraftRouterError::Client(err))
}
}
@@ -48,10 +48,10 @@ impl AudioGenerationRequest {
Self::ArtcraftSunoSample(_) => RouterProvider::Artcraft,
Self::ArtcraftSeedAudio1p0(_) => RouterProvider::Artcraft,
Self::FalSeedAudio1p0(_) => RouterProvider::Fal,
Self::KinoviSunoMusic(_) => RouterProvider::Seedance2Pro,
Self::KinoviSunoRemix(_) => RouterProvider::Seedance2Pro,
Self::KinoviSunoSounds(_) => RouterProvider::Seedance2Pro,
Self::KinoviSunoSample(_) => RouterProvider::Seedance2Pro,
Self::KinoviSunoMusic(_) => RouterProvider::KinoviWeb,
Self::KinoviSunoRemix(_) => RouterProvider::KinoviWeb,
Self::KinoviSunoSounds(_) => RouterProvider::KinoviWeb,
Self::KinoviSunoSample(_) => RouterProvider::KinoviWeb,
}
}
@@ -100,19 +100,19 @@ impl AudioGenerationRequest {
request.send(client_ref).await
},
AudioGenerationRequest::KinoviSunoMusic(request) => {
let client_ref = client.get_seedance2pro_client_ref()?;
let client_ref = client.get_kinovi_web_client_ref()?;
request.send(client_ref).await
},
AudioGenerationRequest::KinoviSunoRemix(request) => {
let client_ref = client.get_seedance2pro_client_ref()?;
let client_ref = client.get_kinovi_web_client_ref()?;
request.send(client_ref).await
},
AudioGenerationRequest::KinoviSunoSounds(request) => {
let client_ref = client.get_seedance2pro_client_ref()?;
let client_ref = client.get_kinovi_web_client_ref()?;
request.send(client_ref).await
},
AudioGenerationRequest::KinoviSunoSample(request) => {
let client_ref = client.get_seedance2pro_client_ref()?;
let client_ref = client.get_kinovi_web_client_ref()?;
request.send(client_ref).await
},
}
@@ -116,10 +116,10 @@ impl GenerateAudioRequestBuilder {
// Fal
(RouterProvider::Fal, RouterAudioModel::SeedAudio1p0) => build_fal_seed_audio_1p0(self),
// Kinovi
(RouterProvider::Seedance2Pro, RouterAudioModel::SunoMusic) => build_kinovi_suno_music(self),
(RouterProvider::Seedance2Pro, RouterAudioModel::SunoRemix) => build_kinovi_suno_remix(self),
(RouterProvider::Seedance2Pro, RouterAudioModel::SunoSounds) => build_kinovi_suno_sounds(self),
(RouterProvider::Seedance2Pro, RouterAudioModel::SunoSample) => build_kinovi_suno_sample(self),
(RouterProvider::KinoviWeb, RouterAudioModel::SunoMusic) => build_kinovi_suno_music(self),
(RouterProvider::KinoviWeb, RouterAudioModel::SunoRemix) => build_kinovi_suno_remix(self),
(RouterProvider::KinoviWeb, RouterAudioModel::SunoSounds) => build_kinovi_suno_sounds(self),
(RouterProvider::KinoviWeb, RouterAudioModel::SunoSample) => build_kinovi_suno_sample(self),
_ => self.unsupported_provider_and_model(),
}
}
@@ -202,7 +202,7 @@ mod tests {
#[test]
fn kinovi_suno_music_dispatches_to_request() {
let result = builder(RouterProvider::Seedance2Pro, RouterAudioModel::SunoMusic).build2().expect("build");
let result = builder(RouterProvider::KinoviWeb, RouterAudioModel::SunoMusic).build2().expect("build");
assert!(matches!(
result,
AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::KinoviSunoMusic(_))
@@ -211,7 +211,7 @@ mod tests {
#[test]
fn kinovi_suno_sounds_dispatches_to_request() {
let result = builder(RouterProvider::Seedance2Pro, RouterAudioModel::SunoSounds).build2().expect("build");
let result = builder(RouterProvider::KinoviWeb, RouterAudioModel::SunoSounds).build2().expect("build");
assert!(matches!(
result,
AudioGenerationDraftOrRequest::Request(AudioGenerationRequest::KinoviSunoSounds(_))
@@ -220,7 +220,7 @@ mod tests {
#[test]
fn kinovi_suno_remix_dispatches_to_draft() {
let result = builder_with_audio_ref(RouterProvider::Seedance2Pro, RouterAudioModel::SunoRemix).build2().expect("build");
let result = builder_with_audio_ref(RouterProvider::KinoviWeb, RouterAudioModel::SunoRemix).build2().expect("build");
assert!(matches!(
result,
AudioGenerationDraftOrRequest::Draft(AudioGenerationDraftRequest::KinoviSunoRemix(_))
@@ -229,7 +229,7 @@ mod tests {
#[test]
fn kinovi_suno_sample_dispatches_to_draft() {
let result = builder_with_audio_ref(RouterProvider::Seedance2Pro, RouterAudioModel::SunoSample).build2().expect("build");
let result = builder_with_audio_ref(RouterProvider::KinoviWeb, RouterAudioModel::SunoSample).build2().expect("build");
assert!(matches!(
result,
AudioGenerationDraftOrRequest::Draft(AudioGenerationDraftRequest::KinoviSunoSample(_))
@@ -242,7 +242,7 @@ mod tests {
#[test]
fn kinovi_seed_audio_1p0_is_unsupported() {
let result = builder(RouterProvider::Seedance2Pro, RouterAudioModel::SeedAudio1p0).build2();
let result = builder(RouterProvider::KinoviWeb, RouterAudioModel::SeedAudio1p0).build2();
assert!(matches!(result, Err(ArtcraftRouterError::UnsupportedProviderAndModelForNewApi(_))));
}
@@ -10,7 +10,7 @@ pub struct ArtcraftAudioResponsePayload {
}
#[derive(Clone, Debug)]
pub struct Seedance2proAudioResponsePayload {
pub struct KinoviWebAudioResponsePayload {
pub order_id: String,
pub task_id: String,
}
@@ -37,7 +37,7 @@ pub struct FalAudioResponsePayload {
#[derive(Clone, Debug)]
pub enum GenerateAudioResponse {
Artcraft(ArtcraftAudioResponsePayload),
Seedance2Pro(Seedance2proAudioResponsePayload),
KinoviWeb(KinoviWebAudioResponsePayload),
Fal(FalAudioResponsePayload),
}
@@ -49,9 +49,9 @@ impl GenerateAudioResponse {
}
}
pub fn get_seedance2pro_payload(&self) -> Option<Seedance2proAudioResponsePayload> {
pub fn get_kinovi_web_payload(&self) -> Option<KinoviWebAudioResponsePayload> {
match self {
Self::Seedance2Pro(p) => Some(p.clone()),
Self::KinoviWeb(p) => Some(p.clone()),
_ => None,
}
}
@@ -5,14 +5,14 @@
//! draft `finalize()` phase. The upload plumbing is shared with the Kinovi
//! video provider (`generate_video::providers::kinovi::upload`).
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::audio_list_ref::AudioListRef;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_audio::audio_generation_draft_context::AudioGenerationDraftContext;
use crate::generate::generate_video::providers::kinovi::upload::upload_to_seedance2pro;
use crate::generate::generate_video::providers::kinovi::upload::upload_to_kinovi_web;
/// A single unresolved audio reference: either an ArtCraft/public URL or a
/// media file token that resolves to one via the draft context's map.
@@ -55,7 +55,7 @@ pub(crate) fn require_single_audio_ref(
/// Resolve a single audio reference to a source URL and re-upload it to the
/// Kinovi CDN, returning the Kinovi CDN URL.
pub(crate) async fn resolve_and_upload_audio_ref(
session: &Seedance2ProSession,
session: &KinoviWebSession,
audio_ref: &SingleAudioRef,
draft_context: &AudioGenerationDraftContext<'_>,
) -> Result<String, ArtcraftRouterError> {
@@ -71,7 +71,7 @@ pub(crate) async fn resolve_and_upload_audio_ref(
})?
}
};
upload_to_seedance2pro(session, &source_url).await
upload_to_kinovi_web(session, &source_url).await
}
#[cfg(test)]
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::audio::generate_suno_music::GenerateSunoMusicRequest;
use kinovi_web_client::generate::audio::generate_suno_music::GenerateSunoMusicRequest;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
@@ -119,7 +119,7 @@ mod tests {
fn base_builder() -> GenerateAudioRequestBuilder {
GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoMusic,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("a song about corgis".to_string()),
style_prompt: Some("sea shanty".to_string()),
..Default::default()
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::audio::generate_suno_music::GenerateSunoMusicRequest;
use kinovi_web_client::generate::audio::generate_suno_music::GenerateSunoMusicRequest;
use crate::generate::generate_audio::audio_generation_cost_estimate::AudioGenerationCostEstimate;
use crate::generate::generate_audio::providers::kinovi::suno_music::request::KinoviSunoMusicRequestState;
@@ -13,7 +13,7 @@ impl KinoviSunoMusicCostState {
}
pub fn estimate_cost(&self) -> AudioGenerationCostEstimate {
// Cost math is owned by seedance2pro_client's binding — the router just
// Cost math is owned by kinovi_web_client's binding — the router just
// forwards the result so router cost ≡ binding cost by construction.
let pricing_request = GenerateSunoMusicRequest {
prompt: String::new(),
@@ -54,7 +54,7 @@ mod tests {
fn cost_from_built_request_is_seven_cents() {
let builder = GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoMusic,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("a song".to_string()),
..Default::default()
};
@@ -1,12 +1,12 @@
use seedance2pro_client::generate::audio::generate_suno_music::{
use kinovi_web_client::generate::audio::generate_suno_music::{
generate_suno_music, GenerateSunoMusicArgs, GenerateSunoMusicRequest,
};
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_audio::generate_audio_response::{
GenerateAudioResponse, Seedance2proAudioResponsePayload,
GenerateAudioResponse, KinoviWebAudioResponsePayload,
};
#[derive(Debug, Clone)]
@@ -16,7 +16,7 @@ pub struct KinoviSunoMusicRequestState {
}
impl KinoviSunoMusicRequestState {
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
let session = &client.session;
let args = GenerateSunoMusicArgs {
@@ -27,9 +27,9 @@ impl KinoviSunoMusicRequestState {
let response = generate_suno_music(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
Ok(GenerateAudioResponse::Seedance2Pro(Seedance2proAudioResponsePayload {
Ok(GenerateAudioResponse::KinoviWeb(KinoviWebAudioResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
}))
@@ -38,12 +38,12 @@ impl KinoviSunoMusicRequestState {
#[cfg(test)]
mod tests {
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use crate::api::router_audio_model::RouterAudioModel;
use crate::api::router_provider::RouterProvider;
use crate::client::router_client::RouterClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
use crate::generate::generate_audio::generate_audio_response::GenerateAudioResponse;
@@ -56,7 +56,7 @@ mod tests {
style_prompt: Some("Sea shanty, folk".to_string()),
..suno_music_builder()
}).await;
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
assert!(matches!(response, GenerateAudioResponse::KinoviWeb(_)));
assert_eq!(1, 2, "Inspect output above");
}
@@ -69,7 +69,7 @@ mod tests {
is_instrumental: Some(true),
..suno_music_builder()
}).await;
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
assert!(matches!(response, GenerateAudioResponse::KinoviWeb(_)));
assert_eq!(1, 2, "Inspect output above");
}
@@ -78,20 +78,20 @@ mod tests {
fn suno_music_builder() -> GenerateAudioRequestBuilder {
GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoMusic,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
..Default::default()
}
}
fn get_seedance2pro_client() -> RouterClient {
fn get_kinovi_web_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
.expect("Failed to read seedance2pro cookies");
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
.expect("Failed to read kinovi_web cookies");
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
}
async fn run_pipeline(builder: GenerateAudioRequestBuilder) -> GenerateAudioResponse {
let client = get_seedance2pro_client();
let client = get_kinovi_web_client();
let draft_or_request = builder.build2().expect("build2 should succeed");
let request = match draft_or_request {
@@ -102,7 +102,7 @@ mod tests {
let response = request.send_request(&client).await.expect("send_request should succeed");
match &response {
GenerateAudioResponse::Seedance2Pro(p) => {
GenerateAudioResponse::KinoviWeb(p) => {
println!("task_id={}, order_id={}", p.task_id, p.order_id);
}
other => println!("response: {:?}", other),
@@ -168,7 +168,7 @@ mod tests {
fn base_builder() -> GenerateAudioRequestBuilder {
GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoRemix,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("make this electronic".to_string()),
style_prompt: Some("EDM style".to_string()),
audio_references: Some(AudioListRef::Urls(vec!["https://example.com/a.mp3".to_string()])),
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::audio::generate_suno_remix::{
use kinovi_web_client::generate::audio::generate_suno_remix::{
GenerateSunoRemixRequest, KinoviSunoRemixSource,
};
@@ -20,7 +20,7 @@ impl KinoviSunoRemixCostState {
}
pub fn estimate_cost(&self) -> AudioGenerationCostEstimate {
// Cost math is owned by seedance2pro_client's binding — the router just
// Cost math is owned by kinovi_web_client's binding — the router just
// forwards the result so router cost ≡ binding cost by construction.
let pricing_request = GenerateSunoRemixRequest {
prompt: String::new(),
@@ -63,7 +63,7 @@ mod tests {
fn cost_from_draft_is_seven_cents() {
let builder = GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoRemix,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("make this electronic".to_string()),
audio_references: Some(AudioListRef::Urls(vec!["https://example.com/a.mp3".to_string()])),
..Default::default()
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::audio::generate_suno_remix::{
use kinovi_web_client::generate::audio::generate_suno_remix::{
GenerateSunoRemixRequest, KinoviSunoRemixSource,
};
@@ -26,7 +26,7 @@ impl KinoviSunoRemixDraftState {
&mut self,
draft_context: &AudioGenerationDraftContext<'_>,
) -> Result<KinoviSunoRemixRequestState, ArtcraftRouterError> {
let client = draft_context.get_seedance2pro_client_ref()?;
let client = draft_context.get_kinovi_web_client_ref()?;
let session = &client.session;
let audio_source = self.audio_source.take().ok_or_else(|| {
@@ -1,12 +1,12 @@
use seedance2pro_client::generate::audio::generate_suno_remix::{
use kinovi_web_client::generate::audio::generate_suno_remix::{
generate_suno_remix, GenerateSunoRemixArgs, GenerateSunoRemixRequest,
};
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_audio::generate_audio_response::{
GenerateAudioResponse, Seedance2proAudioResponsePayload,
GenerateAudioResponse, KinoviWebAudioResponsePayload,
};
#[derive(Debug, Clone)]
@@ -17,7 +17,7 @@ pub struct KinoviSunoRemixRequestState {
}
impl KinoviSunoRemixRequestState {
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
let session = &client.session;
let args = GenerateSunoRemixArgs {
@@ -28,9 +28,9 @@ impl KinoviSunoRemixRequestState {
let response = generate_suno_remix(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
Ok(GenerateAudioResponse::Seedance2Pro(Seedance2proAudioResponsePayload {
Ok(GenerateAudioResponse::KinoviWeb(KinoviWebAudioResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
}))
@@ -39,16 +39,16 @@ impl KinoviSunoRemixRequestState {
#[cfg(test)]
mod tests {
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use seedance2pro_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
use seedance2pro_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use kinovi_web_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
use kinovi_web_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
use test_utils::test_file_path::test_file_path;
use crate::api::audio_list_ref::AudioListRef;
use crate::api::router_audio_model::RouterAudioModel;
use crate::api::router_provider::RouterProvider;
use crate::client::router_client::RouterClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::generate::generate_audio::audio_generation_draft_context::AudioGenerationDraftContext;
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
@@ -59,13 +59,13 @@ mod tests {
#[tokio::test]
#[ignore] // Sends a real generation to Kinovi; costs credits. Requires a local audio file.
async fn remix_uploaded_audio() {
let client = get_seedance2pro_client();
let client = get_kinovi_web_client();
let audio_url = upload_test_audio(&client).await;
println!("Uploaded audio: {}", audio_url);
let builder = GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoRemix,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("Make this electronic".to_string()),
style_prompt: Some("EDM style".to_string()),
audio_references: Some(AudioListRef::Urls(vec![audio_url])),
@@ -73,17 +73,17 @@ mod tests {
};
let response = run_pipeline(&client, builder).await;
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
assert!(matches!(response, GenerateAudioResponse::KinoviWeb(_)));
assert_eq!(1, 2, "Inspect output above");
}
// ── Helpers ──
fn get_seedance2pro_client() -> RouterClient {
fn get_kinovi_web_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
.expect("Failed to read seedance2pro cookies");
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
.expect("Failed to read kinovi_web cookies");
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
}
async fn run_pipeline(client: &RouterClient, builder: GenerateAudioRequestBuilder) -> GenerateAudioResponse {
@@ -102,7 +102,7 @@ mod tests {
let response = request.send_request(client).await.expect("send_request should succeed");
match &response {
GenerateAudioResponse::Seedance2Pro(p) => {
GenerateAudioResponse::KinoviWeb(p) => {
println!("task_id={}, order_id={}", p.task_id, p.order_id);
}
other => println!("response: {:?}", other),
@@ -112,7 +112,7 @@ mod tests {
}
async fn upload_test_audio(client: &RouterClient) -> String {
let session = &client.get_seedance2pro_client_ref().expect("seedance2pro client").session;
let session = &client.get_kinovi_web_client_ref().expect("kinovi_web client").session;
let audio_path = test_file_path(TEST_AUDIO_PATH).expect("test audio should exist");
let audio_bytes = std::fs::read(&audio_path).expect("read test audio");
@@ -162,7 +162,7 @@ mod tests {
fn base_builder() -> GenerateAudioRequestBuilder {
GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoSample,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("mystical RPG adventure".to_string()),
style_prompt: Some("fantasy video game".to_string()),
audio_references: Some(AudioListRef::Urls(vec!["https://example.com/a.aac".to_string()])),
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::audio::generate_suno_sample::GenerateSunoSampleRequest;
use kinovi_web_client::generate::audio::generate_suno_sample::GenerateSunoSampleRequest;
use crate::generate::generate_audio::audio_generation_cost_estimate::AudioGenerationCostEstimate;
use crate::generate::generate_audio::providers::kinovi::suno_sample::draft::KinoviSunoSampleDraftState;
@@ -18,7 +18,7 @@ impl KinoviSunoSampleCostState {
}
pub fn estimate_cost(&self) -> AudioGenerationCostEstimate {
// Cost math is owned by seedance2pro_client's binding — the router just
// Cost math is owned by kinovi_web_client's binding — the router just
// forwards the result so router cost ≡ binding cost by construction.
let pricing_request = GenerateSunoSampleRequest {
prompt: String::new(),
@@ -63,7 +63,7 @@ mod tests {
fn cost_from_draft_is_seven_cents() {
let builder = GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoSample,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("mystical RPG adventure".to_string()),
audio_references: Some(AudioListRef::Urls(vec!["https://example.com/a.aac".to_string()])),
..Default::default()
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::audio::generate_suno_sample::GenerateSunoSampleRequest;
use kinovi_web_client::generate::audio::generate_suno_sample::GenerateSunoSampleRequest;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_audio::audio_generation_draft_context::AudioGenerationDraftContext;
@@ -34,7 +34,7 @@ impl KinoviSunoSampleDraftState {
&mut self,
draft_context: &AudioGenerationDraftContext<'_>,
) -> Result<KinoviSunoSampleRequestState, ArtcraftRouterError> {
let client = draft_context.get_seedance2pro_client_ref()?;
let client = draft_context.get_kinovi_web_client_ref()?;
let session = &client.session;
let audio_source = self.audio_source.take().ok_or_else(|| {
@@ -1,12 +1,12 @@
use seedance2pro_client::generate::audio::generate_suno_sample::{
use kinovi_web_client::generate::audio::generate_suno_sample::{
generate_suno_sample, GenerateSunoSampleArgs, GenerateSunoSampleRequest,
};
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_audio::generate_audio_response::{
GenerateAudioResponse, Seedance2proAudioResponsePayload,
GenerateAudioResponse, KinoviWebAudioResponsePayload,
};
#[derive(Debug, Clone)]
@@ -17,7 +17,7 @@ pub struct KinoviSunoSampleRequestState {
}
impl KinoviSunoSampleRequestState {
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
let session = &client.session;
let args = GenerateSunoSampleArgs {
@@ -28,9 +28,9 @@ impl KinoviSunoSampleRequestState {
let response = generate_suno_sample(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
Ok(GenerateAudioResponse::Seedance2Pro(Seedance2proAudioResponsePayload {
Ok(GenerateAudioResponse::KinoviWeb(KinoviWebAudioResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
}))
@@ -39,16 +39,16 @@ impl KinoviSunoSampleRequestState {
#[cfg(test)]
mod tests {
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use seedance2pro_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
use seedance2pro_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use kinovi_web_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
use kinovi_web_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
use test_utils::test_file_path::test_file_path;
use crate::api::audio_list_ref::AudioListRef;
use crate::api::router_audio_model::RouterAudioModel;
use crate::api::router_provider::RouterProvider;
use crate::client::router_client::RouterClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::generate::generate_audio::audio_generation_draft_context::AudioGenerationDraftContext;
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
@@ -59,13 +59,13 @@ mod tests {
#[tokio::test]
#[ignore] // Sends a real generation to Kinovi; costs credits. Requires a local audio file.
async fn sample_instrumental() {
let client = get_seedance2pro_client();
let client = get_kinovi_web_client();
let audio_url = upload_test_audio(&client).await;
println!("Uploaded audio: {}", audio_url);
let builder = GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoSample,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("Mystical RPG adventure, make it have a grand climax".to_string()),
style_prompt: Some("Fantasy video game score".to_string()),
is_instrumental: Some(true),
@@ -74,17 +74,17 @@ mod tests {
};
let response = run_pipeline(&client, builder).await;
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
assert!(matches!(response, GenerateAudioResponse::KinoviWeb(_)));
assert_eq!(1, 2, "Inspect output above");
}
// ── Helpers ──
fn get_seedance2pro_client() -> RouterClient {
fn get_kinovi_web_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
.expect("Failed to read seedance2pro cookies");
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
.expect("Failed to read kinovi_web cookies");
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
}
async fn run_pipeline(client: &RouterClient, builder: GenerateAudioRequestBuilder) -> GenerateAudioResponse {
@@ -103,7 +103,7 @@ mod tests {
let response = request.send_request(client).await.expect("send_request should succeed");
match &response {
GenerateAudioResponse::Seedance2Pro(p) => {
GenerateAudioResponse::KinoviWeb(p) => {
println!("task_id={}, order_id={}", p.task_id, p.order_id);
}
other => println!("response: {:?}", other),
@@ -113,7 +113,7 @@ mod tests {
}
async fn upload_test_audio(client: &RouterClient) -> String {
let session = &client.get_seedance2pro_client_ref().expect("seedance2pro client").session;
let session = &client.get_kinovi_web_client_ref().expect("kinovi_web client").session;
let audio_path = test_file_path(TEST_AUDIO_PATH).expect("test audio should exist");
let audio_bytes = std::fs::read(&audio_path).expect("read test audio");
@@ -1,5 +1,5 @@
use enums::common::generation::common_musical_key::CommonMusicalKey;
use seedance2pro_client::generate::audio::generate_suno_sound::{
use kinovi_web_client::generate::audio::generate_suno_sound::{
GenerateSunoSoundRequest, KinoviSunoSoundKey, KinoviSunoSoundType,
};
@@ -201,7 +201,7 @@ mod tests {
fn base_builder() -> GenerateAudioRequestBuilder {
GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoSounds,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("rain sound effects".to_string()),
..Default::default()
}
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::audio::generate_suno_sound::{
use kinovi_web_client::generate::audio::generate_suno_sound::{
GenerateSunoSoundRequest, KinoviSunoSoundKey, KinoviSunoSoundType,
};
@@ -15,7 +15,7 @@ impl KinoviSunoSoundsCostState {
}
pub fn estimate_cost(&self) -> AudioGenerationCostEstimate {
// Cost math is owned by seedance2pro_client's binding — the router just
// Cost math is owned by kinovi_web_client's binding — the router just
// forwards the result so router cost ≡ binding cost by construction.
let pricing_request = GenerateSunoSoundRequest {
prompt: String::new(),
@@ -57,7 +57,7 @@ mod tests {
fn cost_from_built_request_is_two_cents() {
let builder = GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoSounds,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("thunder".to_string()),
is_loopable: Some(true),
bpm: Some(60),
@@ -1,12 +1,12 @@
use seedance2pro_client::generate::audio::generate_suno_sound::{
use kinovi_web_client::generate::audio::generate_suno_sound::{
generate_suno_sound, GenerateSunoSoundArgs, GenerateSunoSoundRequest,
};
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_audio::generate_audio_response::{
GenerateAudioResponse, Seedance2proAudioResponsePayload,
GenerateAudioResponse, KinoviWebAudioResponsePayload,
};
#[derive(Debug, Clone)]
@@ -16,7 +16,7 @@ pub struct KinoviSunoSoundsRequestState {
}
impl KinoviSunoSoundsRequestState {
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateAudioResponse, ArtcraftRouterError> {
let session = &client.session;
let args = GenerateSunoSoundArgs {
@@ -27,9 +27,9 @@ impl KinoviSunoSoundsRequestState {
let response = generate_suno_sound(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
Ok(GenerateAudioResponse::Seedance2Pro(Seedance2proAudioResponsePayload {
Ok(GenerateAudioResponse::KinoviWeb(KinoviWebAudioResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
}))
@@ -39,12 +39,12 @@ impl KinoviSunoSoundsRequestState {
#[cfg(test)]
mod tests {
use enums::common::generation::common_musical_key::CommonMusicalKey;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use crate::api::router_audio_model::RouterAudioModel;
use crate::api::router_provider::RouterProvider;
use crate::client::router_client::RouterClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::generate::generate_audio::audio_generation_draft_or_request::AudioGenerationDraftOrRequest;
use crate::generate::generate_audio::generate_audio_request_builder::GenerateAudioRequestBuilder;
use crate::generate::generate_audio::generate_audio_response::GenerateAudioResponse;
@@ -56,7 +56,7 @@ mod tests {
prompt: Some("A heavy castle door creaking open".to_string()),
..suno_sounds_builder()
}).await;
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
assert!(matches!(response, GenerateAudioResponse::KinoviWeb(_)));
assert_eq!(1, 2, "Inspect output above");
}
@@ -70,7 +70,7 @@ mod tests {
musical_key: Some(CommonMusicalKey::AMinor),
..suno_sounds_builder()
}).await;
assert!(matches!(response, GenerateAudioResponse::Seedance2Pro(_)));
assert!(matches!(response, GenerateAudioResponse::KinoviWeb(_)));
assert_eq!(1, 2, "Inspect output above");
}
@@ -79,20 +79,20 @@ mod tests {
fn suno_sounds_builder() -> GenerateAudioRequestBuilder {
GenerateAudioRequestBuilder {
model: RouterAudioModel::SunoSounds,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
..Default::default()
}
}
fn get_seedance2pro_client() -> RouterClient {
fn get_kinovi_web_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
.expect("Failed to read seedance2pro cookies");
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
.expect("Failed to read kinovi_web cookies");
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
}
async fn run_pipeline(builder: GenerateAudioRequestBuilder) -> GenerateAudioResponse {
let client = get_seedance2pro_client();
let client = get_kinovi_web_client();
let draft_or_request = builder.build2().expect("build2 should succeed");
let request = match draft_or_request {
@@ -103,7 +103,7 @@ mod tests {
let response = request.send_request(&client).await.expect("send_request should succeed");
match &response {
GenerateAudioResponse::Seedance2Pro(p) => {
GenerateAudioResponse::KinoviWeb(p) => {
println!("task_id={}, order_id={}", p.task_id, p.order_id);
}
other => println!("response: {:?}", other),
@@ -146,12 +146,12 @@ impl GenerateImageRequestBuilder {
(RouterProvider::Fal, RouterImageModel::QwenEdit2511Angles) => build_fal_qwen_edit_2511_angles(self),
(RouterProvider::Fal, RouterImageModel::Flux2LoraAngles) => build_fal_flux_2_lora_angles(self),
// Kinovi / Seedance2Pro (Midjourney + Seedream)
(RouterProvider::Seedance2Pro, RouterImageModel::Midjourney7) => build_kinovi_midjourney_7(self),
(RouterProvider::Seedance2Pro, RouterImageModel::Midjourney7Niji) => build_kinovi_midjourney_7_niji(self),
(RouterProvider::Seedance2Pro, RouterImageModel::Midjourney8) => build_kinovi_midjourney_8(self),
(RouterProvider::Seedance2Pro, RouterImageModel::Seedream5p0Pro) => build_kinovi_seedream_5p0_pro(self),
(RouterProvider::Seedance2Pro, RouterImageModel::Seedream5p0ProUltra) => build_kinovi_seedream_5p0_pro(self),
// Kinovi / KinoviWeb (Midjourney + Seedream)
(RouterProvider::KinoviWeb, RouterImageModel::Midjourney7) => build_kinovi_midjourney_7(self),
(RouterProvider::KinoviWeb, RouterImageModel::Midjourney7Niji) => build_kinovi_midjourney_7_niji(self),
(RouterProvider::KinoviWeb, RouterImageModel::Midjourney8) => build_kinovi_midjourney_8(self),
(RouterProvider::KinoviWeb, RouterImageModel::Seedream5p0Pro) => build_kinovi_seedream_5p0_pro(self),
(RouterProvider::KinoviWeb, RouterImageModel::Seedream5p0ProUltra) => build_kinovi_seedream_5p0_pro(self),
_ => self.unsupported_provider_and_model(),
}
@@ -25,11 +25,11 @@ pub struct FalImageResponsePayload {
pub maybe_outbound_request: Option<Arc<dyn Debug + Send + Sync>>,
}
/// Response from the Seedance2Pro/Kinovi provider (used for Midjourney
/// image generation). Mirrors `Seedance2proVideoResponsePayload` on the
/// Response from the KinoviWeb/Kinovi provider (used for Midjourney
/// image generation). Mirrors `KinoviWebVideoResponsePayload` on the
/// video side.
#[derive(Clone, Debug)]
pub struct Seedance2proImageResponsePayload {
pub struct KinoviWebImageResponsePayload {
pub order_id: String,
pub task_id: String,
pub maybe_order_ids: Option<Vec<String>>,
@@ -40,7 +40,7 @@ pub struct Seedance2proImageResponsePayload {
pub enum GenerateImageResponse {
Artcraft(ArtcraftImageResponsePayload),
Fal(FalImageResponsePayload),
Seedance2Pro(Seedance2proImageResponsePayload),
KinoviWeb(KinoviWebImageResponsePayload),
}
impl GenerateImageResponse {
@@ -58,9 +58,9 @@ impl GenerateImageResponse {
}
}
pub fn get_seedance2pro_payload(&self) -> Option<Seedance2proImageResponsePayload> {
pub fn get_kinovi_web_payload(&self) -> Option<KinoviWebImageResponsePayload> {
match self {
Self::Seedance2Pro(p) => Some(p.clone()),
Self::KinoviWeb(p) => Some(p.clone()),
_ => None,
}
}
@@ -17,7 +17,7 @@ use crate::generate::generate_image::providers::kinovi::seedream_5p0_pro::draft:
/// Drafts hold validated/planned parameters but may still need media
/// resolution (e.g. uploading local files to a provider) before they can
/// be sent. Today only the Kinovi models need this — when the caller
/// supplies reference images, we must upload them to the Seedance2Pro
/// supplies reference images, we must upload them to the KinoviWeb
/// CDN before sending the request.
#[derive(Clone, Debug)]
pub enum ImageGenerationDraftRequest {
@@ -30,10 +30,10 @@ pub enum ImageGenerationDraftRequest {
impl ImageGenerationDraftRequest {
pub fn get_provider(&self) -> RouterProvider {
match self {
Self::KinoviMidjourney7(_) => RouterProvider::Seedance2Pro,
Self::KinoviMidjourney7Niji(_) => RouterProvider::Seedance2Pro,
Self::KinoviMidjourney8(_) => RouterProvider::Seedance2Pro,
Self::KinoviSeedream5p0Pro(_) => RouterProvider::Seedance2Pro,
Self::KinoviMidjourney7(_) => RouterProvider::KinoviWeb,
Self::KinoviMidjourney7Niji(_) => RouterProvider::KinoviWeb,
Self::KinoviMidjourney8(_) => RouterProvider::KinoviWeb,
Self::KinoviSeedream5p0Pro(_) => RouterProvider::KinoviWeb,
}
}
@@ -4,21 +4,21 @@ use std::fmt::Debug;
use tokens::tokens::media_files::MediaFileToken;
use crate::client::router_client::RouterClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
/// Context passed when finalizing an image draft into a concrete request.
///
/// Drafts only exist for providers that need to upload assets before
/// sending the request. Today that's the Seedance2Pro/Kinovi pipeline
/// sending the request. Today that's the KinoviWeb/Kinovi pipeline
/// (used for Midjourney image generation with reference images); other
/// providers — Artcraft, Fal — accept image URLs directly and skip the
/// draft phase entirely.
#[derive(Clone, Default)]
pub struct ImageGenerationDraftContext<'a> {
/// Optional: the router client, needed for providers that require
/// authenticated calls (e.g. Seedance2Pro/Kinovi uploads).
/// authenticated calls (e.g. KinoviWeb/Kinovi uploads).
pub client: Option<&'a RouterClient>,
/// Optional: a map of Media File Tokens to their ArtCraft URLs.
@@ -29,9 +29,9 @@ pub struct ImageGenerationDraftContext<'a> {
}
impl<'a> ImageGenerationDraftContext<'a> {
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ArtcraftRouterError> {
pub fn get_kinovi_web_client_ref(&self) -> Result<&RouterKinoviWebClient, ArtcraftRouterError> {
let client = self.client.ok_or(ArtcraftRouterError::Client(ClientError::RouterClientNotProvided))?;
client.get_seedance2pro_client_ref()
client.get_kinovi_web_client_ref()
.map_err(ArtcraftRouterError::Client)
}
}
@@ -123,7 +123,7 @@ pub enum ImageGenerationRequest {
FalQwenEdit2511Angles(FalQwenEdit2511AnglesRequestState),
FalFlux2LoraAngles(FalFlux2LoraAnglesRequestState),
// ── Kinovi / Seedance2Pro provider (Midjourney + Seedream image generation) ──
// ── Kinovi / KinoviWeb provider (Midjourney + Seedream image generation) ──
KinoviMidjourney7(KinoviMidjourney7RequestState),
KinoviMidjourney7Niji(KinoviMidjourney7NijiRequestState),
KinoviMidjourney8(KinoviMidjourney8RequestState),
@@ -170,10 +170,10 @@ impl ImageGenerationRequest {
Self::FalQwenEdit2511Angles(_) => RouterProvider::Fal,
Self::FalFlux2LoraAngles(_) => RouterProvider::Fal,
Self::KinoviMidjourney7(_) => RouterProvider::Seedance2Pro,
Self::KinoviMidjourney7Niji(_) => RouterProvider::Seedance2Pro,
Self::KinoviMidjourney8(_) => RouterProvider::Seedance2Pro,
Self::KinoviSeedream5p0Pro(_) => RouterProvider::Seedance2Pro,
Self::KinoviMidjourney7(_) => RouterProvider::KinoviWeb,
Self::KinoviMidjourney7Niji(_) => RouterProvider::KinoviWeb,
Self::KinoviMidjourney8(_) => RouterProvider::KinoviWeb,
Self::KinoviSeedream5p0Pro(_) => RouterProvider::KinoviWeb,
}
}
@@ -416,24 +416,24 @@ impl ImageGenerationRequest {
request.send(fal_client).await
}
// ── Kinovi / Seedance2Pro (Midjourney + Seedream) ──
// ── Kinovi / KinoviWeb (Midjourney + Seedream) ──
Self::KinoviMidjourney7(request) => {
let seedance_client = client.get_seedance2pro_client_ref()
let seedance_client = client.get_kinovi_web_client_ref()
.map_err(ArtcraftRouterError::Client)?;
request.send(seedance_client).await
}
Self::KinoviMidjourney7Niji(request) => {
let seedance_client = client.get_seedance2pro_client_ref()
let seedance_client = client.get_kinovi_web_client_ref()
.map_err(ArtcraftRouterError::Client)?;
request.send(seedance_client).await
}
Self::KinoviMidjourney8(request) => {
let seedance_client = client.get_seedance2pro_client_ref()
let seedance_client = client.get_kinovi_web_client_ref()
.map_err(ArtcraftRouterError::Client)?;
request.send(seedance_client).await
}
Self::KinoviSeedream5p0Pro(request) => {
let seedance_client = client.get_seedance2pro_client_ref()
let seedance_client = client.get_kinovi_web_client_ref()
.map_err(ArtcraftRouterError::Client)?;
request.send(seedance_client).await
}
@@ -2,8 +2,7 @@ use crate::generate::generate_image::image_generation_cost_estimate::ImageGenera
use crate::generate::generate_image::providers::artcraft::flux_2_lora_angles::request::ArtcraftFlux2LoraAnglesRequestState;
/// Cost state for Artcraft Flux 2 LoRA Angles. Pricing mirrors v1
/// (`estimate_image_cost_artcraft_flux_2_lora_angles`): 4¢ per output image
/// (Artcraft markup over Fal's 2¢/image base rate).
/// (`estimate_image_cost_artcraft_flux_2_lora_angles`): 4¢ per output image.
#[derive(Clone, Debug)]
pub struct ArtcraftFlux2LoraAnglesCostState {
pub num_images: u16,
@@ -148,7 +148,7 @@ mod tests {
#[test]
fn price_covers_kinovi_cost() {
use seedance2pro_client::generate::image::generate_seedream_5p0_pro::{
use kinovi_web_client::generate::image::generate_seedream_5p0_pro::{
KinoviSeedream5p0ProBatchCount, KinoviSeedream5p0ProResolution,
};
use crate::generate::generate_image::providers::kinovi::seedream_5p0_pro::cost::KinoviSeedream5p0ProCostState;
@@ -169,7 +169,7 @@ mod tests {
#[test]
fn price_covers_kinovi_cost() {
use seedance2pro_client::generate::image::generate_seedream_5p0_pro::{
use kinovi_web_client::generate::image::generate_seedream_5p0_pro::{
KinoviSeedream5p0ProBatchCount, KinoviSeedream5p0ProResolution,
};
use crate::generate::generate_image::providers::kinovi::seedream_5p0_pro::cost::KinoviSeedream5p0ProCostState;
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::image::generate_midjourney_v7::{
use kinovi_web_client::generate::image::generate_midjourney_v7::{
GenerateMidjourneyV7AspectRatio, GenerateMidjourneyV7Quality,
GenerateMidjourneyV7Request, KinoviMidjourneyBatchCount,
};
@@ -50,7 +50,7 @@ pub fn build_kinovi_midjourney_7(
}
// Image inputs present → must go through the draft phase so we can
// upload each image to the Seedance2Pro CDN before sending.
// upload each image to the KinoviWeb CDN before sending.
let draft = KinoviMidjourney7DraftState {
prompt,
aspect_ratio,
@@ -159,7 +159,7 @@ pub(crate) fn plan_batch_count(
#[cfg(test)]
mod tests {
use seedance2pro_client::generate::image::generate_midjourney_v7::KinoviMidjourneyBatchCount;
use kinovi_web_client::generate::image::generate_midjourney_v7::KinoviMidjourneyBatchCount;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::image_list_ref::ImageListRef;
@@ -178,7 +178,7 @@ mod tests {
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: RouterImageModel::Midjourney7,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("a corgi astronaut".to_string()),
image_inputs: None,
resolution: None,
@@ -291,7 +291,7 @@ mod tests {
mod aspect_ratio_tests {
use super::*;
use seedance2pro_client::generate::image::generate_midjourney_v7::GenerateMidjourneyV7AspectRatio as Ar;
use kinovi_web_client::generate::image::generate_midjourney_v7::GenerateMidjourneyV7AspectRatio as Ar;
fn aspect_ratio_for(input: Option<RouterAspectRatio>) -> Ar {
let builder = GenerateImageRequestBuilder { aspect_ratio: input, ..base_builder() };
@@ -334,7 +334,7 @@ mod tests {
mod quality_tests {
use super::*;
use seedance2pro_client::generate::image::generate_midjourney_v7::GenerateMidjourneyV7Quality as Q;
use kinovi_web_client::generate::image::generate_midjourney_v7::GenerateMidjourneyV7Quality as Q;
fn quality_for(input: Option<RouterQuality>) -> Option<Q> {
let builder = GenerateImageRequestBuilder { quality: input, ..base_builder() };
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::image::generate_midjourney_v7::{
use kinovi_web_client::generate::image::generate_midjourney_v7::{
GenerateMidjourneyV7AspectRatio, GenerateMidjourneyV7Request, KinoviMidjourneyBatchCount,
};
@@ -171,7 +171,7 @@ mod tests {
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: RouterImageModel::Midjourney7,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("test".to_string()),
image_inputs: None,
resolution: None,
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::image::generate_midjourney_v7::{
use kinovi_web_client::generate::image::generate_midjourney_v7::{
GenerateMidjourneyV7AspectRatio, GenerateMidjourneyV7Quality,
GenerateMidjourneyV7Request, KinoviMidjourneyBatchCount,
};
@@ -17,7 +17,7 @@ pub struct KinoviMidjourney7DraftState {
pub quality: Option<GenerateMidjourneyV7Quality>,
pub batch_count: KinoviMidjourneyBatchCount,
/// Image references that still need to be uploaded to Seedance2Pro CDN
/// Image references that still need to be uploaded to KinoviWeb CDN
/// before the request is sendable. `None` once `to_request()` has run.
pub unhandled_request_state: Option<KinoviMidjourney7RemainingItems>,
}
@@ -32,7 +32,7 @@ impl KinoviMidjourney7DraftState {
&mut self,
draft_context: &ImageGenerationDraftContext<'_>,
) -> Result<KinoviMidjourney7RequestState, ArtcraftRouterError> {
let client = draft_context.get_seedance2pro_client_ref()?;
let client = draft_context.get_kinovi_web_client_ref()?;
let session = &client.session;
let mut reference_image_urls = None;
@@ -63,10 +63,10 @@ impl KinoviMidjourney7DraftState {
#[cfg(test)]
mod tests {
use super::*;
use seedance2pro_client::generate::image::generate_midjourney_v7::GenerateMidjourneyV7AspectRatio;
use kinovi_web_client::generate::image::generate_midjourney_v7::GenerateMidjourneyV7AspectRatio;
// Note: most behavioral tests live in `build.rs` and `cost.rs`. The
// `to_request()` method requires a live Seedance2Pro client to upload
// `to_request()` method requires a live KinoviWeb client to upload
// images, so its end-to-end behavior is exercised by the `request.rs`
// #[ignore]'d integration tests rather than unit-tested here.
@@ -1,12 +1,12 @@
use seedance2pro_client::generate::image::generate_midjourney_v7::{
use kinovi_web_client::generate::image::generate_midjourney_v7::{
generate_midjourney_v7, GenerateMidjourneyV7Args, GenerateMidjourneyV7Request,
};
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
GenerateImageResponse, Seedance2proImageResponsePayload,
GenerateImageResponse, KinoviWebImageResponsePayload,
};
#[derive(Debug, Clone)]
@@ -18,7 +18,7 @@ pub struct KinoviMidjourney7RequestState {
impl KinoviMidjourney7RequestState {
pub async fn send(
&self,
client: &RouterSeedance2ProClient,
client: &RouterKinoviWebClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let args = GenerateMidjourneyV7Args {
session: &client.session,
@@ -28,9 +28,9 @@ impl KinoviMidjourney7RequestState {
let response = generate_midjourney_v7(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
Ok(GenerateImageResponse::Seedance2Pro(Seedance2proImageResponsePayload {
Ok(GenerateImageResponse::KinoviWeb(KinoviWebImageResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
maybe_order_ids: response.order_ids,
@@ -43,7 +43,7 @@ impl KinoviMidjourney7RequestState {
mod tests {
use std::collections::HashMap;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::image_list_ref::ImageListRef;
@@ -52,7 +52,7 @@ mod tests {
use crate::api::router_provider::RouterProvider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::client::router_client::RouterClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image::image_generation_draft::ImageGenerationDraftRequest;
@@ -64,7 +64,7 @@ mod tests {
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: RouterImageModel::Midjourney7,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("a serene mountain lake at sunrise, cinematic".to_string()),
image_inputs: None,
resolution: None,
@@ -82,9 +82,9 @@ mod tests {
fn get_kinovi_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
.expect("Failed to read seedance2pro cookies");
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
.expect("Failed to read kinovi_web cookies");
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
}
#[tokio::test]
@@ -98,9 +98,9 @@ mod tests {
_ => panic!("expected direct Request for text-to-image"),
};
let response = request.send(client.get_seedance2pro_client_ref().unwrap())
let response = request.send(client.get_kinovi_web_client_ref().unwrap())
.await.expect("send should succeed");
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
println!("v7 t2i — task_id={}, order_id={}", payload.task_id, payload.order_id);
assert!(!payload.task_id.is_empty());
}
@@ -135,7 +135,7 @@ mod tests {
.finalize(ctx).await.expect("finalize");
let response = request.send_request(&client).await.expect("send_request");
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
println!("v7 with ref — task_id={}, order_id={}", payload.task_id, payload.order_id);
assert!(!payload.task_id.is_empty());
}
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::{
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::{
GenerateMidjourneyV7NijiAspectRatio, GenerateMidjourneyV7NijiQuality,
GenerateMidjourneyV7NijiRequest, KinoviMidjourneyBatchCount,
};
@@ -147,7 +147,7 @@ pub(crate) fn plan_batch_count(
#[cfg(test)]
mod tests {
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::KinoviMidjourneyBatchCount;
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::KinoviMidjourneyBatchCount;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::image_list_ref::ImageListRef;
@@ -166,7 +166,7 @@ mod tests {
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: RouterImageModel::Midjourney7Niji,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("a magical anime forest scene".to_string()),
image_inputs: None,
resolution: None,
@@ -235,7 +235,7 @@ mod tests {
#[test]
fn aspect_ratio_passes_through_to_niji_enum() {
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::GenerateMidjourneyV7NijiAspectRatio as Ar;
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::GenerateMidjourneyV7NijiAspectRatio as Ar;
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(RouterAspectRatio::WideTwentyOneByNine),
..base_builder()
@@ -249,7 +249,7 @@ mod tests {
#[test]
fn quality_high_maps_to_full() {
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::GenerateMidjourneyV7NijiQuality as Q;
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::GenerateMidjourneyV7NijiQuality as Q;
let builder = GenerateImageRequestBuilder {
quality: Some(RouterQuality::High),
..base_builder()
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::{
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::{
GenerateMidjourneyV7NijiAspectRatio, GenerateMidjourneyV7NijiRequest, KinoviMidjourneyBatchCount,
};
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::{
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::{
GenerateMidjourneyV7NijiAspectRatio, GenerateMidjourneyV7NijiQuality,
GenerateMidjourneyV7NijiRequest, KinoviMidjourneyBatchCount,
};
@@ -29,7 +29,7 @@ impl KinoviMidjourney7NijiDraftState {
&mut self,
draft_context: &ImageGenerationDraftContext<'_>,
) -> Result<KinoviMidjourney7NijiRequestState, ArtcraftRouterError> {
let client = draft_context.get_seedance2pro_client_ref()?;
let client = draft_context.get_kinovi_web_client_ref()?;
let session = &client.session;
let mut reference_image_urls = None;
@@ -1,12 +1,12 @@
use seedance2pro_client::generate::image::generate_midjourney_v7_niji::{
use kinovi_web_client::generate::image::generate_midjourney_v7_niji::{
generate_midjourney_v7_niji, GenerateMidjourneyV7NijiArgs, GenerateMidjourneyV7NijiRequest,
};
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
GenerateImageResponse, Seedance2proImageResponsePayload,
GenerateImageResponse, KinoviWebImageResponsePayload,
};
#[derive(Debug, Clone)]
@@ -17,7 +17,7 @@ pub struct KinoviMidjourney7NijiRequestState {
impl KinoviMidjourney7NijiRequestState {
pub async fn send(
&self,
client: &RouterSeedance2ProClient,
client: &RouterKinoviWebClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let args = GenerateMidjourneyV7NijiArgs {
session: &client.session,
@@ -27,9 +27,9 @@ impl KinoviMidjourney7NijiRequestState {
let response = generate_midjourney_v7_niji(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
Ok(GenerateImageResponse::Seedance2Pro(Seedance2proImageResponsePayload {
Ok(GenerateImageResponse::KinoviWeb(KinoviWebImageResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
maybe_order_ids: response.order_ids,
@@ -42,7 +42,7 @@ impl KinoviMidjourney7NijiRequestState {
mod tests {
use std::collections::HashMap;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::image_list_ref::ImageListRef;
@@ -51,7 +51,7 @@ mod tests {
use crate::api::router_provider::RouterProvider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::client::router_client::RouterClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_draft::ImageGenerationDraftRequest;
use crate::generate::generate_image::image_generation_draft_context::ImageGenerationDraftContext;
@@ -62,7 +62,7 @@ mod tests {
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: RouterImageModel::Midjourney7Niji,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("a magical anime fox spirit in a moonlit bamboo forest".to_string()),
image_inputs: None,
resolution: None,
@@ -80,9 +80,9 @@ mod tests {
fn get_kinovi_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
.expect("Failed to read seedance2pro cookies");
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
.expect("Failed to read kinovi_web cookies");
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
}
#[tokio::test]
@@ -96,9 +96,9 @@ mod tests {
_ => panic!("expected direct Request for text-to-image"),
};
let response = request.send(client.get_seedance2pro_client_ref().unwrap())
let response = request.send(client.get_kinovi_web_client_ref().unwrap())
.await.expect("send should succeed");
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
println!("v7-niji t2i — task_id={}, order_id={}", payload.task_id, payload.order_id);
assert!(!payload.task_id.is_empty());
}
@@ -133,7 +133,7 @@ mod tests {
.finalize(ctx).await.expect("finalize");
let response = request.send_request(&client).await.expect("send_request");
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
println!("v7-niji with ref — task_id={}, order_id={}", payload.task_id, payload.order_id);
assert!(!payload.task_id.is_empty());
}
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::image::generate_midjourney_v8::{
use kinovi_web_client::generate::image::generate_midjourney_v8::{
GenerateMidjourneyV8AspectRatio, GenerateMidjourneyV8Quality,
GenerateMidjourneyV8Request, KinoviMidjourneyBatchCount,
};
@@ -147,7 +147,7 @@ pub(crate) fn plan_batch_count(
#[cfg(test)]
mod tests {
use seedance2pro_client::generate::image::generate_midjourney_v8::KinoviMidjourneyBatchCount;
use kinovi_web_client::generate::image::generate_midjourney_v8::KinoviMidjourneyBatchCount;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::image_list_ref::ImageListRef;
@@ -164,7 +164,7 @@ mod tests {
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: RouterImageModel::Midjourney8,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("a spooky horror scene".to_string()),
image_inputs: None,
resolution: None,
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::image::generate_midjourney_v8::{
use kinovi_web_client::generate::image::generate_midjourney_v8::{
GenerateMidjourneyV8AspectRatio, GenerateMidjourneyV8Request, KinoviMidjourneyBatchCount,
};
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::image::generate_midjourney_v8::{
use kinovi_web_client::generate::image::generate_midjourney_v8::{
GenerateMidjourneyV8AspectRatio, GenerateMidjourneyV8Quality,
GenerateMidjourneyV8Request, KinoviMidjourneyBatchCount,
};
@@ -29,7 +29,7 @@ impl KinoviMidjourney8DraftState {
&mut self,
draft_context: &ImageGenerationDraftContext<'_>,
) -> Result<KinoviMidjourney8RequestState, ArtcraftRouterError> {
let client = draft_context.get_seedance2pro_client_ref()?;
let client = draft_context.get_kinovi_web_client_ref()?;
let session = &client.session;
let mut reference_image_urls = None;
@@ -1,12 +1,12 @@
use seedance2pro_client::generate::image::generate_midjourney_v8::{
use kinovi_web_client::generate::image::generate_midjourney_v8::{
generate_midjourney_v8, GenerateMidjourneyV8Args, GenerateMidjourneyV8Request,
};
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
GenerateImageResponse, Seedance2proImageResponsePayload,
GenerateImageResponse, KinoviWebImageResponsePayload,
};
#[derive(Debug, Clone)]
@@ -17,7 +17,7 @@ pub struct KinoviMidjourney8RequestState {
impl KinoviMidjourney8RequestState {
pub async fn send(
&self,
client: &RouterSeedance2ProClient,
client: &RouterKinoviWebClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let args = GenerateMidjourneyV8Args {
session: &client.session,
@@ -27,9 +27,9 @@ impl KinoviMidjourney8RequestState {
let response = generate_midjourney_v8(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
Ok(GenerateImageResponse::Seedance2Pro(Seedance2proImageResponsePayload {
Ok(GenerateImageResponse::KinoviWeb(KinoviWebImageResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
maybe_order_ids: response.order_ids,
@@ -42,7 +42,7 @@ impl KinoviMidjourney8RequestState {
mod tests {
use std::collections::HashMap;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::image_list_ref::ImageListRef;
@@ -51,7 +51,7 @@ mod tests {
use crate::api::router_provider::RouterProvider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::client::router_client::RouterClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_draft::ImageGenerationDraftRequest;
use crate::generate::generate_image::image_generation_draft_context::ImageGenerationDraftContext;
@@ -62,7 +62,7 @@ mod tests {
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: RouterImageModel::Midjourney8,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("a spooky horror scene with ghosts in an abandoned mansion".to_string()),
image_inputs: None,
resolution: None,
@@ -80,9 +80,9 @@ mod tests {
fn get_kinovi_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
.expect("Failed to read seedance2pro cookies");
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
.expect("Failed to read kinovi_web cookies");
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
}
#[tokio::test]
@@ -96,9 +96,9 @@ mod tests {
_ => panic!("expected direct Request for text-to-image"),
};
let response = request.send(client.get_seedance2pro_client_ref().unwrap())
let response = request.send(client.get_kinovi_web_client_ref().unwrap())
.await.expect("send should succeed");
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
println!("v8 t2i — task_id={}, order_id={}", payload.task_id, payload.order_id);
assert!(!payload.task_id.is_empty());
}
@@ -133,7 +133,7 @@ mod tests {
.finalize(ctx).await.expect("finalize");
let response = request.send_request(&client).await.expect("send_request");
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
println!("v8 with ref — task_id={}, order_id={}", payload.task_id, payload.order_id);
assert!(!payload.task_id.is_empty());
}
@@ -1,21 +1,21 @@
use std::collections::HashMap;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::image_list_ref::ImageListRef;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::providers::kinovi::upload::upload_to_seedance2pro;
use crate::generate::generate_image::providers::kinovi::upload::upload_to_kinovi_web;
/// Resolve an [`ImageListRef`] to a list of public Seedance2Pro CDN URLs,
/// Resolve an [`ImageListRef`] to a list of public KinoviWeb CDN URLs,
/// uploading each image one at a time. Preserves order.
///
/// Returns `Ok(None)` when the input is `None` or an empty list — the
/// caller can then omit the field from the wire request entirely. (The
/// downstream Midjourney API rejects empty arrays.)
pub(crate) async fn resolve_and_upload_image_list(
session: &Seedance2ProSession,
session: &KinoviWebSession,
list: Option<ImageListRef>,
maybe_map: Option<&HashMap<MediaFileToken, String>>,
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
@@ -29,7 +29,7 @@ pub(crate) async fn resolve_and_upload_image_list(
let mut uploaded = Vec::with_capacity(source_urls.len());
for url in &source_urls {
uploaded.push(upload_to_seedance2pro(session, url).await?);
uploaded.push(upload_to_kinovi_web(session, url).await?);
}
Ok(Some(uploaded))
}
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::image::generate_seedream_5p0_pro::{
use kinovi_web_client::generate::image::generate_seedream_5p0_pro::{
GenerateSeedream5p0ProRequest, KinoviSeedream5p0ProAspectRatio, KinoviSeedream5p0ProBatchCount,
KinoviSeedream5p0ProResolution, MAX_REFERENCE_IMAGES,
};
@@ -67,7 +67,7 @@ fn has_image_inputs(image_inputs: Option<&ImageListRef>) -> bool {
}
/// Reject over-limit reference lists before any uploads happen — the
/// Seedance2Pro client would reject them anyway, but only after the draft
/// KinoviWeb client would reject them anyway, but only after the draft
/// stage has already uploaded every image to the Kinovi CDN.
fn validate_reference_image_count(
image_inputs: Option<&ImageListRef>,
@@ -191,7 +191,7 @@ mod tests {
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: RouterImageModel::Seedream5p0Pro,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("an anime girl riding a dinosaur".to_string()),
image_inputs: None,
resolution: None,
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::image::generate_seedream_5p0_pro::{
use kinovi_web_client::generate::image::generate_seedream_5p0_pro::{
GenerateSeedream5p0ProRequest, KinoviSeedream5p0ProAspectRatio, KinoviSeedream5p0ProBatchCount,
KinoviSeedream5p0ProResolution,
};
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::image::generate_seedream_5p0_pro::{
use kinovi_web_client::generate::image::generate_seedream_5p0_pro::{
GenerateSeedream5p0ProRequest, KinoviSeedream5p0ProAspectRatio, KinoviSeedream5p0ProBatchCount,
KinoviSeedream5p0ProResolution,
};
@@ -29,7 +29,7 @@ impl KinoviSeedream5p0ProDraftState {
&mut self,
draft_context: &ImageGenerationDraftContext<'_>,
) -> Result<KinoviSeedream5p0ProRequestState, ArtcraftRouterError> {
let client = draft_context.get_seedance2pro_client_ref()?;
let client = draft_context.get_kinovi_web_client_ref()?;
let session = &client.session;
let mut reference_image_urls = None;
@@ -1,12 +1,12 @@
use seedance2pro_client::generate::image::generate_seedream_5p0_pro::{
use kinovi_web_client::generate::image::generate_seedream_5p0_pro::{
generate_seedream_5p0_pro, GenerateSeedream5p0ProArgs, GenerateSeedream5p0ProRequest,
};
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
GenerateImageResponse, Seedance2proImageResponsePayload,
GenerateImageResponse, KinoviWebImageResponsePayload,
};
#[derive(Debug, Clone)]
@@ -17,7 +17,7 @@ pub struct KinoviSeedream5p0ProRequestState {
impl KinoviSeedream5p0ProRequestState {
pub async fn send(
&self,
client: &RouterSeedance2ProClient,
client: &RouterKinoviWebClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let args = GenerateSeedream5p0ProArgs {
session: &client.session,
@@ -27,9 +27,9 @@ impl KinoviSeedream5p0ProRequestState {
let response = generate_seedream_5p0_pro(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
Ok(GenerateImageResponse::Seedance2Pro(Seedance2proImageResponsePayload {
Ok(GenerateImageResponse::KinoviWeb(KinoviWebImageResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
maybe_order_ids: response.order_ids,
@@ -42,7 +42,7 @@ impl KinoviSeedream5p0ProRequestState {
mod tests {
use std::collections::HashMap;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::image_list_ref::ImageListRef;
@@ -52,7 +52,7 @@ mod tests {
use crate::api::router_resolution::RouterResolution;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::client::router_client::RouterClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_draft::ImageGenerationDraftRequest;
use crate::generate::generate_image::image_generation_draft_context::ImageGenerationDraftContext;
@@ -63,7 +63,7 @@ mod tests {
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: RouterImageModel::Seedream5p0Pro,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("an anime girl riding a dinosaur through a neon city".to_string()),
image_inputs: None,
resolution: Some(RouterResolution::OneK),
@@ -81,9 +81,9 @@ mod tests {
fn get_kinovi_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
.expect("Failed to read seedance2pro cookies");
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
.expect("Failed to read kinovi_web cookies");
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
}
#[tokio::test]
@@ -97,9 +97,9 @@ mod tests {
_ => panic!("expected direct Request for text-to-image"),
};
let response = request.send(client.get_seedance2pro_client_ref().unwrap())
let response = request.send(client.get_kinovi_web_client_ref().unwrap())
.await.expect("send should succeed");
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
println!("seedream 5.0 pro t2i — task_id={}, order_id={}", payload.task_id, payload.order_id);
assert!(!payload.task_id.is_empty());
}
@@ -134,7 +134,7 @@ mod tests {
.finalize(ctx).await.expect("finalize");
let response = request.send_request(&client).await.expect("send_request");
let payload = response.get_seedance2pro_payload().expect("expected seedance2pro payload");
let payload = response.get_kinovi_web_payload().expect("expected kinovi_web payload");
println!("seedream 5.0 pro with ref — task_id={}, order_id={}", payload.task_id, payload.order_id);
assert!(!payload.task_id.is_empty());
}
@@ -1,6 +1,6 @@
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use seedance2pro_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
use seedance2pro_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use kinovi_web_client::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
use kinovi_web_client::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
use url_utils::extension::extract_extension_from_url::{extract_extension_from_url_str, ExtractExtensions};
use crate::errors::artcraft_router_error::ArtcraftRouterError;
@@ -8,14 +8,14 @@ use crate::errors::provider_error::ProviderError;
use crate::utils::download_file::download_file;
/// Downloads an image from a source URL and re-uploads it to the
/// Seedance2Pro/Kinovi CDN. Returns the public URL.
/// KinoviWeb/Kinovi CDN. Returns the public URL.
///
/// This is the image-side twin of `generate_video::providers::kinovi::upload`.
/// The actual upload path doesn't care whether the file is a video or an
/// image, but keeping the two helpers separate lets each side evolve
/// independently and makes intent clear at call sites.
pub(crate) async fn upload_to_seedance2pro(
session: &Seedance2ProSession,
pub(crate) async fn upload_to_kinovi_web(
session: &KinoviWebSession,
source_url: &str,
) -> Result<String, ArtcraftRouterError> {
let extension = extract_extension_from_url_str(source_url, &ExtractExtensions::All)
@@ -30,7 +30,7 @@ pub(crate) async fn upload_to_seedance2pro(
host_override: None,
})
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
let upload_response = upload_file(UploadFileArgs {
upload_url: prepare_response.upload_url,
@@ -38,7 +38,7 @@ pub(crate) async fn upload_to_seedance2pro(
host_override: None,
})
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
Ok(upload_response.public_url)
}
@@ -268,7 +268,7 @@ mod tests {
for provider in [
RouterProvider::GmiCloud,
RouterProvider::GrokApi,
RouterProvider::Seedance2Pro,
RouterProvider::KinoviWeb,
RouterProvider::WorldLabs,
] {
let result = builder_with_url_image(provider, RouterMeshModel::Hunyuan3d3).build2();
@@ -252,7 +252,7 @@ mod tests {
RouterProvider::Fal,
RouterProvider::GmiCloud,
RouterProvider::GrokApi,
RouterProvider::Seedance2Pro,
RouterProvider::KinoviWeb,
] {
let result = GenerateSplatRequestBuilder {
provider,
@@ -74,17 +74,17 @@ generate_video/
│ ├── seedance_2p0/ ← Seedance 2.0 Pro model (skips draft, returns Request directly)
│ │ ├── mod.rs
│ │ ├── build.rs ← builder → Request conversion + plan helpers
│ │ ├── cost.rs ← standalone cost estimation (independent of seedance2pro_client)
│ │ ├── cost.rs ← standalone cost estimation (independent of kinovi_web_client)
│ │ └── request.rs ← request state + send() via Artcraft multi-function API
│ └── seedance_2p0_fast/ ← Seedance 2.0 Fast model (skips draft, 480p/720p only)
│ ├── mod.rs
│ ├── build.rs ← builder → Request conversion + plan helpers (no 1080p)
│ ├── cost.rs ← standalone cost estimation with Fast-specific pricing
│ └── request.rs ← request state + send() via Artcraft omni-gen API
└── kinovi/ ← Kinovi/Seedance2Pro provider
└── kinovi/ ← Kinovi/KinoviWeb provider
├── mod.rs
├── resolve.rs ← shared: media token resolution, upload helpers
├── upload.rs ← shared: download + re-upload to Seedance2Pro CDN
├── upload.rs ← shared: download + re-upload to KinoviWeb CDN
├── seedance_2p0/ ← Seedance 2.0 Pro model
│ ├── mod.rs
│ ├── build.rs ← builder → draft conversion + plan helpers
@@ -72,6 +72,8 @@ use crate::generate::generate_video::providers::fal::vidu_q3::build::build_fal_v
use crate::generate::generate_video::providers::fal::vidu_q3_turbo::build::build_fal_vidu_q3_turbo;
use crate::generate::generate_video::providers::kinovi::seedance_2p0_fast::build::build_kinovi_seedance_2p0_fast;
use crate::generate::generate_video::providers::kinovi::seedance_2p0_mini::build::build_kinovi_seedance_2p0_mini;
use crate::generate::generate_video::providers::artcraft::seedance_2p5_preview::build::build_artcraft_seedance_2p5_preview;
use crate::generate::generate_video::providers::kinovi::seedance_2p5_preview::build::build_kinovi_seedance_2p5_preview;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
/// RouterProvider-agnostic video generation request. Distilled by `build2()` into a
@@ -189,6 +191,7 @@ impl GenerateVideoRequestBuilder {
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0Mini) => build_artcraft_seedance_2p0_mini(self),
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlusMini) => build_artcraft_seedance_2p0_bp_mini(self),
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p0BytePlusUltraMini) => build_artcraft_seedance_2p0_bpu_mini(self),
(RouterProvider::Artcraft, RouterVideoModel::Seedance2p5Preview) => build_artcraft_seedance_2p5_preview(self),
(RouterProvider::Artcraft, RouterVideoModel::PreviewModel) => build_artcraft_preview_model(self),
(RouterProvider::Artcraft, RouterVideoModel::PreviewModelFast) => build_artcraft_preview_model_fast(self),
(RouterProvider::Artcraft, RouterVideoModel::Seedance10Lite) => build_artcraft_seedance_1p0_lite(self),
@@ -230,12 +233,13 @@ impl GenerateVideoRequestBuilder {
(RouterProvider::GrokApi, RouterVideoModel::GrokImagineVideo) => build_grok_api_grok_imagine_video(self),
(RouterProvider::GrokApi, RouterVideoModel::GrokImagineVideo1p5) => build_grok_api_grok_imagine_video_1p5(self),
// Kinovi
(RouterProvider::Seedance2Pro, RouterVideoModel::HappyHorse1p0) => build_kinovi_happy_horse_1p0(self),
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0) => build_kinovi_seedance_2p0(self),
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0Fast) => build_kinovi_seedance_2p0_fast(self),
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0Mini) => build_kinovi_seedance_2p0_mini(self),
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0BytePlusMini) => build_kinovi_seedance_2p0_mini(self),
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0BytePlusUltraMini) => build_kinovi_seedance_2p0_mini(self),
(RouterProvider::KinoviWeb, RouterVideoModel::HappyHorse1p0) => build_kinovi_happy_horse_1p0(self),
(RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p0) => build_kinovi_seedance_2p0(self),
(RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p0Fast) => build_kinovi_seedance_2p0_fast(self),
(RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p0Mini) => build_kinovi_seedance_2p0_mini(self),
(RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p0BytePlusMini) => build_kinovi_seedance_2p0_mini(self),
(RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p0BytePlusUltraMini) => build_kinovi_seedance_2p0_mini(self),
(RouterProvider::KinoviWeb, RouterVideoModel::Seedance2p5Preview) => build_kinovi_seedance_2p5_preview(self),
_ => self.unsupported_provider_and_model(),
}
}
@@ -10,7 +10,7 @@ pub struct ArtcraftVideoResponsePayload {
}
#[derive(Clone, Debug)]
pub struct Seedance2proVideoResponsePayload {
pub struct KinoviWebVideoResponsePayload {
pub order_id: String,
pub task_id: String,
pub maybe_order_ids: Option<Vec<String>>,
@@ -58,7 +58,7 @@ pub enum GenerateVideoResponse {
Artcraft(ArtcraftVideoResponsePayload),
GmiCloud(GmiCloudVideoResponsePayload),
Grok(GrokVideoResponsePayload),
Seedance2Pro(Seedance2proVideoResponsePayload),
KinoviWeb(KinoviWebVideoResponsePayload),
Fal(FalVideoResponsePayload),
}
@@ -70,9 +70,9 @@ impl GenerateVideoResponse {
}
}
pub fn get_seedance2pro_payload(&self) -> Option<Seedance2proVideoResponsePayload> {
pub fn get_kinovi_web_payload(&self) -> Option<KinoviWebVideoResponsePayload> {
match self {
Self::Seedance2Pro(p) => Some(p.clone()),
Self::KinoviWeb(p) => Some(p.clone()),
_ => None,
}
}
@@ -14,8 +14,7 @@ use crate::generate::generate_video::video_generation_request::VideoGenerationRe
/// Same constraints as the v1 variant — xAI v1.5 still supports only 480p
/// and 720p and no 21:9 ultra-wide. The on-wire model identifier is
/// `grok-imagine-video-1.5`; the cost calculator in
/// [`super::cost`] keys off that to apply the v1.5 pricing tier (with a 5%
/// ArtCraft markup on top of the upstream rates).
/// [`super::cost`] keys off that to apply the v1.5 pricing tier.
pub fn build_artcraft_grok_imagine_video_1p5(
builder: GenerateVideoRequestBuilder,
) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
@@ -14,10 +14,7 @@ use crate::generate::generate_video::providers::artcraft::grok_imagine_video_1p5
// ── Markup ──
//
// ArtCraft adds a modest 5% markup over the underlying xAI v1.5 pricing. The
// markup is applied at cent granularity with ceiling rounding so the final
// charge always covers the upstream cost (we never bill the user less than
// the cost of the API call we forwarded).
// The markup is applied at cent granularity with ceiling rounding.
const MARKUP_NUMERATOR: u64 = 105;
const MARKUP_DENOMINATOR: u64 = 100;
@@ -69,8 +66,7 @@ impl ArtcraftGrokImagineVideo1p5CostState {
// composing. The image requirement is enforced at send time (request.rs)
// and by the generate endpoints.
let base = self.base_cost_in_cents_for_batch();
// 5% markup, ceil so the user is always charged enough to cover the
// upstream grok cost regardless of rounding.
// Ceil at cent granularity.
let usd_cents = base.saturating_mul(MARKUP_NUMERATOR).div_ceil(MARKUP_DENOMINATOR);
Ok(VideoGenerationCostEstimate {
@@ -29,6 +29,7 @@ pub mod seedance_2p0_bpu_fast;
pub mod seedance_2p0_mini;
pub mod seedance_2p0_bp_mini;
pub mod seedance_2p0_bpu_mini;
pub mod seedance_2p5_preview;
pub mod sora_2;
pub mod sora_2_pro;
pub mod veo_2;
@@ -25,7 +25,7 @@ impl ArtcraftSeedance2p0BytePlusMiniCostState {
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// BytePlus / BytePlus Ultra Mini share a higher-margin rate set; see
// BytePlus / BytePlus Ultra Mini share a rate set; see
// seedance_common::seedance_2p0_byteplus_mini_usd_cents.
let usd_cents = seedance_2p0_byteplus_mini_usd_cents(
self.resolution,
@@ -58,8 +58,7 @@ mod tests {
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
// Every combination, driven through the full build pipeline. These prices are
// the supplier's per-combo cost plus a 5% margin, rounded up to whole cents.
// Every combination, driven through the full build pipeline.
mod pricing_480p {
use super::*;
@@ -25,7 +25,7 @@ impl ArtcraftSeedance2p0BytePlusUltraMiniCostState {
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// BytePlus / BytePlus Ultra Mini share a higher-margin rate set; see
// BytePlus / BytePlus Ultra Mini share a rate set; see
// seedance_common::seedance_2p0_byteplus_mini_usd_cents.
let usd_cents = seedance_2p0_byteplus_mini_usd_cents(
self.resolution,
@@ -58,8 +58,7 @@ mod tests {
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
// Every combination, driven through the full build pipeline. These prices are
// the supplier's per-combo cost plus a 5% margin, rounded up to whole cents.
// Every combination, driven through the full build pipeline.
mod pricing_480p {
use super::*;
@@ -58,8 +58,7 @@ mod tests {
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
// Every combination, driven through the full build pipeline. These prices are
// the supplier's per-combo cost plus a 5% margin, rounded up to whole cents.
// Every combination, driven through the full build pipeline.
mod pricing_480p {
use super::*;
@@ -0,0 +1,250 @@
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
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::providers::artcraft::build_common::{
build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport,
};
use crate::generate::generate_video::providers::artcraft::seedance_2p5_preview::request::ArtcraftSeedance2p5PreviewRequestState;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
pub fn build_artcraft_seedance_2p5_preview(mut builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
let strategy = builder.request_mismatch_mitigation_strategy;
// Parity with the Kinovi build: 2.5 Preview only operates in reference mode
// (no keyframe start/end frames) and has no character support. Erroring
// here keeps the billing estimate consistent with the execution build.
if builder.start_frame.is_some() {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "start_frame",
value: "Seedance 2.5 Preview has no keyframe mode; use reference_images".to_string(),
}));
}
if builder.end_frame.is_some() {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "end_frame",
value: "Seedance 2.5 Preview has no keyframe mode; use reference_images".to_string(),
}));
}
if builder.reference_character_tokens.is_some() {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "reference_character_tokens",
value: "Seedance 2.5 Preview does not support character references".to_string(),
}));
}
// 2.5 Preview allows 4-30 second durations (longer than the shared 4-15
// range in build_common) and generates a single video per request. Plan
// both here, leaving None for the shared builder, then set them after.
let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?;
plan_batch_count(builder.video_batch_count.take(), strategy)?;
let mut request = build_artcraft_omni_video_request(
builder,
CommonVideoModelEnum::Seedance2p5Preview,
SupportedResolutions::Fast,
UltraWideSupport::Supported,
)?;
request.duration_seconds = duration_seconds;
request.video_batch_count = Some(1);
let state = ArtcraftSeedance2p5PreviewRequestState { request };
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p5Preview(state)))
}
// Seedance 2.5 Preview supports durations of 4-30 seconds.
fn plan_duration(
duration_seconds: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<u16>, ArtcraftRouterError> {
const MIN: u16 = 4;
const MAX: u16 = 30;
match duration_seconds {
None => Ok(None),
Some(d) if d >= MIN && d <= MAX => Ok(Some(d)),
Some(d) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "duration_seconds",
value: format!("{}", d),
}))
}
_ => Ok(Some(d.clamp(MIN, MAX))),
},
}
}
// Seedance 2.5 Preview generates a single video per request (no batching).
fn plan_batch_count(
video_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<(), ArtcraftRouterError> {
let count = video_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(()),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "video_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(()),
},
}
}
#[cfg(test)]
mod tests {
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
use tokens::tokens::characters::CharacterToken;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::character_list_ref::CharacterListRef;
use crate::api::image_list_ref::ImageListRef;
use crate::api::image_ref::ImageRef;
use crate::api::router_provider::RouterProvider;
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use crate::generate::generate_video::video_generation_request::VideoGenerationRequest;
use super::*;
mod field_conversions {
use super::*;
#[test]
fn model_is_correct() {
let req = unwrap_request(builder_with(|_| {}));
assert!(matches!(req.request.model, Some(CommonVideoModelEnum::Seedance2p5Preview)));
}
#[test]
fn prompt_is_passed_through() {
let req = unwrap_request(builder_with(|b| { b.prompt = Some("test".to_string()); }));
assert_eq!(req.request.prompt, Some("test".to_string()));
}
#[test]
fn duration_30_is_passed_through() {
let req = unwrap_request(builder_with(|b| { b.duration_seconds = Some(30); }));
assert_eq!(req.request.duration_seconds, Some(30));
}
#[test]
fn duration_clamped_to_max_30() {
let req = unwrap_request(builder_with(|b| { b.duration_seconds = Some(99); }));
assert_eq!(req.request.duration_seconds, Some(30));
}
#[test]
fn duration_clamped_to_min_4() {
let req = unwrap_request(builder_with(|b| { b.duration_seconds = Some(2); }));
assert_eq!(req.request.duration_seconds, Some(4));
}
#[test]
fn batch_count_is_always_one() {
let req = unwrap_request(builder_with(|b| { b.video_batch_count = Some(4); }));
assert_eq!(req.request.video_batch_count, Some(1));
}
#[test]
fn batch_over_one_error_out() {
let result = build_artcraft_seedance_2p5_preview(builder_with(|b| {
b.video_batch_count = Some(4);
b.request_mismatch_mitigation_strategy = RequestMismatchMitigationStrategy::ErrorOut;
}));
assert!(result.is_err());
}
}
mod unsupported_option_errors {
use super::*;
#[test]
fn start_frame_errors() {
let result = build_artcraft_seedance_2p5_preview(builder_with(|b| {
b.start_frame = Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_start".to_string())));
}));
assert!(result.is_err());
}
#[test]
fn end_frame_errors() {
let result = build_artcraft_seedance_2p5_preview(builder_with(|b| {
b.end_frame = Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_end".to_string())));
}));
assert!(result.is_err());
}
#[test]
fn character_references_error() {
let result = build_artcraft_seedance_2p5_preview(builder_with(|b| {
b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(vec![
CharacterToken::new("char_abc".to_string()),
]));
}));
assert!(result.is_err());
}
}
mod resolution_tests {
use super::*;
#[test]
fn res_480p() {
let req = unwrap_request(builder_with(|b| { b.resolution = Some(RouterResolution::FourEightyP); }));
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP));
}
#[test]
fn res_1080p_downgrades_to_720p() {
let req = unwrap_request(builder_with(|b| { b.resolution = Some(RouterResolution::TenEightyP); }));
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
}
}
mod media_token_tests {
use super::*;
#[test]
fn reference_image_tokens_passed_through() {
let tokens = vec![
MediaFileToken::new("mf_a".to_string()),
MediaFileToken::new("mf_b".to_string()),
];
let req = unwrap_request(builder_with(|b| { b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone())); }));
assert_eq!(req.request.reference_image_media_tokens, Some(tokens));
}
}
fn builder_with(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
let mut builder = GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p5Preview,
provider: RouterProvider::Artcraft,
duration_seconds: Some(5),
video_batch_count: Some(1),
..Default::default()
};
f(&mut builder);
builder
}
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftSeedance2p5PreviewRequestState {
let result = build_artcraft_seedance_2p5_preview(builder).expect("build should succeed");
match result {
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p5Preview(state)) => state,
_ => panic!("expected ArtcraftSeedance2p5Preview request"),
}
}
}
@@ -0,0 +1,142 @@
use enums::common::generation::common_resolution::CommonResolution;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video::providers::artcraft::seedance_common::seedance_2p5_preview_usd_cents;
use crate::generate::generate_video::providers::artcraft::seedance_2p5_preview::request::ArtcraftSeedance2p5PreviewRequestState;
/// Only the resolution and duration matter — 2.5 Preview has no
/// video-reference surcharge and no batching.
pub struct ArtcraftSeedance2p5PreviewCostState {
pub resolution: CommonResolution,
pub duration_seconds: u16,
}
impl ArtcraftSeedance2p5PreviewCostState {
pub fn from_request(request: &ArtcraftSeedance2p5PreviewRequestState) -> Self {
let resolution = request.request.resolution
.unwrap_or(CommonResolution::SevenTwentyP);
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
Self { resolution, duration_seconds }
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
let usd_cents = seedance_2p5_preview_usd_cents(
self.resolution,
self.duration_seconds,
);
// ArtCraft credits: 100 credits = $1.00, so credits = cents.
VideoGenerationCostEstimate {
cost_in_credits: Some(usd_cents),
cost_in_usd_cents: Some(usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
}
}
}
#[cfg(test)]
mod tests {
use crate::api::router_provider::RouterProvider;
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::video_list_ref::VideoListRef;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use tokens::tokens::media_files::MediaFileToken;
mod pricing_480p {
use super::*;
#[test]
fn full_duration_table() {
assert_eq!(cents(Some(RouterResolution::FourEightyP), 4), 82);
assert_eq!(cents(Some(RouterResolution::FourEightyP), 10), 205);
assert_eq!(cents(Some(RouterResolution::FourEightyP), 15), 307);
assert_eq!(cents(Some(RouterResolution::FourEightyP), 20), 409);
assert_eq!(cents(Some(RouterResolution::FourEightyP), 25), 511);
assert_eq!(cents(Some(RouterResolution::FourEightyP), 30), 613);
}
}
mod pricing_720p {
use super::*;
#[test]
fn full_duration_table() {
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 4), 164);
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 10), 409);
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 15), 613);
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 20), 817);
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 25), 1021);
assert_eq!(cents(Some(RouterResolution::SevenTwentyP), 30), 1226);
}
#[test]
fn none_defaults_to_720p() {
assert_eq!(cents(None, 10), cents(Some(RouterResolution::SevenTwentyP), 10));
}
}
mod reference_invariance_tests {
use super::*;
/// References never affect 2.5 Preview pricing.
#[test]
fn video_reference_does_not_change_price() {
let without = cents(Some(RouterResolution::SevenTwentyP), 10);
let estimate = estimate_with(|b| {
b.resolution = Some(RouterResolution::SevenTwentyP);
b.duration_seconds = Some(10);
b.reference_videos = Some(VideoListRef::MediaFileTokens(vec![
MediaFileToken::new("mf_ref".to_string()),
]));
});
assert_eq!(estimate.cost_in_usd_cents, Some(without));
}
}
mod credits_tests {
use super::*;
#[test]
fn credits_equal_usd_cents() {
for dur in [4u16, 10, 15, 20, 25, 30] {
let estimate = estimate_with(|b| {
b.resolution = Some(RouterResolution::FourEightyP);
b.duration_seconds = Some(dur);
});
assert_eq!(estimate.cost_in_credits, estimate.cost_in_usd_cents);
}
}
}
// ── Helpers ──
fn estimate_with(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> VideoGenerationCostEstimate {
let mut builder = GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p5Preview,
provider: RouterProvider::Artcraft,
duration_seconds: Some(5),
video_batch_count: Some(1),
..Default::default()
};
f(&mut builder);
builder.build2()
.expect("build should succeed")
.estimate_cost()
.expect("estimate should succeed")
}
fn cents(resolution: Option<RouterResolution>, duration_seconds: u16) -> u64 {
estimate_with(|b| {
b.resolution = resolution;
b.duration_seconds = Some(duration_seconds);
}).cost_in_usd_cents.unwrap()
}
}
@@ -0,0 +1,3 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -0,0 +1,17 @@
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
use crate::generate::generate_video::providers::artcraft::request_common::send_artcraft_omni_video_request;
#[derive(Clone, Debug)]
pub struct ArtcraftSeedance2p5PreviewRequestState {
pub request: OmniGenVideoCostAndGenerateRequest,
}
impl ArtcraftSeedance2p5PreviewRequestState {
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
send_artcraft_omni_video_request(&self.request, client).await
}
}
@@ -15,7 +15,7 @@ const FOUR_K_CENTI_CENTS_PER_SECOND: u64 = 8660;
/// at 4K (17.20 ¢/s surcharge).
const FOUR_K_VIDEO_REFERENCE_SURCHARGE_CENTI_CENTS_PER_SECOND: u64 = 1720;
/// ArtCraft's user-facing price (USD cents) for Seedance 2.0 at 4K.
/// ArtCraft's price (USD cents) for Seedance 2.0 at 4K.
///
/// A reference video adds a per-second surcharge (4K with and without a video
/// reference are priced differently). The cost scales with duration and batch
@@ -39,13 +39,13 @@ pub fn seedance_2p0_four_k_usd_cents(
// ── Seedance 2.0 Mini pricing ──
//
// Mini offers only 480p and 720p. The rates below are the per-second USER price
// in USD cents (margin already included); a reference video adds a per-second
// surcharge. Rates are fractional, so the total is rounded UP to a whole cent
// once at the end (after multiplying by duration × batch).
// Mini offers only 480p and 720p. The rates below are the price in USD cents
// per second; a reference video adds a per-second surcharge. Rates are
// fractional, so the total is rounded UP to a whole cent once at the end
// (after multiplying by duration × batch).
//
// The regular Mini and the BytePlus / BytePlus Ultra Minis carry DIFFERENT
// margins, so they have separate rate sets and helpers below.
// The regular Mini and the BytePlus / BytePlus Ultra Minis have separate
// rate sets and helpers below.
/// Regular Mini — 480p price, USD cents per second.
const MINI_CENTS_PER_SECOND_480P: f64 = 3.24074074;
@@ -65,7 +65,7 @@ const BYTEPLUS_MINI_CENTS_PER_SECOND_720P: f64 = 8.72427984;
/// BytePlus / BytePlus Ultra Mini — 720p reference-video surcharge, USD cents per second.
const BYTEPLUS_MINI_VIDEO_REFERENCE_SURCHARGE_CENTS_PER_SECOND_720P: f64 = 1.74485597;
/// ArtCraft's user-facing price (USD cents) for the regular Seedance 2.0 Mini.
/// ArtCraft's price (USD cents) for the regular Seedance 2.0 Mini.
///
/// Only 480p and 720p are offered; any other resolution prices at 720p. A
/// reference video adds the per-second surcharge. The fractional total is
@@ -88,9 +88,8 @@ pub fn seedance_2p0_mini_usd_cents(
)
}
/// ArtCraft's user-facing price (USD cents) for the Seedance 2.0 BytePlus Mini
/// and BytePlus Ultra Mini (which share the same rates — a higher margin than
/// the regular Mini).
/// ArtCraft's price (USD cents) for the Seedance 2.0 BytePlus Mini and
/// BytePlus Ultra Mini (which share the same rates).
///
/// Only 480p and 720p are offered; any other resolution prices at 720p. A
/// reference video adds the per-second surcharge. The fractional total is
@@ -113,6 +112,35 @@ pub fn seedance_2p0_byteplus_mini_usd_cents(
)
}
// ── Seedance 2.5 Preview pricing ──
//
// 2.5 Preview offers only 480p and 720p, generates one video per request (no
// batching), and — unlike the 2.0 family — references of any kind do NOT
// change the price.
/// Seedance 2.5 Preview — 480p price, USD cents per second.
const SEEDANCE_2P5_PREVIEW_CENTS_PER_SECOND_480P: f64 = 20.41909465;
/// Seedance 2.5 Preview — 720p price, USD cents per second.
const SEEDANCE_2P5_PREVIEW_CENTS_PER_SECOND_720P: f64 = 40.8381893;
/// ArtCraft's price (USD cents) for Seedance 2.5 Preview.
///
/// Only 480p and 720p are offered; any other resolution prices at 720p.
/// References never affect the price and there is no batching. The fractional
/// total is rounded UP to a whole cent.
pub fn seedance_2p5_preview_usd_cents(
resolution: CommonResolution,
duration_seconds: u16,
) -> u64 {
let cents_per_second = match resolution {
CommonResolution::FourEightyP => SEEDANCE_2P5_PREVIEW_CENTS_PER_SECOND_480P,
// Everything else (including 720p and unsupported resolutions) prices at 720p.
_ => SEEDANCE_2P5_PREVIEW_CENTS_PER_SECOND_720P,
};
(cents_per_second * duration_seconds as f64).ceil() as u64
}
#[allow(clippy::too_many_arguments)]
fn mini_usd_cents(
resolution: CommonResolution,
@@ -179,8 +207,7 @@ mod tests {
// ── Seedance 2.0 Mini ──
//
// Every combination (480p/720p × with/without video reference × 4/5/10/15s)
// at batch 1. These are the supplier's per-combo cost plus a 5% margin,
// rounded up to whole cents.
// at batch 1, rounded up to whole cents.
mod mini {
use super::*;
@@ -253,8 +280,8 @@ mod tests {
// ── Seedance 2.0 BytePlus / BytePlus Ultra Mini ──
//
// Same combinations as the regular Mini, but at a higher margin (6% vs 5%),
// so the prices are equal-or-higher than the regular Mini.
// Same combinations as the regular Mini; the prices are equal-or-higher
// than the regular Mini.
mod byteplus_mini {
use super::*;
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::video::generate_happy_horse_1p0::{
use kinovi_web_client::generate::video::generate_happy_horse_1p0::{
KinoviHappyHorse1p0AspectRatio, KinoviHappyHorse1p0BatchCount,
KinoviHappyHorse1p0OutputResolution,
};
@@ -441,7 +441,7 @@ mod tests {
fn happy_horse_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("a cat dancing".to_string()),
duration_seconds: Some(5),
video_batch_count: Some(1),
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::video::generate_happy_horse_1p0::{
use kinovi_web_client::generate::video::generate_happy_horse_1p0::{
GenerateHappyHorse1p0Request, KinoviHappyHorse1p0BatchCount,
KinoviHappyHorse1p0OutputResolution,
};
@@ -310,7 +310,7 @@ mod tests {
use crate::api::router_video_model::RouterVideoModel;
let builder = GenerateVideoRequestBuilder {
model: RouterVideoModel::HappyHorse1p0,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
resolution,
duration_seconds: Some(duration_seconds),
video_batch_count: Some(video_batch_count),
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::video::generate_happy_horse_1p0::{
use kinovi_web_client::generate::video::generate_happy_horse_1p0::{
GenerateHappyHorse1p0Request, KinoviHappyHorse1p0AspectRatio,
KinoviHappyHorse1p0BatchCount, KinoviHappyHorse1p0OutputResolution,
};
@@ -30,7 +30,7 @@ impl KinoviHappyHorse1p0DraftState {
&mut self,
draft_context: &VideoGenerationDraftContext<'_>,
) -> Result<KinoviHappyHorse1p0RequestState, ArtcraftRouterError> {
let client = draft_context.get_seedance2pro_client_ref()?;
let client = draft_context.get_kinovi_web_client_ref()?;
let session = &client.session;
let mut start_frame_url = None;
@@ -1,11 +1,11 @@
use seedance2pro_client::generate::video::generate_happy_horse_1p0::{
use kinovi_web_client::generate::video::generate_happy_horse_1p0::{
generate_happy_horse_1p0, GenerateHappyHorse1p0Args, GenerateHappyHorse1p0Request,
};
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_video::generate_video_response::{GenerateVideoResponse, Seedance2proVideoResponsePayload};
use crate::generate::generate_video::generate_video_response::{GenerateVideoResponse, KinoviWebVideoResponsePayload};
#[derive(Debug, Clone)]
pub struct KinoviHappyHorse1p0RequestState {
@@ -13,7 +13,7 @@ pub struct KinoviHappyHorse1p0RequestState {
}
impl KinoviHappyHorse1p0RequestState {
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let session = &client.session;
let args = GenerateHappyHorse1p0Args {
@@ -24,9 +24,9 @@ impl KinoviHappyHorse1p0RequestState {
let response = generate_happy_horse_1p0(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
Ok(GenerateVideoResponse::Seedance2Pro(Seedance2proVideoResponsePayload {
Ok(GenerateVideoResponse::KinoviWeb(KinoviWebVideoResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
maybe_order_ids: response.order_ids,
@@ -47,12 +47,12 @@ mod tests {
use crate::api::image_ref::ImageRef;
use crate::api::router_provider::RouterProvider;
use crate::client::router_client::RouterClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
use crate::generate::generate_video::video_generation_draft_context::VideoGenerationDraftContext;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
mod aspect_ratio_tests {
@@ -66,7 +66,7 @@ mod tests {
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..happy_horse_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert!(matches!(response, GenerateVideoResponse::KinoviWeb(_)));
assert_eq!(1, 2, "Inspect output above");
}
@@ -78,7 +78,7 @@ mod tests {
aspect_ratio: Some(RouterAspectRatio::TallNineBySixteen),
..happy_horse_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert!(matches!(response, GenerateVideoResponse::KinoviWeb(_)));
assert_eq!(1, 2, "Inspect output above");
}
@@ -90,7 +90,7 @@ mod tests {
aspect_ratio: Some(RouterAspectRatio::Square),
..happy_horse_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert!(matches!(response, GenerateVideoResponse::KinoviWeb(_)));
assert_eq!(1, 2, "Inspect output above");
}
}
@@ -106,7 +106,7 @@ mod tests {
resolution: Some(RouterResolution::SevenTwentyP),
..happy_horse_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert!(matches!(response, GenerateVideoResponse::KinoviWeb(_)));
assert_eq!(1, 2, "Inspect output above");
}
@@ -118,7 +118,7 @@ mod tests {
resolution: Some(RouterResolution::TenEightyP),
..happy_horse_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert!(matches!(response, GenerateVideoResponse::KinoviWeb(_)));
assert_eq!(1, 2, "Inspect output above");
}
}
@@ -134,7 +134,7 @@ mod tests {
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..happy_horse_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert!(matches!(response, GenerateVideoResponse::KinoviWeb(_)));
assert_eq!(1, 2, "Inspect output above");
}
@@ -152,7 +152,7 @@ mod tests {
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..happy_horse_builder()
}, media_map).await;
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert!(matches!(response, GenerateVideoResponse::KinoviWeb(_)));
assert_eq!(1, 2, "Inspect output above");
}
@@ -172,7 +172,7 @@ mod tests {
duration_seconds: Some(15),
..happy_horse_builder()
}, media_map).await;
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert!(matches!(response, GenerateVideoResponse::KinoviWeb(_)));
assert_eq!(1, 2, "Inspect output above");
}
}
@@ -182,22 +182,22 @@ mod tests {
fn happy_horse_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
model: RouterVideoModel::HappyHorse1p0,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
duration_seconds: Some(4),
video_batch_count: Some(1),
..Default::default()
}
}
fn get_seedance2pro_client() -> RouterClient {
fn get_kinovi_web_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/seedance2pro_cookies.txt")
.expect("Failed to read seedance2pro cookies");
let session = Seedance2ProSession::from_cookies_string(cookies.trim().to_string());
RouterClient::Seedance2Pro(RouterSeedance2ProClient::new(session))
.expect("Failed to read kinovi_web cookies");
let session = KinoviWebSession::from_cookies_string(cookies.trim().to_string());
RouterClient::KinoviWeb(RouterKinoviWebClient::new(session))
}
async fn run_pipeline(builder: GenerateVideoRequestBuilder) -> GenerateVideoResponse {
let client = get_seedance2pro_client();
let client = get_kinovi_web_client();
let draft_or_request = builder.build2().expect("build2 should succeed");
let draft = match draft_or_request {
@@ -214,7 +214,7 @@ mod tests {
let response = request.send_request(&client).await.expect("send_request should succeed");
match &response {
GenerateVideoResponse::Seedance2Pro(p) => {
GenerateVideoResponse::KinoviWeb(p) => {
println!("task_id={}, order_id={}", p.task_id, p.order_id);
}
other => println!("response: {:?}", other),
@@ -227,7 +227,7 @@ mod tests {
builder: GenerateVideoRequestBuilder,
media_map: HashMap<MediaFileToken, String>,
) -> GenerateVideoResponse {
let client = get_seedance2pro_client();
let client = get_kinovi_web_client();
let draft_or_request = builder.build2().expect("build2 should succeed");
let draft = match draft_or_request {
@@ -245,7 +245,7 @@ mod tests {
let response = request.send_request(&client).await.expect("send_request should succeed");
match &response {
GenerateVideoResponse::Seedance2Pro(p) => {
GenerateVideoResponse::KinoviWeb(p) => {
println!("task_id={}, order_id={}", p.task_id, p.order_id);
}
other => println!("response: {:?}", other),
@@ -2,5 +2,6 @@ pub mod happy_horse_1p0;
pub mod seedance_2p0;
pub mod seedance_2p0_fast;
pub mod seedance_2p0_mini;
pub mod seedance_2p5_preview;
pub(crate) mod resolve;
pub(crate) mod upload;
@@ -1,6 +1,6 @@
use std::collections::HashMap;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use kinovi_web_client::creds::kinovi_web_session::KinoviWebSession;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::audio_list_ref::AudioListRef;
@@ -10,7 +10,7 @@ use crate::api::image_ref::ImageRef;
use crate::api::video_list_ref::VideoListRef;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_video::providers::kinovi::upload::upload_to_seedance2pro;
use crate::generate::generate_video::providers::kinovi::upload::upload_to_kinovi_web;
use crate::generate::generate_video::video_generation_draft_context::VideoGenerationDraftContext;
/// Either resolved URLs or unresolved media file tokens.
@@ -40,9 +40,9 @@ pub(crate) fn audio_list_ref_into_urls_or_tokens(list: AudioListRef) -> UrlsOrTo
}
}
/// Resolve a single ImageRef and upload to Seedance2Pro CDN.
/// Resolve a single ImageRef and upload to KinoviWeb CDN.
pub(crate) async fn resolve_and_upload_single(
session: &Seedance2ProSession,
session: &KinoviWebSession,
image_ref: Option<ImageRef>,
maybe_map: Option<&HashMap<MediaFileToken, String>>,
) -> Result<Option<String>, ArtcraftRouterError> {
@@ -51,13 +51,13 @@ pub(crate) async fn resolve_and_upload_single(
Some(ImageRef::Url(url)) => url,
Some(ImageRef::MediaFileToken(token)) => resolve_token(maybe_map, &token)?,
};
Ok(Some(upload_to_seedance2pro(session, &source_url).await?))
Ok(Some(upload_to_kinovi_web(session, &source_url).await?))
}
/// Resolve a list of refs to URLs and upload each to Seedance2Pro CDN.
/// Resolve a list of refs to URLs and upload each to KinoviWeb CDN.
/// Order is preserved.
pub(crate) async fn resolve_and_upload_list(
session: &Seedance2ProSession,
session: &KinoviWebSession,
urls_or_tokens: Option<UrlsOrTokens>,
maybe_map: Option<&HashMap<MediaFileToken, String>>,
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
@@ -71,7 +71,7 @@ pub(crate) async fn resolve_and_upload_list(
let mut uploaded = Vec::with_capacity(source_urls.len());
for url in &source_urls {
uploaded.push(upload_to_seedance2pro(session, url).await?);
uploaded.push(upload_to_kinovi_web(session, url).await?);
}
Ok(Some(uploaded))
}
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::video::generate_seedance_2p0::{
use kinovi_web_client::generate::video::generate_seedance_2p0::{
KinoviSeedance2p0AspectRatio as KinoviAspectRatio,
KinoviSeedance2p0Bitrate as KinoviBitrate,
KinoviSeedance2p0BatchCount as KinoviBatchCount,
@@ -212,7 +212,7 @@ fn plan_bitrate(bitrate: Option<RouterBitrate>) -> Option<KinoviBitrate> {
#[cfg(test)]
mod tests {
use seedance2pro_client::generate::video::generate_seedance_2p0::{
use kinovi_web_client::generate::video::generate_seedance_2p0::{
KinoviSeedance2p0AspectRatio as KinoviAspectRatio,
KinoviSeedance2p0BatchCount as KinoviBatchCount,
KinoviSeedance2p0OutputResolution as KinoviOutputResolution,
@@ -242,27 +242,27 @@ mod tests {
#[test]
fn prompt_is_passed_through() {
let draft = unwrap_draft(build_kinovi_seedance_2p0(seedance2pro_builder()));
let draft = unwrap_draft(build_kinovi_seedance_2p0(kinovi_web_builder()));
assert_eq!(draft.prompt, "a cat dancing");
}
#[test]
fn prompt_defaults_to_empty() {
let builder = GenerateVideoRequestBuilder { prompt: None, ..seedance2pro_builder() };
let builder = GenerateVideoRequestBuilder { prompt: None, ..kinovi_web_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert_eq!(draft.prompt, "");
}
#[test]
fn duration_seconds_converted() {
let builder = GenerateVideoRequestBuilder { duration_seconds: Some(10), ..seedance2pro_builder() };
let builder = GenerateVideoRequestBuilder { duration_seconds: Some(10), ..kinovi_web_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert_eq!(draft.duration_seconds, 10);
}
#[test]
fn duration_defaults_to_5() {
let builder = GenerateVideoRequestBuilder { duration_seconds: None, ..seedance2pro_builder() };
let builder = GenerateVideoRequestBuilder { duration_seconds: None, ..kinovi_web_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert_eq!(draft.duration_seconds, 5);
}
@@ -271,7 +271,7 @@ mod tests {
fn duration_clamped_to_max() {
let builder = GenerateVideoRequestBuilder {
duration_seconds: Some(99),
..seedance2pro_builder()
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert_eq!(draft.duration_seconds, 15);
@@ -279,21 +279,21 @@ mod tests {
#[test]
fn batch_count_one() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(1), ..seedance2pro_builder() };
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(1), ..kinovi_web_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::One));
}
#[test]
fn batch_count_two() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(2), ..seedance2pro_builder() };
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(2), ..kinovi_web_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::Two));
}
#[test]
fn batch_count_four() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(4), ..seedance2pro_builder() };
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(4), ..kinovi_web_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::Four));
}
@@ -308,7 +308,7 @@ mod tests {
fn aspect_ratio_wide() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..seedance2pro_builder()
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Landscape16x9));
@@ -318,7 +318,7 @@ mod tests {
fn aspect_ratio_tall() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(RouterAspectRatio::TallNineBySixteen),
..seedance2pro_builder()
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Portrait9x16));
@@ -328,7 +328,7 @@ mod tests {
fn aspect_ratio_square() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(RouterAspectRatio::Square),
..seedance2pro_builder()
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Square1x1));
@@ -336,7 +336,7 @@ mod tests {
#[test]
fn aspect_ratio_defaults_to_landscape() {
let builder = GenerateVideoRequestBuilder { aspect_ratio: None, ..seedance2pro_builder() };
let builder = GenerateVideoRequestBuilder { aspect_ratio: None, ..kinovi_web_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Landscape16x9));
}
@@ -351,7 +351,7 @@ mod tests {
fn resolution_480p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::FourEightyP),
..seedance2pro_builder()
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::FourEightyP)));
@@ -361,7 +361,7 @@ mod tests {
fn resolution_720p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::SevenTwentyP),
..seedance2pro_builder()
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
@@ -371,7 +371,7 @@ mod tests {
fn resolution_1080p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::TenEightyP),
..seedance2pro_builder()
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::TenEightyP)));
@@ -381,7 +381,7 @@ mod tests {
fn resolution_4k() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::FourK),
..seedance2pro_builder()
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::FourK)));
@@ -389,7 +389,7 @@ mod tests {
#[test]
fn resolution_none() {
let builder = GenerateVideoRequestBuilder { resolution: None, ..seedance2pro_builder() };
let builder = GenerateVideoRequestBuilder { resolution: None, ..kinovi_web_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(draft.resolution.is_none());
}
@@ -399,7 +399,7 @@ mod tests {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::ThreeK),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..seedance2pro_builder()
..kinovi_web_builder()
};
assert!(build_kinovi_seedance_2p0(builder).is_err());
}
@@ -409,7 +409,7 @@ mod tests {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::ThreeK),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..seedance2pro_builder()
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::TenEightyP)));
@@ -425,7 +425,7 @@ mod tests {
fn bitrate_high() {
let builder = GenerateVideoRequestBuilder {
bitrate: Some(RouterBitrate::High),
..seedance2pro_builder()
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.bitrate, Some(KinoviBitrate::High)));
@@ -435,7 +435,7 @@ mod tests {
fn bitrate_normal_maps_to_standard() {
let builder = GenerateVideoRequestBuilder {
bitrate: Some(RouterBitrate::Normal),
..seedance2pro_builder()
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(draft.bitrate.is_none());
@@ -443,7 +443,7 @@ mod tests {
#[test]
fn bitrate_none_maps_to_standard() {
let builder = GenerateVideoRequestBuilder { bitrate: None, ..seedance2pro_builder() };
let builder = GenerateVideoRequestBuilder { bitrate: None, ..kinovi_web_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(draft.bitrate.is_none());
}
@@ -456,7 +456,7 @@ mod tests {
#[test]
fn unhandled_state_is_present() {
let draft = unwrap_draft(build_kinovi_seedance_2p0(seedance2pro_builder()));
let draft = unwrap_draft(build_kinovi_seedance_2p0(kinovi_web_builder()));
assert!(draft.unhandled_request_state.is_some());
}
@@ -464,7 +464,7 @@ mod tests {
fn start_frame_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
start_frame: Some(ImageRef::Url("https://example.com/start.jpg".to_string())),
..seedance2pro_builder()
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -475,7 +475,7 @@ mod tests {
fn end_frame_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
end_frame: Some(ImageRef::Url("https://example.com/end.jpg".to_string())),
..seedance2pro_builder()
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -486,7 +486,7 @@ mod tests {
fn start_frame_media_token_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_test123".to_string()))),
..seedance2pro_builder()
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -500,7 +500,7 @@ mod tests {
"https://example.com/ref1.jpg".to_string(),
"https://example.com/ref2.jpg".to_string(),
])),
..seedance2pro_builder()
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -520,7 +520,7 @@ mod tests {
reference_videos: Some(VideoListRef::Urls(vec![
"https://example.com/vid.mp4".to_string(),
])),
..seedance2pro_builder()
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -533,7 +533,7 @@ mod tests {
reference_audio: Some(AudioListRef::Urls(vec![
"https://example.com/audio.mp3".to_string(),
])),
..seedance2pro_builder()
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -547,7 +547,7 @@ mod tests {
CharacterToken::new("char_abc".to_string()),
CharacterToken::new("char_def".to_string()),
])),
..seedance2pro_builder()
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -563,7 +563,7 @@ mod tests {
#[test]
fn empty_refs_are_none_in_unhandled() {
let draft = unwrap_draft(build_kinovi_seedance_2p0(seedance2pro_builder()));
let draft = unwrap_draft(build_kinovi_seedance_2p0(kinovi_web_builder()));
let remaining = draft.unhandled_request_state.unwrap();
assert!(remaining.start_frame.is_none());
assert!(remaining.end_frame.is_none());
@@ -592,7 +592,7 @@ mod tests {
reference_character_tokens: Some(CharacterListRef::CharacterTokens(vec![
CharacterToken::new("char_xyz".to_string()),
])),
..seedance2pro_builder()
..kinovi_web_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
@@ -613,9 +613,9 @@ mod tests {
// ── Helpers ──
fn seedance2pro_builder() -> GenerateVideoRequestBuilder {
fn kinovi_web_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("a cat dancing".to_string()),
duration_seconds: Some(5),
video_batch_count: Some(1),
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::video::generate_seedance_2p0::{
use kinovi_web_client::generate::video::generate_seedance_2p0::{
GenerateSeedance2p0Request, KinoviSeedance2p0BatchCount,
KinoviSeedance2p0OutputResolution,
};
@@ -85,7 +85,7 @@ impl KinoviSeedance2p0CostState {
#[cfg(test)]
mod tests {
use super::*;
use seedance2pro_client::generate::video::generate_seedance_2p0::{
use kinovi_web_client::generate::video::generate_seedance_2p0::{
KinoviSeedance2p0OutputResolution as KinoviOutputResolution,
KinoviSeedance2p0BatchCount as KinoviBatchCount,
};
@@ -177,7 +177,7 @@ mod tests {
// ── 4K pricing ──
//
// Routes to the seedance2pro_client: 4K base is 200 credits/s (vs 90 at 1080p).
// Routes to the kinovi_web_client: 4K base is 200 credits/s (vs 90 at 1080p).
// Credits → cents at 243 credits/$1, rounded up.
mod pricing_4k {
@@ -202,7 +202,7 @@ mod tests {
}
}
// ── 4K: router cost must match the seedance2pro_client binding ──
// ── 4K: router cost must match the kinovi_web_client binding ──
//
// The router delegates to GenerateSeedance2p0Request::calculate_costs(), so these
// assert the exact 4K numbers AND cross-check the router against the binding for
@@ -213,7 +213,7 @@ mod tests {
mod four_k_matches_binding {
use super::*;
use seedance2pro_client::generate::video::generate_seedance_2p0::GenerateSeedance2p0Request;
use kinovi_web_client::generate::video::generate_seedance_2p0::GenerateSeedance2p0Request;
/// Router-side 4K cost (batch 1) for a duration / video-reference combo.
fn router(duration_seconds: u8, has_video_reference: bool) -> VideoGenerationCostEstimate {
@@ -226,7 +226,7 @@ mod tests {
.estimate_cost()
}
/// The seedance2pro_client binding's own 4K cost for the same inputs.
/// The kinovi_web_client binding's own 4K cost for the same inputs.
/// Returns (kinovi_credits, usd_cents_rounded_up).
fn binding(duration_seconds: u8, has_video_reference: bool) -> (u64, u64) {
let reference_video_urls = if has_video_reference {
@@ -579,7 +579,7 @@ mod tests {
};
let builder = GenerateVideoRequestBuilder {
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
resolution,
reference_videos,
duration_seconds: Some(duration_seconds),
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::video::generate_seedance_2p0::{
use kinovi_web_client::generate::video::generate_seedance_2p0::{
GenerateSeedance2p0Request, KinoviSeedance2p0AspectRatio, KinoviSeedance2p0Bitrate,
KinoviSeedance2p0BatchCount, KinoviSeedance2p0OutputResolution,
};
@@ -47,7 +47,7 @@ impl KinoviSeedance2p0DraftState {
&mut self,
draft_context: &VideoGenerationDraftContext<'_>,
) -> Result<KinoviSeedance2p0RequestState, ArtcraftRouterError> {
let client = draft_context.get_seedance2pro_client_ref()?;
let client = draft_context.get_kinovi_web_client_ref()?;
let session = &client.session;
let mut start_frame_url = None;
@@ -1,11 +1,11 @@
use seedance2pro_client::generate::video::generate_seedance_2p0::{
use kinovi_web_client::generate::video::generate_seedance_2p0::{
generate_seedance_2p0, GenerateSeedance2p0Args, GenerateSeedance2p0Request,
};
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_video::generate_video_response::{GenerateVideoResponse, Seedance2proVideoResponsePayload};
use crate::generate::generate_video::generate_video_response::{GenerateVideoResponse, KinoviWebVideoResponsePayload};
#[derive(Debug, Clone)]
pub struct KinoviSeedance2p0RequestState {
@@ -14,7 +14,7 @@ pub struct KinoviSeedance2p0RequestState {
}
impl KinoviSeedance2p0RequestState {
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let session = &client.session;
let args = GenerateSeedance2p0Args {
@@ -25,9 +25,9 @@ impl KinoviSeedance2p0RequestState {
let response = generate_seedance_2p0(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
Ok(GenerateVideoResponse::Seedance2Pro(Seedance2proVideoResponsePayload {
Ok(GenerateVideoResponse::KinoviWeb(KinoviWebVideoResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
maybe_order_ids: response.order_ids,
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::video::generate_seedance_2p0_fast::{
use kinovi_web_client::generate::video::generate_seedance_2p0_fast::{
KinoviSeedance2p0FastAspectRatio as KinoviAspectRatio,
KinoviSeedance2p0FastBitrate as KinoviBitrate,
KinoviSeedance2p0FastBatchCount as KinoviBatchCount,
@@ -226,7 +226,7 @@ fn plan_bitrate(bitrate: Option<RouterBitrate>) -> Option<KinoviBitrate> {
#[cfg(test)]
mod tests {
use seedance2pro_client::generate::video::generate_seedance_2p0_fast::{
use kinovi_web_client::generate::video::generate_seedance_2p0_fast::{
KinoviSeedance2p0FastAspectRatio as KinoviAspectRatio,
KinoviSeedance2p0FastBatchCount as KinoviBatchCount,
KinoviSeedance2p0FastOutputResolution as KinoviOutputResolution,
@@ -258,27 +258,27 @@ mod tests {
#[test]
fn prompt_is_passed_through() {
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(seedance2pro_fast_builder()));
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(kinovi_web_fast_builder()));
assert_eq!(draft.prompt, "a cat dancing");
}
#[test]
fn prompt_defaults_to_empty() {
let builder = GenerateVideoRequestBuilder { prompt: None, ..seedance2pro_fast_builder() };
let builder = GenerateVideoRequestBuilder { prompt: None, ..kinovi_web_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert_eq!(draft.prompt, "");
}
#[test]
fn duration_seconds_converted() {
let builder = GenerateVideoRequestBuilder { duration_seconds: Some(10), ..seedance2pro_fast_builder() };
let builder = GenerateVideoRequestBuilder { duration_seconds: Some(10), ..kinovi_web_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert_eq!(draft.duration_seconds, 10);
}
#[test]
fn duration_defaults_to_5() {
let builder = GenerateVideoRequestBuilder { duration_seconds: None, ..seedance2pro_fast_builder() };
let builder = GenerateVideoRequestBuilder { duration_seconds: None, ..kinovi_web_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert_eq!(draft.duration_seconds, 5);
}
@@ -287,7 +287,7 @@ mod tests {
fn duration_clamped_to_max() {
let builder = GenerateVideoRequestBuilder {
duration_seconds: Some(99),
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert_eq!(draft.duration_seconds, 15);
@@ -295,21 +295,21 @@ mod tests {
#[test]
fn batch_count_one() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(1), ..seedance2pro_fast_builder() };
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(1), ..kinovi_web_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::One));
}
#[test]
fn batch_count_two() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(2), ..seedance2pro_fast_builder() };
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(2), ..kinovi_web_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::Two));
}
#[test]
fn batch_count_four() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(4), ..seedance2pro_fast_builder() };
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(4), ..kinovi_web_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::Four));
}
@@ -324,7 +324,7 @@ mod tests {
fn aspect_ratio_wide() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Landscape16x9));
@@ -334,7 +334,7 @@ mod tests {
fn aspect_ratio_tall() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(RouterAspectRatio::TallNineBySixteen),
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Portrait9x16));
@@ -344,7 +344,7 @@ mod tests {
fn aspect_ratio_square() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(RouterAspectRatio::Square),
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Square1x1));
@@ -352,7 +352,7 @@ mod tests {
#[test]
fn aspect_ratio_defaults_to_landscape() {
let builder = GenerateVideoRequestBuilder { aspect_ratio: None, ..seedance2pro_fast_builder() };
let builder = GenerateVideoRequestBuilder { aspect_ratio: None, ..kinovi_web_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Landscape16x9));
}
@@ -367,7 +367,7 @@ mod tests {
fn resolution_480p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::FourEightyP),
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::FourEightyP)));
@@ -377,7 +377,7 @@ mod tests {
fn resolution_720p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::SevenTwentyP),
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
@@ -388,7 +388,7 @@ mod tests {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::TenEightyP),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
@@ -399,7 +399,7 @@ mod tests {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::TenEightyP),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
@@ -410,14 +410,14 @@ mod tests {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::TenEightyP),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
assert!(build_kinovi_seedance_2p0_fast(builder).is_err());
}
#[test]
fn resolution_none() {
let builder = GenerateVideoRequestBuilder { resolution: None, ..seedance2pro_fast_builder() };
let builder = GenerateVideoRequestBuilder { resolution: None, ..kinovi_web_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(draft.resolution.is_none());
}
@@ -427,7 +427,7 @@ mod tests {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::FourK),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
assert!(build_kinovi_seedance_2p0_fast(builder).is_err());
}
@@ -437,7 +437,7 @@ mod tests {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::FourK),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
@@ -453,7 +453,7 @@ mod tests {
fn bitrate_high() {
let builder = GenerateVideoRequestBuilder {
bitrate: Some(RouterBitrate::High),
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.bitrate, Some(KinoviBitrate::High)));
@@ -463,7 +463,7 @@ mod tests {
fn bitrate_normal_maps_to_standard() {
let builder = GenerateVideoRequestBuilder {
bitrate: Some(RouterBitrate::Normal),
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(draft.bitrate.is_none());
@@ -471,7 +471,7 @@ mod tests {
#[test]
fn bitrate_none_maps_to_standard() {
let builder = GenerateVideoRequestBuilder { bitrate: None, ..seedance2pro_fast_builder() };
let builder = GenerateVideoRequestBuilder { bitrate: None, ..kinovi_web_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(draft.bitrate.is_none());
}
@@ -484,7 +484,7 @@ mod tests {
#[test]
fn unhandled_state_is_present() {
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(seedance2pro_fast_builder()));
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(kinovi_web_fast_builder()));
assert!(draft.unhandled_request_state.is_some());
}
@@ -492,7 +492,7 @@ mod tests {
fn start_frame_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
start_frame: Some(ImageRef::Url("https://example.com/start.jpg".to_string())),
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -503,7 +503,7 @@ mod tests {
fn end_frame_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
end_frame: Some(ImageRef::Url("https://example.com/end.jpg".to_string())),
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -514,7 +514,7 @@ mod tests {
fn start_frame_media_token_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_test123".to_string()))),
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -528,7 +528,7 @@ mod tests {
"https://example.com/ref1.jpg".to_string(),
"https://example.com/ref2.jpg".to_string(),
])),
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -548,7 +548,7 @@ mod tests {
reference_videos: Some(VideoListRef::Urls(vec![
"https://example.com/vid.mp4".to_string(),
])),
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -561,7 +561,7 @@ mod tests {
reference_audio: Some(AudioListRef::Urls(vec![
"https://example.com/audio.mp3".to_string(),
])),
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -575,7 +575,7 @@ mod tests {
CharacterToken::new("char_abc".to_string()),
CharacterToken::new("char_def".to_string()),
])),
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
@@ -591,7 +591,7 @@ mod tests {
#[test]
fn empty_refs_are_none_in_unhandled() {
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(seedance2pro_fast_builder()));
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(kinovi_web_fast_builder()));
let remaining = draft.unhandled_request_state.unwrap();
assert!(remaining.start_frame.is_none());
assert!(remaining.end_frame.is_none());
@@ -620,7 +620,7 @@ mod tests {
reference_character_tokens: Some(CharacterListRef::CharacterTokens(vec![
CharacterToken::new("char_xyz".to_string()),
])),
..seedance2pro_fast_builder()
..kinovi_web_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
@@ -641,10 +641,10 @@ mod tests {
// ── Helpers ──
fn seedance2pro_fast_builder() -> GenerateVideoRequestBuilder {
fn kinovi_web_fast_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0Fast,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("a cat dancing".to_string()),
duration_seconds: Some(5),
video_batch_count: Some(1),
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::video::generate_seedance_2p0_fast::{
use kinovi_web_client::generate::video::generate_seedance_2p0_fast::{
GenerateSeedance2p0FastRequest, KinoviSeedance2p0FastBatchCount,
KinoviSeedance2p0FastOutputResolution,
};
@@ -86,7 +86,7 @@ impl KinoviSeedance2p0FastCostState {
mod tests {
use super::*;
use crate::api::router_resolution::RouterResolution;
use seedance2pro_client::generate::video::generate_seedance_2p0_fast::{
use kinovi_web_client::generate::video::generate_seedance_2p0_fast::{
KinoviSeedance2p0FastOutputResolution as KinoviOutputResolution,
KinoviSeedance2p0FastBatchCount as KinoviBatchCount,
};
@@ -386,7 +386,7 @@ mod tests {
let builder = GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0Fast,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
resolution,
reference_videos,
duration_seconds: Some(duration_seconds),
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::video::generate_seedance_2p0_fast::{
use kinovi_web_client::generate::video::generate_seedance_2p0_fast::{
GenerateSeedance2p0FastRequest, KinoviSeedance2p0FastAspectRatio, KinoviSeedance2p0FastBitrate,
KinoviSeedance2p0FastBatchCount, KinoviSeedance2p0FastOutputResolution,
};
@@ -44,7 +44,7 @@ impl KinoviSeedance2p0FastDraftState {
&mut self,
draft_context: &VideoGenerationDraftContext<'_>,
) -> Result<KinoviSeedance2p0FastRequestState, ArtcraftRouterError> {
let client = draft_context.get_seedance2pro_client_ref()?;
let client = draft_context.get_kinovi_web_client_ref()?;
let session = &client.session;
let mut start_frame_url = None;
@@ -1,11 +1,11 @@
use seedance2pro_client::generate::video::generate_seedance_2p0_fast::{
use kinovi_web_client::generate::video::generate_seedance_2p0_fast::{
generate_seedance_2p0_fast, GenerateSeedance2p0FastArgs, GenerateSeedance2p0FastRequest,
};
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_video::generate_video_response::{GenerateVideoResponse, Seedance2proVideoResponsePayload};
use crate::generate::generate_video::generate_video_response::{GenerateVideoResponse, KinoviWebVideoResponsePayload};
#[derive(Debug, Clone)]
pub struct KinoviSeedance2p0FastRequestState {
@@ -14,7 +14,7 @@ pub struct KinoviSeedance2p0FastRequestState {
}
impl KinoviSeedance2p0FastRequestState {
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let session = &client.session;
let args = GenerateSeedance2p0FastArgs {
@@ -25,9 +25,9 @@ impl KinoviSeedance2p0FastRequestState {
let response = generate_seedance_2p0_fast(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
Ok(GenerateVideoResponse::Seedance2Pro(Seedance2proVideoResponsePayload {
Ok(GenerateVideoResponse::KinoviWeb(KinoviWebVideoResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
maybe_order_ids: response.order_ids,
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::video::generate_seedance_2p0_mini::{
use kinovi_web_client::generate::video::generate_seedance_2p0_mini::{
KinoviSeedance2p0MiniAspectRatio as KinoviAspectRatio,
KinoviSeedance2p0MiniBitrate as KinoviBitrate,
KinoviSeedance2p0MiniBatchCount as KinoviBatchCount,
@@ -221,7 +221,7 @@ fn plan_bitrate(bitrate: Option<RouterBitrate>) -> Option<KinoviBitrate> {
#[cfg(test)]
mod tests {
use seedance2pro_client::generate::video::generate_seedance_2p0_mini::{
use kinovi_web_client::generate::video::generate_seedance_2p0_mini::{
KinoviSeedance2p0MiniAspectRatio as KinoviAspectRatio,
KinoviSeedance2p0MiniBatchCount as KinoviBatchCount,
KinoviSeedance2p0MiniOutputResolution as KinoviOutputResolution,
@@ -491,7 +491,7 @@ mod tests {
fn mini_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0Mini,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
prompt: Some("a cat dancing".to_string()),
duration_seconds: Some(5),
video_batch_count: Some(1),
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::video::generate_seedance_2p0_mini::{
use kinovi_web_client::generate::video::generate_seedance_2p0_mini::{
GenerateSeedance2p0MiniRequest, KinoviSeedance2p0MiniBatchCount,
KinoviSeedance2p0MiniOutputResolution,
};
@@ -89,7 +89,7 @@ impl KinoviSeedance2p0MiniCostState {
mod tests {
use super::*;
use crate::api::router_resolution::RouterResolution;
use seedance2pro_client::generate::video::generate_seedance_2p0_mini::{
use kinovi_web_client::generate::video::generate_seedance_2p0_mini::{
KinoviSeedance2p0MiniOutputResolution as KinoviOutputResolution,
KinoviSeedance2p0MiniBatchCount as KinoviBatchCount,
};
@@ -328,7 +328,7 @@ mod tests {
let builder = GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p0Mini,
provider: RouterProvider::Seedance2Pro,
provider: RouterProvider::KinoviWeb,
resolution,
reference_videos,
duration_seconds: Some(duration_seconds),
@@ -1,4 +1,4 @@
use seedance2pro_client::generate::video::generate_seedance_2p0_mini::{
use kinovi_web_client::generate::video::generate_seedance_2p0_mini::{
GenerateSeedance2p0MiniRequest, KinoviSeedance2p0MiniAspectRatio, KinoviSeedance2p0MiniBitrate,
KinoviSeedance2p0MiniBatchCount, KinoviSeedance2p0MiniOutputResolution,
};
@@ -44,7 +44,7 @@ impl KinoviSeedance2p0MiniDraftState {
&mut self,
draft_context: &VideoGenerationDraftContext<'_>,
) -> Result<KinoviSeedance2p0MiniRequestState, ArtcraftRouterError> {
let client = draft_context.get_seedance2pro_client_ref()?;
let client = draft_context.get_kinovi_web_client_ref()?;
let session = &client.session;
let mut start_frame_url = None;
@@ -1,11 +1,11 @@
use seedance2pro_client::generate::video::generate_seedance_2p0_mini::{
use kinovi_web_client::generate::video::generate_seedance_2p0_mini::{
generate_seedance_2p0_mini, GenerateSeedance2p0MiniArgs, GenerateSeedance2p0MiniRequest,
};
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::client::router_kinovi_web_client::RouterKinoviWebClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_video::generate_video_response::{GenerateVideoResponse, Seedance2proVideoResponsePayload};
use crate::generate::generate_video::generate_video_response::{GenerateVideoResponse, KinoviWebVideoResponsePayload};
#[derive(Debug, Clone)]
pub struct KinoviSeedance2p0MiniRequestState {
@@ -14,7 +14,7 @@ pub struct KinoviSeedance2p0MiniRequestState {
}
impl KinoviSeedance2p0MiniRequestState {
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
pub async fn send(&self, client: &RouterKinoviWebClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let session = &client.session;
let args = GenerateSeedance2p0MiniArgs {
@@ -25,9 +25,9 @@ impl KinoviSeedance2p0MiniRequestState {
let response = generate_seedance_2p0_mini(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::KinoviWeb(err)))?;
Ok(GenerateVideoResponse::Seedance2Pro(Seedance2proVideoResponsePayload {
Ok(GenerateVideoResponse::KinoviWeb(KinoviWebVideoResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
maybe_order_ids: response.order_ids,
@@ -0,0 +1,498 @@
use kinovi_web_client::generate::video::generate_seedance_2p5_preview::{
KinoviSeedance2p5PreviewAspectRatio as KinoviAspectRatio,
KinoviSeedance2p5PreviewOutputResolution as KinoviOutputResolution,
};
use crate::api::router_aspect_ratio::RouterAspectRatio;
use crate::api::router_resolution::RouterResolution;
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::providers::kinovi::seedance_2p5_preview::draft::{KinoviSeedance2p5PreviewDraftState, KinoviSeedance2p5PreviewRemainingItems};
use crate::generate::generate_video::video_generation_draft::VideoGenerationDraftRequest;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
pub fn build_kinovi_seedance_2p5_preview(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
let draft = do_build_kinovi_seedance_2p5_preview(builder)?;
Ok(VideoGenerationDraftOrRequest::Draft(VideoGenerationDraftRequest::KinoviSeedance2p5Preview(draft)))
}
fn do_build_kinovi_seedance_2p5_preview(mut builder: GenerateVideoRequestBuilder) -> Result<KinoviSeedance2p5PreviewDraftState, ArtcraftRouterError> {
let strategy = builder.request_mismatch_mitigation_strategy;
// Seedance 2.5 Preview only operates in reference mode — there is no
// keyframe (start/end frame) mode. Dropping a caller's frames would change
// the meaning of the request, so this errors regardless of strategy.
if builder.start_frame.is_some() {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "start_frame",
value: "Seedance 2.5 Preview has no keyframe mode; use reference_images".to_string(),
}));
}
if builder.end_frame.is_some() {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "end_frame",
value: "Seedance 2.5 Preview has no keyframe mode; use reference_images".to_string(),
}));
}
// Kinovi character references are not supported by the 2.5 Preview API.
if builder.reference_character_tokens.is_some() {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "reference_character_tokens",
value: "Seedance 2.5 Preview does not support character references".to_string(),
}));
}
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy)?;
let resolution = plan_output_resolution(builder.resolution.take(), strategy)?;
plan_batch_count(builder.video_batch_count.take(), strategy)?;
let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?;
let prompt = builder.prompt.take().unwrap_or_default();
// NB: `builder.bitrate` is intentionally ignored — 2.5 Preview has no
// bitrate option (and bitrate never affects cost).
let unhandled_request_state = KinoviSeedance2p5PreviewRemainingItems {
reference_images: builder.reference_images.take(),
reference_videos: builder.reference_videos.take(),
reference_audio: builder.reference_audio.take(),
};
Ok(KinoviSeedance2p5PreviewDraftState {
aspect_ratio,
resolution,
duration_seconds,
prompt,
unhandled_request_state: Some(unhandled_request_state),
})
}
// ── Plan helpers ──
// Seedance 2.5 Preview supports all six aspect ratios:
// 16:9, 21:9, 9:16, 1:1, 4:3, 3:4. All supported ratios cost the same, so
// both upgrade and downgrade pick the nearest match.
fn plan_aspect_ratio(
aspect_ratio: Option<RouterAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<KinoviAspectRatio, ArtcraftRouterError> {
match aspect_ratio {
// No preference or auto — default to landscape
None
| Some(RouterAspectRatio::Auto)
| Some(RouterAspectRatio::Auto2k)
| Some(RouterAspectRatio::Auto4k) => Ok(KinoviAspectRatio::Landscape16x9),
// Direct mappings
Some(RouterAspectRatio::WideSixteenByNine) | Some(RouterAspectRatio::Wide) => {
Ok(KinoviAspectRatio::Landscape16x9)
}
Some(RouterAspectRatio::TallNineBySixteen) | Some(RouterAspectRatio::Tall) => {
Ok(KinoviAspectRatio::Portrait9x16)
}
Some(RouterAspectRatio::Square) | Some(RouterAspectRatio::SquareHd) => {
Ok(KinoviAspectRatio::Square1x1)
}
Some(RouterAspectRatio::WideFourByThree) => Ok(KinoviAspectRatio::Standard4x3),
Some(RouterAspectRatio::TallThreeByFour) => Ok(KinoviAspectRatio::Portrait3x4),
Some(RouterAspectRatio::WideTwentyOneByNine) => Ok(KinoviAspectRatio::UltraWide21x9),
// Mismatches — apply strategy
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(nearest_aspect_ratio(unsupported))
}
},
}
}
/// Pick the nearest supported aspect ratio by AR value (width / height).
fn nearest_aspect_ratio(aspect_ratio: RouterAspectRatio) -> KinoviAspectRatio {
match aspect_ratio {
RouterAspectRatio::WideFiveByFour => KinoviAspectRatio::Standard4x3, // 1.25, nearest 1.33
RouterAspectRatio::WideThreeByTwo => KinoviAspectRatio::Standard4x3, // 1.50, nearest 1.33
RouterAspectRatio::TallFourByFive => KinoviAspectRatio::Portrait3x4, // 0.80, nearest 0.75
RouterAspectRatio::TallTwoByThree => KinoviAspectRatio::Portrait3x4, // 0.67, nearest 0.75
RouterAspectRatio::TallNineByTwentyOne => KinoviAspectRatio::Portrait9x16, // 0.43, nearest 0.56
_ => KinoviAspectRatio::Square1x1,
}
}
// Seedance 2.5 Preview supports output resolutions: 480p and 720p only.
// 1080p (and higher) is NOT supported — downgrade to 720p or error based on strategy.
fn plan_output_resolution(
resolution: Option<RouterResolution>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<KinoviOutputResolution>, ArtcraftRouterError> {
match resolution {
None => Ok(None),
// Direct mappings
Some(RouterResolution::FourEightyP) => Ok(Some(KinoviOutputResolution::FourEightyP)),
Some(RouterResolution::SevenTwentyP) => Ok(Some(KinoviOutputResolution::SevenTwentyP)),
// 1080p is not supported — handle via strategy
Some(RouterResolution::TenEightyP) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "resolution",
value: format!("{:?}", RouterResolution::TenEightyP),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
// 1080p not available — downgrade to 720p (highest supported)
Ok(Some(KinoviOutputResolution::SevenTwentyP))
}
},
// Other unsupported resolutions
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "resolution",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(Some(match unsupported {
RouterResolution::HalfK => KinoviOutputResolution::FourEightyP,
_ => KinoviOutputResolution::SevenTwentyP,
}))
}
},
}
}
// Seedance 2.5 Preview generates a single video per request (no batching).
fn plan_batch_count(
video_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<(), ArtcraftRouterError> {
let count = video_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(()),
// Over the maximum of 1.
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "video_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(()),
},
}
}
// Seedance 2.5 Preview supports durations of 4–30 seconds.
fn plan_duration(
duration_seconds: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<u8, ArtcraftRouterError> {
const MIN: u16 = 4;
const MAX: u16 = 30;
const DEFAULT: u8 = 5;
match duration_seconds {
None => Ok(DEFAULT),
Some(d) if d >= MIN && d <= MAX => Ok(d as u8),
Some(d) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "duration_seconds",
value: format!("{}", d),
}))
}
_ => Ok(d.clamp(MIN, MAX) as u8),
},
}
}
#[cfg(test)]
mod tests {
use kinovi_web_client::generate::video::generate_seedance_2p5_preview::{
KinoviSeedance2p5PreviewAspectRatio as KinoviAspectRatio,
KinoviSeedance2p5PreviewOutputResolution as KinoviOutputResolution,
};
use tokens::tokens::characters::CharacterToken;
use crate::api::audio_list_ref::AudioListRef;
use crate::api::character_list_ref::CharacterListRef;
use crate::api::image_list_ref::ImageListRef;
use crate::api::image_ref::ImageRef;
use crate::api::router_aspect_ratio::RouterAspectRatio;
use crate::api::router_provider::RouterProvider;
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::video_list_ref::VideoListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::providers::kinovi::seedance_2p5_preview::draft::KinoviSeedance2p5PreviewDraftState;
use crate::generate::generate_video::video_generation_draft::VideoGenerationDraftRequest;
use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use super::*;
mod materialized_field_conversions {
use super::*;
#[test]
fn prompt_is_passed_through() {
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(preview_builder()));
assert_eq!(draft.prompt, "a cat dancing");
}
#[test]
fn prompt_defaults_to_empty() {
let builder = GenerateVideoRequestBuilder { prompt: None, ..preview_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
assert_eq!(draft.prompt, "");
}
#[test]
fn duration_seconds_converted() {
let builder = GenerateVideoRequestBuilder { duration_seconds: Some(30), ..preview_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
assert_eq!(draft.duration_seconds, 30);
}
#[test]
fn duration_defaults_to_5() {
let builder = GenerateVideoRequestBuilder { duration_seconds: None, ..preview_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
assert_eq!(draft.duration_seconds, 5);
}
#[test]
fn duration_clamped_to_max_30() {
let builder = GenerateVideoRequestBuilder { duration_seconds: Some(99), ..preview_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
assert_eq!(draft.duration_seconds, 30);
}
#[test]
fn duration_clamped_to_min_4() {
let builder = GenerateVideoRequestBuilder { duration_seconds: Some(2), ..preview_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
assert_eq!(draft.duration_seconds, 4);
}
#[test]
fn duration_out_of_range_error_out() {
let builder = GenerateVideoRequestBuilder {
duration_seconds: Some(99),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..preview_builder()
};
assert!(build_kinovi_seedance_2p5_preview(builder).is_err());
}
}
mod unsupported_option_errors {
use super::*;
#[test]
fn start_frame_errors() {
let builder = GenerateVideoRequestBuilder {
start_frame: Some(ImageRef::Url("https://example.com/start.jpg".to_string())),
..preview_builder()
};
assert!(build_kinovi_seedance_2p5_preview(builder).is_err());
}
#[test]
fn end_frame_errors() {
let builder = GenerateVideoRequestBuilder {
end_frame: Some(ImageRef::Url("https://example.com/end.jpg".to_string())),
..preview_builder()
};
assert!(build_kinovi_seedance_2p5_preview(builder).is_err());
}
#[test]
fn character_references_error() {
let builder = GenerateVideoRequestBuilder {
reference_character_tokens: Some(CharacterListRef::CharacterTokens(vec![
CharacterToken::new("char_abc".to_string()),
])),
..preview_builder()
};
assert!(build_kinovi_seedance_2p5_preview(builder).is_err());
}
}
mod batch_count_conversions {
use super::*;
#[test]
fn batch_one_is_accepted() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(1), ..preview_builder() };
assert!(build_kinovi_seedance_2p5_preview(builder).is_ok());
}
#[test]
fn batch_over_one_downgrades_to_single() {
let builder = GenerateVideoRequestBuilder {
video_batch_count: Some(4),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
..preview_builder()
};
assert!(build_kinovi_seedance_2p5_preview(builder).is_ok());
}
#[test]
fn batch_over_one_error_out() {
let builder = GenerateVideoRequestBuilder {
video_batch_count: Some(4),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..preview_builder()
};
assert!(build_kinovi_seedance_2p5_preview(builder).is_err());
}
#[test]
fn batch_zero_errors() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(0), ..preview_builder() };
assert!(build_kinovi_seedance_2p5_preview(builder).is_err());
}
}
mod aspect_ratio_conversions {
use super::*;
#[test]
fn direct_mappings() {
let expected = [
(RouterAspectRatio::WideSixteenByNine, KinoviAspectRatio::Landscape16x9),
(RouterAspectRatio::WideTwentyOneByNine, KinoviAspectRatio::UltraWide21x9),
(RouterAspectRatio::TallNineBySixteen, KinoviAspectRatio::Portrait9x16),
(RouterAspectRatio::Square, KinoviAspectRatio::Square1x1),
(RouterAspectRatio::WideFourByThree, KinoviAspectRatio::Standard4x3),
(RouterAspectRatio::TallThreeByFour, KinoviAspectRatio::Portrait3x4),
];
for (input, output) in expected {
let builder = GenerateVideoRequestBuilder { aspect_ratio: Some(input), ..preview_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
assert!(
std::mem::discriminant(&draft.aspect_ratio) == std::mem::discriminant(&output),
"aspect ratio {input:?} mapped wrong",
);
}
}
#[test]
fn aspect_ratio_defaults_to_landscape() {
let builder = GenerateVideoRequestBuilder { aspect_ratio: None, ..preview_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Landscape16x9));
}
}
mod resolution_conversions {
use super::*;
#[test]
fn resolution_480p() {
let builder = GenerateVideoRequestBuilder { resolution: Some(RouterResolution::FourEightyP), ..preview_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::FourEightyP)));
}
#[test]
fn resolution_720p() {
let builder = GenerateVideoRequestBuilder { resolution: Some(RouterResolution::SevenTwentyP), ..preview_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
}
#[test]
fn resolution_1080p_downgrades_to_720p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::TenEightyP),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
..preview_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
}
#[test]
fn resolution_1080p_error_out() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::TenEightyP),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..preview_builder()
};
assert!(build_kinovi_seedance_2p5_preview(builder).is_err());
}
#[test]
fn resolution_none() {
let builder = GenerateVideoRequestBuilder { resolution: None, ..preview_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
assert!(draft.resolution.is_none());
}
}
mod unhandled_request_state {
use super::*;
#[test]
fn media_refs_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
reference_images: Some(ImageListRef::Urls(vec!["https://example.com/ref.jpg".to_string()])),
reference_videos: Some(VideoListRef::Urls(vec!["https://example.com/vid.mp4".to_string()])),
reference_audio: Some(AudioListRef::Urls(vec!["https://example.com/audio.mp3".to_string()])),
..preview_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
let remaining = draft.unhandled_request_state.unwrap();
assert!(remaining.reference_images.is_some());
assert!(remaining.reference_videos.is_some());
assert!(remaining.reference_audio.is_some());
}
#[test]
fn empty_refs_are_none_in_unhandled() {
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(preview_builder()));
let remaining = draft.unhandled_request_state.unwrap();
assert!(remaining.reference_images.is_none());
assert!(remaining.reference_videos.is_none());
assert!(remaining.reference_audio.is_none());
}
}
// ── Helpers ──
fn preview_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
model: RouterVideoModel::Seedance2p5Preview,
provider: RouterProvider::KinoviWeb,
prompt: Some("a cat dancing".to_string()),
duration_seconds: Some(5),
video_batch_count: Some(1),
..Default::default()
}
}
fn unwrap_draft(result: Result<VideoGenerationDraftOrRequest, ArtcraftRouterError>) -> KinoviSeedance2p5PreviewDraftState {
match result.expect("build should succeed") {
VideoGenerationDraftOrRequest::Draft(
VideoGenerationDraftRequest::KinoviSeedance2p5Preview(draft)
) => draft,
_ => panic!("expected KinoviSeedance2p5Preview draft"),
}
}
}
@@ -0,0 +1,149 @@
use kinovi_web_client::generate::video::generate_seedance_2p5_preview::{
GenerateSeedance2p5PreviewRequest, KinoviSeedance2p5PreviewOutputResolution,
};
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video::providers::kinovi::seedance_2p5_preview::draft::KinoviSeedance2p5PreviewDraftState;
use crate::generate::generate_video::providers::kinovi::seedance_2p5_preview::request::KinoviSeedance2p5PreviewRequestState;
/// Only the resolution and duration matter — Seedance 2.5 Preview has no
/// video-reference surcharge and no batching, so references never change the
/// price.
pub struct KinoviSeedance2p5PreviewCostState {
pub resolution: Option<KinoviSeedance2p5PreviewOutputResolution>,
pub duration_seconds: u8,
}
impl KinoviSeedance2p5PreviewCostState {
pub fn from_request(request: &KinoviSeedance2p5PreviewRequestState) -> Self {
Self {
resolution: request.request.output_resolution,
duration_seconds: request.request.duration_seconds,
}
}
pub fn from_draft(draft: &KinoviSeedance2p5PreviewDraftState) -> Self {
Self {
resolution: draft.resolution,
duration_seconds: draft.duration_seconds,
}
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
let pricing_request = GenerateSeedance2p5PreviewRequest {
output_resolution: self.resolution,
duration_seconds: self.duration_seconds,
// No impact on price (references never affect 2.5 Preview pricing)
prompt: String::new(),
aspect_ratio: None,
reference_image_urls: None,
reference_video_urls: None,
reference_audio_urls: None,
use_face_blur_hack: None,
};
let costs = pricing_request.calculate_costs();
// 2.5 Preview bills fractional credits (46.81/sec at 480p). The router's
// credit field is an integer, so round to the nearest credit; the USD
// cents (the authoritative charge) are already rounded up.
let cost_in_credits = costs.kinovi_credits.round() as u64;
let cost_in_usd_cents = costs.usd_cents_rounded_up;
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_credits),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
}
}
}
#[cfg(test)]
mod tests {
use kinovi_web_client::generate::video::generate_seedance_2p5_preview::KinoviSeedance2p5PreviewOutputResolution as KinoviOutputResolution;
use super::*;
// ── Credits (rounded to the nearest whole credit for the integer field) ──
mod credits_tests {
use super::*;
#[test]
fn credits_480p() {
// 46.81 credits/s: 187.24 → 187, 702.15 → 702, 1170.25 → 1170.
assert_eq!(credits(Some(KinoviOutputResolution::FourEightyP), 4), 187);
assert_eq!(credits(Some(KinoviOutputResolution::FourEightyP), 10), 468);
assert_eq!(credits(Some(KinoviOutputResolution::FourEightyP), 15), 702);
assert_eq!(credits(Some(KinoviOutputResolution::FourEightyP), 20), 936);
assert_eq!(credits(Some(KinoviOutputResolution::FourEightyP), 25), 1170);
assert_eq!(credits(Some(KinoviOutputResolution::FourEightyP), 30), 1404);
}
#[test]
fn credits_720p() {
// 93.62 credits/s: 374.48 → 374, 1404.3 → 1404, 2808.6 → 2809.
assert_eq!(credits(Some(KinoviOutputResolution::SevenTwentyP), 4), 374);
assert_eq!(credits(Some(KinoviOutputResolution::SevenTwentyP), 10), 936);
assert_eq!(credits(Some(KinoviOutputResolution::SevenTwentyP), 15), 1404);
assert_eq!(credits(Some(KinoviOutputResolution::SevenTwentyP), 20), 1872);
assert_eq!(credits(Some(KinoviOutputResolution::SevenTwentyP), 25), 2341);
assert_eq!(credits(Some(KinoviOutputResolution::SevenTwentyP), 30), 2809);
}
#[test]
fn default_resolution_is_720p() {
assert_eq!(credits(None, 10), credits(Some(KinoviOutputResolution::SevenTwentyP), 10));
}
}
// ── USD cents (rounded up) ──
mod usd_cents_tests {
use super::*;
#[test]
fn cents_480p() {
// 187.24 credits → 18724/243 = 77.05 → 78¢; 702.15 → 288.95 → 289¢.
assert_eq!(usd_cents(Some(KinoviOutputResolution::FourEightyP), 4), 78);
assert_eq!(usd_cents(Some(KinoviOutputResolution::FourEightyP), 15), 289);
}
#[test]
fn cents_720p() {
// 374.48 credits → 37448/243 = 154.11 → 155¢; 2808.6 → 1155.80 → 1156¢.
assert_eq!(usd_cents(Some(KinoviOutputResolution::SevenTwentyP), 4), 155);
assert_eq!(usd_cents(Some(KinoviOutputResolution::SevenTwentyP), 30), 1156);
}
}
// ── Estimate flags ──
#[test]
fn estimate_flags() {
let estimate = cost_state(Some(KinoviOutputResolution::FourEightyP), 4).estimate_cost();
assert!(!estimate.is_free);
assert!(!estimate.is_unlimited);
assert!(!estimate.is_rate_limited);
assert!(!estimate.has_watermark);
assert!(estimate.failures_are_refunded.is_none());
}
// ── Helpers ──
fn cost_state(resolution: Option<KinoviOutputResolution>, duration_seconds: u8) -> KinoviSeedance2p5PreviewCostState {
KinoviSeedance2p5PreviewCostState { resolution, duration_seconds }
}
fn credits(resolution: Option<KinoviOutputResolution>, duration_seconds: u8) -> u64 {
cost_state(resolution, duration_seconds).estimate_cost().cost_in_credits.unwrap()
}
fn usd_cents(resolution: Option<KinoviOutputResolution>, duration_seconds: u8) -> u64 {
cost_state(resolution, duration_seconds).estimate_cost().cost_in_usd_cents.unwrap()
}
}
@@ -0,0 +1,77 @@
use kinovi_web_client::generate::video::generate_seedance_2p5_preview::{
GenerateSeedance2p5PreviewRequest, KinoviSeedance2p5PreviewAspectRatio,
KinoviSeedance2p5PreviewOutputResolution,
};
use crate::api::audio_list_ref::AudioListRef;
use crate::api::image_list_ref::ImageListRef;
use crate::api::video_list_ref::VideoListRef;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_video::providers::kinovi::resolve::{
audio_list_ref_into_urls_or_tokens, image_list_ref_into_urls_or_tokens,
resolve_and_upload_list, video_list_ref_into_urls_or_tokens,
};
use crate::generate::generate_video::providers::kinovi::seedance_2p5_preview::request::KinoviSeedance2p5PreviewRequestState;
use crate::generate::generate_video::video_generation_draft_context::VideoGenerationDraftContext;
#[derive(Debug, Clone)]
pub struct KinoviSeedance2p5PreviewDraftState {
pub prompt: String,
pub aspect_ratio: KinoviSeedance2p5PreviewAspectRatio,
pub resolution: Option<KinoviSeedance2p5PreviewOutputResolution>,
pub duration_seconds: u8,
pub unhandled_request_state: Option<KinoviSeedance2p5PreviewRemainingItems>,
}
/// 2.5 Preview only supports reference media — no start/end frames and no
/// character references (the builder errors on those).
#[derive(Debug, Clone)]
pub struct KinoviSeedance2p5PreviewRemainingItems {
pub reference_images: Option<ImageListRef>,
pub reference_videos: Option<VideoListRef>,
pub reference_audio: Option<AudioListRef>,
}
impl KinoviSeedance2p5PreviewDraftState {
pub async fn to_request(
&mut self,
draft_context: &VideoGenerationDraftContext<'_>,
) -> Result<KinoviSeedance2p5PreviewRequestState, ArtcraftRouterError> {
let client = draft_context.get_kinovi_web_client_ref()?;
let session = &client.session;
let mut reference_image_urls = None;
let mut reference_video_urls = None;
let mut reference_audio_urls = None;
if let Some(remaining) = self.unhandled_request_state.take() {
let map = draft_context.media_file_to_artcraft_url_map;
reference_image_urls = resolve_and_upload_list(
session, remaining.reference_images.map(image_list_ref_into_urls_or_tokens), map,
).await?;
reference_video_urls = resolve_and_upload_list(
session, remaining.reference_videos.map(video_list_ref_into_urls_or_tokens), map,
).await?;
reference_audio_urls = resolve_and_upload_list(
session, remaining.reference_audio.map(audio_list_ref_into_urls_or_tokens), map,
).await?;
}
let request = GenerateSeedance2p5PreviewRequest {
prompt: self.prompt.clone(),
aspect_ratio: Some(self.aspect_ratio),
output_resolution: self.resolution,
duration_seconds: self.duration_seconds,
reference_image_urls,
reference_video_urls,
reference_audio_urls,
use_face_blur_hack: None,
};
Ok(KinoviSeedance2p5PreviewRequestState { request })
}
}
@@ -0,0 +1,4 @@
pub mod build;
pub mod draft;
pub mod request;
pub mod cost;

Some files were not shown because too many files have changed in this diff Show More