RishiKar210's picture
Deploy backend incl. missing-dialogue detection + character creation
03090ce verified
Raw
History Blame Contribute Delete
2.93 kB
"""Parse a CSV/TSV dub script into a ScriptDoc.
Columns are matched by header name (case-insensitive, fuzzy):
start : start / start time / in / tc in
end : end / end time / out / tc out
character : character / speaker / role / name
text : dialogue / dialogues / line / text (optional)
Timecodes may be frame-based (needs fps), milliseconds, HH:MM:SS, or bare seconds.
"""
from __future__ import annotations
import csv
from pathlib import Path
from .base import ScriptDoc, ScriptSegment, normalize_character, parse_timecode
_ALIASES = {
"start": ("start", "start time", "in", "tc in", "start_time", "begin"),
"end": ("end", "end time", "out", "tc out", "end_time", "finish"),
"character": ("character", "speaker", "role", "name", "char"),
"text": ("dialogue", "dialogues", "line", "text", "subtitle", "content"),
}
def _match_columns(header: list[str]) -> dict[str, int]:
norm = [h.strip().lower() for h in header]
cols: dict[str, int] = {}
for key, names in _ALIASES.items():
for i, h in enumerate(norm):
if h in names or any(h == n for n in names):
cols[key] = i
break
if key not in cols: # loose contains-match fallback
for i, h in enumerate(norm):
if any(n in h for n in names):
cols[key] = i
break
return cols
def parse(path: Path, fps: float | None = None) -> ScriptDoc:
text = path.read_text(encoding="utf-8-sig", errors="ignore")
delimiter = "\t" if path.suffix.lower() == ".tsv" or text[:1024].count("\t") > text[:1024].count(",") else ","
rows = list(csv.reader(text.splitlines(), delimiter=delimiter))
rows = [r for r in rows if any(c.strip() for c in r)]
if len(rows) < 2:
raise ValueError("CSV has no data rows")
cols = _match_columns(rows[0])
missing = {"start", "end", "character"} - set(cols)
if missing:
raise ValueError(f"CSV missing required column(s): {sorted(missing)} (header={rows[0]})")
segments: list[ScriptSegment] = []
n = 0
for r in rows[1:]:
if max(cols.values()) >= len(r):
continue
try:
start_s = parse_timecode(r[cols["start"]], fps)
end_s = parse_timecode(r[cols["end"]], fps)
except ValueError:
continue
char = r[cols["character"]].strip()
if end_s <= start_s or not char:
continue
txt = r[cols["text"]].strip() if "text" in cols and cols["text"] < len(r) else ""
segments.append(ScriptSegment(
index=n, start_s=round(start_s, 3), end_s=round(end_s, 3),
character_raw=char, characters=normalize_character(char), text=txt,
))
n += 1
if not segments:
raise ValueError("No valid rows parsed from CSV")
return ScriptDoc(source_format="csv", fps=fps, segments=segments)