Enhance Avisynth video workflow and metadata tools
This commit is contained in:
@@ -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)])
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user