#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""assemble.py — 把 61 个镜头（H3 动态 + 静帧运镜）按分镜时间轴拼成成片，叠加原曲音轨与歌词字幕。

流程：
  1. 逐镜取片段：06-H3片段/SXX/SXX.mp4（H3 动态）优先，否则 06-H3片段/stills/SXX.mp4
     统一缩放裁切到 1920×1080 / 24fps；过长则裁到该镜时长，过短则冻结末帧补足。
  2. concat 拼接（无转场硬切，按分镜卡点）。
  3. 混入音源（00-音源/九九八十一-咻咻满翻唱.wav），并把字卡镜（S60）替换为自绘字幕卡（可选）。
  4. 烧入 ASS 歌词字幕，输出 07-成片/九九八十一-可爱版-MV-v1.mp4

用法: python3 assemble.py [--no-subs] [--draft] [--out 07-成片/xxx.mp4]
      --draft 用 960×540/veryfast 快速出草稿片
"""
import argparse, json, os, subprocess, sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent
os.chdir(ROOT)
W, H, FPS = 1920, 1080, 24
AUDIO = "00-音源/九九八十一-咻咻满翻唱.wav"


def run(cmd, **kw):
    r = subprocess.run(cmd, capture_output=True, text=True, **kw)
    if r.returncode != 0:
        print("!! 命令失败：", " ".join(map(str, cmd))[:300])
        print(r.stderr[-600:])
        sys.exit(1)
    return r


def probe_dur(src):
    r = subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration",
                        "-of", "default=nw=1:nk=1", str(src)], capture_output=True, text=True)
    try:
        return float(r.stdout.strip())
    except Exception:
        return None


def norm_clip(src, dur, out, draft=False):
    """把片段规范化为 1920×1080/24fps、时长 dur。

    H3 片段按 17k+5 帧网格取整，天生比镜头时长略短（最多 ~0.5s）：
    差得不多时用 setpts 轻微放慢补足（动画里几乎看不出），比冻结末帧自然。
    """
    actual = probe_dur(src)
    ratio = (dur / actual) if (actual and 0 < actual < dur and dur / actual < 1.12) else 1.0
    size = (W // 2, H // 2) if draft else (W, H)
    vf = []
    if ratio != 1.0:
        vf.append(f"setpts=PTS*{ratio:.5f}")
    vf.append(f"scale={size[0]}:{size[1]}:force_original_aspect_ratio=increase,crop={size[0]}:{size[1]}")
    vf.append(f"fps={FPS}")
    vf.append("tpad=stop_mode=clone:stop_duration=2")
    vf.append("setsar=1,format=yuv420p")
    run(["ffmpeg", "-v", "error", "-y", "-i", str(src), "-t", f"{dur:.3f}",
         "-vf", ",".join(vf), "-an",
         "-c:v", "libx264", "-preset", "veryfast" if draft else "medium",
         "-crf", "20" if draft else "17", str(out)])


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--out", default="07-成片/九九八十一-可爱版-MV-v1.mp4")
    ap.add_argument("--draft", action="store_true")
    ap.add_argument("--no-subs", action="store_true")
    ap.add_argument("--only-preview", type=float, default=None, help="只合成前 N 秒（调试）")
    ap.add_argument("--rebuild-segs", action="store_true", help="强制重建全部片段缓存")
    a = ap.parse_args()

    shots = json.load(open("05-分镜脚本/shots.json", encoding="utf-8"))["shots"]
    seg_dir = ROOT / "05-分镜脚本/_segs"
    if a.rebuild_segs:
        for f in seg_dir.glob("*.mp4"):
            f.unlink()
    seg_dir.mkdir(parents=True, exist_ok=True)

    missing = []
    for s in shots:
        sid = s["id"]
        c1 = ROOT / "06-H3片段" / sid / f"{sid}_final.mp4"   # 后期加工版（如片头字卡）
        c2 = ROOT / "06-H3片段" / sid / f"{sid}.mp4"
        c3 = ROOT / "06-H3片段" / f"{sid}.mp4"               # 自绘字卡等单文件
        st = ROOT / "06-H3片段/stills" / f"{sid}.mp4"
        src = next((x for x in (c1, c2, c3, st) if x.exists()), None)
        if src is None:
            missing.append(sid)
            continue
        seg = seg_dir / f"{sid}.mp4"
        # 源素材比缓存片段新 → 重建（否则会一直复用旧的静帧占位）
        if (not seg.exists()) or src.stat().st_mtime > seg.stat().st_mtime:
            norm_clip(src, s["dur"], seg, a.draft)
            print(f"[seg] {sid} {s['dur']:.1f}s ← {src.parent.name}")
    if missing:
        print(f"⚠️ 缺 {len(missing)} 镜：{missing}（先出这些镜再合片；也可用 make_stills.py 补静帧）")

    # concat
    lst = ROOT / "05-分镜脚本/_concat.txt"
    with open(lst, "w", encoding="utf-8") as f:
        for s in shots:
            seg = seg_dir / f"{s['id']}.mp4"
            if seg.exists():
                f.write(f"file '{seg}'\n")
    tmp = ROOT / "07-成片/_noon.mp4"
    (ROOT / "07-成片").mkdir(exist_ok=True)
    run(["ffmpeg", "-v", "error", "-y", "-f", "concat", "-safe", "0", "-i", str(lst),
         "-c", "copy", str(tmp)])

    cmd = ["ffmpeg", "-v", "error", "-y", "-i", str(tmp), "-i", AUDIO,
           "-map", "0:v:0", "-map", "1:a:0", "-shortest"]
    if not a.no_subs:
        ass = "05-分镜脚本/九九八十一-可爱版.ass"
        cmd += ["-vf", f"ass={ass}"]
    if a.only_preview:
        cmd += ["-t", str(a.only_preview)]
    cmd += ["-c:v", "libx264", "-preset", "medium", "-crf", "18", "-pix_fmt", "yuv420p",
            "-c:a", "aac", "-b:a", "256k", "-movflags", "+faststart", a.out]
    run(cmd)
    print(f"✅ 成片：{a.out}")
    subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration",
                    "-of", "default=nw=1:nk=1", a.out])


if __name__ == "__main__":
    main()
