113 lines
4.3 KiB
Python
113 lines
4.3 KiB
Python
"""Dataset-specific metadata adapters.
|
|||
|
|
|
||
|
|
Adapters do not normalize signals. They only convert source layouts into
|
||
|
|
`SourceRecord`; the shared writer owns every media transformation.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import ast
|
||
|
|
import csv
|
||
|
|
import json
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Iterable
|
||
|
|
|
||
|
|
from preprocess.asd.schema import SAMPLE_RATE, SourceRecord
|
||
|
|
|
||
|
|
|
||
|
|
def _labels(text: str) -> list[int]:
|
||
|
|
values = ast.literal_eval(text)
|
||
|
|
if not isinstance(values, (list, tuple)):
|
||
|
|
raise ValueError("labels must be a list")
|
||
|
|
result = [int(value) for value in values]
|
||
|
|
if any(value not in (0, 1) for value in result):
|
||
|
|
raise ValueError("labels must contain only 0 and 1")
|
||
|
|
return result
|
||
|
|
|
||
|
|
|
||
|
|
def _existing(path: Path, sample_id: str) -> Path:
|
||
|
|
if not path.exists():
|
||
|
|
raise FileNotFoundError(f"{sample_id}: missing {path}")
|
||
|
|
return path
|
||
|
|
|
||
|
|
|
||
|
|
def clip_directory_records(
|
||
|
|
loader_path: Path,
|
||
|
|
visual_root: Path,
|
||
|
|
audio_root: Path,
|
||
|
|
split: str,
|
||
|
|
dataset: str = "ava",
|
||
|
|
skip_missing: bool = False,
|
||
|
|
) -> Iterable[SourceRecord]:
|
||
|
|
"""Read the AVA-style loader TSV and nested JPEG/WAV layout.
|
||
|
|
|
||
|
|
AVA and WASD ship the identical layout - `clips_videos/<split>/<clip>/
|
||
|
|
<entity>/<timestamp>.jpg` beside `clips_audios/<split>/<clip>/<entity>.wav`
|
||
|
|
- and the identical five-column loader TSV, so one reader serves both.
|
||
|
|
"""
|
||
|
|
face_by_id = {
|
||
|
|
path.name: path for path in (visual_root / split).glob("*/*") if path.is_dir()
|
||
|
|
}
|
||
|
|
audio_by_id = {path.stem: path for path in (audio_root / split).glob("*/*.wav")}
|
||
|
|
with loader_path.open(newline="") as handle:
|
||
|
|
for row_number, row in enumerate(csv.reader(handle, delimiter="\t"), start=1):
|
||
|
|
if len(row) < 4:
|
||
|
|
raise ValueError(
|
||
|
|
f"{loader_path}:{row_number}: expected at least 4 columns"
|
||
|
|
)
|
||
|
|
sample_id, _, fps, label_text = row[:4]
|
||
|
|
face_path = face_by_id.get(sample_id)
|
||
|
|
audio_path = audio_by_id.get(sample_id)
|
||
|
|
if face_path is None or audio_path is None:
|
||
|
|
if skip_missing:
|
||
|
|
continue
|
||
|
|
raise FileNotFoundError(
|
||
|
|
f"{sample_id}: missing face directory or clip-local WAV"
|
||
|
|
)
|
||
|
|
yield SourceRecord(
|
||
|
|
dataset=dataset,
|
||
|
|
sample_id=sample_id,
|
||
|
|
visual_path=face_path,
|
||
|
|
audio_path=audio_path,
|
||
|
|
source_fps=float(fps),
|
||
|
|
labels=_labels(label_text),
|
||
|
|
visual_kind="jpeg_directory",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def manifest_records(
|
||
|
|
manifest_path: Path,
|
||
|
|
dataset: str,
|
||
|
|
root: Path | None = None,
|
||
|
|
default_npy_audio_sample_rate: int = SAMPLE_RATE,
|
||
|
|
) -> Iterable[SourceRecord]:
|
||
|
|
"""Read the canonical TSV adapter used for AVCC and future datasets.
|
||
|
|
|
||
|
|
Required header fields: sample_id, visual_path, audio_path, source_fps,
|
||
|
|
labels. Optional: start_sec, visual_kind, audio_sample_rate.
|
||
|
|
"""
|
||
|
|
root = root or manifest_path.parent
|
||
|
|
with manifest_path.open(newline="") as handle:
|
||
|
|
reader = csv.DictReader(handle, delimiter="\t")
|
||
|
|
required = {"sample_id", "visual_path", "audio_path", "source_fps", "labels"}
|
||
|
|
missing = required.difference(reader.fieldnames or [])
|
||
|
|
if missing:
|
||
|
|
raise ValueError(f"{manifest_path}: missing columns {sorted(missing)}")
|
||
|
|
for row_number, row in enumerate(reader, start=2):
|
||
|
|
try:
|
||
|
|
yield SourceRecord(
|
||
|
|
dataset=dataset,
|
||
|
|
sample_id=row["sample_id"],
|
||
|
|
visual_path=_existing(root / row["visual_path"], row["sample_id"]),
|
||
|
|
audio_path=_existing(root / row["audio_path"], row["sample_id"]),
|
||
|
|
source_fps=float(row["source_fps"]),
|
||
|
|
labels=json.loads(row["labels"]),
|
||
|
|
start_sec=float(row.get("start_sec") or 0.0),
|
||
|
|
npy_audio_sample_rate=int(
|
||
|
|
row.get("audio_sample_rate") or default_npy_audio_sample_rate
|
||
|
|
),
|
||
|
|
visual_kind=row.get("visual_kind") or "npy",
|
||
|
|
)
|
||
|
|
except (TypeError, ValueError, json.JSONDecodeError) as error:
|
||
|
|
raise ValueError(
|
||
|
|
f"{manifest_path}:{row_number}: invalid row"
|
||
|
|
) from error
|