# -*- coding: utf-8 -*-
r"""PKSTUDIO моушн-промо: сведение на ПК. Видео + музыка + звуки по разметке сцен (cues.json из страницы).
  python E:\PKSTUDIO\clients\pk_motion\tools\pm_assemble.py [--music C] [--out pk_motion_v1]

Музыка срезается на 0,05 с (бочка трека C на сетке ролика). Звук нормируется по пику из sfx.json и
ставится на громкость роли. Одинаковые роли могут идти часто (серии тиков), разные крупные звуки ближе
0,12 с друг к другу: остаётся главный. Итог двухпроходный loudnorm -16 LUFS, копия в E:\PKSTUDIO\DEMO.
"""
import json, os, shutil, subprocess, sys, datetime

sys.stdout.reconfigure(encoding="utf-8")
B = r"E:\PKSTUDIO\clients\pk_motion"
FF = r"C:\Users\PKSTUDIO\.cache\hyperframes\ffmpeg\ffmpeg-9.0.1-essentials_build\bin\ffmpeg.exe"
A = sys.argv
MUS = A[A.index("--music") + 1] if "--music" in A else "C"
NAME = A[A.index("--out") + 1] if "--out" in A else "pk_motion_v1"
VID = A[A.index("--video") + 1] if "--video" in A else os.path.join(B, "out", "pk_motion_video.mp4")
NODEMO = "--nodemo" in A
OFF = 0.05
# целевая громкость роли (шкала lufs_or_mean_db из sfx.json): файлы библиотеки при равном пике гуляют на 20 дБ,
# поэтому ровняем по громкости, а не по пику (27.09: удар на 2,0 с был на 10 дБ тише соседних)
LOUD = {"slam": -12, "strike": -16, "decode": -19, "riser": -18, "drop": -10, "whip": -18, "fly": -16, "tick": -23, "snap": -20,
        "sweep": -19, "cut": -17, "pop": -18, "stretch": -17, "rec": -18, "shimmer": -18, "roll": -18, "chime": -19, "stutter": -13,
        "swirl": -15, "logo": -13, "typing": -22, "glitch": -17, "boing": -17, "kiss": -18, "twinkle": -20}
RANK = ["drop", "logo", "boing", "kiss", "slam", "stutter", "fly", "whip", "cut", "strike", "stretch", "snap", "pop", "glitch", "roll",
        "sweep", "decode", "shimmer", "swirl", "chime", "twinkle", "rec", "riser", "typing", "tick"]
BEDS = {"riser", "typing", "swirl", "sweep", "decode", "shimmer", "roll"}   # длинные подложки: не спорят с ударами


def run(cmd):
    r = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="ignore")
    if r.returncode:
        print("!", r.stderr[-1500:]); sys.exit(1)
    return r.stderr


def main():
    cues = json.load(open(os.path.join(B, "out", "cues.json"), encoding="utf-8"))
    sfx = json.load(open(os.path.join(B, "audio", "sfx", "sfx.json"), encoding="utf-8"))
    placed, keep, dropped = [], [], []
    for t, role, gain in sorted(cues, key=lambda c: (RANK.index(c[1].split("_")[0]) if c[1].split("_")[0] in RANK else 99, c[0])):
        kind = role.split("_")[0]
        if role not in sfx:
            alt = [k for k in sfx if k.startswith(kind + "_")]
            if not alt:
                dropped.append((t, role, "нет файла")); continue
            role = alt[0]
        if kind not in BEDS and any(abs(t - q) < 0.12 and k2 != kind for q, k2 in placed):
            dropped.append((t, role, "ближе 0,12 с к главному")); continue
        if kind not in BEDS:
            placed.append((t, kind))
        keep.append((t, role, gain))
    keep.sort()
    ins, flt, mix = ["-i", VID, "-i", os.path.join(B, "audio", f"music_{MUS}.mp3")], [], []
    flt.append(f"[1:a]atrim=start={OFF},asetpts=PTS-STARTPTS,volume=-3dB[mus]"); mix.append("[mus]")
    for i, (t, role, gain) in enumerate(keep):
        info = sfx[role]
        f = info["file"]
        if not os.path.isabs(f):
            f = os.path.join(B, "audio", "sfx", os.path.basename(f))
        loud = float(info.get("lufs_or_mean_db", -16.0))
        db = min(14.0, LOUD.get(role.split("_")[0], -18) - loud) + 20 * __import__("math").log10(max(gain, 0.01))
        ms = int(round(t * 1000))
        ins += ["-i", f]
        flt.append(f"[{i + 2}:a]aformat=sample_rates=48000:channel_layouts=stereo,volume={db:.2f}dB,adelay={ms}|{ms}[s{i}]")
        mix.append(f"[s{i}]")
    flt.append("".join(mix) + f"amix=inputs={len(mix)}:normalize=0:duration=first,apad=whole_dur=30,atrim=0:30,afade=t=out:st=29.6:d=0.4[mx]")
    fc = os.path.join(B, "out", "mix_graph.txt")
    open(fc, "w", encoding="utf-8").write(";\n".join(flt))
    raw = os.path.join(B, "out", "mix_raw.wav")
    run([FF, "-v", "error", "-y", *ins, "-/filter_complex", fc, "-map", "[mx]", "-ar", "48000", raw])
    st = run([FF, "-hide_banner", "-i", raw, "-af", "loudnorm=I=-16:TP=-1.5:LRA=11:print_format=json", "-f", "null", "-"])
    j = json.loads(st[st.rindex("{"):st.rindex("}") + 1])
    ln = (f"loudnorm=I=-16:TP=-1.5:LRA=11:measured_I={j['input_i']}:measured_TP={j['input_tp']}:measured_LRA={j['input_lra']}"
          f":measured_thresh={j['input_thresh']}:offset={j['target_offset']}:linear=true")
    out = os.path.join(B, "out", NAME + ".mp4")
    run([FF, "-v", "error", "-y", "-i", VID, "-i", raw, "-map", "0:v", "-map", "1:a",
         "-c:v", "copy", "-af", ln, "-c:a", "aac", "-b:a", "256k", "-ar", "48000", "-shortest", "-movflags", "+faststart", out])
    demo = os.path.join(r"E:\PKSTUDIO\DEMO", datetime.date.today().isoformat() + "_" + NAME + ".mp4")
    if not NODEMO:
        shutil.copy(out, demo)
    print(f"звуков {len(keep)}, выкинуто {len(dropped)}; вход {j['input_i']} LUFS")
    for d in dropped:
        print("  -", d)
    print("OUT", out); print("DEMO", demo)


if __name__ == "__main__":
    main()
