fix(inspect): video metadata carrier parity between Images and Inspect (#275)

* fix(video): select one prompt carrier from per-key or comment tags

Current ComfyUI savers write one container tag per key (prompt, workflow,
parameters, comfyassets_prompt_pairs); only the old VideoHelperSuite comment
envelope was read. select_video_carrier picks exactly one carrier in the
order per-key tags, comment envelope, description: the first with a role
key wins, else the first with a workflow, and carriers never merge.
Matroska upper-cased tag names fold on our side; differing fold-duplicates
fail the read like duplicate PNG keys, identical ones are one record.
Per-key values stay original-text; envelope objects are reserialized.

* fix(inspect): read per-key video prompt tags

The inspector asked ffprobe only for comment and description, so videos
written by core SaveVideo/SaveWEBM and current VideoHelperSuite (one tag per
key) were inspected as having no metadata. It now asks for
prompt,workflow,parameters,comfyassets_prompt_pairs,comment,description and
hands the tags to the shared carrier selector. ffprobe stdout is bounded at
the role and workflow budgets plus 64 KiB of envelope; past that it probes
once more without the workflow tag within the role budget and marks the
per-key workflow limited, while an over-budget envelope still fails the
read. Differing fold-duplicate tags fail like duplicate PNG keys.

* fix(video): scan videos written by current ComfyUI savers

The output scan and live monitor read container tags through
metadata_from_tags, which now delegates to the shared carrier selector, so
per-key prompt, workflow, parameters and comfyassets_prompt_pairs tags
(including Matroska upper-cased names) are ingested with the default
resolver profile exactly like the same graph in a PNG. Fold-duplicate tags
and null markers are skipped quietly. Pins the behavior with a mocked
ffprobe scan test and real MP4/MKV/WebM round trips when ffmpeg is present.

* docs(testing): document video carrier selection

Record the shared video carrier order (per-key tags, comment envelope,
description), the fold-duplicate rule, the 6 MiB + 64 KiB probe bound with
the role-only re-probe and limited workflow, and that scans read the same
carrier as the inspector.

* fix(inspect): size video probe bounds for JSON escaping, exact-case envelopes

ffprobe JSON-escapes tag text, so bounds measured on its stdout could mark a
video workflow limited, or fail a role record, that the identical PNG reads
within its decoded budget. The first probe now allows twice the role and
workflow budgets plus 64 KiB (12 MiB + 64 KiB) and the role-only re-probe
twice the role budget plus 64 KiB, leaving the decoded per-record budgets as
the only gate; a reader-level test reads near-budget quote-dense records as
PNG and as video and asserts identical metadata and formats.

Envelope keys (comment, description) are matched exactly as the legacy
readers did; only the four per-key carriers fold Matroska case. Fixtures can
write the legacy ilst comment atom (metadata_tags=False), and the docs now
say the admin scan reads the carrier while the live monitor still processes
images only, with a follow-up recorded in TESTING.md.
This commit is contained in:
Vito
2026-10-22 05:04:15 -07:00
committed by GitHub
parent f95f66d76a
commit bc14f29db7
8 changed files with 836 additions and 125 deletions
+52 -1
View File
@@ -2476,7 +2476,8 @@ def _write_mp4(path, comment):
class TestScanReadsVideos(AdminAPITestCase):
"""Videos saved by ComfyUI carry the graph in their comment tag."""
"""Videos carry the graph in per-key container tags (current savers) or in
the old VideoHelperSuite comment envelope; the scan reads one carrier."""
def setUp(self):
super().setUp()
@@ -2506,6 +2507,56 @@ class TestScanReadsVideos(AdminAPITestCase):
images = self.api.db.get_prompt_images(prompts[0]["id"])
self.assertTrue(images[0]["image_path"].endswith("clip.mp4"))
async def test_scan_links_per_key_tag_video_with_default_profile(self):
"""A current saver's per-key tags scan exactly like the same graph as PNG."""
from utils import video_metadata
_write_png(self.output_dir / "same.png", prompt_text="a cat")
graph = {
"1": {"class_type": "CLIPTextEncode", "inputs": {"text": "a cat"}},
"2": {"class_type": "CLIPTextEncode", "inputs": {"text": "bad"}},
"3": {
"class_type": "KSampler",
"inputs": {"positive": ["1", 0], "negative": ["2", 0]},
},
}
(self.output_dir / "clip.mkv").write_bytes(b"\x1aE\xdf\xa3 fixture")
probed = {
"format": {
"tags": {
"PROMPT": json.dumps(graph),
"WORKFLOW": '{"nodes":[]}',
"COMMENT": json.dumps({"prompt": "must not win"}),
}
}
}
GalleryConfig.MONITORING_DIRECTORIES = [self._root(self.output_dir)]
with (
patch.object(video_metadata.shutil, "which", return_value="/bin/ffprobe"),
patch.object(
video_metadata.subprocess,
"run",
return_value=MagicMock(returncode=0, stdout=json.dumps(probed)),
) as run,
self.assertNoLogs(level="ERROR"),
):
body = await (
await self.client.request("POST", "/prompt_manager/scan")
).text()
self.assertEqual(run.call_args.args[0][-1], str(self.output_dir / "clip.mkv"))
self.assertIn('"found": 2', body)
prompts = self.api.db.get_recent_prompts(limit=10)["prompts"]
self.assertEqual(sorted(p["text"] for p in prompts), ["a cat", "bad"])
positive = next(p for p in prompts if p["text"] == "a cat")
images = self.api.db.get_prompt_images(positive["id"])
self.assertEqual(
sorted(os.path.basename(image["image_path"]) for image in images),
["clip.mkv", "same.png"],
)
self.assertNotIn("must not win", json.dumps(prompts))
async def test_scan_without_ffprobe_skips_videos_quietly(self):
from unittest.mock import patch
from utils import video_metadata