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",
"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 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)
}
#[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),
_ => None,
}
}
pub fn max_frames_for_family(family: &str) -> Option<u32> {
max_frames_for_family_at_fps(family, LTX2_DEFAULT_FPS)
}
pub fn max_runtime_seconds_for_family(family: &str) -> Option<u32> {
(family == "ltx2").then_some(LTX2_MAX_RUNTIME_SECONDS)
}
pub fn max_frames_absolute_for_family(family: &str) -> Option<u32> {
(family == "ltx2").then_some(LTX2_MAX_FRAMES_ABSOLUTE)
}
pub fn frame_step_for_family(family: &str) -> Option<u32> {
matches!(family, "ltx2" | "ltx-video").then_some(8)
}
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,
_ => 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")) {
32
} else {
16
}
}
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> {
if width == 0 || height == 0 {
return Err("width and height must be > 0".to_string());
}
let alignment = dimension_alignment_for_family(family);
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) {
let limit = max_pixels_for_family(family);
let align = dimension_alignment_for_family(family);
let axis_limit = max_axis_pixels_for_family(family);
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) {
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) / 16 * 16).max(16);
let h = ((h as u32) / 16 * 16).max(16);
clamp_to_megapixel_limit(w, h)
}
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 validate_keyframes(
keyframes: &[KeyframeCondition],
frames: Option<u32>,
family: Option<&str>,
) -> Result<(), String> {
match family {
Some("ltx2") => {}
None => {
return Err(
"unknown model family; keyframes are only supported for LTX-2 / LTX-2.3 models"
.to_string(),
);
}
_ => {
return Err("keyframes are only supported for LTX-2 / LTX-2.3 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_ltx2_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_ltx2_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();
if overlap == 0 {
return Err(
"extend_overlap_frames must be >= 1 so the continuation has motion context".to_string(),
);
}
if overlap % 8 != 1 {
return Err(format!(
"extend_overlap_frames ({overlap}) must be 8k+1 (1, 9, 17, 25, …) so the carryover \
frames re-encode cleanly through the LTX-2 video VAE's 8x causal grid"
));
}
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(())
}
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, 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, 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 validate_generate_request_with_family(
req: &GenerateRequest,
family_hint: Option<&str>,
) -> Result<(), String> {
let family = resolved_family(&req.model, family_hint);
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
};
validate_generation_dimensions_composed(req.width, req.height, family, composition)?;
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()
));
}
}
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")?;
}
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")?;
}
}
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) {
if frames > 1 && (frames - 1) % step != 0 {
return Err(format!(
"frames ({frames}) must be {step}n+1 for current LTX-Video / LTX-2 models (e.g. 9, 17, 25, 33, 41, 49, …)"
));
}
}
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 if frames > MAX_FRAMES_GLOBAL {
return Err(format!("frames ({frames}) must be <= {MAX_FRAMES_GLOBAL}"));
}
}
if let Some(keyframes) = &req.keyframes {
validate_keyframes(keyframes, req.frames, family)?;
}
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("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(())
}
const SD15_DIMS: &[(u32, u32)] = &[(512, 512), (512, 768), (768, 512), (384, 512), (512, 384)];
const SDXL_DIMS: &[(u32, u32)] = &[
(1024, 1024),
(1152, 896),
(896, 1152),
(1216, 832),
(832, 1216),
(1344, 768),
(768, 1344),
(1536, 640),
(640, 1536),
];
const SD3_DIMS: &[(u32, u32)] = &[
(1024, 1024),
(1152, 896),
(896, 1152),
(1216, 832),
(832, 1216),
(1344, 768),
(768, 1344),
];
const FLUX_DIMS: &[(u32, u32)] = &[
(1024, 1024),
(1024, 768),
(768, 1024),
(1024, 576),
(576, 1024),
(768, 768),
];
const ZIMAGE_DIMS: &[(u32, u32)] = &[(1024, 1024), (1024, 768), (768, 1024)];
const QWEN_IMAGE_DIMS: &[(u32, u32)] = &[
(1328, 1328), (1024, 1024), (1152, 896), (896, 1152), (1216, 832), (832, 1216), (1344, 768), (768, 1344), (1664, 928), (928, 1664), (768, 768), (512, 512), ];
const WUERSTCHEN_DIMS: &[(u32, u32)] = &[(1024, 1024)];
const LTX_VIDEO_DIMS: &[(u32, u32)] = &[
(704, 480), (768, 512), (512, 512), (1024, 576), (1216, 704), (576, 1024), (768, 768), (512, 768), ];
const LTX2_DIMS: &[(u32, u32)] = &[
(704, 480), (768, 512), (512, 512), (1024, 576), (1216, 704), (704, 1216), (576, 1024), (768, 768), (512, 768), (1920, 1088), (1088, 1920), ];
pub fn recommended_dimensions(family: &str) -> &'static [(u32, u32)] {
match family {
"sd15" => SD15_DIMS,
"sdxl" => SDXL_DIMS,
"sd3" => SD3_DIMS,
"flux" => FLUX_DIMS,
"flux2" => FLUX_DIMS,
"z-image" => ZIMAGE_DIMS,
"qwen-image" => QWEN_IMAGE_DIMS,
"qwen-image-edit" => QWEN_IMAGE_DIMS,
"wuerstchen" => WUERSTCHEN_DIMS,
"ltx-video" => LTX_VIDEO_DIMS,
"ltx2" => LTX2_DIMS,
_ => &[],
}
}
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 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,
}]);
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 {
hdr_exr_dir: None,
hdr_exr_full_float: false,
guidance_overrides: 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,
strength: 0.75,
mask_image: None,
control_image: None,
control_model: None,
control_scale: 1.0,
expand: None,
original_prompt: 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,
}
}
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 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);
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);
let err = validate_generate_request(&req).unwrap_err();
assert!(err.contains("pipeline=t2a"), "got: {err}");
}
#[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
};
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.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.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.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(),
}]);
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(),
},
crate::KeyframeCondition {
frame: 16,
image: png_bytes(),
},
]);
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(),
}]);
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("LTX-Video / LTX-2"), "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_is_ltx2_only() {
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 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(),
}]);
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);
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 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,
});
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_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,
});
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,
});
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,
});
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,
});
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,
});
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,
});
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,
},
crate::LoraWeight {
path: "b.safetensors".to_string(),
scale: 0.4,
},
]);
assert!(
validate_generate_request(&req).is_ok(),
"SDXL + 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,
},
crate::LoraWeight {
path: "b.safetensors".into(),
scale: 0.4,
},
]);
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,
});
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,
},
crate::LoraWeight {
path: "b.safetensors".into(),
scale: 0.4,
},
]);
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,
});
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,
},
crate::LoraWeight {
path: "b.safetensors".into(),
scale: 0.4,
},
]);
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,
});
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,
});
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,
},
crate::LoraWeight {
path: "b.safetensors".into(),
scale: 0.4,
},
]);
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,
});
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,
},
crate::LoraWeight {
path: "lora-b.safetensors".into(),
scale: 0.4,
},
]);
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,
});
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,
});
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,
});
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,
},
crate::LoraWeight {
path: "b.safetensors".into(),
scale: 0.4,
},
]);
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,
});
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,
});
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,
},
crate::LoraWeight {
path: "b.safetensors".into(),
scale: 0.4,
},
]);
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,
});
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_has_native_resolution() {
let dims = recommended_dimensions("qwen-image");
assert!(
dims.contains(&(1328, 1328)),
"must include native 1328x1328"
);
assert!(dims.contains(&(512, 512)), "must include 512x512");
assert!(dims.contains(&(1024, 1024)), "must include 1024x1024");
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),
("z-image", 1024, 1024),
("qwen-image", 1024, 1024),
("qwen-image-edit", 1024, 1024),
("wuerstchen", 1024, 1024),
("ltx-video", 768, 512),
];
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"
);
}
}
#[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,
})
.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(),
}]);
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"));
}
}