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:
1 parent
31c1b14d61
commit
deb3d051ac
8 files changed
+308
-31
No files matched your search
@@ -360,7 +360,12 @@ hash — design it alongside Branch D.
|
||||
|
||||
A **version** is a non-destructive restoration, upscale or edit of a photo that
|
||||
an external process (the ComfyUI node in `comfyui-imageapi-versions`) writes next
|
||||
to the original, as `<dir>/aiedits/<original name>.<kind>[.<n>].png`. ImageApi
|
||||
to the original, as `<dir>/aiedits/<original name>.<kind>[.<n>].png`. A
|
||||
**derived clip** (kind `animated`, `CLIP_KINDS`) is a video made from a photo
|
||||
(Enhance "Animate", `<name>.animated[.<n>].mp4`): linked, hidden and lifecycled
|
||||
exactly like a version, but never counted as one (`photo_version_counts`,
|
||||
`version_count`), never a display choice (`PUT /image/versions/display` answers 400
|
||||
`clip_not_displayable`), and listed apart, as `clips` in `GET /image/versions`. ImageApi
|
||||
only detects it (library roots stay read-only) and shows original + versions as
|
||||
**one photo**; a *global* choice (not per user) says which file is displayed.
|
||||
Code: `src/database/version_dao.rs` (tables, DAO, SQL predicates),
|
||||
@@ -376,8 +381,9 @@ for the `link_versions` binary).
|
||||
global choice; no row = the original, and a display file with no live row reads as
|
||||
the original.
|
||||
- **Linking at ingest** (`watcher::process_new_files`, before any derived work):
|
||||
(1) a PNG's XMP `xmpMM:DerivedFrom/stRef:documentID = blake3:<hex>` (iTXt chunk
|
||||
`XML:com.adobe.xmp`, read by hand, no decode); (2) the file-name convention next
|
||||
(1) the file's XMP `xmpMM:DerivedFrom/stRef:documentID = blake3:<hex>`: a PNG's
|
||||
iTXt chunk `XML:com.adobe.xmp`, or an MP4 / MOV's top-level `uuid` box with the XMP
|
||||
UUID (both read by hand, no decode; `mdat` is seeked over); (2) the file-name convention next
|
||||
to an indexed original in the same library (same dir, or the parent of
|
||||
`aiedits/`). Name links are retried every tick (`sweep_name_links`, paged by a
|
||||
per-library resume cursor that wraps, so candidates that never resolve can't starve
|
||||
@@ -455,7 +461,9 @@ auth and stays LAN-only, so ImageApi relays previews too.
|
||||
`file`, `prompt_mode`, `prompt_join`, `output_megapixels`).
|
||||
- **Templates** are API-format workflow exports, read-only (never edit them from
|
||||
here). Nodes are found by `_meta.title`: `imageapi:source` (required, gets the
|
||||
photo path), `imageapi:save` (required, forced to `require_approval = true`),
|
||||
photo path), `imageapi:save` (required, forced to `require_approval = true`; a
|
||||
`ImageApiSaveVideoVersion` there makes the template's `output` `video`, an MP4 clip,
|
||||
else `image`),
|
||||
`imageapi:prompt` (optional; fills the first of `text` / `prompt` / `value`),
|
||||
`imageapi:reference` (optional). `prompt_mode = "append"`: the user's text is joined
|
||||
after the template's own prompt with `prompt_join` (default one space); empty text
|
||||
@@ -503,7 +511,7 @@ GET /enhance/templates
|
||||
"templates": [{"name","label","description","accepts_prompt",
|
||||
"prompt_mode": "append"|"replace", "prompt_required",
|
||||
"accepts_reference","default","output_megapixels",
|
||||
"warn_above_megapixels"}]}
|
||||
"warn_above_megapixels", "output": "image"|"video"}]}
|
||||
POST /enhance {"path","template","prompt"?,"reference_path"?,"library"?} -> job
|
||||
GET /enhance/jobs/{id} -> job
|
||||
GET /enhance/jobs?path=&library= -> {"jobs": [job, ...]} newest first, discarded hidden
|
||||
@@ -511,8 +519,9 @@ GET /enhance/prompts?templates=a,b&limit=10 (templates optional = all; limit 1
|
||||
-> {"prompts": [{"prompt","template","last_used","uses"}]} distinct trimmed prompts, newest
|
||||
use first; template/last_used from the newest job; discarded + failed jobs count;
|
||||
DB only (no library scope, answers even when ComfyUI is down or Enhance is off)
|
||||
GET /enhance/jobs/{id}/preview/{n} -> the staged PNG (relayed from ComfyUI /view)
|
||||
POST /enhance/jobs/{id}/approve {"n"} -> a /image/versions `versions[]` entry
|
||||
GET /enhance/jobs/{id}/preview/{n} -> the staged PNG or MP4 (relayed from ComfyUI /view,
|
||||
with its content type)
|
||||
POST /enhance/jobs/{id}/approve {"n"} -> a /image/versions `versions[]` (or `clips[]`) entry
|
||||
POST /enhance/jobs/{id}/discard -> job (hidden from the list; ComfyUI untouched)
|
||||
|
||||
job = {"id","status","queue_position"?,"error"?,"path","library_id","template",
|
||||
@@ -737,7 +746,8 @@ POST /image/tags/bulk (many files, one transaction: { file_names, add: [names
|
||||
GET /image/versions?path=...&library=... (path of the original or any version ->
|
||||
{ original:{path,library_id,hash}, versions:[{path,
|
||||
library_id,hash,kind,source,created_at,references,
|
||||
width,height}], display_hash })
|
||||
width,height}], clips:[same shape, kind animated],
|
||||
display_hash })
|
||||
POST /enhance ... (Enhance on demand; see "Enhance on demand")
|
||||
PUT /image/versions/display (body { path, display_hash|null, library? }; global choice)
|
||||
// /photos hides versions behind their original (include_versions=true lists them) and adds
|
||||
|
||||
Reference in new issue
Block a user