Enhance Avisynth video workflow and metadata tools

This commit is contained in:
ajp_anton
2026-07-30 03:20:19 +00:00
parent d60ce51b14
commit 100cad1cb5
26 changed files with 2350 additions and 1161 deletions
+114
View File
@@ -0,0 +1,114 @@
from __future__ import annotations
import json
import subprocess
from pathlib import Path
from typing import Any
def extract_hdr10plus_metadata(
*,
ffmpeg: Path,
hdr10plus_tool: Path,
source: Path,
output: Path,
) -> None:
"""Extract HDR10+ JSON without creating a full intermediate video file."""
output.parent.mkdir(parents=True, exist_ok=True)
ffmpeg_process = subprocess.Popen(
[
str(ffmpeg),
"-v", "error", "-i", str(source), "-map", "0:v:0",
"-c", "copy", "-bsf:v", "hevc_mp4toannexb", "-f", "hevc", "-",
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
assert ffmpeg_process.stdout is not None
try:
extracted = subprocess.run(
[str(hdr10plus_tool), "extract", "-o", str(output), "-"],
stdin=ffmpeg_process.stdout,
capture_output=True,
text=True,
)
finally:
ffmpeg_process.stdout.close()
ffmpeg_stderr = ffmpeg_process.stderr.read().decode(errors="replace")
ffmpeg_process.stderr.close()
ffmpeg_returncode = ffmpeg_process.wait()
if ffmpeg_returncode != 0:
raise RuntimeError(
f"FFmpeg could not extract the HEVC stream from {source}:\n{ffmpeg_stderr.strip()}"
)
if extracted.returncode != 0:
raise RuntimeError(
f"hdr10plus_tool could not extract metadata from {source}:\n"
f"{extracted.stderr.strip() or extracted.stdout.strip()}"
)
if not output.is_file():
raise RuntimeError(f"hdr10plus_tool did not create metadata JSON for {source}")
def select_hdr10plus_frames(
*,
source_json: Path,
source_frames: list[int],
output_json: Path,
) -> None:
"""Keep metadata for the encoded source frames and rebuild scene indices."""
payload = json.loads(source_json.read_text(encoding="utf-8"))
entries = payload.get("SceneInfo")
if not isinstance(entries, list) or not entries:
raise RuntimeError(f"HDR10+ JSON has no SceneInfo entries: {source_json}")
if not source_frames:
raise RuntimeError("Cannot create HDR10+ metadata for an empty output")
if min(source_frames) < 0 or max(source_frames) >= len(entries):
raise RuntimeError(
"HDR10+ metadata frame count does not cover the validated source frames "
f"({len(entries)} metadata entries, highest source frame {max(source_frames)})"
)
selected = [dict(entries[index]) for index in source_frames]
_reindex_scenes(selected)
payload["SceneInfo"] = selected
payload["SceneInfoSummary"] = _scene_summary(selected)
output_json.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
def _reindex_scenes(entries: list[dict[str, Any]]) -> None:
scene_id = 0
scene_frame_index = 0
previous: dict[str, Any] | None = None
for sequence_frame_index, entry in enumerate(entries):
if previous is not None and _scene_payload(entry) != _scene_payload(previous):
scene_id += 1
scene_frame_index = 0
entry["SceneFrameIndex"] = scene_frame_index
entry["SceneId"] = scene_id
entry["SequenceFrameIndex"] = sequence_frame_index
scene_frame_index += 1
previous = entry
def _scene_payload(entry: dict[str, Any]) -> dict[str, Any]:
return {
key: value
for key, value in entry.items()
if key not in {"SceneFrameIndex", "SceneId", "SequenceFrameIndex"}
}
def _scene_summary(entries: list[dict[str, Any]]) -> dict[str, list[int]]:
starts = [
index
for index, entry in enumerate(entries)
if entry.get("SceneFrameIndex") == 0
]
return {
"SceneFirstFrameIndex": starts,
"SceneFrameNumbers": [
next_start - start
for start, next_start in zip(starts, [*starts[1:], len(entries)])
],
}