#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
《说书人》MV 生成器（静态分镜 + 运镜 + 歌词字幕 + 音轨）

时间码来源：从音源视频画面中的书法字幕逐帧读取（每 1.5s 采样），
并用音频能量包络交叉验证（音乐 0:09.25 进入，2:52.85 结束）。

用法：python3 build_mv.py [--preview] [--no-audio]
输出：说书人-MV-v1.mp4
"""
import os
import subprocess
import sys
import tempfile

BASE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(BASE)
FONT = "/usr/share/fonts/opentype/source-han-cjk/SourceHanSansSC-Medium.otf"
AUDIO = os.path.join(ROOT, "05-音频", "说书人-音源.wav")
FPS = 30
W, H = 1920, 1080
SRC_W, SRC_H = 3840, 2160
ZMAX = 0.18
END = 173.0          # 音乐在 2:52.85 结束，留 0.15s 余量

# (素材, 起, 止, 运镜, 字幕, 类型)
#   类型: "" 普通歌词 / "dialogue" 对白 / "title" 大标题 / "credit" 制作信息
SHOTS = [
    ("03-场景设定/场景-江山风雨-水墨.png",   0.0,   7.0, "in", "说书人", "title"),
    ("06-合成镜头/合成-02-说书人×戏台.png",   7.0,  14.0, "out", "原唱 暗杠 / 寅子", "credit"),
    ("06-合成镜头/合成-01-说书人×路边茶楼.png",  14.0,  17.0, "in", "路边的茶楼 人影错落", ""),
    ("03-场景设定/场景-街市吆喝-水墨.png",  17.0,  20.0, "right", "街上传来 两三声吆喝", ""),
    ("02-人物设定/说书人-手部特写-醒木-水墨.png",  20.0,  23.0, "in", "人前摇扇 醒木拍桌", ""),
    ("06-合成镜头/合成-08-茶楼看客群像×路边茶楼.png",  23.0,  26.5, "left", "各位看官 你细听分说", ""),
    ("03-场景设定/场景-江山风雨-水墨.png",  26.5,  30.0, "out", "这江山风雨 岁月山河", ""),
    ("06-合成镜头/合成-11-侠女×城中楼阁-夜.png",  30.0,  33.0, "in", "刀光剑影 美了多少世间传说", ""),
    ("02-人物设定/说书人-侧面-水墨.png",  33.0,  38.0, "right", "且看他口若悬河 衣上有风尘", ""),
    ("06-合成镜头/合成-02-说书人×戏台.png",  38.0,  41.5, "in", "却原来是一位 江湖说书人", ""),
    ("06-合成镜头/合成-03-天山女子×天山枯城.png",  41.5,  45.0, "in", "那天山女子 独守枯城", ""),
    ("02-人物设定/天山女子-设定图-水墨.png",  45.0,  48.0, "in", "也只是为了 曾经的那一个人", ""),
    ("06-合成镜头/合成-04-昆仑痴儿×昆仑雪山.png",  48.0,  50.0, "out", "那昆仑痴儿 一情难分", ""),
    ("02-人物设定/昆仑痴儿-设定图-水墨.png",  50.0,  52.5, "out", "谁曾想这一去 再不相逢", ""),
    ("03-场景设定/场景-江山风雨-晨-水墨.png",  52.5,  56.0, "right", "这江山风雨 岁月山河", ""),
    ("06-合成镜头/合成-08-茶楼看客群像×路边茶楼.png",  56.0,  59.0, "in", "侠骨柔肠 醉了多少此间看客", ""),
    ("06-合成镜头/合成-10-游子异客×街市吆喝.png",  59.0,  63.0, "left", "本就是浮萍游子 漂泊本无根", ""),
    ("06-合成镜头/合成-06-游子异客×酒剑随马.png",  63.0,  65.5, "in", "萍水相逢浪迹天涯君莫问", ""),
    ("03-场景设定/场景-江山风雨-晨-水墨.png",  65.5,  68.5, "out", "那江山如画 各走一程", ""),
    ("03-场景设定/场景-街市吆喝-水墨.png",  68.5,  72.0, "right", "也苦尽了人间的多少苍生", ""),
    ("06-合成镜头/合成-05-侠女×桃花林.png",  72.0,  74.5, "in", "那美女多娇 爱看英雄", ""),
    ("06-合成镜头/合成-11-侠女×城中楼阁-夜.png",  74.5,  77.5, "in", "道尽了江湖的血雨腥风", ""),
    ("03-场景设定/场景-城中楼阁-水墨.png",  77.5,  80.5, "in", "城中楼阁 几经风霜", ""),
    ("02-人物设定/游子异客-设定图-水墨.png",  80.5,  83.5, "in", "天涯游子 一梦黄粱", ""),
    ("03-场景设定/场景-神鬼志异-水墨.png",  83.5,  86.5, "in", "神鬼志异 荒唐一场", ""),
    ("02-人物设定/说书人-仰首大笑-水墨.png",  86.5,  89.5, "right", "谈笑一段 半生疏狂", ""),
    ("03-场景设定/场景-城中楼阁-夜-水墨.png",  89.5,  92.5, "out", "江山易老嘛 几度斑驳", ""),
    ("06-合成镜头/合成-04-昆仑痴儿×昆仑雪山.png",  92.5,  95.5, "right", "痴儿侠女 奈何情多", ""),
    ("06-合成镜头/合成-06-游子异客×酒剑随马.png",  95.5,  98.5, "left", "酒剑随马 他乡异客", ""),
    ("06-合成镜头/合成-09-天山女子×桃花林.png",  98.5, 103.5, "in", "白衣不见 桃花如昨", ""),
    ("03-场景设定/场景-戏台-水墨.png", 103.5, 109.0, "in", "作词 古道背棺人", "credit"),
    ("03-场景设定/场景-桃花林-近景-水墨.png", 109.0, 114.0, "out", "编曲 慢镜头 / 暗杠", "credit"),
    ("06-合成镜头/合成-07-判官×戏台.png", 114.0, 117.0, "in", "哎呦 嘛时候是津门第一捏", "dialogue"),
    ("02-人物设定/判官-设定图-水墨.png", 117.0, 120.0, "in", "嘛事儿 你说", "dialogue"),
    ("02-人物设定/判官-设定图-水墨.png", 120.0, 121.5, "in", "弟弟", "dialogue"),
    ("06-合成镜头/合成-08-茶楼看客群像×路边茶楼.png", 121.5, 123.0, "right", "哎呀到底干嘛啊", "dialogue"),
    ("02-人物设定/判官-设定图-水墨.png", 123.0, 124.5, "in", "弟弟", "dialogue"),
    ("02-人物设定/说书人-设定图-水墨.png", 124.5, 126.0, "in", "这猴卖吗", "dialogue"),
    ("02-人物设定/判官-设定图-水墨.png", 126.0, 127.5, "in", "弟弟", "dialogue"),
    ("06-合成镜头/合成-01-说书人×路边茶楼.png", 127.5, 129.0, "in", "我跟你说 这猴不卖", "dialogue"),
    ("06-合成镜头/合成-08-茶楼看客群像×路边茶楼.png", 129.0, 132.0, "left", "在坐的看官 莫想太多", ""),
    ("02-人物设定/说书人-侧面-水墨.png", 132.0, 136.5, "in", "书中故事 是世间蹉跎", ""),
    ("06-合成镜头/合成-01-说书人×路边茶楼.png", 136.5, 141.0, "out", "各人心中 它自有评说", ""),
    ("02-人物设定/说书人-仰首大笑-水墨.png", 141.0, 144.0, "right", "听完这段 你一笑而过", ""),
    ("03-场景设定/场景-城中楼阁-夜-水墨.png", 144.0, 145.5, "in", "城中楼阁 几经风霜", ""),
    ("02-人物设定/游子异客-设定图-水墨.png", 145.5, 148.5, "in", "天涯游子 一梦黄粱", ""),
    ("03-场景设定/场景-神鬼志异-水墨.png", 148.5, 151.5, "in", "神鬼志异 荒唐一场", ""),
    ("02-人物设定/说书人-仰首大笑-水墨.png", 151.5, 154.5, "in", "谈笑一段 半生疏狂", ""),
    ("03-场景设定/场景-江山风雨-水墨.png", 154.5, 159.0, "out", "这江山风雨 岁月山河", ""),
    ("06-合成镜头/合成-12-说书人×江山风雨.png", 159.0, 162.0, "in", "人世苦短 又有几人看破", ""),
    ("06-合成镜头/合成-07-判官×戏台.png", 162.0, 165.0, "in", "大梦一场也只是戏中你我", ""),
    ("06-合成镜头/合成-02-说书人×戏台.png", 165.0, 169.5, "in", "欲知后事如何 且听我下回分说", ""),
    ("03-场景设定/场景-桃花林-近景-水墨.png", 169.5, 173.0, "out", "", ""),
]


def zoompan_expr(motion, n):
    zr = ZMAX / max(n - 1, 1)
    cx, cy = "iw/2-(iw/zoom/2)", "ih/2-(ih/zoom/2)"
    if motion == "in":
        z, x, y = f"min(1+{zr:.6f}*on,{1+ZMAX})", cx, cy
    elif motion == "out":
        z, x, y = f"max({1+ZMAX}-{zr:.6f}*on,1.0)", cx, cy
    elif motion == "right":
        z, x, y = f"{1+ZMAX/2:.4f}", f"(iw-iw/zoom)*on/{n-1}", cy
    elif motion == "left":
        z, x, y = f"{1+ZMAX/2:.4f}", f"(iw-iw/zoom)*(1-on/{n-1})", cy
    else:
        raise ValueError(motion)
    return f"zoompan=z='{z}':x='{x}':y='{y}':d={n}:s={W}x{H}:fps={FPS}"


def esc(p):
    return p.replace("\\", "\\\\").replace(":", "\\:").replace("'", "\\'")


def txt(tmpdir, idx, tag, s):
    p = os.path.join(tmpdir, f"{tag}_{idx:04d}.txt")
    with open(p, "w", encoding="utf-8") as f:
        f.write(s)
    return p


def build_segment(idx, shot, tmpdir):
    img, t0, t1, motion, text, kind = shot
    src = os.path.join(ROOT, img)
    n = max(int(round((t1 - t0) * FPS)), 1)
    out = os.path.join(tmpdir, f"seg_{idx:04d}.mp4")

    f = [f"scale={SRC_W}:{SRC_H}:force_original_aspect_ratio=increase",
         f"crop={SRC_W}:{SRC_H}", zoompan_expr(motion, n)]

    lab = txt(tmpdir, idx, "lab", f"S{idx:02d}  {os.path.basename(img).replace('.png','')}")
    f.append(f"drawtext=fontfile={FONT}:textfile='{esc(lab)}':x=28:y=24:fontsize=24"
             f":fontcolor=white@0.45:borderw=2:bordercolor=black@0.4")

    if text:
        lf = txt(tmpdir, idx, "ly", text)
        if kind == "title":
            f.append(f"drawtext=fontfile={FONT}:textfile='{esc(lf)}':x=(w-text_w)/2:y=(h-text_h)/2"
                     f":fontsize=150:fontcolor=white:borderw=6:bordercolor=black@0.75")
        elif kind == "credit":
            f.append(f"drawtext=fontfile={FONT}:textfile='{esc(lf)}':x=(w-text_w)/2:y=(h-text_h)/2"
                     f":fontsize=64:fontcolor=white@0.92:borderw=4:bordercolor=black@0.7")
        elif kind == "dialogue":
            f.append(f"drawtext=fontfile={FONT}:textfile='{esc(lf)}':x=(w-text_w)/2:y=h-140"
                     f":fontsize=52:fontcolor=0xFFE9A8:borderw=4:bordercolor=black@0.9")
        else:
            f.append(f"drawtext=fontfile={FONT}:textfile='{esc(lf)}':x=(w-text_w)/2:y=h-140"
                     f":fontsize=50:fontcolor=white:borderw=4:bordercolor=black@0.9")

    f.append("format=yuv420p")
    if idx == len(SHOTS):
        f.append(f"fade=t=out:st={max(t1-t0-1.6,0):.2f}:d=1.6")

    subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", src,
                    "-vf", ",".join(f), "-frames:v", str(n),
                    "-c:v", "libx264", "-preset", "veryfast", "-crf", "20",
                    "-pix_fmt", "yuv420p", out], check=True)
    return out


def main():
    preview = "--preview" in sys.argv
    noaudio = "--no-audio" in sys.argv
    shots = SHOTS[:3] if preview else SHOTS
    print(f"共 {len(shots)} 镜，时间轴 {shots[0][1]:.1f}s → {shots[-1][2]:.1f}s")

    tmpdir = tempfile.mkdtemp(prefix="mv_")
    segs = []
    for i, s in enumerate(shots, 1):
        print(f"  [{i:02d}/{len(shots)}] {s[1]:6.1f}-{s[2]:6.1f} ({s[2]-s[1]:4.1f}s) {s[3]:5s} {s[4][:24]}")
        segs.append(build_segment(i, s, tmpdir))

    lf = os.path.join(tmpdir, "list.txt")
    with open(lf, "w", encoding="utf-8") as f:
        for s in segs:
            f.write(f"file '{s}'\n")

    silent = os.path.join(tmpdir, "silent.mp4")
    subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-f", "concat", "-safe", "0",
                    "-i", lf, "-c", "copy", silent], check=True)

    name = "预览-前3镜.mp4" if preview else "说书人-MV-v2-全水墨.mp4"
    out = os.path.join(BASE, name)
    if noaudio or not os.path.exists(AUDIO):
        subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", silent, "-c", "copy", out], check=True)
    else:
        subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", silent, "-i", AUDIO,
                        "-t", str(END), "-af", f"afade=t=out:st={END-1.2:.2f}:d=1.2",
                        "-c:v", "copy", "-c:a", "aac", "-b:a", "256k",
                        "-movflags", "+faststart", "-shortest", out], check=True)

    print(f"\n✅ {out}")
    subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration,size",
                    "-show_entries", "stream=codec_name,width,height", "-of", "default=nw=1", out])


if __name__ == "__main__":
    main()
