Add Avisynth video encoding workflow
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
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)"
|
||||
Reference in New Issue
Block a user