# -*- coding: utf-8 -*-
"""ショート「AIあるある」の共通の書き出し（1080×1920・2026-11-02「確認いらん」で作り、2本目から共通にした）。
各回の作業フォルダに narration.py と spec.py（NAME・NO・HEAD・scene(T,S)・FACE・POSE・LEAN・SE・ENDING・LEAD）を置き、そこで
  P2M_BASE=<_制作ベース> python3 ../AIあるある_共通/aiaru_kit.py [--still 秒,…]

書き写した元: 動画/現場で扱えるか/kabe_short.py（考察回のショート。2人とも全身・左下ハコブくん・右下エイイチ）
この回で足したもの:
  - 確認の窓（回数つき）が、ハコブくんが確認するたびに積み重なる
  - エイイチの心の声を、縦書きの黒い札で叩きつける（参考: AI と酔っぱらいの縦長動画のツッコミ）。言葉はエイイチが決めた
  - ハコブくんは確認するたびに大きく・前に乗り出す
  - 最後にエイイチがコケる（全身の絵を足もとを軸に倒す）
  - 字幕の色: ハコブくんは水色、エイイチは白（マンガのショートと同じ決まり）
使い方: P2M_BASE=<_制作ベース> python3 kakunin_short.py [--still 秒,秒,...]
"""
import os, sys, json, wave, subprocess, html as _html
import numpy as np
from PIL import Image, ImageFilter, ImageOps
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
sys.path.insert(0, os.getcwd())
import kabe_kit as K
from kabe_kit import ease, blend, rotate_about, hk, FPS, SR, shake_js
sys.path.insert(1, K.BASE)
import short_kit as SK
from scene_kit import LOGO

W, H = 1080, 1920
SANS = "font-family:'Noto Sans CJK JP',sans-serif;"
FOOT = 1880
SUB_MID, SUB_CX, SUB_MAXC = 1150, 540, 11
EI_SCALE = 4.5
HK_SMALL_H, HK_X, HK_BIG_H = 515, 10, 900
EI_POSES = {1: "立つ", 2: "腕組み", 3: "驚く", 4: "笑う"}
HK_BLUE = "rgb(120,200,255)"

VCSS = """
html, body { width:1080px; height:1920px; }
.f { width:1080px; height:1920px; background: radial-gradient(ellipse at 50% 42%, #3a362f 0%, #22201c 55%, #141311 100%); }
.vhead { position:absolute; left:60px; right:60px; top:170px; height:270px; display:flex; flex-direction:column; justify-content:center; align-items:center; text-align:center; z-index:5; }
.vhead img { height:54px; align-self:flex-start; margin-bottom:18px; filter:invert(1) drop-shadow(0 0 6px rgba(0,0,0,.8)); opacity:.9; }
.vhead .q { font-family:'Noto Sans CJK JP',sans-serif; font-size:84px; font-weight:900; line-height:1.2; color:#f5f3ee; text-shadow:0 0 18px rgba(0,0,0,.9), 0 4px 0 #141311; }
.dlg { position:absolute; background:#f5f3ee; border-radius:22px; box-shadow:0 26px 50px rgba(0,0,0,.55); overflow:hidden; opacity:0; font-family:'Noto Sans CJK JP',sans-serif; color:#22201c; }
.dlg .bar { background:#22201c; color:#f5f3ee; font-size:32px; font-weight:700; padding:14px 28px; display:flex; align-items:center; gap:14px; }
.dlg .bar i { font-style:normal; width:44px; height:44px; border-radius:50%; background:#ffd84a; color:#22201c; display:flex; align-items:center; justify-content:center; font-weight:900; }
.dlg .body { font-size:50px; font-weight:800; line-height:1.35; text-align:center; padding:34px 30px 22px; }
.dlg .btns { display:flex; gap:24px; justify-content:center; padding:0 30px 30px; }
.dlg .btns b { font-size:36px; padding:12px 0; width:220px; text-align:center; border-radius:14px; border:4px solid #22201c; }
.dlg .btns b.y { background:#22201c; color:#f5f3ee; }
.dlg.red .bar { background:#c4472a; }
@keyframes dlgIn { 0% { opacity:0; transform:var(--r) scale(1.5); } 70% { opacity:1; transform:var(--r) scale(.96); } 100% { opacity:1; transform:var(--r) scale(1); } }
.tk { position:absolute; writing-mode:vertical-rl; background:#0d0d0d; color:#fff; border:6px solid #fff; font-family:'Noto Sans CJK JP',sans-serif;
      font-weight:900; line-height:1.12; padding:20px 12px; box-shadow:0 14px 30px rgba(0,0,0,.6); opacity:0; z-index:8; text-orientation:upright; }
.dlg .body.eng { font-family:'Noto Sans','Noto Sans CJK JP',sans-serif; font-size:30px; font-weight:500; line-height:1.4; text-align:left; padding:24px 34px 16px; }
.dlg .note { position:absolute; right:22px; top:12px; font-size:24px; color:#bdb7ab; font-weight:700; }
.chk { font-size:44px; font-weight:900; color:#f5f3ee; text-shadow:0 4px 0 #141311; }
@keyframes dim { to { filter:brightness(.42); } }
@keyframes tkIn { 0% { opacity:0; transform:rotate(4deg) scale(2.2); } 70% { opacity:1; transform:rotate(4deg) scale(.95); } 100% { opacity:1; transform:rotate(4deg) scale(1); } }
"""


def vpage(head, body, bg_html=""):
    return K.page(bg_html + body + f'<div class="vhead"><img src="{LOGO}"><div class="q">{head}</div></div>', VCSS)


def bg_layer(T):
    """背景の写真（Gemini で作成）。最初は明るく見せ、話の中身が出る所で暗くして、窓と字幕を読みやすくする"""
    if not BG:
        return ""
    p = "file://" + os.path.abspath(BG["file"])
    t = T(*BG.get("at", (0, 0.0)))
    v = BG.get("veil", 0.62)
    return (f'<div style="position:absolute;inset:0;background:url(\'{p}\') center/cover no-repeat"></div>'
            '<div style="position:absolute;left:0;right:0;top:0;height:560px;background:linear-gradient(rgba(20,19,17,.8),rgba(20,19,17,.4) 70%,rgba(20,19,17,0))"></div>'
            f'<style>@keyframes bgv {{ to {{ opacity:{v}; }} }}</style>'
            f'<div style="position:absolute;inset:0;background:#141311;opacity:.12;animation:bgv .5s ease {t:.3f}s forwards"></div>'
            '<div style="position:absolute;right:40px;top:446px;font-family:\'Noto Sans CJK JP\';font-size:22px;color:#fff;opacity:.85;text-shadow:0 0 6px #000;z-index:7">背景は生成AI（Gemini）で作成</div>')


import importlib.util as _ilu


def _here(name):
    """作業フォルダ（いまのフォルダ）の name.py を読む（_制作ベース に同じ名前があるため、場所を決めて読む）"""
    sp = _ilu.spec_from_file_location(name + "_here", os.path.join(os.getcwd(), name + ".py"))
    m = _ilu.module_from_spec(sp); sp.loader.exec_module(m)
    return m


SP = _here("spec")
NAME, LEAD, FACE, POSE, LEAN, SE = SP.NAME, SP.LEAD, SP.FACE, SP.POSE, SP.LEAN, SP.SE
ENDING = getattr(SP, "ENDING", dict(kind="fall"))
INTRO = getattr(SP, "INTRO", "hop")      # 冒頭の大きなハコブくんの動き: hop / rise / dash / drop / spin（2026-11-02 エイイチ「いろんな動き、いろんな向きに」）
MOVES = getattr(SP, "MOVES", [])         # [(文, 秒, 動き, 長さ)] 動き: hop / shake / tilt / bow / spin / turn / dash / back / jump
BG = getattr(SP, "BG", None)             # dict(file=背景の写真, at=(文, 秒) 暗くし始める所, veil=暗さ)。話によって写真を敷く


def vpages10(text):
    out = []
    for part in text.split("｜"):
        if not part:
            continue
        ls = SK.wrap(part, SUB_MAXC)
        if ls is None:
            out.append(None); continue
        while ls:
            n = len(ls)
            k = 3 if n == 3 or n >= 5 else min(n, 2)
            out.append("\n".join(ls[:k])); ls = ls[k:]
    return out


def build(stills=None):
    N = _here("narration")
    idx_main = json.load(open("tts/index.json"))
    ei_main = set(N.EI_LINES)
    lead = round(LEAD * FPS)
    segs, t = [], lead
    for k, sg in enumerate(N.SEGS):
        _, face, text, pause = N.parse(sg)
        o = idx_main[k]
        frames = round((o["dur"] + pause) * FPS)
        segs.append(dict(k=k, i=k, text=text, face=face, start=t, speech=round(o["dur"] * FPS), frames=frames, ei=k in ei_main))
        t += frames
    total = t
    if total / FPS > 30.0:
        sys.exit(f"{total / FPS:.1f}秒あります（30秒以内）")

    def Tf(k, off=0.0):
        if k == "end":
            s = segs[-1]; return (s["start"] + s["speech"]) / FPS + 0.15 + off
        return (segs[k]["start"] / FPS if k >= 0 else 0.0) + off
    Sf = lambda k: segs[k]["speech"] / FPS
    html = vpage(SP.HEAD, SP.scene(Tf, Sf), bg_layer(Tf))
    os.makedirs("out/png", exist_ok=True)

    from playwright.sync_api import sync_playwright
    if stills:
        with sync_playwright() as p:
            br = p.chromium.launch(); pg = br.new_page(viewport={"width": W, "height": H})
            open("_block.html", "w", encoding="utf-8").write(html)
            pg.goto("file://" + os.path.abspath("_block.html")); pg.evaluate("document.fonts.ready"); pg.wait_for_timeout(300)
            for s in stills:
                pg.evaluate(f"window.setT({s})"); pg.screenshot(path=f"out/png/still_{s:05.2f}.png")
            br.close()
        print("総尺", round(total / FPS, 2)); return

    # --- 声 ---
    pcm = np.zeros(total * SR // FPS + SR, np.int16)
    for s in segs:
        with wave.open(f"tts/{s['i']:03d}.wav") as w:
            a = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16)
        a = a[:s["frames"] * SR // FPS]
        s0 = s["start"] * SR // FPS
        pcm[s0:s0 + len(a)] = a
    pcm = pcm[:total * SR // FPS]

    # --- 字幕（ハコブくんは水色・エイイチは白）と画面 ---
    subs = []
    with sync_playwright() as p:
        br = p.chromium.launch()
        pg = br.new_page(viewport={"width": W, "height": H})
        for s in segs:
            fill = "#fff" if s["ei"] else HK_BLUE
            SUBCSS = (f"*{{margin:0;padding:0}}html,body{{width:{W}px;height:{H}px;background:transparent}}svg{{position:absolute;left:0;top:0}}"
                      f".t{{font-family:'Noto Sans CJK JP',sans-serif;font-size:{SK.SUB_FS}px;font-weight:800;fill:{fill};stroke:#0D0D0D;stroke-width:12px;stroke-linejoin:round;paint-order:stroke fill}}")
            pages_ = vpages10(s["text"])
            n, tot, acc = len(pages_), sum(len(x or "") for x in pages_), 0
            for pk, ptxt in enumerate(pages_):
                if ptxt is None:
                    sys.exit(f"字幕が折れない: {s['text']}")
                svg = ''.join(f'<text x="{SUB_CX}" text-anchor="middle" class="t">{_html.escape(ln)}</text>' for ln in ptxt.split("\n"))
                pg.set_content(f'<!doctype html><html><head><meta charset="utf-8"><style>{SUBCSS}</style></head><body><svg width="{W}" height="{H}">{svg}</svg></body></html>')
                pg.evaluate(f"""() => {{ const ts=[...document.querySelectorAll('text.t')]; const n=ts.length; let fs={SK.SUB_FS};
                  for (const t of ts) {{ const w=t.getComputedTextLength(); if (w>{SK.SUB_MAXW}) fs=Math.min(fs, Math.floor({SK.SUB_FS}*{SK.SUB_MAXW}/w)); }}
                  ts.forEach((t,k)=>{{ t.style.fontSize=fs+'px'; t.setAttribute('y', Math.round({SUB_MID}+(k-(n-1)/2)*{SK.SUB_LH}+fs*0.36)); }}); }}""")
                path = f"out/png/sub_{s['k']}_{pk}.png"
                pg.screenshot(path=path, omit_background=True)
                K.fill_holes(path)
                a = s["start"] + round(s["speech"] * acc / max(1, tot))
                acc += len(ptxt)
                b = s["start"] + (s["frames"] if pk == n - 1 else round(s["speech"] * acc / max(1, tot)))
                subs.append((a, b, K.sub_layer(path)))
        mp4 = "out/_paper.mp4"
        K.render_block(dict(name=NAME, html=html), total, mp4, pg)
        br.close()

    # --- ハコブくん ---
    def full(hh):
        o = hk.Hakobu(mode="full")
        bb = o.body.getbbox()
        o.h = hh; o.w = round((bb[2] - bb[0]) * hh / (bb[3] - bb[1])); o._cache = {}
        return o
    hs, hb = full(HK_SMALL_H), full(HK_BIG_H)
    tline = [(s["start"], s["start"] + s["frames"], s["start"] + s["speech"], s["face"], not s["ei"]) for s in segs]
    tline = [(0, lead, lead, "base", False)] + tline
    mp = pcm.astype(np.float32) / 32768.0
    speak = np.zeros(total, bool); eispk = np.zeros(total, bool)
    for s in segs:
        a0, a1 = s["start"] * SR // FPS, (s["start"] + s["frames"]) * SR // FPS
        if s["ei"]:
            mp[a0:a1] = 0; eispk[s["start"]:s["start"] + s["speech"]] = True
        else:
            speak[s["start"]:s["start"] + s["speech"]] = True
    params = hs.animate(tline, mp)
    for k, a, d, f in FACE:
        ex = hk.EXPR[f]
        for i in range(int(Tf(k, a) * FPS), min(total, int((Tf(k, a) + d) * FPS))):
            eyes, brows, mouth, ring, dy = params[i]
            if not speak[i]:
                mouth = ex["rest"]
            params[i] = (ex["eyes"], ex["brows"], mouth, ring, dy)
    t_end_big, SHR = Tf(0), 0.5
    lean = [(Tf(k, a), hgt, dx) for k, a, hgt, dx in LEAN]

    def intro_pose(t):
        """冒頭の大きなハコブくん → (足もとの中心 x, 足もと y, 角度)"""
        if INTRO == "rise":      # 下から飛び出す
            k = 1.15 * np.sin(min(1, t / 0.45) * np.pi / 2) if t < 0.45 else 1.0 + 0.15 * np.exp(-(t - 0.45) * 6) * np.cos((t - 0.45) * 14)
            return 540 + 40 * np.sin(t * 1.1), 1990 + (1900 - 1990) * k, 6 * np.sin(t * 1.3)
        if INTRO == "dash":      # 左から走ってきて、キキッと止まる
            u = min(1.0, t / 0.55)
            x = -400 + (560 + 400) * ease(u)
            rot = -14 * (1 - u) if u < 1 else 10 * np.exp(-(t - 0.55) * 5) * np.sin((t - 0.55) * 16)
            return x, 1900, rot
        if INTRO == "drop":      # 上から落ちてきて弾む
            if t < 0.4:
                return 540, -100 + 2000 * (t / 0.4) ** 2, 0
            u = t - 0.4
            return 540, 1900 - 180 * abs(np.sin(u * 7)) * np.exp(-u * 3), 5 * np.sin(u * 9) * np.exp(-u * 2)
        if INTRO == "spin":      # くるくる向きを変えながら跳ねる（向きは hk_mirror で）
            ph = (t * 2.4) % 1.0
            return 540, 1900 - 120 * 4 * ph * (1 - ph), 0
        ph = (t * 1.9) % 1.0     # hop: 跳ねて喜ぶ
        by = 1900 - 160 * 4 * ph * (1 - ph)
        if t < 0.3:
            by = 1920 + (by - 1920) * ease(t / 0.3)
        return 540 + 30 * np.sin(t * 2.0), by, 8 * np.sin(t * 3.8)

    moves = [(Tf(k, a), kind, d) for k, a, kind, d in MOVES]

    def move_off(t):
        """文の途中の動き → (横, 縦, 角度, 左右反転)"""
        dx = dy = dr = 0.0
        mir = False
        if t < t_end_big and INTRO == "spin":
            mir = int(t * 6) % 2 == 1
        for a, kind, d in moves:
            if not (a <= t < a + d):
                continue
            u = (t - a) / d
            if kind == "hop":
                dy -= 70 * abs(np.sin(np.pi * u * 3))
            elif kind == "shake":
                dx += 18 * np.sin((t - a) * 45) * (1 - u)
            elif kind == "tilt":
                dr += 14 * np.sin(np.pi * u)
            elif kind == "bow":
                dr -= 22 * np.sin(np.pi * u)
            elif kind == "spin":
                mir = int(u * 8) % 2 == 1
                dy -= 40 * abs(np.sin(np.pi * u * 4))
            elif kind == "turn":
                mir = True
            elif kind == "dash":
                dx += 240 * np.sin(np.pi * u)
                dr += 8 * np.sin(np.pi * u)
            elif kind == "back":
                dx -= 130 * np.sin(np.pi * u)
                dr -= 6 * np.sin(np.pi * u)
            elif kind == "jump":
                dy -= 260 * np.sin(np.pi * u)
                dr += 360 * u if d >= 0.8 else 0
        return dx, dy, dr, mir

    def hk_place(t):
        sx = HK_X + hs.w / 2
        if t < t_end_big + SHR:
            tt = min(t, t_end_big)
            bx, by, rot = intro_pose(tt)
            if t < t_end_big:
                return HK_BIG_H, bx, by, rot
            u = ease((t - t_end_big) / SHR)
            return HK_BIG_H + (HK_SMALL_H - HK_BIG_H) * u, bx + (sx - bx) * u, by + (FOOT - by) * u, rot * (1 - u)
        hh, dx = HK_SMALL_H, 0.0
        for a, h2, d2 in lean:   # 乗り出すときは 0.3 秒で寄って、少し行き過ぎて戻る
            if t >= a:
                u = min(1.0, (t - a) / 0.3)
                k = ease(u) + (0.08 * np.sin(min(1, (t - a - 0.3) / 0.25) * np.pi) if t > a + 0.3 else 0)
                hh, dx = hh + (h2 - hh) * k, dx + (d2 - dx) * k
        rot = 4.0 * np.exp(-(t - lean[0][0]) * 3) * np.sin((t - lean[0][0]) * 12) if t > lean[0][0] else 0.0
        return hh, sx + dx, FOOT, rot

    # --- エイイチ（全身）---
    sprites = {}
    for k_, nm in EI_POSES.items():
        im = Image.open(f"src/ei/エイイチ_全身_{nm}_170px_v2.png").convert("RGBA")
        im = im.resize((round(im.width * EI_SCALE), round(im.height * EI_SCALE)), Image.NEAREST)
        pad = 20
        g = Image.new("L", (im.width + 2 * pad, im.height + 2 * pad), 0)
        g.paste(im.split()[3], (pad, pad))
        g = g.filter(ImageFilter.GaussianBlur(7)).point(lambda v: int(min(255, v * 1.6) * 0.45))
        lay = Image.merge("RGBA", (Image.new("L", g.size, 235), Image.new("L", g.size, 232), Image.new("L", g.size, 225), g))
        lay.alpha_composite(im, (pad, pad))
        sprites[k_] = lay.crop((pad, 0, pad + im.width, pad + im.height))
    ei_pcm = pcm.astype(np.float32) / 32768.0
    hop = SR // FPS
    rms = np.array([np.sqrt(np.mean(ei_pcm[i * hop:(i + 1) * hop] ** 2)) if eispk[i] else 0.0 for i in range(total)])
    ref = np.percentile(rms[rms > 0.01], 85) if (rms > 0.01).any() else 1.0
    bob = np.zeros(total, int)
    for i in range(0, total, 3):
        bob[i:i + 3] = -EI_SCALE if rms[i:i + 3].mean() > 0.45 * ref else 0
    poses = sorted((Tf(k, a), p_) for k, a, p_ in POSE)
    t_fall = Tf("end", 0.0)

    t_end = Tf(*ENDING["at"]) if "at" in ENDING else t_fall

    def ei_jump(t):
        if ENDING["kind"] != "jump" or t < t_end:
            return 0
        u = t - t_end
        return int(-120 * abs(np.sin(u * 7.0)) * np.exp(-u * 1.6)) if u < 2.0 else 0

    def fall_deg(t):
        if ENDING["kind"] != "fall":
            return 0.0
        t_fall_ = t_end
        if t < t_fall_:
            return 0.0
        u = (t - t_fall_) / 0.35
        if u < 1:
            return -78 * ease(u)
        return -78 + 6 * np.exp(-(t - t_fall_ - 0.35) * 7) * np.sin((t - t_fall_ - 0.35) * 22)

    # --- 音 ---
    voice = pcm.astype(np.float32) / 32768.0
    ref_v = np.percentile(np.abs(voice[voice != 0]), 90) if (voice != 0).any() else 0.1
    raw = subprocess.run(["ffmpeg", "-v", "error", "-i", "sound/bgm_明るいB_2.mp3", "-ac", "1", "-ar", str(SR), "-f", "s16le", "-"], capture_output=True, check=True).stdout
    bgm = np.frombuffer(raw, np.int16).astype(np.float32) / 32768.0
    bgm = np.tile(bgm, int(np.ceil(len(voice) / len(bgm))) + 1)[:len(voice)]
    bgm *= ref_v / (np.percentile(np.abs(bgm), 90) + 1e-6) * 10 ** (-24 / 20)
    fade = np.ones(len(voice)); nfo = int(1.0 * SR); fade[-nfo:] = np.linspace(1, 0, nfo)
    mix = voice + bgm * fade
    for k, off, name, lv in SE:
        f_ = os.path.join(K.BASE, "sound", name + ".wav")
        if not os.path.exists(f_):
            f_ = os.path.join(os.path.dirname(K.BASE), "_ショート専用", "_se_20261020", name + ".wav")
        raw = subprocess.run(["ffmpeg", "-v", "error", "-i", f_, "-ac", "1", "-ar", str(SR), "-f", "s16le", "-"], capture_output=True, check=True).stdout
        se = np.frombuffer(raw, np.int16).astype(np.float32) / 32768.0
        se *= ref_v / (np.abs(se).max() + 1e-6) * 10 ** (lv / 20)
        s0 = int(Tf(k, off) * SR)
        n = min(len(se), len(mix) - s0)
        if n > 0:
            mix[s0:s0 + n] += se[:n]
    mix *= 0.98 / max(0.98, np.abs(mix).max())
    K.write_wav("out/_mix.wav", (mix * 32767).astype(np.int16))

    # --- 重ねる ---
    out = f"out/AIあるある{SP.NO:02d}_{NAME}_版{getattr(SP, 'VER', 1)}.mp4"
    enc = subprocess.Popen(["ffmpeg", "-v", "error", "-y", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{W}x{H}", "-r", str(FPS), "-i", "-",
                            "-i", "out/_mix.wav", "-map", "0:v", "-map", "1:a", "-c:v", "libx264", "-preset", "medium", "-crf", "20", "-pix_fmt", "yuv420p",
                            "-af", "loudnorm=I=-14:TP=-1.5:LRA=11", "-c:a", "aac", "-b:a", "160k", "-ar", "48000", "-movflags", "+faststart",
                            "-frames:v", str(total), out], stdin=subprocess.PIPE)
    dec = subprocess.Popen(["ffmpeg", "-v", "error", "-i", mp4, "-f", "rawvideo", "-pix_fmt", "rgb24", "-"], stdout=subprocess.PIPE)
    for f in range(total):
        t = f / FPS
        bgf = np.frombuffer(dec.stdout.read(W * H * 3), np.uint8).reshape(H, W, 3).copy()
        pose = 1
        for a, p_ in poses:
            if t >= a:
                pose = p_
        spr = sprites[pose]
        ex, ey = W - spr.width - 12, FOOT - spr.height + bob[f] + ei_jump(t)
        deg = fall_deg(t)
        if abs(deg) > 0.2:
            img, (ox, oy) = rotate_about(spr, deg, (spr.width / 2, spr.height))
            blend(bgf, np.asarray(img).astype(np.float32), ex + ox, ey + oy)
        else:
            blend(bgf, np.asarray(spr).astype(np.float32), ex, ey)
        size, cx, fy, rot = hk_place(t)
        mdx, mdy, mdr, mir = move_off(t)
        cx, fy, rot = cx + mdx, fy + mdy, rot + mdr
        img = (hb if size > HK_SMALL_H + 60 else hs).frame(params[f])
        if mir:
            img = ImageOps.mirror(img)
        if abs(size - img.height) > 1:
            img = img.resize((round(img.width * size / img.height), round(size)), Image.LANCZOS)
        x, y = int(cx - img.width / 2), int(fy - img.height)
        if abs(rot) > 0.2:
            img, (ox, oy) = rotate_about(img, rot, (img.width / 2, img.height))
            x, y = x + ox, y + oy
        blend(bgf, np.asarray(img).astype(np.float32), x, y)
        for a, b, (sl, sx_, sy_) in subs:
            if a <= f < b:
                blend(bgf, sl, sx_, sy_)
        enc.stdin.write(bgf.tobytes())
    dec.stdout.close(); dec.wait()
    enc.stdin.close(); enc.wait()
    print(f"{out}  {total / FPS:.1f}秒")


if __name__ == "__main__":
    if len(sys.argv) > 2 and sys.argv[1] == "--still":
        build([float(x) for x in sys.argv[2].split(",")])
    else:
        build()
