Part of #208 (Animate a photo, built to support video Enhance next). Pairs with comfyui-imageapi-versions #5 / #6 (Save Video Version node).
What changes
Linking: version links are read from MP4 / MOV as well as PNG.
The XMP packet is in a top-level uuid box with the XMP UUID.
It's read by hand, like the PNG chunks: the walker reads only the box headers and seeks over mdat.
Covers ingest and the Enhance approve ingest.
Derived clips (kind animated, CLIP_KINDS): a video made from a photo. Linked, hidden and lifecycled exactly like a version, but:
not counted in photo_version_counts / version_count (one change in photo_summaries covers /photos, memories, search and /photos/exif);
never a display choice: PUT /image/versions/display answers 400 clip_not_displayable, and the display join ignores clips;
listed apart: GET /image/versions gains clips: [...], the same shape as versions. That's additive, and older apps simply don't see clips.
Enhance:
/enhance/templates gains output: "image" | "video", inferred from the template's imageapi:save class (ImageApiSaveVideoVersion = video), so there's no new enhance.toml key.
Approve returns the clip's entry.
The preview relay already passes ComfyUI's content type through (video/mp4).
Docs: CLAUDE.md, under Photo versions, Enhance, and the endpoint shapes.
Testing
cargo test --lib (DATABASE_URL=:memory:): 1063 pass.
New tests:
The MP4 XMP reader: a clip, .mov, a non-XMP extension, no XMP, junk bytes, and 64-bit box sizes.
The template output kind.
End to end: an .animated.mp4 linked by XMP is hidden from /photos, isn't counted, is listed under clips (from the photo's path and the clip's own), and is refused as the display version.
fmt and clippy are clean.
Node side: the Python node's box layout was checked against exiftool on a real ffmpeg clip (node pack #5).
Part of #208 (Animate a photo, built to support video Enhance next). Pairs with comfyui-imageapi-versions #5 / #6 (Save Video Version node).
## What changes
- **Linking:** version links are read from MP4 / MOV as well as PNG.
- The XMP packet is in a top-level `uuid` box with the XMP UUID.
- It's read by hand, like the PNG chunks: the walker reads only the box headers and seeks over `mdat`.
- Covers ingest and the Enhance approve ingest.
- **Derived clips (kind `animated`, `CLIP_KINDS`):** a video made from a photo. Linked, hidden and lifecycled exactly like a version, but:
- not counted in `photo_version_counts` / `version_count` (one change in `photo_summaries` covers `/photos`, memories, search and `/photos/exif`);
- never a display choice: `PUT /image/versions/display` answers 400 `clip_not_displayable`, and the display join ignores clips;
- listed apart: `GET /image/versions` gains `clips: [...]`, the same shape as `versions`. That's additive, and older apps simply don't see clips.
- **Enhance:**
- `/enhance/templates` gains `output: "image" | "video"`, inferred from the template's `imageapi:save` class (`ImageApiSaveVideoVersion` = video), so there's no new `enhance.toml` key.
- Approve returns the clip's entry.
- The preview relay already passes ComfyUI's content type through (`video/mp4`).
- **Docs:** CLAUDE.md, under Photo versions, Enhance, and the endpoint shapes.
## Testing
- **`cargo test --lib`** (`DATABASE_URL=:memory:`): 1063 pass.
- **New tests:**
- The MP4 XMP reader: a clip, `.mov`, a non-XMP extension, no XMP, junk bytes, and 64-bit box sizes.
- The template output kind.
- End to end: an `.animated.mp4` linked by XMP is hidden from `/photos`, isn't counted, is listed under `clips` (from the photo's path and the clip's own), and is refused as the display version.
- **fmt and clippy** are clean.
- **Node side:** the Python node's box layout was checked against exiftool on a real ffmpeg clip (node pack #5).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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>
cameron
merged commit 19cbc9d59c into master2026-11-02 00:39:38 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Part of #208 (Animate a photo, built to support video Enhance next). Pairs with comfyui-imageapi-versions #5 / #6 (Save Video Version node).
What changes
uuidbox with the XMP UUID.mdat.animated,CLIP_KINDS): a video made from a photo. Linked, hidden and lifecycled exactly like a version, but:photo_version_counts/version_count(one change inphoto_summariescovers/photos, memories, search and/photos/exif);PUT /image/versions/displayanswers 400clip_not_displayable, and the display join ignores clips;GET /image/versionsgainsclips: [...], the same shape asversions. That's additive, and older apps simply don't see clips./enhance/templatesgainsoutput: "image" | "video", inferred from the template'simageapi:saveclass (ImageApiSaveVideoVersion= video), so there's no newenhance.tomlkey.video/mp4).Testing
cargo test --lib(DATABASE_URL=:memory:): 1063 pass..mov, a non-XMP extension, no XMP, junk bytes, and 64-bit box sizes..animated.mp4linked by XMP is hidden from/photos, isn't counted, is listed underclips(from the photo's path and the clip's own), and is refused as the display version.🤖 Generated with Claude Code