# -*- coding: utf-8 -*-
"""考察回「壁打ちの型」の縦長ショート(1080×1920)・2版。
2026-11-02 エイイチの指摘で作り直した:
「おれが上半身で、ハコブくんが全身は変」→ エイイチも全身のドット絵(src/ei/エイイチ_全身_*_170px_v1.png・Gemini で写真の服装から作った)
「この始まりだとすぐ離脱しそう」「宇宙・紙の山・引退式の背景を使って、ハコブくんが大きく動く所から始める」
→ 最初の数秒は、画面いっぱいの絵の前で、大きなハコブくん(高さ900)が動く。本題に入ったら、絵を暗くして紙(考察ノート)を出し、
ハコブくんは左下の全身(高さ400)に戻る
画面の割り振りは video_rules.md 9-2: 0〜150 何も置かない/170〜440 見出し/480〜1130 紙/左下 ハコブくん・右下 エイイチ(足もと y=1640)/字幕は2人の間の上
使い方: python3 kabe_short.py [番号 ...]
"""
import os, sys, json, wave, subprocess, html as _html
import numpy as np
from PIL import Image
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import kabe_kit as K
from kabe_kit import ink, land, card, counter_js, shake_js, ease, blend, rotate_about, hk, FPS, SR
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 # 2人の足もと(下の方まで使ってよい・2026-11-02 エイイチ)
SUB_MID = 1150 # 字幕の真ん中
SUB_CX, SUB_MAXC = 540, 11 # 字幕は画面の真ん中(2026-11-02 エイイチの指摘で 470 から直した)・1行11字
EI_SCALE, EI_X = 4.5, None # エイイチの全身(170px の4.5倍=高さ765・右端に寄せる)。身長はエイイチ 1700mm・ハコブくん 1150mm(2026-11-02 エイイチ)
HK_SMALL_H, HK_X = 515, 10 # ふだんのハコブくん(全身)。765 × 1150/1700
HK_BIG_H = 900 # 最初の数秒の大きなハコブくん
EI_POSES = {1: "立つ", 2: "腕組み", 3: "驚く", 4: "笑う"}
VCSS = """
html, body { width:1080px; height:1920px; }
.f { width:1080px; height:1920px; }
.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; }
.vpaper { position:absolute; left:100px; top:480px; width:880px; height:590px; background:#f5f3ee; box-shadow:0 30px 60px rgba(0,0,0,.45); opacity:0; }
.vpaper .card { font-size:40px; }
.vpaper .card small { font-size:24px; }
.vstack { position:absolute; left:0; right:0; top:40px; bottom:40px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:22px; }
.bgimg { position:absolute; inset:0; background-repeat:no-repeat; background-color:#fff; }
@keyframes veil { to { opacity:.74; } }
@keyframes pIn { from { opacity:0; transform:translateY(60px) rotate(-1.5deg); } to { opacity:1; transform:none; } }
.vbig { position:absolute; left:0; right:0; text-align:center; font-family:'Noto Sans CJK JP',sans-serif; font-weight:900; color:#fff; line-height:1.15;
-webkit-text-stroke:5px #141311; paint-order:stroke fill; text-shadow:0 10px 0 #c4472a; z-index:6; }
.vbig span { display:inline-block; opacity:0; }
"""
def vpage(head, body):
return K.page(body + f'
{head}
', VCSS)
def bg(img, size_h, pos_x, pos_y, t_veil):
p = "file://" + os.path.abspath(img)
return (f'
'
f'
'
'絵は生成AI(Gemini)で作成
')
def paper(inner, t_in):
return f'{inner}
'
def grp(inner, t_in=0.0, t_out=None):
an = f"show .01s linear {t_in:.3f}s forwards" + (f", hide .2s linear {t_out:.3f}s forwards" if t_out is not None else "")
return f'{inner}
'
def line(text, t, size=40, weight=700, step=0.05, style=""):
return f'{ink(text, t, step)}
'
def words(rows, t0, t_out, top, size=130, gap=0.32):
"""大きな一言を1行ずつ叩きつける。rows は [(文字, 色)]"""
out = []
for k, (txt, col) in enumerate(rows):
out.append(f'{txt} ')
return f'{" ".join(out)}
'
def bigword(text, t, t_out, top, size=300, color="#c4472a", rot=-8):
return (f'{text}
')
WHITE, GOLD = "#ffffff", "#ffd84a"
# ====================== 3本の中身(T(k, 秒) は k 番目の文の頭から。k=-1 は動画の頭) ======================
def short_uchu(T, S):
tp = T(2, 0.0) # 「宇宙は、今日は置いておきます」で、絵を暗くして紙を出す
return vpage("ロボットのAI、 考えるのに18.6秒", (
bg("src/bg/宇宙.jpg", 1920, -1500, 0, tp)
# 誰の言葉かを先に見せる(2026-11-02 エイイチ「イーロン・マスク氏の話を伝えないと何か分からない」)
+ f'イーロン・マスクさんの X の投稿
'
+ words([("超知能の", WHITE), ("ほぼ100%は", WHITE), ("深宇宙で計算", GOLD)], T(0, 0.3), tp, 570, size=120, gap=0.9)
+ paper(grp(f'ハコブくん 考え中
'
+ f'0.0 {land("秒", T(4, 0.05), "font-size:84px")}
'
+ line("大きなAIだと、1回考えるのにこのくらい", T(4, 0.6), size=34, weight=500)
+ line("その間に π0.5 は、動きを370回ほど直せる", T(6, 0.5), size=36, weight=700), tp + 0.1), tp)
+ bigword("遅っ", T(5, 0.0), T(6, 1.0), 640, size=330)
+ counter_js("cnt", T(3, 0.2), T(4, -0.05), 18.6) + shake_js([(T(5, 0.25), 0.3)])))
def short_kami(T, S):
tp = T(1, 0.0)
return vpage("424本、 捨てられない?", (
bg("src/bg/紙の山.jpg", 1920, -1180, 0, tp)
+ words([("424本", GOLD)], T(0, 5.0), tp, 520, size=230)
+ words([("捨てられない人?", WHITE)], T(0, 7.6), tp, 800, size=110)
+ paper(grp(card("mi", ink("LOGは、全部の積み重ね。基幹情報", T(1, 0.4), 0.05), T(1, 0.15), "見立て ・ エイイチ", w=760, anim="wipe")
+ card("mi", ink("情報がたくさんあるだけでは、言葉や動きは良くならない", T(2, 0.4), 0.04), T(2, 0.15), "見立て ・ エイイチ", w=780, anim="wipe")
+ ''
+ card("fact", "記事の全文", T(3, 0.4), "海馬", w=330)
+ card("fact", "整理した記事", T(3, 2.3), "大脳", w=330)
+ '
', tp + 0.1), tp)))
def short_intai(T, S):
tp = T(2, 0.0) # 本題(何にでも使える大きなAIが…)に入ったら紙を出す
return vpage("π0.5 は、 もう引退?", (
bg("src/bg/引退式.jpg", 2300, -1933, 230, tp)
+ f''
f'祝・引退
'
+ bigword("まだ現役", T(1, 0.1), tp, 850, size=180)
+ paper(grp(card("fact", ink("何にでも使える大きなAIが、追加の練習なしで、ブロックを器に入れた", T(2, 0.4), 0.035), T(2, 0.15), "海外の投資家の書き込み", w=760)
+ card("hb", "π0.5 は、引退", T(2, 0.3), "ハコブくん", w=460, botsu=T(2, 0.6), fade=T(2, 0.8))
+ card("mi", ink("汎用モデルも、使い所次第。組み合わせになる", T(3, 0.4), 0.045), T(3, 0.15), "見立て ・ エイイチ", w=760, anim="wipe"),
tp + 0.1, T(4, 0.0))
+ grp(f'{ink("分けるしかない", T(4, 0.3), 0.07)}
'
+ ''
+ f'
遅く考える 頭
'
+ f'
速く直す 手
'
+ '
', T(4, 0.05)), tp)
+ shake_js([(T(1, 0.4), 0.25)])))
# segs: (本編の文番号, 後ろの間[, 頭を切る秒, 字幕の文])
# big: 大きなハコブくんの出る間(end の文の頭で、左下の大きさへ)。kind: rise(下から飛び出す)/ hop(跳ねて喜ぶ)
SHORTS = [
dict(no=1, name="遅っ", fn=short_uchu, lead=0.5,
segs=[(18, 0.3), (19, 0.4), (20, 0.5), (21, 3.0), (22, 0.3), (23, 0.5), (25, 1.2)],
big=dict(kind="rise", end=1),
face=[(-1, 0.0, 6.5, "surprise"), (1, 0.0, 1.6, "trouble"), (3, 0.0, 5.5, "think"), (4, 0.0, 1.0, "smile"), (5, 0.0, 1.4, "trouble")],
pose=[(-1, 0.0, 3), (1, 0.0, 2), (2, 0.5, 1), (5, 0.0, 2), (6, 0.0, 3)], ei_rise=None,
se=[(-1, 0.0, "se_whoosh", -8), (0, 0.3, "se_pop", -10), (0, 1.2, "se_pop", -10), (0, 2.1, "se_pop", -8), (5, 0.0, "se_pop", -6)]
+ [(3, 0.2 + k, "se_tick", -16) for k in range(5)]),
dict(no=2, name="捨てられない人", fn=short_kami, lead=0.4,
segs=[(59, 0.5, 1.15, "エイイチは、記事を全部、全文で残しています。番号は、424番まで来ています。……捨てられない人ですか?"), (60, 0.4), (61, 0.6, 0.0, "情報がたくさんある|だけじゃ、言葉や動きは良くならない。"), (63, 1.2)],
big=dict(kind="rise", end=1),
face=[(-1, 0.0, 9.5, "doubt"), (1, 0.0, 4.0, "surprise"), (3, 0.0, 4.5, "smile")],
pose=[(-1, 0.0, 3), (1, 0.0, 2), (2, 0.0, 1), (3, 0.0, 4)],
ei_rise=(0, 7.4), # 「捨てられない人ですか?」で、紙の山の下からエイイチが出てくる
se=[(-1, 0.0, "se_whoosh", -8), (0, 5.0, "se_pop", -6), (0, 7.4, "se_rise", -10), (0, 7.6, "se_pop", -8)]),
dict(no=3, name="勝手に引退させないで", fn=short_intai, lead=0.6,
segs=[(13, 1.0, 1.05, "エイイチの研究室の π0.5 は……引退ですね。お疲れさまでした。"), (14, 0.4), (12, 0.35), (15, 0.5), (36, 1.2)],
big=dict(kind="hop", end=1),
face=[(-1, 0.0, 7.0, "joy"), (1, 0.0, 2.0, "surprise"), (3, 0.0, 4.4, "trouble")],
pose=[(-1, 0.0, 1), (0, 3.0, 3), (1, 0.0, 2), (2, 0.0, 1), (3, 0.0, 2), (4, 0.0, 1)], ei_rise=None,
se=[(-1, 0.0, "se_rise", -8), (-1, 0.2, "se_pop", -8), (1, 0.1, "se_pop", -6), (2, 0.0, "se_whoosh", -12)]),
]
def vpages10(text):
"""字幕の枚(1行10字・3行まで)。short_kit.wrap の折り方で"""
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(sh):
idx_main = json.load(open("tts/index.json"))
ei_main = set(K._ei_lines())
lead = round(sh.get("lead", 0) * FPS)
segs, t = [], lead
for k, sg in enumerate(sh["segs"]):
i, pause = sg[0], sg[1]
trim = sg[2] if len(sg) > 2 else 0.0
o = idx_main[i]
dur = o["dur"] - trim
frames = round((dur + pause) * FPS)
segs.append(dict(k=k, i=i, trim=trim, text=sg[3] if len(sg) > 3 else o["text"], face=o.get("face", "base"),
start=t, speech=round(dur * FPS), frames=frames, ei=i in ei_main))
t += frames
total = t
if total / FPS > 30.5:
sys.exit(f"ショート{sh['no']} が {total / FPS:.1f}秒あります(30秒以内)")
Tf = lambda k, off=0.0: (segs[k]["start"] / FPS if k >= 0 else 0.0) + off
Sf = lambda k: segs[k]["speech"] / FPS
html = sh["fn"](Tf, Sf)
folder = f"ショート/{sh['no']:02d}_{sh['name']}"
os.makedirs(folder + "/png", exist_ok=True)
# --- 声 ---
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[int(s["trim"] * SR):][:s["frames"] * SR // FPS]
if s["trim"]:
a = (a * np.minimum(1, np.arange(len(a)) / (0.02 * SR))).astype(np.int16) # 切った頭を短くなじませる
s0 = s["start"] * SR // FPS
pcm[s0:s0 + len(a)] = a
pcm = pcm[:total * SR // FPS]
# --- 字幕と紙の映像 ---
from playwright.sync_api import sync_playwright
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:#fff;stroke:#0D0D0D;stroke-width:12px;stroke-linejoin:round;paint-order:stroke fill}}")
subs = []
with sync_playwright() as p:
br = p.chromium.launch()
pg = br.new_page(viewport={"width": W, "height": H})
for s in segs:
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'{_html.escape(ln)} ' for ln in ptxt.split("\n"))
pg.set_content(f'{svg} ')
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"{folder}/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 = f"{folder}/_paper.mp4"
K.render_block(dict(name=sh["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]
if lead:
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 sh["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 = Tf(sh["big"]["end"])
SHR = 0.5 # 小さくなるのにかける秒
def hk_place(t):
"""→ (高さ, 足もとの中心 x, 足もと y, 角度)"""
sx, sy = HK_X + hs.w / 2, FOOT
if t >= t_end_big + SHR:
return HK_SMALL_H, sx, sy, 0.0
if sh["big"]["kind"] == "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)
bx, by = 540 + 40 * np.sin(t * 1.1), 1990 + (1900 - 1990) * k + 14 * np.sin(t * 2.2)
rot = 6 * np.sin(t * 1.3)
else: # hop: 跳ねて喜ぶ
ph = (t * 1.9) % 1.0
bx, by = 380 + 30 * np.sin(t * 2.0), 1900 - 160 * 4 * ph * (1 - ph)
rot = 8 * np.sin(t * 3.8)
if t < 0.3:
by = 1920 + (by - 1920) * ease(t / 0.3)
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 + (sy - by) * u, rot * (1 - u)
# --- エイイチ(全身・2倍) ---
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)
# 暗い背景に服が沈むので、うすい光の縁を付ける(本編の暗い場面と同じ)
from PIL import ImageFilter
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_] = np.asarray(lay.crop((pad, 0, pad + im.width, pad + im.height))).astype(np.float32)
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 sh["pose"])
def ei_dy(t):
"""ei_rise: その時刻まで画面の下に隠れていて、下から出てくる"""
if not sh.get("ei_rise"):
return 0
t0 = Tf(*sh["ei_rise"])
if t < t0:
return 760
return int(760 * (1 - ease((t - t0) / 0.35)))
# --- 音 ---
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 sh["se"]:
raw = subprocess.run(["ffmpeg", "-v", "error", "-i", os.path.join(K.BASE, "sound", name + ".wav"), "-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(f"{folder}/_mix.wav", (mix * 32767).astype(np.int16))
# --- 重ねる ---
out = f"{folder}/ショート{sh['no']}_{sh['name']}_版2.mp4"
enc = subprocess.Popen(["ffmpeg", "-v", "error", "-y", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{W}x{H}", "-r", str(FPS), "-i", "-",
"-i", f"{folder}/_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]
blend(bgf, spr, W - spr.shape[1] - 12, FOOT - spr.shape[0] + bob[f] + ei_dy(t)) # エイイチは右端に寄せる
size, cx, fy, rot = hk_place(t)
img = (hb if size > HK_SMALL_H + 1 else hs).frame(params[f])
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}秒")
return out
if __name__ == "__main__":
want = [int(a) for a in sys.argv[1:]] or [s["no"] for s in SHORTS]
for sh in SHORTS:
if sh["no"] in want:
build(sh)