use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Ltx2Generation {
V2,
V2_3,
}
impl Ltx2Generation {
pub fn label(self) -> &'static str {
match self {
Self::V2 => "LTX-2",
Self::V2_3 => "LTX-2.3",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)]
pub struct Ltx2ImagePreprocessingProfile {
pub generation: Ltx2Generation,
pub image_crf: u8,
}
const LTX2_IMAGE_CRF: u8 = 33;
pub fn ltx2_generation(
model_name: &str,
model_version_hint: Option<&str>,
) -> Option<Ltx2Generation> {
for marker in ["ltx-2", "ltx2"] {
let Some(idx) = model_name.find(marker) else {
continue;
};
let rest = &model_name[idx + marker.len()..];
let Some(after_dot) = rest.strip_prefix('.') else {
if marker == "ltx-2" {
return Some(Ltx2Generation::V2);
}
continue;
};
let digits: String = after_dot
.chars()
.take_while(|c| c.is_ascii_digit())
.collect();
if digits.is_empty() {
if marker == "ltx-2" {
return Some(Ltx2Generation::V2);
}
continue;
}
return generation_for_minor(digits.parse().ok()?);
}
let version = model_version_hint?;
let rest = version.strip_prefix("2.")?;
let digits = rest.split(['.', '-', '+']).next().unwrap_or("");
generation_for_minor(digits.parse().ok()?)
}
fn generation_for_minor(minor: u32) -> Option<Ltx2Generation> {
match minor {
0..=2 => Some(Ltx2Generation::V2),
3 => Some(Ltx2Generation::V2_3),
_ => None,
}
}
pub fn ltx2_image_preprocessing_profile(
generation: Ltx2Generation,
) -> Ltx2ImagePreprocessingProfile {
Ltx2ImagePreprocessingProfile {
generation,
image_crf: LTX2_IMAGE_CRF,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ltx2_generation_resolves_known_names_and_hints() {
assert_eq!(ltx2_generation("ltx-2-19b", None), Some(Ltx2Generation::V2));
assert_eq!(
ltx2_generation("ltx-2-19b-distilled:fp8", None),
Some(Ltx2Generation::V2)
);
assert_eq!(
ltx2_generation("ltx-2.3-22b", None),
Some(Ltx2Generation::V2_3)
);
assert_eq!(
ltx2_generation("ltx-2.3-22b-distilled:q8", None),
Some(Ltx2Generation::V2_3)
);
assert_eq!(
ltx2_generation("ltx2.3-vae", None),
Some(Ltx2Generation::V2_3)
);
assert_eq!(
ltx2_generation("cv:2752735", Some("2.3.0")),
Some(Ltx2Generation::V2_3)
);
assert_eq!(
ltx2_generation("hf:someone/some-repo", Some("2.0.1")),
Some(Ltx2Generation::V2)
);
assert_eq!(
ltx2_generation("cv:1", Some("2.1.0")),
Some(Ltx2Generation::V2)
);
assert_eq!(
ltx2_generation("cv:1", Some("2.2")),
Some(Ltx2Generation::V2)
);
}
#[test]
fn ltx2_generation_fails_closed_on_unknown() {
assert_eq!(ltx2_generation("cv:1", Some("2.4.0")), None);
assert_eq!(ltx2_generation("cv:1", Some("3.0")), None);
assert_eq!(ltx2_generation("cv:1", Some("2.30")), None);
assert_eq!(ltx2_generation("cv:1", Some("garbage")), None);
assert_eq!(ltx2_generation("cv:1", None), None);
assert_eq!(ltx2_generation("some-model", None), None);
assert_eq!(ltx2_generation("ltx-2.4-24b", None), None);
assert_eq!(ltx2_generation("ltx-2.30-22b", None), None);
assert_eq!(ltx2_generation("ltx2.30-vae", None), None);
}
#[test]
fn explicit_v2_minor_names_resolve_to_v2() {
for name in ["ltx-2.0-19b", "ltx-2.1-19b:fp8", "ltx-2.2-19b"] {
assert_eq!(
ltx2_generation(name, None),
Some(Ltx2Generation::V2),
"{name}"
);
}
}
#[test]
fn profile_maps_both_generations_to_crf_33() {
for generation in [Ltx2Generation::V2, Ltx2Generation::V2_3] {
let profile = ltx2_image_preprocessing_profile(generation);
assert_eq!(profile.generation, generation);
assert_eq!(profile.image_crf, 33);
}
}
#[test]
fn profile_serde_round_trips() {
let profile = ltx2_image_preprocessing_profile(Ltx2Generation::V2_3);
let json = serde_json::to_string(&profile).unwrap();
assert_eq!(json, r#"{"generation":"v2_3","image_crf":33}"#);
let back: Ltx2ImagePreprocessingProfile = serde_json::from_str(&json).unwrap();
assert_eq!(back, profile);
}
}