Derived clips: link MP4 versions, list them apart, video Enhance output

Groundwork for Enhance "Animate" (#208): a video made from a photo.

- Version linking reads the XMP link from MP4 / MOV too (top-level uuid box
  with the XMP UUID, mdat seeked over), at ingest and on Enhance approve.
- New kind `animated`, a derived clip (CLIP_KINDS): hidden behind the photo
  like a version, but not counted in photo_version_counts / version_count,
  never a display choice (400 clip_not_displayable), and listed as `clips`
  in GET /image/versions. Approve returns the clip entry.
- Templates whose imageapi:save is ImageApiSaveVideoVersion report
  output "video" in /enhance/templates (no enhance.toml change). Previews
  already relay ComfyUI's content type (video/mp4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cameron CordesandClaude Opus 5.5 committed 2026-11-01 19:38:56 -05:00
1 parent 31c1b14d61
commit deb3d051ac
8 files changed
+308 -31

No files matched your search

+38
View File
@@ -19,6 +19,19 @@ pub const TITLE_REFERENCE: &str = "imageapi:reference";
/// Prompt-node inputs, in the order they are tried.
const PROMPT_INPUTS: &[&str] = &["text", "prompt", "value"];
/// What a template produces, from its `imageapi:save` node's class.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "lowercase")]
pub enum OutputKind {
/// Save Image Version: PNG versions.
Image,
/// Save Video Version: an MP4 (e.g. a clip animated from the photo).
Video,
}
/// The save node class that makes a template's output a video.
pub const VIDEO_SAVE_CLASS: &str = "ImageApiSaveVideoVersion";
/// Output nodes that write into ComfyUI's output folder on every API run.
const STRAY_OUTPUT_CLASSES: &[&str] = &[
"SaveImage",
@@ -38,6 +51,8 @@ pub struct Template {
/// `(node id, input name)` of the prompt text.
prompt: Option<(String, &'static str)>,
reference_id: Option<String>,
/// Image or video, from the save node's class.
pub output: OutputKind,
/// Startup warnings (stray output nodes).
pub warnings: Vec<String>,
}
@@ -143,6 +158,11 @@ impl Template {
));
}
}
let output = if workflow[&save_id]["class_type"].as_str() == Some(VIDEO_SAVE_CLASS) {
OutputKind::Video
} else {
OutputKind::Image
};
Ok(Template {
cfg,
workflow,
@@ -150,6 +170,7 @@ impl Template {
save_id,
prompt,
reference_id,
output,
warnings,
})
}
@@ -414,6 +435,23 @@ mod tests {
assert_ne!(input(&x.prompt, "5", "seed"), input(&y.prompt, "5", "seed"));
}
#[test]
fn output_kind_follows_the_save_node() {
assert_eq!(
load(RESTORE, PromptMode::Append).unwrap().output,
OutputKind::Image
);
let video = RESTORE.replace(
"\"ImageApiSaveImageVersion\"",
"\"ImageApiSaveVideoVersion\"",
);
assert_ne!(video, RESTORE);
assert_eq!(
load(&video, PromptMode::Append).unwrap().output,
OutputKind::Video
);
}
#[test]
fn random_seeds_fit_seedvr2s_u32_limit() {
// SeedVR2VideoUpscaler rejects seeds above 2^32 - 1 at validation.