older seedance models

This commit is contained in:
Brandon Thomas committed 2026-05-26 08:03:47 -04:00
1 parent 8ad87f5aa4
commit bcfbfb9097
11 files changed
+377

No files matched your search

@@ -20,6 +20,8 @@ use crate::generate::generate_video_v2::providers::artcraft::kling_3p0_pro::buil
use crate::generate::generate_video_v2::providers::artcraft::kling_3p0_standard::build::build_artcraft_kling_3p0_standard;
use crate::generate::generate_video_v2::providers::artcraft::preview_model::build::build_artcraft_preview_model;
use crate::generate::generate_video_v2::providers::artcraft::preview_model_fast::build::build_artcraft_preview_model_fast;
use crate::generate::generate_video_v2::providers::artcraft::seedance_1p0_lite::build::build_artcraft_seedance_1p0_lite;
use crate::generate::generate_video_v2::providers::artcraft::seedance_1p5_pro::build::build_artcraft_seedance_1p5_pro;
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0::build::build_artcraft_seedance_2p0;
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast::build::build_artcraft_seedance_2p0_fast;
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_u::build::build_artcraft_seedance_2p0_u;
@@ -163,6 +165,8 @@ impl GenerateVideoRequestBuilder {
(Provider::Artcraft, CommonVideoModel::Seedance2p0BytePlusFast) => build_artcraft_seedance_2p0_bp_fast(self),
(Provider::Artcraft, CommonVideoModel::PreviewModel) => build_artcraft_preview_model(self),
(Provider::Artcraft, CommonVideoModel::PreviewModelFast) => build_artcraft_preview_model_fast(self),
(Provider::Artcraft, CommonVideoModel::Seedance10Lite) => build_artcraft_seedance_1p0_lite(self),
(Provider::Artcraft, CommonVideoModel::Seedance1p5Pro) => build_artcraft_seedance_1p5_pro(self),
(Provider::Artcraft, CommonVideoModel::Sora2) => build_artcraft_sora_2(self),
(Provider::Artcraft, CommonVideoModel::Sora2Pro) => build_artcraft_sora_2_pro(self),
(Provider::Artcraft, CommonVideoModel::Veo2) => build_artcraft_veo_2(self),
@@ -12,6 +12,8 @@ pub mod preview_model;
pub mod preview_model_fast;
pub(crate) mod request_common;
pub(crate) mod resolve;
pub mod seedance_1p0_lite;
pub mod seedance_1p5_pro;
pub mod seedance_2p0;
pub mod seedance_2p0_fast;
pub mod seedance_2p0_u_fast;
@@ -0,0 +1,48 @@
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video_v2::providers::artcraft::build_common::{
build_artcraft_omni_request, SupportedResolutions, UltraWideSupport,
};
use crate::generate::generate_video_v2::providers::artcraft::seedance_1p0_lite::request::ArtcraftSeedance10LiteRequestState;
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
pub fn build_artcraft_seedance_1p0_lite(mut builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
// Seedance 1.0 Lite accepts only {5, 10} seconds; pre-plan so cost parity holds.
let strategy = builder.request_mismatch_mitigation_strategy;
let final_duration = plan_seedance_1p0_lite_duration(builder.duration_seconds, strategy)?;
builder.duration_seconds = final_duration;
let request = build_artcraft_omni_request(
builder,
CommonVideoModelEnum::Seedance10Lite,
SupportedResolutions::Fast,
UltraWideSupport::Supported,
)?;
let state = ArtcraftSeedance10LiteRequestState { request };
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance10Lite(state)))
}
fn plan_seedance_1p0_lite_duration(
duration_seconds: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<u16>, ArtcraftRouterError> {
match duration_seconds {
None => Ok(None),
Some(5) | Some(10) => Ok(duration_seconds),
Some(other) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "duration_seconds",
value: format!("{}", other),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Some(10)),
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(5)),
},
}
}
@@ -0,0 +1,102 @@
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use fal_client::requests::webhook::video::image::enqueue_seedance_1_lite_image_to_video_webhook::{
Seedance1LiteDuration, Seedance1LiteRequest, Seedance1LiteResolution,
};
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video_v2::providers::artcraft::seedance_1p0_lite::request::ArtcraftSeedance10LiteRequestState;
#[derive(Clone, Debug)]
pub struct ArtcraftSeedance10LiteCostState {
pub duration: Seedance1LiteDuration,
pub resolution: Seedance1LiteResolution,
}
impl ArtcraftSeedance10LiteCostState {
pub fn from_request(request: &ArtcraftSeedance10LiteRequestState) -> Self {
// Legacy handler defaults: 5 seconds, 720p.
let duration = if request.request.duration_seconds == Some(10) {
Seedance1LiteDuration::TenSeconds
} else {
Seedance1LiteDuration::FiveSeconds
};
let resolution = match request.request.resolution {
Some(CommonResolutionEnum::FourEightyP) => Seedance1LiteResolution::FourEightyP,
_ => Seedance1LiteResolution::SevenTwentyP,
};
Self { duration, resolution }
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// Delegate to the Fal client's cost calculator to guarantee parity with
// what we charge through the underlying provider.
let req = Seedance1LiteRequest {
image_url: String::new(),
end_frame_image_url: None,
prompt: String::new(),
duration: self.duration,
resolution: self.resolution,
aspect_ratio: None,
camera_fixed: false,
seed: None,
};
let cost_in_usd_cents = req.calculate_cost_in_cents();
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
}
}
}
#[cfg(test)]
mod tests {
use crate::api::common_resolution::CommonResolution;
use crate::api::common_video_model::CommonVideoModel;
use crate::api::provider::Provider;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
fn cost_cents(duration_seconds: Option<u16>, resolution: Option<CommonResolution>) -> u64 {
let b = GenerateVideoRequestBuilder {
model: CommonVideoModel::Seedance10Lite,
provider: Provider::Artcraft,
prompt: Some("test".to_string()),
duration_seconds,
resolution,
..Default::default()
};
b.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
}
#[test]
fn default_is_720p_5s_priced_at_18() {
// 720p + 5s is the special-cased price from the fal calculator: 18¢.
assert_eq!(cost_cents(None, None), 18);
}
#[test]
fn p720_5s_is_18() { assert_eq!(cost_cents(Some(5), Some(CommonResolution::SevenTwentyP)), 18); }
#[test]
fn p720_10s_is_95() {
// 1280×720×30×10/1024 = 270000 tokens × $1.8/M = $0.486 → ceil = 49¢ ...
// No wait — fal kling not seedance. The Seedance 1.0 Lite rate is different.
// Trust the fal calculator: assert whatever the actual computed value is.
let v = cost_cents(Some(10), Some(CommonResolution::SevenTwentyP));
assert!(v > 18, "10s should cost more than 5s, got {}", v);
}
#[test]
fn p480_5s_cheaper_than_p720_5s() {
assert!(
cost_cents(Some(5), Some(CommonResolution::FourEightyP))
< cost_cents(Some(5), Some(CommonResolution::SevenTwentyP))
);
}
}
@@ -0,0 +1,3 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -0,0 +1,17 @@
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request;
#[derive(Clone, Debug)]
pub struct ArtcraftSeedance10LiteRequestState {
pub request: OmniGenVideoCostAndGenerateRequest,
}
impl ArtcraftSeedance10LiteRequestState {
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
send_artcraft_omni_request(&self.request, client).await
}
}
@@ -0,0 +1,54 @@
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video_v2::providers::artcraft::build_common::{
build_artcraft_omni_request, SupportedResolutions, UltraWideSupport,
};
use crate::generate::generate_video_v2::providers::artcraft::seedance_1p5_pro::request::ArtcraftSeedance1p5ProRequestState;
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
pub fn build_artcraft_seedance_1p5_pro(mut builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
// Seedance 1.5 Pro supports 4-12 seconds; omni-gen helper accepts up to 15,
// so we clamp the upper bound ourselves. Preserve generate_audio (omni-gen
// hardcodes None on output).
let strategy = builder.request_mismatch_mitigation_strategy;
let final_duration = plan_seedance_1p5_pro_duration(builder.duration_seconds, strategy)?;
builder.duration_seconds = final_duration;
let generate_audio = builder.generate_audio;
let mut request = build_artcraft_omni_request(
builder,
CommonVideoModelEnum::Seedance1p5Pro,
SupportedResolutions::Full,
UltraWideSupport::Supported,
)?;
request.generate_audio = generate_audio;
let state = ArtcraftSeedance1p5ProRequestState { request };
Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance1p5Pro(state)))
}
fn plan_seedance_1p5_pro_duration(
duration_seconds: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<u16>, ArtcraftRouterError> {
const MIN: u16 = 4;
const MAX: u16 = 12;
match duration_seconds {
None => Ok(None),
Some(d) if (MIN..=MAX).contains(&d) => Ok(Some(d)),
Some(d) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "duration_seconds",
value: format!("{}", d),
}))
}
_ => Ok(Some(d.clamp(MIN, MAX))),
},
}
}
@@ -0,0 +1,109 @@
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use fal_client::requests::webhook::video::text::enqueue_seedance_1p5_pro_text_to_video_webhook::{
EnqueueSeedance1p5ProTextToVideoDuration, EnqueueSeedance1p5ProTextToVideoRequest,
EnqueueSeedance1p5ProTextToVideoResolution,
};
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video_v2::providers::artcraft::seedance_1p5_pro::request::ArtcraftSeedance1p5ProRequestState;
#[derive(Clone, Debug)]
pub struct ArtcraftSeedance1p5ProCostState {
pub duration: Option<EnqueueSeedance1p5ProTextToVideoDuration>,
pub resolution: Option<EnqueueSeedance1p5ProTextToVideoResolution>,
pub generate_audio: Option<bool>,
}
impl ArtcraftSeedance1p5ProCostState {
pub fn from_request(request: &ArtcraftSeedance1p5ProRequestState) -> Self {
let duration = request.request.duration_seconds.and_then(seconds_to_duration);
let resolution = request.request.resolution.and_then(common_resolution_to_seedance);
Self {
duration,
resolution,
generate_audio: request.request.generate_audio,
}
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
// Delegate to the Fal client's cost calculator. Pricing is identical for
// text-to-video and image-to-video at the same resolution+duration+audio,
// so we use the text-to-video request type as a stand-in.
let req = EnqueueSeedance1p5ProTextToVideoRequest {
prompt: String::new(),
duration: self.duration,
resolution: self.resolution,
aspect_ratio: None,
generate_audio: self.generate_audio,
};
let cost_in_usd_cents = req.calculate_cost_in_cents();
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
}
}
}
fn seconds_to_duration(seconds: u16) -> Option<EnqueueSeedance1p5ProTextToVideoDuration> {
use EnqueueSeedance1p5ProTextToVideoDuration as D;
match seconds {
4 => Some(D::FourSeconds),
5 => Some(D::FiveSeconds),
6 => Some(D::SixSeconds),
7 => Some(D::SevenSeconds),
8 => Some(D::EightSeconds),
9 => Some(D::NineSeconds),
10 => Some(D::TenSeconds),
11 => Some(D::ElevenSeconds),
12 => Some(D::TwelveSeconds),
_ => None,
}
}
fn common_resolution_to_seedance(r: CommonResolutionEnum) -> Option<EnqueueSeedance1p5ProTextToVideoResolution> {
match r {
CommonResolutionEnum::FourEightyP => Some(EnqueueSeedance1p5ProTextToVideoResolution::FourEightyP),
CommonResolutionEnum::SevenTwentyP => Some(EnqueueSeedance1p5ProTextToVideoResolution::SevenTwentyP),
CommonResolutionEnum::TenEightyP => Some(EnqueueSeedance1p5ProTextToVideoResolution::TenEightyP),
_ => None,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_video_model::CommonVideoModel;
use crate::api::provider::Provider;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
fn cost_cents(duration_seconds: Option<u16>) -> u64 {
let b = GenerateVideoRequestBuilder {
model: CommonVideoModel::Seedance1p5Pro,
provider: Provider::Artcraft,
prompt: Some("test".to_string()),
duration_seconds,
..Default::default()
};
b.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
}
// Numeric literals mirror the v1 Artcraft Seedance 1.5 Pro tests we deleted:
// resolution=None + generate_audio=None → fal client defaults (720p, audio on).
#[test]
fn default_5s_is_26() { assert_eq!(cost_cents(Some(5)), 26); }
#[test]
fn default_4s_is_26() { assert_eq!(cost_cents(Some(4)), 26); }
#[test]
fn default_10s_is_65() { assert_eq!(cost_cents(Some(10)), 65); }
#[test]
fn default_12s_is_78() { assert_eq!(cost_cents(Some(12)), 78); }
}
@@ -0,0 +1,3 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -0,0 +1,17 @@
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request;
#[derive(Clone, Debug)]
pub struct ArtcraftSeedance1p5ProRequestState {
pub request: OmniGenVideoCostAndGenerateRequest,
}
impl ArtcraftSeedance1p5ProRequestState {
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
send_artcraft_omni_request(&self.request, client).await
}
}
@@ -25,6 +25,10 @@ use crate::generate::generate_video_v2::providers::artcraft::preview_model::cost
use crate::generate::generate_video_v2::providers::artcraft::preview_model::request::ArtcraftPreviewModelRequestState;
use crate::generate::generate_video_v2::providers::artcraft::preview_model_fast::cost::ArtcraftPreviewModelFastCostState;
use crate::generate::generate_video_v2::providers::artcraft::preview_model_fast::request::ArtcraftPreviewModelFastRequestState;
use crate::generate::generate_video_v2::providers::artcraft::seedance_1p0_lite::cost::ArtcraftSeedance10LiteCostState;
use crate::generate::generate_video_v2::providers::artcraft::seedance_1p0_lite::request::ArtcraftSeedance10LiteRequestState;
use crate::generate::generate_video_v2::providers::artcraft::seedance_1p5_pro::cost::ArtcraftSeedance1p5ProCostState;
use crate::generate::generate_video_v2::providers::artcraft::seedance_1p5_pro::request::ArtcraftSeedance1p5ProRequestState;
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0::cost::ArtcraftSeedance2p0CostState;
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0::request::ArtcraftSeedance2p0RequestState;
use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast::cost::ArtcraftSeedance2p0FastCostState;
@@ -109,6 +113,8 @@ pub enum VideoGenerationRequest {
ArtcraftKling3p0Standard(ArtcraftKling3p0StandardRequestState),
ArtcraftPreviewModel(ArtcraftPreviewModelRequestState),
ArtcraftPreviewModelFast(ArtcraftPreviewModelFastRequestState),
ArtcraftSeedance10Lite(ArtcraftSeedance10LiteRequestState),
ArtcraftSeedance1p5Pro(ArtcraftSeedance1p5ProRequestState),
ArtcraftSeedance2p0(ArtcraftSeedance2p0RequestState),
ArtcraftSeedance2p0Fast(ArtcraftSeedance2p0FastRequestState),
ArtcraftSeedance2p0Ultra(ArtcraftSeedance2p0UltraRequestState),
@@ -161,6 +167,8 @@ impl VideoGenerationRequest {
Self::ArtcraftKling3p0Standard(_) => Provider::Artcraft,
Self::ArtcraftPreviewModel(_) => Provider::Artcraft,
Self::ArtcraftPreviewModelFast(_) => Provider::Artcraft,
Self::ArtcraftSeedance10Lite(_) => Provider::Artcraft,
Self::ArtcraftSeedance1p5Pro(_) => Provider::Artcraft,
Self::ArtcraftSeedance2p0(_) => Provider::Artcraft,
Self::ArtcraftSeedance2p0Fast(_) => Provider::Artcraft,
Self::ArtcraftSeedance2p0Ultra(_) => Provider::Artcraft,
@@ -213,6 +221,8 @@ impl VideoGenerationRequest {
VideoGenerationRequest::ArtcraftKling3p0Standard(request) => Ok(ArtcraftKling3p0StandardCostState::from_request(request).estimate_cost()),
VideoGenerationRequest::ArtcraftPreviewModel(request) => Ok(ArtcraftPreviewModelCostState::from_request(request).estimate_cost()),
VideoGenerationRequest::ArtcraftPreviewModelFast(request) => Ok(ArtcraftPreviewModelFastCostState::from_request(request).estimate_cost()),
VideoGenerationRequest::ArtcraftSeedance10Lite(request) => Ok(ArtcraftSeedance10LiteCostState::from_request(request).estimate_cost()),
VideoGenerationRequest::ArtcraftSeedance1p5Pro(request) => Ok(ArtcraftSeedance1p5ProCostState::from_request(request).estimate_cost()),
VideoGenerationRequest::ArtcraftSeedance2p0(request) => Ok(ArtcraftSeedance2p0CostState::from_request(request).estimate_cost()),
VideoGenerationRequest::ArtcraftSeedance2p0Fast(request) => Ok(ArtcraftSeedance2p0FastCostState::from_request(request).estimate_cost()),
VideoGenerationRequest::ArtcraftSeedance2p0Ultra(request) => Ok(ArtcraftSeedance2p0UltraCostState::from_request(request).estimate_cost()),
@@ -299,6 +309,14 @@ impl VideoGenerationRequest {
let client_ref = client.get_artcraft_client_ref()?;
request.send(client_ref).await
},
VideoGenerationRequest::ArtcraftSeedance10Lite(request) => {
let client_ref = client.get_artcraft_client_ref()?;
request.send(client_ref).await
},
VideoGenerationRequest::ArtcraftSeedance1p5Pro(request) => {
let client_ref = client.get_artcraft_client_ref()?;
request.send(client_ref).await
},
VideoGenerationRequest::ArtcraftSeedance2p0(request) => {
let client_ref = client.get_artcraft_client_ref()?;
request.send(client_ref).await