mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
add seedance 2.0 tests
This commit is contained in:
1 parent
9a03b2fc2a
commit
198aed0f91
3 files changed
+461
No files matched your search
+1
@@ -0,0 +1 @@
|
||||
mod test_kinovi_seedance_2p0;
|
||||
+459
@@ -0,0 +1,459 @@
|
||||
//! Tests for omni-gen video against the Seedance 2.0 model (Kinovi/Seedance2Pro provider).
|
||||
//!
|
||||
//! Seedance 2.0 is special: it uses the Seedance2Pro/Kinovi provider for execution
|
||||
//! instead of Fal. The cost estimation uses Artcraft provider with media-token-form
|
||||
//! references, while the execution plan uses URL-form references.
|
||||
//!
|
||||
//! Pricing: 16¢ per second × batch count.
|
||||
//! - 4s/batch 1 = 64¢, 5s/batch 1 = 80¢, 15s/batch 1 = 240¢
|
||||
//! - Batch 2 = 2×, Batch 4 = 4×
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use url::Url;
|
||||
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::distill_video_request::{
|
||||
distill_video_request, DistilledVideoRequest,
|
||||
};
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
fn make_request(
|
||||
prompt: Option<&str>,
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
duration_seconds: Option<u16>,
|
||||
video_batch_count: Option<u16>,
|
||||
start_frame_token: Option<MediaFileToken>,
|
||||
end_frame_token: Option<MediaFileToken>,
|
||||
reference_image_tokens: Option<Vec<MediaFileToken>>,
|
||||
reference_video_tokens: Option<Vec<MediaFileToken>>,
|
||||
reference_audio_tokens: Option<Vec<MediaFileToken>>,
|
||||
) -> OmniGenVideoCostAndGenerateRequest {
|
||||
OmniGenVideoCostAndGenerateRequest {
|
||||
idempotency_token: Some("11111111-1111-1111-1111-111111111111".to_string()),
|
||||
model: Some(CommonVideoModel::Seedance2p0),
|
||||
prompt: prompt.map(|s| s.to_string()),
|
||||
negative_prompt: None,
|
||||
start_frame_image_media_token: start_frame_token,
|
||||
end_frame_image_media_token: end_frame_token,
|
||||
reference_image_media_tokens: reference_image_tokens,
|
||||
reference_video_media_tokens: reference_video_tokens,
|
||||
reference_audio_media_tokens: reference_audio_tokens,
|
||||
reference_character_tokens: None,
|
||||
resolution: None,
|
||||
aspect_ratio,
|
||||
quality: None,
|
||||
duration_seconds,
|
||||
video_batch_count,
|
||||
generate_audio: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn text_only_request(
|
||||
duration_seconds: Option<u16>,
|
||||
video_batch_count: Option<u16>,
|
||||
) -> OmniGenVideoCostAndGenerateRequest {
|
||||
make_request(
|
||||
Some("a cat dancing"),
|
||||
None,
|
||||
duration_seconds,
|
||||
video_batch_count,
|
||||
None, None, None, None, None,
|
||||
)
|
||||
}
|
||||
|
||||
fn fake_token(name: &str) -> MediaFileToken {
|
||||
MediaFileToken::new_from_str(name)
|
||||
}
|
||||
|
||||
fn build_hydration_map(tokens: &[&MediaFileToken]) -> HashMap<MediaFileToken, Url> {
|
||||
tokens.iter().enumerate().map(|(i, t)| {
|
||||
let url = Url::parse(&format!("https://cdn.example.com/media/{}.png", i)).unwrap();
|
||||
((*t).clone(), url)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
fn distill_text(request: &OmniGenVideoCostAndGenerateRequest) -> DistilledVideoRequest {
|
||||
let empty: HashMap<MediaFileToken, Url> = HashMap::new();
|
||||
distill_video_request(request, Some(&empty), Provider::Seedance2Pro)
|
||||
.expect("distill should succeed for Seedance 2.0 (text)")
|
||||
}
|
||||
|
||||
fn distill_with_map(
|
||||
request: &OmniGenVideoCostAndGenerateRequest,
|
||||
hydration: &HashMap<MediaFileToken, Url>,
|
||||
) -> DistilledVideoRequest {
|
||||
distill_video_request(request, Some(hydration), Provider::Seedance2Pro)
|
||||
.expect("distill should succeed for Seedance 2.0 (with media)")
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// COST TESTS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
mod cost_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_is_present() {
|
||||
let d = distill_text(&text_only_request(Some(5), None));
|
||||
assert!(d.cost.cost_in_credits.is_some());
|
||||
assert!(d.cost.cost_in_usd_cents.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_is_not_free() {
|
||||
let d = distill_text(&text_only_request(Some(5), None));
|
||||
assert!(!d.cost.is_free);
|
||||
}
|
||||
|
||||
// Pricing: 16¢/sec × batch_count
|
||||
|
||||
#[test]
|
||||
fn cost_4s_batch_1() {
|
||||
let d = distill_text(&text_only_request(Some(4), Some(1)));
|
||||
assert_eq!(d.cost.cost_in_usd_cents, Some(64));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_5s_batch_1() {
|
||||
let d = distill_text(&text_only_request(Some(5), Some(1)));
|
||||
assert_eq!(d.cost.cost_in_usd_cents, Some(80));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_6s_batch_1() {
|
||||
let d = distill_text(&text_only_request(Some(6), Some(1)));
|
||||
assert_eq!(d.cost.cost_in_usd_cents, Some(96));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_10s_batch_1() {
|
||||
let d = distill_text(&text_only_request(Some(10), Some(1)));
|
||||
assert_eq!(d.cost.cost_in_usd_cents, Some(160));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_15s_batch_1() {
|
||||
let d = distill_text(&text_only_request(Some(15), Some(1)));
|
||||
assert_eq!(d.cost.cost_in_usd_cents, Some(240));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_5s_batch_2() {
|
||||
let d = distill_text(&text_only_request(Some(5), Some(2)));
|
||||
assert_eq!(d.cost.cost_in_usd_cents, Some(160));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_5s_batch_4() {
|
||||
let d = distill_text(&text_only_request(Some(5), Some(4)));
|
||||
assert_eq!(d.cost.cost_in_usd_cents, Some(320));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_15s_batch_4() {
|
||||
let d = distill_text(&text_only_request(Some(15), Some(4)));
|
||||
assert_eq!(d.cost.cost_in_usd_cents, Some(960));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_same_with_or_without_start_frame() {
|
||||
// Cost should not change based on media presence — it's duration × batch.
|
||||
let text_d = distill_text(&text_only_request(Some(5), Some(1)));
|
||||
|
||||
let start_token = fake_token("start_frame");
|
||||
let map = build_hydration_map(&[&start_token]);
|
||||
let req = make_request(
|
||||
Some("a cat"), None, Some(5), Some(1),
|
||||
Some(start_token), None, None, None, None,
|
||||
);
|
||||
let frame_d = distill_with_map(&req, &map);
|
||||
|
||||
assert_eq!(text_d.cost.cost_in_usd_cents, frame_d.cost.cost_in_usd_cents);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_estimation_uses_media_tokens_not_urls() {
|
||||
// The Artcraft cost plan should use token-form references.
|
||||
// If it used URLs, the Artcraft plan builder would reject them.
|
||||
// This test passes if distillation succeeds at all — the Artcraft
|
||||
// builder would error out on URL-form refs.
|
||||
let start_token = fake_token("start_frame");
|
||||
let ref_img = fake_token("ref_img_1");
|
||||
let ref_vid = fake_token("ref_vid_1");
|
||||
let ref_aud = fake_token("ref_aud_1");
|
||||
|
||||
let map = build_hydration_map(&[&start_token, &ref_img, &ref_vid, &ref_aud]);
|
||||
|
||||
let req = make_request(
|
||||
Some("test"), None, Some(5), Some(1),
|
||||
Some(start_token), None,
|
||||
Some(vec![ref_img]),
|
||||
Some(vec![ref_vid]),
|
||||
Some(vec![ref_aud]),
|
||||
);
|
||||
|
||||
// If this succeeds, the Artcraft cost path handled tokens correctly.
|
||||
let d = distill_with_map(&req, &map);
|
||||
assert!(d.cost.cost_in_credits.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_duration_produces_valid_cost() {
|
||||
// When duration_seconds is None, the model should use a default.
|
||||
let d = distill_text(&text_only_request(None, None));
|
||||
assert!(d.cost.cost_in_usd_cents.unwrap_or(0) > 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// GENERATION PLAN TESTS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
mod generation_plan_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn provider_is_seedance2pro() {
|
||||
let d = distill_text(&text_only_request(Some(5), None));
|
||||
assert!(
|
||||
matches!(d.execution_provider, Provider::Seedance2Pro),
|
||||
"expected Seedance2Pro provider, got {:?}", d.execution_provider
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_is_seedance2pro_variant() {
|
||||
let d = distill_text(&text_only_request(Some(5), None));
|
||||
assert!(
|
||||
matches!(d.plan(), VideoGenerationPlan::Seedance2proSeedance2p0(_)),
|
||||
"expected Seedance2proSeedance2p0 plan variant, got {:?}", d.plan()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_only_has_no_media_urls() {
|
||||
let d = distill_text(&text_only_request(Some(5), None));
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
assert!(plan.start_frame_url.is_none());
|
||||
assert!(plan.end_frame_url.is_none());
|
||||
assert!(plan.reference_image_urls.is_none());
|
||||
assert!(plan.reference_video_urls.is_none());
|
||||
assert!(plan.reference_audio_urls.is_none());
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_only_prompt_is_passed_through() {
|
||||
let d = distill_text(&text_only_request(Some(5), None));
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
assert_eq!(plan.prompt.as_deref(), Some("a cat dancing"));
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_frame_uses_url_not_token() {
|
||||
let start_token = fake_token("start_frame");
|
||||
let map = build_hydration_map(&[&start_token]);
|
||||
|
||||
let req = make_request(
|
||||
Some("test"), None, Some(5), None,
|
||||
Some(start_token), None, None, None, None,
|
||||
);
|
||||
let d = distill_with_map(&req, &map);
|
||||
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
let url = plan.start_frame_url.as_ref().expect("start_frame_url should be set");
|
||||
assert!(url.starts_with("https://"), "expected URL, got: {}", url);
|
||||
assert!(!url.contains("m_"), "URL should not look like a media token");
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn end_frame_uses_url_not_token() {
|
||||
let end_token = fake_token("end_frame");
|
||||
let map = build_hydration_map(&[&end_token]);
|
||||
|
||||
let req = make_request(
|
||||
Some("test"), None, Some(5), None,
|
||||
None, Some(end_token), None, None, None,
|
||||
);
|
||||
let d = distill_with_map(&req, &map);
|
||||
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
let url = plan.end_frame_url.as_ref().expect("end_frame_url should be set");
|
||||
assert!(url.starts_with("https://"), "expected URL, got: {}", url);
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_and_end_frame_both_resolved() {
|
||||
let start = fake_token("sf");
|
||||
let end = fake_token("ef");
|
||||
let map = build_hydration_map(&[&start, &end]);
|
||||
|
||||
let req = make_request(
|
||||
Some("test"), None, Some(5), None,
|
||||
Some(start), Some(end), None, None, None,
|
||||
);
|
||||
let d = distill_with_map(&req, &map);
|
||||
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
assert!(plan.start_frame_url.is_some());
|
||||
assert!(plan.end_frame_url.is_some());
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_images_use_urls() {
|
||||
let img1 = fake_token("img1");
|
||||
let img2 = fake_token("img2");
|
||||
let map = build_hydration_map(&[&img1, &img2]);
|
||||
|
||||
let req = make_request(
|
||||
Some("test"), None, Some(5), None,
|
||||
None, None,
|
||||
Some(vec![img1, img2]),
|
||||
None, None,
|
||||
);
|
||||
let d = distill_with_map(&req, &map);
|
||||
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
let urls = plan.reference_image_urls.as_ref().expect("reference_image_urls should be set");
|
||||
assert_eq!(urls.len(), 2);
|
||||
for url in urls {
|
||||
assert!(url.starts_with("https://"), "expected URL, got: {}", url);
|
||||
}
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_videos_use_urls() {
|
||||
let vid1 = fake_token("vid1");
|
||||
let map = build_hydration_map(&[&vid1]);
|
||||
|
||||
let req = make_request(
|
||||
Some("test"), None, Some(5), None,
|
||||
None, None, None,
|
||||
Some(vec![vid1]),
|
||||
None,
|
||||
);
|
||||
let d = distill_with_map(&req, &map);
|
||||
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
let urls = plan.reference_video_urls.as_ref().expect("reference_video_urls should be set");
|
||||
assert_eq!(urls.len(), 1);
|
||||
assert!(urls[0].starts_with("https://"));
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_audio_use_urls() {
|
||||
let aud1 = fake_token("aud1");
|
||||
let map = build_hydration_map(&[&aud1]);
|
||||
|
||||
let req = make_request(
|
||||
Some("test"), None, Some(5), None,
|
||||
None, None, None, None,
|
||||
Some(vec![aud1]),
|
||||
);
|
||||
let d = distill_with_map(&req, &map);
|
||||
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
let urls = plan.reference_audio_urls.as_ref().expect("reference_audio_urls should be set");
|
||||
assert_eq!(urls.len(), 1);
|
||||
assert!(urls[0].starts_with("https://"));
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_reference_types_together() {
|
||||
let sf = fake_token("sf");
|
||||
let ef = fake_token("ef");
|
||||
let img = fake_token("img");
|
||||
let vid = fake_token("vid");
|
||||
let aud = fake_token("aud");
|
||||
let map = build_hydration_map(&[&sf, &ef, &img, &vid, &aud]);
|
||||
|
||||
let req = make_request(
|
||||
Some("full test"), None, Some(8), Some(2),
|
||||
Some(sf), Some(ef),
|
||||
Some(vec![img]),
|
||||
Some(vec![vid]),
|
||||
Some(vec![aud]),
|
||||
);
|
||||
let d = distill_with_map(&req, &map);
|
||||
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
assert!(plan.start_frame_url.is_some());
|
||||
assert!(plan.end_frame_url.is_some());
|
||||
assert_eq!(plan.reference_image_urls.as_ref().map(|v| v.len()), Some(1));
|
||||
assert_eq!(plan.reference_video_urls.as_ref().map(|v| v.len()), Some(1));
|
||||
assert_eq!(plan.reference_audio_urls.as_ref().map(|v| v.len()), Some(1));
|
||||
assert!(plan.prompt.as_deref() == Some("full test"));
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_is_clamped_to_valid_range() {
|
||||
// Duration below 4 should clamp to 4.
|
||||
let d = distill_text(&text_only_request(Some(1), None));
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
assert!(plan.duration_seconds >= 4, "expected >= 4, got {}", plan.duration_seconds);
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
|
||||
// Duration above 15 should clamp to 15.
|
||||
let d = distill_text(&text_only_request(Some(99), None));
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
assert!(plan.duration_seconds <= 15, "expected <= 15, got {}", plan.duration_seconds);
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_prompt_defaults_to_empty() {
|
||||
let req = make_request(
|
||||
None, None, Some(5), None,
|
||||
None, None, None, None, None,
|
||||
);
|
||||
let d = distill_text(&req);
|
||||
if let VideoGenerationPlan::Seedance2proSeedance2p0(plan) = d.plan() {
|
||||
// Prompt should be None or empty string.
|
||||
let prompt = plan.prompt.as_deref().unwrap_or("");
|
||||
assert!(prompt.is_empty() || plan.prompt.is_none());
|
||||
} else {
|
||||
panic!("wrong plan variant");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
mod test_seedance_1p5_pro;
|
||||
mod test_veo_2;
|
||||
mod test_veo_3;
|
||||
mod kinovi;
|
||||
Reference in new issue
Block a user