#!/usr/bin/env python3
"""breakdown.py: download one post, transcribe it word for word, and pull keyframes + scene cuts,
so your AI can break it down shot by shot. Everything runs on your computer: free, no API key.

Needs:
  - ffmpeg                      (brew install ffmpeg / sudo apt install ffmpeg / winget install Gyan.FFmpeg)
  - pip install faster-whisper  (the transcriber; first run downloads a ~500 MB model once)
  - pip install yt-dlp          (downloader, used when the API doesn't hand over the video file)
  - optional: IG_USER_ID + IG_ACCESS_TOKEN set (same as fetch.py) to get the file from the official API first

Usage:
  python3 breakdown.py https://www.instagram.com/reel/XXXX/
  python3 breakdown.py https://www.instagram.com/reel/XXXX/ --file my-download.mp4   # use a video you already have
  python3 breakdown.py --file some-video.mp4                                         # local file only
  python3 breakdown.py URL --model base.en     # smaller/faster model (default small.en)
  python3 breakdown.py URL --no-transcribe     # frames + cuts only
Output: posts/<handle>-<id>/ with source.mp4 (or slide-NN.jpg for carousels), transcript.md,
        frames/sheetNN.jpg (1 frame per second, 12 per sheet, timestamped), frames/cutNN.jpg + cuts.txt
"""
import argparse, glob, json, os, re, shutil, subprocess, sys, urllib.parse, urllib.request

V = "v23.0"


def need(binary, how):
    if not shutil.which(binary):
        sys.exit(f"{binary} not found. Install it: {how}")


def find_handle(sc):
    for f in glob.glob("raw/*.json"):
        d = json.load(open(f, encoding="utf-8"))
        if any(sc in (m.get("permalink") or "") for m in d.get("media", [])):
            return d.get("handle", "unknown")
    return "unknown"


def graph_media(handle, sc):
    tok, igid = os.environ.get("IG_ACCESS_TOKEN"), os.environ.get("IG_USER_ID")
    if not (tok and igid and handle != "unknown"):
        return None
    f = f"business_discovery.username({handle}){{media.limit(100){{permalink,media_type,media_url,children{{media_type,media_url}}}}}}"
    q = f"https://graph.facebook.com/{V}/{igid}?" + urllib.parse.urlencode({"fields": f, "access_token": tok})
    try:
        for m in json.load(urllib.request.urlopen(q, timeout=90))["business_discovery"]["media"]["data"]:
            if sc in (m.get("permalink") or ""):
                return m
    except Exception as e:  # never print the URL: it contains the token
        print("Graph API media lookup failed:", type(e).__name__, "(falling back to yt-dlp)")
    return None


def frames(video, out):
    os.makedirs(f"{out}/frames", exist_ok=True)
    stamp = "drawtext=text='%{pts\\:hms}':x=6:y=6:fontsize=18:fontcolor=yellow:box=1:boxcolor=black,"
    for vf in (f"fps=1,scale=270:-1,{stamp}tile=6x2", "fps=1,scale=270:-1,tile=6x2"):  # 2nd = ffmpeg without drawtext
        r = subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", video, "-vf", vf, f"{out}/frames/sheet%02d.jpg"],
                           capture_output=True, text=True)
        if r.returncode == 0:
            break
    else:
        sys.exit("ffmpeg couldn't read the video: " + r.stderr.strip()[-300:])
    r = subprocess.run(["ffmpeg", "-hide_banner", "-i", video, "-vf", "select='gt(scene,0.25)',showinfo,scale=360:-1",
                        "-vsync", "vfr", f"{out}/frames/cut%02d.jpg"], capture_output=True, text=True)
    cuts = re.findall(r"pts_time:([\d.]+)", r.stderr)
    open(f"{out}/frames/cuts.txt", "w").write("\n".join(cuts) + ("\n" if cuts else ""))
    return cuts


def transcribe(video, out, label, model_name):
    try:
        import numpy as np
        from faster_whisper import WhisperModel
    except ImportError:
        sys.exit("faster-whisper is missing. Run: pip install faster-whisper   (or use --no-transcribe)")
    pcm = subprocess.run(["ffmpeg", "-v", "error", "-i", video, "-f", "s16le", "-ac", "1", "-ar", "16000", "-"],
                         capture_output=True).stdout
    with open(f"{out}/transcript.md", "w", encoding="utf-8") as f:
        f.write(f"# {label}\n\n")
        if not pcm:
            f.write("(no audio track: music-only or silent. Break it down from the frames and the caption.)\n")
            return 0
        audio = np.frombuffer(pcm, np.int16).astype(np.float32) / 32768
        segs, _ = WhisperModel(model_name, compute_type="int8").transcribe(audio, word_timestamps=True)
        f.write("## Lines\n")
        words, n = [], 0
        for s in segs:
            f.write(f"- [{s.start:5.1f}-{s.end:5.1f}] {s.text.strip()}\n")
            words += s.words or []
            n += 1
        if not n:
            f.write("(no speech found: music-only reel. Break it down from the frames and the caption.)\n")
        f.write("\n## Words\n" + " ".join(f"{w.word.strip()}({w.start:.1f})" for w in words) + "\n")
    return n


def main():
    ap = argparse.ArgumentParser(description="Download + transcribe + keyframe one Instagram post.")
    ap.add_argument("url", nargs="?", help="the post URL (https://www.instagram.com/reel/XXXX/)")
    ap.add_argument("--file", help="use a local video instead of downloading")
    ap.add_argument("--model", default="small.en", help="faster-whisper model (tiny.en, base.en, small.en, medium.en)")
    ap.add_argument("--no-transcribe", action="store_true", help="skip the transcript (frames + cuts only)")
    a = ap.parse_args()
    if not a.url and not a.file:
        ap.error("give a post URL, or --file path/to/video.mp4")
    need("ffmpeg", "brew install ffmpeg / sudo apt install ffmpeg / winget install Gyan.FFmpeg")

    if a.url:
        m = re.search(r"/(?:reel|reels|p|tv)/([^/?#]+)", a.url)
        if not m:
            sys.exit("That doesn't look like a post URL. Expected https://www.instagram.com/reel/XXXX/")
        sc = m.group(1)
    else:
        sc = re.sub(r"[^A-Za-z0-9_-]", "", os.path.splitext(os.path.basename(a.file))[0]) or "local"
    handle = find_handle(sc) if a.url else "local"
    out = f"posts/{handle}-{sc}"
    os.makedirs(out, exist_ok=True)
    video = f"{out}/source.mp4"

    if a.file:
        if not os.path.exists(a.file):
            sys.exit(f"{a.file} not found")
        shutil.copyfile(a.file, video)
    else:
        gm = graph_media(handle, sc)
        if gm and gm.get("children"):  # carousel: save every slide, nothing to transcribe
            for i, c in enumerate(gm["children"]["data"], 1):
                if c.get("media_url"):
                    ext = "mp4" if c.get("media_type") == "VIDEO" else "jpg"
                    urllib.request.urlretrieve(c["media_url"], f"{out}/slide-{i:02d}.{ext}")
            print(f"carousel saved to {out}/ (hand the slides to your AI to read)")
            return
        if gm and gm.get("media_url"):
            urllib.request.urlretrieve(gm["media_url"], video)
        else:  # the API often hides media_url on other accounts' reels
            need("yt-dlp", "pip install yt-dlp")
            r = subprocess.run(["yt-dlp", "-q", "-f", "mp4/best", "-o", video, a.url])
            if r.returncode or not os.path.exists(video):
                sys.exit("Download failed. Save the reel yourself and rerun with --file path/to/video.mp4")

    cuts = frames(video, out)
    lines = 0 if a.no_transcribe else transcribe(video, out, a.url or a.file, a.model)
    sheets = len(glob.glob(f"{out}/frames/sheet*.jpg"))
    print(f"done → {out}/  ({sheets} contact sheets, {len(cuts)} scene cuts, {lines} transcript lines)")


if __name__ == "__main__":
    main()
