158 lines
5.2 KiB
Python
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)"
|