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
+80 -26
View File
@@ -8,6 +8,7 @@ from datetime import timedelta, timezone
from pathlib import Path
from statistics import median
from tools.avisynth_paths import avs_path
from tools.video_formatting import (
format_frame_count,
format_framerate,
@@ -16,7 +17,13 @@ from tools.video_formatting import (
video_parts,
)
from tools.video_inputs import VideoInput
from tools.video_probe import VideoProbe
from tools.video_probe import (
VideoProbe,
avisynth_primaries_from_color_primaries,
avisynth_range_from_color_range,
avisynth_transfer_from_color_transfer,
hdr_kind,
)
WORKSPACE_DIR = Path("video_workspace")
@@ -197,7 +204,7 @@ def _visible_script(
item: VideoInput,
probe: VideoProbe,
) -> str:
import_path = _avs_path(os.path.relpath(source_script, visible_script.parent))
import_path = avs_path(os.path.relpath(source_script, visible_script.parent))
audio_script_name = f"{visible_script.stem}_audio.avs"
timing_kind = probe.timing.timing_kind.upper()
final_clip = _default_final_clip(probe)
@@ -220,24 +227,37 @@ def _visible_script(
"# Frame properties must be preserved.\n"
"\n"
"# Bundled helper functions:\n"
"# MBT_Drop(last, int start, int \"end\"), MBT_Undrop(...)\n"
"# MBT_DropEvery(last, int cycle, int offset0, ...), MBT_UndropEvery(...)\n"
"# MBT_Drop(clip c, int start, int \"end\"), MBT_Undrop(...)\n"
"# MBT_DropEvery(clip c, int cycle, int offset0, ...), MBT_UndropEvery(...)\n"
"# Similar syntax as Trim/SelectEvery. end defaults to -1, meaning one frame.\n"
"# Examples: MBT_Drop(last, 100), MBT_Drop(last, 100, -20),\n"
"# MBT_DropEvery(last, 4, 1, 3)\n"
"# MBT_Info(last, int \"size\", int \"align\")\n"
"# MBT_Info(clip c, int \"size\", int \"align\")\n"
"# Overlay source frame, video time, absolute time, duration, and drop marker.\n"
"# MBT_AbsoluteTimeStretch(clip c, float factor)\n"
"# Map source timestamps to real time, anchored to the metadata end timestamp.\n"
"# Example: MBT_AbsoluteTimeStretch(last, 1.0/8.0) for 8x slow motion.\n"
"# Resize(clip v, int \"w\", int \"h\", float \"dar\", float \"xcrop\", float \"ycrop\", int \"pos\", bool \"linear\", string \"input_matrix\", string \"output_matrix\")\n"
"# Resize/crop. linear auto-enables for large downscales unless set explicitly.\n"
"# MBT_CorrectMatrix(last)\n"
"# Convert to the resolution-expected matrix using _Matrix as input metadata.\n"
"# Resize/crop. HDR linear resizing requires HDRTools; linear=false preserves HDR unchanged.\n"
"# MBT_CorrectMatrix(clip c, string \"input_matrix\", string \"output_matrix\")\n"
"# Correct SDR matrix metadata. HDR color systems are preserved.\n"
"# MBT_ToSDR(clip c, float \"hdr_peak\", float \"sdr_peak\", string \"method\", float \"exposure\", bool \"chroma_correction\", string \"output_matrix\")\n"
"# HDR to SDR. MBT_ToSDR(10000, 125, \"MPC\") uses a 10,000 cd/m² PQ source\n"
"# peak and a 125 cd/m² SDR display target. Methods: MPC (default), Hable, Mobius,\n"
"# Reinhard, ACES, BT2446C. exposure overrides the method's default curve scale; chroma_correction\n"
"# applies only to BT2446C.\n"
"# MPC applies the Hable curve to XYZ luminance, then preserves chromaticity and compresses\n"
"# target-gamut chroma. Defaults: PQ hdr_peak=10000, HLG hdr_peak=1000, sdr_peak=125,\n"
"# chroma_correction=false, output_matrix=Rec709/Rec601 by output size.\n"
"# HDR10+ metadata is dynamic and is not used by HDRTools; this is a static tone map.\n"
"# Add LoadPlugin(\"path/to/HDRTools.dll\") below when using HDRTools.\n"
"# DeShake(clip c, int \"w\", int \"h\", float \"dar\", float \"crop\", float \"xcrop\", float \"ycrop\", int \"pos\", bool \"zoom\", bool \"rot\", float \"freq\")\n"
"# Stabilize through MVTools + DePan; those plugins must be loaded separately.\n"
"# Cropf(...)\n"
"# Cropf(clip c, float l, float t, float w, float h)\n"
"# Crop using fractional dimensions.\n"
"# RotateCrop(clip c, float angle, float \"dar\")\n"
"# Rotate with manyPlus Turn, then crop the largest background-free rectangle.\n"
"# FixContrast(...)\n"
"# FixContrast(clip c, int \"low\", int \"high\")\n"
"# Remap luma levels to limited-range video luma.\n"
"\n"
"# Optional audio edits:\n"
@@ -265,23 +285,23 @@ def _hidden_source_script(
duration_values: Path,
duration_label_values: Path,
) -> str:
source_path = _avs_path(str(item.path.resolve()))
import_path = _avs_path(os.path.relpath(helper_script, source_script.parent))
cache_path = _avs_path(
source_path = avs_path(str(item.path.resolve()))
import_path = avs_path(os.path.relpath(helper_script, source_script.parent))
cache_path = avs_path(
str(source_script.with_name(f"{item.collapsed_relative.name}.ffindex").resolve())
)
audio_cache_path = _avs_path(
audio_cache_path = avs_path(
str(source_script.with_name(f"{item.collapsed_relative.name}.audio.ffindex").resolve())
)
timestamp_values_path = _avs_path(str(timestamp_values.resolve()))
frame_time_values_path = _avs_path(str(frame_time_values.resolve()))
frame_time_label_values_path = _avs_path(str(frame_time_label_values.resolve()))
duration_values_path = _avs_path(str(duration_values.resolve()))
duration_label_values_path = _avs_path(str(duration_label_values.resolve()))
timestamp_values_path = avs_path(str(timestamp_values.resolve()))
frame_time_values_path = avs_path(str(frame_time_values.resolve()))
frame_time_label_values_path = avs_path(str(frame_time_label_values.resolve()))
duration_values_path = avs_path(str(duration_values.resolve()))
duration_label_values_path = avs_path(str(duration_label_values.resolve()))
if ffms2_plugin_path is None:
plugin_setup = "# FFMS2 is expected from Avisynth autoload or a previously loaded plugin.\n"
else:
plugin_setup = f'LoadPlugin("{_avs_path(str(ffms2_plugin_path.resolve()))}")\n'
plugin_setup = f'LoadPlugin("{avs_path(str(ffms2_plugin_path.resolve()))}")\n'
audio_setup = (
"try {\n"
" mbt_video_only = mbt_video_only\n"
@@ -310,16 +330,22 @@ def _hidden_source_script(
"\n"
f"{plugin_setup}"
f'Import("{import_path}")\n'
f'global mbt_color_matrix = "{probe.color_matrix or probe.expected_color_matrix or "Rec709"}"\n'
f"global mbt_color_primaries = {_primaries_code_for_source(probe)}\n"
f"global mbt_color_transfer = {_transfer_code_for_source(probe)}\n"
f"global mbt_color_range = {_range_code_for_source(probe)}\n"
f"global mbt_hdr_peak = {_hdr_peak_for_source(probe):.6f}\n"
"\n"
f'video = FFVideoSource("{source_path}", cachefile="{cache_path}")\n'
f"{validation_blank_setup}"
f"{audio_setup}"
f"last = MBT_MarkSource(source, {source_id}, {_matrix_code_for_source(probe)})\n"
f"last = MBT_MarkSource(source, {source_id}, {_matrix_code_for_source(probe)}, {_primaries_code_for_source(probe)}, {_transfer_code_for_source(probe)}, {_range_code_for_source(probe)}, {_hdr_peak_for_source(probe):.6f}, {_source_end_epoch_seconds(probe):.9f}, {(probe.duration_seconds or 0.0):.9f})\n"
f'last = ConditionalReader(last, "{timestamp_values_path}", "mbt_absolute_timestamp", false)\n'
f'last = ConditionalReader(last, "{frame_time_values_path}", "mbt_frame_time_seconds", false)\n'
f'last = ConditionalReader(last, "{frame_time_label_values_path}", "mbt_relative_timestamp", false)\n'
f'last = ConditionalReader(last, "{duration_values_path}", "mbt_frame_duration_seconds", false)\n'
f'last = ConditionalReader(last, "{duration_label_values_path}", "mbt_frame_duration", false)\n'
"last = MBT_MarkTiming(last)\n"
"last\n"
)
@@ -328,7 +354,7 @@ def _validation_script(
visible_script: Path,
validation_script: Path,
) -> str:
import_path = _avs_path(os.path.relpath(visible_script, validation_script.parent))
import_path = avs_path(os.path.relpath(visible_script, validation_script.parent))
return (
"# Generated by media-batch-tools. This file is regenerated by video_encode.py.\n"
"# The media-batch-tools runner reads frame identity properties from this clip.\n"
@@ -356,6 +382,15 @@ def _timestamp_values_file(probe: VideoProbe) -> str:
return "\n".join(lines) + "\n"
def _source_end_epoch_seconds(probe: VideoProbe) -> float:
if probe.metadata_end is None:
return 0.0
source_end = probe.metadata_end
if source_end.tzinfo is None:
source_end = source_end.replace(tzinfo=timezone.utc)
return source_end.timestamp()
def _frame_time_values_file(probe: VideoProbe) -> str:
lines = [
"# Generated by media-batch-tools for AviSynth ConditionalReader.",
@@ -453,6 +488,9 @@ def _format_video_summary(probe: VideoProbe) -> str:
bit_depth=probe.bit_depth,
)
parts.append(_format_matrix(probe))
detected_hdr = hdr_kind(probe.color_transfer, dynamic=probe.hdr10plus)
if detected_hdr is not None:
parts.append(detected_hdr)
return join_parts(parts)
@@ -471,6 +509,24 @@ def _matrix_code_for_source(probe: VideoProbe) -> int:
return _matrix_code(probe.color_matrix or probe.expected_color_matrix)
def _primaries_code_for_source(probe: VideoProbe) -> int:
return avisynth_primaries_from_color_primaries(probe.color_primaries)
def _transfer_code_for_source(probe: VideoProbe) -> int:
return avisynth_transfer_from_color_transfer(probe.color_transfer)
def _range_code_for_source(probe: VideoProbe) -> int:
return avisynth_range_from_color_range(probe.color_range)
def _hdr_peak_for_source(probe: VideoProbe) -> float:
# Zero means unknown. MBT_ToSDR then uses HDRTools' own HDR-mode default
# instead of pretending that an absent mastering display is 1000 cd/m².
return probe.hdr_peak_luminance or 0.0
def _matrix_code(matrix: str | None) -> int:
if matrix == "Rec709":
return 1
@@ -482,6 +538,8 @@ def _matrix_code(matrix: str | None) -> int:
def _default_final_clip(probe: VideoProbe) -> str:
if hdr_kind(probe.color_transfer) is not None:
return "last"
source_matrix = probe.color_matrix
expected_matrix = probe.expected_color_matrix
if source_matrix is None or expected_matrix is None or source_matrix == expected_matrix:
@@ -493,9 +551,5 @@ def _format_frame_count(probe: VideoProbe) -> str:
return format_frame_count(probe.stream_frame_count, len(probe.frame_timestamps))
def _avs_path(path: str) -> str:
return path.replace("\\", "/").replace('"', '\\"')
def helper_script_path() -> Path:
return Path(__file__).resolve().with_name(HELPER_SCRIPT_NAME)