Zum Inhalt

transcript_diff.py

"""Diff transcript vs. .original.json snapshot.

Pure function — read-only. Produces a per-current-segment list with
text-diff tags, speaker-change flag, and merge/split origin metadata.
"""

import difflib
import json
from pathlib import Path
from typing import Any


def _backup_path_for(json_path: Path) -> Path:
    return json_path.with_name(json_path.stem + ".original.json")


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 compute_segment_diff(json_path: str) -> list[dict[str, Any]]:
    """Return per-current-segment diff entries.

    If .original.json doesn't exist, returns []. Otherwise each entry has:
      - current_index: int
      - original_indices: list[int] — derived from _split_from / _merged_from
            or [current_index] when no metadata is present
      - text_changed: bool
      - text_diff: list[tuple(tag, original_word, current_word)]  # via SequenceMatcher
      - speaker_changed: bool
      - original_speaker: str
      - current_speaker: str
      - merge_or_split: "merged" | "split" | None
    """
    cur_path = Path(json_path)
    orig_path = _backup_path_for(cur_path)
    if not orig_path.exists():
        return []
    current = _load(json_path)
    original = _load(str(orig_path))
    cur_segs = current.get("segments", [])
    orig_segs = original.get("segments", [])

    out: list[dict[str, Any]] = []
    for i, seg in enumerate(cur_segs):
        if "_merged_from" in seg:
            orig_indices = list(seg["_merged_from"])
            merge_or_split = "merged"
        elif "_split_from" in seg:
            orig_indices = [int(seg["_split_from"])]
            merge_or_split = "split"
        else:
            orig_indices = [i] if i < len(orig_segs) else []
            merge_or_split = None

        # Concat original text(s) for comparison
        original_text = " ".join(
            orig_segs[oi]["text"] for oi in orig_indices if 0 <= oi < len(orig_segs)
        ).strip()
        current_text = seg.get("text", "").strip()
        text_changed = original_text != current_text
        # Word-level diff via SequenceMatcher
        orig_words = original_text.split()
        cur_words = current_text.split()
        matcher = difflib.SequenceMatcher(a=orig_words, b=cur_words)
        text_diff: list[tuple[str, str, str]] = []
        for tag, i1, i2, j1, j2 in matcher.get_opcodes():
            text_diff.append((tag, " ".join(orig_words[i1:i2]), " ".join(cur_words[j1:j2])))

        # Speaker compare
        original_speaker = (
            orig_segs[orig_indices[0]].get("speaker", "")
            if orig_indices and 0 <= orig_indices[0] < len(orig_segs)
            else ""
        )
        current_speaker = seg.get("speaker", "")
        speaker_changed = original_speaker != current_speaker

        out.append(
            {
                "current_index": i,
                "original_indices": orig_indices,
                "text_changed": text_changed,
                "text_diff": text_diff,
                "speaker_changed": speaker_changed,
                "original_speaker": original_speaker,
                "current_speaker": current_speaker,
                "merge_or_split": merge_or_split,
            }
        )
    return out