use crate::{
GenerateRequest, KeyframeCondition, LoraWeight, Ltx2GuidanceOverrides, Ltx2PipelineMode,
Ltx2SpatialUpscale, OutputFormat, UpscaleRequest,
};
pub const MAX_PIXELS: u64 = 1_800_000;
pub const LTX2_MAX_PIXELS: u64 = 1_920 * 1_088;
pub const LTX2_MAX_AXIS_PIXELS: u32 = 2_048;
pub const LTX2_COMPOSED_MAX_AXIS_PIXELS: u32 = 2 * LTX2_MAX_AXIS_PIXELS;
pub const LTX2_COMPOSED_MAX_PIXELS: u64 = 4_096 * 2_176;
pub const MAX_INLINE_AUDIO_BYTES: usize = 64 * 1024 * 1024;
pub const MAX_INLINE_SOURCE_VIDEO_BYTES: usize = 64 * 1024 * 1024;
pub const FLUX2_DEV_MAX_REFERENCE_IMAGES: usize = 4;
pub const FLUX2_DEV_SINGLE_REFERENCE_MAX_PIXELS: u64 = 2_024 * 2_024;
pub const FLUX2_DEV_MULTI_REFERENCE_MAX_PIXELS: u64 = 1_024 * 1_024;
pub const LORA_CAPABLE_FAMILIES: &[&str] = &[
"flux",
"flux2",
"ltx2",
"sd15",
"sd3",
"sdxl",
"qwen-image",
"qwen-image-edit",
"wan",
"z-image",
];
pub fn family_supports_lora(family: &str) -> bool {
LORA_CAPABLE_FAMILIES.contains(&family)
}
pub const LTX2_MAX_RUNTIME_SECONDS: u32 = 20;
pub const LTX2_DEFAULT_FPS: u32 = 24;
pub const LTX2_MAX_FRAMES_ABSOLUTE: u32 = LTX2_MAX_RUNTIME_SECONDS * 30 + 4;
pub const MAX_FRAMES_GLOBAL: u32 = 257;
pub const DEFAULT_EXTEND_OVERLAP_FRAMES: u32 = 17;
pub fn default_extend_overlap_frames_for_family(family: Option<&str>) -> u32 {
match family {
Some("wan") => WAN_HANDOFF_DUPLICATED_FRAMES,
_ => DEFAULT_EXTEND_OVERLAP_FRAMES,
}
}
pub fn materialize_extend_overlap_frames(req: &mut GenerateRequest, family: Option<&str>) {
if req.is_extend() && req.extend_overlap_frames.is_none() {
req.extend_overlap_frames = Some(default_extend_overlap_frames_for_family(family));
}
}
pub fn chain_motion_tail_frames_for_family(
family: &str,
source_image: Option<crate::SourceImageCapability>,
requested: u32,
) -> u32 {
match family {
"wan" => {
let carries_context = source_image.is_some_and(|capability| {
matches!(
capability,
crate::SourceImageCapability::Required | crate::SourceImageCapability::Optional
)
});
if carries_context {
WAN_HANDOFF_DUPLICATED_FRAMES
} else {
0
}
}
"ltx-video" => 0,
_ => requested,
}
}
pub const MAX_INLINE_EXTEND_VIDEO_BYTES: usize = MAX_INLINE_SOURCE_VIDEO_BYTES;
pub const MAX_STG_BLOCK_INDEX: u32 = 64;
pub const MAX_STG_BLOCKS: usize = 8;
pub fn ltx2_max_frames_at_fps(fps: u32) -> u32 {
LTX2_MAX_RUNTIME_SECONDS
.saturating_mul(fps.max(1))
.saturating_add(4)
.min(LTX2_MAX_FRAMES_ABSOLUTE)
}
pub fn ltx2_max_frames_on_grid_at_fps(fps: u32) -> u32 {
snap_frames_to_8k1(ltx2_max_frames_at_fps(fps))
}
pub const LTX2_TWO_STAGE_ALIGNMENT: u32 = 64;
pub const LTX2_TEMPORAL_SCALE: u32 = 8;
pub fn snap_frames_to_8k1(frames: u32) -> u32 {
if frames <= 1 {
return 1;
}
frames - ((frames - 1) % LTX2_TEMPORAL_SCALE)
}
pub const WAN_TEMPORAL_SCALE: u32 = 4;
pub const WAN_HANDOFF_DUPLICATED_FRAMES: u32 = 1;
pub const WAN_TI2V_FLF_MIN_FRAMES: u32 = 9;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LipDubTiming {
pub frames: u32,
pub fps: u32,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LipDubReference {
pub frames: u32,
pub fps: u32,
pub has_audio: bool,
}
pub fn resolve_lip_dub_timing(
reference: LipDubReference,
requested_frames: Option<u32>,
requested_fps: Option<u32>,
) -> Result<LipDubTiming, String> {
let LipDubReference {
frames: reference_frames,
fps: reference_fps,
has_audio,
} = reference;
if reference_fps == 0 {
return Err("lip-dub reference video reports a frame rate of 0".to_string());
}
if !has_audio {
return Err(
"lip-dub reference video has no audio track; the pipeline re-voices existing \
speech, so the reference must contain some"
.to_string(),
);
}
let frames = snap_frames_to_8k1(reference_frames);
if frames < 9 {
return Err(format!(
"lip-dub reference video is too short: {reference_frames} frames snap down to \
{frames}, and the pipeline needs at least 9"
));
}
let mut warnings = Vec::new();
if requested_frames.is_some_and(|requested| requested != frames) {
warnings.push(format!(
"lip-dub takes its length from the reference video: rendering {frames} frames \
instead of the requested {}",
requested_frames.unwrap_or_default()
));
} else if requested_frames.is_none() && frames != reference_frames {
warnings.push(format!(
"lip-dub snapped the reference video's {reference_frames} frames down to {frames} \
(LTX-2 renders 8k+1 frames)"
));
}
if requested_fps.is_some_and(|requested| requested != reference_fps) {
warnings.push(format!(
"lip-dub takes its frame rate from the reference video: rendering at \
{reference_fps} fps instead of the requested {}",
requested_fps.unwrap_or_default()
));
}
Ok(LipDubTiming {
frames,
fps: reference_fps,
warnings,
})
}
pub fn max_frames_for_family_at_fps(family: &str, fps: u32) -> Option<u32> {
match family {
"ltx2" => Some(ltx2_max_frames_on_grid_at_fps(fps)),
"ltx-video" => Some(MAX_FRAMES_GLOBAL),
"wan" => Some(MAX_FRAMES_GLOBAL),
family if crate::minimax_h3::is_family(family) => Some(crate::minimax_h3::MAX_FRAMES),
_ => None,
}
}
pub fn max_frames_for_family(family: &str) -> Option<u32> {
max_frames_for_family_at_fps(family, LTX2_DEFAULT_FPS)
}
pub fn min_frames_for_family(family: &str) -> Option<u32> {
crate::minimax_h3::is_family(family).then_some(crate::minimax_h3::MIN_FRAMES)
}
pub fn fixed_fps_for_family(family: &str) -> Option<u32> {
crate::minimax_h3::is_family(family).then_some(crate::minimax_h3::FIXED_FPS)
}
pub fn max_runtime_seconds_for_family(family: &str) -> Option<u32> {
match family {
"ltx2" => Some(LTX2_MAX_RUNTIME_SECONDS),
family if crate::minimax_h3::is_family(family) => {
Some(crate::minimax_h3::MAX_DURATION_SECONDS)
}
_ => None,
}
}
pub fn max_frames_absolute_for_family(family: &str) -> Option<u32> {
match family {
"ltx2" => Some(LTX2_MAX_FRAMES_ABSOLUTE),
family if crate::minimax_h3::is_family(family) => Some(crate::minimax_h3::MAX_FRAMES),
_ => None,
}
}
pub fn frame_step_for_family(family: &str) -> Option<u32> {
match family {
"ltx2" | "ltx-video" => Some(LTX2_TEMPORAL_SCALE),
"wan" => Some(WAN_TEMPORAL_SCALE),
family if crate::minimax_h3::is_family(family) => Some(crate::minimax_h3::FRAME_STEP),
_ => None,
}
}
pub fn frame_offset_for_family(family: &str) -> Option<u32> {
frame_step_for_family(family).map(|_| {
if crate::minimax_h3::is_family(family) {
crate::minimax_h3::FRAME_OFFSET
} else {
1
}
})
}
fn validate_family_video_timing_constraints(
frames: Option<u32>,
fps: Option<u32>,
family: Option<&str>,
) -> Result<(), String> {
if let (Some(family), Some(fps)) = (family, fps) {
if let Some(fixed_fps) = fixed_fps_for_family(family) {
if fps != fixed_fps {
return Err(format!("{family} requires {fixed_fps} fps; received {fps}"));
}
}
}
if let (Some(family), Some(frames)) = (family, frames) {
if let Some(min_frames) = min_frames_for_family(family) {
if frames < min_frames {
return Err(format!(
"frames ({frames}) must be >= {min_frames} for {family}"
));
}
}
}
Ok(())
}
fn megapixel_limit_label_for(limit: u64) -> String {
format!("{:.1}MP", limit as f64 / 1_000_000.0)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Ltx2SpatialComposition {
#[default]
SinglePass,
TiledTwoStage,
}
fn model_has_spatial_upsampler(model: &str) -> bool {
let canonical = crate::manifest::resolve_model_name(model);
crate::manifest::find_manifest(&canonical).is_some_and(|manifest| {
manifest
.files
.iter()
.any(|file| file.component == crate::manifest::ModelComponent::SpatialUpscaler)
})
}
pub fn ltx2_spatial_composition(
model: &str,
pipeline: Option<Ltx2PipelineMode>,
) -> Ltx2SpatialComposition {
if !model_has_spatial_upsampler(model) {
return Ltx2SpatialComposition::SinglePass;
}
let refines = match pipeline {
Some(mode) => mode.refines_spatially(),
None => true,
};
if refines {
Ltx2SpatialComposition::TiledTwoStage
} else {
Ltx2SpatialComposition::SinglePass
}
}
fn ltx2_implicit_pipeline(req: &GenerateRequest) -> Option<Ltx2PipelineMode> {
if req.retake_range.is_some() {
return Some(Ltx2PipelineMode::Retake);
}
if req.audio_file.is_some() || req.audio_file_path.is_some() {
return Some(Ltx2PipelineMode::A2Vid);
}
if req.keyframes.as_ref().is_some_and(|items| items.len() > 1) {
return Some(Ltx2PipelineMode::Keyframe);
}
if req.source_video.is_some() || req.source_video_path.is_some() {
return Some(Ltx2PipelineMode::IcLora);
}
None
}
pub fn ltx2_spatial_composition_for_request(req: &GenerateRequest) -> Ltx2SpatialComposition {
ltx2_spatial_composition(
&req.model,
req.pipeline.or_else(|| ltx2_implicit_pipeline(req)),
)
}
pub fn max_pixels_for_family(family: Option<&str>) -> u64 {
max_pixels_for_family_composed(family, Ltx2SpatialComposition::SinglePass)
}
pub fn max_pixels_for_family_composed(
family: Option<&str>,
composition: Ltx2SpatialComposition,
) -> u64 {
match (family, composition) {
(Some("ltx2"), Ltx2SpatialComposition::TiledTwoStage) => LTX2_COMPOSED_MAX_PIXELS,
(Some("ltx2"), Ltx2SpatialComposition::SinglePass) => LTX2_MAX_PIXELS,
(Some(family), _) if crate::minimax_h3::is_family(family) => crate::minimax_h3::MAX_PIXELS,
_ => MAX_PIXELS,
}
}
pub fn max_axis_pixels_for_family(family: Option<&str>) -> Option<u32> {
max_axis_pixels_for_family_composed(family, Ltx2SpatialComposition::SinglePass)
}
pub fn max_axis_pixels_for_family_composed(
family: Option<&str>,
composition: Ltx2SpatialComposition,
) -> Option<u32> {
match (family, composition) {
(Some("ltx2"), Ltx2SpatialComposition::TiledTwoStage) => {
Some(LTX2_COMPOSED_MAX_AXIS_PIXELS)
}
(Some("ltx2"), Ltx2SpatialComposition::SinglePass) => Some(LTX2_MAX_AXIS_PIXELS),
_ => None,
}
}
pub fn dimension_alignment_for_family(family: Option<&str>) -> u32 {
if matches!(family, Some("ltx-video" | "ltx2"))
|| family.is_some_and(crate::minimax_h3::is_family)
{
32
} else {
16
}
}
pub fn dimension_alignment_for_model(model: &str, family_hint: Option<&str>) -> u32 {
let family = resolved_family(model, family_hint);
if family == Some("wan") {
return wan_dimension_alignment(model);
}
dimension_alignment_for_family(family)
}
pub fn validate_generation_dimensions(
width: u32,
height: u32,
family: Option<&str>,
) -> Result<(), String> {
validate_generation_dimensions_composed(
width,
height,
family,
Ltx2SpatialComposition::SinglePass,
)
}
pub fn validate_generation_dimensions_composed(
width: u32,
height: u32,
family: Option<&str>,
composition: Ltx2SpatialComposition,
) -> Result<(), String> {
validate_generation_dimensions_with_alignment(
width,
height,
family,
composition,
dimension_alignment_for_family(family),
)
}
pub fn validate_generation_dimensions_for_model(
model: &str,
width: u32,
height: u32,
family: Option<&str>,
composition: Ltx2SpatialComposition,
) -> Result<(), String> {
validate_generation_dimensions_with_alignment(
width,
height,
family,
composition,
dimension_alignment_for_model(model, family),
)
}
fn validate_generation_dimensions_with_alignment(
width: u32,
height: u32,
family: Option<&str>,
composition: Ltx2SpatialComposition,
alignment: u32,
) -> Result<(), String> {
if width == 0 || height == 0 {
return Err("width and height must be > 0".to_string());
}
if !width.is_multiple_of(alignment) || !height.is_multiple_of(alignment) {
let family_label = family
.filter(|value| !value.is_empty())
.map(|value| format!(" for {value} models"))
.unwrap_or_default();
return Err(format!(
"width ({width}) and height ({height}) must be multiples of {alignment}{family_label}"
));
}
if let Some(axis_limit) = max_axis_pixels_for_family_composed(family, composition) {
let longest = width.max(height);
if longest > axis_limit {
let mut remedy = String::new();
if composition == Ltx2SpatialComposition::SinglePass
&& longest <= LTX2_COMPOSED_MAX_AXIS_PIXELS
{
remedy.push_str(
" This checkpoint renders in one pass; reaching that size needs a checkpoint \
that ships the spatial upsampler, which renders stage 1 at half size and \
refines it over tiles.",
);
}
if let Some(rung) = largest_ltx2_rung_within(axis_limit) {
remedy.push_str(&format!(
" The largest output this render reaches is {} ({}x{}).",
rung.label, rung.width, rung.height
));
}
return Err(format!(
"{width}x{height} has a {longest}px axis, beyond the {axis_limit}px span this \
render can hold — positions past it are out of distribution. Render at or below \
{axis_limit}px on the long edge.{remedy}"
));
}
}
let limit = max_pixels_for_family_composed(family, composition);
let pixels = width as u64 * height as u64;
if pixels > limit {
return Err(format!(
"{width}x{height} = {:.2} megapixels exceeds the {} limit (VAE VRAM constraint)",
pixels as f64 / 1_000_000.0,
megapixel_limit_label_for(limit)
));
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Ltx2OutputRung {
pub id: &'static str,
pub label: &'static str,
pub width: u32,
pub height: u32,
}
impl Ltx2OutputRung {
pub const fn stage1_shape(&self) -> (u32, u32) {
(
ltx2_stage1_axis_for(self.width, Some(Ltx2SpatialUpscale::X2)),
ltx2_stage1_axis_for(self.height, Some(Ltx2SpatialUpscale::X2)),
)
}
pub const fn requires_tiled_stage2(&self) -> bool {
self.width > LTX2_MAX_AXIS_PIXELS || self.height > LTX2_MAX_AXIS_PIXELS
}
pub const fn stage2_tiles(&self) -> (u32, u32) {
(
ltx2_axis_tile_count(self.width),
ltx2_axis_tile_count(self.height),
)
}
}
pub const fn ltx2_stage1_axis_for(target: u32, upscale: Option<Ltx2SpatialUpscale>) -> u32 {
let grid = LTX2_SPATIAL_LATENT_STRIDE;
let Some(upscale) = upscale else {
return if target < grid { grid } else { target };
};
let target_latent = if target < grid {
1
} else {
target.div_ceil(grid)
};
let stage1_latent = match upscale {
Ltx2SpatialUpscale::X2 => target_latent.div_ceil(2),
Ltx2SpatialUpscale::X1_5 => target_latent
.saturating_mul(2)
.saturating_sub(1)
.div_ceil(3),
};
if stage1_latent == 0 {
grid
} else {
stage1_latent * grid
}
}
pub fn ltx2_composed_axis_ceiling(upscale: Option<Ltx2SpatialUpscale>) -> u32 {
match upscale {
None | Some(Ltx2SpatialUpscale::X2) => LTX2_COMPOSED_MAX_AXIS_PIXELS,
Some(Ltx2SpatialUpscale::X1_5) => {
let mut ceiling = LTX2_MAX_AXIS_PIXELS;
while ceiling < LTX2_COMPOSED_MAX_AXIS_PIXELS
&& ltx2_stage1_axis_for(ceiling + LTX2_SPATIAL_LATENT_STRIDE, upscale)
<= LTX2_MAX_AXIS_PIXELS
{
ceiling += LTX2_SPATIAL_LATENT_STRIDE;
}
ceiling
}
}
}
pub fn validate_ltx2_stage1_span(
width: u32,
height: u32,
upscale: Option<Ltx2SpatialUpscale>,
) -> Result<(), String> {
let effective = upscale.unwrap_or(Ltx2SpatialUpscale::X2);
let stage1 = (
ltx2_stage1_axis_for(width, Some(effective)),
ltx2_stage1_axis_for(height, Some(effective)),
);
let longest = stage1.0.max(stage1.1);
if longest <= LTX2_MAX_AXIS_PIXELS {
return Ok(());
}
let rung = match effective {
Ltx2SpatialUpscale::X1_5 => "x1.5",
Ltx2SpatialUpscale::X2 => "x2",
};
let ceiling = ltx2_composed_axis_ceiling(upscale);
Err(format!(
"{width}x{height} with {rung} spatial upscale renders stage 1 at {}x{}, whose {longest}px \
axis is past the {}px span these checkpoints were trained on. The rung sets the ceiling: \
it reaches {ceiling}px on the long edge. Use a x2 upscale, or render at or below \
{ceiling}px.",
stage1.0, stage1.1, LTX2_MAX_AXIS_PIXELS,
))
}
const fn ltx2_axis_tile_count(target: u32) -> u32 {
if target <= LTX2_MAX_AXIS_PIXELS {
return 1;
}
let count = target.div_ceil(LTX2_MAX_AXIS_PIXELS);
if count < 2 {
2
} else {
count
}
}
pub const LTX2_SPATIAL_LATENT_STRIDE: u32 = 32;
pub const LTX2_OUTPUT_RUNGS: &[Ltx2OutputRung] = &[
Ltx2OutputRung {
id: "720p",
label: "720p HD",
width: 1_280,
height: 704,
},
Ltx2OutputRung {
id: "1080p",
label: "1080p Full HD",
width: 1_920,
height: 1_088,
},
Ltx2OutputRung {
id: "1440p",
label: "1440p QHD",
width: 2_560,
height: 1_408,
},
Ltx2OutputRung {
id: "4k-uhd",
label: "4K UHD",
width: 3_840,
height: 2_112,
},
];
pub fn ltx2_output_rung(width: u32, height: u32) -> Option<&'static Ltx2OutputRung> {
LTX2_OUTPUT_RUNGS.iter().find(|rung| {
(rung.width == width && rung.height == height)
|| (rung.width == height && rung.height == width)
})
}
pub fn largest_ltx2_rung_within(axis_limit: u32) -> Option<&'static Ltx2OutputRung> {
LTX2_OUTPUT_RUNGS
.iter()
.rfind(|rung| rung.width.max(rung.height) <= axis_limit)
}
fn mib_label(bytes: usize) -> String {
format!("{:.0} MiB", bytes as f64 / (1024.0 * 1024.0))
}
pub fn clamp_to_megapixel_limit(w: u32, h: u32) -> (u32, u32) {
clamp_to_family_pixel_limit(w, h, None)
}
pub fn clamp_to_family_pixel_limit(w: u32, h: u32, family: Option<&str>) -> (u32, u32) {
clamp_dims_to(
w,
h,
max_pixels_for_family(family),
dimension_alignment_for_family(family),
max_axis_pixels_for_family(family),
)
}
fn clamp_dims_to(w: u32, h: u32, limit: u64, align: u32, axis_limit: Option<u32>) -> (u32, u32) {
let pixels = w as u64 * h as u64;
let within_axis = axis_limit.is_none_or(|axis| w.max(h) <= axis);
if pixels <= limit && within_axis {
return (w, h);
}
let mut scale = if pixels > limit {
(limit as f64 / pixels as f64).sqrt()
} else {
1.0
};
if let Some(axis) = axis_limit {
let longest = w.max(h) as f64;
if longest * scale > axis as f64 {
scale = axis as f64 / longest;
}
}
let new_w = ((w as f64 * scale) as u32 / align) * align;
let new_h = ((h as f64 * scale) as u32 / align) * align;
(new_w.max(align), new_h.max(align))
}
pub fn fit_to_model_dimensions(src_w: u32, src_h: u32, model_w: u32, model_h: u32) -> (u32, u32) {
fit_to_model_dimensions_aligned(src_w, src_h, model_w, model_h, 16)
}
pub fn fit_to_model_dimensions_aligned(
src_w: u32,
src_h: u32,
model_w: u32,
model_h: u32,
align: u32,
) -> (u32, u32) {
let align = align.max(1);
let src_ratio = src_w as f64 / src_h as f64;
let model_ratio = model_w as f64 / model_h as f64;
let (w, h) = if src_ratio > model_ratio {
(model_w as f64, model_w as f64 / src_ratio)
} else {
(model_h as f64 * src_ratio, model_h as f64)
};
let w = ((w as u32) / align * align).max(align);
let h = ((h as u32) / align * align).max(align);
clamp_dims_to(w, h, MAX_PIXELS, align, None)
}
pub fn fit_to_target_area(src_w: u32, src_h: u32, target_area: u32, align: u32) -> (u32, u32) {
let src_w = src_w.max(1);
let src_h = src_h.max(1);
let align = align.max(1);
let scale = (f64::from(target_area) / (f64::from(src_w) * f64::from(src_h))).sqrt();
let width = ((f64::from(src_w) * scale) / f64::from(align)).round() as u32 * align;
let height = ((f64::from(src_h) * scale) / f64::from(align)).round() as u32 * align;
clamp_to_megapixel_limit(width.max(align), height.max(align))
}
fn is_valid_image_format(data: &[u8]) -> bool {
let is_png = data.len() >= 4 && data[..4] == [0x89, 0x50, 0x4E, 0x47];
let is_jpeg = data.len() >= 2 && data[..2] == [0xFF, 0xD8];
is_png || is_jpeg
}
fn model_family(model_name: &str) -> Option<&str> {
crate::manifest::find_manifest(model_name)
.map(|m| m.family.as_str())
.or_else(|| {
if model_name.starts_with("qwen-image-edit") {
Some("qwen-image-edit")
} else if model_name.starts_with("qwen-image") {
Some("qwen-image")
} else {
None
}
})
}
fn resolved_family<'a>(model_name: &'a str, family_hint: Option<&'a str>) -> Option<&'a str> {
family_hint
.filter(|h| !h.is_empty())
.or_else(|| model_family(model_name))
}
pub fn prompt_required_for(req: &GenerateRequest, family_hint: Option<&str>) -> bool {
prompt_required_with_conditioning(
resolved_family(&req.model, family_hint),
has_visual_conditioning(req),
)
}
pub fn has_visual_conditioning(req: &GenerateRequest) -> bool {
req.source_image.is_some()
|| req.keyframes.as_ref().is_some_and(|k| !k.is_empty())
|| req.source_video.is_some()
|| req.source_video_path.is_some()
|| req.is_extend()
}
pub fn prompt_required_with_conditioning(
family: Option<&str>,
has_visual_conditioning: bool,
) -> bool {
!(matches!(family, Some("ltx2" | "ltx-video")) && has_visual_conditioning)
}
fn validate_lora_weight(lora: &LoraWeight, field_name: &str) -> Result<(), String> {
if lora.scale < 0.0 || lora.scale > 2.0 {
return Err(format!(
"{field_name} scale ({}) must be in range [0.0, 2.0]",
lora.scale
));
}
if !lora.path.ends_with(".safetensors") && !lora.path.starts_with("camera-control:") {
return Err(format!(
"{field_name} file must be a .safetensors file or camera-control preset"
));
}
Ok(())
}
fn require_expert_routable_model(
lora: &LoraWeight,
model: &str,
family: Option<&str>,
) -> Result<(), String> {
let Some(expert) = lora.expert else {
return Ok(());
};
let expert = match expert {
crate::LoraExpert::High => "high",
crate::LoraExpert::Low => "low",
};
if family != Some("wan") {
return Err(format!(
"lora expert ('{expert}') applies to the Wan 2.2 A14B expert pair; \
{} is not a Wan model",
model
));
}
let opaque = model.starts_with("cv:") || model.starts_with("hf:");
if !opaque && !model.to_ascii_lowercase().contains("a14b") {
return Err(format!(
"lora expert ('{expert}') needs the Wan 2.2 A14B two-expert pair; \
{model} is a single-expert checkpoint — drop the expert field to \
apply the adapter to it"
));
}
Ok(())
}
fn validate_keyframes(
keyframes: &[KeyframeCondition],
frames: Option<u32>,
family: Option<&str>,
) -> Result<(), String> {
match family {
Some("ltx2") => {}
Some("wan") => {
if keyframes.len() != 2 {
return Err(format!(
"Wan supports exactly two keyframes — the first and last pixel frames — \
got {}",
keyframes.len()
));
}
let Some(frames) = frames else {
return Err(
"Wan first/last-frame keyframes require an explicit frames count — the \
closing keyframe must anchor the clip's final frame"
.to_string(),
);
};
if frames < 2 {
return Err(
"Wan first/last-frame keyframes need a multi-frame clip — frames=1 \
renders a single still, which has no distinct last frame"
.to_string(),
);
}
let last = frames.saturating_sub(1);
if keyframes[0].frame != 0 || keyframes[1].frame != last {
return Err(format!(
"Wan first/last-frame keyframes must anchor frames 0 and {last} (the \
clip's endpoints), got frames {} and {}",
keyframes[0].frame, keyframes[1].frame
));
}
}
None => {
return Err(
"unknown model family; keyframes are only supported for LTX-2 / LTX-2.3 and \
Wan models"
.to_string(),
);
}
_ => {
return Err(
"keyframes are only supported for LTX-2 / LTX-2.3 and Wan models".to_string(),
);
}
}
if keyframes.is_empty() {
return Err("keyframes must not be empty".to_string());
}
let mut seen = std::collections::BTreeSet::new();
for keyframe in keyframes {
if !is_valid_image_format(&keyframe.image) {
return Err("keyframes must contain only PNG or JPEG images".to_string());
}
if let Some(total_frames) = frames {
if keyframe.frame >= total_frames {
return Err(format!(
"keyframe frame ({}) must be less than frames ({total_frames})",
keyframe.frame
));
}
}
if !seen.insert(keyframe.frame) {
return Err(format!("duplicate keyframe frame: {}", keyframe.frame));
}
}
Ok(())
}
fn validate_guidance_overrides(overrides: &Ltx2GuidanceOverrides) -> Result<(), String> {
if overrides.is_empty() {
return Err(
"guidance_overrides must set at least one field; omit it to keep pipeline defaults"
.to_string(),
);
}
let bounded = |value: Option<f64>, name: &str, max: f64| -> Result<(), String> {
match value {
Some(value) if !value.is_finite() => Err(format!("{name} must be a finite number")),
Some(value) if !(0.0..=max).contains(&value) => {
Err(format!("{name} ({value}) must be between 0.0 and {max}"))
}
_ => Ok(()),
}
};
bounded(
overrides.stg_scale,
"guidance_overrides.stg_scale",
Ltx2GuidanceOverrides::MAX_SCALE,
)?;
bounded(
overrides.modality_scale,
"guidance_overrides.modality_scale",
Ltx2GuidanceOverrides::MAX_SCALE,
)?;
bounded(
overrides.rescale_scale,
"guidance_overrides.rescale_scale",
1.0,
)?;
if let Some(skip_step) = overrides.skip_step {
if skip_step > Ltx2GuidanceOverrides::MAX_SKIP_STEP {
return Err(format!(
"guidance_overrides.skip_step ({skip_step}) must be <= {}",
Ltx2GuidanceOverrides::MAX_SKIP_STEP
));
}
}
if let Some(blocks) = &overrides.stg_blocks {
if blocks.is_empty() {
return Err(
"guidance_overrides.stg_blocks must not be empty; omit it to keep the pipeline default block"
.to_string(),
);
}
if blocks.len() > MAX_STG_BLOCKS {
return Err(format!(
"guidance_overrides.stg_blocks lists {} blocks; at most {MAX_STG_BLOCKS} are supported",
blocks.len()
));
}
for (index, block) in blocks.iter().enumerate() {
if *block >= MAX_STG_BLOCK_INDEX {
return Err(format!(
"guidance_overrides.stg_blocks[{index}] ({block}) exceeds the deepest supported transformer block ({})",
MAX_STG_BLOCK_INDEX - 1
));
}
if blocks[..index].contains(block) {
return Err(format!(
"guidance_overrides.stg_blocks[{index}] ({block}) is listed more than once"
));
}
}
}
Ok(())
}
fn validate_extend(req: &GenerateRequest, family: Option<&str>) -> Result<(), String> {
if let Some(video) = &req.extend_video {
require_extend_capable_family(family, "extend_video")?;
if req.extend_video_path.is_some() {
return Err("extend_video_path cannot be combined with extend_video".to_string());
}
if video.is_empty() {
return Err("extend_video must not be empty".to_string());
}
validate_inline_media_size(video, "extend_video", MAX_INLINE_EXTEND_VIDEO_BYTES)?;
}
if let Some(path) = &req.extend_video_path {
require_extend_capable_family(family, "extend_video_path")?;
if path.trim().is_empty() {
return Err("extend_video_path must not be empty".to_string());
}
}
if !req.is_extend() {
if req.extend_overlap_frames.is_some() {
return Err(
"extend_overlap_frames requires extend_video or extend_video_path".to_string(),
);
}
return Ok(());
}
if req.source_video.is_some() || req.source_video_path.is_some() {
return Err(
"extend_video cannot be combined with source_video; extend continues an existing \
clip, while source_video is reference conditioning for a fresh render"
.to_string(),
);
}
if req.source_image.is_some() {
return Err(
"extend_video cannot be combined with source_image; the continuation's first frames \
are pinned by the source video's tail"
.to_string(),
);
}
if req.keyframes.is_some() {
return Err("extend_video cannot be combined with keyframes".to_string());
}
let overlap = req.effective_extend_overlap_frames_for_family(family);
if overlap == 0 {
return Err(
"extend_overlap_frames must be >= 1 so the continuation has motion context".to_string(),
);
}
let step = family.and_then(frame_step_for_family).unwrap_or(8);
if overlap % step != 1 {
let examples: Vec<String> = (0..4).map(|k| (k * step + 1).to_string()).collect();
return Err(format!(
"extend_overlap_frames ({overlap}) must be {step}k+1 ({}, …) so the carryover \
frames re-encode cleanly through this family's video VAE temporal grid",
examples.join(", "),
));
}
if let Some(frames) = req.frames {
if overlap >= frames {
return Err(format!(
"extend_overlap_frames ({overlap}) must be strictly less than frames ({frames}) \
so the continuation adds at least one new frame"
));
}
}
Ok(())
}
pub fn require_extend_capable_family(
family: Option<&str>,
feature_name: &str,
) -> Result<(), String> {
match family {
Some("ltx2") | Some("wan") => Ok(()),
None => Err(format!(
"unknown model family; {feature_name} is only supported for LTX-2 / LTX-2.3 and Wan models"
)),
_ => Err(format!(
"{feature_name} is only supported for LTX-2 / LTX-2.3 and Wan models"
)),
}
}
fn require_ltx2_family(family: Option<&str>, feature_name: &str) -> Result<(), String> {
match family {
Some("ltx2") => Ok(()),
None => Err(format!(
"unknown model family; {feature_name} is only supported for LTX-2 / LTX-2.3 models"
)),
_ => Err(format!(
"{feature_name} is only supported for LTX-2 / LTX-2.3 models"
)),
}
}
fn require_lora_capable_family(family: Option<&str>) -> Result<(), String> {
match family {
Some(family) if family_supports_lora(family) => Ok(()),
Some(other) => Err(format!(
"LoRA is currently supported for FLUX, Flux.2, LTX-2, SD1.5, SD3, SDXL, Qwen-Image, Wan, and Z-Image models; got family {other:?}"
)),
None => Err(
"LoRA requires a known model family — pick a FLUX, Flux.2, LTX-2, SD1.5, SD3, SDXL, Qwen-Image, Wan, or Z-Image model first"
.to_string(),
),
}
}
fn require_controlnet_capable_family(family: Option<&str>) -> Result<(), String> {
match family {
Some("sd15" | "sd1.5" | "stable-diffusion-1.5") => Ok(()),
Some(other) => Err(format!(
"ControlNet generation is currently supported for SD1.5 models; got family {other:?}"
)),
None => Err(
"ControlNet generation requires a known model family — pick an SD1.5 model first"
.to_string(),
),
}
}
fn validate_inline_media_size(
bytes: &[u8],
field_name: &str,
max_bytes: usize,
) -> Result<(), String> {
if bytes.len() > max_bytes {
return Err(format!(
"{field_name} exceeds the {} inline request limit (got {:.1} MiB)",
mib_label(max_bytes),
bytes.len() as f64 / (1024.0 * 1024.0)
));
}
Ok(())
}
pub fn validate_generate_request(req: &GenerateRequest) -> Result<(), String> {
validate_generate_request_with_family(req, None)
}
pub fn require_generate_request_model_activation(
req: &GenerateRequest,
artifact_root: Option<&std::path::Path>,
family_hint: Option<&str>,
) -> Result<(), crate::ModelActivationError> {
crate::require_model_activation(&req.model, family_hint)?;
for identity in [req.control_model.as_deref(), req.upscale_model.as_deref()]
.into_iter()
.flatten()
{
crate::require_model_activation(identity, None)?;
}
for lora in req.lora.iter().chain(req.loras.iter().flatten()) {
crate::require_model_artifact_activation(
std::path::Path::new(&lora.path),
artifact_root,
None,
)?;
}
Ok(())
}
pub fn request_carries_source_frames(req: &GenerateRequest) -> bool {
req.source_image.is_some()
|| req.keyframes.as_ref().is_some_and(|k| !k.is_empty())
|| req.is_extend()
}
pub fn source_image_contract_violation(
family: Option<&str>,
model: &str,
capability: Option<crate::types::SourceImageCapability>,
has_source: bool,
) -> Option<String> {
use crate::types::SourceImageCapability;
let wan = family == Some("wan");
match capability {
Some(SourceImageCapability::Unsupported) if has_source => Some(if wan {
"this Wan checkpoint is text-to-video only and does not accept a source image \
or keyframes — remove them, or pick an I2V-capable checkpoint such as \
wan22-ti2v-5b or wan22-i2v-a14b"
.to_string()
} else {
format!(
"{model} is text-to-video only and does not accept a source image — its \
engine has no image-to-video path; remove the image, or pick an \
image-capable checkpoint such as an LTX-2 model"
)
}),
Some(SourceImageCapability::Required) if !has_source => Some(if wan {
"this Wan I2V checkpoint needs a source image; supply one, or pick a \
text-to-video checkpoint such as wan22-t2v-a14b"
.to_string()
} else {
format!("{model} needs a source image; supply one")
}),
_ => None,
}
}
pub fn validate_generate_request_with_family(
req: &GenerateRequest,
family_hint: Option<&str>,
) -> Result<(), String> {
crate::require_model_activation(&req.model, family_hint).map_err(|error| error.to_string())?;
validate_generate_request_after_activation(req, family_hint)
}
#[cfg(any(feature = "h3", feature = "h3-private-uat"))]
pub fn validate_h3_private_uat_request(req: &GenerateRequest) -> Result<(), String> {
if !matches!(
req.model.as_str(),
crate::minimax_h3::FL2VA_COMFY | crate::minimax_h3::REF2VA_COMFY
) {
return Err(
"private MiniMax H3 validation requires an exact reviewed task model".to_string(),
);
}
validate_generate_request_after_activation(req, Some(crate::minimax_h3::FAMILY))
}
fn validate_generate_request_after_activation(
req: &GenerateRequest,
family_hint: Option<&str>,
) -> Result<(), String> {
let family = resolved_family(&req.model, family_hint);
if req.references.is_some() && !family.is_some_and(crate::minimax_h3::is_family) {
return Err(
"references is only supported by MiniMax H3 Ref2VA; other families retain their existing source/edit fields"
.to_string(),
);
}
if req.prompt.trim().is_empty() && prompt_required_for(req, family_hint) {
return Err("prompt must not be empty".to_string());
}
let composition = if family == Some("ltx2") {
ltx2_spatial_composition_for_request(req)
} else {
Ltx2SpatialComposition::SinglePass
};
let audio_only =
family == Some("ltx2") && req.pipeline.is_some_and(Ltx2PipelineMode::is_audio_only);
if !audio_only {
validate_generation_dimensions_for_model(
&req.model,
req.width,
req.height,
family,
composition,
)?;
}
validate_family_video_timing_constraints(req.frames, req.fps, family)?;
if composition == Ltx2SpatialComposition::TiledTwoStage {
validate_ltx2_stage1_span(req.width, req.height, req.spatial_upscale)?;
}
if req.steps == 0 {
return Err("steps must be >= 1".to_string());
}
if req.steps > 100 {
return Err(format!("steps ({}) must be <= 100", req.steps));
}
if req.batch_size == 0 {
return Err("batch_size must be >= 1".to_string());
}
if req.guidance < 0.0 {
return Err(format!("guidance ({}) must be >= 0.0", req.guidance));
}
if req.guidance > 100.0 {
return Err(format!("guidance ({}) must be <= 100.0", req.guidance));
}
if req.prompt.len() > 77_000 {
return Err(format!(
"prompt length ({} bytes) exceeds the 77,000-byte limit",
req.prompt.len()
));
}
if let Some(ref neg) = req.negative_prompt {
if neg.len() > 77_000 {
return Err(format!(
"negative_prompt length ({} bytes) exceeds the 77,000-byte limit",
neg.len()
));
}
}
if family.is_some_and(crate::minimax_h3::is_family) {
let task = crate::minimax_h3::task_for_model(&req.model).ok_or_else(|| {
"MiniMax H3 requests must resolve an explicit FL2VA or Ref2VA task partition"
.to_string()
})?;
if req.mask_image.is_some() {
return Err("MiniMax H3 does not support mask_image".to_string());
}
if req.control_image.is_some() || req.control_model.is_some() {
return Err("MiniMax H3 does not support ControlNet inputs".to_string());
}
if req.cfg_plus.is_some() {
return Err("MiniMax H3 does not support cfg_plus".to_string());
}
if req.scheduler.is_some() {
return Err(
"MiniMax H3 uses its dedicated synchronized dual-shift schedule; scheduler overrides are unsupported"
.to_string(),
);
}
if req.lora.is_some() || req.loras.is_some() {
return Err("MiniMax H3 does not support LoRA".to_string());
}
if req.upscale_model.is_some() {
return Err("MiniMax H3 does not support post-generation image upscaling".to_string());
}
if req.pipeline.is_some()
|| req.ic_lora_control.is_some()
|| req.retake_range.is_some()
|| req.spatial_upscale.is_some()
|| req.temporal_upscale.is_some()
|| req.guidance_overrides.is_some()
|| req.hdr_exr_dir.is_some()
|| req.hdr_exr_full_float
{
return Err("MiniMax H3 does not accept LTX-2 pipeline controls".to_string());
}
if req
.source_image
.as_deref()
.is_some_and(|image| !is_valid_image_format(image))
{
return Err("source_image must be a PNG or JPEG image".to_string());
}
if req.source_image.is_some()
&& (!req.strength.is_finite() || !(0.0..=1.0).contains(&req.strength))
{
return Err(format!(
"strength ({}) must be a finite value in range [0.0, 1.0] when source_image is provided",
req.strength
));
}
if req
.edit_images
.as_ref()
.is_some_and(|images| images.iter().any(|image| !is_valid_image_format(image)))
{
return Err("edit_images must contain only PNG or JPEG images".to_string());
}
if req.edit_images.as_ref().is_some_and(Vec::is_empty) {
return Err("edit_images must not be empty when provided".to_string());
}
if req.keyframes.as_ref().is_some_and(|keyframes| {
keyframes
.iter()
.any(|keyframe| !is_valid_image_format(&keyframe.image))
}) {
return Err("keyframes must contain only PNG or JPEG images".to_string());
}
if req.keyframes.as_ref().is_some_and(Vec::is_empty) {
return Err("keyframes must not be empty when provided".to_string());
}
if req.extend_overlap_frames.is_some() {
return Err(
"extend_overlap_frames requires extend_video or extend_video_path, which MiniMax H3 does not support"
.to_string(),
);
}
crate::minimax_h3::validate_request_contract(req, task)
.map(|_| ())
.map_err(|error| error.to_string())?;
return Ok(());
}
let flux2_dev = is_flux2_dev_model(&req.model);
if family == Some("qwen-image-edit") {
if req.edit_images.as_ref().is_none_or(Vec::is_empty) {
return Err(
"Qwen Image Edit needs at least one image. Add a Target image and try again."
.to_string(),
);
}
if req.batch_size != 1 {
return Err("qwen-image-edit only supports batch_size = 1".to_string());
}
if req.source_image.is_some() {
return Err("qwen-image-edit uses edit_images instead of source_image".to_string());
}
if req.mask_image.is_some() {
return Err("qwen-image-edit does not support mask_image".to_string());
}
if req.control_image.is_some() || req.control_model.is_some() {
return Err("qwen-image-edit does not support ControlNet inputs".to_string());
}
if let Some(ref images) = req.edit_images {
for image in images {
if !is_valid_image_format(image) {
return Err("edit_images must contain only PNG or JPEG images".to_string());
}
}
}
} else if flux2_dev {
if req.batch_size != 1
&& req
.edit_images
.as_ref()
.is_some_and(|images| !images.is_empty())
{
return Err("flux2-dev reference editing only supports batch_size = 1".to_string());
}
if req.source_image.is_some() {
return Err("flux2-dev uses edit_images instead of source_image".to_string());
}
if req.mask_image.is_some() {
return Err("flux2-dev does not support mask_image".to_string());
}
if req.control_image.is_some() || req.control_model.is_some() {
return Err("flux2-dev does not support ControlNet inputs".to_string());
}
if req.lora.is_some() || req.loras.as_ref().is_some_and(|loras| !loras.is_empty()) {
return Err("flux2-dev does not support LoRA".to_string());
}
if let Some(images) = &req.edit_images {
if images.len() > FLUX2_DEV_MAX_REFERENCE_IMAGES {
return Err(format!(
"flux2-dev supports at most {FLUX2_DEV_MAX_REFERENCE_IMAGES} ordered reference images"
));
}
if images.iter().any(|image| !is_valid_image_format(image)) {
return Err("edit_images must contain only PNG or JPEG images".to_string());
}
}
} else if req.edit_images.is_some() {
return Err(
"edit_images are only supported for qwen-image-edit and flux2-dev models".to_string(),
);
}
if let Some(ref img) = req.source_image {
if req.strength < 0.0 || req.strength > 1.0 {
return Err(format!(
"strength ({}) must be in range [0.0, 1.0] when source_image is provided",
req.strength
));
}
if !is_valid_image_format(img) {
return Err("source_image must be a PNG or JPEG image".to_string());
}
}
if let Some(ref ctrl) = req.control_image {
require_controlnet_capable_family(family)?;
if req.control_model.is_none() {
return Err("control_image requires control_model to also be provided".to_string());
}
if !is_valid_image_format(ctrl) {
return Err("control_image must be a PNG or JPEG image".to_string());
}
if req.control_scale < 0.0 {
return Err(format!(
"control_scale ({}) must be >= 0.0",
req.control_scale
));
}
}
if req.control_model.is_some() && req.control_image.is_none() {
require_controlnet_capable_family(family)?;
return Err("control_model requires control_image to also be provided".to_string());
}
if let Some(ref mask) = req.mask_image {
if req.source_image.is_none() {
return Err("mask_image requires source_image to also be provided".to_string());
}
if !is_valid_image_format(mask) {
return Err("mask_image must be a PNG or JPEG image".to_string());
}
}
if let Some(ref lora) = req.lora {
require_lora_capable_family(family)?;
validate_lora_weight(lora, "lora")?;
require_expert_routable_model(lora, &req.model, family)?;
}
if let Some(ref loras) = req.loras {
if loras.is_empty() {
return Err("loras must not be empty when provided".to_string());
}
require_lora_capable_family(family)?;
for lora in loras {
validate_lora_weight(lora, "loras")?;
require_expert_routable_model(lora, &req.model, family)?;
}
}
if let Some(fps) = req.fps {
if fps == 0 {
return Err("fps must be >= 1".to_string());
}
if fps > 120 {
return Err(format!("fps ({fps}) must be <= 120"));
}
}
if let Some(frames) = req.frames {
if frames == 0 {
return Err("frames must be >= 1".to_string());
}
if let Some(step) = family.and_then(frame_step_for_family) {
let offset = family.and_then(frame_offset_for_family).unwrap_or(1);
if frames < offset || !(frames - offset).is_multiple_of(step) {
return Err(format!(
"frames ({frames}) must be {step}n+{offset} for this model family (e.g. {}, {}, {}, …)",
step + offset,
2 * step + offset,
3 * step + offset,
));
}
}
if matches!(family, Some("ltx2")) {
let fps = req.fps.unwrap_or(LTX2_DEFAULT_FPS).max(1);
let (stage1_frames, stage1_fps) = match req.temporal_upscale {
Some(crate::Ltx2TemporalUpscale::X2) => {
(frames.saturating_sub(1) / 2 + 1, (fps / 2).max(1))
}
None => (frames, fps),
};
let stage1_cap = ltx2_max_frames_at_fps(stage1_fps);
if stage1_frames > stage1_cap {
let delivered_cap = match req.temporal_upscale {
Some(crate::Ltx2TemporalUpscale::X2) => (stage1_cap - 1) * 2 + 1,
None => stage1_cap,
};
let delivered_cap = if delivered_cap > 1 {
delivered_cap - ((delivered_cap - 1) % 8)
} else {
delivered_cap
};
return Err(format!(
"frames ({frames}) exceeds the LTX-2 / LTX-2.3 temporal RoPE budget of \
{LTX2_MAX_RUNTIME_SECONDS}s: at {fps} fps the ceiling is {delivered_cap} frames. \
Raise --fps, lower --frames, or render the shot as a multi-clip sequence"
));
}
} else {
let max_frames = family
.and_then(|family| {
max_frames_for_family_at_fps(family, req.fps.unwrap_or(LTX2_DEFAULT_FPS).max(1))
})
.unwrap_or(MAX_FRAMES_GLOBAL);
if frames > max_frames {
return Err(format!("frames ({frames}) must be <= {max_frames}"));
}
}
}
if let Some(keyframes) = &req.keyframes {
validate_keyframes(keyframes, req.frames, family)?;
if family == Some("wan")
&& keyframes.len() == 2
&& crate::manifest::resolve_model_name(&req.model).starts_with("wan22-ti2v-5b")
&& req
.frames
.is_some_and(|frames| frames < WAN_TI2V_FLF_MIN_FRAMES)
{
return Err(
"wan22-ti2v-5b first/last-frame conditioning needs at least 9 frames — \
shorter clips leave no latent frames to denoise between the pinned endpoints"
.to_string(),
);
}
}
if let Some(audio) = &req.audio_file {
require_ltx2_family(family, "audio_file")?;
if req.audio_file_path.is_some() {
return Err("audio_file_path cannot be combined with audio_file".to_string());
}
if audio.is_empty() {
return Err("audio_file must not be empty".to_string());
}
validate_inline_media_size(audio, "audio_file", MAX_INLINE_AUDIO_BYTES)?;
}
if let Some(path) = &req.audio_file_path {
require_ltx2_family(family, "audio_file_path")?;
if path.trim().is_empty() {
return Err("audio_file_path must not be empty".to_string());
}
}
if let Some(video) = &req.source_video {
require_ltx2_family(family, "source_video")?;
if req.source_video_path.is_some() {
return Err("source_video_path cannot be combined with source_video".to_string());
}
if video.is_empty() {
return Err("source_video must not be empty".to_string());
}
validate_inline_media_size(video, "source_video", MAX_INLINE_SOURCE_VIDEO_BYTES)?;
}
if let Some(path) = &req.source_video_path {
require_ltx2_family(family, "source_video_path")?;
if path.trim().is_empty() {
return Err("source_video_path must not be empty".to_string());
}
}
validate_extend(req, family)?;
if req.enable_audio == Some(true) {
require_ltx2_family(family, "enable_audio")?;
}
if req.retake_range.is_some() {
require_ltx2_family(family, "retake_range")?;
}
if req.spatial_upscale.is_some() {
require_ltx2_family(family, "spatial_upscale")?;
}
if req.temporal_upscale.is_some() {
require_ltx2_family(family, "temporal_upscale")?;
}
if req.pipeline.is_some() {
require_ltx2_family(family, "pipeline")?;
}
if let Some(overrides) = &req.guidance_overrides {
require_ltx2_family(family, "guidance_overrides")?;
validate_guidance_overrides(overrides)?;
if req.pipeline.is_some_and(Ltx2PipelineMode::is_audio_only) {
if let Some(modality_scale) = overrides.modality_scale {
if (modality_scale - 1.0).abs() > f64::EPSILON {
return Err(
"guidance_overrides.modality_scale must be 1.0 for pipeline=t2a: \
audio-only generation has no video modality to guide against"
.to_string(),
);
}
}
}
}
if let Some(dir) = req.hdr_exr_dir.as_deref() {
require_ltx2_family(family, "hdr_exr_dir")?;
if dir.trim().is_empty() {
return Err("hdr_exr_dir must not be empty".to_string());
}
if req.extend_video.is_some() || req.extend_video_path.is_some() {
return Err("hdr_exr_dir cannot be combined with extend_video".to_string());
}
if req
.ic_lora_control
.as_deref()
.map(crate::ltx2_control::normalize_control_id)
.as_deref()
!= Some("hdr")
{
return Err(
"hdr_exr_dir requires ic_lora_control=hdr — EXR output is only meaningful for \
the HDR adapter's LogC3 signal"
.to_string(),
);
}
} else if req.hdr_exr_full_float {
return Err("hdr_exr_full_float requires hdr_exr_dir".to_string());
}
if let Some(control) = req.ic_lora_control.as_deref() {
require_ltx2_family(family, "ic_lora_control")?;
if control.trim().is_empty() {
return Err("ic_lora_control must not be empty".to_string());
}
let required_pipeline = crate::ltx2_control::pipeline_for_control_id(control);
if req.pipeline != Some(required_pipeline) {
return Err(format!(
"ic_lora_control '{}' requires pipeline={required_pipeline}",
crate::ltx2_control::normalize_control_id(control)
));
}
if req.source_video.is_none() && req.source_video_path.is_none() {
return Err("ic_lora_control requires source_video or source_video_path".to_string());
}
let user_loras = usize::from(req.lora.is_some()) + req.loras.as_ref().map_or(0, Vec::len);
if user_loras + 1 > 4 {
return Err(
"ic_lora_control plus custom LoRAs exceeds the four-LoRA stack limit".to_string(),
);
}
}
if family == Some("wan") {
match (req.resolved_output_format(), req.frames) {
(
OutputFormat::Gif | OutputFormat::Apng | OutputFormat::Webp | OutputFormat::Mp4,
_,
) => {}
(OutputFormat::Png | OutputFormat::Jpeg, Some(1)) => {}
_ => return Err("Wan outputs must use mp4, gif, apng, or webp".to_string()),
}
if req.source_image.is_some() && req.keyframes.as_ref().is_some_and(|k| !k.is_empty()) {
return Err(
"Wan takes the first frame from either source_image or keyframes[0], not both \
— for a first/last-frame render, put both endpoints in keyframes"
.to_string(),
);
}
}
match req.scheduler {
Some(crate::Scheduler::Euler | crate::Scheduler::DpmPp) if family != Some("wan") => {
return Err(format!(
"scheduler '{}' is a Wan sample solver and is only supported for wan models",
req.scheduler.expect("matched Some")
));
}
Some(crate::Scheduler::Ddim | crate::Scheduler::EulerAncestral)
if family == Some("wan") =>
{
return Err(format!(
"Wan supports the uni-pc, euler, and dpm-pp sample solvers; '{}' is a UNet \
scheduler",
req.scheduler.expect("matched Some")
));
}
_ => {}
}
if let Some(shift) = req.sample_shift {
if family != Some("wan") {
return Err(
"sample_shift is a Wan flow-matching control and is not supported for this model"
.to_string(),
);
}
if !shift.is_finite() || shift <= 0.0 {
return Err(format!(
"sample_shift must be finite and positive, got {shift}"
));
}
}
if family == Some("wan")
&& (req.lora.is_some() || req.loras.as_ref().is_some_and(|list| !list.is_empty()))
{
let canonical = crate::manifest::resolve_model_name(&req.model);
if canonical.ends_with(":fp8") && canonical.contains("a14b") {
return Err(format!(
"{canonical} is fp8-scaled and refuses LoRA stacks — merging would re-round \
every targeted weight to three mantissa bits. Use the :q5/:q8 GGUF or bf16 \
tier for adapters"
));
}
}
for (label, value) in [
("high", req.distill_strength_high),
("low", req.distill_strength_low),
] {
if let Some(strength) = value {
if family != Some("wan") {
return Err(format!(
"distill_strength_{label} is a Wan Lightning control and is not supported \
for this model"
));
}
if !strength.is_finite() || strength <= 0.0 || strength > 4.0 {
return Err(format!(
"distill_strength_{label} must be in (0, 4], got {strength}"
));
}
}
}
if family == Some("ltx2") {
let audio_only = req.pipeline.is_some_and(Ltx2PipelineMode::is_audio_only);
match (req.resolved_output_format(), audio_only) {
(OutputFormat::Wav, true) => {}
(OutputFormat::Wav, false) => {
return Err("wav output requires pipeline=t2a".to_string());
}
(_, true) => {
return Err("pipeline=t2a renders audio only; set output_format=wav".to_string());
}
(
OutputFormat::Gif | OutputFormat::Apng | OutputFormat::Webp | OutputFormat::Mp4,
false,
) => {}
(_, false) => return Err("LTX-2 outputs must use mp4, gif, apng, or webp".to_string()),
}
if req.enable_audio == Some(true)
&& !audio_only
&& req.resolved_output_format() != OutputFormat::Mp4
{
return Err("audio-enabled LTX-2 outputs must use mp4 format".to_string());
}
if req.enable_audio == Some(false) && audio_only {
return Err("pipeline=t2a cannot be combined with enable_audio=false".to_string());
}
if req.retake_range.is_some()
&& req.source_video.is_none()
&& req.source_video_path.is_none()
{
return Err(
"retake_range requires source_video or source_video_path to also be provided"
.to_string(),
);
}
if let Some(range) = &req.retake_range {
if !(range.start_seconds.is_finite() && range.end_seconds.is_finite()) {
return Err("retake_range values must be finite numbers".to_string());
}
if range.start_seconds < 0.0 {
return Err("retake_range start_seconds must be >= 0.0".to_string());
}
if range.end_seconds <= range.start_seconds {
return Err(
"retake_range end_seconds must be greater than start_seconds".to_string(),
);
}
}
if let Some(pipeline) = req.pipeline {
match pipeline {
Ltx2PipelineMode::A2Vid => {
if req.audio_file.is_none() && req.audio_file_path.is_none() {
return Err(
"pipeline=a2-vid requires audio_file or audio_file_path".to_string()
);
}
}
Ltx2PipelineMode::Retake => {
if req.source_video.is_none() && req.source_video_path.is_none() {
return Err("pipeline=retake requires source_video or source_video_path"
.to_string());
}
if req.retake_range.is_none() {
return Err("pipeline=retake requires retake_range".to_string());
}
}
Ltx2PipelineMode::Keyframe => {
let keyframe_count = req.keyframes.as_ref().map_or(0, Vec::len);
if keyframe_count < 2 {
return Err("pipeline=keyframe requires at least 2 keyframes".to_string());
}
}
Ltx2PipelineMode::IcLora => {
if req.source_video.is_none() && req.source_video_path.is_none() {
return Err(
"pipeline=ic-lora requires source_video or source_video_path"
.to_string(),
);
}
if req.ic_lora_control.is_none()
&& req.lora.is_none()
&& req.loras.as_ref().is_none_or(Vec::is_empty)
{
return Err("pipeline=ic-lora requires at least one LoRA".to_string());
}
}
Ltx2PipelineMode::LipDub => {
if req.source_video.is_none() && req.source_video_path.is_none() {
return Err(
"pipeline=lip-dub requires source_video or source_video_path (the \
clip being re-voiced)"
.to_string(),
);
}
if req.ic_lora_control.is_none()
&& req.lora.is_none()
&& req.loras.as_ref().is_none_or(Vec::is_empty)
{
return Err("pipeline=lip-dub requires the lip-dub IC-LoRA; pass \
ic_lora_control=lipdub"
.to_string());
}
if !req.width.is_multiple_of(LTX2_TWO_STAGE_ALIGNMENT)
|| !req.height.is_multiple_of(LTX2_TWO_STAGE_ALIGNMENT)
{
return Err(format!(
"pipeline=lip-dub renders in two stages, so width and height must be \
multiples of {LTX2_TWO_STAGE_ALIGNMENT}; got {}x{}",
req.width, req.height
));
}
if req.retake_range.is_some() {
return Err(
"pipeline=lip-dub cannot be combined with retake_range".to_string()
);
}
if req
.keyframes
.as_ref()
.is_some_and(|items| !items.is_empty())
{
return Err(
"pipeline=lip-dub cannot be combined with keyframes".to_string()
);
}
if req.spatial_upscale.is_some() || req.temporal_upscale.is_some() {
return Err(
"pipeline=lip-dub cannot be combined with spatial_upscale or \
temporal_upscale; the render must match the reference video"
.to_string(),
);
}
}
Ltx2PipelineMode::T2a => {
for (present, field) in [
(req.source_image.is_some(), "source_image"),
(req.source_video.is_some(), "source_video"),
(req.source_video_path.is_some(), "source_video_path"),
(req.audio_file.is_some(), "audio_file"),
(req.audio_file_path.is_some(), "audio_file_path"),
(req.is_extend(), "extend_video"),
(
req.keyframes.as_ref().is_some_and(|k| !k.is_empty()),
"keyframes",
),
(req.retake_range.is_some(), "retake_range"),
(req.spatial_upscale.is_some(), "spatial_upscale"),
(req.temporal_upscale.is_some(), "temporal_upscale"),
(req.upscale_model.is_some(), "upscale_model"),
] {
if present {
return Err(format!(
"pipeline=t2a generates audio only and cannot be combined with {field}"
));
}
}
}
Ltx2PipelineMode::OneStage
| Ltx2PipelineMode::TwoStage
| Ltx2PipelineMode::TwoStageHq
| Ltx2PipelineMode::Distilled => {}
}
}
}
Ok(())
}
pub fn is_flux2_dev_model(model: &str) -> bool {
let model = model.to_ascii_lowercase();
model.contains("flux2-dev") || model.contains("flux.2-dev")
}
pub fn validate_upscale_request(req: &UpscaleRequest) -> Result<(), String> {
if req.model.trim().is_empty() {
return Err("upscale model must not be empty".to_string());
}
if req.image.is_empty() {
return Err("upscale image must not be empty".to_string());
}
if !is_valid_image_format(&req.image) {
return Err("upscale image must be a PNG or JPEG image".to_string());
}
if let Some(tile_size) = req.tile_size {
if tile_size != 0 && tile_size < 64 {
return Err(format!(
"tile_size ({tile_size}) must be 0 (disabled) or >= 64"
));
}
}
Ok(())
}
pub fn wan_recommended_dimensions(model: &str) -> &'static [(u32, u32)] {
crate::generation_profile::presets_for_identity(model, "wan", None)
}
pub fn wan_dimension_alignment(model: &str) -> u32 {
let canonical = crate::manifest::resolve_model_name(model);
if canonical.starts_with("wan22-ti2v-5b") {
return 32;
}
dimension_alignment_for_family(Some("wan"))
}
pub fn recommended_dimensions(family: &str) -> &'static [(u32, u32)] {
crate::generation_profile::family_presets(family)
}
pub fn recommended_dimensions_composed(
family: &str,
composition: Ltx2SpatialComposition,
) -> Vec<(u32, u32)> {
let base = recommended_dimensions(family);
if family != "ltx2" || composition != Ltx2SpatialComposition::TiledTwoStage {
return base.to_vec();
}
let mut out = base.to_vec();
for rung in LTX2_OUTPUT_RUNGS
.iter()
.filter(|rung| rung.requires_tiled_stage2())
{
out.push((rung.width, rung.height));
out.push((rung.height, rung.width));
}
out
}
pub fn dimension_warning(width: u32, height: u32, family: &str) -> Option<String> {
dimension_warning_composed(width, height, family, Ltx2SpatialComposition::SinglePass)
}
pub fn dimension_warning_composed(
width: u32,
height: u32,
family: &str,
composition: Ltx2SpatialComposition,
) -> Option<String> {
let dims = recommended_dimensions_composed(family, composition);
if dims.is_empty() {
return None;
}
if dims.contains(&(width, height)) {
return None;
}
let suggestions: Vec<String> = dims
.iter()
.take(4)
.map(|(w, h)| format!("{w}x{h}"))
.collect();
let more = if dims.len() > 4 {
format!(", ... ({} total)", dims.len())
} else {
String::new()
};
Some(format!(
"{width}x{height} is not a recommended resolution for {family} models. \
Suggested: {}{}",
suggestions.join(", "),
more,
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::OutputFormat;
#[test]
fn an_expert_bound_lora_needs_a_two_expert_checkpoint() {
let lora = |expert| LoraWeight {
path: "/loras/high_noise_model.safetensors".to_string(),
scale: 1.0,
expert,
};
assert!(require_expert_routable_model(
&lora(Some(crate::LoraExpert::High)),
"wan22-t2v-a14b:q5",
Some("wan"),
)
.is_ok());
for model in ["wan21-t2v-1.3b:bf16", "wan22-ti2v-5b:fp16"] {
let error = require_expert_routable_model(
&lora(Some(crate::LoraExpert::Low)),
model,
Some("wan"),
)
.unwrap_err();
assert!(error.contains("single-expert"), "{error}");
assert!(error.contains("drop the expert field"), "{error}");
}
let error = require_expert_routable_model(
&lora(Some(crate::LoraExpert::High)),
"flux-dev:q8",
Some("flux"),
)
.unwrap_err();
assert!(error.contains("not a Wan model"), "{error}");
assert!(require_expert_routable_model(
&lora(Some(crate::LoraExpert::High)),
"cv:123456",
Some("wan"),
)
.is_ok());
assert!(require_expert_routable_model(&lora(None), "flux-dev:q8", Some("flux")).is_ok());
}
#[test]
fn ltx2_admits_upstreams_shipped_1080p_shape() {
assert!(validate_generation_dimensions(1920, 1088, Some("ltx2")).is_ok());
assert!(validate_generation_dimensions(1088, 1920, Some("ltx2")).is_ok());
}
#[test]
fn non_ltx2_families_keep_the_default_ceiling() {
for family in [Some("flux"), Some("ltx-video"), Some("sdxl"), None] {
let err = validate_generation_dimensions(1920, 1088, family)
.expect_err("only LTX-2 gets the raised ceiling");
assert!(
err.contains("1.8MP"),
"{family:?} must still report the default limit, got: {err}"
);
}
}
#[test]
fn ltx2_rejects_an_axis_beyond_the_rope_span() {
let err = validate_generation_dimensions(3200, 512, Some("ltx2"))
.expect_err("an over-wide axis must be rejected on its own merits");
assert!(
err.contains("2048"),
"the error must name the axis limit, got: {err}"
);
assert!(validate_generation_dimensions(512, 3200, Some("ltx2")).is_err());
assert!(validate_generation_dimensions(2048, 992, Some("ltx2")).is_ok());
assert!(validate_generation_dimensions(2048, 1024, Some("ltx2"))
.expect_err("over the pixel budget")
.contains("megapixels"));
}
#[test]
fn ltx2_recommended_dimensions_are_grid_aligned_and_inside_the_family_ceiling() {
for &(width, height) in recommended_dimensions("ltx2") {
assert!(
validate_generation_dimensions(width, height, Some("ltx2")).is_ok(),
"advertised preset {width}x{height} must be admissible"
);
}
}
#[test]
fn single_pass_admission_is_byte_for_byte_unchanged() {
for &(width, height) in &[
(768u32, 512u32),
(1216, 704),
(1920, 1088),
(1088, 1920),
(2048, 992),
] {
assert!(
validate_generation_dimensions(width, height, Some("ltx2")).is_ok(),
"{width}x{height} was admissible before the composed ceiling"
);
}
assert!(validate_generation_dimensions(2048, 1024, Some("ltx2"))
.expect_err("2.10 MP is over the single-pass pixel budget")
.contains("megapixels"));
assert!(validate_generation_dimensions(3200, 512, Some("ltx2"))
.expect_err("a 3200px axis is past the trained span")
.contains("2048"));
assert!(validate_generation_dimensions(2080, 512, Some("ltx2")).is_err());
}
#[test]
fn the_axis_threshold_fires_exactly_at_the_trained_span() {
assert!(validate_generation_dimensions(2048, 512, Some("ltx2")).is_ok());
assert!(validate_generation_dimensions(2080, 512, Some("ltx2")).is_err());
let composed = Ltx2SpatialComposition::TiledTwoStage;
assert!(validate_generation_dimensions_composed(2080, 512, Some("ltx2"), composed).is_ok());
assert!(
validate_generation_dimensions_composed(4096, 2176, Some("ltx2"), composed).is_ok()
);
assert!(
validate_generation_dimensions_composed(4128, 2176, Some("ltx2"), composed).is_err(),
"past 4096 the halved stage-1 shape is itself out of distribution"
);
}
#[test]
fn the_composed_ceiling_is_where_stage_one_leaves_the_trained_span() {
assert_eq!(LTX2_COMPOSED_MAX_AXIS_PIXELS, 2 * LTX2_MAX_AXIS_PIXELS);
let widest = Ltx2OutputRung {
id: "test",
label: "test",
width: LTX2_COMPOSED_MAX_AXIS_PIXELS,
height: 2_176,
};
assert_eq!(widest.stage1_shape().0, LTX2_MAX_AXIS_PIXELS);
let too_wide = Ltx2OutputRung {
id: "test",
label: "test",
width: LTX2_COMPOSED_MAX_AXIS_PIXELS + 64,
height: 2_176,
};
assert!(too_wide.stage1_shape().0 > LTX2_MAX_AXIS_PIXELS);
}
#[test]
fn the_composed_ceiling_requires_a_checkpoint_that_can_compose() {
assert_eq!(
ltx2_spatial_composition("ltx-2-19b-distilled:fp8", None),
Ltx2SpatialComposition::TiledTwoStage
);
assert_eq!(
ltx2_spatial_composition("cv:3143864", None),
Ltx2SpatialComposition::SinglePass
);
for mode in [
Ltx2PipelineMode::OneStage,
Ltx2PipelineMode::Retake,
Ltx2PipelineMode::LipDub,
] {
assert_eq!(
ltx2_spatial_composition("ltx-2-19b-distilled:fp8", Some(mode)),
Ltx2SpatialComposition::SinglePass,
"{mode} denoises once and cannot hold an oversized axis"
);
}
for mode in Ltx2PipelineMode::ALL
.iter()
.filter(|m| m.refines_spatially())
{
assert_eq!(
ltx2_spatial_composition("ltx-2-19b-distilled:fp8", Some(*mode)),
Ltx2SpatialComposition::TiledTwoStage,
"{mode} refines a halved stage 1 and can hold one"
);
}
}
#[test]
fn a_4k_request_is_admitted_only_where_the_composition_exists() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.width = 3_840;
req.height = 2_176;
req.frames = Some(25);
req.fps = Some(24);
req.output_format = Some(OutputFormat::Mp4);
validate_generate_request_with_family(&req, Some("ltx2"))
.expect("a composing checkpoint reaches 4K UHD");
req.model = "cv:3143864".to_string();
let err = validate_generate_request_with_family(&req, Some("ltx2"))
.expect_err("a one-stage checkpoint cannot");
assert!(
err.contains("3840") && err.contains("spatial upsampler"),
"the refusal must name the axis and the way out, got: {err}"
);
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.pipeline = Some(Ltx2PipelineMode::OneStage);
assert!(validate_generate_request_with_family(&req, Some("ltx2")).is_err());
}
#[test]
fn every_composed_rung_is_admissible_exactly_under_composition() {
let two_stage = Ltx2SpatialComposition::TiledTwoStage;
for (width, height) in recommended_dimensions_composed("ltx2", two_stage) {
assert!(
validate_generation_dimensions_composed(width, height, Some("ltx2"), two_stage)
.is_ok(),
"advertised composed preset {width}x{height} must be admissible"
);
}
for rung in LTX2_OUTPUT_RUNGS {
let (width, height) = (rung.width, rung.height);
assert!(
width.is_multiple_of(LTX2_TWO_STAGE_ALIGNMENT)
&& height.is_multiple_of(LTX2_TWO_STAGE_ALIGNMENT),
"{width}x{height} must survive halving onto the 32px latent grid"
);
if !rung.requires_tiled_stage2() {
continue;
}
for shape in [(width, height), (height, width)] {
assert!(
validate_generation_dimensions(shape.0, shape.1, Some("ltx2")).is_err(),
"{}x{} must not be offered to a single-pass checkpoint",
shape.0,
shape.1
);
assert!(
recommended_dimensions_composed("ltx2", two_stage).contains(&shape),
"{}x{} must be advertised to a composing checkpoint",
shape.0,
shape.1
);
}
}
assert_eq!(
recommended_dimensions_composed("ltx2", Ltx2SpatialComposition::SinglePass),
recommended_dimensions("ltx2").to_vec()
);
}
#[test]
fn rung_composition_arithmetic_is_exact() {
struct ExpectedRung {
id: &'static str,
stage1: (u32, u32),
tiles: (u32, u32),
tiled: bool,
}
let expected = [
ExpectedRung {
id: "720p",
stage1: (640, 352),
tiles: (1, 1),
tiled: false,
},
ExpectedRung {
id: "1080p",
stage1: (960, 544),
tiles: (1, 1),
tiled: false,
},
ExpectedRung {
id: "1440p",
stage1: (1_280, 704),
tiles: (2, 1),
tiled: true,
},
ExpectedRung {
id: "4k-uhd",
stage1: (1_920, 1_056),
tiles: (2, 2),
tiled: true,
},
];
assert_eq!(LTX2_OUTPUT_RUNGS.len(), expected.len());
for (
rung,
ExpectedRung {
id,
stage1,
tiles,
tiled,
},
) in LTX2_OUTPUT_RUNGS.iter().zip(&expected)
{
let (id, stage1, tiles, tiled) = (*id, *stage1, *tiles, *tiled);
assert_eq!(rung.id, id);
assert_eq!(rung.stage1_shape(), stage1, "{id} stage-1 shape");
assert_eq!(rung.stage2_tiles(), tiles, "{id} stage-2 tile counts");
assert_eq!(rung.requires_tiled_stage2(), tiled, "{id} tiling need");
assert!(validate_generation_dimensions_composed(
rung.width,
rung.height,
Some("ltx2"),
Ltx2SpatialComposition::TiledTwoStage,
)
.is_ok());
}
}
#[test]
fn a_smaller_spatial_rung_lowers_the_ceiling_it_can_reach() {
assert_eq!(
ltx2_composed_axis_ceiling(Some(Ltx2SpatialUpscale::X2)),
LTX2_COMPOSED_MAX_AXIS_PIXELS
);
assert_eq!(
ltx2_composed_axis_ceiling(None),
LTX2_COMPOSED_MAX_AXIS_PIXELS
);
assert_eq!(
ltx2_composed_axis_ceiling(Some(Ltx2SpatialUpscale::X1_5)),
3_072
);
for upscale in [Some(Ltx2SpatialUpscale::X2), Some(Ltx2SpatialUpscale::X1_5)] {
let ceiling = ltx2_composed_axis_ceiling(upscale);
assert!(
ltx2_stage1_axis_for(ceiling, upscale) <= LTX2_MAX_AXIS_PIXELS,
"{upscale:?} must reach its own ceiling"
);
assert!(
ltx2_stage1_axis_for(ceiling + LTX2_SPATIAL_LATENT_STRIDE, upscale)
> LTX2_MAX_AXIS_PIXELS,
"{upscale:?} must not reach one grid step past it"
);
}
let err = validate_ltx2_stage1_span(3_840, 2_176, Some(Ltx2SpatialUpscale::X1_5))
.expect_err("x1.5 cannot halve 3840 back inside the span");
assert!(err.contains("2560") && err.contains("3072"), "got: {err}");
assert!(validate_ltx2_stage1_span(3_840, 2_176, Some(Ltx2SpatialUpscale::X2)).is_ok());
assert!(validate_ltx2_stage1_span(3_072, 1_728, Some(Ltx2SpatialUpscale::X1_5)).is_ok());
}
#[test]
fn an_implicit_retake_is_admitted_as_single_pass() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.width = 3_840;
req.height = 2_176;
req.frames = Some(25);
req.fps = Some(24);
req.output_format = Some(OutputFormat::Mp4);
assert_eq!(
ltx2_spatial_composition_for_request(&req),
Ltx2SpatialComposition::TiledTwoStage
);
validate_generate_request_with_family(&req, Some("ltx2")).expect("4K composes");
req.retake_range = Some(crate::TimeRange {
start_seconds: 0.0,
end_seconds: 0.5,
});
req.source_video_path = Some("/tmp/clip.mp4".to_string());
assert_eq!(
ltx2_spatial_composition_for_request(&req),
Ltx2SpatialComposition::SinglePass
);
let err = validate_generate_request_with_family(&req, Some("ltx2"))
.expect_err("a retake denoises once and cannot hold a 3840px axis");
assert!(err.contains("3840"), "got: {err}");
}
#[test]
fn implicit_refining_pipelines_keep_the_composed_ceiling() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.width = 3_840;
req.height = 2_176;
req.frames = Some(25);
req.fps = Some(24);
req.output_format = Some(OutputFormat::Mp4);
let mut with_audio = req.clone();
with_audio.audio_file_path = Some("/tmp/voice.wav".to_string());
assert_eq!(
ltx2_spatial_composition_for_request(&with_audio),
Ltx2SpatialComposition::TiledTwoStage
);
let mut with_source = req.clone();
with_source.source_video_path = Some("/tmp/clip.mp4".to_string());
assert_eq!(
ltx2_spatial_composition_for_request(&with_source),
Ltx2SpatialComposition::TiledTwoStage
);
let mut explicit = with_source.clone();
explicit.pipeline = Some(Ltx2PipelineMode::OneStage);
assert_eq!(
ltx2_spatial_composition_for_request(&explicit),
Ltx2SpatialComposition::SinglePass
);
}
#[test]
fn rungs_resolve_in_either_orientation() {
assert_eq!(ltx2_output_rung(3_840, 2_112).map(|r| r.id), Some("4k-uhd"));
assert_eq!(ltx2_output_rung(2_112, 3_840).map(|r| r.id), Some("4k-uhd"));
assert_eq!(ltx2_output_rung(1_920, 1_088).map(|r| r.id), Some("1080p"));
assert_eq!(ltx2_output_rung(1_234, 567), None);
}
#[test]
fn an_oversize_rejection_names_the_largest_reachable_rung() {
assert_eq!(
largest_ltx2_rung_within(LTX2_MAX_AXIS_PIXELS).map(|rung| rung.id),
Some("1080p"),
);
assert_eq!(
largest_ltx2_rung_within(LTX2_COMPOSED_MAX_AXIS_PIXELS).map(|rung| rung.id),
Some("4k-uhd"),
);
assert_eq!(largest_ltx2_rung_within(64), None);
let err = validate_generation_dimensions(3_840, 2_112, Some("ltx2"))
.expect_err("a single-pass render cannot reach 4K");
assert!(err.contains("spatial upsampler"), "got: {err}");
assert!(err.contains("1080p Full HD (1920x1088)"), "got: {err}");
let err = validate_generation_dimensions_composed(
4_160,
2_176,
Some("ltx2"),
Ltx2SpatialComposition::TiledTwoStage,
)
.expect_err("past the composed ceiling");
assert!(
!err.contains("spatial upsampler"),
"a composing render is already using it, got: {err}"
);
assert!(err.contains("4K UHD (3840x2112)"), "got: {err}");
}
#[test]
fn ltx2_offers_portrait_presets() {
let presets = recommended_dimensions("ltx2");
assert!(
presets.contains(&(704, 1216)),
"704x1216 portrait must be advertised, got: {presets:?}"
);
assert!(
presets.iter().any(|(w, h)| h > w && w * h > 1_000_000),
"a high-resolution portrait preset must be advertised, got: {presets:?}"
);
}
#[test]
fn ltx2_grid_snapped_cap_is_actually_requestable() {
for fps in [6, 12, 24, 30, 48, 60, 120] {
let cap = ltx2_max_frames_on_grid_at_fps(fps);
assert_eq!(
(cap - 1) % 8,
0,
"the advertised cap at {fps} fps must sit on the 8n+1 grid"
);
assert!(cap <= ltx2_max_frames_at_fps(fps));
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.width = 768;
req.height = 512;
req.output_format = Some(OutputFormat::Mp4);
req.frames = Some(cap);
req.fps = Some(fps);
validate_generate_request_with_family(&req, Some("ltx2")).unwrap_or_else(|err| {
panic!("the advertised cap {cap} at {fps} fps must validate, got: {err}")
});
}
assert_eq!(ltx2_max_frames_at_fps(24), 484);
assert_eq!(ltx2_max_frames_on_grid_at_fps(24), 481);
assert_eq!(ltx2_max_frames_at_fps(48), LTX2_MAX_FRAMES_ABSOLUTE);
assert_eq!(ltx2_max_frames_on_grid_at_fps(48), 601);
}
#[test]
fn exr_output_requires_the_hdr_adapter() {
let mut req = valid_req();
req.model = "ltx-2.3-22b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.hdr_exr_dir = Some("/tmp/shot_exr".to_string());
let err = validate_generate_request_with_family(&req, Some("ltx2"))
.expect_err("EXR without the HDR adapter must be rejected");
assert!(err.contains("ic_lora_control=hdr"), "got: {err}");
req.ic_lora_control = Some("hdr".to_string());
req.pipeline = Some(Ltx2PipelineMode::IcLora);
req.source_video_path = Some("/tmp/reference.mp4".to_string());
req.loras = Some(vec![LoraWeight {
path: "/models/hdr.safetensors".to_string(),
scale: 1.0,
expert: None,
}]);
validate_generate_request_with_family(&req, Some("ltx2"))
.expect("the HDR adapter makes EXR output valid");
}
#[test]
fn exr_output_rejects_extend_directly() {
let mut req = valid_req();
req.model = "ltx-2.3-22b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.hdr_exr_dir = Some("/tmp/shot_exr".to_string());
req.extend_video_path = Some("/tmp/base.mp4".to_string());
let err = validate_generate_request_with_family(&req, Some("ltx2"))
.expect_err("EXR + extend must be rejected");
assert!(err.contains("extend_video"), "got: {err}");
}
#[test]
fn exr_options_are_rejected_for_non_ltx2_families() {
let mut req = valid_req();
req.hdr_exr_dir = Some("/tmp/shot_exr".to_string());
assert!(validate_generate_request_with_family(&req, Some("flux")).is_err());
}
#[test]
fn exr_precision_without_an_output_directory_is_rejected() {
let mut req = valid_req();
req.model = "ltx-2.3-22b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.hdr_exr_full_float = true;
let err = validate_generate_request_with_family(&req, Some("ltx2"))
.expect_err("a precision knob with nothing to write is a mistake");
assert!(err.contains("hdr_exr_dir"), "got: {err}");
}
#[test]
fn exr_accepts_any_spelling_the_control_registry_accepts() {
for spelling in ["hdr", "HDR", " Hdr ", "\tHDR\n"] {
let mut req = valid_req();
req.model = "ltx-2.3-22b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.source_video_path = Some("/tmp/reference.mp4".to_string());
req.pipeline = Some(Ltx2PipelineMode::IcLora);
req.ic_lora_control = Some(spelling.to_string());
req.hdr_exr_dir = Some("/tmp/shot_exr".to_string());
let result = validate_generate_request_with_family(&req, Some("ltx2"));
assert!(
result.is_ok(),
"spelling {spelling:?} must be accepted, got: {result:?}"
);
}
}
#[test]
fn exr_still_rejects_a_different_control() {
let mut req = valid_req();
req.model = "ltx-2.3-22b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.source_video_path = Some("/tmp/reference.mp4".to_string());
req.pipeline = Some(Ltx2PipelineMode::IcLora);
req.ic_lora_control = Some("union".to_string());
req.hdr_exr_dir = Some("/tmp/shot_exr".to_string());
let err = validate_generate_request_with_family(&req, Some("ltx2"))
.expect_err("only the HDR adapter produces a LogC3 signal");
assert!(err.contains("ic_lora_control=hdr"), "got: {err}");
}
#[test]
fn saved_metadata_records_where_the_exr_sequence_went() {
let mut req = valid_req();
req.model = "ltx-2.3-22b-distilled:fp8".to_string();
req.ic_lora_control = Some("hdr".to_string());
req.hdr_exr_dir = Some("/tmp/shot_exr".to_string());
req.hdr_exr_full_float = true;
let metadata = crate::OutputMetadata::from_generate_request(&req, 7, None, "test");
assert_eq!(metadata.hdr_exr_dir.as_deref(), Some("/tmp/shot_exr"));
assert!(metadata.hdr_exr_full_float);
let round_tripped: crate::OutputMetadata =
serde_json::from_str(&serde_json::to_string(&metadata).unwrap()).unwrap();
assert_eq!(round_tripped.hdr_exr_dir.as_deref(), Some("/tmp/shot_exr"));
assert!(round_tripped.hdr_exr_full_float);
}
#[test]
fn a_non_hdr_render_serializes_no_exr_fields() {
let metadata = crate::OutputMetadata::from_generate_request(&valid_req(), 7, None, "test");
let json = serde_json::to_string(&metadata).unwrap();
assert!(!json.contains("hdr_exr"), "got: {json}");
}
fn valid_req() -> GenerateRequest {
GenerateRequest {
source_fit: None,
hdr_exr_dir: None,
hdr_exr_full_float: false,
guidance_overrides: None,
sample_shift: None,
distill_strength_high: None,
distill_strength_low: None,
prompt: "a red apple".to_string(),
negative_prompt: None,
model: "test-model".to_string(),
width: 1024,
height: 1024,
steps: 4,
guidance: 0.0,
seed: Some(42),
batch_size: 1,
output_format: Some(OutputFormat::Png),
embed_metadata: None,
scheduler: None,
cfg_plus: None,
source_image: None,
source_image_name: None,
edit_images: None,
references: None,
strength: 0.75,
mask_image: None,
control_image: None,
control_model: None,
control_scale: 1.0,
expand: None,
original_prompt: None,
prompt_transform: None,
batch_id: None,
batch_index: None,
batch_count: None,
lora: None,
frames: None,
fps: None,
upscale_model: None,
gif_preview: false,
enable_audio: None,
audio_file: None,
audio_file_path: None,
source_video: None,
source_video_path: None,
extend_video: None,
extend_video_path: None,
extend_overlap_frames: None,
keyframes: None,
pipeline: None,
ic_lora_control: None,
loras: None,
retake_range: None,
spatial_upscale: None,
temporal_upscale: None,
placement: None,
}
}
#[test]
fn generation_rejects_compliance_gated_model_identity_before_other_validation() {
let mut req = valid_req();
req.model = "hf:MiniMaxAI/MiniMax-H3".to_string();
req.prompt.clear();
let error = validate_generate_request_with_family(&req, None).unwrap_err();
assert!(error.contains(crate::MINIMAX_H3_AUTHORIZATION_REQUIRED));
assert!(!error.contains(&req.model));
}
#[test]
fn generation_rejects_opaque_catalog_id_with_compliance_gated_family() {
let mut req = valid_req();
req.model = "cv:42".to_string();
let error = validate_generate_request_with_family(&req, Some("minimax-h3")).unwrap_err();
assert!(error.contains(crate::MINIMAX_H3_AUTHORIZATION_REQUIRED));
}
fn valid_h3_request(model: &str) -> GenerateRequest {
let mut req = valid_req();
req.model = model.to_string();
req.width = crate::minimax_h3::DEFAULT_WIDTH;
req.height = crate::minimax_h3::DEFAULT_HEIGHT;
req.steps = crate::minimax_h3::DEFAULT_STEPS;
req.frames = Some(crate::minimax_h3::MIN_FRAMES);
req.fps = Some(crate::minimax_h3::FIXED_FPS);
req.output_format = Some(OutputFormat::Mp4);
req.enable_audio = Some(true);
req.strength = 1.0;
req
}
#[cfg(any(feature = "h3", feature = "h3-private-uat"))]
#[test]
fn private_h3_validation_bypasses_only_activation_for_exact_reviewed_models() {
let req = valid_h3_request(crate::minimax_h3::FL2VA_COMFY);
let public_error =
validate_generate_request_with_family(&req, Some(crate::minimax_h3::FAMILY))
.unwrap_err();
assert!(public_error.contains(crate::MINIMAX_H3_AUTHORIZATION_REQUIRED));
validate_h3_private_uat_request(&req).unwrap();
let mut official = req;
official.model = crate::minimax_h3::FL2VA_OFFICIAL.to_string();
assert!(validate_h3_private_uat_request(&official)
.unwrap_err()
.contains("exact reviewed task model"));
}
#[test]
fn h3_post_activation_fl2va_accepts_first_and_last_boundary_frames() {
let mut req = valid_h3_request(crate::minimax_h3::FL2VA_COMFY);
req.source_image = Some(png_bytes());
req.keyframes = Some(vec![crate::KeyframeCondition {
frame: crate::minimax_h3::MIN_FRAMES - 1,
image: jpeg_bytes(),
name: None,
}]);
assert!(
validate_generate_request_after_activation(&req, Some(crate::minimax_h3::FAMILY),)
.is_ok()
);
}
#[test]
fn h3_post_activation_ref2va_accepts_image_references() {
let mut req = valid_h3_request(crate::minimax_h3::REF2VA_COMFY);
req.references = Some(vec![crate::GenerationReference::Image {
media: crate::GenerationReferenceAuthority::Inline { data: png_bytes() },
provenance: crate::GenerationReferenceProvenance {
name: Some("reference.png".to_string()),
sha256: None,
},
mime_type: "image/png".to_string(),
width: 1920,
height: 1080,
}]);
assert!(
validate_generate_request_after_activation(&req, Some(crate::minimax_h3::FAMILY),)
.is_ok()
);
}
#[test]
fn h3_post_activation_rejects_non_boundary_keyframes() {
let mut req = valid_h3_request(crate::minimax_h3::FL2VA_COMFY);
req.keyframes = Some(vec![crate::KeyframeCondition {
frame: 17,
image: png_bytes(),
name: None,
}]);
let error =
validate_generate_request_after_activation(&req, Some(crate::minimax_h3::FAMILY))
.unwrap_err();
assert!(
error.contains("only frame 0 or final frame"),
"got: {error}"
);
}
#[test]
fn h3_post_activation_rejects_generic_scheduler_and_lora_overrides() {
let mut req = valid_h3_request(crate::minimax_h3::FL2VA_COMFY);
req.scheduler = Some(crate::Scheduler::UniPc);
let error =
validate_generate_request_after_activation(&req, Some(crate::minimax_h3::FAMILY))
.unwrap_err();
assert!(error.contains("scheduler overrides"), "got: {error}");
req.scheduler = None;
req.lora = Some(crate::LoraWeight {
path: "/tmp/adapter.safetensors".to_string(),
scale: 1.0,
expert: None,
});
let error =
validate_generate_request_after_activation(&req, Some(crate::minimax_h3::FAMILY))
.unwrap_err();
assert!(error.contains("does not support LoRA"), "got: {error}");
req.lora = None;
req.loras = Some(Vec::new());
let error =
validate_generate_request_after_activation(&req, Some(crate::minimax_h3::FAMILY))
.unwrap_err();
assert!(error.contains("does not support LoRA"), "got: {error}");
}
#[test]
fn h3_post_activation_preserves_source_and_extend_invariants() {
for strength in [-1.0, 1.01, f64::NAN] {
let mut req = valid_h3_request(crate::minimax_h3::FL2VA_COMFY);
req.source_image = Some(png_bytes());
req.strength = strength;
let error =
validate_generate_request_after_activation(&req, Some(crate::minimax_h3::FAMILY))
.unwrap_err();
assert!(error.contains("finite value in range"), "got: {error}");
}
let mut req = valid_h3_request(crate::minimax_h3::FL2VA_COMFY);
req.extend_overlap_frames = Some(9);
let error =
validate_generate_request_after_activation(&req, Some(crate::minimax_h3::FAMILY))
.unwrap_err();
assert!(
error.contains("extend_overlap_frames requires extend_video"),
"got: {error}"
);
}
#[test]
fn h3_post_activation_rejects_empty_conditioning_collections() {
let mut req = valid_h3_request(crate::minimax_h3::FL2VA_COMFY);
req.edit_images = Some(Vec::new());
let error =
validate_generate_request_after_activation(&req, Some(crate::minimax_h3::FAMILY))
.unwrap_err();
assert!(
error.contains("edit_images must not be empty"),
"got: {error}"
);
req.edit_images = None;
req.keyframes = Some(Vec::new());
let error =
validate_generate_request_after_activation(&req, Some(crate::minimax_h3::FAMILY))
.unwrap_err();
assert!(
error.contains("keyframes must not be empty"),
"got: {error}"
);
}
#[test]
fn generation_model_preflight_gates_nested_identities_and_artifacts() {
let root = std::path::Path::new("/Volumes/ExternalStorage/mold-uat/minimax-h3/models");
let mut req = valid_req();
req.control_model = Some("MiniMax-H3-FL2VA".to_string());
assert!(require_generate_request_model_activation(&req, Some(root), Some("flux")).is_err());
req.control_model = None;
req.upscale_model = Some("hf:MiniMaxAI/MiniMax-H3".to_string());
assert!(require_generate_request_model_activation(&req, Some(root), Some("flux")).is_err());
req.upscale_model = None;
req.lora = Some(crate::LoraWeight {
path: root
.join("custom/MiniMax-H3/adapter.safetensors")
.to_string_lossy()
.into_owned(),
scale: 1.0,
expert: None,
});
assert!(require_generate_request_model_activation(&req, Some(root), Some("flux")).is_err());
req.lora.as_mut().unwrap().path = root
.join("flux/ordinary-adapter.safetensors")
.to_string_lossy()
.into_owned();
assert!(require_generate_request_model_activation(&req, Some(root), Some("flux")).is_ok());
}
fn png_bytes() -> Vec<u8> {
vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
}
fn jpeg_bytes() -> Vec<u8> {
vec![0xFF, 0xD8, 0xFF, 0xE0]
}
#[test]
fn clamp_noop_within_limit() {
assert_eq!(super::clamp_to_megapixel_limit(1024, 1024), (1024, 1024));
}
#[test]
fn clamp_noop_qwen_image_native_resolution() {
assert_eq!(super::clamp_to_megapixel_limit(1328, 1328), (1328, 1328));
}
#[test]
fn clamp_noop_qwen_image_landscape() {
assert_eq!(super::clamp_to_megapixel_limit(1664, 928), (1664, 928));
}
#[test]
fn clamp_downscales_oversized() {
let (w, h) = super::clamp_to_megapixel_limit(1888, 1168);
assert!(w % 16 == 0 && h % 16 == 0, "must be multiples of 16");
let pixels = w as u64 * h as u64;
assert!(
pixels <= super::MAX_PIXELS,
"must be within limit: {pixels}"
);
let orig_ratio = 1888.0 / 1168.0;
let new_ratio = w as f64 / h as f64;
assert!(
(orig_ratio - new_ratio).abs() < 0.05,
"aspect ratio drift too large"
);
}
#[test]
fn clamp_large_square() {
let (w, h) = super::clamp_to_megapixel_limit(2048, 2048);
assert!(w % 16 == 0 && h % 16 == 0);
assert!(w as u64 * h as u64 <= super::MAX_PIXELS);
}
#[test]
fn clamp_extreme_aspect_ratio() {
let (w, h) = super::clamp_to_megapixel_limit(4096, 256);
assert!(w % 16 == 0 && h % 16 == 0);
assert!(w as u64 * h as u64 <= super::MAX_PIXELS);
assert!(w > h, "should remain landscape");
}
#[test]
fn wan_surface_parity_fixture_pins_the_core_contracts() {
let fixture: serde_json::Value = serde_json::from_str(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tests/fixtures/wan/surface-parity-v1.json"
)))
.expect("fixture parses");
let mp4_families: Vec<&str> = fixture["container_default"]["mp4_default_families"]
.as_array()
.expect("mp4_default_families")
.iter()
.map(|value| value.as_str().expect("family string"))
.collect();
for family in &mp4_families {
assert!(
crate::family_output_defaults_to_mp4(family),
"{family} must default to mp4"
);
}
for family in ["flux", "sdxl", "qwen-image", "z-image", ""] {
assert!(
!crate::family_output_defaults_to_mp4(family),
"{family:?} must not default to mp4"
);
}
let mut req = valid_req();
req.model = "wan22-t2v-a14b:q8".to_string();
req.frames = Some(81);
req.output_format = None;
req.normalise_output_format(Some(fixture["family"].as_str().unwrap()));
assert_eq!(
format!("{:?}", req.resolved_output_format()).to_lowercase(),
fixture["container_default"]["unset_multi_frame"]
.as_str()
.unwrap()
);
let mut still = valid_req();
still.model = "wan22-t2v-a14b:q8".to_string();
still.frames = Some(1);
still.output_format = None;
still.normalise_output_format(Some("wan"));
assert_eq!(
format!("{:?}", still.resolved_output_format()).to_lowercase(),
fixture["container_default"]["wan_single_frame"]
.as_str()
.unwrap()
);
assert_eq!(
u64::from(WAN_TEMPORAL_SCALE),
fixture["frame_grid"]["step"].as_u64().unwrap()
);
assert_eq!(
u64::from(frame_offset_for_family("wan").expect("wan has a grid")),
fixture["frame_grid"]["offset"].as_u64().unwrap()
);
assert_eq!(
u64::from(WAN_TI2V_FLF_MIN_FRAMES),
fixture["first_last_frame"]["ti2v_min_frames"]
.as_u64()
.unwrap()
);
assert!(fixture["first_last_frame"]["ti2v_model_prefix"]
.as_str()
.unwrap()
.starts_with("wan22-ti2v-5b"));
}
#[test]
fn normalise_output_format_unset_for_ltx2_picks_mp4() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = None;
req.normalise_output_format(Some("ltx2"));
assert_eq!(
req.resolved_output_format(),
OutputFormat::Mp4,
"ltx2 with no explicit format should default to mp4"
);
}
#[test]
fn normalise_output_format_unset_for_ltx2_with_audio_picks_mp4() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = None;
req.enable_audio = Some(true);
req.normalise_output_format(Some("ltx2"));
assert_eq!(
req.resolved_output_format(),
OutputFormat::Mp4,
"ltx2 with audio and no explicit format should default to mp4"
);
}
#[test]
fn normalise_output_format_unset_for_ltx_video_picks_mp4() {
let mut req = valid_req();
req.model = "ltx-video:fp16".to_string();
req.output_format = None;
req.normalise_output_format(Some("ltx-video"));
assert_eq!(
req.resolved_output_format(),
OutputFormat::Mp4,
"ltx-video with no explicit format should default to mp4"
);
}
#[test]
fn normalise_output_format_unset_for_flux_picks_png() {
let mut req = valid_req();
req.model = "flux-schnell:q8".to_string();
req.output_format = None;
req.normalise_output_format(Some("flux"));
assert_eq!(
req.resolved_output_format(),
OutputFormat::Png,
"flux with no explicit format should default to png"
);
}
#[test]
fn normalise_output_format_explicit_png_for_ltx2_remains_png_and_validation_rejects_it() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Png);
req.normalise_output_format(Some("ltx2"));
assert_eq!(req.output_format, Some(OutputFormat::Png));
let err = validate_generate_request(&req).unwrap_err();
assert!(
err.contains("LTX-2 outputs must use"),
"expected validation error for explicit png on ltx2, got: {err}"
);
}
#[test]
fn valid_request_passes() {
assert!(validate_generate_request(&valid_req()).is_ok());
}
#[test]
fn ltx2_audio_requires_mp4() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Gif);
req.enable_audio = Some(true);
assert!(validate_generate_request(&req).unwrap_err().contains("mp4"));
}
#[test]
fn ltx2_t2a_requires_wav_output_and_wav_requires_t2a() {
let mut req = valid_req();
req.model = "ltx-2.3-22b-dev:fp8".to_string();
req.pipeline = Some(Ltx2PipelineMode::T2a);
req.output_format = Some(OutputFormat::Wav);
req.width = 0;
req.height = 0;
assert!(validate_generate_request(&req).is_ok());
req.output_format = Some(OutputFormat::Mp4);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("audio only"), "got: {err}");
req.pipeline = None;
req.output_format = Some(OutputFormat::Wav);
req.width = 1024;
req.height = 1024;
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("pipeline=t2a"), "got: {err}");
}
#[test]
fn ltx2_t2a_is_dimensionless_and_ignores_a_legacy_raster_canvas() {
let mut req = valid_req();
req.model = "ltx-2.3-22b-dev:fp8".to_string();
req.pipeline = Some(Ltx2PipelineMode::T2a);
req.output_format = Some(OutputFormat::Wav);
req.width = 0;
req.height = 0;
validate_generate_request(&req).unwrap();
req.width = 1024;
req.height = 576;
validate_generate_request(&req).unwrap();
}
#[test]
fn ltx2_t2a_rejects_every_conditioning_input() {
let base = || {
let mut req = valid_req();
req.model = "ltx-2.3-22b-dev:fp8".to_string();
req.pipeline = Some(Ltx2PipelineMode::T2a);
req.output_format = Some(OutputFormat::Wav);
req.width = 0;
req.height = 0;
req
};
let mut with_image = base();
with_image.source_image = Some(vec![1, 2, 3]);
assert!(validate_generate_request(&with_image)
.unwrap_err()
.contains("source_image"));
let mut with_audio = base();
with_audio.audio_file_path = Some("/srv/voice.wav".to_string());
assert!(validate_generate_request(&with_audio)
.unwrap_err()
.contains("audio_file_path"));
let mut with_upscale = base();
with_upscale.spatial_upscale = Some(crate::Ltx2SpatialUpscale::X2);
assert!(validate_generate_request(&with_upscale)
.unwrap_err()
.contains("spatial_upscale"));
let mut with_post_upscale = base();
with_post_upscale.upscale_model = Some("real-esrgan-x4plus:fp16".to_string());
assert!(validate_generate_request(&with_post_upscale)
.unwrap_err()
.contains("upscale_model"));
}
#[test]
fn ltx2_t2a_cannot_carry_controlnet_inputs() {
let mut req = valid_req();
req.model = "ltx-2.3-22b-dev:fp8".to_string();
req.pipeline = Some(Ltx2PipelineMode::T2a);
req.output_format = Some(OutputFormat::Wav);
req.width = 0;
req.height = 0;
req.control_image = Some(png_bytes());
req.control_model = Some("controlnet-canny-sd15".to_string());
req.control_scale = 0.8;
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("ControlNet"), "got: {err}");
req.control_image = None;
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("ControlNet"), "got: {err}");
}
#[test]
fn ltx2_t2a_rejects_enable_audio_false() {
let mut req = valid_req();
req.model = "ltx-2.3-22b-dev:fp8".to_string();
req.pipeline = Some(Ltx2PipelineMode::T2a);
req.output_format = Some(OutputFormat::Wav);
req.width = 0;
req.height = 0;
req.enable_audio = Some(false);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("enable_audio=false"), "got: {err}");
}
#[test]
fn ltx2_t2a_rejects_non_unit_modality_scale_override() {
let mut req = valid_req();
req.model = "ltx-2.3-22b-dev:fp8".to_string();
req.pipeline = Some(Ltx2PipelineMode::T2a);
req.output_format = Some(OutputFormat::Wav);
req.width = 0;
req.height = 0;
req.guidance_overrides = Some(crate::Ltx2GuidanceOverrides {
modality_scale: Some(3.0),
..Default::default()
});
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("modality_scale"), "got: {err}");
req.guidance_overrides = Some(crate::Ltx2GuidanceOverrides {
modality_scale: Some(1.0),
..Default::default()
});
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn ltx2_retake_requires_source_video() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.retake_range = Some(crate::TimeRange {
start_seconds: 0.0,
end_seconds: 1.0,
});
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("source_video"));
}
#[test]
fn ltx2_audio_file_rejects_inline_payloads_above_limit() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.audio_file = Some(vec![0; MAX_INLINE_AUDIO_BYTES + 1]);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("audio_file exceeds"), "got: {err}");
assert!(err.contains("64 MiB"), "got: {err}");
}
#[test]
fn ltx2_source_video_rejects_inline_payloads_above_limit() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.source_video = Some(vec![0; MAX_INLINE_SOURCE_VIDEO_BYTES + 1]);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("source_video exceeds"), "got: {err}");
assert!(err.contains("64 MiB"), "got: {err}");
}
#[test]
fn ltx2_audio_file_path_is_family_gated_and_preserves_inline_limit() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.audio_file_path = Some("/srv/mold-media/voice.wav".to_string());
assert!(validate_generate_request(&req).is_ok());
req.audio_file = Some(vec![0; MAX_INLINE_AUDIO_BYTES + 1]);
let err = validate_generate_request(&req).unwrap_err();
assert!(
err.contains("audio_file_path cannot be combined"),
"got: {err}"
);
let mut wrong_family = valid_req();
wrong_family.model = "flux-schnell:q8".to_string();
wrong_family.audio_file_path = Some("/srv/mold-media/voice.wav".to_string());
let err = validate_generate_request(&wrong_family).unwrap_err();
assert!(
err.contains("audio_file_path is only supported"),
"got: {err}"
);
}
#[test]
fn ltx2_source_video_path_satisfies_retake_requirements() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.source_video_path = Some("/srv/mold-media/clip.mp4".to_string());
req.retake_range = Some(crate::TimeRange {
start_seconds: 0.0,
end_seconds: 1.0,
});
assert!(validate_generate_request(&req).is_ok());
req.source_video = Some(vec![0; MAX_INLINE_SOURCE_VIDEO_BYTES + 1]);
let err = validate_generate_request(&req).unwrap_err();
assert!(
err.contains("source_video_path cannot be combined"),
"got: {err}"
);
}
#[test]
fn ltx2_keyframe_pipeline_requires_multiple_keyframes() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.pipeline = Some(crate::Ltx2PipelineMode::Keyframe);
req.frames = Some(17);
req.keyframes = Some(vec![crate::KeyframeCondition {
frame: 0,
image: png_bytes(),
name: None,
}]);
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("at least 2 keyframes"));
}
#[test]
fn keyframes_on_unknown_family_report_unknown_model_family() {
let mut req = valid_req();
req.model = "private-ltx2-style-model".to_string();
req.frames = Some(17);
req.keyframes = Some(vec![
crate::KeyframeCondition {
frame: 0,
image: png_bytes(),
name: None,
},
crate::KeyframeCondition {
frame: 16,
image: png_bytes(),
name: None,
},
]);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("unknown model family"), "got: {err}");
}
fn ltx2_req_with_overrides(overrides: Ltx2GuidanceOverrides) -> GenerateRequest {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.frames = Some(17);
req.guidance_overrides = Some(overrides);
req
}
#[test]
fn ltx2_guidance_overrides_accept_upstream_ranges() {
validate_generate_request(<x2_req_with_overrides(Ltx2GuidanceOverrides {
stg_scale: Some(1.5),
stg_blocks: Some(vec![28, 29]),
rescale_scale: Some(0.7),
modality_scale: Some(3.0),
skip_step: Some(2),
}))
.unwrap();
}
#[test]
fn ltx2_guidance_overrides_are_family_gated() {
let mut req = valid_req();
req.guidance_overrides = Some(Ltx2GuidanceOverrides {
stg_scale: Some(1.0),
..Ltx2GuidanceOverrides::default()
});
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("guidance_overrides"), "got: {err}");
assert!(err.contains("LTX-2"), "got: {err}");
}
#[test]
fn ltx2_guidance_overrides_reject_empty_objects() {
let err =
validate_generate_request(<x2_req_with_overrides(Ltx2GuidanceOverrides::default()))
.unwrap_err();
assert!(err.contains("at least one field"), "got: {err}");
}
#[test]
fn ltx2_guidance_overrides_reject_out_of_range_scales() {
let err = validate_generate_request(<x2_req_with_overrides(Ltx2GuidanceOverrides {
stg_scale: Some(-0.5),
..Ltx2GuidanceOverrides::default()
}))
.unwrap_err();
assert!(err.contains("stg_scale"), "got: {err}");
let err = validate_generate_request(<x2_req_with_overrides(Ltx2GuidanceOverrides {
stg_scale: Some(f64::NAN),
..Ltx2GuidanceOverrides::default()
}))
.unwrap_err();
assert!(err.contains("finite"), "got: {err}");
let err = validate_generate_request(<x2_req_with_overrides(Ltx2GuidanceOverrides {
rescale_scale: Some(1.5),
..Ltx2GuidanceOverrides::default()
}))
.unwrap_err();
assert!(err.contains("rescale_scale"), "got: {err}");
validate_generate_request(<x2_req_with_overrides(Ltx2GuidanceOverrides {
modality_scale: Some(1.5),
..Ltx2GuidanceOverrides::default()
}))
.unwrap();
}
#[test]
fn ltx2_guidance_overrides_reject_unusable_stg_blocks() {
let err = validate_generate_request(<x2_req_with_overrides(Ltx2GuidanceOverrides {
stg_blocks: Some(Vec::new()),
..Ltx2GuidanceOverrides::default()
}))
.unwrap_err();
assert!(err.contains("must not be empty"), "got: {err}");
let err = validate_generate_request(<x2_req_with_overrides(Ltx2GuidanceOverrides {
stg_blocks: Some(vec![MAX_STG_BLOCK_INDEX]),
..Ltx2GuidanceOverrides::default()
}))
.unwrap_err();
assert!(err.contains("deepest supported"), "got: {err}");
let err = validate_generate_request(<x2_req_with_overrides(Ltx2GuidanceOverrides {
stg_blocks: Some(vec![29, 29]),
..Ltx2GuidanceOverrides::default()
}))
.unwrap_err();
assert!(err.contains("more than once"), "got: {err}");
}
#[test]
fn ltx2_guidance_overrides_bound_the_skip_stride() {
let err = validate_generate_request(<x2_req_with_overrides(Ltx2GuidanceOverrides {
skip_step: Some(Ltx2GuidanceOverrides::MAX_SKIP_STEP + 1),
..Ltx2GuidanceOverrides::default()
}))
.unwrap_err();
assert!(err.contains("skip_step"), "got: {err}");
}
#[test]
fn enable_audio_some_false_does_not_trip_family_check() {
let mut req = valid_req();
req.model = "cv:2781713".to_string();
req.enable_audio = Some(false);
validate_generate_request(&req).unwrap();
}
#[test]
fn enable_audio_some_true_with_family_hint_passes_for_catalog_ltx2() {
let mut req = valid_req();
req.model = "cv:2781713".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.enable_audio = Some(true);
validate_generate_request_with_family(&req, Some("ltx2")).unwrap();
}
#[test]
fn enable_audio_some_true_without_hint_still_errors_on_unknown_family() {
let mut req = valid_req();
req.model = "cv:2781713".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.enable_audio = Some(true);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("unknown model family"), "got: {err}");
assert!(err.contains("enable_audio"), "got: {err}");
}
#[test]
fn family_hint_overrides_manifest_lookup() {
let mut req = valid_req();
req.model = "private-name".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.enable_audio = Some(true);
validate_generate_request_with_family(&req, Some("ltx2")).unwrap();
}
#[test]
fn ltx2_allows_temporal_upscale_request() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.temporal_upscale = Some(crate::Ltx2TemporalUpscale::X2);
validate_generate_request(&req).unwrap();
}
#[test]
fn ltx2_allows_x1_5_spatial_upscale_request() {
let mut req = valid_req();
req.model = "ltx-2.3-22b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.spatial_upscale = Some(crate::Ltx2SpatialUpscale::X1_5);
validate_generate_request(&req).unwrap();
}
#[test]
fn empty_prompt_rejected() {
let mut req = valid_req();
req.prompt = " ".to_string();
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("prompt"));
}
fn ltx2_video_req() -> GenerateRequest {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.fps = Some(24);
req.frames = Some(97);
req
}
#[test]
fn empty_prompt_allowed_for_ltx2_with_source_image() {
let mut req = ltx2_video_req();
req.prompt = String::new();
req.source_image = Some(png_bytes());
validate_generate_request(&req).unwrap();
req.prompt = " \n ".to_string();
validate_generate_request(&req).unwrap();
let mut catalog = req.clone();
catalog.model = "cv:2781713".to_string();
assert!(validate_generate_request(&catalog).is_err());
validate_generate_request_with_family(&catalog, Some("ltx2")).unwrap();
}
#[test]
fn empty_prompt_allowed_for_ltx2_keyframes_video_and_extend() {
let mut keyframed = ltx2_video_req();
keyframed.prompt = String::new();
keyframed.keyframes = Some(vec![KeyframeCondition {
frame: 0,
image: png_bytes(),
name: None,
}]);
validate_generate_request(&keyframed).unwrap();
let mut from_video = ltx2_video_req();
from_video.prompt = String::new();
from_video.source_video = Some(vec![0, 0, 0, 0x20, b'f', b't', b'y', b'p']);
validate_generate_request(&from_video).unwrap();
let mut from_video_path = ltx2_video_req();
from_video_path.prompt = String::new();
from_video_path.source_video_path = Some("/srv/clips/shot.mp4".to_string());
validate_generate_request(&from_video_path).unwrap();
let mut extended = extend_req();
extended.prompt = String::new();
validate_generate_request(&extended).unwrap();
let mut extended_path = ltx2_video_req();
extended_path.prompt = String::new();
extended_path.extend_video_path = Some("/srv/clips/shot.mp4".to_string());
validate_generate_request(&extended_path).unwrap();
}
#[test]
fn empty_prompt_allowed_for_ltx_video_with_source_image() {
let mut req = valid_req();
req.model = "ltx-video-0.9.8-2b-distilled:bf16".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.prompt = String::new();
req.source_image = Some(png_bytes());
validate_generate_request(&req).unwrap();
}
#[test]
fn empty_prompt_still_rejected_for_ltx2_text_to_video() {
let mut req = ltx2_video_req();
req.prompt = String::new();
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("prompt"));
}
#[test]
fn empty_prompt_still_rejected_for_flux_and_sd() {
for model in [
"flux-dev:q8",
"sd15:fp16",
"sdxl:fp16",
"z-image-turbo:bf16",
] {
let mut req = valid_req();
req.model = model.to_string();
req.prompt = String::new();
req.source_image = Some(png_bytes());
assert!(
validate_generate_request(&req)
.unwrap_err()
.contains("prompt"),
"{model} must still require a prompt"
);
}
}
#[test]
fn prompt_required_predicate_matches_validation() {
let mut req = ltx2_video_req();
assert!(super::prompt_required_for(&req, None));
req.source_image = Some(png_bytes());
assert!(!super::prompt_required_for(&req, None));
let mut catalog = req.clone();
catalog.model = "hf:Lightricks/LTX-2".to_string();
assert!(super::prompt_required_for(&catalog, None));
assert!(!super::prompt_required_for(&catalog, Some("ltx2")));
}
#[test]
fn prompt_length_limit_still_enforced_without_a_prompt_requirement() {
let mut req = ltx2_video_req();
req.source_image = Some(png_bytes());
req.prompt = "a".repeat(77_001);
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("77,000"));
}
#[test]
fn zero_dimensions_rejected() {
let mut req = valid_req();
req.width = 0;
assert!(validate_generate_request(&req).is_err());
req.width = 1024;
req.height = 0;
assert!(validate_generate_request(&req).is_err());
}
#[test]
fn dimensions_must_be_multiple_of_16() {
let mut req = valid_req();
req.width = 513; assert!(validate_generate_request(&req)
.unwrap_err()
.contains("multiples of 16"));
}
#[test]
fn ltx2_dimensions_must_be_multiple_of_32() {
let mut req = valid_req();
req.width = 1008; req.height = 704;
let error = validate_generate_request_with_family(&req, Some("ltx2"))
.expect_err("LTX-2 must reject a 16px-only canvas");
assert!(error.contains("multiples of 32"), "{error}");
assert!(error.contains("ltx2"), "{error}");
}
#[test]
fn ltx2_accepts_custom_32_aligned_dimensions() {
let mut req = valid_req();
req.width = 1056;
req.height = 736;
req.output_format = Some(OutputFormat::Mp4);
assert!(validate_generate_request_with_family(&req, Some("ltx2")).is_ok());
}
#[test]
fn valid_non_square_dimensions() {
let mut req = valid_req();
req.width = 512;
req.height = 768;
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn oversized_image_rejected() {
let mut req = valid_req();
req.width = 1408;
req.height = 1408; assert!(validate_generate_request(&req)
.unwrap_err()
.contains("megapixels"));
}
#[test]
fn oversized_image_error_reports_current_megapixel_limit() {
let mut req = valid_req();
req.width = 1408;
req.height = 1408;
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("1.8MP"), "got: {err}");
}
#[test]
fn zero_steps_rejected() {
let mut req = valid_req();
req.steps = 0;
assert!(validate_generate_request(&req).is_err());
}
#[test]
fn excessive_steps_rejected() {
let mut req = valid_req();
req.steps = 101;
assert!(validate_generate_request(&req).is_err());
}
#[test]
fn valid_step_counts() {
for steps in [1, 4, 20, 28, 50, 100] {
let mut req = valid_req();
req.steps = steps;
assert!(
validate_generate_request(&req).is_ok(),
"steps={steps} should be valid"
);
}
}
#[test]
fn ltx2_frames_must_still_follow_8n_plus_1() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.frames = Some(10);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("8n+1"), "got: {err}");
assert!(err.contains("9, 17, 25"), "got: {err}");
}
fn extend_req() -> GenerateRequest {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.fps = Some(24);
req.frames = Some(97);
req.extend_video = Some(vec![0, 0, 0, 0x20, b'f', b't', b'y', b'p']);
req
}
#[test]
fn extend_accepts_a_video_with_the_default_overlap() {
let req = extend_req();
assert!(validate_generate_request(&req).is_ok());
assert!(req.is_extend());
assert_eq!(
req.effective_extend_overlap_frames(),
DEFAULT_EXTEND_OVERLAP_FRAMES
);
assert_eq!(req.extend_new_frames(), Some(80));
}
#[test]
fn extend_carries_the_source_frames_the_contract_gate_looks_for() {
use crate::types::SourceImageCapability;
let mut req = extend_req();
req.model = "wan22-i2v-a14b:q8".to_string();
req.width = 832;
req.height = 480;
req.fps = Some(16);
req.frames = Some(49);
assert!(req.source_image.is_none() && req.keyframes.is_none());
assert!(request_carries_source_frames(&req));
assert_eq!(
source_image_contract_violation(
Some("wan"),
&req.model,
Some(SourceImageCapability::Required),
request_carries_source_frames(&req),
),
None
);
assert!(source_image_contract_violation(
Some("wan"),
"wan22-t2v-a14b:q8",
Some(SourceImageCapability::Unsupported),
request_carries_source_frames(&req),
)
.is_some());
let plain = valid_req();
assert!(!request_carries_source_frames(&plain));
}
#[test]
fn extend_overlap_default_follows_the_familys_own_carryover() {
assert_eq!(
default_extend_overlap_frames_for_family(Some("wan")),
WAN_HANDOFF_DUPLICATED_FRAMES
);
assert_eq!(WAN_HANDOFF_DUPLICATED_FRAMES, 1);
assert_eq!(
default_extend_overlap_frames_for_family(Some("ltx2")),
DEFAULT_EXTEND_OVERLAP_FRAMES
);
assert_eq!(
default_extend_overlap_frames_for_family(None),
DEFAULT_EXTEND_OVERLAP_FRAMES
);
let mut req = extend_req();
req.model = "wan22-ti2v-5b:fp16".to_string();
req.width = 704;
req.height = 384;
req.fps = Some(24);
req.frames = Some(49);
assert_eq!(
req.effective_extend_overlap_frames_for_family(Some("wan")),
WAN_HANDOFF_DUPLICATED_FRAMES
);
assert_eq!(
req.effective_extend_overlap_frames(),
WAN_HANDOFF_DUPLICATED_FRAMES
);
assert_eq!(req.extend_new_frames(), Some(48));
assert!(validate_generate_request(&req).is_ok());
req.extend_overlap_frames = Some(9);
assert_eq!(
req.effective_extend_overlap_frames_for_family(Some("wan")),
9
);
}
#[test]
fn chain_motion_tail_follows_the_checkpoints_carryover_not_the_request() {
use crate::SourceImageCapability::{Optional, Required, Unsupported};
for capability in [Required, Optional] {
assert_eq!(
chain_motion_tail_frames_for_family("wan", Some(capability), 17),
WAN_HANDOFF_DUPLICATED_FRAMES,
"{capability:?} carries context, so the tail is one frame"
);
}
assert_eq!(
chain_motion_tail_frames_for_family("wan", Some(Unsupported), 17),
0,
"a text-to-video checkpoint has no channel to be seeded through"
);
assert_eq!(chain_motion_tail_frames_for_family("wan", None, 17), 0);
assert_eq!(
chain_motion_tail_frames_for_family("wan", Some(Required), 1),
WAN_HANDOFF_DUPLICATED_FRAMES
);
assert_eq!(
chain_motion_tail_frames_for_family("ltx-video", None, 17),
0
);
assert_eq!(chain_motion_tail_frames_for_family("ltx2", None, 17), 17);
assert_eq!(chain_motion_tail_frames_for_family("ltx2", None, 9), 9);
assert_eq!(chain_motion_tail_frames_for_family("", None, 17), 17);
}
#[test]
fn materializing_the_overlap_makes_saved_provenance_match_the_render() {
let installed_wan = || {
let mut req = extend_req();
req.model = "cv:2041121".to_string();
req.width = 832;
req.height = 480;
req.fps = Some(16);
req.frames = Some(49);
req
};
let unmaterialized = installed_wan();
assert_eq!(
unmaterialized.effective_extend_overlap_frames(),
DEFAULT_EXTEND_OVERLAP_FRAMES,
"the family-blind fallback is exactly what makes materialization necessary"
);
let mut req = installed_wan();
materialize_extend_overlap_frames(&mut req, Some("wan"));
assert_eq!(
req.extend_overlap_frames,
Some(WAN_HANDOFF_DUPLICATED_FRAMES)
);
let metadata = crate::OutputMetadata::from_generate_request(&req, 7, None, "test");
assert_eq!(
metadata.extend_overlap_frames,
Some(WAN_HANDOFF_DUPLICATED_FRAMES),
"recorded provenance must be the overlap the engine applied"
);
assert_eq!(req.extend_new_frames(), Some(48));
let mut explicit = installed_wan();
explicit.extend_overlap_frames = Some(5);
materialize_extend_overlap_frames(&mut explicit, Some("wan"));
assert_eq!(explicit.extend_overlap_frames, Some(5));
let mut ltx2 = extend_req();
materialize_extend_overlap_frames(&mut ltx2, Some("ltx2"));
assert_eq!(
ltx2.extend_overlap_frames,
Some(DEFAULT_EXTEND_OVERLAP_FRAMES)
);
let mut plain = valid_req();
materialize_extend_overlap_frames(&mut plain, Some("wan"));
assert_eq!(plain.extend_overlap_frames, None);
}
#[test]
fn extend_is_limited_to_families_with_a_continuation_path() {
let mut req = extend_req();
req.model = "ltx-video-0.9.6-distilled:bf16".to_string();
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("extend_video"), "got: {err}");
}
#[test]
fn wan_extend_uses_wans_own_temporal_grid() {
let wan_req = |overlap: Option<u32>| {
let mut req = extend_req();
req.model = "wan22-ti2v-5b:fp16".to_string();
req.width = 704;
req.height = 384;
req.fps = Some(24);
req.frames = Some(49);
req.extend_overlap_frames = overlap;
req
};
assert!(validate_generate_request(&wan_req(Some(1))).is_ok());
for overlap in [5u32, 9, 13] {
assert!(
validate_generate_request(&wan_req(Some(overlap))).is_ok(),
"{overlap} is on wan's 4k+1 grid",
);
}
let err = validate_generate_request(&wan_req(Some(4))).unwrap_err();
assert!(err.contains("4k+1"), "got: {err}");
assert!(!err.contains("8k+1"), "got: {err}");
}
#[test]
fn extend_rejects_both_inline_bytes_and_a_path() {
let mut req = extend_req();
req.extend_video_path = Some("/srv/mold/clip.mp4".to_string());
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("cannot be combined"), "got: {err}");
}
#[test]
fn extend_rejects_empty_payloads() {
let mut req = extend_req();
req.extend_video = Some(Vec::new());
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("must not be empty"));
let mut req = extend_req();
req.extend_video = None;
req.extend_video_path = Some(" ".to_string());
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("must not be empty"));
}
#[test]
fn extend_overlap_must_sit_on_the_latent_grid() {
let mut req = extend_req();
req.extend_overlap_frames = Some(12);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("8k+1"), "got: {err}");
for overlap in [1u32, 9, 17, 25] {
let mut req = extend_req();
req.extend_overlap_frames = Some(overlap);
assert!(
validate_generate_request(&req).is_ok(),
"{overlap} is on the 8k+1 grid",
);
}
}
#[test]
fn extend_overlap_must_leave_room_for_new_frames() {
let mut req = extend_req();
req.frames = Some(25);
req.extend_overlap_frames = Some(25);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("strictly less than"), "got: {err}");
req.extend_overlap_frames = Some(17);
assert!(validate_generate_request(&req).is_ok());
assert_eq!(req.extend_new_frames(), Some(8));
}
#[test]
fn extend_overlap_requires_a_video_to_extend() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.frames = Some(97);
req.extend_overlap_frames = Some(17);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("requires extend_video"), "got: {err}");
}
#[test]
fn extend_rejects_competing_conditioning_inputs() {
let mut req = extend_req();
req.source_video = Some(vec![1, 2, 3]);
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("source_video"));
let mut req = extend_req();
req.source_image = Some(png_bytes());
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("source_image"));
let mut req = extend_req();
req.keyframes = Some(vec![KeyframeCondition {
frame: 0,
image: png_bytes(),
name: None,
}]);
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("keyframes"));
}
#[test]
fn extend_respects_the_temporal_budget() {
let mut req = extend_req();
req.frames = Some(481);
assert!(validate_generate_request(&req).is_ok());
req.frames = Some(489);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("RoPE"), "got: {err}");
}
#[test]
fn extend_provenance_reaches_output_metadata() {
let mut req = extend_req();
req.extend_video = None;
req.extend_video_path = Some("/srv/mold/clip.mp4".to_string());
req.extend_overlap_frames = Some(25);
let metadata = crate::OutputMetadata::from_generate_request(&req, 7, None, "test");
assert_eq!(
metadata.extend_video_path.as_deref(),
Some("/srv/mold/clip.mp4")
);
assert_eq!(metadata.extend_overlap_frames, Some(25));
let plain = crate::OutputMetadata::from_generate_request(&valid_req(), 7, None, "test");
assert_eq!(plain.extend_video_path, None);
assert_eq!(plain.extend_overlap_frames, None);
}
#[test]
fn ltx2_frame_ceiling_tracks_fps() {
assert_eq!(ltx2_max_frames_at_fps(24), 484);
assert_eq!(ltx2_max_frames_at_fps(25), 504);
assert_eq!(ltx2_max_frames_at_fps(12), 244);
assert_eq!(ltx2_max_frames_at_fps(8), 164);
assert_eq!(ltx2_max_frames_at_fps(6), 124);
assert_eq!(ltx2_max_frames_at_fps(60), LTX2_MAX_FRAMES_ABSOLUTE);
assert_eq!(ltx2_max_frames_at_fps(120), LTX2_MAX_FRAMES_ABSOLUTE);
assert_eq!(ltx2_max_frames_at_fps(0), ltx2_max_frames_at_fps(1));
}
#[test]
fn frame_constraint_helpers_match_validator_behavior() {
assert_eq!(
max_frames_for_family("ltx2"),
Some(ltx2_max_frames_on_grid_at_fps(LTX2_DEFAULT_FPS))
);
assert_eq!(max_frames_for_family_at_fps("ltx2", 12), Some(241));
assert_eq!(max_frames_for_family_at_fps("ltx-video", 12), Some(257));
assert_eq!(max_frames_for_family("ltx-video"), Some(257));
assert_eq!(max_frames_for_family("flux"), None);
assert_eq!(max_frames_for_family("sdxl"), None);
assert_eq!(frame_step_for_family("ltx2"), Some(8));
assert_eq!(frame_step_for_family("ltx-video"), Some(8));
assert_eq!(frame_step_for_family("flux"), None);
assert_eq!(min_frames_for_family("flux"), None);
assert_eq!(fixed_fps_for_family("flux"), None);
let cap = max_frames_for_family("ltx-video").unwrap();
let mut req = valid_req();
req.model = "ltx-video-0.9.6-distilled:bf16".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.frames = Some(cap + 8); let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains(&cap.to_string()), "got: {err}");
let cap = max_frames_for_family_at_fps("ltx2", 12).unwrap();
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.fps = Some(12);
req.frames = Some(249); let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains(&cap.to_string()), "got: {err}");
}
#[test]
fn h3_post_activation_timing_authority_rejects_short_or_retimed_requests() {
assert_eq!(
min_frames_for_family(crate::minimax_h3::FAMILY),
Some(crate::minimax_h3::MIN_FRAMES)
);
assert_eq!(
fixed_fps_for_family(crate::minimax_h3::FAMILY),
Some(crate::minimax_h3::FIXED_FPS)
);
assert_eq!(
max_frames_for_family(crate::minimax_h3::FAMILY),
Some(crate::minimax_h3::MAX_FRAMES)
);
assert_eq!(
max_runtime_seconds_for_family(crate::minimax_h3::FAMILY),
Some(crate::minimax_h3::MAX_DURATION_SECONDS)
);
assert_eq!(
max_frames_absolute_for_family(crate::minimax_h3::FAMILY),
Some(crate::minimax_h3::MAX_FRAMES)
);
let short = validate_family_video_timing_constraints(
Some(crate::minimax_h3::FRAME_OFFSET),
Some(crate::minimax_h3::FIXED_FPS),
Some(crate::minimax_h3::FAMILY),
)
.unwrap_err();
assert!(short.contains("124"), "got: {short}");
let retimed = validate_family_video_timing_constraints(
Some(crate::minimax_h3::MIN_FRAMES),
Some(23),
Some(crate::minimax_h3::FAMILY),
)
.unwrap_err();
assert!(retimed.contains("24 fps"), "got: {retimed}");
assert!(validate_family_video_timing_constraints(
Some(crate::minimax_h3::MIN_FRAMES),
Some(crate::minimax_h3::FIXED_FPS),
Some(crate::minimax_h3::FAMILY),
)
.is_ok());
}
#[test]
fn wan_frame_contract_helpers() {
assert_eq!(frame_step_for_family("wan"), Some(WAN_TEMPORAL_SCALE));
assert_eq!(
max_frames_for_family_at_fps("wan", 16),
Some(MAX_FRAMES_GLOBAL)
);
assert_eq!(max_frames_for_family("wan"), Some(MAX_FRAMES_GLOBAL));
assert_eq!(max_runtime_seconds_for_family("wan"), None);
assert_eq!(max_frames_absolute_for_family("wan"), None);
assert_eq!((MAX_FRAMES_GLOBAL - 1) % WAN_TEMPORAL_SCALE, 0);
}
#[test]
fn wan_frames_grid_and_cap_enforced() {
let mut req = valid_req();
req.model = "wan22-ti2v-5b:fp16".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.fps = Some(24);
req.frames = Some(81);
assert!(validate_generate_request(&req).is_ok());
req.frames = Some(80);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("4n+1"), "got: {err}");
let cap = max_frames_for_family("wan").unwrap();
req.frames = Some(cap + WAN_TEMPORAL_SCALE);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains(&cap.to_string()), "got: {err}");
}
#[test]
fn wan_routes_through_the_video_authorities() {
use crate::ExpandTask;
assert_eq!(ExpandTask::for_family("wan"), ExpandTask::TextToVideo);
assert_eq!(
ExpandTask::for_conditioning("wan", None, true, false, false, 0, false, None),
ExpandTask::ImageToVideo
);
assert_eq!(
ExpandTask::for_conditioning("wan", None, false, false, false, 0, false, None),
ExpandTask::TextToVideo
);
let mut req = valid_req();
req.model = "wan22-ti2v-5b:fp16".to_string();
req.output_format = None;
req.normalise_output_format(Some("wan"));
assert_eq!(req.resolved_output_format(), OutputFormat::Mp4);
req.output_format = Some(OutputFormat::Png);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("mp4"), "got: {err}");
}
#[test]
fn wan_single_frame_is_a_still() {
use crate::ExpandTask;
let mut req = valid_req();
req.model = "wan22-t2v-a14b:q5".to_string();
req.frames = Some(1);
req.output_format = None;
req.normalise_output_format(Some("wan"));
assert_eq!(req.resolved_output_format(), OutputFormat::Png);
assert!(validate_generate_request(&req).is_ok());
req.output_format = Some(OutputFormat::Jpeg);
assert!(validate_generate_request(&req).is_ok());
req.output_format = Some(OutputFormat::Mp4);
assert!(validate_generate_request(&req).is_ok());
req.output_format = Some(OutputFormat::Wav);
assert!(validate_generate_request(&req).is_err());
for frames in [Some(5), None] {
req.frames = frames;
req.output_format = Some(OutputFormat::Png);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("mp4, gif, apng, or webp"), "got: {err}");
req.output_format = None;
req.normalise_output_format(Some("wan"));
assert_eq!(req.resolved_output_format(), OutputFormat::Mp4);
}
assert_eq!(
ExpandTask::for_conditioning("wan", None, false, false, false, 0, false, Some(1)),
ExpandTask::TextToImage
);
assert_eq!(
ExpandTask::for_conditioning("wan", None, true, false, false, 0, false, Some(1)),
ExpandTask::ImageToVideo
);
assert_eq!(
ExpandTask::for_conditioning("wan", None, false, false, false, 0, false, Some(81)),
ExpandTask::TextToVideo
);
assert_eq!(
ExpandTask::for_conditioning("ltx2", None, false, false, false, 0, false, Some(1)),
ExpandTask::TextToVideo
);
let mut still_req = valid_req();
still_req.model = "wan22-t2v-a14b:q5".to_string();
still_req.frames = Some(1);
assert_eq!(
ExpandTask::for_generation("wan", &still_req),
ExpandTask::TextToImage
);
}
#[test]
fn wan_recipe_knobs_gate_by_family() {
use crate::Scheduler;
let mut wan = valid_req();
wan.model = "wan22-t2v-a14b:q8".to_string();
wan.output_format = Some(OutputFormat::Mp4);
wan.sample_shift = Some(12.0);
assert!(validate_generate_request(&wan).is_ok());
for bad in [0.0, -1.0, f64::NAN, f64::INFINITY] {
wan.sample_shift = Some(bad);
assert!(
validate_generate_request(&wan).is_err(),
"shift {bad} must be rejected"
);
}
wan.sample_shift = None;
for solver in [Scheduler::UniPc, Scheduler::Euler, Scheduler::DpmPp] {
wan.scheduler = Some(solver);
assert!(
validate_generate_request(&wan).is_ok(),
"wan must accept {solver}"
);
}
for unet in [Scheduler::Ddim, Scheduler::EulerAncestral] {
wan.scheduler = Some(unet);
let err = validate_generate_request(&wan).unwrap_err();
assert!(err.contains("UNet scheduler"), "got: {err}");
}
wan.scheduler = None;
wan.model = "wan22-t2v-a14b:fp8".to_string();
wan.loras = Some(vec![crate::LoraWeight {
path: "distill.safetensors".to_string(),
scale: 1.0,
expert: None,
}]);
let err = validate_generate_request(&wan).unwrap_err();
assert!(err.contains("fp8-scaled"), "got: {err}");
wan.model = "wan22-t2v-a14b:q8".to_string();
assert!(validate_generate_request(&wan).is_ok());
wan.loras = None;
wan.distill_strength_high = Some(1.8);
wan.distill_strength_low = Some(1.0);
assert!(validate_generate_request(&wan).is_ok());
wan.distill_strength_high = Some(4.5);
assert!(validate_generate_request(&wan).is_err());
wan.distill_strength_high = Some(0.0);
assert!(validate_generate_request(&wan).is_err());
let mut flux = valid_req();
flux.sample_shift = Some(5.0);
let err = validate_generate_request(&flux).unwrap_err();
assert!(err.contains("sample_shift"), "got: {err}");
flux.sample_shift = None;
flux.distill_strength_high = Some(1.5);
let err = validate_generate_request(&flux).unwrap_err();
assert!(err.contains("distill_strength_high"), "got: {err}");
flux.distill_strength_high = None;
flux.scheduler = Some(Scheduler::Euler);
let err = validate_generate_request(&flux).unwrap_err();
assert!(err.contains("Wan sample solver"), "got: {err}");
flux.scheduler = Some(Scheduler::Ddim);
assert!(validate_generate_request(&flux).is_ok());
}
#[test]
fn wan_keyframes_admit_only_the_endpoint_pair() {
use crate::{ExpandTask, KeyframeCondition};
let keyframe = |frame: u32| KeyframeCondition {
frame,
image: vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A],
name: None,
};
let mut req = valid_req();
req.model = "wan22-i2v-a14b:q5".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.frames = Some(33);
req.keyframes = Some(vec![keyframe(0), keyframe(32)]);
assert!(validate_generate_request(&req).is_ok());
req.keyframes = Some(vec![keyframe(0)]);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("exactly two keyframes"), "got: {err}");
req.keyframes = Some(vec![keyframe(0), keyframe(7)]);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("frames 0 and 32"), "got: {err}");
req.keyframes = Some(vec![keyframe(0), keyframe(32)]);
req.source_image = Some(vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("not both"), "got: {err}");
req.source_image = None;
req.frames = None;
req.keyframes = Some(vec![keyframe(0), keyframe(32)]);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("explicit frames count"), "got: {err}");
req.frames = Some(1);
req.keyframes = Some(vec![keyframe(0), keyframe(0)]);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("multi-frame clip"), "got: {err}");
req.frames = Some(33);
req.keyframes = Some(vec![keyframe(0), keyframe(32)]);
let mut ti2v = valid_req();
ti2v.model = "wan22-ti2v-5b:fp16".to_string();
ti2v.output_format = Some(OutputFormat::Mp4);
ti2v.width = 1280;
ti2v.height = 704;
ti2v.frames = Some(5);
ti2v.keyframes = Some(vec![keyframe(0), keyframe(4)]);
let err = validate_generate_request(&ti2v).unwrap_err();
assert!(err.contains("at least 9 frames"), "got: {err}");
ti2v.frames = Some(9);
ti2v.keyframes = Some(vec![keyframe(0), keyframe(8)]);
assert!(validate_generate_request(&ti2v).is_ok());
assert_eq!(
ExpandTask::for_generation("wan", &req),
ExpandTask::KeyframeInterpolation
);
let mut flux = valid_req();
flux.keyframes = Some(vec![keyframe(0), keyframe(8)]);
assert!(validate_generate_request(&flux).is_err());
}
#[test]
fn wan_recommended_dimensions_are_per_checkpoint() {
assert_eq!(
wan_recommended_dimensions("wan21-t2v-1.3b"),
&[(832, 480), (480, 832)]
);
assert_eq!(
wan_recommended_dimensions("wan22-ti2v-5b:fp16"),
&[(1280, 704), (704, 1280)]
);
assert_eq!(
wan_recommended_dimensions("cv:someone/some-wan-finetune"),
recommended_dimensions("wan")
);
for model in ["wan21-t2v-1.3b", "wan22-ti2v-5b"] {
for (w, h) in wan_recommended_dimensions(model) {
assert!(
validate_generation_dimensions(*w, *h, Some("wan")).is_ok(),
"{model}: advertised {w}x{h} must pass the validator"
);
assert!(
validate_generation_dimensions_for_model(
model,
*w,
*h,
Some("wan"),
Ltx2SpatialComposition::SinglePass,
)
.is_ok(),
"{model}: advertised {w}x{h} must pass its own model-aware validator"
);
}
}
}
#[test]
fn wan_dimension_alignment_is_per_checkpoint() {
assert_eq!(wan_dimension_alignment("wan22-ti2v-5b"), 32);
assert_eq!(wan_dimension_alignment("wan22-ti2v-5b:fp16"), 32);
assert_eq!(wan_dimension_alignment("wan22-ti2v-5b:q8"), 32);
assert_eq!(wan_dimension_alignment("wan22-ti2v-5b-fp16"), 32);
assert_eq!(wan_dimension_alignment("wan21-t2v-1.3b"), 16);
assert_eq!(wan_dimension_alignment("wan22-t2v-a14b:q5"), 16);
assert_eq!(wan_dimension_alignment("wan22-i2v-a14b:q8"), 16);
assert_eq!(wan_dimension_alignment("cv:someone/some-wan-finetune"), 16);
}
#[test]
fn dimension_alignment_for_model_dispatches_wan_checkpoints() {
assert_eq!(
dimension_alignment_for_model("wan22-ti2v-5b", Some("wan")),
32
);
assert_eq!(
dimension_alignment_for_model("wan22-ti2v-5b:fp16", None),
32
);
assert_eq!(
dimension_alignment_for_model("wan21-t2v-1.3b", Some("wan")),
16
);
assert_eq!(
dimension_alignment_for_model("cv:someone/some-wan-finetune", Some("wan")),
16
);
assert_eq!(
dimension_alignment_for_model("ltx-2-19b-distilled:fp8", Some("ltx2")),
32
);
assert_eq!(
dimension_alignment_for_model("flux-dev:q4", Some("flux")),
16
);
}
#[test]
fn wan22_ti2v_5b_off_grid_dimensions_rejected_at_admission() {
let mut req = valid_req();
req.model = "wan22-ti2v-5b".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.width = 1280;
req.height = 720;
let err = validate_generate_request_with_family(&req, Some("wan")).unwrap_err();
assert!(err.contains("multiples of 32"), "got: {err}");
req.width = 704;
req.height = 1280;
validate_generate_request_with_family(&req, Some("wan"))
.expect("the 5B's native portrait bucket is on its 32px grid");
req.model = "wan21-t2v-1.3b".to_string();
req.width = 1280;
req.height = 720;
validate_generate_request_with_family(&req, Some("wan"))
.expect("the 2.1-VAE checkpoints keep the family's 16px grid");
}
#[test]
fn validate_generation_dimensions_for_model_uses_the_checkpoint_grid() {
let err = validate_generation_dimensions_for_model(
"wan22-ti2v-5b",
1280,
720,
Some("wan"),
Ltx2SpatialComposition::SinglePass,
)
.unwrap_err();
assert!(err.contains("multiples of 32"), "got: {err}");
validate_generation_dimensions_for_model(
"wan22-ti2v-5b",
1280,
704,
Some("wan"),
Ltx2SpatialComposition::SinglePass,
)
.expect("1280x704 sits on the 32px grid");
assert!(validate_generation_dimensions(1280, 720, Some("wan")).is_ok());
}
#[test]
fn wan_recommended_dimensions_fit_their_own_contracts() {
let dims = recommended_dimensions("wan");
assert!(!dims.is_empty());
for (w, h) in dims {
assert!(
w.is_multiple_of(16) && h.is_multiple_of(16),
"{w}x{h} must sit on the family's 16px grid"
);
assert!(
u64::from(*w) * u64::from(*h) <= MAX_PIXELS,
"{w}x{h} must fit the generic pixel budget"
);
assert!(
validate_generation_dimensions(*w, *h, Some("wan")).is_ok(),
"{w}x{h} must pass the validator it is advertised against"
);
}
}
#[test]
fn ltx2_frames_at_rope_budget_accepted() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.fps = Some(24);
req.frames = Some(481);
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn ltx2_frames_over_the_old_flat_cap_are_accepted_within_the_duration_budget() {
for frames in [161u32, 193, 257, 401] {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.fps = Some(24);
req.frames = Some(frames);
assert!(
validate_generate_request(&req).is_ok(),
"{frames} frames at 24 fps is {:.1}s, inside the {LTX2_MAX_RUNTIME_SECONDS}s budget",
frames as f64 / 24.0,
);
}
}
#[test]
fn ltx2_frames_over_rope_budget_rejected() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.fps = Some(24);
req.frames = Some(489); let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("489"), "got: {err}");
assert!(err.contains("481"), "got: {err}");
assert!(err.contains("RoPE"), "got: {err}");
}
#[test]
fn ltx2_frame_budget_is_a_duration_not_a_frame_count() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.frames = Some(193);
req.fps = Some(24);
assert!(validate_generate_request(&req).is_ok());
req.fps = Some(6);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("121"), "got: {err}");
}
#[test]
fn ltx2_absolute_frame_guard_binds_above_thirty_fps() {
let mut req = valid_req();
req.model = "ltx-2.3-22b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.fps = Some(120);
req.frames = Some(609); let err = validate_generate_request(&req).unwrap_err();
assert!(
err.contains(<x2_max_frames_on_grid_at_fps(120).to_string()),
"got: {err}"
);
assert_eq!(ltx2_max_frames_on_grid_at_fps(120), 601);
}
#[test]
fn ltx_video_family_is_not_subject_to_the_ltx2_rope_cap() {
let mut req = valid_req();
req.model = "ltx-video-0.9.6-distilled:bf16".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.frames = Some(161);
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn ltx_video_keeps_the_flat_global_ceiling() {
let mut req = valid_req();
req.model = "ltx-video-0.9.6-distilled:bf16".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.fps = Some(30);
req.frames = Some(MAX_FRAMES_GLOBAL + 8);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains(&MAX_FRAMES_GLOBAL.to_string()), "got: {err}");
}
#[test]
fn ltx2_temporal_upscale_x2_does_not_extend_the_duration_budget() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.fps = Some(24);
req.temporal_upscale = Some(crate::Ltx2TemporalUpscale::X2);
req.frames = Some(481);
assert!(validate_generate_request(&req).is_ok());
req.frames = Some(497);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("RoPE"), "got: {err}");
}
#[test]
fn non_ltx_models_do_not_apply_the_ltx_frame_grid_rule() {
let mut req = valid_req();
req.frames = Some(10);
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn zero_batch_rejected() {
let mut req = valid_req();
req.batch_size = 0;
assert!(validate_generate_request(&req).is_err());
}
#[test]
fn large_batch_accepted() {
let mut req = valid_req();
req.batch_size = 100;
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn negative_guidance_rejected() {
let mut req = valid_req();
req.guidance = -1.0;
assert!(validate_generate_request(&req).is_err());
}
#[test]
fn zero_guidance_valid() {
let mut req = valid_req();
req.guidance = 0.0;
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn high_guidance_valid() {
let mut req = valid_req();
req.guidance = 20.0;
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn guidance_over_100_rejected() {
let mut req = valid_req();
req.guidance = 100.1;
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("guidance"));
}
#[test]
fn guidance_at_100_valid() {
let mut req = valid_req();
req.guidance = 100.0;
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn prompt_too_long_rejected() {
let mut req = valid_req();
req.prompt = "x".repeat(77_001);
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("77,000"));
}
#[test]
fn prompt_at_limit_valid() {
let mut req = valid_req();
req.prompt = "x".repeat(77_000);
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn negative_prompt_too_long_rejected() {
let mut req = valid_req();
req.negative_prompt = Some("x".repeat(77_001));
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("negative_prompt"));
}
#[test]
fn negative_prompt_at_limit_valid() {
let mut req = valid_req();
req.negative_prompt = Some("x".repeat(77_000));
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn negative_prompt_none_valid() {
let req = valid_req();
assert!(req.negative_prompt.is_none());
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn negative_prompt_empty_valid() {
let mut req = valid_req();
req.negative_prompt = Some(String::new());
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn seed_is_optional() {
let mut req = valid_req();
req.seed = None;
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn img2img_strength_zero_accepted() {
let mut req = valid_req();
req.source_image = Some(png_bytes());
req.strength = 0.0;
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn img2img_strength_negative_rejected() {
let mut req = valid_req();
req.source_image = Some(png_bytes());
req.strength = -0.1;
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("strength"));
}
#[test]
fn img2img_strength_one_accepted() {
let mut req = valid_req();
req.source_image = Some(png_bytes());
req.strength = 1.0;
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn img2img_strength_half_accepted() {
let mut req = valid_req();
req.source_image = Some(png_bytes());
req.strength = 0.5;
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn img2img_invalid_magic_bytes_rejected() {
let mut req = valid_req();
req.source_image = Some(vec![0x00, 0x01, 0x02, 0x03]);
req.strength = 0.75;
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("PNG or JPEG"));
}
#[test]
fn img2img_jpeg_accepted() {
let mut req = valid_req();
req.source_image = Some(jpeg_bytes());
req.strength = 0.75;
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn img2img_no_source_image_skips_strength_check() {
let mut req = valid_req();
req.source_image = None;
req.strength = 0.0; assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn qwen_image_edit_requires_edit_images() {
let mut req = valid_req();
req.model = "qwen-image-edit:q4".to_string();
let err = validate_generate_request(&req).unwrap_err();
assert_eq!(
err,
"Qwen Image Edit needs at least one image. Add a Target image and try again."
);
}
#[test]
fn qwen_image_edit_rejects_batch_size_above_one() {
let mut req = valid_req();
req.model = "qwen-image-edit:q4".to_string();
req.edit_images = Some(vec![png_bytes()]);
req.batch_size = 2;
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("batch_size = 1"), "got: {err}");
}
#[test]
fn qwen_image_edit_accepts_edit_images() {
let mut req = valid_req();
req.model = "qwen-image-edit:q4".to_string();
req.edit_images = Some(vec![png_bytes()]);
req.guidance = 4.0;
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn flux2_dev_accepts_text_only_and_ordered_references() {
let mut req = valid_req();
req.model = "flux2-dev:bf16".to_string();
req.guidance = 4.0;
assert!(validate_generate_request(&req).is_ok());
req.edit_images = Some(vec![png_bytes(), jpeg_bytes()]);
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn flux2_dev_catalog_id_accepts_references_but_rejects_img2img_fields() {
let mut req = valid_req();
req.model = "hf:black-forest-labs/FLUX.2-dev".to_string();
req.edit_images = Some(vec![png_bytes()]);
assert!(validate_generate_request_with_family(&req, Some("flux2")).is_ok());
req.source_image = Some(png_bytes());
let error = validate_generate_request_with_family(&req, Some("flux2")).unwrap_err();
assert!(error.contains("edit_images instead of source_image"));
}
#[test]
fn flux2_dev_bounds_reference_count_and_rejects_lora() {
let mut req = valid_req();
req.model = "flux2-dev:bf16".to_string();
req.edit_images = Some(vec![png_bytes(); FLUX2_DEV_MAX_REFERENCE_IMAGES + 1]);
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("at most"));
req.edit_images = None;
req.lora = Some(LoraWeight {
path: "adapter.safetensors".into(),
scale: 1.0,
expert: None,
});
assert_eq!(
validate_generate_request(&req).unwrap_err(),
"flux2-dev does not support LoRA"
);
}
#[test]
fn qwen_image_edit_rejects_source_image_field() {
let mut req = valid_req();
req.model = "qwen-image-edit:q4".to_string();
req.edit_images = Some(vec![png_bytes()]);
req.source_image = Some(png_bytes());
let err = validate_generate_request(&req).unwrap_err();
assert!(
err.contains("edit_images instead of source_image"),
"got: {err}"
);
}
#[test]
fn non_edit_models_reject_edit_images() {
let mut req = valid_req();
req.model = "flux-schnell:q8".to_string();
req.edit_images = Some(vec![png_bytes()]);
let err = validate_generate_request(&req).unwrap_err();
assert!(
err.contains("only supported for qwen-image-edit"),
"got: {err}"
);
}
#[test]
fn non_edit_models_reject_edit_images_before_format_validation() {
let mut req = valid_req();
req.model = "flux-schnell:q8".to_string();
req.edit_images = Some(vec![b"not-an-image".to_vec()]);
let err = validate_generate_request(&req).unwrap_err();
assert!(
err.contains("only supported for qwen-image-edit"),
"got: {err}"
);
}
#[test]
fn controlnet_valid_request() {
let mut req = valid_req();
req.model = "dreamshaper-v8:fp16".to_string();
req.control_image = Some(png_bytes());
req.control_model = Some("controlnet-canny-sd15".to_string());
req.control_scale = 0.8;
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn controlnet_image_without_model_rejected() {
let mut req = valid_req();
req.model = "dreamshaper-v8:fp16".to_string();
req.control_image = Some(png_bytes());
req.control_model = None;
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("control_model"));
}
#[test]
fn controlnet_model_without_image_rejected() {
let mut req = valid_req();
req.model = "dreamshaper-v8:fp16".to_string();
req.control_image = None;
req.control_model = Some("controlnet-canny-sd15".to_string());
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("control_image"));
}
#[test]
fn controlnet_invalid_image_rejected() {
let mut req = valid_req();
req.model = "dreamshaper-v8:fp16".to_string();
req.control_image = Some(vec![0x00, 0x01, 0x02, 0x03]);
req.control_model = Some("controlnet-canny-sd15".to_string());
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("PNG or JPEG"));
}
#[test]
fn controlnet_negative_scale_rejected() {
let mut req = valid_req();
req.model = "dreamshaper-v8:fp16".to_string();
req.control_image = Some(png_bytes());
req.control_model = Some("controlnet-canny-sd15".to_string());
req.control_scale = -0.1;
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("control_scale"));
}
#[test]
fn controlnet_zero_scale_accepted() {
let mut req = valid_req();
req.model = "dreamshaper-v8:fp16".to_string();
req.control_image = Some(png_bytes());
req.control_model = Some("controlnet-canny-sd15".to_string());
req.control_scale = 0.0;
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn controlnet_high_scale_accepted() {
let mut req = valid_req();
req.model = "dreamshaper-v8:fp16".to_string();
req.control_image = Some(png_bytes());
req.control_model = Some("controlnet-canny-sd15".to_string());
req.control_scale = 2.0;
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn controlnet_jpeg_accepted() {
let mut req = valid_req();
req.model = "dreamshaper-v8:fp16".to_string();
req.control_image = Some(jpeg_bytes());
req.control_model = Some("controlnet-canny-sd15".to_string());
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn controlnet_rejected_for_non_sd15_family() {
let mut req = valid_req();
req.model = "sdxl:fp16".to_string();
req.control_image = Some(png_bytes());
req.control_model = Some("controlnet-canny-sd15".to_string());
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("SD1.5"), "got: {err}");
}
#[test]
fn mask_without_source_image_rejected() {
let mut req = valid_req();
req.mask_image = Some(png_bytes());
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("mask_image requires source_image"));
}
#[test]
fn mask_with_source_image_accepted() {
let mut req = valid_req();
req.source_image = Some(png_bytes());
req.mask_image = Some(png_bytes());
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn mask_jpeg_accepted() {
let mut req = valid_req();
req.source_image = Some(png_bytes());
req.mask_image = Some(jpeg_bytes());
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn mask_invalid_bytes_rejected() {
let mut req = valid_req();
req.source_image = Some(png_bytes());
req.mask_image = Some(vec![0x00, 0x01, 0x02, 0x03]);
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("mask_image must be a PNG or JPEG"));
}
#[test]
fn no_mask_no_source_passes() {
let req = valid_req();
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn fit_same_aspect_downscale() {
assert_eq!(fit_to_model_dimensions(1024, 1024, 512, 512), (512, 512));
}
#[test]
fn fit_wide_source_downscale() {
assert_eq!(fit_to_model_dimensions(1920, 1080, 512, 512), (512, 288));
}
#[test]
fn fit_small_source_upscale_to_model_native() {
assert_eq!(fit_to_model_dimensions(512, 512, 1024, 1024), (1024, 1024));
}
#[test]
fn fit_portrait_source() {
assert_eq!(fit_to_model_dimensions(768, 1024, 512, 512), (384, 512));
}
#[test]
fn fit_identity() {
assert_eq!(
fit_to_model_dimensions(1024, 1024, 1024, 1024),
(1024, 1024)
);
}
#[test]
fn fit_extreme_landscape() {
assert_eq!(fit_to_model_dimensions(3840, 720, 1024, 1024), (1024, 192));
}
#[test]
fn fit_non_square_model_bounds() {
assert_eq!(fit_to_model_dimensions(1920, 1080, 1024, 768), (1024, 576));
}
#[test]
fn fit_dimensions_are_16px_aligned() {
let (w, h) = fit_to_model_dimensions(1000, 600, 512, 512);
assert!(w % 16 == 0, "width {w} must be 16px aligned");
assert!(h % 16 == 0, "height {h} must be 16px aligned");
}
#[test]
fn fit_within_megapixel_limit() {
let (w, h) = fit_to_model_dimensions(4096, 4096, 2048, 2048);
let pixels = w as u64 * h as u64;
assert!(
pixels <= MAX_PIXELS,
"{}x{} = {} pixels exceeds limit",
w,
h,
pixels
);
}
#[test]
fn fit_tiny_source_gets_model_native() {
assert_eq!(fit_to_model_dimensions(64, 64, 1024, 1024), (1024, 1024));
}
#[test]
fn fit_to_model_dimensions_aligned_rounds_to_the_models_grid() {
assert_eq!(
fit_to_model_dimensions_aligned(1617, 1000, 1280, 704, 32),
(1120, 704)
);
assert_eq!(
fit_to_model_dimensions_aligned(1617, 1000, 1280, 704, 16),
(1136, 704)
);
assert_eq!(fit_to_model_dimensions(1617, 1000, 1280, 704), (1136, 704));
}
#[test]
fn fit_to_target_area_preserves_ratio_and_alignment() {
let (w, h) = fit_to_target_area(1600, 900, 1024 * 1024, 16);
assert_eq!((w, h), (1360, 768));
}
fn valid_flux_req() -> GenerateRequest {
GenerateRequest {
model: "flux-dev".to_string(),
..valid_req()
}
}
#[test]
fn lora_none_valid() {
let req = valid_req();
assert!(req.lora.is_none());
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn lora_scale_too_low_rejected() {
let mut req = valid_flux_req();
req.lora = Some(crate::LoraWeight {
path: "adapter.safetensors".to_string(),
scale: -0.1,
expert: None,
});
let err = validate_generate_request(&req).unwrap_err();
assert!(
err.contains("lora scale"),
"expected lora scale error: {err}"
);
}
#[test]
fn lora_scale_too_high_rejected() {
let mut req = valid_flux_req();
req.lora = Some(crate::LoraWeight {
path: "adapter.safetensors".to_string(),
scale: 2.1,
expert: None,
});
let err = validate_generate_request(&req).unwrap_err();
assert!(
err.contains("lora scale"),
"expected lora scale error: {err}"
);
}
#[test]
fn lora_scale_boundary_valid() {
for scale in [0.0, 1.0, 2.0] {
let mut req = valid_flux_req();
req.lora = Some(crate::LoraWeight {
path: "adapter.safetensors".to_string(),
scale,
expert: None,
});
assert!(
validate_generate_request(&req).is_ok(),
"scale={scale} should be valid"
);
}
}
#[test]
fn lora_path_not_found_passes_validation() {
let mut req = valid_flux_req();
req.lora = Some(crate::LoraWeight {
path: "/nonexistent/path/adapter.safetensors".to_string(),
scale: 1.0,
expert: None,
});
assert!(validate_generate_request(&req).is_ok());
}
#[test]
fn lora_wrong_extension_rejected() {
let mut req = valid_flux_req();
req.lora = Some(crate::LoraWeight {
path: "/some/path/adapter.bin".to_string(),
scale: 1.0,
expert: None,
});
let err = validate_generate_request(&req).unwrap_err();
assert!(
err.contains("safetensors"),
"expected safetensors error: {err}"
);
}
fn valid_sdxl_req() -> GenerateRequest {
GenerateRequest {
model: "sdxl-base:fp16".to_string(),
..valid_req()
}
}
#[test]
fn lora_on_sdxl_accepted() {
let mut req = valid_sdxl_req();
req.lora = Some(crate::LoraWeight {
path: "adapter.safetensors".to_string(),
scale: 1.0,
expert: None,
});
assert!(
validate_generate_request(&req).is_ok(),
"SDXL + LoRA must pass validation now that sdxl/lora.rs is live"
);
}
#[test]
fn loras_plural_on_sdxl_accepted() {
let mut req = valid_sdxl_req();
req.loras = Some(vec![
crate::LoraWeight {
path: "a.safetensors".to_string(),
scale: 0.8,
expert: None,
},
crate::LoraWeight {
path: "b.safetensors".to_string(),
scale: 0.4,
expert: None,
},
]);
assert!(
validate_generate_request(&req).is_ok(),
"SDXL + plural LoRAs (multi-LoRA stack) must pass validation"
);
}
#[test]
fn lora_on_wan_accepted() {
let mut req = valid_req();
req.model = "wan21-t2v-1.3b".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.fps = Some(16);
req.frames = Some(33);
req.lora = Some(crate::LoraWeight {
path: "adapter.safetensors".to_string(),
scale: 1.0,
expert: None,
});
assert!(
validate_generate_request(&req).is_ok(),
"Wan + LoRA must pass validation now that wan/lora.rs is live"
);
req.lora = None;
req.loras = Some(vec![
crate::LoraWeight {
path: "a.safetensors".to_string(),
scale: 0.8,
expert: None,
},
crate::LoraWeight {
path: "b.safetensors".to_string(),
scale: 0.4,
expert: None,
},
]);
assert!(
validate_generate_request(&req).is_ok(),
"Wan + plural LoRAs (multi-LoRA stack) must pass validation"
);
}
#[test]
fn loras_plural_on_flux_valid() {
let mut req = valid_flux_req();
req.loras = Some(vec![
crate::LoraWeight {
path: "a.safetensors".into(),
scale: 0.8,
expert: None,
},
crate::LoraWeight {
path: "b.safetensors".into(),
scale: 0.4,
expert: None,
},
]);
assert!(validate_generate_request(&req).is_ok());
}
fn valid_ltx2_req() -> GenerateRequest {
GenerateRequest {
model: "ltx-2-19b-distilled:fp8".to_string(),
output_format: Some(OutputFormat::Mp4),
..valid_req()
}
}
#[test]
fn lora_on_ltx2_accepted() {
let mut req = valid_ltx2_req();
req.lora = Some(crate::LoraWeight {
path: "LTX2.3_Crisp_Enhance.safetensors".to_string(),
scale: 1.0,
expert: None,
});
assert!(
validate_generate_request(&req).is_ok(),
"LTX-2 + LoRA must pass validation"
);
}
#[test]
fn loras_plural_on_ltx2_accepted() {
let mut req = valid_ltx2_req();
req.loras = Some(vec![
crate::LoraWeight {
path: "a.safetensors".into(),
scale: 0.8,
expert: None,
},
crate::LoraWeight {
path: "b.safetensors".into(),
scale: 0.4,
expert: None,
},
]);
assert!(
validate_generate_request(&req).is_ok(),
"LTX-2 + loras plural must pass validation"
);
}
fn valid_zimage_req() -> GenerateRequest {
GenerateRequest {
model: "z-image-turbo:bf16".to_string(),
..valid_req()
}
}
fn valid_sd3_req() -> GenerateRequest {
GenerateRequest {
model: "sd3.5-large".to_string(),
..valid_req()
}
}
#[test]
fn lora_on_sd3_accepted() {
let mut req = valid_sd3_req();
req.lora = Some(crate::LoraWeight {
path: "sd35_style.safetensors".to_string(),
scale: 1.0,
expert: None,
});
assert!(
validate_generate_request(&req).is_ok(),
"SD3 + LoRA must pass validation: {:?}",
validate_generate_request(&req)
);
}
#[test]
fn loras_plural_on_sd3_accepted() {
let mut req = valid_sd3_req();
req.loras = Some(vec![
crate::LoraWeight {
path: "a.safetensors".into(),
scale: 0.8,
expert: None,
},
crate::LoraWeight {
path: "b.safetensors".into(),
scale: 0.4,
expert: None,
},
]);
assert!(
validate_generate_request(&req).is_ok(),
"SD3 + loras plural must pass validation"
);
}
#[test]
fn lora_rejection_message_lists_sd3() {
let mut req = valid_req();
req.model = "wuerstchen-c".to_string();
req.lora = Some(crate::LoraWeight {
path: "adapter.safetensors".to_string(),
scale: 1.0,
expert: None,
});
let err = validate_generate_request(&req).unwrap_err();
assert!(
err.to_lowercase().contains("sd3"),
"rejection message must list SD3 alongside FLUX/LTX-2: {err}"
);
}
#[test]
fn lora_on_zimage_accepted() {
let mut req = valid_zimage_req();
req.lora = Some(crate::LoraWeight {
path: "NSFW_master_ZIT_000017532.safetensors".to_string(),
scale: 1.0,
expert: None,
});
assert!(
validate_generate_request(&req).is_ok(),
"Z-Image + LoRA must pass validation"
);
}
#[test]
fn loras_plural_on_zimage_accepted() {
let mut req = valid_zimage_req();
req.loras = Some(vec![
crate::LoraWeight {
path: "a.safetensors".into(),
scale: 0.8,
expert: None,
},
crate::LoraWeight {
path: "b.safetensors".into(),
scale: 0.4,
expert: None,
},
]);
assert!(
validate_generate_request(&req).is_ok(),
"Z-Image + loras plural must pass validation"
);
}
#[test]
fn lora_on_flux2_accepted() {
let mut req = valid_req();
req.model = "flux2-klein".to_string();
req.lora = Some(crate::LoraWeight {
path: "DarkKlein9b.safetensors".to_string(),
scale: 1.0,
expert: None,
});
assert!(
validate_generate_request(&req).is_ok(),
"Flux.2 + LoRA must pass validation"
);
}
#[test]
fn loras_plural_on_flux2_accepted() {
let mut req = valid_req();
req.model = "flux2-klein-9b".to_string();
req.loras = Some(vec![
crate::LoraWeight {
path: "lora-a.safetensors".into(),
scale: 0.8,
expert: None,
},
crate::LoraWeight {
path: "lora-b.safetensors".into(),
scale: 0.4,
expert: None,
},
]);
assert!(
validate_generate_request(&req).is_ok(),
"Flux.2 + loras plural must pass validation"
);
}
#[test]
fn lora_on_unsupported_family_lists_sdxl_in_message() {
let mut req = valid_req();
req.model = "wuerstchen-c".to_string();
req.lora = Some(crate::LoraWeight {
path: "adapter.safetensors".to_string(),
scale: 1.0,
expert: None,
});
let err = validate_generate_request(&req).unwrap_err();
assert!(
err.to_lowercase().contains("flux"),
"error must mention FLUX: {err}"
);
assert!(
err.to_lowercase().contains("flux.2") || err.to_lowercase().contains("flux2"),
"error must mention Flux.2: {err}"
);
assert!(
err.to_lowercase().contains("ltx-2") || err.to_lowercase().contains("ltx2"),
"error must mention LTX-2: {err}"
);
assert!(
err.to_lowercase().contains("sdxl"),
"error must mention SDXL: {err}"
);
assert!(
err.to_lowercase().contains("qwen-image"),
"error must mention Qwen-Image: {err}"
);
}
#[test]
fn lora_on_qwen_image_accepted() {
let mut req = valid_req();
req.model = "qwen-image-2512".to_string();
req.lora = Some(crate::LoraWeight {
path: "adapter.safetensors".to_string(),
scale: 1.0,
expert: None,
});
assert!(
validate_generate_request(&req).is_ok(),
"Qwen-Image + LoRA must pass validation",
);
}
#[test]
fn lora_on_qwen_image_edit_passes_lora_gate() {
let mut req = valid_req();
req.model = "qwen-image-edit-2511:q4".to_string();
req.lora = Some(crate::LoraWeight {
path: "adapter.safetensors".to_string(),
scale: 1.0,
expert: None,
});
let err = validate_generate_request(&req).unwrap_err();
assert!(
!err.to_lowercase().contains("lora"),
"LoRA gate must not reject qwen-image-edit; remaining failure should be on the target image: {err}",
);
assert!(
err.contains("Add a Target image"),
"expected the only failure to be the target-image requirement: {err}",
);
}
#[test]
fn loras_plural_on_qwen_image_accepted() {
let mut req = valid_req();
req.model = "qwen-image-2512".to_string();
req.loras = Some(vec![
crate::LoraWeight {
path: "a.safetensors".into(),
scale: 0.8,
expert: None,
},
crate::LoraWeight {
path: "b.safetensors".into(),
scale: 0.4,
expert: None,
},
]);
assert!(
validate_generate_request(&req).is_ok(),
"Qwen-Image + multi-LoRA must pass validation",
);
}
#[test]
fn lora_on_unknown_family_still_rejected() {
let mut req = valid_req();
req.model = "some-unknown-model-xyz".to_string();
req.lora = Some(crate::LoraWeight {
path: "adapter.safetensors".to_string(),
scale: 1.0,
expert: None,
});
let err = validate_generate_request(&req).unwrap_err();
assert!(
!err.is_empty(),
"unknown family with LoRA must produce an error: {err}"
);
}
#[test]
fn lora_on_sd15_accepted() {
let mut req = valid_req();
req.model = "sd15:fp16".to_string();
req.width = 512;
req.height = 512;
req.guidance = 7.0;
req.lora = Some(crate::LoraWeight {
path: "adapter.safetensors".to_string(),
scale: 0.8,
expert: None,
});
assert!(
validate_generate_request(&req).is_ok(),
"SD1.5 + LoRA must pass validation"
);
}
#[test]
fn loras_plural_on_sd15_accepted() {
let mut req = valid_req();
req.model = "sd15:fp16".to_string();
req.width = 512;
req.height = 512;
req.guidance = 7.0;
req.loras = Some(vec![
crate::LoraWeight {
path: "a.safetensors".into(),
scale: 0.8,
expert: None,
},
crate::LoraWeight {
path: "b.safetensors".into(),
scale: 0.4,
expert: None,
},
]);
assert!(
validate_generate_request(&req).is_ok(),
"SD1.5 + loras plural must pass validation"
);
}
#[test]
fn lora_on_sdxl_message_now_lists_sd15() {
let mut req = valid_req();
req.model = "sdxl".to_string();
req.lora = Some(crate::LoraWeight {
path: "adapter.safetensors".to_string(),
scale: 1.0,
expert: None,
});
let err = validate_generate_request(&req).unwrap_err();
assert!(
err.to_lowercase().contains("sd1.5")
|| err.to_lowercase().contains("sd15")
|| err.to_lowercase().contains("sd 1.5"),
"error must list SD1.5 as a supported family: {err}"
);
}
#[test]
fn dimension_warning_matching_returns_none() {
assert!(dimension_warning(1024, 1024, "flux").is_none());
assert!(dimension_warning(512, 512, "sd15").is_none());
assert!(dimension_warning(1024, 1024, "sdxl").is_none());
assert!(dimension_warning(1024, 1024, "wuerstchen").is_none());
}
#[test]
fn dimension_warning_non_matching_returns_some() {
let warning = dimension_warning(256, 256, "flux");
assert!(warning.is_some());
let msg = warning.unwrap();
assert!(msg.contains("256x256"), "should mention requested dims");
assert!(msg.contains("flux"), "should mention model family");
assert!(msg.contains("Suggested"), "should include suggestions");
}
#[test]
fn dimension_warning_unknown_family_returns_none() {
assert!(dimension_warning(256, 256, "unknown-model").is_none());
}
#[test]
fn dimension_warning_empty_family_returns_none() {
assert!(dimension_warning(512, 512, "").is_none());
}
#[test]
fn dimension_warning_sd15_at_1024_warns() {
let warning = dimension_warning(1024, 1024, "sd15");
assert!(warning.is_some(), "SD1.5 at 1024x1024 should warn");
assert!(warning.unwrap().contains("512x512"));
}
#[test]
fn dimension_warning_sdxl_buckets_accepted() {
for (w, h) in recommended_dimensions("sdxl") {
assert!(
dimension_warning(*w, *h, "sdxl").is_none(),
"SDXL bucket {w}x{h} should not warn"
);
}
}
#[test]
fn dimension_warning_qwen_image_withholds_unqualified_candidates() {
assert!(recommended_dimensions("qwen-image").is_empty());
assert_eq!(dimension_warning(1328, 1328, "qwen-image"), None);
assert_eq!(dimension_warning(512, 512, "qwen-image"), None);
}
#[test]
fn dimension_warning_qwen_image_edit_reuses_qwen_dimensions() {
assert_eq!(
recommended_dimensions("qwen-image-edit"),
recommended_dimensions("qwen-image")
);
assert_eq!(dimension_warning(1024, 1024, "qwen-image-edit"), None);
}
#[test]
fn dimension_warning_flux2_uses_flux_dims() {
assert_eq!(
recommended_dimensions("flux2"),
recommended_dimensions("flux"),
"flux2 should share FLUX dimensions"
);
}
#[test]
fn every_family_native_in_recommendations() {
let families = &[
("sd15", 512, 512),
("sdxl", 1024, 1024),
("sd3", 1024, 1024),
("flux", 1024, 1024),
("flux2", 1024, 1024),
("wuerstchen", 1024, 1024),
("ltx-video", 768, 512),
("minimax-h3", 1344, 768),
("z-image", 1024, 1024),
];
for (family, w, h) in families {
let dims = recommended_dimensions(family);
assert!(
dims.contains(&(*w, *h)),
"{family} native {w}x{h} missing from recommended list"
);
}
for family in ["qwen-image", "qwen-image-edit"] {
assert!(recommended_dimensions(family).is_empty());
}
}
#[test]
fn h3_recommendations_are_the_official_product_ratios_on_the_oracle_canvas() {
assert_eq!(
recommended_dimensions(crate::minimax_h3::FAMILY),
&[
(1536, 672),
(1344, 768),
(1024, 768),
(768, 768),
(768, 1024),
(768, 1344),
]
);
}
#[test]
fn dimension_warning_message_format() {
let msg = dimension_warning(800, 600, "sd15").unwrap();
assert!(msg.contains("800x600"));
assert!(msg.contains("sd15"));
assert!(msg.contains("Suggested:"));
assert!(msg.contains("512x512"));
}
#[test]
fn dimension_warning_truncates_long_lists() {
let msg = dimension_warning(800, 600, "sdxl").unwrap();
assert!(msg.contains("total"), "long lists should show total count");
}
fn valid_upscale_req() -> crate::UpscaleRequest {
crate::UpscaleRequest {
model: "real-esrgan-x4plus:fp16".to_string(),
image: png_bytes(),
output_format: crate::OutputFormat::Png,
tile_size: None,
metadata: None,
}
}
#[test]
fn upscale_valid_request_passes() {
assert!(validate_upscale_request(&valid_upscale_req()).is_ok());
}
#[test]
fn upscale_empty_model_rejected() {
let mut req = valid_upscale_req();
req.model = " ".to_string();
assert!(validate_upscale_request(&req)
.unwrap_err()
.contains("model"));
}
#[test]
fn upscale_empty_image_rejected() {
let mut req = valid_upscale_req();
req.image = vec![];
assert!(validate_upscale_request(&req)
.unwrap_err()
.contains("empty"));
}
#[test]
fn upscale_invalid_image_format_rejected() {
let mut req = valid_upscale_req();
req.image = vec![0x00, 0x01, 0x02, 0x03];
assert!(validate_upscale_request(&req)
.unwrap_err()
.contains("PNG or JPEG"));
}
#[test]
fn upscale_jpeg_accepted() {
let mut req = valid_upscale_req();
req.image = jpeg_bytes();
assert!(validate_upscale_request(&req).is_ok());
}
#[test]
fn upscale_tile_size_too_small_rejected() {
let mut req = valid_upscale_req();
req.tile_size = Some(32);
assert!(validate_upscale_request(&req)
.unwrap_err()
.contains("tile_size"));
}
#[test]
fn upscale_tile_size_zero_accepted() {
let mut req = valid_upscale_req();
req.tile_size = Some(0);
assert!(validate_upscale_request(&req).is_ok());
}
#[test]
fn upscale_tile_size_64_accepted() {
let mut req = valid_upscale_req();
req.tile_size = Some(64);
assert!(validate_upscale_request(&req).is_ok());
}
#[test]
fn upscale_tile_size_none_accepted() {
let req = valid_upscale_req();
assert!(validate_upscale_request(&req).is_ok());
}
#[test]
fn built_in_ic_lora_control_requires_video_pipeline_and_reserves_a_stack_slot() {
let mut req = valid_req();
req.model = "ltx-2-19b-distilled:fp8".to_string();
req.output_format = Some(crate::OutputFormat::Mp4);
req.frames = Some(97);
req.ic_lora_control = Some("union".to_string());
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("pipeline=ic-lora"));
req.pipeline = Some(crate::Ltx2PipelineMode::IcLora);
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("source_video"));
req.source_video_path = Some("/guides/canny.mp4".to_string());
assert!(validate_generate_request(&req).is_ok());
req.loras = Some(
(0..4)
.map(|index| crate::LoraWeight {
path: format!("/loras/{index}.safetensors"),
scale: 1.0,
expert: None,
})
.collect(),
);
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("four-LoRA"));
}
fn lip_dub_req() -> GenerateRequest {
let mut req = valid_req();
req.model = "ltx-2.3-22b-distilled:fp8".to_string();
req.output_format = Some(OutputFormat::Mp4);
req.width = 1216;
req.height = 704;
req.pipeline = Some(Ltx2PipelineMode::LipDub);
req.ic_lora_control = Some("lipdub".to_string());
req.source_video_path = Some("/clips/speaker.mp4".to_string());
req
}
#[test]
fn snap_frames_to_8k1_rounds_down_never_up() {
for on_grid in [1, 9, 17, 97, 121, 481] {
assert_eq!(super::snap_frames_to_8k1(on_grid), on_grid);
}
assert_eq!(super::snap_frames_to_8k1(2), 1);
assert_eq!(super::snap_frames_to_8k1(8), 1);
assert_eq!(super::snap_frames_to_8k1(16), 9);
assert_eq!(super::snap_frames_to_8k1(96), 89);
assert_eq!(super::snap_frames_to_8k1(100), 97);
assert_eq!(super::snap_frames_to_8k1(0), 1);
assert_eq!(super::ltx2_max_frames_on_grid_at_fps(24), 481);
}
fn lip_dub_reference(frames: u32, fps: u32) -> super::LipDubReference {
super::LipDubReference {
frames,
fps,
has_audio: true,
}
}
#[test]
fn lip_dub_timing_comes_from_the_reference_video() {
let timing = super::resolve_lip_dub_timing(lip_dub_reference(120, 25), None, None).unwrap();
assert_eq!(timing.frames, 113);
assert_eq!(timing.fps, 25);
assert_eq!(timing.warnings.len(), 1, "{:?}", timing.warnings);
assert!(timing.warnings[0].contains("113"));
let timing =
super::resolve_lip_dub_timing(lip_dub_reference(97, 24), Some(97), Some(24)).unwrap();
assert_eq!((timing.frames, timing.fps), (97, 24));
assert!(timing.warnings.is_empty());
}
#[test]
fn lip_dub_timing_overrides_and_reports_conflicting_requests() {
let timing =
super::resolve_lip_dub_timing(lip_dub_reference(97, 24), Some(241), Some(30)).unwrap();
assert_eq!((timing.frames, timing.fps), (97, 24));
assert_eq!(timing.warnings.len(), 2, "{:?}", timing.warnings);
assert!(timing.warnings[0].contains("241") && timing.warnings[0].contains("97"));
assert!(timing.warnings[1].contains("30") && timing.warnings[1].contains("24"));
}
#[test]
fn lip_dub_timing_rejects_unusable_references() {
assert!(
super::resolve_lip_dub_timing(lip_dub_reference(97, 0), None, None)
.unwrap_err()
.contains("frame rate")
);
assert!(
super::resolve_lip_dub_timing(lip_dub_reference(8, 24), None, None)
.unwrap_err()
.contains("too short")
);
let silent = super::LipDubReference {
has_audio: false,
..lip_dub_reference(97, 24)
};
assert!(super::resolve_lip_dub_timing(silent, None, None)
.unwrap_err()
.contains("no audio track"));
}
#[test]
fn lip_dub_requires_a_reference_video_and_the_adapter() {
let mut req = lip_dub_req();
req.source_video_path = None;
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("source_video"));
let mut req = lip_dub_req();
req.ic_lora_control = None;
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("ic_lora_control=lipdub"));
assert!(validate_generate_request(&lip_dub_req()).is_ok());
}
#[test]
fn lip_dub_rejects_dimensions_that_are_not_multiples_of_64() {
let mut req = lip_dub_req();
req.height = 736;
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("multiples of 64"), "{err}");
let mut req = lip_dub_req();
req.width = 1184;
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("multiples of 64"));
}
#[test]
fn lip_dub_control_id_routes_to_the_lip_dub_pipeline_not_ic_lora() {
use crate::ltx2_control::pipeline_for_control_id;
assert_eq!(pipeline_for_control_id("lipdub"), Ltx2PipelineMode::LipDub);
assert_eq!(pipeline_for_control_id("LipDub"), Ltx2PipelineMode::LipDub);
assert_eq!(pipeline_for_control_id("union"), Ltx2PipelineMode::IcLora);
let mut req = lip_dub_req();
req.pipeline = Some(Ltx2PipelineMode::IcLora);
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("requires pipeline=lip-dub"));
let mut req = lip_dub_req();
req.ic_lora_control = Some("union".to_string());
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("requires pipeline=ic-lora"));
}
#[test]
fn lip_dub_rejects_conflicting_conditioning_modes() {
let mut req = lip_dub_req();
req.retake_range = Some(crate::TimeRange {
start_seconds: 0.0,
end_seconds: 1.0,
});
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("retake_range"));
let mut req = lip_dub_req();
req.keyframes = Some(vec![KeyframeCondition {
frame: 0,
image: png_bytes(),
name: None,
}]);
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("keyframes"));
let mut req = lip_dub_req();
req.spatial_upscale = Some(crate::Ltx2SpatialUpscale::X2);
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("spatial_upscale"));
let mut req = lip_dub_req();
req.temporal_upscale = Some(crate::Ltx2TemporalUpscale::X2);
assert!(validate_generate_request(&req)
.unwrap_err()
.contains("temporal_upscale"));
}
}