use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, Default)]
pub(crate) struct VideoModelInfo {
pub(crate) resolutions: Option<Vec<String>>,
pub(crate) aspect_ratios: Option<Vec<String>>,
pub(crate) durations: Option<Vec<i64>>,
pub(crate) sizes: Option<Vec<String>>,
pub(crate) frame_images: Option<Vec<String>>,
pub(crate) generate_audio: Option<bool>,
pub(crate) seed: Option<bool>,
}
#[derive(Debug, Default)]
pub(crate) struct VideoCatalog {
models: HashMap<String, VideoModelInfo>,
}
impl VideoCatalog {
#[must_use]
pub(crate) fn find(&self, model: &str) -> Option<&VideoModelInfo> {
self.models.get(model)
}
}
pub(crate) fn parse_catalog(body: &Value) -> anyhow::Result<VideoCatalog> {
Ok(VideoCatalog {
models: crate::tools::catalog_cache::parse_envelope(
body,
"Video models catalog",
parse_model,
)?,
})
}
fn parse_model(entry: &Value) -> Option<(String, VideoModelInfo)> {
let id = entry.get("id").and_then(Value::as_str)?.to_string();
let info = VideoModelInfo {
resolutions: parse_str_array(entry.get("supported_resolutions")),
aspect_ratios: parse_str_array(entry.get("supported_aspect_ratios")),
durations: parse_i64_array(entry.get("supported_durations")),
sizes: parse_str_array(entry.get("supported_sizes")),
frame_images: parse_str_array(entry.get("supported_frame_images")),
generate_audio: entry.get("generate_audio").and_then(Value::as_bool),
seed: entry.get("seed").and_then(Value::as_bool),
};
Some((id, info))
}
fn parse_str_array(v: Option<&Value>) -> Option<Vec<String>> {
v.and_then(Value::as_array).map(|arr| {
arr.iter()
.filter_map(Value::as_str)
.map(String::from)
.collect()
})
}
fn parse_i64_array(v: Option<&Value>) -> Option<Vec<i64>> {
v.and_then(Value::as_array)
.map(|arr| arr.iter().filter_map(Value::as_i64).collect())
}
static CATALOG: crate::tools::catalog_cache::Catalog<VideoCatalog> =
crate::tools::catalog_cache::Catalog::new(
"/videos/models",
"Video models catalog",
parse_catalog,
);
pub(crate) async fn get_catalog() -> Option<Arc<VideoCatalog>> {
let endpoint = crate::config::CONFIG.provider_endpoint();
get_catalog_for_endpoint(&endpoint).await
}
pub(crate) async fn get_catalog_for_endpoint(endpoint: &str) -> Option<Arc<VideoCatalog>> {
CATALOG.get(endpoint).await
}
#[cfg(test)]
pub(crate) fn seed_cache(catalog: Option<Arc<VideoCatalog>>) {
let endpoint = crate::providers::ensure_base_url(&crate::config::CONFIG.provider_endpoint());
CATALOG.seed(&endpoint, catalog);
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_parse_and_query_video_catalog() {
let catalog = parse_catalog(&json!({
"data": [
{
"id": "minimax/hailuo-3",
"canonical_slug": "minimax/hailuo-03-20260730",
"supported_resolutions": ["2K"],
"supported_aspect_ratios": ["16:9", "9:16", "1:1"],
"supported_durations": [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
"supported_sizes": null,
"supported_frame_images": ["first_frame", "last_frame"],
"generate_audio": true,
"seed": false,
"pricing_skus": { "duration_seconds": { "type": "second" } }
},
{
"id": "runway/aleph-2",
"canonical_slug": "runway/aleph-2-20260729",
"supported_resolutions": null,
"supported_durations": null,
"generate_audio": false,
"seed": true
},
{ "canonical_slug": "no-id-model" }
]
}))
.expect("valid fixture");
let hailuo = catalog.find("minimax/hailuo-3").expect("found by id");
assert_eq!(hailuo.resolutions.as_deref(), Some(&["2K".to_string()][..]));
assert_eq!(hailuo.durations.as_deref().map(<[i64]>::len), Some(11));
assert_eq!(hailuo.generate_audio, Some(true));
assert_eq!(hailuo.seed, Some(false));
let aleph = catalog.find("runway/aleph-2").expect("found by id");
assert_eq!(aleph.resolutions, None);
assert_eq!(aleph.durations, None);
assert_eq!(aleph.generate_audio, Some(false));
assert!(catalog.find("runway/aleph-2-20260729").is_none());
assert!(catalog.find("no-id-model").is_none());
}
#[test]
fn test_parse_catalog_tolerates_unknown_shapes() {
let catalog = parse_catalog(&json!({
"data": [
{ "id": "m1", "supported_durations": ["not-an-int"], "seed": "maybe" },
{ "id": "m2", "supported_resolutions": [42], "supported_aspect_ratios": null }
]
}))
.expect("tolerated");
let m1 = catalog.find("m1").expect("m1 present");
assert_eq!(m1.durations, Some(vec![]));
assert_eq!(m1.seed, None);
let m2 = catalog.find("m2").expect("m2 present");
assert_eq!(m2.resolutions, Some(vec![]));
assert_eq!(m2.aspect_ratios, None);
}
}