#!/usr/bin/env python3
"""Karaoke / word-by-word captions from a Scribe transcript.

Generates an ASS subtitle file where the transcript 'plays' at the bottom — words
revealed/highlighted in sync with speech (the Submagic / Opus look). Times are
remapped onto the OUTPUT timeline using the EDL ranges, so it lines up after cuts.

Usage:
  word_captions.py <transcript.json> --ranges 641.52-667.43[,783-824] -o out.ass
Options: --per-line, --size, --marginv, --hi (BGR &H..), --base, --mode {karaoke,pop}
"""
import argparse
import json


def ts(t):
    cs = int(round(max(0, t) * 100))
    return f"{cs//360000}:{(cs//6000)%60:02d}:{(cs//100)%60:02d}.{cs%100:02d}"


def esc(w):
    return w.replace("{", "(").replace("}", ")").replace("\n", " ")


def load_words(path):
    d = json.load(open(path))
    return [w for w in d.get("words", []) if w.get("type", "word") == "word"]


def build(words, ranges, out, per_line=4, play=(1080, 1920), font="Helvetica Neue",
          size=58, marginv=300, hi="&H005AFFD5", base="&H00FFFFFF", mode="karaoke", align=2):
    events, offset = [], 0.0
    for rs, re_ in ranges:
        seg = [w for w in words if w["start"] >= rs - 0.05 and w["end"] <= re_ + 0.05]
        for i in range(0, len(seg), per_line):
            grp = seg[i:i + per_line]
            if not grp:
                continue
            if mode == "pop":
                # each word appears alone, centered-bottom, as spoken
                for j, w in enumerate(grp):
                    s = w["start"] - rs + offset
                    e = (grp[j + 1]["start"] if j + 1 < len(grp) else w["end"] + 0.15) - rs + offset
                    events.append((s, e, f"{{\\fad(60,40)}}{esc(w['text'].upper())}"))
            else:  # karaoke: whole line, highlight sweeps word by word
                ls = grp[0]["start"] - rs + offset
                le = grp[-1]["end"] - rs + offset + 0.15
                txt = "".join(f"{{\\kf{max(1,int(round((w['end']-w['start'])*100)))}}}{esc(w['text'].upper())} "
                              for w in grp).strip()
                events.append((ls, le, txt))
        offset += re_ - rs

    head = (
        "[Script Info]\nScriptType: v4.00+\n"
        f"PlayResX: {play[0]}\nPlayResY: {play[1]}\nWrapStyle: 2\nScaledBorderAndShadow: yes\n\n"
        "[V4+ Styles]\n"
        "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, "
        "BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, "
        "BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n"
        f"Style: Cap,{font},{size},{hi},{base},&H00000000,&H64000000,1,0,0,0,100,100,0,0,1,6,1,{align},80,80,{marginv},1\n\n"
        "[Events]\n"
        "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
    )
    body = "".join(f"Dialogue: 0,{ts(s)},{ts(e)},Cap,,0,0,0,,{t}\n" for s, e, t in events)
    open(out, "w").write(head + body)
    return len(events)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("transcript")
    ap.add_argument("--ranges", required=True, help="comma list of start-end seconds")
    ap.add_argument("-o", "--out", required=True)
    ap.add_argument("--per-line", type=int, default=4)
    ap.add_argument("--size", type=int, default=58)
    ap.add_argument("--marginv", type=int, default=300)
    ap.add_argument("--hi", default="&H005AFFD5")
    ap.add_argument("--base", default="&H00FFFFFF")
    ap.add_argument("--mode", default="karaoke", choices=["karaoke", "pop"])
    ap.add_argument("--align", type=int, default=2, help="ASS alignment: 2=bottom, 5=middle, 8=top")
    a = ap.parse_args()
    ranges = [tuple(float(x) for x in r.split("-")) for r in a.ranges.split(",")]
    n = build(load_words(a.transcript), ranges, a.out, a.per_line, (1080, 1920),
              "Helvetica Neue", a.size, a.marginv, a.hi, a.base, a.mode, a.align)
    print(f"wrote {a.out} ({n} caption events)")


if __name__ == "__main__":
    main()
