from __future__ import annotations import csv import json from pathlib import Path from typing import Any def read_eval_records(path: Path) -> list[dict[str, Any]]: suffix = path.suffix.lower() if suffix == ".csv": with path.open("r", encoding="utf-8", newline="") as input_file: return [normalize_record(row, index) for index, row in enumerate(csv.DictReader(input_file), start=1)] if suffix in {".jsonl", ".ndjson"}: return read_jsonl(path) raise ValueError(f"Unsupported eval input format: {path.suffix}. Use CSV or JSONL.") def read_jsonl(path: Path) -> list[dict[str, Any]]: rows = [] with path.open("r", encoding="utf-8") as input_file: for line_number, line in enumerate(input_file, start=1): text = line.strip() if not text: continue try: record = json.loads(text) except json.JSONDecodeError as exc: raise ValueError(f"{path}:{line_number} is not valid JSON") from exc if not isinstance(record, dict): raise ValueError(f"{path}:{line_number} must be a JSON object") rows.append(normalize_record(record, line_number)) return rows def normalize_record(record: dict[str, Any], index: int) -> dict[str, Any]: transcript = str(record.get("transcript") or record.get("text") or "").strip() normalized: dict[str, Any] = { "id": str(record.get("id") or record.get("speech_id") or f"record-{index}"), "transcript": transcript, "stats": build_stats(record, transcript), } for key in ("category", "speech_type", "scenario_notes", "has_garble", "gold_feedback"): value = record.get(key) if value not in (None, ""): normalized[key] = value return normalized def build_stats(record: dict[str, Any], transcript: str) -> dict[str, Any]: from speech_stats import build_transcript_stats duration_seconds = optional_float(record.get("duration_sec") or record.get("duration_seconds")) duration_mmss = str(record.get("duration_mmss") or "").strip() or None return build_transcript_stats( transcript, duration_seconds=duration_seconds, duration_mmss=duration_mmss, ) def optional_int(value: Any) -> int | None: if value in (None, ""): return None return int(float(value)) def optional_float(value: Any) -> float | None: if value in (None, ""): return None return float(value)