Files
media-batch-tools/tools/avisynth_validate.py
T

158 lines
5.2 KiB
Python

from __future__ import annotations
import csv
from dataclasses import dataclass, field
from pathlib import Path
IDENTITY_KEYS = ("mbt_source_id", "mbt_source_frame", "mbt_drop_frame")
@dataclass(frozen=True)
class FrameIdentity:
output_frame: int
source_id: int | None
source_frame: int | None
drop_frame: bool = False
matrix: int | None = None
@dataclass(frozen=True)
class IdentityValidationResult:
accepted: bool
frame_count: int
kept_frame_count: int
dropped_frame_count: int
source_ids: list[int]
issues: list[str] = field(default_factory=list)
def validate_frame_identities(
frames: list[FrameIdentity],
*,
expected_source_ids: set[int] | None = None,
) -> IdentityValidationResult:
issues: list[str] = []
source_ids: set[int] = set()
seen_refs: set[tuple[int, int]] = set()
seen_output_frames: set[int] = set()
last_ref: tuple[int, int] | None = None
kept_count = 0
dropped_count = 0
for frame in frames:
if frame.output_frame < 0:
issues.append(f"output frame {frame.output_frame} is negative")
if frame.output_frame in seen_output_frames:
issues.append(f"output frame {frame.output_frame} appears more than once")
seen_output_frames.add(frame.output_frame)
if frames:
max_output_frame = max(frame.output_frame for frame in frames)
missing_output_frames = sorted(
set(range(max_output_frame + 1)) - seen_output_frames
)
if missing_output_frames:
issues.append(
"missing output frame row(s): " + _format_int_list(missing_output_frames)
)
for frame in sorted(frames, key=lambda item: item.output_frame):
if frame.source_id is None:
issues.append(f"output frame {frame.output_frame} is missing mbt_source_id")
continue
if frame.source_frame is None:
issues.append(f"output frame {frame.output_frame} is missing mbt_source_frame")
continue
if frame.source_frame < 0:
issues.append(f"output frame {frame.output_frame} has a negative source frame")
continue
if expected_source_ids is not None and frame.source_id not in expected_source_ids:
issues.append(
f"output frame {frame.output_frame} references unknown source id "
f"{frame.source_id}"
)
continue
source_ids.add(frame.source_id)
ref = (frame.source_id, frame.source_frame)
if ref in seen_refs:
issues.append(
f"output frame {frame.output_frame} duplicates source "
f"{frame.source_id}:{frame.source_frame}"
)
seen_refs.add(ref)
if last_ref is not None and ref < last_ref:
issues.append(
f"output frame {frame.output_frame} reorders source frames "
f"from {last_ref[0]}:{last_ref[1]} to {ref[0]}:{ref[1]}"
)
last_ref = ref
if frame.drop_frame:
dropped_count += 1
continue
kept_count += 1
return IdentityValidationResult(
accepted=not issues,
frame_count=len(frames),
kept_frame_count=kept_count,
dropped_frame_count=dropped_count,
source_ids=sorted(source_ids),
issues=issues,
)
def read_frame_identity_csv(path: Path) -> list[FrameIdentity]:
frames: list[FrameIdentity] = []
with path.open(newline="", encoding="utf-8") as handle:
reader = csv.reader(handle)
for line_number, row in enumerate(reader, start=1):
if line_number == 1 and row in (
["output_frame", "source_id", "source_frame", "drop_frame"],
["output_frame", "source_id", "source_frame", "drop_frame", "matrix"],
):
continue
if not row or all(not value.strip() for value in row):
continue
if len(row) not in {4, 5}:
raise RuntimeError(
f"{path}:{line_number}: expected 4 or 5 CSV columns, got {len(row)}"
)
try:
output_frame = int(row[0])
source_id = _optional_int(row[1])
source_frame = _optional_int(row[2])
drop_frame = bool(int(row[3] or "0"))
matrix = _optional_int(row[4]) if len(row) == 5 else None
except ValueError as exc:
raise RuntimeError(
f"{path}:{line_number}: invalid frame identity row: {row}"
) from exc
frames.append(
FrameIdentity(
output_frame=output_frame,
source_id=source_id,
source_frame=source_frame,
drop_frame=drop_frame,
matrix=matrix,
)
)
return frames
def _optional_int(value: str) -> int | None:
value = value.strip()
return int(value) if value else None
def _format_int_list(values: list[int]) -> str:
if len(values) <= 10:
return ", ".join(str(value) for value in values)
head = ", ".join(str(value) for value in values[:10])
return f"{head}, ... ({len(values)} total)"