# -*- coding: utf-8 -*- # 動画「人型ロボットは人間より速く走ったのに、なぜ洗濯物はたためないのか(仮題)」の場面(謎解きの型の1本目) PATTERN = 2 # 画面パターン2(大文字型・video_rules.md 4-4c)。2026-10-28 エイイチが決定(大文字型+動きあり) MOVE = True # 画面型の動き(4-4d)を使う # 画面の文字は「名前・数字・一言」だけ。中身は声と字幕に任せる # 他社のロボット・大会の映像や写真・ロゴは使わない(許可がないため)。図はコードで描いた模式図だけ # 謎解きの型(2026-10-28): # - 出どころの札(左上): 大会や会社の発表(黒)/ニュース(青)/論文や本(灰の枠)/エイイチの記録と見立て(オレンジ) # - エイイチ(ドット絵・声なし): ei_intro で真ん中に大きく登場 → ei_react・ei_desk*・mitate* で右下の小窓。ほかの場面には出さない # ドット絵は 100 ドットの絵を等倍の整数倍で広げる(ぼかさない)。まばたきは閉じた目の絵を 0.15 秒だけ重ねる # 強弱(4-4d): 強=叩きつけ・入れ替え(hook・hook_time・hook_answer・question・teaser・map2・c2_joke・solve・solve_fold・answer) # 中=数え上げ(c1_sim・c3_figure)・エイイチの小窓(ぽん・まばたき)・机の上の映像 import os as _os, sys as _sys, json as _json, base64 as _b64 _here = _os.path.dirname(_os.path.abspath(__file__)) for _c in (_os.environ.get("P2M_BASE", ""), _os.path.join(_here, "..", "base"), _os.path.join(_here, "..", "_制作ベース")): if _c and _os.path.exists(_os.path.join(_c, "scene_kit.py")): _sys.path.insert(0, _c); break from scene_kit import * from scene_kit import _p2page from motion_kit import use_motion T1 = "1練習する場所" T2 = "2相手の形" T3 = "3お手本の数" T4 = "4エイイチの見立て" # --- 場面ごとの、文の始まりの秒(声の長さから)--- _idx = _json.load(open(_os.path.join(_here, 'tts/index.json'))) if _os.path.exists(_os.path.join(_here, 'tts/index.json')) else [] _LINES = {} for _o in _idx: _d = _LINES.setdefault(_o['scene'], []) _t0 = (_d[-1][0] + _d[-1][1] + _d[-1][2]) if _d else 0.0 _d.append((_t0, _o['dur'], _o['pause'])) def at(scene, k=0, f=0.0): """その場面の k 番目の文の、読み上げの f 割の所(秒)""" L = _LINES.get(scene) if not L: return 0.3 k = min(k, len(L) - 1) return round(L[k][0] + L[k][1] * f, 2) def scene_len(scene): L = _LINES.get(scene) return (L[-1][0] + L[-1][1] + L[-1][2]) if L else 4.0 # --- 出どころの札(左上)--- FUDA_CSS = """ .fuda{position:absolute;left:140px;top:96px;display:flex;align-items:center;gap:14px;font-size:30px;font-weight:700;color:#0D0D0D} .fuda .k{padding:6px 18px;border-radius:999px;font-size:28px;font-weight:900} .fuda .k.hap{background:#0D0D0D;color:#fff} .fuda .k.news{background:#1A6C98;color:#fff} .fuda .k.ken{border:3px solid #888888;color:#555} .fuda .k.ei{background:#C4471F;color:#fff} """ FUDA_NAME = {"hap": "大会や会社の発表", "news": "ニュース", "ken": "論文や本", "ei": "エイイチの記録と見立て"} def fuda(kind, who): return f'
{FUDA_NAME[kind]}{who}
' # --- エイイチ(ドット絵)--- def _uri(p): return 'data:image/png;base64,' + _b64.b64encode(open(_os.path.join(_here, p), 'rb').read()).decode() import ei_talk as _talk EI = {k: (_talk.base_uri(k), _talk.base_uri(k, True)) for k in ('udegumi', 'unazuku', 'yubisasu', 'hirameku')} # エイイチが話す文(narration.py の EI_LINES)→ (場面, 場面の中で何番目の文か, 文番号) import importlib.util as _ilu _sp = _ilu.spec_from_file_location('_narr_here', _os.path.join(_here, 'narration.py')) _nm = _ilu.module_from_spec(_sp); _sp.loader.exec_module(_nm) _EIL = getattr(_nm, 'EI_LINES', []) _EI_SPEAK = {} _cnt = {} for _o in _idx: _k = _cnt.get(_o['scene'], 0); _cnt[_o['scene']] = _k + 1 if _o['i'] in _EIL: _EI_SPEAK.setdefault(_o['scene'], []).append((_k, _o['i'])) EI_CSS = """ .eiw{position:absolute;right:10px;bottom:0px;width:420px;height:420px;overflow:hidden} .eiw img{position:absolute;left:0;top:8px;width:420px;height:420px;image-rendering:pixelated} .ein{position:absolute;right:40px;bottom:346px;width:300px;text-align:center;font-size:30px;font-weight:900;color:#0D0D0D} .ein span{display:inline-block;background:#F5F5F0;border:3px solid #D6D6D1;border-radius:999px;padding:4px 20px} """ def _blinks(start, end): t, out = start, [] while t < end - 0.4: out.append(round(t, 2)); t += 3.3 return out def ei_wipe(pose, scene, pop=None, name=None, poses=None): """右下の小窓。pop=秒 で、ぽんと出る。poses=[(秒, 絵)] で途中で絵を替える(指さしは大事な所だけ。2026-10-28 エイイチの指示)。 話している間は口と目を声に合わせて動かし、話していない間はまばたきだけする。 絵ごとにまとまり(div)を作り、替える時刻に前のまとまりを消して(out)、次を出す(fade)。絵の透明な所から前の絵が見えないように""" seq_p = sorted(poses or [(0, pose)]) if seq_p[0][0] > 0: seq_p = [(0, pose)] + seq_p L = scene_len(scene) + 1 segs = [(t, seq_p[k + 1][0] if k + 1 < len(seq_p) else L, p) for k, (t, p) in enumerate(seq_p)] spans, mouth, blinks = [], [], [] for k, i in _EI_SPEAK.get(scene, []): t0 = at(scene, k, 0.0) sq, end = _talk.mouth_seq(i, t0) spans.append((t0, end)); mouth += sq blinks += [(b, 0.13) for b in _talk.blink_times(t0, end)] mouth.sort() blinks += [(b, 0.15) for b in _blinks((pop or 0) + 1.3, scene_len(scene)) if not any(a0 - 0.2 <= b <= a1 + 0.2 for a0, a1 in spans)] out = '' for s0, s1, p in segs: ev = [] lv = ([l for (t, l) in mouth if t <= s0] or [1])[-1] if lv != 1 and p in _talk.MOUTH: ev.append((s0, f'')) for t, l in mouth: if s0 < t < s1 and p in _talk.MOUTH: ev.append((t, f'')) e = _talk.eye_patches(p) if p != 'unazuku' else None for b, d in blinks: if e and s0 <= b and b + d < s1: ev.append((b, f'')) ev.append((round(b + d, 2), f'')) ev.sort(key=lambda x: x[0]) inner = f'' + ''.join(h for _, h in ev) if s0 > 0: inner = f'
{inner}
' if s1 < L: inner = f'
{inner}
' out += inner a = f' data-a="pop" data-t="{pop}" data-d="0.35"' if pop is not None else '' nm = f'
{name}
' if name else '' return f'{nm}
{out}
' def add(page_html, *parts): return page_html.replace('', ''.join(parts) + '', 1) S = {} MOTION = {} # 0 謎 S["hook"] = p2_word(['世界記録より速い', '洗濯物はまだ'], kicker="2026年の、ちょっと不思議な話", kc="muted", size=150) S["hook_race"] = add(p2_num("300", "台以上", kicker="2026年4月 北京|人型ロボットのハーフマラソン", sub="21.0975km"), fuda("hap", "大会の発表(新華社など)")) S["hook_time"] = add(p2_num("50:26", "", kicker="優勝したロボットのタイム", sub="自律で走る区分"), fuda("hap", "大会の結果(2026年4月)")) S["hook_wr"] = add(p2_pair("50:26", "56:51", da="ロボット(優勝)", db="人間の男子
(世界記録候補・公認手続き中)", ca="red", cb="", mid="/", size=170), fuda("hap", "World Athletics(2026年8月)")) S["hook_last"] = add(p2_pair("2:40:42", "50:26", da="2025年の優勝", db="2026年の優勝", ca="", cb="red", mid="→", size=150, sub="機体はちがう|平均の速さは3倍あまり"), fuda("hap", "大会の結果(2025・2026)")) S["hook_joke"] = p2_word(['走るのは', 'あきらめた'], kicker="ハコブくん", kc="muted", size=170) # エイイチ、登場(真ん中に大きく) EI_BIG = """""" _o, _c = EI['udegumi'] _bl = ''.join(f'' for b in _blinks(1.6, scene_len('ei_intro'))) S["ei_intro"] = page(EI_BIG + f'
{_bl}
エイイチこの番組を作っている人
') MOTION["ei_intro"] = dict(type='html', html=S["ei_intro"]) S["ei_react"] = add(p2_word(['性能は、すごい', '仕事とは別?'], kicker="このニュースを見たエイイチの感想", kc="muted", size=140, sub="少なくとも直接は、仕事に関係なさそう"), fuda("ei", "エイイチの感想"), ei_wipe('udegumi', 'ei_react', pop=0.1)) MOTION["ei_react"] = dict(type='html', html=S["ei_react"]) S["hook_turn"] = p2_quiz(['あなたの洗濯物', '任せられる?'], pill="聞かせてください", size=150) S["hook_answer"] = p2_word(['まだ'], kicker="今日までに確かめた公開資料では", kc="muted", size=300, sub="人の手を借りずに、家で毎日たたみ続ける
人型ロボットは、見当たらない") S["hook_q2"] = p2_word(['人より速いのに', '任せられない'], kicker="この逆転、ちょっと不思議", kc="muted", size=150) # 1 自己紹介(自分のアームの映像) O = { "title": overlay('
フィジカルAI解説
案内役:ハコブくん(AI音声)
'), "title_eiichi": overlay('
エイイチ
デザイナー/ロボティクスの研究・開発
'), "outro": overlay('
相談は概要欄の連絡先から
高評価・チャンネル登録/コメントはエイイチが全部読んでいます
'), } VIDEO = { "title": ("src/day124_crop.mp4", 4.5, False), "title_eiichi": ("src/day124_crop.mp4", None, False), "outro": ("src/day124_crop.mp4", 18.0, False), } # 2 今日の謎 S["question"] = p2_quiz(['走るは追い越した', 'たたむは、なぜまだ?'], pill="今日の謎", size=130) S["define"] = p2_word(['家で・人の手なしで', '毎日'], kicker="ここで言う、まだ", kc="muted", size=130, sub="任せられるところまでは来ていない") S["teaser"] = p2_word(['犯人は', '足でも手でもない'], kicker="先に言っておくと", kc="muted", size=140) # 3 聞くときの約束(4種類の札を大きく見せる) _cards = ''.join(f'
{FUDA_NAME[k]}
' for k in ("hap", "news", "ken", "ei")) S["promise_cards"] = _p2page(f'
出どころの札(画面の左上に出します)
{_cards}
') S["promise_fair"] = add(p2_list([("1", "電池の交換"), ("2", "機体の交換"), ("3", "伴走の車"), ("4", "一部の補助")], kicker="ロボットの条件(人の記録とはちがう)", size=96), fuda("news", "大会の規則(中国新聞網など)")) # 4 地図(たとえは1つだけ) S["map"] = p2_pair("体育", "家庭科", da="走る", db="洗濯物をたたむ", ca="blue", cb="red", mid="/", kicker="たとえ|ロボットの学校", size=170) S["map2"] = p2_pair("5", "1", da="体育", db="家庭科", ca="blue", cb="red", mid="/", kicker="成績表にすると(たとえ)", size=220) S["toc"] = p2_list([("1", "練習する場所"), ("2", "相手の形"), ("3", "お手本の数"), ("4", "エイイチの見立て")], kicker="3つの手がかり", size=100) # 5 ハコブくんの早とちり S["hayatochiri"] = p2_word(['手のほうが', '部品が細かい?'], kicker="ハコブくんの推理", kc="blue", size=150) S["hayatochiri2"] = p2_word(['問題は', '覚えさせ方'], kicker="細かく動く手は、もう作られ始めている", kc="muted", size=160) # 6 手がかり1 練習する場所 S["c1"] = chapter2("1つ目", "練習する場所", "体育の練習は、コンピューターの中でもできる") SRC_RUDIN = "出典:Rudin ら(ETH Zurich・NVIDIA)CoRL 2021" S["c1_sim"] = add(p2_num("4096", "台", kicker="仮想の世界で、同時に歩く練習", sub="4本足|平地4分足らず・でこぼこ20分足らず|計算機1台", tag=T1, src=SRC_RUDIN), fuda("ken", "論文(2021年)")) S["c1_joke"] = p2_word(['転んだら', 'やり直し'], kicker="4096台の体育館", kc="muted", size=170, tag=T1) S["c1_note"] = add(p2_pair("2本足も", "優勝機は?", da="2021年に仮想の練習で走った", db="学ばせ方は見当たらない", ca="blue", cb="red", mid="/", size=140, tag=T1), fuda("hap", "オレゴン州立大学/大会の報道")) S["c1_why"] = p2_word(['相手は', '地面'], kicker="計算で再現しやすい", kc="muted", size=180, sub="坂・段差・すべりやすさのちがいはある", tag=T1) S["c1_speed"] = add(p2_pair("練習のしかた", "機体づくり", da="走りを覚えさせる", db="モーター・冷やし方・投資", ca="blue", cb="red", mid="と", kicker="この1年で記録が縮んだのは", size=120, sub="分けて考える", tag=T1), fuda("news", "Scientific American(専門家の見方)")) # 7 手がかり2 相手の形 S["c2"] = chapter2("2つ目", "相手の形", "洗濯物は、さわるたびに形が変わる") S["c2_cloth"] = add(p2_word(['同じ服でも', '毎回ちがう形'], kicker="袖が垂れ・すそがねじれ・布が重なる", kc="muted", size=150, sub="仮想の世界で正しく動かすのが難しい", tag=T2, src="出典:RGBench(2025)"), fuda("ken", "論文(2025年)")) S["c2_joke"] = p2_word(['問題の形まで', '変えてくる'], kicker="洗濯物の先生", kc="muted", size=150, sub="正解のたたみ方も、家ごとにちがう", tag=T2) S["c2_end"] = p2_pair("仮想の練習", "本物のお手本", da="", db="", ca="blue", cb="red", mid="+", kicker="どう組み合わせるか", size=105, tag=T2) # 8 手がかり3 お手本の数 S["c3"] = chapter2("3つ目", "お手本の数", "今日いちばん大事なところ") S["c3_pi"] = add(p2_num("1万", "時間超", kicker="Physical Intelligence(π0 の論文)", sub="いろいろな作業を合わせた動きのデータ|動かない台に腕2本", tag=T3, src="出典:π0(arXiv 2410.24164)"), fuda("ken", "論文(2024年)")) S["c3_figure"] = add(p2_num("62", "%", kicker="Figure の発表(2026年9月)|初めて入る30の家", sub="タオルたたみ|会社自身の試験・毎日任せた記録とは別", tag=T3), fuda("hap", "Figure の発表")) S["c3_neo"] = add(p2_word(['量産は', '始まった'], kicker="1X社 NEO(2026年4月の発表)", kc="muted", size=160, sub="一般の出荷は2026年予定|覚えていない作業は遠隔で導く", tag=T3), fuda("hap", "1X の発表")) S["c3_gap"] = add(p2_num("10万", "年分", kicker="言葉のAIが学んだ文章を、人が読む時間に直すと", sub="ゴールドバーグさん|データの規模のたとえ", tag=T3), fuda("news", "UCバークレーのインタビュー")) S["c3_end"] = p2_word(['数だけでは', '足りない'], kicker="撮り方・見え方の工夫も要る", kc="muted", size=160, tag=T3) # 証人: エイイチの机の上(自分のアームの映像の上に黒い板。エイイチは右下) CARD_CSS = """.card{position:absolute;left:420px;top:170px;width:1080px;padding:44px 64px;border-radius:30px;background:rgba(8,8,8,.86);color:#fff;text-align:center} .card .ck{font-size:42px;font-weight:700;color:#bbb;margin-bottom:12px}.card .cb{font-size:170px;font-weight:900;line-height:1.05} .card .cb small{font-size:80px;color:#C4471F;margin-left:10px}.card .cs{font-size:44px;font-weight:700;color:#bbb;margin-top:16px} .cap{position:absolute;left:96px;top:80px;background:rgba(13,13,13,.78);color:#fff;font-size:30px;font-weight:700;padding:12px 24px;border-radius:10px}""" def desk(scene, cards, pose, pop=None): body = f'' for i, (t, k, b, s) in enumerate(cards): # 2枚目からは板を不透明にして、前の板の文字が透けないようにする(2026-10-28 確認用で透けていた) st = ' style="background:#080808"' if i else '' body += f'
{k}
{b}
{s}
' body += fuda("ei", "エイイチの記録(机の上の mechArm 270-Pi)").replace('class="fuda"', 'class="fuda" style="color:#fff"') + ei_wipe(pose, scene, pop=pop) return overlay(body) DESK_BG = dict(file='src/day124_crop.mp4', ss=6.0, dim=0.55, blur=0, cy=0.5) S["ei_desk"] = add(p2_word(['現場の', '証人'], kicker="エイイチの机の上", kc="muted", size=160, tag=T3), fuda("ei", "エイイチの記録"), ei_wipe('udegumi', 'ei_desk')) MOTION["ei_desk"] = dict(type='html', bg=DESK_BG, html=desk('ei_desk', [ (0.2, "ここで、現場の証人", "机の上", "ブロックをつかんで箱に置く練習"), (at('ei_desk', 2, 0.0), "6月〜10月|通しの試行", "750回", "失敗も含めた、試行錯誤の記録"), ], 'udegumi', pop=0.2)) S["ei_desk2"] = add(p2_word(['上カメラ', '1台'], kicker="エイイチの苦労", kc="muted", size=160, tag=T3), fuda("ei", "エイイチの記録"), ei_wipe('udegumi', 'ei_desk2')) MOTION["ei_desk2"] = dict(type='html', bg=dict(DESK_BG, ss=14.0), html=desk('ei_desk2', [ (0.2, "いま試しているお手本の集め方", '手で教えて再生', "要所の姿勢を教え、再生して撮る"), (at('ei_desk2', 1, 0.0), "エイイチの苦労", '上カメラ1台', "アームや指でブロックが隠れる"), ], 'udegumi')) # 9 謎が解ける S["solve"] = p2_word(['謎を', '解きます'], kicker="3つの手がかりがそろいました", kc="muted", size=190) S["solve_run"] = p2_pair("練習しやすい", "機体と投資", da="相手は地面(仮想の世界)", db="この1年の伸び", ca="blue", cb="blue", mid="+", kicker="走るが追い越したのは(と考えられる)", size=100) S["solve_fold"] = p2_pair("練習しにくい", "お手本に時間", da="布は形が変わる", db="本物を集める", ca="red", cb="red", mid="+", kicker="たたむが、まだなのは(理由の1つ)", size=100) S["solve_moravec"] = add(p2_pair("知能テスト", "見て動く", da="比較的やさしい", db="1歳児並みでも難しい", ca="blue", cb="red", mid="/", kicker="ハンス・モラベック(1988年の本の趣旨)", size=140), fuda("ken", "本(1988年)")) S["solve_twist"] = add(p2_pair("走る・宙返り", "器用な手作業", da="仮想の練習でうまくいく", db="まだうまくいかない", ca="blue", cb="red", mid="/", kicker="ゴールドバーグさん", size=100, sub="速く走るほうが難しく思えるのは、人の思い込み(別の研究者)"), fuda("news", "インタビュー・報道")) # 10 見立て(エイイチは右下) S["mitate"] = add(chapter2("最後に", "見立て", "エイイチの考え"), ei_wipe('udegumi', 'mitate', pop=0.3)) MOTION["mitate"] = dict(type='html', html=S["mitate"]) # 指さし・ひらめきは大事な所だけ、少しの間(2026-10-28 エイイチの指示「ずっと指さしてるのはおかしい」) EI_POINT = { "mitate2": [(at('mitate2', 1, 0.0), 'yubisasu'), (round(at('mitate2', 1, 0.0) + 2.8, 2), 'udegumi')], # 「出資を集める意図が強い」と言い切る所 "mitate_end": [(0.3, 'hirameku'), (3.2, 'udegumi')], # 「別のものさしで」 } for _n, _p, _pg in ( ("mitate1", 'udegumi', p2_word(['人型で', 'なくていい'], kicker="エイイチの見立て", kc="muted", size=160, sub="台の上でたたむだけなら、足は要らない", tag=T4)), ("mitate2", 'udegumi', p2_word(['派手な動きは', '出資集め'], kicker="エイイチの見立て|中国の人型ロボット", kc="muted", size=160, sub="現場で仕事ができるかは、仕事ごとに確かめる", tag=T4)), ("mitate3", 'udegumi', p2_num("5", "年くらい", kicker="どの家庭でも任せられるまで", sub="エイイチの感覚", tag=T4)), ("mitate_end", 'udegumi', p2_word(['別の', 'ものさしで'], kicker="走るニュースと、家の洗濯物", kc="muted", size=170, tag=T4)), ): S[_n] = add(_pg, fuda("ei", "エイイチの見立て"), ei_wipe(_p, _n, poses=EI_POINT.get(_n))) MOTION[_n] = dict(type='html', html=S[_n]) # 11 答えと、持ち帰る3つ S["answer"] = p2_pair("練習しやすい
+機体と投資", "形が変わる
+お手本に時間", da="走る", db="たたむ", ca="blue", cb="red", mid="/", kicker="今日の謎の答え", size=96, sub="まだ=家で、人の手を借りずに、毎日") S["check3"] = p2_list([("1", "自分で動く?"), ("2", "何回中何回?"), ("3", "お手本の数は?")], kicker="すごい動画を見たら、確かめる3つ", size=110) S["hakobu_end"] = p2_word(['たたむのも', '練習中'], kicker="ハコブくん", kc="muted", size=170) # 動きのある場面(紙の図) MOTION.update({ "hook": dict(type='slam', at=(0, 0.3)), # 強 "hook_time": dict(type='slam', at=(0, 0.45)), # 強 "hook_answer": dict(type='slam', at=(0, 0.1)), # 強 "question": dict(type='slam', at=(0, 0.45)), # 強 "teaser": dict(type='slam', at=(0, 0.45)), # 強 "map2": dict(type='reveal', left_at=(0, 0.35), at=(0, 0.6)), # 強 "c1_sim": dict(type='count', counts=[('4096', 4096, (0, 0.3), (0, 0.8))]), # 中 "c2_joke": dict(type='slam', at=(0, 0.5)), # 強 "c3_figure": dict(type='count', counts=[('62', 62, (1, 0.3), (1, 0.75))]), # 中 "solve": dict(type='slam', at=(0, 0.5)), # 強 "solve_fold": dict(type='reveal', left_at=(0, 0.2), at=(0, 0.6)), # 強 "answer": dict(type='reveal', left_at=(1, 0.05), at=(1, 0.55)), # 強 }) use_motion(MOTION, VIDEO, O, _here, MOVE)