artcraft_router v2 kinovi seedance 2 fast (#1410)

This commit is contained in:
Brandon Thomas
2026-04-21 18:34:49 -04:00
committed by GitHub
parent 9bd3726d7a
commit df4e300119
14 changed files with 2216 additions and 658 deletions
@@ -48,6 +48,7 @@ use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_see
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0_fast::plan_generate_video_seedance2pro_seedance2p0_fast;
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0::build::build_kinovi_seedance_2p0;
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::build::build_kinovi_seedance_2p0_fast;
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
/// Plan to either (1) generate a video or (2) determine how much it costs to generate that video.
@@ -140,6 +141,7 @@ impl GenerateVideoRequestBuilder {
pub fn use_new_builder(&self) -> bool {
match (self.provider, self.model) {
(Provider::Seedance2Pro, CommonVideoModel::Seedance2p0) => true,
(Provider::Seedance2Pro, CommonVideoModel::Seedance2p0Fast) => true,
_ => false,
}
}
@@ -148,6 +150,7 @@ impl GenerateVideoRequestBuilder {
pub fn build2(self) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
match (self.provider, self.model) {
(Provider::Seedance2Pro, CommonVideoModel::Seedance2p0) => build_kinovi_seedance_2p0(self),
(Provider::Seedance2Pro, CommonVideoModel::Seedance2p0Fast) => build_kinovi_seedance_2p0_fast(self),
_ => self.unsupported_provider_and_model(),
}
}
@@ -0,0 +1,110 @@
# generate_video_v2
This module implements the v2 video generation pipeline. It replaces the v1 system
(in `generate_video/`) with a two-phase approach that separates request planning from
execution, allowing cost estimation and media uploads to happen at the right time.
## Lifecycle
```
GenerateVideoRequestBuilder
│
▼ build2()
VideoGenerationDraftOrRequest
│
├─ Draft(VideoGenerationDraftRequest)
│ │
│ ▼ estimate_cost() ← cost estimate from draft state
│ │
│ ▼ finalize(context) ← uploads media, resolves tokens
│ │
│ ▼
│ VideoGenerationRequest
│ │
│ ▼ estimate_cost() ← cost estimate from finalized request
│ │
│ ▼ send_request() ← fires the API call
│ │
│ ▼
│ GenerateVideoResponse
│
└─ Request(VideoGenerationRequest) ← skips draft phase (not used yet)
```
### Key types
- **`GenerateVideoRequestBuilder`** — generic builder with provider/model/prompt/resolution/etc.
Located in `generate_video/generate_video_request_builder.rs`. The `use_new_builder()` method
gates which provider+model combos use the v2 path. `build2()` dispatches to the correct
provider's build function.
- **`VideoGenerationDraftOrRequest`** — the output of `build2()`. Currently always `Draft`.
- **`VideoGenerationDraftRequest`** — enum with one variant per provider+model. Holds
materialized settings (resolution, duration, batch count) plus unresolved media references.
Can estimate cost without network calls.
- **`VideoGenerationDraftContext`** — context needed during `finalize()`: the provider client,
media file token-to-URL map, and character token-to-ID map.
- **`VideoGenerationRequest`** — enum with one variant per provider+model. Holds a fully
materialized request ready to send. All media URLs are resolved and uploaded.
### Cost estimation
Cost can be estimated at two stages:
1. **From draft** — before `finalize()`. Uses the planned resolution/duration/batch count.
Good for showing the user a price before committing.
2. **From request** — after `finalize()`. Uses the final materialized request. Should match
the draft estimate for the same parameters.
## Module layout
```
generate_video_v2/
├── mod.rs
├── video_generation_draft.rs ← VideoGenerationDraftRequest enum
├── video_generation_draft_context.rs ← context for finalize()
├── video_generation_draft_or_request.rs
├── video_generation_request.rs ← VideoGenerationRequest enum
└── providers/
└── kinovi/ ← Kinovi/Seedance2Pro provider
├── mod.rs
├── resolve.rs ← shared: media token resolution, upload helpers
├── upload.rs ← shared: download + re-upload to Seedance2Pro CDN
├── seedance_2p0/ ← Seedance 2.0 Pro model
│ ├── mod.rs
│ ├── build.rs ← builder → draft conversion + plan helpers
│ ├── cost.rs ← cost estimation state
│ ├── draft.rs ← draft state + finalize (to_request)
│ └── request.rs ← request state + send()
└── seedance_2p0_fast/ ← Seedance 2.0 Fast model
├── mod.rs
├── build.rs
├── cost.rs
├── draft.rs
└── request.rs
```
## Adding a new provider or model
1. **Create the module** under `providers/{provider}/{model}/` with these files:
- `mod.rs` — declares sub-modules
- `build.rs` — `build_{provider}_{model}(builder) -> VideoGenerationDraftOrRequest`.
Contains `plan_*` helpers for aspect ratio, resolution, batch count, duration.
- `draft.rs` — draft state struct with `to_request()` async method for finalization.
- `cost.rs` — cost state with `from_draft()`, `from_request()`, and `estimate_cost()`.
- `request.rs` — request state with `send()` async method.
2. **Register the module** in the parent provider's `mod.rs`.
3. **Add enum variants** in:
- `video_generation_draft.rs` — `VideoGenerationDraftRequest` enum + `estimate_cost` + `finalize`
- `video_generation_request.rs` — `VideoGenerationRequest` enum + `estimate_cost` + `send_request`
4. **Wire into the builder** in `generate_video_request_builder.rs`:
- Add the `(Provider, Model)` pair to `use_new_builder()` returning `true`
- Add the dispatch arm in `build2()`
5. **Tests**: each `build.rs` and `cost.rs` should have comprehensive tests. `request.rs`
should have `#[ignore]` live API tests for manual verification.
@@ -1,2 +1,4 @@
pub mod seedance_2p0;
pub mod seedance_2p0_fast;
pub(crate) mod resolve;
pub(crate) mod upload;
@@ -0,0 +1,126 @@
use std::collections::HashMap;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use tokens::tokens::media_files::MediaFileToken;
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::video_list_ref::VideoListRef;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_video_v2::providers::kinovi::upload::upload_to_seedance2pro;
use crate::generate::generate_video_v2::video_generation_draft_context::VideoGenerationDraftContext;
/// Either resolved URLs or unresolved media file tokens.
pub(crate) enum UrlsOrTokens {
Urls(Vec<String>),
Tokens(Vec<MediaFileToken>),
}
pub(crate) fn image_list_ref_into_urls_or_tokens(list: ImageListRef) -> UrlsOrTokens {
match list {
ImageListRef::Urls(urls) => UrlsOrTokens::Urls(urls),
ImageListRef::MediaFileTokens(tokens) => UrlsOrTokens::Tokens(tokens),
}
}
pub(crate) fn video_list_ref_into_urls_or_tokens(list: VideoListRef) -> UrlsOrTokens {
match list {
VideoListRef::Urls(urls) => UrlsOrTokens::Urls(urls),
VideoListRef::MediaFileTokens(tokens) => UrlsOrTokens::Tokens(tokens),
}
}
pub(crate) fn audio_list_ref_into_urls_or_tokens(list: AudioListRef) -> UrlsOrTokens {
match list {
AudioListRef::Urls(urls) => UrlsOrTokens::Urls(urls),
AudioListRef::MediaFileTokens(tokens) => UrlsOrTokens::Tokens(tokens),
}
}
/// Resolve a single ImageRef and upload to Seedance2Pro CDN.
pub(crate) async fn resolve_and_upload_single(
session: &Seedance2ProSession,
image_ref: Option<ImageRef>,
maybe_map: Option<&HashMap<MediaFileToken, String>>,
) -> Result<Option<String>, ArtcraftRouterError> {
let source_url = match image_ref {
None => return Ok(None),
Some(ImageRef::Url(url)) => url,
Some(ImageRef::MediaFileToken(token)) => resolve_token(maybe_map, &token)?,
};
Ok(Some(upload_to_seedance2pro(session, &source_url).await?))
}
/// Resolve a list of refs to URLs and upload each to Seedance2Pro CDN.
/// Order is preserved.
pub(crate) async fn resolve_and_upload_list(
session: &Seedance2ProSession,
urls_or_tokens: Option<UrlsOrTokens>,
maybe_map: Option<&HashMap<MediaFileToken, String>>,
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
let source_urls = match urls_or_tokens {
None => return Ok(None),
Some(UrlsOrTokens::Urls(urls)) if urls.is_empty() => return Ok(None),
Some(UrlsOrTokens::Urls(urls)) => urls,
Some(UrlsOrTokens::Tokens(tokens)) if tokens.is_empty() => return Ok(None),
Some(UrlsOrTokens::Tokens(tokens)) => resolve_tokens(maybe_map, &tokens)?,
};
let mut uploaded = Vec::with_capacity(source_urls.len());
for url in &source_urls {
uploaded.push(upload_to_seedance2pro(session, url).await?);
}
Ok(Some(uploaded))
}
/// Map character tokens to their Kinovi character IDs, preserving order.
pub(crate) fn resolve_character_tokens(
character_list_ref: Option<&CharacterListRef>,
draft_context: &VideoGenerationDraftContext<'_>,
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
let list = match character_list_ref {
None => return Ok(None),
Some(r) => r,
};
let tokens = match list {
CharacterListRef::CharacterTokens(tokens) if tokens.is_empty() => return Ok(None),
CharacterListRef::CharacterTokens(tokens) => tokens,
};
let map = draft_context.get_character_token_to_kinovi_map()?;
let ids: Result<Vec<String>, _> = tokens.iter()
.map(|token| {
map.get(token).cloned().ok_or_else(|| {
ArtcraftRouterError::Client(ClientError::CharacterTokenNotFoundInMap {
token: token.clone(),
})
})
})
.collect();
ids.map(Some)
}
fn resolve_token(
maybe_map: Option<&HashMap<MediaFileToken, String>>,
token: &MediaFileToken,
) -> Result<String, ArtcraftRouterError> {
let map = maybe_map.ok_or(ArtcraftRouterError::Client(ClientError::MediaFileToUrlMapNotProvided))?;
map.get(token).cloned().ok_or_else(|| {
ArtcraftRouterError::Client(ClientError::MediaFileTokenNotFoundInMap {
token: token.clone(),
})
})
}
fn resolve_tokens(
maybe_map: Option<&HashMap<MediaFileToken, String>>,
tokens: &[MediaFileToken],
) -> Result<Vec<String>, ArtcraftRouterError> {
tokens.iter().map(|t| resolve_token(maybe_map, t)).collect()
}
@@ -212,303 +212,300 @@ mod tests {
use super::*;
fn seedance2pro_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
provider: Provider::Seedance2Pro,
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>) -> KinoviSeedance2p0DraftState {
match result.expect("build should succeed") {
VideoGenerationDraftOrRequest::Draft(
VideoGenerationDraftRequest::KinoviSeedance2p0(draft)
) => draft,
_ => panic!("expected KinoviSeedance2p0 draft"),
}
}
// ── Materialized field conversions ──
#[test]
fn prompt_is_passed_through() {
let draft = unwrap_draft(build_kinovi_seedance_2p0(seedance2pro_builder()));
assert_eq!(draft.prompt, "a cat dancing");
}
mod materialized_field_conversions {
use super::*;
#[test]
fn prompt_defaults_to_empty() {
let builder = GenerateVideoRequestBuilder { prompt: None, ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert_eq!(draft.prompt, "");
}
#[test]
fn prompt_is_passed_through() {
let draft = unwrap_draft(build_kinovi_seedance_2p0(seedance2pro_builder()));
assert_eq!(draft.prompt, "a cat dancing");
}
#[test]
fn duration_seconds_converted() {
let builder = GenerateVideoRequestBuilder { duration_seconds: Some(10), ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert_eq!(draft.duration_seconds, 10);
}
#[test]
fn prompt_defaults_to_empty() {
let builder = GenerateVideoRequestBuilder { prompt: None, ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert_eq!(draft.prompt, "");
}
#[test]
fn duration_defaults_to_5() {
let builder = GenerateVideoRequestBuilder { duration_seconds: None, ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert_eq!(draft.duration_seconds, 5);
}
#[test]
fn duration_seconds_converted() {
let builder = GenerateVideoRequestBuilder { duration_seconds: Some(10), ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert_eq!(draft.duration_seconds, 10);
}
#[test]
fn duration_clamped_to_max() {
let builder = GenerateVideoRequestBuilder {
duration_seconds: Some(99),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert_eq!(draft.duration_seconds, 15);
}
#[test]
fn duration_defaults_to_5() {
let builder = GenerateVideoRequestBuilder { duration_seconds: None, ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert_eq!(draft.duration_seconds, 5);
}
#[test]
fn batch_count_one() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(1), ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::One));
}
#[test]
fn duration_clamped_to_max() {
let builder = GenerateVideoRequestBuilder {
duration_seconds: Some(99),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert_eq!(draft.duration_seconds, 15);
}
#[test]
fn batch_count_two() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(2), ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::Two));
}
#[test]
fn batch_count_one() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(1), ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::One));
}
#[test]
fn batch_count_four() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(4), ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::Four));
#[test]
fn batch_count_two() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(2), ..seedance2pro_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 draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::Four));
}
}
// ── Aspect ratio conversions ──
#[test]
fn aspect_ratio_wide() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Landscape16x9));
}
mod aspect_ratio_conversions {
use super::*;
#[test]
fn aspect_ratio_tall() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Portrait9x16));
}
#[test]
fn aspect_ratio_wide() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Landscape16x9));
}
#[test]
fn aspect_ratio_square() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Square1x1));
}
#[test]
fn aspect_ratio_tall() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Portrait9x16));
}
#[test]
fn aspect_ratio_defaults_to_landscape() {
let builder = GenerateVideoRequestBuilder { aspect_ratio: None, ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Landscape16x9));
#[test]
fn aspect_ratio_square() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Square1x1));
}
#[test]
fn aspect_ratio_defaults_to_landscape() {
let builder = GenerateVideoRequestBuilder { aspect_ratio: None, ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Landscape16x9));
}
}
// ── Resolution conversions ──
#[test]
fn resolution_480p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(CommonResolution::FourEightyP),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::FourEightyP)));
}
mod resolution_conversions {
use super::*;
#[test]
fn resolution_720p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(CommonResolution::SevenTwentyP),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
}
#[test]
fn resolution_480p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(CommonResolution::FourEightyP),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::FourEightyP)));
}
#[test]
fn resolution_1080p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(CommonResolution::TenEightyP),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::TenEightyP)));
}
#[test]
fn resolution_720p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(CommonResolution::SevenTwentyP),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
}
#[test]
fn resolution_none() {
let builder = GenerateVideoRequestBuilder { resolution: None, ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(draft.resolution.is_none());
}
#[test]
fn resolution_1080p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(CommonResolution::TenEightyP),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::TenEightyP)));
}
#[test]
fn unsupported_resolution_error_out() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(CommonResolution::FourK),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..seedance2pro_builder()
};
assert!(build_kinovi_seedance_2p0(builder).is_err());
}
#[test]
fn resolution_none() {
let builder = GenerateVideoRequestBuilder { resolution: None, ..seedance2pro_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(draft.resolution.is_none());
}
#[test]
fn unsupported_resolution_rounds_up() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(CommonResolution::FourK),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::TenEightyP)));
#[test]
fn unsupported_resolution_error_out() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(CommonResolution::FourK),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..seedance2pro_builder()
};
assert!(build_kinovi_seedance_2p0(builder).is_err());
}
#[test]
fn unsupported_resolution_rounds_up() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(CommonResolution::FourK),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::TenEightyP)));
}
}
// ── unhandled_request_state: media refs are placed there ──
#[test]
fn unhandled_state_is_present() {
let draft = unwrap_draft(build_kinovi_seedance_2p0(seedance2pro_builder()));
assert!(draft.unhandled_request_state.is_some());
}
mod unhandled_request_state {
use super::*;
#[test]
fn start_frame_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
start_frame: Some(ImageRef::Url("https://example.com/start.jpg".to_string())),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
assert!(matches!(remaining.start_frame, Some(ImageRef::Url(url)) if url == "https://example.com/start.jpg"));
}
#[test]
fn end_frame_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
end_frame: Some(ImageRef::Url("https://example.com/end.jpg".to_string())),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
assert!(matches!(remaining.end_frame, Some(ImageRef::Url(url)) if url == "https://example.com/end.jpg"));
}
#[test]
fn start_frame_media_token_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_test123".to_string()))),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
assert!(matches!(remaining.start_frame, Some(ImageRef::MediaFileToken(t)) if t.as_str() == "mf_test123"));
}
#[test]
fn reference_images_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
reference_images: Some(ImageListRef::Urls(vec![
"https://example.com/ref1.jpg".to_string(),
"https://example.com/ref2.jpg".to_string(),
])),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
match remaining.reference_images {
Some(ImageListRef::Urls(urls)) => {
assert_eq!(urls.len(), 2);
assert_eq!(urls[0], "https://example.com/ref1.jpg");
assert_eq!(urls[1], "https://example.com/ref2.jpg");
}
_ => panic!("expected Urls variant"),
#[test]
fn unhandled_state_is_present() {
let draft = unwrap_draft(build_kinovi_seedance_2p0(seedance2pro_builder()));
assert!(draft.unhandled_request_state.is_some());
}
}
#[test]
fn reference_videos_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
reference_videos: Some(VideoListRef::Urls(vec![
"https://example.com/vid.mp4".to_string(),
])),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
assert!(matches!(remaining.reference_videos, Some(VideoListRef::Urls(urls)) if urls.len() == 1));
}
#[test]
fn reference_audio_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
reference_audio: Some(AudioListRef::Urls(vec![
"https://example.com/audio.mp3".to_string(),
])),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
assert!(matches!(remaining.reference_audio, Some(AudioListRef::Urls(urls)) if urls.len() == 1));
}
#[test]
fn character_tokens_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
reference_character_tokens: Some(CharacterListRef::CharacterTokens(vec![
CharacterToken::new("char_abc".to_string()),
CharacterToken::new("char_def".to_string()),
])),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
match remaining.reference_character_tokens {
Some(CharacterListRef::CharacterTokens(tokens)) => {
assert_eq!(tokens.len(), 2);
assert_eq!(tokens[0].as_str(), "char_abc");
assert_eq!(tokens[1].as_str(), "char_def");
}
_ => panic!("expected CharacterTokens variant"),
#[test]
fn start_frame_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
start_frame: Some(ImageRef::Url("https://example.com/start.jpg".to_string())),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
assert!(matches!(remaining.start_frame, Some(ImageRef::Url(url)) if url == "https://example.com/start.jpg"));
}
}
#[test]
fn empty_refs_are_none_in_unhandled() {
let draft = unwrap_draft(build_kinovi_seedance_2p0(seedance2pro_builder()));
let remaining = draft.unhandled_request_state.unwrap();
assert!(remaining.start_frame.is_none());
assert!(remaining.end_frame.is_none());
assert!(remaining.reference_images.is_none());
assert!(remaining.reference_videos.is_none());
assert!(remaining.reference_audio.is_none());
assert!(remaining.reference_character_tokens.is_none());
#[test]
fn end_frame_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
end_frame: Some(ImageRef::Url("https://example.com/end.jpg".to_string())),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
assert!(matches!(remaining.end_frame, Some(ImageRef::Url(url)) if url == "https://example.com/end.jpg"));
}
#[test]
fn start_frame_media_token_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_test123".to_string()))),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
assert!(matches!(remaining.start_frame, Some(ImageRef::MediaFileToken(t)) if t.as_str() == "mf_test123"));
}
#[test]
fn reference_images_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
reference_images: Some(ImageListRef::Urls(vec![
"https://example.com/ref1.jpg".to_string(),
"https://example.com/ref2.jpg".to_string(),
])),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
match remaining.reference_images {
Some(ImageListRef::Urls(urls)) => {
assert_eq!(urls.len(), 2);
assert_eq!(urls[0], "https://example.com/ref1.jpg");
assert_eq!(urls[1], "https://example.com/ref2.jpg");
}
_ => panic!("expected Urls variant"),
}
}
#[test]
fn reference_videos_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
reference_videos: Some(VideoListRef::Urls(vec![
"https://example.com/vid.mp4".to_string(),
])),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
assert!(matches!(remaining.reference_videos, Some(VideoListRef::Urls(urls)) if urls.len() == 1));
}
#[test]
fn reference_audio_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
reference_audio: Some(AudioListRef::Urls(vec![
"https://example.com/audio.mp3".to_string(),
])),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
assert!(matches!(remaining.reference_audio, Some(AudioListRef::Urls(urls)) if urls.len() == 1));
}
#[test]
fn character_tokens_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
reference_character_tokens: Some(CharacterListRef::CharacterTokens(vec![
CharacterToken::new("char_abc".to_string()),
CharacterToken::new("char_def".to_string()),
])),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
let remaining = draft.unhandled_request_state.unwrap();
match remaining.reference_character_tokens {
Some(CharacterListRef::CharacterTokens(tokens)) => {
assert_eq!(tokens.len(), 2);
assert_eq!(tokens[0].as_str(), "char_abc");
assert_eq!(tokens[1].as_str(), "char_def");
}
_ => panic!("expected CharacterTokens variant"),
}
}
#[test]
fn empty_refs_are_none_in_unhandled() {
let draft = unwrap_draft(build_kinovi_seedance_2p0(seedance2pro_builder()));
let remaining = draft.unhandled_request_state.unwrap();
assert!(remaining.start_frame.is_none());
assert!(remaining.end_frame.is_none());
assert!(remaining.reference_images.is_none());
assert!(remaining.reference_videos.is_none());
assert!(remaining.reference_audio.is_none());
assert!(remaining.reference_character_tokens.is_none());
}
}
// ── Full combination ──
@@ -547,4 +544,25 @@ mod tests {
assert!(remaining.reference_audio.is_some());
assert!(remaining.reference_character_tokens.is_some());
}
// ── Helpers ──
fn seedance2pro_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
provider: Provider::Seedance2Pro,
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>) -> KinoviSeedance2p0DraftState {
match result.expect("build should succeed") {
VideoGenerationDraftOrRequest::Draft(
VideoGenerationDraftRequest::KinoviSeedance2p0(draft)
) => draft,
_ => panic!("expected KinoviSeedance2p0 draft"),
}
}
}
@@ -84,7 +84,296 @@ mod tests {
use crate::api::video_list_ref::VideoListRef;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
// ======================== Helpers ========================
// ── 720p pricing ──
mod pricing_720p {
use super::*;
#[test]
fn cost_720p_batch_1() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One), 64);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 80);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 6, KinoviBatchCount::One), 96);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 7, KinoviBatchCount::One), 112);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One), 160);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One), 240);
}
#[test]
fn cost_720p_batch_2() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::Two), 128);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Two), 160);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::Two), 480);
}
#[test]
fn cost_720p_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::Four), 256);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Four), 320);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::Four), 960);
}
}
// ── 480p pricing ──
mod pricing_480p {
use super::*;
#[test]
fn cost_480p_batch_1() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One), 31);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 39);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 10, KinoviBatchCount::One), 78);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 15, KinoviBatchCount::One), 117);
}
#[test]
fn cost_480p_batch_2() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Two), 78);
}
#[test]
fn cost_480p_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Four), 155);
}
}
// ── 1080p pricing ──
mod pricing_1080p {
use super::*;
#[test]
fn cost_1080p_batch_1() {
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 4, KinoviBatchCount::One), 187);
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One), 233);
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 10, KinoviBatchCount::One), 466);
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 15, KinoviBatchCount::One), 699);
}
#[test]
fn cost_1080p_batch_2() {
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Two), 466);
}
#[test]
fn cost_1080p_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Four), 933);
}
}
// ── Relative pricing ──
mod relative_pricing_tests {
use super::*;
#[test]
fn cost_480p_cheaper_than_720p_cheaper_than_1080p() {
let c480 = usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One);
let c720 = usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One);
let c1080 = usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One);
assert!(c480 < c720, "480p ({}) should be cheaper than 720p ({})", c480, c720);
assert!(c720 < c1080, "720p ({}) should be cheaper than 1080p ({})", c720, c1080);
}
#[test]
fn cost_scales_with_duration() {
let c4 = usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One);
let c10 = usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One);
let c15 = usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One);
assert!(c4 < c10);
assert!(c10 < c15);
}
#[test]
fn cost_scales_with_batch() {
let b1 = usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One);
let b2 = usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Two);
let b4 = usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Four);
assert!(b1 < b2);
assert!(b2 < b4);
}
}
// -- Video reference does NOT affect cost (yet) --
#[test]
fn video_reference_does_not_affect_cost() {
let base = KinoviSeedance2p0CostState {
resolution: KinoviOutputResolution::SevenTwentyP,
duration_seconds: 5,
batch_count: KinoviBatchCount::One,
has_video_reference: false,
};
let without = base.estimate_cost();
let with = KinoviSeedance2p0CostState { has_video_reference: true, ..base }.estimate_cost();
assert_eq!(without.cost_in_usd_cents, with.cost_in_usd_cents);
assert_eq!(without.cost_in_credits, with.cost_in_credits);
}
// ── from_request() tests ──
mod from_request_tests {
use super::*;
#[test]
fn from_request_720p() {
let req = make_request_state(Some(KinoviOutputResolution::SevenTwentyP), 5, KinoviBatchCount::One, false);
let cost = KinoviSeedance2p0CostState::from_request(&req);
assert!(matches!(cost.resolution, KinoviOutputResolution::SevenTwentyP));
assert_eq!(cost.duration_seconds, 5);
assert!(matches!(cost.batch_count, KinoviBatchCount::One));
assert!(!cost.has_video_reference);
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(80));
}
#[test]
fn from_request_none_defaults_to_720p() {
let req = make_request_state(None, 5, KinoviBatchCount::One, false);
let cost = KinoviSeedance2p0CostState::from_request(&req);
assert!(matches!(cost.resolution, KinoviOutputResolution::SevenTwentyP));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(80));
}
#[test]
fn from_request_480p() {
let req = make_request_state(Some(KinoviOutputResolution::FourEightyP), 5, KinoviBatchCount::One, false);
let cost = KinoviSeedance2p0CostState::from_request(&req);
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(39));
}
#[test]
fn from_request_1080p_batch_2() {
let req = make_request_state(Some(KinoviOutputResolution::TenEightyP), 5, KinoviBatchCount::Two, false);
let cost = KinoviSeedance2p0CostState::from_request(&req);
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(466));
}
#[test]
fn from_request_with_video_reference() {
let req = make_request_state(Some(KinoviOutputResolution::SevenTwentyP), 5, KinoviBatchCount::One, true);
let cost = KinoviSeedance2p0CostState::from_request(&req);
assert!(cost.has_video_reference);
// Video refs don't affect cost yet
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(80));
}
#[test]
fn from_request_without_video_reference() {
let req = make_request_state(Some(KinoviOutputResolution::SevenTwentyP), 5, KinoviBatchCount::One, false);
let cost = KinoviSeedance2p0CostState::from_request(&req);
assert!(!cost.has_video_reference);
}
}
// ── from_draft() tests ──
mod from_draft_tests {
use super::*;
#[test]
fn from_draft_720p_default() {
let draft = make_draft(5, 1, None, false);
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
assert!(matches!(cost.resolution, KinoviOutputResolution::SevenTwentyP));
assert_eq!(cost.duration_seconds, 5);
assert!(matches!(cost.batch_count, KinoviBatchCount::One));
assert!(!cost.has_video_reference);
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(80));
}
#[test]
fn from_draft_480p() {
let draft = make_draft(5, 1, Some(CommonResolution::FourEightyP), false);
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
assert!(matches!(cost.resolution, KinoviOutputResolution::FourEightyP));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(39));
}
#[test]
fn from_draft_1080p_batch_4() {
let draft = make_draft(5, 4, Some(CommonResolution::TenEightyP), false);
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
assert!(matches!(cost.resolution, KinoviOutputResolution::TenEightyP));
assert!(matches!(cost.batch_count, KinoviBatchCount::Four));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(933));
}
#[test]
fn from_draft_with_video_reference() {
let draft = make_draft(5, 1, None, true);
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
assert!(cost.has_video_reference);
// Video refs don't affect cost yet
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(80));
}
#[test]
fn from_draft_without_video_reference() {
let draft = make_draft(5, 1, None, false);
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
assert!(!cost.has_video_reference);
}
#[test]
fn from_draft_duration_15_batch_2() {
let draft = make_draft(15, 2, Some(CommonResolution::SevenTwentyP), false);
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
assert_eq!(cost.duration_seconds, 15);
assert!(matches!(cost.batch_count, KinoviBatchCount::Two));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(480));
}
}
// ── Cross-check: from_draft matches from_request ──
mod cross_check_tests {
use super::*;
#[test]
fn draft_and_request_produce_same_cost() {
// 720p, 5s, batch 1
let draft = make_draft(5, 1, Some(CommonResolution::SevenTwentyP), false);
let req = make_request_state(Some(KinoviOutputResolution::SevenTwentyP), 5, KinoviBatchCount::One, false);
let draft_cost = KinoviSeedance2p0CostState::from_draft(&draft).estimate_cost();
let req_cost = KinoviSeedance2p0CostState::from_request(&req).estimate_cost();
assert_eq!(draft_cost.cost_in_usd_cents, req_cost.cost_in_usd_cents);
assert_eq!(draft_cost.cost_in_credits, req_cost.cost_in_credits);
}
#[test]
fn draft_and_request_produce_same_cost_1080p() {
let draft = make_draft(10, 2, Some(CommonResolution::TenEightyP), false);
let req = make_request_state(Some(KinoviOutputResolution::TenEightyP), 10, KinoviBatchCount::Two, false);
let draft_cost = KinoviSeedance2p0CostState::from_draft(&draft).estimate_cost();
let req_cost = KinoviSeedance2p0CostState::from_request(&req).estimate_cost();
assert_eq!(draft_cost.cost_in_usd_cents, req_cost.cost_in_usd_cents);
assert_eq!(draft_cost.cost_in_credits, req_cost.cost_in_credits);
}
}
// ── Credits spot checks ──
mod credits_tests {
use super::*;
#[test]
fn credits_720p() {
assert_eq!(credits(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 200);
}
#[test]
fn credits_480p() {
assert_eq!(credits(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 75);
}
#[test]
fn credits_1080p() {
assert_eq!(credits(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One), 450);
}
}
// ── Helpers ──
fn usd_cents(
resolution: KinoviOutputResolution,
@@ -169,264 +458,4 @@ mod tests {
},
}
}
// ======================== Direct estimate_cost() tests ========================
// These must match the reference implementation values exactly.
// -- 720p (legacy pricing: 250 credits/$1, 40 credits/sec) --
#[test]
fn cost_720p_batch_1() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One), 64);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 80);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 6, KinoviBatchCount::One), 96);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 7, KinoviBatchCount::One), 112);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One), 160);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One), 240);
}
#[test]
fn cost_720p_batch_2() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::Two), 128);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Two), 160);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::Two), 480);
}
#[test]
fn cost_720p_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::Four), 256);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Four), 320);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::Four), 960);
}
// -- 480p (new pricing: 193 credits/$1, 15 credits/sec) --
#[test]
fn cost_480p_batch_1() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One), 31);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 39);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 10, KinoviBatchCount::One), 78);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 15, KinoviBatchCount::One), 117);
}
#[test]
fn cost_480p_batch_2() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Two), 78);
}
#[test]
fn cost_480p_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Four), 155);
}
// -- 1080p (new pricing: 193 credits/$1, 90 credits/sec) --
#[test]
fn cost_1080p_batch_1() {
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 4, KinoviBatchCount::One), 187);
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One), 233);
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 10, KinoviBatchCount::One), 466);
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 15, KinoviBatchCount::One), 699);
}
#[test]
fn cost_1080p_batch_2() {
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Two), 466);
}
#[test]
fn cost_1080p_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Four), 933);
}
// -- Relative pricing --
#[test]
fn cost_480p_cheaper_than_720p_cheaper_than_1080p() {
let c480 = usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One);
let c720 = usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One);
let c1080 = usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One);
assert!(c480 < c720, "480p ({}) should be cheaper than 720p ({})", c480, c720);
assert!(c720 < c1080, "720p ({}) should be cheaper than 1080p ({})", c720, c1080);
}
#[test]
fn cost_scales_with_duration() {
let c4 = usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One);
let c10 = usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One);
let c15 = usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One);
assert!(c4 < c10);
assert!(c10 < c15);
}
#[test]
fn cost_scales_with_batch() {
let b1 = usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One);
let b2 = usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Two);
let b4 = usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Four);
assert!(b1 < b2);
assert!(b2 < b4);
}
// -- Video reference does NOT affect cost (yet) --
#[test]
fn video_reference_does_not_affect_cost() {
let base = KinoviSeedance2p0CostState {
resolution: KinoviOutputResolution::SevenTwentyP,
duration_seconds: 5,
batch_count: KinoviBatchCount::One,
has_video_reference: false,
};
let without = base.estimate_cost();
let with = KinoviSeedance2p0CostState { has_video_reference: true, ..base }.estimate_cost();
assert_eq!(without.cost_in_usd_cents, with.cost_in_usd_cents);
assert_eq!(without.cost_in_credits, with.cost_in_credits);
}
// ======================== from_request() tests ========================
#[test]
fn from_request_720p() {
let req = make_request_state(Some(KinoviOutputResolution::SevenTwentyP), 5, KinoviBatchCount::One, false);
let cost = KinoviSeedance2p0CostState::from_request(&req);
assert!(matches!(cost.resolution, KinoviOutputResolution::SevenTwentyP));
assert_eq!(cost.duration_seconds, 5);
assert!(matches!(cost.batch_count, KinoviBatchCount::One));
assert!(!cost.has_video_reference);
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(80));
}
#[test]
fn from_request_none_defaults_to_720p() {
let req = make_request_state(None, 5, KinoviBatchCount::One, false);
let cost = KinoviSeedance2p0CostState::from_request(&req);
assert!(matches!(cost.resolution, KinoviOutputResolution::SevenTwentyP));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(80));
}
#[test]
fn from_request_480p() {
let req = make_request_state(Some(KinoviOutputResolution::FourEightyP), 5, KinoviBatchCount::One, false);
let cost = KinoviSeedance2p0CostState::from_request(&req);
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(39));
}
#[test]
fn from_request_1080p_batch_2() {
let req = make_request_state(Some(KinoviOutputResolution::TenEightyP), 5, KinoviBatchCount::Two, false);
let cost = KinoviSeedance2p0CostState::from_request(&req);
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(466));
}
#[test]
fn from_request_with_video_reference() {
let req = make_request_state(Some(KinoviOutputResolution::SevenTwentyP), 5, KinoviBatchCount::One, true);
let cost = KinoviSeedance2p0CostState::from_request(&req);
assert!(cost.has_video_reference);
// Video refs don't affect cost yet
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(80));
}
#[test]
fn from_request_without_video_reference() {
let req = make_request_state(Some(KinoviOutputResolution::SevenTwentyP), 5, KinoviBatchCount::One, false);
let cost = KinoviSeedance2p0CostState::from_request(&req);
assert!(!cost.has_video_reference);
}
// ======================== from_draft() tests ========================
#[test]
fn from_draft_720p_default() {
let draft = make_draft(5, 1, None, false);
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
assert!(matches!(cost.resolution, KinoviOutputResolution::SevenTwentyP));
assert_eq!(cost.duration_seconds, 5);
assert!(matches!(cost.batch_count, KinoviBatchCount::One));
assert!(!cost.has_video_reference);
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(80));
}
#[test]
fn from_draft_480p() {
let draft = make_draft(5, 1, Some(CommonResolution::FourEightyP), false);
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
assert!(matches!(cost.resolution, KinoviOutputResolution::FourEightyP));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(39));
}
#[test]
fn from_draft_1080p_batch_4() {
let draft = make_draft(5, 4, Some(CommonResolution::TenEightyP), false);
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
assert!(matches!(cost.resolution, KinoviOutputResolution::TenEightyP));
assert!(matches!(cost.batch_count, KinoviBatchCount::Four));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(933));
}
#[test]
fn from_draft_with_video_reference() {
let draft = make_draft(5, 1, None, true);
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
assert!(cost.has_video_reference);
// Video refs don't affect cost yet
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(80));
}
#[test]
fn from_draft_without_video_reference() {
let draft = make_draft(5, 1, None, false);
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
assert!(!cost.has_video_reference);
}
#[test]
fn from_draft_duration_15_batch_2() {
let draft = make_draft(15, 2, Some(CommonResolution::SevenTwentyP), false);
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
assert_eq!(cost.duration_seconds, 15);
assert!(matches!(cost.batch_count, KinoviBatchCount::Two));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(480));
}
// ======================== Cross-check: from_draft matches from_request ========================
#[test]
fn draft_and_request_produce_same_cost() {
// 720p, 5s, batch 1
let draft = make_draft(5, 1, Some(CommonResolution::SevenTwentyP), false);
let req = make_request_state(Some(KinoviOutputResolution::SevenTwentyP), 5, KinoviBatchCount::One, false);
let draft_cost = KinoviSeedance2p0CostState::from_draft(&draft).estimate_cost();
let req_cost = KinoviSeedance2p0CostState::from_request(&req).estimate_cost();
assert_eq!(draft_cost.cost_in_usd_cents, req_cost.cost_in_usd_cents);
assert_eq!(draft_cost.cost_in_credits, req_cost.cost_in_credits);
}
#[test]
fn draft_and_request_produce_same_cost_1080p() {
let draft = make_draft(10, 2, Some(CommonResolution::TenEightyP), false);
let req = make_request_state(Some(KinoviOutputResolution::TenEightyP), 10, KinoviBatchCount::Two, false);
let draft_cost = KinoviSeedance2p0CostState::from_draft(&draft).estimate_cost();
let req_cost = KinoviSeedance2p0CostState::from_request(&req).estimate_cost();
assert_eq!(draft_cost.cost_in_usd_cents, req_cost.cost_in_usd_cents);
assert_eq!(draft_cost.cost_in_credits, req_cost.cost_in_credits);
}
// ======================== Credits spot checks ========================
#[test]
fn credits_720p() {
assert_eq!(credits(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 200);
}
#[test]
fn credits_480p() {
assert_eq!(credits(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 75);
}
#[test]
fn credits_1080p() {
assert_eq!(credits(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One), 450);
}
}
@@ -1,8 +1,4 @@
use std::collections::HashMap;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
use seedance2pro_client::requests::generate_video::generate_video::{KinoviAspectRatio, KinoviBatchCount, KinoviGenerateVideoRequest, KinoviModelType, KinoviOutputResolution};
use tokens::tokens::media_files::MediaFileToken;
use crate::api::audio_list_ref::AudioListRef;
use crate::api::character_list_ref::CharacterListRef;
@@ -10,9 +6,12 @@ use crate::api::image_list_ref::ImageListRef;
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_v2::providers::kinovi::resolve::{
audio_list_ref_into_urls_or_tokens, image_list_ref_into_urls_or_tokens,
resolve_and_upload_list, resolve_and_upload_single, resolve_character_tokens,
video_list_ref_into_urls_or_tokens,
};
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0::request::KinoviSeedance2p0RequestState;
use crate::generate::generate_video_v2::providers::kinovi::upload::upload_to_seedance2pro;
use crate::generate::generate_video_v2::video_generation_draft_context::VideoGenerationDraftContext;
#[derive(Debug, Clone)]
@@ -61,15 +60,15 @@ impl KinoviSeedance2p0DraftState {
end_frame_url = resolve_and_upload_single(session, remaining.end_frame, map).await?;
reference_image_urls = resolve_and_upload_list(
session, remaining.reference_images.map(ImageListRef::into_urls_or_tokens), map,
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(VideoListRef::into_urls_or_tokens), map,
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(AudioListRef::into_urls_or_tokens), map,
session, remaining.reference_audio.map(audio_list_ref_into_urls_or_tokens), map,
).await?;
character_ids = resolve_character_tokens(
@@ -97,128 +96,3 @@ impl KinoviSeedance2p0DraftState {
Ok(KinoviSeedance2p0RequestState { request })
}
}
/// Either resolved URLs or unresolved media file tokens.
enum UrlsOrTokens {
Urls(Vec<String>),
Tokens(Vec<MediaFileToken>),
}
// Allow each list ref type to decompose into this common representation.
impl ImageListRef {
fn into_urls_or_tokens(self) -> UrlsOrTokens {
match self {
Self::Urls(urls) => UrlsOrTokens::Urls(urls),
Self::MediaFileTokens(tokens) => UrlsOrTokens::Tokens(tokens),
}
}
}
impl VideoListRef {
fn into_urls_or_tokens(self) -> UrlsOrTokens {
match self {
Self::Urls(urls) => UrlsOrTokens::Urls(urls),
Self::MediaFileTokens(tokens) => UrlsOrTokens::Tokens(tokens),
}
}
}
impl AudioListRef {
fn into_urls_or_tokens(self) -> UrlsOrTokens {
match self {
Self::Urls(urls) => UrlsOrTokens::Urls(urls),
Self::MediaFileTokens(tokens) => UrlsOrTokens::Tokens(tokens),
}
}
}
// --- Resolve + upload helpers ---
/// Resolve a single ImageRef and upload to Seedance2Pro CDN.
async fn resolve_and_upload_single(
session: &Seedance2ProSession,
image_ref: Option<ImageRef>,
maybe_map: Option<&HashMap<MediaFileToken, String>>,
) -> Result<Option<String>, ArtcraftRouterError> {
let source_url = match image_ref {
None => return Ok(None),
Some(ImageRef::Url(url)) => url,
Some(ImageRef::MediaFileToken(token)) => resolve_token(maybe_map, &token)?,
};
Ok(Some(upload_to_seedance2pro(session, &source_url).await?))
}
/// Resolve a list of refs to URLs and upload each to Seedance2Pro CDN.
/// Order is preserved.
async fn resolve_and_upload_list(
session: &Seedance2ProSession,
urls_or_tokens: Option<UrlsOrTokens>,
maybe_map: Option<&HashMap<MediaFileToken, String>>,
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
let source_urls = match urls_or_tokens {
None => return Ok(None),
Some(UrlsOrTokens::Urls(urls)) if urls.is_empty() => return Ok(None),
Some(UrlsOrTokens::Urls(urls)) => urls,
Some(UrlsOrTokens::Tokens(tokens)) if tokens.is_empty() => return Ok(None),
Some(UrlsOrTokens::Tokens(tokens)) => resolve_tokens(maybe_map, &tokens)?,
};
let mut uploaded = Vec::with_capacity(source_urls.len());
for url in &source_urls {
uploaded.push(upload_to_seedance2pro(session, url).await?);
}
Ok(Some(uploaded))
}
// --- Token resolution ---
fn resolve_token(
maybe_map: Option<&HashMap<MediaFileToken, String>>,
token: &MediaFileToken,
) -> Result<String, ArtcraftRouterError> {
let map = maybe_map.ok_or(ArtcraftRouterError::Client(ClientError::MediaFileToUrlMapNotProvided))?;
map.get(token).cloned().ok_or_else(|| {
ArtcraftRouterError::Client(ClientError::MediaFileTokenNotFoundInMap {
token: token.clone(),
})
})
}
fn resolve_tokens(
maybe_map: Option<&HashMap<MediaFileToken, String>>,
tokens: &[MediaFileToken],
) -> Result<Vec<String>, ArtcraftRouterError> {
tokens.iter().map(|t| resolve_token(maybe_map, t)).collect()
}
// --- Character token resolution ---
/// Map character tokens to their Kinovi character IDs, preserving order.
fn resolve_character_tokens(
character_list_ref: Option<&CharacterListRef>,
draft_context: &VideoGenerationDraftContext<'_>,
) -> Result<Option<Vec<String>>, ArtcraftRouterError> {
let list = match character_list_ref {
None => return Ok(None),
Some(r) => r,
};
let tokens = match list {
CharacterListRef::CharacterTokens(tokens) if tokens.is_empty() => return Ok(None),
CharacterListRef::CharacterTokens(tokens) => tokens,
};
let map = draft_context.get_character_token_to_kinovi_map()?;
let ids: Result<Vec<String>, _> = tokens.iter()
.map(|token| {
map.get(token).cloned().ok_or_else(|| {
ArtcraftRouterError::Client(ClientError::CharacterTokenNotFoundInMap {
token: token.clone(),
})
})
})
.collect();
ids.map(Some)
}
@@ -0,0 +1,608 @@
use seedance2pro_client::requests::generate_video::generate_video::{KinoviAspectRatio, KinoviBatchCount, KinoviOutputResolution};
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_resolution::CommonResolution;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::draft::{KinoviSeedance2p0FastDraftState, KinoviSeedance2p0FastRemainingItems};
use crate::generate::generate_video_v2::video_generation_draft::VideoGenerationDraftRequest;
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
pub fn build_kinovi_seedance_2p0_fast(builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
let draft = do_build_kinovi_seedance_2p0_fast(builder)?;
Ok(VideoGenerationDraftOrRequest::Draft(VideoGenerationDraftRequest::KinoviSeedance2p0Fast(draft)))
}
fn do_build_kinovi_seedance_2p0_fast(mut builder: GenerateVideoRequestBuilder) -> Result<KinoviSeedance2p0FastDraftState, ArtcraftRouterError> {
let strategy = builder.request_mismatch_mitigation_strategy;
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy)?;
let resolution = plan_output_resolution(builder.resolution.take(), strategy)?;
let batch_count = 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();
let unhandled_request_state = KinoviSeedance2p0FastRemainingItems {
start_frame: builder.start_frame.take(),
end_frame: builder.end_frame.take(),
reference_images: builder.reference_images.take(),
reference_videos: builder.reference_videos.take(),
reference_audio: builder.reference_audio.take(),
reference_character_tokens: builder.reference_character_tokens.take(),
};
Ok(KinoviSeedance2p0FastDraftState {
aspect_ratio,
resolution,
batch_count,
duration_seconds,
prompt,
unhandled_request_state: Some(unhandled_request_state),
})
}
// ── Plan helpers ──
// Supported aspect ratios and their AR values (width / height):
// Portrait9x16 = 0.5625, Portrait3x4 = 0.75, Square1x1 = 1.0, Standard4x3 = 1.33, Landscape16x9 = 1.78
//
// All supported ratios cost the same, so PayMoreUpgrade and PayLessDowngrade both
// select the nearest match rather than rounding in a specific direction.
fn plan_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<KinoviAspectRatio, ArtcraftRouterError> {
match aspect_ratio {
// No preference or auto — default to landscape
None
| Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k) => Ok(KinoviAspectRatio::Landscape16x9),
// Direct mappings
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
Ok(KinoviAspectRatio::Landscape16x9)
}
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
Ok(KinoviAspectRatio::Portrait9x16)
}
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
Ok(KinoviAspectRatio::Square1x1)
}
Some(CommonAspectRatio::WideFourByThree) => Ok(KinoviAspectRatio::Standard4x3),
Some(CommonAspectRatio::TallThreeByFour) => Ok(KinoviAspectRatio::Portrait3x4),
// 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: CommonAspectRatio) -> KinoviAspectRatio {
match aspect_ratio {
CommonAspectRatio::WideFiveByFour => KinoviAspectRatio::Standard4x3, // 1.25, nearest 1.33
CommonAspectRatio::WideThreeByTwo => KinoviAspectRatio::Standard4x3, // 1.50, nearest 1.33
CommonAspectRatio::WideTwentyOneByNine => KinoviAspectRatio::Landscape16x9, // 2.33, nearest 1.78
CommonAspectRatio::TallFourByFive => KinoviAspectRatio::Portrait3x4, // 0.80, nearest 0.75
CommonAspectRatio::TallTwoByThree => KinoviAspectRatio::Portrait3x4, // 0.67, nearest 0.75
CommonAspectRatio::TallNineByTwentyOne => KinoviAspectRatio::Portrait9x16, // 0.43, nearest 0.56
_ => KinoviAspectRatio::Square1x1,
}
}
// Seedance 2.0 Fast supports output resolutions: 480p and 720p only.
// 1080p is NOT supported — downgrade to 720p or error based on strategy.
fn plan_output_resolution(
resolution: Option<CommonResolution>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<KinoviOutputResolution>, ArtcraftRouterError> {
match resolution {
None => Ok(None),
// Direct mappings
Some(CommonResolution::FourEightyP) => Ok(Some(KinoviOutputResolution::FourEightyP)),
Some(CommonResolution::SevenTwentyP) => Ok(Some(KinoviOutputResolution::SevenTwentyP)),
// 1080p is not supported for Fast — handle via strategy
Some(CommonResolution::TenEightyP) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "resolution",
value: format!("{:?}", CommonResolution::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 => {
Ok(Some(match unsupported {
CommonResolution::HalfK => KinoviOutputResolution::FourEightyP,
_ => KinoviOutputResolution::SevenTwentyP,
}))
}
RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(Some(match unsupported {
CommonResolution::HalfK => KinoviOutputResolution::FourEightyP,
_ => KinoviOutputResolution::SevenTwentyP,
}))
}
},
}
}
// Seedance2p0Fast supports batch counts of 1, 2, and 4 only.
fn plan_batch_count(
video_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<KinoviBatchCount, ArtcraftRouterError> {
let count = video_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(KinoviBatchCount::One),
2 => Ok(KinoviBatchCount::Two),
4 => Ok(KinoviBatchCount::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "video_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
Ok(if count < 4 { KinoviBatchCount::Four } else { KinoviBatchCount::Four })
}
RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(if count < 4 { KinoviBatchCount::Two } else { KinoviBatchCount::Four })
}
},
}
}
// Seedance2p0Fast supports duration of 4–15 seconds.
fn plan_duration(
duration_seconds: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<u8, ArtcraftRouterError> {
const MIN: u16 = 4;
const MAX: u16 = 15;
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 seedance2pro_client::requests::generate_video::generate_video::{KinoviAspectRatio, KinoviBatchCount, KinoviOutputResolution};
use tokens::tokens::characters::CharacterToken;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::audio_list_ref::AudioListRef;
use crate::api::character_list_ref::CharacterListRef;
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_resolution::CommonResolution;
use crate::api::common_video_model::CommonVideoModel;
use crate::api::image_list_ref::ImageListRef;
use crate::api::image_ref::ImageRef;
use crate::api::provider::Provider;
use crate::api::video_list_ref::VideoListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::draft::KinoviSeedance2p0FastDraftState;
use crate::generate::generate_video_v2::video_generation_draft::VideoGenerationDraftRequest;
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use super::*;
// ── Materialized field conversions ──
mod materialized_field_conversions {
use super::*;
#[test]
fn prompt_is_passed_through() {
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(seedance2pro_fast_builder()));
assert_eq!(draft.prompt, "a cat dancing");
}
#[test]
fn prompt_defaults_to_empty() {
let builder = GenerateVideoRequestBuilder { prompt: None, ..seedance2pro_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 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 draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert_eq!(draft.duration_seconds, 5);
}
#[test]
fn duration_clamped_to_max() {
let builder = GenerateVideoRequestBuilder {
duration_seconds: Some(99),
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert_eq!(draft.duration_seconds, 15);
}
#[test]
fn batch_count_one() {
let builder = GenerateVideoRequestBuilder { video_batch_count: Some(1), ..seedance2pro_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 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 draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.batch_count, KinoviBatchCount::Four));
}
}
// ── Aspect ratio conversions ──
mod aspect_ratio_conversions {
use super::*;
#[test]
fn aspect_ratio_wide() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Landscape16x9));
}
#[test]
fn aspect_ratio_tall() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Portrait9x16));
}
#[test]
fn aspect_ratio_square() {
let builder = GenerateVideoRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Square1x1));
}
#[test]
fn aspect_ratio_defaults_to_landscape() {
let builder = GenerateVideoRequestBuilder { aspect_ratio: None, ..seedance2pro_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Landscape16x9));
}
}
// ── Resolution conversions ──
mod resolution_conversions {
use super::*;
#[test]
fn resolution_480p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(CommonResolution::FourEightyP),
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::FourEightyP)));
}
#[test]
fn resolution_720p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(CommonResolution::SevenTwentyP),
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
}
#[test]
fn resolution_1080p_downgrades_to_720p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(CommonResolution::TenEightyP),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
}
#[test]
fn resolution_1080p_upgrade_also_downgrades_to_720p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(CommonResolution::TenEightyP),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
}
#[test]
fn resolution_1080p_error_out() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(CommonResolution::TenEightyP),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..seedance2pro_fast_builder()
};
assert!(build_kinovi_seedance_2p0_fast(builder).is_err());
}
#[test]
fn resolution_none() {
let builder = GenerateVideoRequestBuilder { resolution: None, ..seedance2pro_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(draft.resolution.is_none());
}
#[test]
fn unsupported_resolution_error_out() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(CommonResolution::FourK),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..seedance2pro_fast_builder()
};
assert!(build_kinovi_seedance_2p0_fast(builder).is_err());
}
#[test]
fn unsupported_resolution_rounds_to_720p() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(CommonResolution::FourK),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
}
}
// ── unhandled_request_state: media refs are placed there ──
mod unhandled_request_state {
use super::*;
#[test]
fn unhandled_state_is_present() {
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(seedance2pro_fast_builder()));
assert!(draft.unhandled_request_state.is_some());
}
#[test]
fn start_frame_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
start_frame: Some(ImageRef::Url("https://example.com/start.jpg".to_string())),
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
assert!(matches!(remaining.start_frame, Some(ImageRef::Url(url)) if url == "https://example.com/start.jpg"));
}
#[test]
fn end_frame_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
end_frame: Some(ImageRef::Url("https://example.com/end.jpg".to_string())),
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
assert!(matches!(remaining.end_frame, Some(ImageRef::Url(url)) if url == "https://example.com/end.jpg"));
}
#[test]
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()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
assert!(matches!(remaining.start_frame, Some(ImageRef::MediaFileToken(t)) if t.as_str() == "mf_test123"));
}
#[test]
fn reference_images_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
reference_images: Some(ImageListRef::Urls(vec![
"https://example.com/ref1.jpg".to_string(),
"https://example.com/ref2.jpg".to_string(),
])),
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
match remaining.reference_images {
Some(ImageListRef::Urls(urls)) => {
assert_eq!(urls.len(), 2);
assert_eq!(urls[0], "https://example.com/ref1.jpg");
assert_eq!(urls[1], "https://example.com/ref2.jpg");
}
_ => panic!("expected Urls variant"),
}
}
#[test]
fn reference_videos_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
reference_videos: Some(VideoListRef::Urls(vec![
"https://example.com/vid.mp4".to_string(),
])),
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
assert!(matches!(remaining.reference_videos, Some(VideoListRef::Urls(urls)) if urls.len() == 1));
}
#[test]
fn reference_audio_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
reference_audio: Some(AudioListRef::Urls(vec![
"https://example.com/audio.mp3".to_string(),
])),
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
assert!(matches!(remaining.reference_audio, Some(AudioListRef::Urls(urls)) if urls.len() == 1));
}
#[test]
fn character_tokens_placed_in_unhandled() {
let builder = GenerateVideoRequestBuilder {
reference_character_tokens: Some(CharacterListRef::CharacterTokens(vec![
CharacterToken::new("char_abc".to_string()),
CharacterToken::new("char_def".to_string()),
])),
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
let remaining = draft.unhandled_request_state.unwrap();
match remaining.reference_character_tokens {
Some(CharacterListRef::CharacterTokens(tokens)) => {
assert_eq!(tokens.len(), 2);
assert_eq!(tokens[0].as_str(), "char_abc");
assert_eq!(tokens[1].as_str(), "char_def");
}
_ => panic!("expected CharacterTokens variant"),
}
}
#[test]
fn empty_refs_are_none_in_unhandled() {
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(seedance2pro_fast_builder()));
let remaining = draft.unhandled_request_state.unwrap();
assert!(remaining.start_frame.is_none());
assert!(remaining.end_frame.is_none());
assert!(remaining.reference_images.is_none());
assert!(remaining.reference_videos.is_none());
assert!(remaining.reference_audio.is_none());
assert!(remaining.reference_character_tokens.is_none());
}
}
// ── Full combination ──
#[test]
fn full_request_all_fields() {
let builder = GenerateVideoRequestBuilder {
prompt: Some("full test".to_string()),
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
resolution: Some(CommonResolution::SevenTwentyP),
duration_seconds: Some(10),
video_batch_count: Some(4),
start_frame: Some(ImageRef::Url("https://example.com/start.jpg".to_string())),
end_frame: Some(ImageRef::Url("https://example.com/end.jpg".to_string())),
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()])),
reference_character_tokens: Some(CharacterListRef::CharacterTokens(vec![
CharacterToken::new("char_xyz".to_string()),
])),
..seedance2pro_fast_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert_eq!(draft.prompt, "full test");
assert!(matches!(draft.aspect_ratio, KinoviAspectRatio::Portrait9x16));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
assert_eq!(draft.duration_seconds, 10);
assert!(matches!(draft.batch_count, KinoviBatchCount::Four));
let remaining = draft.unhandled_request_state.unwrap();
assert!(remaining.start_frame.is_some());
assert!(remaining.end_frame.is_some());
assert!(remaining.reference_images.is_some());
assert!(remaining.reference_videos.is_some());
assert!(remaining.reference_audio.is_some());
assert!(remaining.reference_character_tokens.is_some());
}
// ── Helpers ──
fn seedance2pro_fast_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
model: CommonVideoModel::Seedance2p0Fast,
provider: Provider::Seedance2Pro,
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>) -> KinoviSeedance2p0FastDraftState {
match result.expect("build should succeed") {
VideoGenerationDraftOrRequest::Draft(
VideoGenerationDraftRequest::KinoviSeedance2p0Fast(draft)
) => draft,
_ => panic!("expected KinoviSeedance2p0Fast draft"),
}
}
}
@@ -0,0 +1,413 @@
use seedance2pro_client::requests::generate_video::generate_video::{KinoviAspectRatio, KinoviBatchCount, KinoviGenerateVideoRequest, KinoviModelType, KinoviOutputResolution};
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::draft::KinoviSeedance2p0FastDraftState;
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::request::KinoviSeedance2p0FastRequestState;
pub struct KinoviSeedance2p0FastCostState {
pub resolution: KinoviOutputResolution,
pub duration_seconds: u8,
pub batch_count: KinoviBatchCount,
/// Forward-looking: Kinovi charges for this. We need to take it into
/// account in the future.
pub has_video_reference: bool,
}
impl KinoviSeedance2p0FastCostState {
pub fn from_request(request: &KinoviSeedance2p0FastRequestState) -> Self {
Self {
resolution: request.request.output_resolution.unwrap_or(KinoviOutputResolution::SevenTwentyP),
duration_seconds: request.request.duration_seconds,
batch_count: request.request.batch_count,
has_video_reference: request.request.reference_video_urls
.as_ref()
.is_some_and(|urls| !urls.is_empty()),
}
}
pub fn from_draft(draft: &KinoviSeedance2p0FastDraftState) -> Self {
let has_video_reference = draft.unhandled_request_state
.as_ref()
.and_then(|rem| rem.reference_videos.as_ref())
.is_some();
Self {
resolution: draft.resolution.unwrap_or(KinoviOutputResolution::SevenTwentyP),
duration_seconds: draft.duration_seconds,
batch_count: draft.batch_count,
has_video_reference,
}
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
let request = KinoviGenerateVideoRequest {
// Pricing factors
model_type: KinoviModelType::Seedance2Fast,
output_resolution: Some(self.resolution),
duration_seconds: self.duration_seconds,
batch_count: self.batch_count,
// NB: has_video_reference is tracked but does not affect price yet.
// When Kinovi starts charging for video references, add cost here.
// No impact on price
prompt: String::new(),
aspect_ratio: KinoviAspectRatio::Portrait9x16,
start_frame_url: None,
end_frame_url: None,
reference_image_urls: None,
reference_video_urls: None,
reference_audio_urls: None,
character_ids: None,
use_face_blur_hack: None,
};
let cost_in_credits = request.estimate_credits();
let cost_in_usd_cents = request.estimate_cost_in_usd_cents();
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_credits as u64),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::common_resolution::CommonResolution;
use crate::api::common_video_model::CommonVideoModel;
use crate::api::provider::Provider;
use crate::api::video_list_ref::VideoListRef;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
// ── Direct estimate_cost() tests ──
// These must match the reference implementation values exactly.
// -- 720p (legacy pricing: 220 credits/$1, 28 credits/sec) --
mod pricing_720p {
use super::*;
#[test]
fn cost_720p_batch_1() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One), 51);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 64);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One), 127);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One), 191);
}
#[test]
fn cost_720p_batch_2() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Two), 127);
}
#[test]
fn cost_720p_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Four), 255);
}
}
// -- 480p (new pricing: 193 credits/$1, 10 credits/sec) --
mod pricing_480p {
use super::*;
#[test]
fn cost_480p_batch_1() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One), 21);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 26);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 10, KinoviBatchCount::One), 52);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 15, KinoviBatchCount::One), 78);
}
#[test]
fn cost_480p_batch_2() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Two), 52);
}
#[test]
fn cost_480p_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Four), 104);
}
}
// -- Relative pricing --
mod relative_pricing_tests {
use super::*;
#[test]
fn cost_480p_cheaper_than_720p() {
let c480 = usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One);
let c720 = usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One);
assert!(c480 < c720, "480p ({}) should be cheaper than 720p ({})", c480, c720);
}
#[test]
fn cost_scales_with_duration() {
let c4 = usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One);
let c10 = usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One);
let c15 = usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One);
assert!(c4 < c10);
assert!(c10 < c15);
}
#[test]
fn cost_scales_with_batch() {
let b1 = usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One);
let b2 = usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Two);
let b4 = usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Four);
assert!(b1 < b2);
assert!(b2 < b4);
}
}
// -- Video reference does NOT affect cost (yet) --
#[test]
fn video_reference_does_not_affect_cost() {
let base = KinoviSeedance2p0FastCostState {
resolution: KinoviOutputResolution::SevenTwentyP,
duration_seconds: 5,
batch_count: KinoviBatchCount::One,
has_video_reference: false,
};
let without = base.estimate_cost();
let with = KinoviSeedance2p0FastCostState { has_video_reference: true, ..base }.estimate_cost();
assert_eq!(without.cost_in_usd_cents, with.cost_in_usd_cents);
assert_eq!(without.cost_in_credits, with.cost_in_credits);
}
// ── from_request() tests ──
mod from_request_tests {
use super::*;
#[test]
fn from_request_720p() {
let req = make_request_state(Some(KinoviOutputResolution::SevenTwentyP), 5, KinoviBatchCount::One, false);
let cost = KinoviSeedance2p0FastCostState::from_request(&req);
assert!(matches!(cost.resolution, KinoviOutputResolution::SevenTwentyP));
assert_eq!(cost.duration_seconds, 5);
assert!(matches!(cost.batch_count, KinoviBatchCount::One));
assert!(!cost.has_video_reference);
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(64));
}
#[test]
fn from_request_none_defaults_to_720p() {
let req = make_request_state(None, 5, KinoviBatchCount::One, false);
let cost = KinoviSeedance2p0FastCostState::from_request(&req);
assert!(matches!(cost.resolution, KinoviOutputResolution::SevenTwentyP));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(64));
}
#[test]
fn from_request_480p() {
let req = make_request_state(Some(KinoviOutputResolution::FourEightyP), 5, KinoviBatchCount::One, false);
let cost = KinoviSeedance2p0FastCostState::from_request(&req);
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(26));
}
#[test]
fn from_request_with_video_reference() {
let req = make_request_state(Some(KinoviOutputResolution::SevenTwentyP), 5, KinoviBatchCount::One, true);
let cost = KinoviSeedance2p0FastCostState::from_request(&req);
assert!(cost.has_video_reference);
// Video refs don't affect cost yet
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(64));
}
#[test]
fn from_request_without_video_reference() {
let req = make_request_state(Some(KinoviOutputResolution::SevenTwentyP), 5, KinoviBatchCount::One, false);
let cost = KinoviSeedance2p0FastCostState::from_request(&req);
assert!(!cost.has_video_reference);
}
}
// ── from_draft() tests ──
mod from_draft_tests {
use super::*;
#[test]
fn from_draft_720p_default() {
let draft = make_draft(5, 1, None, false);
let cost = KinoviSeedance2p0FastCostState::from_draft(&draft);
assert!(matches!(cost.resolution, KinoviOutputResolution::SevenTwentyP));
assert_eq!(cost.duration_seconds, 5);
assert!(matches!(cost.batch_count, KinoviBatchCount::One));
assert!(!cost.has_video_reference);
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(64));
}
#[test]
fn from_draft_480p() {
let draft = make_draft(5, 1, Some(CommonResolution::FourEightyP), false);
let cost = KinoviSeedance2p0FastCostState::from_draft(&draft);
assert!(matches!(cost.resolution, KinoviOutputResolution::FourEightyP));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(26));
}
#[test]
fn from_draft_with_video_reference() {
let draft = make_draft(5, 1, None, true);
let cost = KinoviSeedance2p0FastCostState::from_draft(&draft);
assert!(cost.has_video_reference);
// Video refs don't affect cost yet
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(64));
}
#[test]
fn from_draft_without_video_reference() {
let draft = make_draft(5, 1, None, false);
let cost = KinoviSeedance2p0FastCostState::from_draft(&draft);
assert!(!cost.has_video_reference);
}
#[test]
fn from_draft_duration_15_batch_2() {
let draft = make_draft(15, 2, Some(CommonResolution::SevenTwentyP), false);
let cost = KinoviSeedance2p0FastCostState::from_draft(&draft);
assert_eq!(cost.duration_seconds, 15);
assert!(matches!(cost.batch_count, KinoviBatchCount::Two));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(382));
}
}
// ── Cross-check: from_draft matches from_request ──
mod cross_check_tests {
use super::*;
#[test]
fn draft_and_request_produce_same_cost() {
// 720p, 5s, batch 1
let draft = make_draft(5, 1, Some(CommonResolution::SevenTwentyP), false);
let req = make_request_state(Some(KinoviOutputResolution::SevenTwentyP), 5, KinoviBatchCount::One, false);
let draft_cost = KinoviSeedance2p0FastCostState::from_draft(&draft).estimate_cost();
let req_cost = KinoviSeedance2p0FastCostState::from_request(&req).estimate_cost();
assert_eq!(draft_cost.cost_in_usd_cents, req_cost.cost_in_usd_cents);
assert_eq!(draft_cost.cost_in_credits, req_cost.cost_in_credits);
}
#[test]
fn draft_and_request_produce_same_cost_480p() {
let draft = make_draft(10, 2, Some(CommonResolution::FourEightyP), false);
let req = make_request_state(Some(KinoviOutputResolution::FourEightyP), 10, KinoviBatchCount::Two, false);
let draft_cost = KinoviSeedance2p0FastCostState::from_draft(&draft).estimate_cost();
let req_cost = KinoviSeedance2p0FastCostState::from_request(&req).estimate_cost();
assert_eq!(draft_cost.cost_in_usd_cents, req_cost.cost_in_usd_cents);
assert_eq!(draft_cost.cost_in_credits, req_cost.cost_in_credits);
}
}
// ── Credits spot checks ──
mod credits_tests {
use super::*;
#[test]
fn credits_720p() {
assert_eq!(credits(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 140);
}
#[test]
fn credits_480p() {
assert_eq!(credits(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 50);
}
}
// ── Helpers ──
fn usd_cents(
resolution: KinoviOutputResolution,
duration_seconds: u8,
batch_count: KinoviBatchCount,
) -> u64 {
KinoviSeedance2p0FastCostState { resolution, duration_seconds, batch_count, has_video_reference: false }
.estimate_cost()
.cost_in_usd_cents
.unwrap()
}
fn credits(
resolution: KinoviOutputResolution,
duration_seconds: u8,
batch_count: KinoviBatchCount,
) -> u64 {
KinoviSeedance2p0FastCostState { resolution, duration_seconds, batch_count, has_video_reference: false }
.estimate_cost()
.cost_in_credits
.unwrap()
}
/// Build a draft via the builder to test from_draft().
fn make_draft(
duration_seconds: u16,
video_batch_count: u16,
resolution: Option<CommonResolution>,
with_video_ref: bool,
) -> KinoviSeedance2p0FastDraftState {
let reference_videos = if with_video_ref {
Some(VideoListRef::Urls(vec!["https://example.com/video.mp4".to_string()]))
} else {
None
};
let builder = GenerateVideoRequestBuilder {
model: CommonVideoModel::Seedance2p0Fast,
provider: Provider::Seedance2Pro,
resolution,
reference_videos,
duration_seconds: Some(duration_seconds),
video_batch_count: Some(video_batch_count),
..Default::default()
};
match builder.build2().expect("build2 should succeed") {
crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest::Draft(
crate::generate::generate_video_v2::video_generation_draft::VideoGenerationDraftRequest::KinoviSeedance2p0Fast(draft)
) => draft,
_ => panic!("expected KinoviSeedance2p0Fast draft"),
}
}
/// Build a request state for from_request() tests.
fn make_request_state(
resolution: Option<KinoviOutputResolution>,
duration_seconds: u8,
batch_count: KinoviBatchCount,
with_video_ref: bool,
) -> KinoviSeedance2p0FastRequestState {
let reference_video_urls = if with_video_ref {
Some(vec!["https://cdn.seedance2-pro.com/video.mp4".to_string()])
} else {
None
};
KinoviSeedance2p0FastRequestState {
request: KinoviGenerateVideoRequest {
model_type: KinoviModelType::Seedance2Fast,
prompt: "test".to_string(),
aspect_ratio: KinoviAspectRatio::Landscape16x9,
output_resolution: resolution,
duration_seconds,
batch_count,
start_frame_url: None,
end_frame_url: None,
reference_image_urls: None,
reference_video_urls,
reference_audio_urls: None,
character_ids: None,
use_face_blur_hack: None,
},
}
}
}
@@ -0,0 +1,98 @@
use seedance2pro_client::requests::generate_video::generate_video::{KinoviAspectRatio, KinoviBatchCount, KinoviGenerateVideoRequest, KinoviModelType, KinoviOutputResolution};
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::video_list_ref::VideoListRef;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_video_v2::providers::kinovi::resolve::{
audio_list_ref_into_urls_or_tokens, image_list_ref_into_urls_or_tokens,
resolve_and_upload_list, resolve_and_upload_single, resolve_character_tokens,
video_list_ref_into_urls_or_tokens,
};
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::request::KinoviSeedance2p0FastRequestState;
use crate::generate::generate_video_v2::video_generation_draft_context::VideoGenerationDraftContext;
#[derive(Debug, Clone)]
pub struct KinoviSeedance2p0FastDraftState {
// Materialized / finalized types
pub prompt: String,
pub aspect_ratio: KinoviAspectRatio,
pub resolution: Option<KinoviOutputResolution>,
pub duration_seconds: u8,
pub batch_count: KinoviBatchCount,
// Pending types that need to be queried.
pub unhandled_request_state: Option<KinoviSeedance2p0FastRemainingItems>,
}
#[derive(Debug, Clone)]
pub struct KinoviSeedance2p0FastRemainingItems {
pub start_frame: Option<ImageRef>,
pub end_frame: Option<ImageRef>,
pub reference_images: Option<ImageListRef>,
pub reference_videos: Option<VideoListRef>,
pub reference_audio: Option<AudioListRef>,
pub reference_character_tokens: Option<CharacterListRef>,
}
impl KinoviSeedance2p0FastDraftState {
pub async fn to_request(
&mut self,
draft_context: &VideoGenerationDraftContext<'_>,
) -> Result<KinoviSeedance2p0FastRequestState, ArtcraftRouterError> {
let client = draft_context.get_seedance2pro_client_ref()?;
let session = &client.session;
let mut start_frame_url = None;
let mut end_frame_url = None;
let mut reference_image_urls = None;
let mut reference_video_urls = None;
let mut reference_audio_urls = None;
let mut character_ids = None;
if let Some(remaining) = self.unhandled_request_state.take() {
let map = draft_context.media_file_to_artcraft_url_map;
start_frame_url = resolve_and_upload_single(session, remaining.start_frame, map).await?;
end_frame_url = resolve_and_upload_single(session, remaining.end_frame, map).await?;
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?;
character_ids = resolve_character_tokens(
remaining.reference_character_tokens.as_ref(),
draft_context,
)?;
}
let request = KinoviGenerateVideoRequest {
model_type: KinoviModelType::Seedance2Fast,
prompt: self.prompt.clone(),
aspect_ratio: self.aspect_ratio,
output_resolution: self.resolution,
duration_seconds: self.duration_seconds,
batch_count: self.batch_count,
start_frame_url,
end_frame_url,
reference_image_urls,
reference_video_urls,
reference_audio_urls,
character_ids,
use_face_blur_hack: None,
};
Ok(KinoviSeedance2p0FastRequestState { request })
}
}
@@ -0,0 +1,4 @@
pub mod build;
pub mod draft;
pub mod request;
pub mod cost;
@@ -0,0 +1,257 @@
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_video::generate_video_response::{GenerateVideoResponse, Seedance2proVideoResponsePayload};
use seedance2pro_client::requests::generate_video::generate_video::{generate_video, GenerateVideoArgs, KinoviGenerateVideoRequest};
#[derive(Debug, Clone)]
pub struct KinoviSeedance2p0FastRequestState {
/// Final materialized request; ready to fire.
pub request: KinoviGenerateVideoRequest,
}
impl KinoviSeedance2p0FastRequestState {
pub async fn send(&self, client: &RouterSeedance2ProClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let session = &client.session;
let args = GenerateVideoArgs {
session,
host_override: None,
request: self.request.clone(), // TODO: Yuck.
};
let response = generate_video(args)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Seedance2Pro(err)))?;
Ok(GenerateVideoResponse::Seedance2Pro(Seedance2proVideoResponsePayload {
order_id: response.order_id,
task_id: response.task_id,
}))
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use tokens::tokens::characters::CharacterToken;
use tokens::tokens::media_files::MediaFileToken;
use crate::api::character_list_ref::CharacterListRef;
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_resolution::CommonResolution;
use crate::api::common_video_model::CommonVideoModel;
use crate::api::image_list_ref::ImageListRef;
use crate::api::image_ref::ImageRef;
use crate::api::provider::Provider;
use crate::api::video_list_ref::VideoListRef;
use crate::client::router_client::RouterClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
use crate::generate::generate_video_v2::video_generation_draft_context::VideoGenerationDraftContext;
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
mod aspect_ratio_tests {
use super::*;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn landscape() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A corgi running through a field of wildflowers at sunset.".to_string()),
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
..seedance2pro_fast_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn portrait() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A cat sitting on a windowsill watching rain.".to_string()),
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
..seedance2pro_fast_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn square() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A hummingbird hovering near a flower.".to_string()),
aspect_ratio: Some(CommonAspectRatio::Square),
..seedance2pro_fast_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
}
mod resolution_tests {
use super::*;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn res_480p() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A shiba inu playing in autumn leaves.".to_string()),
resolution: Some(CommonResolution::FourEightyP),
..seedance2pro_fast_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn res_720p() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A golden retriever catching a frisbee on the beach.".to_string()),
resolution: Some(CommonResolution::SevenTwentyP),
..seedance2pro_fast_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
}
mod modality_tests {
use super::*;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn text_to_video() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("A whale breaching in the open ocean at dawn, cinematic.".to_string()),
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
..seedance2pro_fast_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn keyframe_start_and_end_frame() {
let start_token = MediaFileToken::new("mf_start".to_string());
let end_token = MediaFileToken::new("mf_end".to_string());
let mut media_map = HashMap::new();
media_map.insert(start_token.clone(), test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL.to_string());
media_map.insert(end_token.clone(), test_data::web::image_urls::FOREST_BACKDROP_IMAGE_URL.to_string());
let response = run_pipeline_with_media_map(GenerateVideoRequestBuilder {
prompt: Some("The dog walks from the lake to the forest.".to_string()),
start_frame: Some(ImageRef::MediaFileToken(start_token)),
end_frame: Some(ImageRef::MediaFileToken(end_token)),
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
..seedance2pro_fast_builder()
}, media_map).await;
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn image_references() {
let response = run_pipeline(GenerateVideoRequestBuilder {
prompt: Some("The dog in @2 runs through the scenery in @1 towards the building in @3.".to_string()),
reference_images: Some(ImageListRef::Urls(vec![
test_data::web::image_urls::FOREST_BACKDROP_IMAGE_URL.to_string(),
test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL.to_string(),
test_data::web::image_urls::WHITE_HOUSE_SUNSET_IMAGE_URL.to_string(),
])),
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
..seedance2pro_fast_builder()
}).await;
assert!(matches!(response, GenerateVideoResponse::Seedance2Pro(_)));
assert_eq!(1, 2, "Inspect output above");
}
}
// ── Helpers ──
fn seedance2pro_fast_builder() -> GenerateVideoRequestBuilder {
GenerateVideoRequestBuilder {
model: CommonVideoModel::Seedance2p0Fast,
provider: Provider::Seedance2Pro,
duration_seconds: Some(4),
video_batch_count: Some(1),
..Default::default()
}
}
fn get_seedance2pro_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))
}
/// Run the full pipeline: builder -> build2 -> finalize -> send.
async fn run_pipeline(builder: GenerateVideoRequestBuilder) -> GenerateVideoResponse {
let client = get_seedance2pro_client();
let draft_or_request = builder.build2().expect("build2 should succeed");
let draft = match draft_or_request {
VideoGenerationDraftOrRequest::Draft(d) => d,
_ => panic!("expected Draft variant"),
};
let draft_context = VideoGenerationDraftContext {
client: Some(&client),
..Default::default()
};
let request = draft.finalize(draft_context).await.expect("finalize should succeed");
let response = request.send_request(&client).await.expect("send_request should succeed");
match &response {
GenerateVideoResponse::Seedance2Pro(p) => {
println!("task_id={}, order_id={}", p.task_id, p.order_id);
}
other => println!("response: {:?}", other),
}
response
}
/// Run the full pipeline with a media file token map for resolving references.
async fn run_pipeline_with_media_map(
builder: GenerateVideoRequestBuilder,
media_map: HashMap<MediaFileToken, String>,
) -> GenerateVideoResponse {
let client = get_seedance2pro_client();
let draft_or_request = builder.build2().expect("build2 should succeed");
let draft = match draft_or_request {
VideoGenerationDraftOrRequest::Draft(d) => d,
_ => panic!("expected Draft variant"),
};
let draft_context = VideoGenerationDraftContext {
client: Some(&client),
media_file_to_artcraft_url_map: Some(&media_map),
..Default::default()
};
let request = draft.finalize(draft_context).await.expect("finalize should succeed");
let response = request.send_request(&client).await.expect("send_request should succeed");
match &response {
GenerateVideoResponse::Seedance2Pro(p) => {
println!("task_id={}, order_id={}", p.task_id, p.order_id);
}
other => println!("response: {:?}", other),
}
response
}
}
@@ -3,6 +3,8 @@ use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0::cost::KinoviSeedance2p0CostState;
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0::draft::KinoviSeedance2p0DraftState;
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::cost::KinoviSeedance2p0FastCostState;
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::draft::KinoviSeedance2p0FastDraftState;
use crate::generate::generate_video_v2::video_generation_draft_context::VideoGenerationDraftContext;
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
@@ -12,6 +14,7 @@ use crate::generate::generate_video_v2::video_generation_request::VideoGeneratio
#[derive(Clone, Debug)]
pub enum VideoGenerationDraftRequest {
KinoviSeedance2p0(KinoviSeedance2p0DraftState),
KinoviSeedance2p0Fast(KinoviSeedance2p0FastDraftState),
}
impl VideoGenerationDraftRequest {
@@ -20,6 +23,7 @@ impl VideoGenerationDraftRequest {
pub fn estimate_cost(&self) -> Result<VideoGenerationCostEstimate, ArtcraftRouterError> {
match self {
VideoGenerationDraftRequest::KinoviSeedance2p0(draft) => Ok(KinoviSeedance2p0CostState::from_draft(draft).estimate_cost()),
VideoGenerationDraftRequest::KinoviSeedance2p0Fast(draft) => Ok(KinoviSeedance2p0FastCostState::from_draft(draft).estimate_cost()),
}
}
@@ -31,6 +35,10 @@ impl VideoGenerationDraftRequest {
let result = draft.to_request(&draft_context).await?;
Ok(VideoGenerationRequest::KinoviSeedance2p0(result))
},
VideoGenerationDraftRequest::KinoviSeedance2p0Fast(mut draft) => {
let result = draft.to_request(&draft_context).await?;
Ok(VideoGenerationRequest::KinoviSeedance2p0Fast(result))
},
}
}
}
@@ -4,10 +4,13 @@ use crate::generate::generate_video::generate_video_response::GenerateVideoRespo
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0::cost::KinoviSeedance2p0CostState;
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0::request::KinoviSeedance2p0RequestState;
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::cost::KinoviSeedance2p0FastCostState;
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::request::KinoviSeedance2p0FastRequestState;
#[derive(Clone, Debug)]
pub enum VideoGenerationRequest {
KinoviSeedance2p0(KinoviSeedance2p0RequestState),
KinoviSeedance2p0Fast(KinoviSeedance2p0FastRequestState),
}
impl VideoGenerationRequest {
@@ -16,6 +19,7 @@ impl VideoGenerationRequest {
pub fn estimate_cost(&self) -> Result<VideoGenerationCostEstimate, ArtcraftRouterError> {
match self {
VideoGenerationRequest::KinoviSeedance2p0(request) => Ok(KinoviSeedance2p0CostState::from_request(request).estimate_cost()),
VideoGenerationRequest::KinoviSeedance2p0Fast(request) => Ok(KinoviSeedance2p0FastCostState::from_request(request).estimate_cost()),
}
}
@@ -27,6 +31,10 @@ impl VideoGenerationRequest {
let client_ref = client.get_seedance2pro_client_ref()?;
request.send(client_ref).await
},
VideoGenerationRequest::KinoviSeedance2p0Fast(request) => {
let client_ref = client.get_seedance2pro_client_ref()?;
request.send(client_ref).await
},
}
}
}