This commit is contained in:
Brandon Thomas committed 2026-02-23 16:23:01 +07:00
1 parent 22d0631280
commit 2833c2a05b
2 files changed
+88 -71

No files matched your search

@@ -22,6 +22,12 @@ pub enum ClientError {
/// The model does not support the given option value.
/// `field` is the request field name, `value` is what was requested.
ModelDoesNotSupportOption { field: &'static str, value: String },
/// The caller requested zero generations, which is never valid.
UserRequestedZeroGenerations,
/// ArtCraft only accepts media tokens for image inputs, not raw URLs.
ArtcraftOnlySupportsMediaTokens,
}
impl Error for ClientError {}
@@ -35,6 +41,12 @@ impl Display for ClientError {
Self::ModelDoesNotSupportOption { field, value } => {
write!(f, "Model does not support '{}' for field '{}'", value, field)
}
Self::UserRequestedZeroGenerations => {
write!(f, "Cannot request zero generations")
}
Self::ArtcraftOnlySupportsMediaTokens => {
write!(f, "ArtCraft only supports media tokens for image inputs; upload the image first to obtain a media token")
}
}
}
}
@@ -8,21 +8,73 @@ use crate::generate::generate_video::generate_video_request::GenerateVideoReques
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{
Seedance2p0AspectRatio, Seedance2p0BatchCount,
};
use tokens::tokens::media_files::MediaFileToken;
pub struct PlanArtcraftSeedance2p0<'a> {
pub prompt: Option<&'a str>,
pub start_frame: Option<ImageRef<'a>>,
pub end_frame: Option<ImageRef<'a>>,
pub reference_images: Option<ImageListRef<'a>>,
pub start_frame: Option<&'a MediaFileToken>,
pub end_frame: Option<&'a MediaFileToken>,
pub reference_images: Option<&'a Vec<MediaFileToken>>,
pub aspect_ratio: Option<Seedance2p0AspectRatio>,
pub duration_seconds: Option<u8>,
pub batch_count: Seedance2p0BatchCount,
pub idempotency_token: String,
}
// Aspect ratio values (width / height) for ordering:
// Portrait9x16 = 0.5625, Portrait3x4 = 0.75, Square1x1 = 1.0, Standard4x3 = 1.33, Landscape16x9 = 1.78
pub fn plan_generate_video_artcraft_seedance2p0<'a>(
request: &'a GenerateVideoRequest<'a>,
) -> Result<PlanArtcraftSeedance2p0<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let start_frame = resolve_image_ref(request.start_frame)?;
let end_frame = resolve_image_ref(request.end_frame)?;
let reference_images = resolve_image_list_ref(request.reference_images)?;
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
let batch_count = plan_batch_count(request.video_batch_count, strategy)?;
let duration_seconds = plan_duration(request.duration_seconds, strategy)?;
Ok(PlanArtcraftSeedance2p0 {
prompt: request.prompt,
start_frame,
end_frame,
reference_images,
aspect_ratio,
duration_seconds,
batch_count,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}
fn resolve_image_ref<'a>(
image_ref: Option<ImageRef<'a>>,
) -> Result<Option<&'a MediaFileToken>, ArtcraftRouterError> {
match image_ref {
None => Ok(None),
Some(ImageRef::MediaFileToken(t)) => Ok(Some(t)),
Some(ImageRef::Url(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
}
}
fn resolve_image_list_ref<'a>(
image_list_ref: Option<ImageListRef<'a>>,
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
match image_list_ref {
None => Ok(None),
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
}
}
// 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,
@@ -55,52 +107,36 @@ fn plan_aspect_ratio(
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
Ok(Some(aspect_ratio_upgrade(unsupported)))
}
RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(Some(aspect_ratio_downgrade(unsupported)))
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(Some(nearest_aspect_ratio(unsupported)))
}
},
}
}
/// Round an unsupported aspect ratio up toward a higher AR (wider / less portrait).
fn aspect_ratio_upgrade(aspect_ratio: CommonAspectRatio) -> Seedance2p0AspectRatio {
/// Pick the nearest supported aspect ratio by AR value (width / height).
/// All Seedance2p0 aspect ratios cost the same, so this is used for both upgrade and downgrade.
fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> Seedance2p0AspectRatio {
match aspect_ratio {
// Wide mismatches: round up to the next wider supported ratio
CommonAspectRatio::WideFiveByFour => Seedance2p0AspectRatio::Standard4x3, // 1.25 → 1.33
CommonAspectRatio::WideThreeByTwo => Seedance2p0AspectRatio::Landscape16x9, // 1.50 → 1.78
CommonAspectRatio::WideTwentyOneByNine => Seedance2p0AspectRatio::Landscape16x9, // 2.33 → 1.78 (max)
// Tall mismatches: round up toward Square (higher AR = less portrait)
CommonAspectRatio::TallFourByFive => Seedance2p0AspectRatio::Square1x1, // 0.80 → 1.00
CommonAspectRatio::TallTwoByThree => Seedance2p0AspectRatio::Portrait3x4, // 0.67 → 0.75
CommonAspectRatio::TallNineByTwentyOne => Seedance2p0AspectRatio::Portrait9x16, // 0.43 → 0.56 (max portrait)
_ => Seedance2p0AspectRatio::Square1x1,
}
}
/// Round an unsupported aspect ratio down toward a lower AR (narrower / more portrait).
fn aspect_ratio_downgrade(aspect_ratio: CommonAspectRatio) -> Seedance2p0AspectRatio {
match aspect_ratio {
// Wide mismatches: round down to the next narrower supported ratio
CommonAspectRatio::WideFiveByFour => Seedance2p0AspectRatio::Square1x1, // 1.25 → 1.00
CommonAspectRatio::WideThreeByTwo => Seedance2p0AspectRatio::Standard4x3, // 1.50 → 1.33
CommonAspectRatio::WideTwentyOneByNine => Seedance2p0AspectRatio::Landscape16x9, // 2.33 → 1.78 (only option)
// Tall mismatches: round down toward most portrait (lower AR)
CommonAspectRatio::TallFourByFive => Seedance2p0AspectRatio::Portrait3x4, // 0.80 → 0.75
CommonAspectRatio::TallTwoByThree => Seedance2p0AspectRatio::Portrait9x16, // 0.67 → 0.56
CommonAspectRatio::TallNineByTwentyOne => Seedance2p0AspectRatio::Portrait9x16, // 0.43 → 0.56 (only option)
CommonAspectRatio::WideFiveByFour => Seedance2p0AspectRatio::Standard4x3, // 1.25, nearest 1.33
CommonAspectRatio::WideThreeByTwo => Seedance2p0AspectRatio::Standard4x3, // 1.50, nearest 1.33
CommonAspectRatio::WideTwentyOneByNine => Seedance2p0AspectRatio::Landscape16x9, // 2.33, nearest 1.78
CommonAspectRatio::TallFourByFive => Seedance2p0AspectRatio::Portrait3x4, // 0.80, nearest 0.75
CommonAspectRatio::TallTwoByThree => Seedance2p0AspectRatio::Portrait3x4, // 0.67, nearest 0.75
CommonAspectRatio::TallNineByTwentyOne => Seedance2p0AspectRatio::Portrait9x16, // 0.43, nearest 0.56
_ => Seedance2p0AspectRatio::Square1x1,
}
}
// Seedance2p0 supports batch counts of 1, 2, and 4 only.
fn plan_batch_count(
video_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Seedance2p0BatchCount, ArtcraftRouterError> {
let count = video_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(Seedance2p0BatchCount::One),
2 => Ok(Seedance2p0BatchCount::Two),
4 => Ok(Seedance2p0BatchCount::Four),
@@ -112,29 +148,18 @@ fn plan_batch_count(
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade => {
// Round up to the next supported count; clamp at 4
if count == 0 {
Ok(Seedance2p0BatchCount::One)
} else if count < 4 {
Ok(Seedance2p0BatchCount::Four)
} else {
Ok(Seedance2p0BatchCount::Four)
}
// Round up: 3 → Four; anything > 4 clamps to Four
Ok(if count < 4 { Seedance2p0BatchCount::Four } else { Seedance2p0BatchCount::Four })
}
RequestMismatchMitigationStrategy::PayLessDowngrade => {
// Round down to the previous supported count; clamp at 4 for overflows
if count == 0 {
Ok(Seedance2p0BatchCount::One)
} else if count < 4 {
Ok(Seedance2p0BatchCount::Two)
} else {
Ok(Seedance2p0BatchCount::Four)
}
// Round down: 3 → Two; anything > 4 clamps to Four (max available)
Ok(if count < 4 { Seedance2p0BatchCount::Two } else { Seedance2p0BatchCount::Four })
}
},
}
}
// Seedance2p0 supports duration of 4–15 seconds.
fn plan_duration(
duration_seconds: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
@@ -155,23 +180,3 @@ fn plan_duration(
},
}
}
pub fn plan_generate_video_artcraft_seedance2p0<'a>(
request: &'a GenerateVideoRequest<'a>,
) -> Result<PlanArtcraftSeedance2p0<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
let batch_count = plan_batch_count(request.video_batch_count, strategy)?;
let duration_seconds = plan_duration(request.duration_seconds, strategy)?;
Ok(PlanArtcraftSeedance2p0 {
prompt: request.prompt,
start_frame: request.start_frame,
end_frame: request.end_frame,
reference_images: request.reference_images,
aspect_ratio,
duration_seconds,
batch_count,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}