Zum Inhalt

transcript_word_ops.py

"""Word-level edit operations.

V1 does not re-align word timings against audio. After a word edit, the
word's start/end remain at the pre-edit values; segment.text is
rebuilt from the (possibly updated) word list.
"""

import json
from pathlib import Path
from typing import Any

from transcript_editor import regenerate_srt_txt


def _load(json_path: str) -> dict[str, Any]:
    data: dict[str, Any] = json.loads(Path(json_path).read_text(encoding="utf-8"))
    return data


def _save(json_path: str, data: dict[str, Any]) -> None:
    Path(json_path).write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")


def load_words_flat(json_path: str, segment_index: int | None = None) -> list[dict[str, Any]]:
    """Return a flat list of {word, start, end, score, _edited, segment_index, word_index}.

    If segment_index is given, returns only words for that segment.
    """
    data = _load(json_path)
    out: list[dict[str, Any]] = []
    for seg_i, seg in enumerate(data.get("segments", [])):
        if segment_index is not None and seg_i != segment_index:
            continue
        for word_i, w in enumerate(seg.get("words", [])):
            out.append(
                {
                    **w,
                    "segment_index": seg_i,
                    "word_index": word_i,
                }
            )
    return out


def save_word_edits(json_path: str, segment_index: int, new_words: list[str]) -> dict[str, Any]:
    """Update word strings within a single segment.

    Sets _edited: true on each word whose string changed. Rebuilds the
    segment.text via " ".join(words). Does NOT set segment._edited
    (word-level tracking is separate).

    Returns: {"edited_count": int, "total_words": int}.
    Raises ValueError on length mismatch or out-of-range index.
    """
    data = _load(json_path)
    segments = data.get("segments", [])
    if segment_index < 0 or segment_index >= len(segments):
        raise ValueError(f"segment_index {segment_index} out of range")
    seg = segments[segment_index]
    words = seg.get("words", [])
    if len(new_words) != len(words):
        raise ValueError(f"new_words length {len(new_words)} != words length {len(words)}")
    edited_count = 0
    for word, new_str in zip(words, new_words, strict=True):
        if word.get("word") != new_str:
            word["word"] = new_str
            word["_edited"] = True
            edited_count += 1
    if edited_count > 0:
        # Rebuild segment.text from words
        seg["text"] = " ".join(w.get("word", "") for w in words)
    _save(json_path, data)
    if edited_count > 0:
        regenerate_srt_txt(json_path)
    return {"edited_count": edited_count, "total_words": len(words)}