use std::fmt::Write as _;
use std::fs;
use std::path::{Path, PathBuf};
use mold_core::generation_profile::{
resolution_qualification_record, AdapterControlProfile, AspectGroup, ControlMode,
FeatureControlProfile, FloatControl, FpsControl, GenerationCapabilitiesProfile,
GenerationDefaultsProfile, GenerationProfileSet, GenerationRecipeProfile, IntegerControl,
OffBucketPolicy, OutputCapabilitiesProfile, ProfileProvenance, ProvenanceKind, RecipeSelector,
ResolutionDomain, ResolutionPreset, ResolutionProfile, TemporalProfile,
WanRecipeCapabilitiesProfile,
};
use mold_core::manifest::known_manifests;
use mold_core::{
GuidanceCapabilities, Ltx2PipelineMode, OutputFormat, Scheduler, SourceImageCapability,
};
use serde::Serialize;
use ts_rs::TS;
const JSON_PATH: &str = "docs/generated/generation-profiles-v1.json";
const MARKDOWN_PATH: &str = "docs/model-resolution-matrix.md";
const TYPESCRIPT_PATH: &str = "studio/lib/generated/generationProfileV1.ts";
#[derive(Serialize)]
struct ModelIdentity<'a> {
model: &'a str,
family: &'a str,
hidden: bool,
}
#[derive(Serialize)]
struct ProfileDocument<'a> {
models: Vec<ModelIdentity<'a>>,
profile: GenerationProfileSet,
}
#[derive(Serialize)]
struct RegistryDocument<'a> {
schema_version: u32,
resolution_candidates: Vec<ResolutionCandidateDocument<'a>>,
profiles: Vec<ProfileDocument<'a>>,
}
#[derive(Serialize)]
struct ResolutionCandidateDocument<'a> {
family: &'a str,
source: &'a str,
revision: &'a str,
qualified: bool,
evidence: &'a str,
candidates: &'a [(u32, u32)],
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let check = match std::env::args().nth(1).as_deref() {
None => false,
Some("--check") => true,
Some(argument) => return Err(format!("unknown argument: {argument}").into()),
};
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("mold-core must live under crates/")
.to_path_buf();
let registry = model_profiles();
let json = format!("{}\n", serde_json::to_string_pretty(®istry)?);
let markdown = render_markdown(®istry);
let typescript = render_typescript_contract();
update(&root.join(JSON_PATH), &json, check)?;
update(&root.join(MARKDOWN_PATH), &markdown, check)?;
update(&root.join(TYPESCRIPT_PATH), &typescript, check)?;
Ok(())
}
fn render_typescript_contract() -> String {
let mut out = String::from(
"// Generated by `cargo run -p mold-ai-core --bin generate_generation_profiles`.\n\
// Do not edit: these declarations come directly from the Rust wire types.\n\n",
);
macro_rules! declaration {
($type:ty) => {{
out.push_str("export ");
let declaration = <$type as TS>::decl();
for (index, line) in declaration.lines().enumerate() {
if index > 0 {
out.push('\n');
}
out.push_str(line.trim_end());
}
out.push_str("\n\n");
}};
}
declaration!(ResolutionDomain);
declaration!(OffBucketPolicy);
declaration!(ControlMode);
declaration!(ProvenanceKind);
declaration!(ProfileProvenance);
declaration!(ResolutionPreset);
declaration!(AspectGroup);
declaration!(ResolutionProfile);
declaration!(IntegerControl);
declaration!(FloatControl);
declaration!(FpsControl);
declaration!(TemporalProfile);
declaration!(GenerationDefaultsProfile);
declaration!(Ltx2PipelineMode);
declaration!(RecipeSelector);
declaration!(FeatureControlProfile);
declaration!(AdapterControlProfile);
declaration!(OutputFormat);
declaration!(OutputCapabilitiesProfile);
declaration!(WanRecipeCapabilitiesProfile);
declaration!(GuidanceCapabilities);
declaration!(SourceImageCapability);
declaration!(Scheduler);
declaration!(GenerationCapabilitiesProfile);
declaration!(GenerationRecipeProfile);
declaration!(GenerationProfileSet);
out.push_str("export const LEGACY_RESOLUTION_PRESETS_V1 = {\n");
for family in [
"sd15",
"sdxl",
"sd3",
"flux",
"flux2",
"z-image",
"qwen-image",
"qwen-image-edit",
"wuerstchen",
"ltx-video",
"ltx2",
"wan",
"minimax-h3",
] {
let groups = mold_core::generation_profile::family_aspect_groups(family);
let presets = mold_core::generation_profile::family_presets(family)
.iter()
.map(|&(width, height)| {
let group = groups
.iter()
.find(|group| {
group
.presets
.iter()
.any(|preset| preset.width == width && preset.height == height)
})
.expect("every family preset belongs to an aspect group");
format!(
"{{ width: {width}, height: {height}, aspect: {:?} }}",
group.label
)
})
.collect::<Vec<_>>()
.join(", ");
writeln!(out, " {family:?}: [{presets}],").unwrap();
}
out.push_str("} as const;\n");
out
}
fn model_profiles() -> RegistryDocument<'static> {
let mut models = known_manifests()
.iter()
.filter(|manifest| manifest.is_generation_model())
.map(|manifest| {
let family = manifest.family.as_str();
let profile = mold_core::generation_profile_for_manifest(manifest);
(
ModelIdentity {
model: &manifest.name,
family,
hidden: manifest.hidden,
},
profile,
)
})
.collect::<Vec<_>>();
models.sort_by(|left, right| {
left.0
.family
.cmp(right.0.family)
.then_with(|| left.0.model.cmp(right.0.model))
});
let mut profiles: Vec<ProfileDocument<'static>> = Vec::new();
for (model, profile) in models {
if let Some(existing) = profiles
.iter_mut()
.find(|entry| entry.profile.profile_hash == profile.profile_hash)
{
existing.models.push(model);
} else {
profiles.push(ProfileDocument {
models: vec![model],
profile,
});
}
}
RegistryDocument {
schema_version: mold_core::GENERATION_PROFILE_SCHEMA_VERSION,
resolution_candidates: ["z-image", "qwen-image"]
.into_iter()
.map(|family| {
let record = resolution_qualification_record(family)
.expect("candidate family must have a qualification record");
ResolutionCandidateDocument {
family: record.family,
source: record.source,
revision: record.revision,
qualified: record.qualified,
evidence: record.evidence,
candidates: record.candidates,
}
})
.collect(),
profiles,
}
}
fn render_markdown(registry: &RegistryDocument<'_>) -> String {
let mut out = String::from(
"<!-- Generated by `cargo run -p mold-ai-core --bin generate_generation_profiles`. -->\n\
# Model generation profile matrix\n\n\
This file is generated from the typed registry in `mold-core`. Do not edit it by hand.\n\n\
Regenerate with `cargo run -p mold-ai-core --bin generate_generation_profiles`. Check drift with the same command plus `--check`. The machine-readable companion is [`generated/generation-profiles-v1.json`](generated/generation-profiles-v1.json).\n\n\
## Contract semantics\n\n\
- **Dynamic** accepts any canvas satisfying the listed alignment and bounds; presets are qualified recommendations.\n\
- **Buckets** accepts only the exact listed presets.\n\
- **Source driven** derives its canvas from the source; listed presets are guidance for source fitting.\n\
- Limits in this document are effective Mold admission limits. Provenance records whether their authored source is upstream or Mold policy.\n\n",
);
out.push_str("## Upstream resolution qualification records\n\n");
out.push_str(
"Pinned upstream dimensions become supported recommendations after their authored contract is verified against Mold's admission and decoded output-delivery path. This is not a per-size runtime-performance claim: dynamic families may qualify a pinned oracle when those paths are resolution-generic, while bucketed or size-sensitive families additionally require an exact-size generation campaign. Unqualified candidates remain absent from the wire profile aspect groups.\n\n",
);
for candidate in ®istry.resolution_candidates {
writeln!(out, "### `{}`\n", candidate.family).unwrap();
writeln!(
out,
"Status: qualified `{}`. Evidence: `{}`.\n",
candidate.qualified, candidate.evidence
)
.unwrap();
writeln!(
out,
"Pinned source: [{}]({}) at `{}`.\n",
candidate.source, candidate.source, candidate.revision
)
.unwrap();
let dimensions = candidate
.candidates
.iter()
.map(|(width, height)| format!("`{width}x{height}`"))
.collect::<Vec<_>>()
.join(", ");
writeln!(out, "Candidates: {dimensions}.\n").unwrap();
}
let mut current_family = "";
for entry in ®istry.profiles {
let family = entry.models[0].family;
if family != current_family {
current_family = family;
writeln!(out, "## `{current_family}`\n").unwrap();
}
let title = if entry.models.len() == 1 {
format!("`{}`", entry.models[0].model)
} else {
format!("Profile `{}`", entry.profile.profile_id)
};
writeln!(
out,
"### {title}\n\nSchema {} · hash `{}` · default recipe `{}`\n",
entry.profile.schema_version,
entry.profile.profile_hash,
entry.profile.default_recipe_id,
)
.unwrap();
out.push_str("Models: ");
for (index, model) in entry.models.iter().enumerate() {
if index > 0 {
out.push_str(", ");
}
write!(
out,
"`{}`{}",
model.model,
if model.hidden { " (policy-hidden)" } else { "" }
)
.unwrap();
}
out.push_str(".\n\n");
for recipe in &entry.profile.recipes {
render_recipe(&mut out, recipe);
}
}
while out.ends_with("\n\n") {
out.pop();
}
out
}
fn render_recipe(out: &mut String, recipe: &GenerationRecipeProfile) {
writeln!(out, "#### {} (`{}`)\n", recipe.label, recipe.id).unwrap();
let resolution = &recipe.resolution;
let domain = match resolution.domain {
ResolutionDomain::Dynamic => "dynamic",
ResolutionDomain::Buckets => "buckets",
ResolutionDomain::SourceDriven => "source driven",
ResolutionDomain::None => "none",
};
let axis = resolution
.max_axis_pixels
.map_or_else(|| "none".to_string(), |value| value.to_string());
let aspect = match (resolution.min_aspect_ratio, resolution.max_aspect_ratio) {
(Some(min), Some(max)) => format!("{min}–{max}"),
_ => "unbounded".to_string(),
};
writeln!(
out,
"- Resolution: {domain}; alignment `{}`; minimum `{}x{}`; maximum `{}` pixels; axis limit `{axis}`; aspect range `{aspect}`.",
resolution.alignment,
resolution.min_width,
resolution.min_height,
resolution.max_pixels,
)
.unwrap();
writeln!(
out,
"- Defaults: `{}x{}`, {} steps, guidance {}.",
recipe.defaults.width,
recipe.defaults.height,
recipe.defaults.steps,
recipe.defaults.guidance,
)
.unwrap();
writeln!(
out,
"- Steps: {}–{} by {}; guidance: {}–{} by {} ({:?}).",
recipe.steps.min,
recipe.steps.max,
recipe.steps.step,
recipe.guidance.min,
recipe.guidance.max,
recipe.guidance.step,
recipe.guidance.mode,
)
.unwrap();
if let Some(temporal) = &recipe.temporal {
let fps = match temporal.fps {
FpsControl::Fixed { value } => format!("fixed {value}"),
FpsControl::Adjustable {
default,
min,
max,
step,
} => format!("{min}–{max} by {step}, default {default}"),
};
writeln!(
out,
"- Temporal: frames {}–{} on `{}n+{}` (default {}); FPS {fps}; duration limit {}.",
temporal.frames.min,
temporal.frames.max,
temporal.frames.step,
temporal.frame_offset,
temporal.frames.default,
temporal
.max_duration_seconds
.map_or_else(|| "none".to_string(), |value| format!("{value}s")),
)
.unwrap();
}
if resolution.aspect_groups.is_empty() {
out.push_str("- Presets: none.\n");
} else {
out.push_str("\n| Exact ratio | Qualified presets |\n|---|---|\n");
for group in &resolution.aspect_groups {
let presets = group
.presets
.iter()
.map(|preset| format!("`{}x{}` ({})", preset.width, preset.height, preset.tier))
.collect::<Vec<_>>()
.join(", ");
writeln!(out, "| `{}` | {presets} |", group.label).unwrap();
}
}
if !recipe.provenance.is_empty() {
out.push_str("\nProvenance: ");
for (index, provenance) in recipe.provenance.iter().enumerate() {
if index > 0 {
out.push_str("; ");
}
let revision = provenance
.revision
.as_deref()
.map_or_else(String::new, |revision| format!(" at `{revision}`"));
let evidence = provenance
.evidence
.as_deref()
.map_or_else(String::new, |evidence| format!(", evidence: `{evidence}`"));
if provenance.source.starts_with("http") {
write!(
out,
"[{:?}]({}){revision}, qualified: `{}`{evidence}",
provenance.kind, provenance.source, provenance.qualified,
)
.unwrap();
} else {
write!(
out,
"{:?} `{}`{revision}, qualified: `{}`{evidence}",
provenance.kind, provenance.source, provenance.qualified,
)
.unwrap();
}
}
out.push_str(".\n");
}
out.push('\n');
}
fn update(path: &Path, contents: &str, check: bool) -> Result<(), Box<dyn std::error::Error>> {
if check {
let existing = fs::read_to_string(path)
.map_err(|error| format!("{} is missing or unreadable: {error}", path.display()))?;
if existing != contents {
return Err(format!(
"{} is stale; run the generation command without --check",
path.display()
)
.into());
}
return Ok(());
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, contents)?;
println!("generated {}", path.display());
Ok(())
}