#!/usr/bin/env python3
"""Personal research podcast: turn a text source into a spoken episode + RSS.

Pipeline: source text -> gemma writes a spoken-friendly script -> Piper TTS (local)
-> MP3 -> episodes/ -> podcast.xml (RSS with enclosures; subscribe from any app).

RULE: machine voices never pronounce Lakota. The script prompt instructs gemma to
spell out any Lakota term letter by letter ("u-ogonek, k, i-acute...") or refer to
it descriptively. Human-recorded audio dropped into episodes/human/ is included in
the feed as-is, unmodified.

Usage: make_episode.py --source ../digest.md --title "Research radar" [--base-url URL]
"""
import os, sys, json, re, wave, argparse, subprocess, urllib.request, email.utils, html
from datetime import datetime, timezone

HERE = os.path.dirname(os.path.abspath(__file__))
EPISODES = os.path.join(HERE, "episodes")
VOICE = os.path.join(HERE, "voices", "en_US-lessac-medium.onnx")
PIPER = os.path.expanduser("~/research-feed/.venv/bin/piper")
LLM_URL = "http://localhost:8000/v1/chat/completions"
LLM = "gemma4-26b-a4b"

SCRIPT_PROMPT = """Turn the following notes into a spoken podcast script for one host.
Rules:
- Conversational, clear, no headers or markdown — words to be read aloud only.
- Never read URLs, arXiv IDs, or file paths aloud.
- No citation brackets, no symbols: say "F one score" not "F1", "about eighty percent"
  not "80%". Expand acronyms on first use.
- CRITICAL: never attempt to pronounce Lakota or other Indigenous-language words.
  When one appears, either describe it ("the Lakota word for 'plums'") or spell it
  slowly letter by letter, naming diacritics ("u with a nasal hook, k, i with an
  accent..."). Machine pronunciation of these words teaches learners errors.
- 3 to 6 minutes of speech. Start with exactly "Hey Mason!" followed by a one-sentence
  summary of the episode.

NOTES:
{notes}
"""


def llm_script(notes):
    body = {"model": LLM, "temperature": 0.3, "max_tokens": 1800,
            "messages": [{"role": "user", "content": SCRIPT_PROMPT.format(notes=notes[:12000])}]}
    req = urllib.request.Request(LLM_URL, data=json.dumps(body).encode(),
                                 headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=300) as r:
        return json.load(r)["choices"][0]["message"]["content"].strip()


def tts_to_mp3(text, mp3_path):
    wav_path = mp3_path.replace(".mp3", ".wav")
    subprocess.run([PIPER, "-m", VOICE, "-f", wav_path, "--sentence-silence", "0.4"],
                   input=text.encode(), check=True, capture_output=True)
    import lameenc
    with wave.open(wav_path) as w:
        rate, nch, frames = w.getframerate(), w.getnchannels(), w.readframes(w.getnframes())
        duration = w.getnframes() // rate
    enc = lameenc.Encoder()
    enc.set_bit_rate(64); enc.set_in_sample_rate(rate); enc.set_channels(nch); enc.set_quality(5)
    mp3 = enc.encode(frames) + enc.flush()
    open(mp3_path, "wb").write(bytes(mp3))
    os.remove(wav_path)
    return duration


def rebuild_rss(base_url):
    """Feed = every mp3 in episodes/ (machine) + episodes/human/ (community audio,
    included untouched). Newest first by file mtime."""
    items = []
    for root in (EPISODES, os.path.join(EPISODES, "human")):
        if not os.path.isdir(root):
            continue
        for f in os.listdir(root):
            if f.endswith(".mp3") and ".part." not in f:
                p = os.path.join(root, f)
                rel = os.path.relpath(p, HERE)
                meta_p = p.replace(".mp3", ".json")
                meta = json.load(open(meta_p)) if os.path.exists(meta_p) else {}
                items.append((os.path.getmtime(p), f, rel, os.path.getsize(p), meta))
    items.sort(reverse=True)
    from urllib.parse import quote
    xml = []
    for mtime, f, rel, size, meta in items[:100]:
        title = meta.get("title", f.replace(".mp3", "").replace("_", " "))
        pub = email.utils.format_datetime(datetime.fromtimestamp(mtime, tz=timezone.utc))
        url = html.escape(f"{base_url}/{quote(rel)}")   # full URL: valid XML, unique
        xml.append(f"<item><title>{html.escape(title)}</title>"
                   f"<enclosure url=\"{url}\" length=\"{size}\" type=\"audio/mpeg\"/>"
                   f"<guid isPermaLink=\"false\">{url}</guid><pubDate>{pub}</pubDate>"
                   f"<description>{html.escape(meta.get('description',''))}</description></item>")
    now = email.utils.format_datetime(datetime.now(timezone.utc))
    rss = ('<?xml version="1.0" encoding="UTF-8"?>'
           '<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"><channel>'
           "<title>LakotaLLM — Research Radar</title>"
           f"<link>{base_url}</link><language>en-us</language>"
           "<description>Auto-generated research briefings; human-recorded language "
           "audio included unmodified.</description>"
           f'<itunes:image href="{base_url}/cover.png"/>'
           f"<image><url>{base_url}/cover.png</url>"
           f"<title>LakotaLLM — Research Radar</title><link>{base_url}</link></image>"
           f"<lastBuildDate>{now}</lastBuildDate>" + "".join(xml) + "</channel></rss>")
    tmp = os.path.join(HERE, "podcast.xml.tmp")
    open(tmp, "w").write(rss)
    os.replace(tmp, os.path.join(HERE, "podcast.xml"))   # atomic: never a half-written feed
    return len(items)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--source", required=True)
    ap.add_argument("--title", required=True)
    ap.add_argument("--base-url", default="https://edgexpert-83b2.tail6663b0.ts.net")
    ap.add_argument("--script-out", default=None,
                    help="write the spoken script + meta here and stop — a separate "
                         "renderer (e.g. VibeVoice, which needs the LLM's memory) takes over")
    args = ap.parse_args()

    os.makedirs(EPISODES, exist_ok=True)
    notes = open(args.source).read()
    print("[podcast] writing script via local model...", file=sys.stderr)
    script = llm_script(notes)
    slug = re.sub(r"[^a-z0-9]+", "_", args.title.lower()).strip("_")
    stamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M")   # HHMM: reruns never collide
    if args.script_out:
        open(args.script_out, "w").write(script)
        json.dump({"title": f"{args.title} — {stamp[:8]}", "description": script[:300],
                   "mp3": f"{stamp}_{slug}.mp3"},
                  open(args.script_out + ".meta.json", "w"))
        print(f"[podcast] script written to {args.script_out} (TTS deferred)")
        return
    mp3 = os.path.join(EPISODES, f"{stamp}_{slug}.mp3")
    print("[podcast] synthesizing...", file=sys.stderr)
    dur = tts_to_mp3(script, mp3)
    json.dump({"title": f"{args.title} — {stamp[:8]}", "description": script[:300]},
              open(mp3.replace(".mp3", ".json"), "w"))
    n = rebuild_rss(args.base_url)
    print(f"[podcast] episode {os.path.basename(mp3)} ({dur}s) · feed has {n} items")


if __name__ == "__main__":
    main()
