---
title: "考察記事を YouTube の掛け合い動画にした ― ハコブくんと作り手の壁打ちの型"
slug: deep-dive-article-to-dialogue-video
date: 2026-11-02
day: 427
meta_description: "Pixels to Motors の YouTube に、考察記事をもとに案内役のハコブくんと作り手が掛け合う考察回を作った。1本目は7分05秒・77文のうち本人が18文。紙の文字だけが動く書き出しの部品と、縦長ショート3本も作った。"
tags: [YouTube, 解説動画, 考察回, AI音声, ElevenLabs, Gemini, ドット絵, ハコブくん, ショート動画, 生成AI]
series: "ひとり研究所の裏側"
---

Pixels to Motors の YouTube に、考察記事をもとに案内役のロボット「ハコブくん」と作り手が掛け合う新しい型「考察回（壁打ちの型）」を作った。1本目「ロボットの頭脳は、脳のように分かれるのか｜大きなAIは大脳、専用AIは小脳」（7分05秒）は、77文のうち18文（55.2秒）を作り手本人の AI 音声が、59文をハコブくんが話し、画面の真ん中の「考察ノート」の紙の文字だけが、書く・数える・判子を押すように動く。

この型を作ったことで、良くなること・できるようになったことは、次の3つです。

- **答えではなく、考えている途中を見せる回ができた。** これまでの解説回は、ハコブくんが1人で答えを伝える形でした。考察回では、作り手が持ち込んだ話にハコブくんがまじめすぎる返事をし、作り手が本音でツッコむ。考えが動いた所だけを見せ、細かい数字はサイトの記事に任せます
- **次の回は、部品を写して場面を書き替えるだけで作れる。** 紙の動き・ハコブくん・作り手のドット絵・字幕を重ねる部品（kabe_kit.py・701行）と、縦長ショートの部品（kabe_short.py）を、この回で作りました。本編の書き出しは1回あたり約15分（12,761コマ・作業環境の CPU 2つ）。前の型と比べた手間は測っていないので、2本目を作るときの時間で確かめます
- **縦長ショートが、画面いっぱいの絵と大きく動くハコブくんから始まる。** 3本（24.0〜26.8秒）とも、最初の数秒は本編の「脱線」の絵（宇宙・紙の山・引退式）の前でハコブくんが飛び出し、そのあと紙の話に入ります。見る人が離れにくくなるかは見込みで、公開後の YouTube アナリティクスの「視聴を続けた割合」で確かめます

この型を作るきっかけは、考察記事 LOG424（ロボットの AI は脳のように分業する、という考察）を、いつもの解説回の形では動画にしにくかったことでした。考察記事は、作り手と Claude のやり取りで考えが動いていく記録です。ハコブくんが1人で答えを伝えると、その「動き」が消えてしまいます。そこで、Claude 役のハコブくんと作り手が壁打ちをする形を、台本のたたき・絵コンテ・40秒ほどの動きの試しから作りました。動画の中身（脳のたとえや数字）は LOG424 にあるので、この記事では、動画の作り方だけを書きます。

確認用の動画を見た作り手からは、何度か直しが入りました。いちばん大きかったのは、ショートの始まりへの一言です。

> あと、この始まりだとユーザーすぐ離脱しそう

| やったこと | なぜ | 結果 |
|---|---|---|
| 考察回の型（ボケ → ツッコミ兼本音 → へー）と画面の決まりを作った | 考察記事の「考えが動いた所」を見せるため | 左にハコブくん・右に作り手・真ん中に紙。朱は判子だけ |
| 台本を作り、別のエージェントに出典と照合させた | 数字と言い過ぎを確かめるため | 2版（直す5・整える12） |
| 話題の入りに、つなぎの5文と言い換え2文を足した | 作り手「唐突に話題がはじまる所がある」 | 3版・77文。6分46秒 → 7分05秒 |
| 声を ElevenLabs で作った | ナレーションは AI 音声の決まり | 72文を作り、3版では変わった7文だけ作り直した |
| 紙の文字を HTML で動かし、1コマずつ撮って重ねる部品を作った | 字幕は動かさず、紙だけを動かすため | kabe_kit.py。動きのないコマは撮らずに前のコマを使う |
| 脱線の絵4枚を Gemini で作った | 話が大きくなった所で、画面ごと替えるため | 宇宙・引退式・割れたコップ・紙の山 |
| ハコブくんが全身で動く所を、全身の絵で動かした | 上半身の絵を傾けると体が切れた（作り手が2回指摘） | 飛び出す2回・のけぞる1回・コケる1回 |
| サムネイルの脳を、つるっとした立体に替えた | 作り手「リアルな脳が気持ち悪く見える」 | 右下に大きく置き直した案に決まった |
| ショートを版2に作り直した | 作り手「この始まりだとすぐ離脱しそう」「おれが上半身で、ハコブくんが全身は変」 | 絵の前で大きなハコブくん・2人とも全身 |

下の図で、7分05秒のどこで、だれが話し、何が動いたかを見られます。帯を押すと、その時刻の文が出ます。

【図解】LOG427_kabe_timeline.html

【画像】LOG427_01_hook.png 考察回の冒頭。真ん中の紙に「0.05秒」「18.6秒」と作り手の見立て、左下にハコブくん、右下に作り手のドット絵、下に字幕

【画像】LOG427_02_retire_stamp.png 紙の上で、ハコブくんの「π0.5 は、引退」に朱の「ボツ」の判子と「まだ現役」の判子が押された場面。字幕は「勝手に引退させないで。」


## 作業ログ ― 型を決めてから、本編とショートを書き出すまで

ここから下は作業の記録です。効果の数字は、測れたものだけを「変わったこと」に書き、ほかは見込みです。視聴の数字は、YouTube に公開したあとにアナリティクスで確かめます。本編はこの記事を書いた時点では、まだアップロードしていません。

### 使った道具：ElevenLabs・Gemini・Playwright・ffmpeg

この回で使った道具と、この記事での呼び方です。

| 道具 | 何か | この回での使い方 |
|---|---|---|
| [ElevenLabs](https://elevenlabs.io) | 文章を読み上げる AI 音声のサービス | ハコブくんの声と、作り手の声（マンガの登場人物用に作った声を借りている）を作った。鍵は KIOXIA に置き、作り手の Mac の中でスクリプトが読むだけ |
| [Gemini の画像の API](https://ai.google.dev/gemini-api/docs/image-generation) | Google の、文章から画像を作る AI（この回は gemini-3.1-flash-image） | 脱線の絵4枚・作り手の全身のドット絵4枚・サムネイルの背景を作った |
| [Playwright](https://playwright.dev) | プログラムからブラウザを動かす道具 | 紙（考察ノート）の HTML を、時刻を止めて1コマずつ画像に撮った |
| [ffmpeg](https://ffmpeg.org) | 動画と音をまとめて扱う道具 | 撮ったコマを動画にし、声・BGM・効果音と重ねて mp4 にした |

「脱線」は、この記事では、話が大きくそれた所で、紙が飛んでいき画面いっぱいの絵に替わる場面のことです。「ボツ」は、ハコブくんの的外れな返事に押す朱の判子です。

### 考察回の型：解説回と分けて、ボケ・ツッコミ兼本音・へーの3拍にした

解説回（ハコブくんが1人で答えを伝える回）とは別の型にしました。決めたことは、動画づくりの決まりの文書 video_rules.md の10節に書いています。

- 構図は変えない。左にハコブくん（右向きの上半身）、右に作り手のドット絵（4倍の大きさ・声に合わせて1ドット弾む）、真ん中に紙（1060×780）
- 1回の掛け合いは「ボケ（ハコブくんがまじめすぎる）→ ツッコミ兼本音（作り手）→ へー（数字・脳の雑学）」の3拍
- 作り手のセリフは、LOG424 に残っている本人の言葉を話し言葉に整えたもの（11文）。短い相づち・ツッコミ（7文）は Claude の案で、本人が選んで了承した（「勝手に引退させないで」は、3版で作り手の指摘を受けて足した）
- 数字は1回のやり取りに1つまで。出どころは紙の下に小さく出し、読み上げない
- 動かすのは紙の上の文字だけ（書く・置く・数える・判子・ボツ）。字幕は今までどおり動かさない。線を使わず面で分け、事実は墨、作り手の見立ては濃い灰、ハコブくんは薄い灰。朱は判子とリアクションだけ
- くり返しの笑いを2つ置く。ハコブくんが考え込むと「考え中」の秒数が数え上がる（最後だけ即答する）。ハコブくんが詳しく話し始めると、作り手が「細かいのは、記事に書いといて」で止める

ねらい・変わったこと・見込める効果をまとめます。

- ねらい：考察記事の「考えが動いた所」を、答えを並べるニュースの形にせずに見せる
- 変わったこと：作り手の声が出る文は、解説回の LOG425 の回（11分10秒で4文・30.4秒）から、7分05秒で18文・55.2秒になった。ハコブくんのボケには、まとめで「ボツになった3つは、全部僕が言ったことです」と落ちを付けた
- 見込める効果：だれの考えかが声と絵で分かり、記事を読まない人にも考えの道すじが届く。届いたかは、公開後のコメントと視聴を続けた割合で確かめる

【画像】LOG427_03_think_count.png 「ハコブくん 考え中」の数字が紙の真ん中で数え上がっている場面。字幕は「では、なぜ組み合わせになるのか。……」

### 台本：別のエージェントの照合で2版、作り手の指摘で3版にした

台本は narration.py に1文ずつ書きます（場面の名前・ハコブくんの表情・字幕・後ろの間）。1版を書いたあと、書いたのとは別のエージェントに、出典の原文と1文ずつ突き合わせてもらいました。指摘は「直す5・整える12」で、たとえば、ある人物の投稿が別の人への返事であるかのように読める所を、別の投稿だと分かる言い方に直しました。これが2版（72文・6分46秒）です。

2版の確認用の動画を見た作り手の指摘は2つでした。

> 動画の全体の流れが（台本）唐突に話題がはじまるところがあるように感じるんだけど。見直してみてくれない？

> 引退のところおれのツッコミがあった方がいいね。

話題が変わる所を1つずつ見て、前の話とつなぐ一言を5文足し、2文の頭を言い換えました。引退の所には、作り手のツッコミ「勝手に引退させないで。」を足しました。3版は77文・7分05秒です。足した文と言い換えた文は、次のとおりです（diff の出力は、下の「使ったコード」の節に全文を載せています）。

| 場所 | 足した・言い換えた文 |
|---|---|
| 引退のボケのあと | 勝手に引退させないで。（作り手） |
| 別の人の大きな話に入る前 | ただ、頭脳の話を、もっと大きくする人もいます。 |
| 「もっと工夫が必要」の前 | つまり、大きなAIだけでは、ロボットは間に合いません。 |
| 脳の雑学の入り | しかも、人の小脳には、… → もう1つ、人の脳の話です。小脳には、… |
| 失敗の話の前 | では、その忙しい小脳は、どうやって育つのでしょう。 |
| 記録の話の入り | ところでエイイチは、… → 記録といえば、エイイチは、… |
| まとめの前 | ここまでを、1枚にまとめます。 |

文を足すと、文の番号が後ろへずれます。場面の動き（scenes_kabe.py）は2版の番号のまま書いてあったので、全部を書き替えず、足した文だけ名前で書く形にしました（`NEW = {"tsukkomi": 14, ...}` と、2版の番号をいまの番号に直す `ix()`）。決まりとして、video_rules.md の10節にも書いています。

- ねらい：話題が変わる所で、見ている人が置いていかれないようにする
- 変わったこと：19秒長くなった（6分46秒 → 7分05秒）。場面の動きの書き替えは、足した5文の名前を書くだけで済んだ
- 見込める効果：話の途中で離れる人が減る。公開後の視聴を続けた割合の曲線で、話題の変わり目に落ち込みがないかを見る

### 声：ElevenLabs で72文を作り、3版では変わった7文だけ作り直した

声は、作り手の Mac の中で ElevenLabs のスクリプト（共通の部品 tts_eleven.py）を動かして作りました。鍵は KIOXIA に置いてあり、スクリプトが読むだけです（画面にも記事にも出していません）。1回に作るのは18文までにして、2版の72文を分けて作りました。ハコブくんの声と、作り手の声（4コママンガの登場人物用に作った声を借りている）の2つを使い分けます。作り手の声で読むのは、場面の名前が `_ei` で終わる文です（narration.py の `EI_LINES`）。

3版で足した5文と言い換えた2文、計7文だけを作り直しました。ほかの70文は、文の中身が同じなら前の mp3 をそのまま引き継ぎます（同じ文をもう一度作ると、抑揚が変わることがあるため）。

読みは、ElevenLabs に渡す前に `ELEVEN_READ` で置き換えます。この回で足した読みは「0.05秒 → れいてんれいごびょう」「18.6秒 → じゅうはちてんろくびょう」「脊髄 → せきずい」「遅っ → おそっ」などです。

### 紙の文字：HTML で動かし、時刻を止めて1コマずつ撮る

紙（考察ノート）の上の文字の動きは、HTML の CSS アニメーションで書きました。書く（1文字ずつにじんで出る）・置く（少し大きく出てはねる）・数える（0から数え上がる）・判子（大きく落ちて画面が少し揺れる）・ボツ（判子が押されて枠が薄くなる）の5つです。

書き出すときは、ブラウザ（Playwright）でページを開き、`window.setT(t)` で時刻を止めてから1コマずつ撮ります。止めてから撮るので、何度書き出しても同じ絵になります。もう1つの工夫は、何も動いていないコマを撮らないことです。ページの側に `window.busy(t)` を置き、その時刻に動いている部品がなければ、前のコマをそのまま使います。冒頭のブロックでは、871コマのうち撮ったのは238コマでした。

撮ったコマは、ブロック（場面のまとまり）ごとに1本の動画にします。全部で25ブロックです。中身が変わっていないブロックは、前に作った動画を使い回します（`block_hash`）。

その上に、Python でハコブくん（口・目・緑の輪・全身の動き）、作り手のドット絵（声の大きさに合わせて1ドット弾む・8つの姿）、字幕を重ね、声・BGM・効果音と合わせて mp4 にします。BGM はいつもの曲を、考察回なので3dB小さくしました（話している間 -26dB・間 -19dB）。

- ねらい：字幕は動かさず、紙の文字だけを動かして、考えが書き込まれていく感じを出す
- 変わったこと：本編1本の書き出しが約15分（12,761コマ・作業環境の CPU 2つ）。中身が変わらないブロックは撮り直さない
- 見込める効果：台本を直したとき、変わったブロックだけ撮り直せば済む。2本目を作るときに、書き出しの時間を記録して確かめる

【画像】LOG427_06_summary_board.png まとめの場面。紙に「大脳・小脳・反射」の3行と「速い手を育てるのは、失敗を手直しした記録。」の一文

### 脱線の絵：Gemini で4枚を作り、文字は動画の側で重ねた

話が大きくそれた所（4か所）では、紙が飛んでいき、画面いっぱいの絵に替わって、数秒で戻ります。絵は Gemini の画像の API で作りました。決まりは2つです。絵の中に文字を描かせず、一言は動画の側で重ねること。絵にハコブくんが描いてあるときは、左下のハコブくんを消すこと（引退式の絵）。

| 絵 | 出る時刻 | 作るのにかかった時間 | 参考に渡した画像 |
|---|---|---|---|
| 引退式（マンガ風） | 1:30〜1:34 | 20.0秒 | ハコブくんの正面・横 |
| 宇宙のデータセンター | 1:50〜1:58 | 16.3秒（2回作った2枚目 15.7秒を使った） | なし |
| 割れたコップ | 2:26〜2:32 | 16.4秒 | なし |
| 紙の山 | 5:30〜5:33 | 15.5秒 | なし |

かかった時間は、画像を作るスクリプト（gemini_bg.py）が残した記録の値です。宇宙の絵は2回作り、2回目の絵を使いました。いまのプロンプトには「1枚の続いた写真・帯や継ぎ目なし」の指定が入っています（2回目のほうが、渡した文の量が137から159に増えている）。

【画像】LOG427_04_dassen_space.png 脱線の場面。宇宙に浮かぶ輪の形のデータセンターの絵の上に、大きく「超知能の ほぼ100%は 深宇宙で計算。」。左下にハコブくんが全身で飛び出している

### ハコブくんの体：上半身の絵を傾けると切れたので、全身の絵で動かした

ハコブくんは、ふだんは左端の上半身で、口と目と緑の輪だけが動きます。1本に4回だけ、体ごと動かします（飛び出す2回・のけぞる1回・コケる1回）。最初は上半身の絵をそのまま回して倒していましたが、確認用の動画で、作り手に2回指摘されました。

> 転けているハコブくんの体が切れてるので、全身入れるようにしてください。

> またハコブくんが切れちゃってるよ！3分50あたり

上半身の絵は画面の下で切ってあるので、傾けると切り口が見えます。コケる所は、全身の絵を足もとの角を軸にして倒す形に変え、回したときに絵の端が切れないよう、回す前に周りに足す余白を420から760ピクセルに広げました。3分50秒あたりの「のけぞる」も、全身の絵を上の位置に置いて動かす形にしました。決まりとして「体ごとの動きは、どれも全身の絵で動かす」を video_rules.md に書きました。

【画像】LOG427_05_fall_fullbody.png ハコブくんが全身で前のめりにコケた場面。左に「ドテッ」の文字、右下で作り手のドット絵が吹き出して笑っている

### サムネイル：リアルな脳は気持ち悪く見えたので、つるっとした立体にした

タイトルは、作り手が「もう少し具体的なことを書いて分かりやすく伝えたい」として、次の形に決めました。

> ロボットの頭脳は、脳のように分かれるのか｜大きなAIは大脳、専用AIは小脳

サムネイルは、文字の組み方が違う A 案・B 案を作り、背景を Gemini で作りました。最初の背景は写真のような脳の模型でしたが、作り手の一言で作り直しました。

> サムネイル脳の画像が気持ち悪く見えるな。リアルっぽいのが原因かも

「なめらかなつや消しの素材・解剖の細かさなし」と指定して、立体（光）と平面の2つを作り、立体を選びました。そのあと「脳とロボットをもっと右下に寄せる」「もっと大きくしていい」で、脳とロボットアームを背景から切り抜いて右下に置き直し、1.16倍に大きくした案に決まりました。背景を作るのにかかった時間は、1枚15.8〜19.7秒です。

【画像】LOG427_09_thumbnail_before_after.png サムネイルの最初の案（左・写真のような脳の模型）と決めた案（右・つるっとした立体の大脳と小脳を右下に大きく）

### 縦長ショート：版2で、絵の前の大きなハコブくんから始め、2人とも全身にした

本編が合格したあと、縦長ショート（1080×1920）を3本作りました（kabe_short.py）。版1は、本編と同じく紙から始まり、ハコブくんは全身・作り手は上半身でした。作り手の指摘は3つでした。

> 俺が上半身だけで、ハコブくんが全身は変だね

> あと、この始まりだとユーザーすぐ離脱しそう

> 宇宙の背景、本の山、引退のイラスト背景を上手く使って、ハコブくんが大きく動くところから始まった方がいんじゃない？ あと必要なら俺の全身イラストが必要（服装は写真参照）

版2で変えたことです。

- 最初の数秒は、本編の脱線の絵を画面いっぱいに出し、大きなハコブくん（高さ900ピクセル）が飛び出す・跳ねる所から始める。本題に入ったら絵を暗くして紙を出し、ハコブくんは左下の全身に戻る
- 作り手の全身のドット絵を、作り手の服装の写真と上半身のドット絵を Gemini に渡して4枚作った（1枚17.0〜21.7秒）。プロンプトは4つ（立つ・ツッコミの手つき・驚く・笑う）で、ツッコミの手つきを頼んだ2枚目は腕組みで出てきたので、腕組みとして使った。作った絵を高さ170ピクセルに縮め、64色に減らしてドット絵にした
- 大きさは身長に合わせた。作り手は「ハコブくん1150mmで俺は1700mmだよ！」「デフォルメしているからピッタリ合わせる必要はないけど、大まかなイメージが伝わるようにしてね！」。作り手 765ピクセル（170ピクセルの4.5倍）、ハコブくん 515ピクセル（765 × 1150/1700）、足もとは画面の下から40ピクセル
- 服は「上下セットアップなので濃いダークグレー」。ズボンを上着と同じ濃い灰に塗り直し（v2）、インナーは黒にそろえた（上着の裾より下だけを塗り直した）
- 2本目を「ドテッ」から「捨てられない人」（紙の山の絵から始まる）に替えた
- 細かい直し：作り手の絵を右端に寄せてはみ出さないように／引退式の絵の帯の中に「祝・引退」が入る位置に絵をずらす／生成AIの注記を、キャラと重ならない見出しの下の右へ／宇宙の絵の大きな言葉の前に「イーロン・マスクさんの X の投稿」の札を出す

字幕のセンタリングでは、作り手にこう聞かれました。

> ハコブくんのセリフの字幕のセンタリングがされてないよ（なぜセンタリングが簡単にできないのか考えてみて）

原因は、Claude が、右側に並ぶ YouTube のボタンの列を避けるつもりで、字幕と紙の中心を x=470 に寄せていたことでした。そのことを先に伝えていませんでした。画面の真ん中（x=540）に戻しました。あわせて、字幕の行の割れ方がおかしかった所を、1行11字までにして直しました。

| ショート | 始まりの絵 | 版1 | 版2 |
|---|---|---|---|
| 1 遅っ | 宇宙 | 25.9秒 | 24.3秒 |
| 2 ドテッ → 捨てられない人 | 紙の山 | 25.4秒 | 24.0秒 |
| 3 勝手に引退させないで | 引退式 | 27.4秒 | 26.8秒 |

- ねらい：最初の1〜2秒で、動くもの・大きい絵で手を止めてもらう
- 変わったこと：3本とも、絵の前の大きなハコブくんから始まる。2人とも全身で、身長の比が見える
- 見込める効果：ショートで最後まで見る人が増える。公開後、版2の3本の「視聴を続けた割合」を、これまでのショートと並べて確かめる

【画像】LOG427_08_shorts.png 縦長ショート版2の3本の始まり（宇宙・紙の山・引退式の絵の前の大きなハコブくん）と、本題に入ったあとの画面（左下にハコブくん、右下に全身の作り手、真ん中に紙）

【画像】LOG427_07_eiichi_fullbody.png 作り手の全身のドット絵4つ（立つ・腕組み・驚く・笑う）。濃い灰のセットアップに黒のインナー

### 保存した場所と、本編の指紋

| もの | 場所 |
|---|---|
| 作り方のファイル（台本・narration.py・scenes_kabe.py・kabe_kit.py・kabe_short.py・sound.py・サムネイル・概要欄の下書き） | iCloud `PixelstoMotors/デジタルツイン学習R2R2R/動画/頭脳は脳のように分かれる/` |
| 型の台本のたたき・絵コンテ・動きの試し・脱線の絵・作り手の全身のドット絵 | iCloud `動画/_ネタ帳/考察回_壁打ちの型/` |
| 本編・サムネイル・タイトル・概要欄 | KIOXIA `r2r2r-data/videos/頭脳は脳のように分かれる/アップロード用/` |
| ショート3本（版2）とタイトル・説明文 | KIOXIA `r2r2r-data/videos/頭脳は脳のように分かれる/ショート/`（版1は `_旧版_版1/`） |
| この回で決めたこと | `動画/_制作ベース/video_rules.md` の10節と101回目の更新履歴 |

本編は、作業環境で tar.gz にまとめて18MBずつに分けて Mac へ送り、つないで指紋（md5）が同じことを確かめました。最初は mp4 をそのまま分けて送りましたが、送る前の出力のフォルダで、分けたファイルの1つの中身が書き換わり、指紋が合いませんでした。

```text
d9fd497c0f163a0477f293c8c56d2194  out.mp4（作業環境・425.4秒・45,264,197バイト）
d9fd497c0f163a0477f293c8c56d2194  KIOXIA r2r2r-data/videos/頭脳は脳のように分かれる/アップロード用/頭脳は脳のように分かれる.mp4
```


## 作業ログ ― この回で使ったコード・プロンプト・出力（全文）

作業フォルダ（iCloud の `動画/頭脳は脳のように分かれる/`）と型のネタ帳（`動画/_ネタ帳/考察回_壁打ちの型/`）にあるファイルの、書き出しに使った版の全文です。共通の部品（`_制作ベース/` の hakobu.py・timing.py・subtitle.py・audio_mix.py・short_kit.py・tts_eleven.py など）は、前の回の記事に載せたものから、この回では変えていません。作り手の全身のドット絵を縮めて色を減らした作業と、ズボンの塗り直しは、その場のコマンドで行い、スクリプトとしては残していません。

### narration.py（台本の1文ずつ（場面・表情・字幕・後ろの間）と、読みの直し・作り手の声で読む文）

```python
# -*- coding: utf-8 -*-
# 台本「ロボットの頭脳は、脳のように分かれるのか」— 考察回「壁打ちの型」の1本目（もとは LOG424）
#   2026-11-02 1版。型と絵の決まり: 動画/_ネタ帳/考察回_壁打ちの型/（台本たたき・絵コンテ・動きの試し）
#   2026-11-02 3版: エイイチの指摘「話題が唐突に始まる所がある」「引退にツッコミを」で、つなぎの5文を足し2文を言い換えた（引退のツッコミ・マスクの前・工夫の前・小脳の4倍・失敗の前・記録といえば・まとめの前）
#   2026-11-02 2版: 別のエージェントの照合（直す5・整える12）で直した（マスクは Freda への返事ではない・18.6秒・見立ての言い方・反射・海馬・概要欄の出どころ）
#   書き写した元: 動画/ハコブくんの声と絵/narration.py（形・読みの直し・自己紹介とアウトロの定型・EI_LINES）
#   エイイチのセリフ: LOG424 の本人の言葉を話し言葉に整えたもの。相づち（短い反応）は Claude の案で、エイイチが「これで先に進めて」と了承（2026-11-02）
#     本人: hook_ei・retire_ei・ei_more・faster_ei・brain_ei・fail_ei（2つ）・web_ei・log_ei（2つ）・hippo_ei
#     相づち: musk_ei（話がでかい）・think_ei（遅っ）・detail_ei（細かいのは記事に）・reflex_ei・cerebellum_ei・end_ei
#   出典（台本の中の事実）:
#     - 0.05秒・18秒・11〜19秒・使い方が3つ: NVIDIA「Astranomy」（2026-10-14）の π0.5 46.96ミリ秒・Direct 18.56秒／Agentic 13.37秒／Orchestrated 11.50秒
#     - 370回ほど: 18.6秒 ÷ 0.05秒＝372（画面の数え上げは 18.6 で止める。待ちの実時間は短いので「大きなAIなら、ここで」と言う）
#     - ブロックを器に入れた: Robonomics「Robotics' New Era After Astra」（RoboCurve の実験・20回中19回）
#     - 深宇宙: Elon Musk の X の投稿（2026-10-27 日本時間）の "~100% of SI will be deep space compute. Obviously." を訳した
#     - 画面写真7枚: この会話の最初に届いた画面写真の枚数（時刻は言わない・エイイチの指示 2026-11-02）
#     - 小脳の神経細胞が大脳の表面（大脳皮質）の約4倍: Herculano-Houzel（MIRI のインタビュー「about 4 neurons in the cerebellum to every neuron in the cerebral cortex」）
#     - 424番: LOG424（この考察回のもとの記事）
# 話すのはハコブくん。エイイチの文は EI_LINES（槙原の声）
#
# 各要素: (場面の名前, 表情, 字幕の文, 後ろの間[秒])  間を省略すると 0.35 秒
FACES = ("base", "smile", "surprise", "doubt", "think", "trouble", "joy", "wink")

SEGS = [
 # 0 つかみ（問いの縦書き）
 ("hook_num", "base", "0.05秒と、18.6秒。"),
 ("hook_num", "base", "どちらも、ロボットを動かすAIが、1回考えるのにかかる時間です。"),
 ("hook_num", "base", "速いほうは、ロボット専用のAI。遅いほうは、OpenAI の、何にでも使える大きなAIです。", 0.8),
 ("hook_ei", "base", "LLMは反応が遅いんだよ。テキストで返すならいいけど、動くものには、そのままじゃ使えない。", 0.6),
 ("hook_ei", "trouble", "……すみません。僕の頭も、そっち側です。", 1.2),
 ("hook_q", "base", "今日は、答えを伝える回ではありません。エイイチが考えている途中を、そのまま見せます。", 1.8),
 # 1 自己紹介
 ("title", "smile", "Pixels to Motors。案内役のハコブくんです。"),
 ("title", "base", "この番組では、フィジカルAIのニュースや実機の記録から、現場で使える形を考えます。", 1.2),
 ("title_eiichi", "base", "エイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。｜その中で分かったことと、そこから考えた見立てを、ハコブくんがお伝えします。", 1.2),
 ("title_kabe", "smile", "今回は、エイイチの壁打ちに、ハコブくんが付き合います。", 1.8),
 # 2 持ち込み
 ("bring", "base", "夜、エイイチから、画面写真が7枚届きました。"),
 ("bring", "base", "海外の投資家の書き込みでした。一言でいうと、ロボット専用の大きな頭脳は、いらなくなるかもしれない、という話です。"),
 ("bring", "surprise", "何にでも使える大きなAIが、ロボット用の追加の練習なしで、ブロックを器に入れられたそうです。", 1.0),
 ("retire", "joy", "ということは、エイイチの研究室の π0.5 は……引退ですね。お疲れさまでした。", 1.2),
 ("retire_ei", "surprise", "勝手に引退させないで。", 0.5),
 ("retire_ei", "base", "汎用モデルも、使い所次第。組み合わせになると思う。", 0.8),
 ("musk", "base", "ただ、頭脳の話を、もっと大きくする人もいます。", 0.5),
 ("musk", "base", "この話の少し前に、イーロン・マスクさんも、別の投稿でこう言っていました。"),
 ("musk", "surprise", "超知能の、ほぼ100%は、深宇宙で計算される。明らかに。", 1.0),
 ("musk_ei", "base", "話がでかい。", 0.6),
 ("musk", "smile", "宇宙は、今日は置いておきます。", 1.8),
 # 3 やり取り1：遅い頭（考え中の数え上げ）
 ("think", "think", "では、なぜ組み合わせになるのか。……", 4.0),
 ("think", "smile", "お待たせしました。", 0.5),
 ("think_ei", "base", "遅っ。", 0.6),
 ("think", "trouble", "大きなAIなら、ここで18.6秒かかります。ロボットを動かすとき、1回考えるのに、だいたいこのくらいです。", 0.8),
 ("cup", "base", "その間に π0.5 は、動きを370回ほど直せます。"),
 ("cup", "trouble", "コップが手から滑り始めても、僕が考え終わるころには、床で割れています。", 1.0),
 ("detail", "base", "ちなみにこの時間は、使い方が3つあって、それによって11秒から19秒まで幅があって……"),
 ("detail_ei", "base", "細かいのは、記事に書いといて。", 0.5),
 ("detail", "wink", "もう書いてあります。", 1.2),
 ("more", "base", "つまり、大きなAIだけでは、ロボットは間に合いません。", 0.5),
 ("more_ei", "base", "もっと工夫が必要だよね。", 0.6),
 ("faster", "joy", "大丈夫です。AIは毎年賢くなっているので、そのうち速くなります。"),
 ("faster_ei", "base", "LLMの進化で、反応の速さは上がらないんじゃないかな。", 0.6),
 ("faster", "doubt", "考えてみました。……半分、おっしゃる通りだと思います。", 0.8),
 ("faster", "base", "大きなAIは、賢くなるほど体が大きくなって、答える前に考える時間も、長くなる方向に進んでいる、と僕は見ています。速くなるのは、主に小さくした版のほうです。", 1.8),
 # 4 山場：脳のたとえ（転換点）
 ("split", "base", "だから、分けるしかありません。遅く考える頭と、速く直す手。", 0.8),
 ("brain_ei", "base", "人も、小脳と大脳で役割が分かれてるし、それと同じことなんじゃないかな。", 1.5),
 ("brain", "joy", "……それです。", 1.0),
 ("brain", "base", "大脳は、考えて決める所。小脳は、練習で覚えた動きを合わせる所。"),
 ("brain", "base", "そして脊髄の反射は、脳を通らずに、体を動かします。", 0.8),
 ("reflex", "smile", "ちょっと実演します。", 1.2),
 ("reflex", "surprise", "今のは、考える前に動きました。これが反射です。", 0.6),
 ("reflex_ei", "base", "ロボットにも、反射ってあるの？", 0.5),
 ("reflex", "smile", "あります。ぶつかりそうになったら止まる仕組みです。", 1.0),
 ("cerebellum", "surprise", "もう1つ、人の脳の話です。小脳には、考える大脳の表面の、4倍ほどの神経細胞があるそうです。"),
 ("cerebellum", "base", "動きを合わせる所に、こんなに多くの神経細胞が使われているんです。", 0.6),
 ("cerebellum_ei", "base", "頭より、体のほうが忙しいんだ。", 0.5),
 ("cerebellum", "wink", "はい。僕も、運ぶほうが忙しいです。", 1.8),
 # 5 やり取り2：失敗の手直し
 ("fail_q", "think", "では、その忙しい小脳は、どうやって育つのでしょう。", 0.6),
 ("fail_ei", "base", "失敗を手直しするデータの積み重ねって、思ってる以上に大事なんじゃないか。", 0.8),
 ("fail", "base", "人は自転車で、転びかけて持ち直すのをくり返して、考えずに乗れるようになります。", 0.6),
 ("fail", "joy", "では僕も、小脳を鍛えるために、今夜から毎晩転びます。", 1.2),
 ("fail_ei2", "base", "失敗だけ覚えちゃって、良くないね。", 0.6),
 ("fail", "trouble", "……それは困ります。", 1.0),
 ("three", "base", "なので、記録を3つの時点で切ります。失敗した瞬間、手直しを始めた瞬間、戻った瞬間です。"),
 ("three", "base", "失敗の区間は、学ばせない。手直しの区間を、重く学ばせる。何度も失敗する場所には、きれいなお手本を足す。", 0.8),
 ("web", "think", "インターネットの動画には、持ち直す場面が、ほとんど写っていません。何にでも使える大きなAIに、いちばん足りないのは、ここだと思います。", 0.6),
 ("web_ei", "base", "これ、今おれがやってるね。", 1.8),
 # 6 やり取り3：記憶の整理
 ("log", "doubt", "記録といえば、エイイチは、記事を全部、全文で残しています。番号は、424番まで来ています。……捨てられない人ですか？", 0.6),
 ("log_ei", "base", "LOGは、全部の積み重ね。基幹情報なんだよ。", 0.4),
 ("log_ei", "base", "情報がたくさんあるだけじゃ、言葉や動きは良くならない。", 0.8),
 ("hippo", "surprise", "脳も同じです。その日の出来事は、いったん海馬に残って、眠っている間に、大事なものが、大脳の表面へ移っていきます。"),
 ("hippo", "base", "記事の全文が海馬。整理した記事が、大脳です。", 0.6),
 ("hippo_ei", "base", "残す基準は、まだまだこれからだね。", 0.5),
 ("hippo", "smile", "では僕は、残した理由を、一言ずつ書いておきます。", 1.8),
 # 7 まとめ（ボードが表になる）
 ("sum", "smile", "ここまでを、1枚にまとめます。", 0.5),
 ("sum", "base", "遅く考える頭と、速く直す手。速い手を育てるのは、失敗を手直しした記録です。", 0.8),
 ("sum", "wink", "ちなみに、ボツになった3つは、全部僕が言ったことです。", 1.8),
 # 8 残った問い
 ("rest", "base", "答えは、まだ出ていません。手直しの区間を分けて学ばせたら、戻れる率はどれだけ変わるか。続きは、机の上の実験で確かめます。"),
 ("rest", "base", "このノートの全文は、サイトの記事にあります。数字の出どころは、概要欄に置いてあります。", 0.8),
 ("ask", "base", "あなたの現場では、失敗の記録は、どこに残っていますか。", 1.0),
 ("end_ei", "base", "今の返事、速かったね。", 0.5),
 ("end", "joy", "小脳で答えました。", 1.8),
 # 9 アウトロ
 ("outro", "smile", "この動画が役に立ったら、高評価とチャンネル登録をお願いします。"),
 ("outro", "base", "ロボットの開発や導入の相談は、概要欄の連絡先から、エイイチに直接どうぞ。"),
 ("outro", "wink", "コメントは、エイイチが全部読んでいます。それではまた。", 1.8),
]

# エイイチの声で読む文（槙原の声を流用・video_rules.md 1-4）。場面の名前が _ei で終わるか、_ei2
EI_LINES = [i for i, seg in enumerate(SEGS) if seg[0].endswith(("_ei", "_ei2")) and not seg[2].startswith("……すみません")]

# 「エイイチ」を残す文（自己紹介・アウトロのほかに名前が要る文）。文の頭で探す（video_rules.md 3-1）
NAME_OK = [i + 1 for i, seg in enumerate(SEGS) if seg[2].startswith((
    "今日は、答えを伝える回では", "今回は、エイイチの壁打ちに", "夜、エイイチから",
    "ということは、エイイチの研究室", "記録といえば、エイイチは"))]

READ = [
 ("Pixels to Motors", "ピクセルズ トゥー モーターズ"), ("OpenAI", "オープンエーアイ"), ("LLM", "エルエルエム"), ("LOG", "ログ"),
 ("π0.5", "パイレイテンゴ"), ("フィジカルAI", "フィジカルエーアイ"), ("AI", "エーアイ"), ("100%", "ひゃくパーセント"),
 ("1回", "いっかい"), ("3つ", "みっつ"), ("4倍", "よんばい"),
 ("栄一", "えいいち"), ("「", ""), ("」", ""), ("｜", ""), ("…", ""),
]

ELEVEN_READ = [
    ('エイイチ', '栄一'),   # video_rules.md 3-1
    ('OpenAI', 'オープンエーアイ'),
    ('π0.5', 'パイレイテンゴ'),   # KDDIの300億円 と同じ
    ('LLM', 'エルエルエム'),
    ('LOG', 'ログ'),
    ('物差し', 'ものさし'), ('人型', 'ひとがた'),
    # この回で先回りして足す読み（全文を聞いて確かめる）
    ('0.05秒', 'れいてんれいごびょう'),
    ('18.6秒', 'じゅうはちてんろくびょう'),
    ('424番', 'よんひゃくにじゅうよんばん'),
    ('脊髄', 'せきずい'),
    ('小脳', 'しょうのう'), ('大脳', 'だいのう'),
    ('基幹情報', 'きかんじょうほう'),
    ('使い所', 'つかいどころ'),
    ('遅っ', 'おそっ'),
]

OVERRIDE = {}

def reading(t):
    t = OVERRIDE.get(t, t)
    for a, b in READ:
        t = t.replace(a, b)
    return t


def parse(seg):
    """1行を (場面, 表情, 字幕, 間) にそろえる"""
    if len(seg) >= 3 and seg[1] in FACES:
        scene, face, text = seg[0], seg[1], seg[2]
        pause = seg[3] if len(seg) > 3 else 0.35
    else:
        scene, face, text = seg[0], "base", seg[1]
        pause = seg[2] if len(seg) > 2 else 0.35
    return scene, face, text, pause
```

### 台本の2版から3版への差分（diff narration_2版.py narration.py の出力）

```text
3a4
> #   2026-11-02 3版: エイイチの指摘「話題が唐突に始まる所がある」「引退にツッコミを」で、つなぎの5文を足し2文を言い換えた（引退のツッコミ・マスクの前・工夫の前・小脳の4倍・失敗の前・記録といえば・まとめの前）
39a41
>  ("retire_ei", "surprise", "勝手に引退させないで。", 0.5),
40a43
>  ("musk", "base", "ただ、頭脳の話を、もっと大きくする人もいます。", 0.5),
54a58
>  ("more", "base", "つまり、大きなAIだけでは、ロボットは間に合いません。", 0.5),
70c74
<  ("cerebellum", "surprise", "しかも、人の小脳には、考える大脳の表面の、4倍ほどの神経細胞があるそうです。"),
---
>  ("cerebellum", "surprise", "もう1つ、人の脳の話です。小脳には、考える大脳の表面の、4倍ほどの神経細胞があるそうです。"),
74a79
>  ("fail_q", "think", "では、その忙しい小脳は、どうやって育つのでしょう。", 0.6),
85c90
<  ("log", "doubt", "ところでエイイチは、記事を全部、全文で残しています。番号は、424番まで来ています。……捨てられない人ですか？", 0.6),
---
>  ("log", "doubt", "記録といえば、エイイチは、記事を全部、全文で残しています。番号は、424番まで来ています。……捨てられない人ですか？", 0.6),
92a98
>  ("sum", "smile", "ここまでを、1枚にまとめます。", 0.5),
113c119
<     "ということは、エイイチの研究室", "ところでエイイチは"))]
---
>     "ということは、エイイチの研究室", "記録といえば、エイイチは"))]
```

### scenes_kabe.py（紙の場面・脱線・ハコブくんの表情と全身の動き・作り手の姿・効果音）

```python
# -*- coding: utf-8 -*-
# 動画「ロボットの頭脳は、脳のように分かれるのか」— 考察回「壁打ちの型」の1本目（もとは LOG424）の場面
# 型の決まり: 動画/_ネタ帳/考察回_壁打ちの型/（台本たたき・絵コンテ・動きの試し）
#   紙（考察ノート）の文字は真ん中にそろえる／線を使わず面で分ける（事実＝墨・エイイチの見立て＝濃い灰・ハコブくん＝薄い灰）
#   朱は判子とリアクション（遅っ・ボツ・この見立ては、エイイチから）だけ／字幕は今まで通り（出る・消えるだけ）
#   脱線（画面いっぱいの絵）は4回: 引退式（マンガ）・宇宙・コップ・紙の山（どれも Gemini で作った絵）
import os
from kabe_kit import *
import scene_kit as _sk

_tl = Timeline()
# 3版（2026-11-02）で足したつなぎの文。2版の番号 p の文の前に入れた。場面の中では、2版の番号か、この名前で時刻を書く
NEW = {"tsukkomi": 14, "musk_b": 15, "more_b": 28, "fail_b": 46, "sum_b": 62}
_INS = sorted(NEW.values())


def ix(i):
    """2版の番号（または NEW の名前）→ いまの番号"""
    if isinstance(i, str):
        return ix(NEW[i]) - 1
    return i + sum(1 for p in _INS if p <= i)


class _TLx:
    idx = _tl.idx
    start = staticmethod(lambda i: _tl.start(ix(i)))
    speech = staticmethod(lambda i: _tl.speech(ix(i)))
    end = staticmethod(lambda i: _tl.end(ix(i)))


tl = _TLx
N = len(_tl.idx)
A = lambda i, off=0.0: tl.start(i) + off          # 動画の頭からの秒
S = tl.speech                                      # その文を読み終わるまでの長さ
LOGO = _sk.LOGO
BRAND = f'<img src="{LOGO}" style="position:absolute;right:56px;top:40px;height:30px;filter:invert(1);opacity:.55">'
SANS = "font-family:'Noto Sans CJK JP',sans-serif;"
BG = "src/bg/"

BLOCKS = []


def block(name, sents, fn, video=None):
    sents = sorted([ix(o) for o in sents] + [ix(k) for k, p in NEW.items() if p in sents])   # いまの番号にする（足した文は、次の文の場面に入れる）
    b0 = _tl.start(sents[0])
    T = lambda i, off=0.0: tl.start(i) - b0 + off
    E = lambda i: tl.end(i) - b0
    b = dict(name=name, sents=sents)
    if video:
        b["video"] = video
    else:
        b["html"] = page(fn(T, E) + BRAND)
    BLOCKS.append(b)


def line(text, t, size=30, weight=700, step=0.05, style=""):
    return f'<div style="{SANS}font-size:{size}px;font-weight:{weight};{style}">{ink(text, t, step)}</div>'


def qline(text, t, size=46, step=0.06, style=""):
    return f'<div class="serif" style="font-size:{size}px;font-weight:700;line-height:1.5;{style}">{ink(text, t, step)}</div>'


# ================= 0 つかみ =================
def hook(T, E):
    num = lambda v, t: (f'<div class="card k-fact" style="width:400px;opacity:0;animation:pop .45s ease-out {t:.3f}s forwards">'
                        f'<span class="num" style="font-size:110px;line-height:1.2">{v}</span><span style="font-size:44px;font-weight:700"> 秒</span></div>')
    col = lambda v, t, lab, tl_: (f'<div style="display:flex;flex-direction:column;align-items:center;gap:12px">{num(v, t)}'
                                  f'<div style="{SANS}font-size:28px;font-weight:700;height:42px">{ink(lab, tl_)}</div></div>')
    return paper(tag("考察ノート") + '<div class="stack" style="top:100px">'
                 + f'<div class="row2">{col("0.05", T(0, 0.15), "ロボット専用のAI", T(2, 0.3))}{col("18.6", T(0, 1.35), "何にでも使える大きなAI", T(2, 3.7))}</div>'
                 + line("ロボットを動かすAIが、1回考えるのにかかる時間", T(1, 0.5), size=30, weight=500)
                 + card("mi", ink("LLMは反応が遅い。動くものには、そのままでは使えない", T(3, 0.5), 0.04), T(3, 0.2), "見立て ・ エイイチ", w=820, anim="wipe")
                 + card("hb", ink("僕の頭も、そっち側です", T(4, 1.3), 0.05), T(4, 1.2), "ハコブくん", w=520, style="font-size:30px")
                 + '</div>', 0.0)
block("hook", [0, 1, 2, 3, 4], hook)


def hook_q(T, E):
    return paper(tag("考察回　壁打ち") +
                 '<div class="center"><p class="v serif" style="font-size:54px;line-height:1.6;font-weight:300;letter-spacing:.06em;">'
                 f'<span data-ink="{T(5, 0.3):.2f},0.09" style="font-size:78px;font-weight:700;">ロボットの</span><br>'
                 f'<span data-ink="{T(5, 0.8):.2f},0.09">頭脳は、</span><br>'
                 f'<span data-ink="{T(5, 1.2):.2f},0.09">脳のように</span><br>'
                 f'{land("分かれる", T(5, 2.0), "font-size:104px;font-weight:900")}<span data-ink="{T(5, 2.6):.2f},0.09">のか。</span></p></div>', 0.0)
block("hook_q", [5], hook_q)

# ================= 1 自己紹介（DAY124 の映像とテロップ） =================
O_TITLE = '<div class="lt"><img class="a" src="' + LOGO + '"><div class="b">フィジカルAI解説</div><div class="c">案内役：ハコブくん（AI音声）</div></div>'
O_EIICHI = '<div class="lt"><img class="a" src="' + LOGO + '"><div class="b">エイイチ</div><div class="c">デザイナー／ロボティクスの研究・開発</div></div>'
O_OUTRO = '<div class="lt"><img class="a" src="' + LOGO + '"><div class="b">相談は概要欄の連絡先から</div><div class="c">高評価・チャンネル登録／コメントはエイイチが全部読んでいます</div></div>'
OVERLAYS = {"title": _sk.overlay(O_TITLE), "title_eiichi": _sk.overlay(O_EIICHI), "outro": _sk.overlay(O_OUTRO)}
_t_title = tl.end(7) - tl.start(6)
block("title", [6, 7], None, video=("src/day124_crop.mp4", 4.5, "png/ovl/title.png"))
block("title_eiichi", [8], None, video=("src/day124_crop.mp4", 4.5 + _t_title, "png/ovl/title_eiichi.png"))


def title_kabe(T, E):
    return paper(tag("考察回") + '<div class="center" style="gap:30px">'
                 + f'<div class="serif" style="font-size:150px;font-weight:900;line-height:1.1">{land("壁打ち", T(9, 0.5))}</div>'
                 + line("エイイチの考えに、ハコブくんが付き合う", T(9, 1.3), size=34, weight=500)
                 + '</div>', 0.0)
block("title_kabe", [9], title_kabe)

# ================= 2 持ち込み =================
def bring(T, E):
    fly = (T(13, 3.2), E(13) - 0.15)
    return (dassen(BG + "引退式.jpg", fly[0] + 0.05, fly[1],
                   words="祝・引退", word_style="top:212px;left:820px;right:auto;width:660px;transform:rotate(-1deg);font-size:78px;color:#141311;-webkit-text-stroke:0;text-shadow:none",
                   src_note="絵は生成AI（Gemini）で作成")
            + paper(tag("持ち込み") + '<div class="stack" style="top:88px;gap:20px">'
                    + line("夜、エイイチから　画面写真が7枚", T(10, 0.3), size=28, weight=500)
                    + qline("ロボット専用の大きな頭脳は、いらなくなる？", T(11, 3.0))
                    + card("fact", ink("何にでも使える大きなAIが、追加の練習なしで、ブロックを器に入れた", T(12, 0.5), 0.035), T(12, 0.2), "海外の投資家の書き込み", w=860)
                    + card("hb", "π0.5 は、引退", T(13, 0.6), "ハコブくん", w=420, botsu=T("tsukkomi", 0.7), fade=T("tsukkomi", 0.9), style="font-size:30px")
                    + card("mi", ink("汎用モデルも、使い所次第。組み合わせになる", T(14, 0.5), 0.045), T(14, 0.2), "見立て ・ エイイチ", w=820, anim="wipe")
                    + '</div>' + f'<div class="src" style="opacity:0;animation:show .01s linear {T(11, 0.3):.3f}s forwards">出どころ：Robonomics「Robotics\' New Era After Astra」</div>',
                    0.0, flights=[fly])
            + stamp("まだ現役", T("tsukkomi", 0.1), "right:380px;top:470px;font-size:44px")
            + shake_js([(T("tsukkomi", 0.42), 0.25)]))
block("bring", [10, 11, 12, 13, 14], bring)


def musk(T, E):
    fly = (T(16, -0.15), T(18, 0.0))
    return (dassen(BG + "宇宙.jpg", fly[0] + 0.1, fly[1] - 0.05, words="超知能の|<br>|ほぼ100％は|<br>|!深宇宙|で計算。",
                   word_style="top:230px;font-size:104px", src_note="出どころ：イーロン・マスクさんの X の投稿（2026-10-27）／絵は生成AI（Gemini）")
            + paper(tag("そのころ") + '<div class="stack" style="top:150px;gap:26px">'
                    + qline("イーロン・マスクさんの投稿", T(15, 1.6))
                    + line("この話の少し前の、別の投稿", T(15, 2.6), size=26, weight=500, style="color:#5a564f")
                    + card("fact", '超知能の、ほぼ100%は、深宇宙で計算される。明らかに。<div style="font-size:22px;color:#bdb7ab;margin-top:6px">"~100% of SI will be deep space compute. Obviously."</div>',
                           T(18, 0.45), "訳", w=860, style="font-size:30px")
                    + card("hb", "宇宙は、今日は置いておく", T(18, 1.1), "ハコブくん", w=520, style="font-size:30px")
                    + '</div>', 0.0, flights=[fly]))
block("musk", [15, 16, 17, 18], musk)

# ================= 3 やり取り1：遅い頭 =================
def think(T, E):
    c0, c1 = T(19, 0.3), T(20, -0.1)
    return (paper(tag("やり取り 1 ／ 考え中") + '<div class="stack" style="top:150px;gap:6px">'
                  + f'<div style="{SANS}font-size:30px;letter-spacing:.2em;">ハコブくん　考え中</div>'
                  + f'<div class="num" style="font-size:250px;line-height:1.05;"><span id="cnt">0.0</span>{land("秒", T(20, 0.05), "font-size:100px")}</div>'
                  + line("大きなAIなら、1回考えるのに、だいたいこのくらい", T(22, 0.5), size=34, weight=500, style="margin-top:24px")
                  + '</div>', 0.0)
            + stamp("遅っ", T(21, 0.0), "right:350px;top:560px;font-size:64px")
            + counter_js("cnt", c0, c1, 18.6) + shake_js([(T(21, 0.3), 0.3)]))
block("think", [19, 20, 21, 22], think)


def cup(T, E):
    return (dassen(BG + "コップ.jpg", T(24, 0.2), E(24) + 5, words="考え中……", word_style="top:120px;font-size:120px",
                   src_note="絵は生成AI（Gemini）で作成")
            + paper(tag("その間に") + '<div class="center" style="gap:10px">'
                    + line("π0.5 が、動きを直せる回数", T(23, 0.4), size=32, weight=700)
                    + f'<div class="num" style="font-size:230px;line-height:1.1"><span id="c370">0</span>{land("回ほど", T(23, 3.0), "font-size:80px")}</div>'
                    + line("18.6秒 ÷ 0.05秒", T(23, 3.3), size=26, weight=500, style="color:#8a857c")
                    + '</div>', 0.0, flights=[(T(24, 0.1), E(24) + 9)])
            + counter_js("c370", T(23, 1.0), T(23, 2.9), 370, dec=0))
block("cup", [23, 24], cup)


def detail(T, E):
    c = lambda n, v, t: card("fact", f'<span class="num" style="font-size:64px">{v}</span><span style="font-size:30px"> 秒</span>', t, f"使い方 {n}", w=250, fade=T(26, 0.3))
    return paper(tag("ちなみに") + '<div class="stack" style="top:110px;gap:26px">'
                 + f'<div class="row2" style="gap:24px">{c(1, "11.5", T(25, 1.0))}{c(2, "13.4", T(25, 1.4))}{c(3, "18.6", T(25, 1.8))}</div>'
                 + line("使い方によって、11秒から19秒まで", T(25, 3.4), size=30, weight=500)
                 + card("hb", 'ロボットのAIは脳のように分業する<div style="font-size:24px;color:#5a564f;margin-top:4px">pixelstomotors.com ／ LOG424</div>', T(27, 0.15), "サイトの記事", w=720, style="font-size:32px;margin-top:20px")
                 + '</div>' + '<div class="src">出どころ：NVIDIA「Astranomy」（2026-10-14）</div>', 0.0)
block("detail", [25, 26, 27], detail)


def faster(T, E):
    return paper(tag("やり取り 1 ／ 遅い頭") + '<div class="stack" style="top:88px;gap:20px">'
                 + line("大きなAIだけでは、間に合わない", T("more_b", 0.4), size=32, weight=700)
                 + card("mi", ink("もっと工夫が必要", T(28, 0.4), 0.06), T(28, 0.15), "見立て ・ エイイチ", w=600, anim="wipe")
                 + card("hb", "AIは毎年賢くなる。そのうち速くなる", T(29, 0.6), "ハコブくん", w=640, botsu=T(31, 2.5), fade=T(31, 2.7), style="font-size:30px")
                 + card("mi", ink("LLMの進化で、反応の速さは上がらないのでは", T(30, 0.4), 0.045), T(30, 0.15), "見立て ・ エイイチ", w=820, anim="wipe")
                 + qline("……半分、おっしゃる通り", T(31, 1.6), size=44)
                 + '</div>', 0.0)
block("faster", [28, 29, 30, 31], faster)


def faster2(T, E):
    return paper(tag("ハコブくんの見立て") + '<div class="center" style="gap:34px">'
                 + '<div class="row2">'
                 + card("fact", ink("賢くなるほど、考える時間が長く", T(32, 1.0), 0.05), T(32, 0.5), "大きなAI", w=430)
                 + card("hb", ink("速くなる", T(32, 9.9), 0.08), T(32, 9.6), "小さくした版", w=330, style="font-size:40px;font-weight:700")
                 + '</div>' + line("と、僕は見ています", T(32, 6.8), size=26, weight=500, style="color:#5a564f")
                 + '</div>', 0.0)
block("faster2", [32], faster2)

# ================= 4 山場：脳 =================
def split(T, E):
    big = lambda a, b, t, k: (f'<div class="card k-{k}" style="width:340px;opacity:0;animation:pop .45s ease-out {t:.3f}s forwards">'
                              f'<small>{a}</small><span class="serif" style="font-size:110px;font-weight:900;line-height:1.2">{b}</span></div>')
    return paper(tag("だから") + '<div class="center" style="gap:40px">'
                 + qline("分けるしかない", T(33, 0.3), size=52)
                 + f'<div class="row2" style="gap:44px">{big("遅く考える", "頭", T(33, 2.4), "fact")}{big("速く直す", "手", T(33, 3.6), "fact")}</div>'
                 + '</div>', 0.0)
block("split", [33], split)


def brain_ei(T, E):
    return ('<div style="position:absolute;inset:0;background:#141311"></div>'
            '<div class="paper" style="opacity:.08"></div>'
            '<div class="center" style="left:430px;width:1060px;right:auto;top:64px;height:780px;bottom:auto">'
            '<p class="v" style="color:#f5f3ee;font-size:58px;line-height:1.7;font-weight:500;letter-spacing:.08em;">'
            f'<span data-ink="{T(34, 0.4):.2f},0.1">人も、</span><br>'
            f'{land("小脳", T(34, 1.0), "font-weight:900;font-size:80px")}<span data-ink="{T(34, 1.5):.2f},0.1">と</span>'
            f'{land("大脳", T(34, 1.8), "font-weight:900;font-size:80px")}<span data-ink="{T(34, 2.3):.2f},0.1">で、</span><br>'
            f'<span data-ink="{T(34, 2.8):.2f},0.09">役割が分かれている。</span></p></div>'
            + stamp("この見立ては、エイイチから", T(34, 4.6), "left:740px;top:740px")
            + shake_js([(T(34, 4.95), 0.3)]))
block("brain_ei", [34], brain_ei)


def brain(T, E):
    row = lambda b, txt, t, extra="": (f'<div class="trow {extra}" style="opacity:0;animation:pop .45s ease-out {t:.3f}s forwards">'
                                       f'<b>{b}</b><span>{txt}</span></div>')
    hl = T(39, 2.3)
    return paper(tag("脳の分かれ方") + '<div class="stack" style="top:110px;gap:16px">'
                 + row("大脳", "考えて決める所", T(36, 0.3))
                 + row("小脳", "練習で覚えた動きを合わせる所", T(36, 2.9))
                 + (f'<div class="trow" style="opacity:0;animation:pop .45s ease-out {T(37, 0.4):.3f}s forwards;background:#e6e1d6">'
                    f'<b>反射</b><span class="hl" style="animation-delay:{hl:.3f}s">脳を通らずに、体を動かす</span><i>脊髄</i></div>')
                 + f'<div style="height:30px"></div>'
                 + card("fact", "ぶつかりそうになったら、止まる仕組み", T(41, 0.5), "ロボットの反射", w=760)
                 + '</div>', 0.0)
block("brain", [35, 36, 37, 38, 39, 40, 41], brain)


def cerebellum(T, E):
    return paper(tag("小脳") + '<div class="stack" style="top:86px;gap:10px">'
                 + line("人の小脳の神経細胞は、考える大脳の表面の", T(42, 2.7), size=30, weight=500)
                 + f'<div class="num" style="font-size:170px;line-height:1.1">{land("4倍", T(42, 5.8))}<span style="font-size:60px">{land("ほど", T(42, 6.1))}</span></div>'
                 + line("動きを合わせる所に、こんなに多く", T(43, 0.4), size=30, weight=500)
                 + card("mi", ink("頭より、体のほうが忙しい", T(44, 0.4), 0.06), T(44, 0.15), "エイイチ", w=600, anim="wipe")
                 + card("hb", "僕も、運ぶほうが忙しい", T(45, 0.6), "ハコブくん", w=520, style="font-size:30px")
                 + '</div>' + '<div class="src">出どころ：Herculano-Houzel の研究（小脳と大脳皮質の神経細胞の数）</div>', 0.0)
block("cerebellum", [42, 43, 44, 45], cerebellum)

# ================= 5 やり取り2：失敗の手直し =================
FALL_T = None


def fail(T, E):
    tf = T(48, S(48) + 0.15)
    return (paper(tag("やり取り 2 ／ 失敗の手直し") + '<div class="stack" style="top:84px;gap:18px">'
                  + qline("忙しい小脳は、どう育つ？", T("fail_b", 0.4), size=40)
                  + card("mi", ink("失敗を手直しするデータの積み重ねは、思っている以上に大事", T(46, 0.5), 0.04), T(46, 0.2), "見立て ・ エイイチ", w=860, anim="wipe")
                  + card("fact", ink("転びかけて、持ち直す　→　考えずに乗れる", T(47, 0.8), 0.045), T(47, 0.4), "自転車", w=760)
                  + card("hb", "小脳を鍛えるため、毎晩転ぶ", T(48, 1.6), "ハコブくん", w=560, botsu=T(49, 1.4), fade=T(49, 1.6), style="font-size:30px")
                  + card("mi", ink("失敗だけ覚えちゃって、良くない", T(49, 0.4), 0.05), T(49, 0.15), "見立て ・ エイイチ", w=640, anim="wipe")
                  + '</div>', 0.0)
            + f'<div class="big" style="left:60px;right:auto;top:600px;font-size:96px;transform:rotate(-8deg)"><span style="animation:boom .3s cubic-bezier(.2,1.8,.4,1) {tf + 0.3:.3f}s forwards, hide .2s linear {T(49, 0.2):.3f}s forwards">ドテッ</span></div>'
            + shake_js([(tf + 0.3, 0.25)]))
block("fail", [46, 47, 48, 49, 50], fail)


def three(T, E):
    lab = lambda txt, t: f'<div style="{SANS}font-size:24px;font-weight:700;color:#5a564f;height:36px;text-align:center">{ink(txt, t, 0.05)}</div>'
    seg = lambda txt, w, bg, fg, t, fade=None: (
        f'<div class="seg" style="width:{w}px;background:{bg};color:{fg};clip-path:inset(0 100% 0 0);'
        f'animation:wipe .45s ease-out {t:.3f}s forwards{f", fadeto .5s ease {fade:.3f}s forwards" if fade else ""}">{txt}</div>')
    rule = lambda txt, t, style="": f'<div style="{SANS}font-size:30px;font-weight:700;{style}">{land(txt, t)}</div>'
    return paper(tag("記録を、3つの時点で切る") + '<div class="stack" style="top:120px;gap:14px">'
                 + '<div style="display:flex;width:920px">'
                 + f'<div style="width:260px">{lab("① 失敗した瞬間", T(51, 3.4))}</div>'
                 + f'<div style="width:400px">{lab("② 手直しを始めた瞬間", T(51, 4.8))}</div>'
                 + f'<div style="width:260px">{lab("③ 戻った瞬間", T(51, 6.3))}</div></div>'
                 + '<div style="display:flex;width:920px">'
                 + seg("失敗の区間", 260, "#cfc8ba", "#22201c", T(51, 3.6), fade=T(52, 0.9))
                 + seg("手直しの区間", 400, "#22201c", "#f5f3ee", T(51, 5.0))
                 + seg("戻ったあと", 260, "#ece9e2", "#22201c", T(51, 6.5)) + '</div>'
                 + '<div style="display:flex;width:920px;text-align:center">'
                 + f'<div style="width:260px">{rule("学ばせない", T(52, 0.6), "color:#8a857c")}</div>'
                 + f'<div style="width:400px">{rule("重く学ばせる", T(52, 2.4), "font-size:38px;font-weight:900")}</div>'
                 + '<div style="width:260px"></div></div>'
                 + '<div style="height:24px"></div>'
                 + card("fact", "何度も失敗する場所には、きれいなお手本を足す", T(52, 4.9), "もう1つ", w=900)
                 + '</div>', 0.0)
block("three", [51, 52], three)


def web(T, E):
    return paper(tag("汎用AIに、いちばん足りないもの") + '<div class="center" style="gap:26px">'
                 + qline("インターネットの動画には、", T(53, 0.3), size=46)
                 + f'<div class="serif" style="font-size:46px;font-weight:700">{land("持ち直す場面", T(53, 2.0), "font-size:64px;font-weight:900")}{ink("が、ほとんど写っていない", T(53, 2.6), 0.06)}</div>'
                 + '<div style="height:20px"></div>'
                 + card("mi", ink("これ、今おれがやってる", T(54, 0.4), 0.06), T(54, 0.15), "エイイチ", w=560, anim="wipe")
                 + '</div>', 0.0)
block("web", [53, 54], web)

# ================= 6 やり取り3：記憶の整理 =================
LOG_DASSEN = None


def log(T, E):
    d0, d1 = T(55, 7.2), T(56, -0.05)
    return (dassen(BG + "紙の山.jpg", d0 + 0.05, d1 - 0.05, words="!424|本", word_style="top:120px;font-size:150px",
                   src_note="絵は生成AI（Gemini）で作成")
            + paper(tag("やり取り 3 ／ 記憶の整理") + '<div class="stack" style="top:110px;gap:22px">'
                    + f'<div class="num" style="font-size:150px;line-height:1.1"><span style="font-size:70px">LOG </span><span id="c424">0</span></div>'
                    + line("全部、全文で残している", T(55, 2.2), size=30, weight=500)
                    + '<div style="height:16px"></div>'
                    + card("mi", ink("LOGは、全部の積み重ね。基幹情報", T(56, 0.4), 0.05), T(56, 0.15), "見立て ・ エイイチ", w=760, anim="wipe")
                    + card("mi", ink("情報がたくさんあるだけでは、言葉や動きは良くならない", T(57, 0.4), 0.04), T(57, 0.15), "見立て ・ エイイチ", w=860, anim="wipe")
                    + '</div>', 0.0, flights=[(d0, d1)])
            + counter_js("c424", T(55, 3.9), T(55, 6.1), 424, dec=0))
block("log", [55, 56, 57], log)


def hippo(T, E):
    col = lambda lab, body, chip, t1, t2: (
        f'<div style="display:flex;flex-direction:column;align-items:center;gap:14px">'
        + card("fact", ink(body, t1 + 0.3, 0.05), t1, lab, w=380)
        + f'<div class="card k-hb" style="width:300px;font-size:30px;opacity:0;animation:pop .45s ease-out {t2:.3f}s forwards">{chip}</div></div>')
    return paper(tag("脳の整理") + '<div class="stack" style="top:110px;gap:26px">'
                 + '<div class="row2" style="align-items:flex-start;gap:20px">'
                 + col("海馬", "その日の出来事を、いったん残す", "記事の全文", T(58, 2.0), T(59, 0.5))
                 + f'<div style="{SANS}font-size:24px;font-weight:700;color:#5a564f;padding-top:56px;width:120px;text-align:center">{ink("眠っている間に", T(58, 4.6), 0.06)}<br>{land("→", T(58, 5.6), "font-size:40px")}</div>'
                 + col("大脳の表面", "大事なものが、移っていく", "整理した記事", T(58, 6.3), T(59, 2.3))
                 + '</div>'
                 + card("mi", ink("残す基準は、まだまだこれから", T(60, 0.4), 0.05), T(60, 0.15), "見立て ・ エイイチ", w=640, anim="wipe")
                 + card("hb", "残した理由を、一言ずつ書いておく", T(61, 0.6), "ハコブくん", w=600, style="font-size:30px")
                 + '</div>', 0.0)
block("hippo", [58, 59, 60, 61], hippo)

# ================= 7 まとめ =================
SUM_CSS = """@keyframes gather { from { opacity:0; transform: translate(var(--dx),var(--dy)) rotate(var(--r)); } to { opacity:1; transform: none; } }"""


def summ(T, E):
    row = lambda b, mid, right, dx, dy, r, t: (f'<div class="trow" style="--dx:{dx}px;--dy:{dy}px;--r:{r}deg;opacity:0;'
                                              f'animation:gather .9s cubic-bezier(.3,1.2,.4,1) {t:.3f}s forwards"><b>{b}</b><span>{mid}</span><i>{right}</i></div>')
    b3 = lambda txt, t: card("hb", txt, t, None, w=250, botsu=t + 0.25, style="font-size:24px;padding:12px 16px")
    return ('<style>' + SUM_CSS + '</style>'
            + paper(tag("残したもの") + '<div class="stack" style="top:96px;gap:12px">'
                    + row("大脳", "何にでも使える大きなAI", "遅く考える", 420, 420, 8, 0.3)
                    + row("小脳", "π0.5 のような専用のAI", "速く直す", 300, -60, -6, 0.45)
                    + row("反射", "ぶつかる前に止まる仕組み", "考える前に", -420, 180, 10, 0.6)
                    + f'<p class="serif" style="margin-top:30px;text-align:center;font-size:44px;line-height:1.55;font-weight:900;">'
                      f'{ink("速い手を育てるのは、", T(62, 2.6), 0.06)}<br>{land("失敗を手直しした記録。", T(62, 4.0), "font-size:56px")}</p>'
                    + f'<div class="row2" style="gap:30px;margin-top:22px">{b3("π0.5 は引退", T(63, 1.0))}{b3("そのうち速くなる", T(63, 1.4))}{b3("毎晩転ぶ", T(63, 1.8))}</div>'
                    + '</div>', 0.0))
block("sum", [62, 63], summ)

# ================= 8 残った問い =================
def rest(T, E):
    return paper(tag("残った問い") + '<div class="center" style="gap:20px">'
                 + qline("手直しの区間を分けて学ばせたら、", T(64, 2.0), size=44)
                 + qline("戻れる率は、どれだけ変わるか。", T(64, 4.3), size=44)
                 + line("続きは、机の上の実験で確かめる", T(64, 7.4), size=28, weight=500, style="color:#5a564f;margin-top:6px")
                 + '<div style="height:30px"></div>'
                 + card("hb", 'このノートの全文<div style="font-size:24px;color:#5a564f;margin-top:4px">pixelstomotors.com ／ LOG424</div>', T(65, 0.3), "サイトの記事", w=600)
                 + line("数字の出どころは、概要欄に", T(65, 3.4), size=26, weight=500, style="color:#5a564f")
                 + '</div>', 0.0)
block("rest", [64, 65], rest)


def ask(T, E):
    return paper(tag("あなたの現場では") + '<div class="center" style="gap:12px">'
                 + qline("あなたの現場では、", T(66, 0.2), size=54)
                 + qline("失敗の記録は、どこに", T(66, 1.3), size=54)
                 + qline("残っていますか。", T(66, 2.4), size=54)
                 + '<div style="height:40px"></div>'
                 + f'<div style="{SANS}font-size:30px;letter-spacing:.2em;opacity:0;animation:pop .4s ease-out {T(67, 1.0):.3f}s forwards">ハコブくん　考え中</div>'
                 + f'<div class="num" style="font-size:110px;line-height:1.1">{land("0.0秒", T(67, 1.2))}</div>'
                 + '</div>', 0.0)
block("ask", [66, 67, 68], ask)

# ================= 9 アウトロ =================
block("outro", [69, 70, 71], None, video=("src/day124_crop.mp4", 18.0, "png/ovl/outro.png"))

assert [i for b in BLOCKS for i in b["sents"]] == list(range(N)), "場面の文の番号が抜けている・重なっている"

# ================= ハコブくん =================
# 表情は narration.py の表情が基本。ここは文の途中で替える所: (文, 文の頭から何秒, 何秒間, 表情)
HK_FACE = [
    (3, 0.3, 6.5, "doubt"),
    (13, 3.2, 3.0, "joy"), ("tsukkomi", 0.0, 1.8, "surprise"), (14, 0.0, 4.6, "trouble"),
    (17, 0.0, 1.9, "smile"),
    (21, 0.1, 1.3, "trouble"),
    (24, 0.2, 5.6, "think"),
    (26, 0.0, 2.4, "doubt"),
    (28, 0.0, 2.4, "base"),
    (30, 0.4, 4.2, "surprise"),
    (31, 2.6, 2.6, "trouble"),
    (34, 0.0, 7.0, "surprise"),
    (38, S(38) + 0.85, 1.0, "x"),
    (40, 0.0, 2.6, "doubt"),
    (44, 0.0, 3.2, "joy"),
    (46, 0.0, 6.0, "think"),
    (48, S(48) + 0.1, 4.3, "x"),
    (49, 0.0, 3.2, "x"),
    (54, 0.4, 2.6, "smile"),
    (56, 0.0, 8.4, "think"),
    (60, 0.0, 3.1, "smile"),
    (67, 0.0, 2.5, "smile"),
]
HK_FACE = [(A(i, a), A(i, a) + d, f) for i, a, d, f in HK_FACE]

# 体ごとの動き: jump＝驚いて全身で飛び出す / fall＝前のめりにコケる（up で起き上がる） / reflex＝のけぞる
HK_MOVES = [
    dict(kind="jump", t0=A(16, 0.25), dur=3.4),
    dict(kind="jump", t0=A(35, 0.0), dur=1.9),
    dict(kind="reflex", t0=A(38, S(38) + 0.85), dur=0.9),
    dict(kind="fall", t0=A(48, S(48) + 0.1), t1=A(50, 0.0), dur=0.5),
]
HK_ALPHA = [(A(34, 0.0), tl.end(34), 0.3),
            (A(13, 3.3), tl.end(13) - 0.15, 0.0)]   # 引退式の絵には泣いているハコブくんが描いてあるので、重ねない   # 暗い場面（エイイチの見立て）では薄く

# ================= エイイチ =================
# (文, 文の頭から何秒, 姿) 1腕組み 2うなずく 3指さす 4首をかしげる 5ひらめく 6のけぞって驚く 7半目でツッコむ 8吹き出して笑う
EI_POSE = [
    (0, 0.0, 1), (3, 0.0, 3), (4, 0.0, 7), (4, 1.4, 1),
    (8, 0.0, 2), (9, 0.0, 1),
    (10, 0.0, 1), (12, 0.0, 4), (13, 3.2, 6), ("tsukkomi", 0.0, 7), (14, 0.0, 3),
    (15, 0.0, 1), (16, 0.2, 6), (17, 0.0, 7), (18, 0.4, 1),
    (19, 0.0, 1), (21, 0.0, 7), (22, 2.0, 1),
    (23, 0.0, 4), (24, 3.0, 6), (25, 0.0, 1), (26, 0.0, 7), (27, 0.9, 2), ("more_b", 0.0, 1),
    (28, 0.0, 3), (29, 1.5, 4), (30, 0.0, 3), (31, 0.0, 1), (32, 3.0, 2),
    (33, 0.0, 1), (34, 0.0, 5), (35, 0.0, 6), (36, 0.0, 2),
    (38, 0.0, 3), (40, 0.0, 4), (41, 1.0, 2),
    (42, 0.0, 4), (43, 0.0, 2), (44, 0.0, 3), (45, 0.4, 8),
    ("fail_b", 0.0, 4), (46, 0.0, 3), (47, 0.0, 2), (48, S(48) + 0.3, 8), (49, 0.0, 7), (50, 0.5, 1),
    (51, 0.0, 2), (53, 0.0, 4), (54, 0.0, 3),
    (55, 0.0, 1), (55, 7.2, 6), (56, 0.0, 3), (57, 0.0, 1),
    (58, 0.0, 4), (59, 0.0, 2), (60, 0.0, 3), (61, 0.0, 2),
    ("sum_b", 0.0, 2), (63, 0.8, 8),
    (64, 0.0, 1), (65, 0.0, 2),
    (66, 0.0, 1), (67, 0.0, 3), (68, 0.3, 8),
    (69, 0.0, 2),
]
EI_POSE = sorted((A(i, a), p) for i, a, p in EI_POSE)
# 置き方の替え: (開始, 終了, 置き方)。hide＝出さない / big＝暗い場面で大きく光らせる / buried＝紙の山に埋もれる
# slide＝ハコブくんへ指を近づける（反射の実演）
EI_PLACE = [
    (A(6), tl.end(7), "hide"),
    (A(34), tl.end(34), "big"),
    (A(55, 7.25), A(56, -0.05), "buried"),
    (A(38, S(38) + 0.05), A(39, 1.4), "slide"),
]

# ================= 効果音 =================
# (文, 文の頭から何秒, 音, ナレーションより何dB)
SE = [
    (13, 3.2, "se_whoosh", -10), ("tsukkomi", 0.1, "se_pop", -8), ("tsukkomi", 0.7, "se_pop", -12),
    (16, -0.15, "se_whoosh", -10), (18, 0.0, "se_whoosh", -14),
    (21, 0.0, "se_pop", -8),
    (24, 0.1, "se_whoosh", -10),
    (31, 2.5, "se_pop", -12),
    (34, 4.6, "se_pop", -8),
    (35, 0.0, "se_rise", -10),
    (38, S(38) + 0.85, "se_pop", -10),
    (48, S(48) + 0.1, "se_whoosh", -12), (48, S(48) + 0.45, "se_pop", -6), (49, 1.4, "se_pop", -12),
    (55, 7.2, "se_whoosh", -10), (56, -0.05, "se_whoosh", -14),
    (63, 1.25, "se_pop", -14), (63, 1.65, "se_pop", -14), (63, 2.05, "se_pop", -14),
    (67, 1.2, "se_ding", -12),
] + [(19, 0.3 + k, "se_tick", -16) for k in range(6)]
SE = [(ix(i), off, f, lv) for i, off, f, lv in SE]   # audio_mix.py はいまの番号で読む

# 一覧の画像に撮る時刻（場面の頭からの秒）。書かない場面は最後の0.4秒前
def _rel(b, i, off=0.0):
    return tl.start(i) - _tl.start(b["sents"][0]) + off
SHEET = {}
for _b in BLOCKS:
    _end = _tl.end(_b["sents"][-1]) - _tl.start(_b["sents"][0]) - 0.4
    SHEET[_b["name"]] = [_end]
_B = {b["name"]: b for b in BLOCKS}
SHEET["bring"] = [_rel(_B["bring"], 13, 2.0), _rel(_B["bring"], 13, 4.4), SHEET["bring"][0]]
SHEET["musk"] = [_rel(_B["musk"], 16, 1.6), _rel(_B["musk"], 17, 0.5), SHEET["musk"][0]]
SHEET["think"] = [_rel(_B["think"], 19, 3.0), SHEET["think"][0]]
SHEET["cup"] = [_rel(_B["cup"], 23, 4.0), SHEET["cup"][0]]
SHEET["brain"] = [_rel(_B["brain"], 35, 0.5), _rel(_B["brain"], 38, S(38) + 0.9), SHEET["brain"][0]]
SHEET["fail"] = [_rel(_B["fail"], 48, S(48) + 0.8), SHEET["fail"][0]]
SHEET["log"] = [_rel(_B["log"], 55, 8.5), SHEET["log"][0]]
SHEET["title"] = [2.0]
```

### kabe_kit.py（紙を1コマずつ撮り、ハコブくん・作り手・字幕を重ねて書き出す部品）

```python
# -*- coding: utf-8 -*-
"""考察回「壁打ちの型」の書き出しの部品（2026-11-02 1本目「頭脳は脳のように分かれる」で作った）。
紙（考察ノート）の上の文字の動きは HTML の CSS アニメーションで描き、時刻を止めて1コマずつ撮る（決まった絵になる）。
ハコブくん（口・目・リング・飛び出す・コケる）、エイイチのドット絵（声に合わせて小さく弾む）、字幕は、ここで重ねる。

使い方（動画の作業フォルダで。scenes_kabe.py に場面を書く）
  python3 kabe_kit.py sheet          場面ごとの静止画の一覧（一覧_場面.jpg）
  python3 kabe_kit.py render [名前..] 紙の映像を作る（できているものは使い回す）
  python3 kabe_kit.py build [--first-blocks N]  重ねて書き出す → out.mp4・out_確認用.mp4・out.srt
"""
import json, os, sys, subprocess, hashlib, html as _html, wave
import numpy as np
from PIL import Image, ImageFilter

HERE = os.path.dirname(os.path.abspath(__file__))
for _c in (os.environ.get("P2M_BASE", ""), os.path.join(HERE, "..", "_制作ベース")):
    if _c and os.path.exists(os.path.join(_c, "hakobu.py")):
        BASE = os.path.abspath(_c); break
sys.path.insert(0, HERE)
sys.path.insert(1, BASE)
os.environ.setdefault("P2M_BASE", BASE)
from timing import FPS, SR, load, layout, narration, write_wav
from subtitle import pages as sub_pages, split_lines, SUB_CX, SUB_MAXW, SUB_MAXC
import hakobu as hk

W, H = 1920, 1080

# ---------------- ハコブくんの顔を足す（ショートの動き hakobu_motion.py の顔の形だけを読む） ----------------
_src = open(os.path.join(BASE, "hakobu_motion.py"), encoding="utf-8").read().split("# ================= 体と背景の準備")[0]
_ns = {"__name__": "hakobu_faces"}
exec(compile(_src, "hakobu_motion(顔)", "exec"), _ns)
hk.EXPR.setdefault("x", dict(eyes=("x", "x"), brows=None, rest="o", closed="o"))
hk.EXPR.setdefault("dot", dict(eyes=("dot", "dot"), brows=None, rest="flat", closed="flat"))
hk.EXPR.setdefault("down", dict(eyes=("down", "down"), brows="trouble", rest="frown", closed="frown"))

# ---------------- 紙の CSS（動きの試しの base.css を本番用に） ----------------
CSS = r"""
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { width: 1920px; height: 1080px; overflow: hidden; background: #22201c; }
.f { width: 1920px; height: 1080px; position: relative; overflow: hidden; background: #22201c; color: #22201c; font-family: "Noto Serif CJK JP", serif; }
.paper { position: absolute; left: 430px; top: 64px; width: 1060px; height: 780px; background: #f5f3ee; box-shadow: 0 30px 60px rgba(0,0,0,.45); }
.tag { position: absolute; left: 0; right: 0; top: 30px; text-align: center; font-family: "Noto Sans CJK JP", sans-serif; font-weight: 500; font-size: 24px; letter-spacing: .2em; color: #5a564f; }
.src { position: absolute; left: 0; right: 0; bottom: 24px; text-align: center; font-family: "Noto Sans CJK JP", sans-serif; font-weight: 500; font-size: 20px; color: #8a857c; }
.v { writing-mode: vertical-rl; }
.center { position: absolute; left: 0; right: 0; top: 0; bottom: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.stack { position: absolute; left: 0; right: 0; top: 96px; display: flex; flex-direction: column; align-items: center; gap: 22px; }
.row2 { display: flex; gap: 32px; justify-content: center; }
.card { position: relative; font-family: "Noto Sans CJK JP", sans-serif; font-weight: 500; font-size: 34px; line-height: 1.5; padding: 16px 34px; text-align: center; }
.card small { display: block; font-family: "Noto Sans CJK JP", sans-serif; font-weight: 700; font-size: 20px; letter-spacing: .12em; margin-bottom: 4px; }
.k-q { font-family: "Noto Serif CJK JP", serif; font-size: 46px; font-weight: 700; padding: 0 0 6px; }
.k-fact { background: #22201c; color: #f5f3ee; }
.k-fact small { color: #bdb7ab; }
.k-mi { background: #dcd6c9; color: #22201c; }
.k-mi small { color: #5a564f; }
.k-hb { background: #ece9e2; color: #22201c; }
.k-hb small { color: #5a564f; }
.num { font-family: "Noto Serif CJK JP", serif; font-weight: 900; }
.serif { font-family: "Noto Serif CJK JP", serif; }
@keyframes show { to { opacity: 1; } }
@keyframes hide { to { opacity: 0; } }
[data-ink] > span { display: inline-block; opacity: 0; filter: blur(6px); animation: ink .45s ease-out forwards; }
@keyframes ink { 0% { opacity: 0; filter: blur(6px); transform: translateY(-6px); } 100% { opacity: 1; filter: blur(0); transform: none; } }
.land { display: inline-block; opacity: 0; animation: land .6s cubic-bezier(.2,1.6,.4,1) forwards; }
@keyframes land { 0% { opacity: 0; transform: scale(1.5); filter: blur(4px); } 100% { opacity: 1; transform: scale(1); filter: blur(0); } }
.pop { opacity: 0; animation: pop .45s ease-out forwards; }
@keyframes pop { 0% { opacity: 0; transform: translateY(14px); } 100% { opacity: 1; transform: none; } }
.wipe { clip-path: inset(0 100% 0 0); animation: wipe .45s ease-out forwards; }
@keyframes wipe { to { clip-path: inset(0 0 0 0); } }
.stamp { position: absolute; background: #c4472a; color: #f5f3ee; font-family: "Noto Sans CJK JP", sans-serif; font-size: 30px; font-weight: 900; padding: 10px 20px; letter-spacing: .08em; opacity: 0; animation: slam .35s cubic-bezier(.5,0,.75,0) forwards; white-space: nowrap; }
@keyframes slam { 0% { opacity: 0; transform: rotate(-6deg) scale(2.6); } 85% { opacity: 1; transform: rotate(-6deg) scale(.94); } 100% { opacity: 1; transform: rotate(-6deg) scale(1); } }
.botsu { position: absolute; right: -30px; top: -22px; background: #c4472a; color: #f5f3ee; font-family: "Noto Sans CJK JP", sans-serif; font-size: 26px; font-weight: 900; padding: 4px 14px; opacity: 0; animation: slam .35s cubic-bezier(.5,0,.75,0) forwards; }
@keyframes fadeto { to { opacity: .5; } }
.hl { background: linear-gradient(#e6dfcf,#e6dfcf) no-repeat left 85% / 0% 45%; animation: hl .5s ease-out forwards; }
@keyframes hl { to { background-size: 100% 45%; } }
.dassen { position: absolute; inset: 0; background-size: cover; background-position: center; opacity: 0; }
@keyframes dIn { from { opacity: 0; transform: scale(1.3); } to { opacity: 1; transform: none; } }
@keyframes dOut { to { opacity: 0; } }
@keyframes fly { from { transform: none; } to { transform: translate(500px,-1100px) rotate(28deg); } }
@keyframes back { from { transform: translate(-1400px,0) rotate(-8deg); } to { transform: none; } }
@keyframes paperIn { from { opacity: 0; transform: translateY(40px) rotate(-1.2deg); } to { opacity: 1; transform: none; } }
.big { position: absolute; left: 0; right: 0; text-align: center; transform: rotate(-5deg); font-family: "Noto Sans CJK JP", sans-serif; font-weight: 900; color: #fff; font-size: 120px; line-height: 1.2; -webkit-text-stroke: 4px #141311; text-shadow: 0 10px 0 #c4472a; }
.big span { display: inline-block; opacity: 0; }
@keyframes boom { from { opacity: 0; transform: scale(2.4) rotate(8deg); } to { opacity: 1; transform: none; } }
.trow { display: flex; align-items: center; width: 760px; padding: 16px 30px; background: #e6e1d6; font-family: "Noto Sans CJK JP", sans-serif; font-weight: 500; font-size: 32px; text-align: left; }
.trow b { width: 150px; font-weight: 900; } .trow i { margin-left: auto; font-style: normal; font-size: 24px; color: #5a564f; }
.seg { font-family: "Noto Sans CJK JP", sans-serif; text-align: center; padding: 22px 10px; font-size: 30px; font-weight: 700; }
"""

JS = r"""<script>
function ink(el, start, step){ const t=el.textContent; el.textContent=''; [...t].forEach((c,i)=>{const s=document.createElement('span'); s.textContent=c===' '?' ':c; s.style.animationDelay=(start+i*step)+'s'; el.appendChild(s);}); }
document.querySelectorAll('[data-ink]').forEach(el=>{const [a,b]=el.dataset.ink.split(','); ink(el,+a,+b||0.06);});
document.getAnimations().forEach(a=>a.pause());
window.setT=function(t){ document.getAnimations().forEach(a=>{a.pause(); a.currentTime=t*1000;}); if(window.hook) window.hook(t); };
window.busy=function(t){ const e=1/30+0.002;
  for (const a of document.getAnimations()) { const c=a.effect.getComputedTiming(); const d=c.delay/1000, end=c.endTime/1000;
    if (t>=d-e && t<=end+e) return true; }
  return window.hookBusy ? !!window.hookBusy(t) : false; };
</script>"""


def page(body, extra_css=""):
    return ('<!doctype html><html lang="ja"><head><meta charset="utf-8"><style>' + CSS + extra_css +
            '</style></head><body><div class="f" id="F">' + body + '</div>' + JS + '</body></html>')


# ---------------- 紙の上の部品（時刻は場面の頭からの秒） ----------------
def ink(text, t, step=0.05, cls="", style=""):
    return f'<span data-ink="{t:.3f},{step}" class="{cls}" style="{style}">{_html.escape(text)}</span>'


def land(text, t, style=""):
    return f'<span class="land" style="animation-delay:{t:.3f}s;{style}">{text}</span>'


def card(kind, body, t, label=None, w=None, anim="pop", fade=None, botsu=None, style=""):
    """kind: fact（墨）/ mi（エイイチの見立て・濃い灰）/ hb（ハコブくん・薄い灰）/ q（問い）"""
    an = {"pop": f"pop .45s ease-out {t:.3f}s forwards", "wipe": f"wipe .45s ease-out {t:.3f}s forwards"}[anim]
    init = "opacity:0;" if anim == "pop" else "clip-path:inset(0 100% 0 0);"
    if fade is not None:
        an += f", fadeto .5s ease {fade:.3f}s forwards"
    lab = f"<small>{label}</small>" if label else ""
    bs = f'<div class="botsu" style="animation-delay:{botsu:.3f}s">ボツ</div>' if botsu is not None else ""
    wd = f"width:{w}px;" if w else ""
    return f'<div class="card k-{kind}" style="{init}{wd}animation:{an};{style}">{lab}{body}{bs}</div>'


def stamp(text, t, style):
    return f'<div class="stamp" style="animation-delay:{t:.3f}s;{style}">{text}</div>'


def tag(text):
    return f'<div class="tag">{text}</div>'


def paper(inner, t_in=0.0, flights=(), style=""):
    """flights: [(飛んでいく秒, 戻る秒), ...] 脱線のときに紙が画面の外へ飛ぶ"""
    an = [f"paperIn .35s ease-out {t_in:.3f}s both"] if t_in is not None else []
    for a, b in flights:
        an.append(f"fly .5s cubic-bezier(.5,0,.9,.4) {a:.3f}s forwards")
        an.append(f"back .5s cubic-bezier(.2,1,.4,1) {b:.3f}s forwards")
    return f'<div class="paper" style="animation:{", ".join(an)};{style}">{inner}</div>'


def dassen(img, t_in, t_out, words="", word_style="top:200px", src_note=""):
    """画面いっぱいの写真・イラスト（t_in〜t_out）。words は大きな一言（<span> は boom で出る）"""
    p = "file://" + os.path.abspath(img)
    out = (f'<div class="dassen" style="background-image:url(\'{p}\');'
           f'animation:dIn .45s ease-out {t_in:.3f}s forwards, dOut .25s linear {t_out:.3f}s forwards"></div>')
    if words:
        spans, k = [], 0
        for part in words.split("|"):
            if part == "<br>":
                spans.append("<br>"); continue
            st = ""
            if part.startswith("!"):
                part, st = part[1:], "color:#ffd84a;"
            spans.append(f'<span style="{st}animation:boom .35s cubic-bezier(.2,1.8,.4,1) {t_in + 0.3 + 0.35 * k:.3f}s forwards, '
                         f'hide .2s linear {t_out - 0.1:.3f}s forwards">{part}</span>')
            k += 1
        out += f'<div class="big" style="{word_style}">{"".join(spans)}</div>'
    if src_note:
        out += (f'<div style="position:absolute;left:40px;top:28px;font-family:\'Noto Sans CJK JP\';font-size:20px;color:#fff;'
                f'text-shadow:0 0 6px #000;opacity:0;animation:show .01s linear {t_in + 0.3:.3f}s forwards, hide .1s linear {t_out:.3f}s forwards">{src_note}</div>')
    return out


def shake_js(windows, el="F", amp=5):
    """判子などで画面を少し揺らす（windows: [(開始秒, 長さ)]）"""
    w = json.dumps(windows)
    return (f'<script>(function(){{const W={w};const p=window.hook;window.hook=function(t){{if(p)p(t);let s="none";'
            f'for(const [a,d] of W){{if(t>=a&&t<a+d){{s="translate("+(Math.sin(t*90)*{amp})+"px,"+(Math.cos(t*70)*{amp * 0.8})+"px)";}}}}'
            f'document.getElementById("{el}").style.transform=s;}};const q=window.hookBusy;window.hookBusy=function(t){{'
            f'if(q&&q(t))return true;for(const [a,d] of W){{if(t>=a-0.05&&t<a+d+0.05)return true;}}return false;}};}})();</script>')


def counter_js(el, t0, t1, v1, dec=1, suffix=""):
    """数え上げ（t0〜t1 で 0→v1）"""
    return (f'<script>(function(){{const p=window.hook;window.hook=function(t){{if(p)p(t);const u=Math.min(1,Math.max(0,(t-{t0})/({t1}-{t0})));'
            f'const v={v1}*u;document.getElementById("{el}").textContent=v.toFixed({dec})+"{suffix}";}};const q=window.hookBusy;'
            f'window.hookBusy=function(t){{if(q&&q(t))return true;return t>={t0}-0.05&&t<={t1}+0.05;}};}})();</script>')


# ---------------- 場面の時刻 ----------------
class Timeline:
    def __init__(self, first=None):
        self.idx = load(first)
        self.total = layout(self.idx)

    def start(self, i):
        return self.idx[i]["start"] / FPS

    def speech(self, i):
        return self.idx[i]["speech"] / FPS

    def end(self, i):
        return (self.idx[i]["start"] + self.idx[i]["frames"]) / FPS


# ---------------- ドット絵のエイイチ ----------------
def fill_teeth(im):
    """顔の中の透明な小さい穴（歯）を白で埋める（v4 の差分は歯が透明）"""
    from scipy import ndimage as nd
    a = np.array(im.convert("RGBA"))
    holes = a[..., 3] < 128
    lab, n = nd.label(holes)
    edge = set(np.unique(np.concatenate([lab[0], lab[-1], lab[:, 0], lab[:, -1]]))) | {0}
    for k in range(1, n + 1):
        if k in edge:
            continue
        m = lab == k
        if m.sum() < 80:
            a[m] = (250, 250, 246, 255)
    return Image.fromarray(a)


class Eiichi:
    POSES = {1: "1_腕組み", 2: "2_うなずく", 3: "3_指さす", 4: "4_首をかしげる", 5: "5_ひらめく",
             6: "6_のけぞって驚く", 7: "7_半目でツッコむ", 8: "8_吹き出して笑う"}

    def __init__(self, folder="src/ei"):
        self.raw = {}
        for k, n in self.POSES.items():
            fs = [f for f in os.listdir(folder) if f.startswith(f"エイイチ_{n}_100px")]
            im = Image.open(os.path.join(folder, sorted(fs)[-1])).convert("RGBA")
            self.raw[k] = fill_teeth(im)
        self._cache = {}

    def sprite(self, pose, scale=4, glow=False, crop=1.0):
        key = (pose, scale, glow, crop)
        if key not in self._cache:
            im = self.raw[pose]
            im = im.resize((im.width * scale, im.height * scale), Image.NEAREST)
            if crop < 1.0:
                im = im.crop((0, 0, im.width, int(im.height * crop)))
            if glow:
                pad = 60
                g = Image.new("RGBA", (im.width + 2 * pad, im.height + 2 * pad), (0, 0, 0, 0))
                al = Image.new("L", g.size, 0); al.paste(im.split()[3], (pad, pad))
                al = al.filter(ImageFilter.GaussianBlur(28)).point(lambda v: int(v * 0.35))
                g = Image.merge("RGBA", (Image.new("L", g.size, 245), Image.new("L", g.size, 243), Image.new("L", g.size, 238), al))
                g.alpha_composite(im, (pad, pad))
                im = g
            self._cache[key] = (np.array(im).astype(np.float32), (-60 if glow else 0))
        return self._cache[key]


def blend(dst, src, x, y, alpha=1.0):
    """dst: H×W×3 uint8、src: h×w×4 float（0-255）を (x,y) に重ねる（はみ出しは切る）"""
    h, w = src.shape[:2]
    DH, DW = dst.shape[:2]   # 縦長（ショート）でも使えるように、重ね先の大きさで切る
    x0, y0, x1, y1 = max(0, x), max(0, y), min(DW, x + w), min(DH, y + h)
    if x0 >= x1 or y0 >= y1:
        return
    s = src[y0 - y:y1 - y, x0 - x:x1 - x]
    a = s[..., 3:4] / 255.0 * alpha
    d = dst[y0:y1, x0:x1].astype(np.float32)
    dst[y0:y1, x0:x1] = (d * (1 - a) + s[..., :3] * a + 0.5).clip(0, 255).astype(np.uint8)


def rotate_about(img, deg, pivot, pad=760):
    """img（PIL RGBA）を pivot（img 内の座標）のまわりに回す。返り値: (回した画像, img の左上からのずれ)"""
    c = Image.new("RGBA", (img.width + 2 * pad, img.height + 2 * pad), (0, 0, 0, 0))
    c.paste(img, (pad, pad))
    c = c.rotate(-deg, resample=Image.BICUBIC, center=(pad + pivot[0], pad + pivot[1]))
    bb = c.getbbox()
    if not bb:
        return c, (-pad, -pad)
    return c.crop(bb), (bb[0] - pad, bb[1] - pad)


def ease(u):
    u = min(1.0, max(0.0, u))
    return u * u * (3 - 2 * u)


# ---------------- 字幕 ----------------
SUBCSS = """*{margin:0;padding:0}html,body{width:1920px;height:1080px;background:transparent}
svg{position:absolute;left:0;top:0}
.t{font-family:'Noto Sans CJK JP',sans-serif;font-size:52px;font-weight:800;fill:#fff;stroke:#0D0D0D;stroke-width:11px;stroke-linejoin:round;stroke-linecap:round;paint-order:stroke fill}"""


def fill_holes(path, ink=(13, 13, 13), maxarea=400):
    from scipy import ndimage as _nd
    a = np.array(Image.open(path).convert('RGBA')).astype(np.float32)
    al = a[..., 3] / 255.0
    lab, n = _nd.label(al < 0.98)
    if not n:
        return
    edge = set(np.unique(np.concatenate([lab[0], lab[-1], lab[:, 0], lab[:, -1]]))) | {0}
    sz = _nd.sum(np.ones_like(al), lab, range(1, n + 1))
    keep = np.array([0] + [(k not in edge and sz[k - 1] <= maxarea) for k in range(1, n + 1)])
    m = keep[lab].astype(bool)
    if not m.any():
        return
    for c in range(3):
        a[..., c][m] = a[..., c][m] * al[m] + ink[c] * (1 - al[m])
    a[..., 3][m] = 255
    Image.fromarray(a.clip(0, 255).astype(np.uint8)).save(path)


def make_subs(idx, pg):
    """render.py と同じ字幕（中央・白・黒縁・2行まで）。png/sub/{番号}_{枚目}.png"""
    os.makedirs("png/sub", exist_ok=True)
    bad = []
    for o in idx:
        if not o["text"]:
            continue
        for pk, ptxt in enumerate(sub_pages(o["text"])):
            lines = split_lines(ptxt) if ptxt else None
            if lines is None:
                bad.append(f"字幕 {o['i']:03d}: 2行に割れない「{o['text']}」"); continue
            out = f"png/sub/{o['i']:03d}_{pk}.png"
            key = hashlib.md5(repr(lines).encode()).hexdigest()[:10]
            if os.path.exists(out) and os.path.exists(out + ".key") and open(out + ".key").read() == key:
                continue
            svg_lines = ''.join(f'<text x="{SUB_CX}" text-anchor="middle" class="t">{_html.escape(ln)}</text>' for ln in lines)
            pg.set_content(f"""<!doctype html><html><head><meta charset="utf-8"><style>{SUBCSS}</style></head><body>
<svg width="1920" height="1080" viewBox="0 0 1920 1080">{svg_lines}</svg></body></html>""")
            pg.evaluate(f"""() => {{
              const ts=[...document.querySelectorAll('text.t')]; const lh=72; const n=ts.length;
              let fs=52; for (const t of ts){{ const w=t.getComputedTextLength(); if (w>{SUB_MAXW}) fs=Math.min(fs, Math.floor(52*{SUB_MAXW}/w)); }}
              ts.forEach((t,k)=>{{ t.style.fontSize=fs+'px'; t.setAttribute('y', 1080-62-(n-1-k)*lh); }}); }}""")
            pg.screenshot(path=out, omit_background=True)
            fill_holes(out)
            open(out + ".key", "w").write(key)
    if bad:
        sys.exit("\n".join(bad))


def sub_layer(path):
    im = Image.open(path).convert("RGBA")
    bb = im.getbbox()
    return np.array(im.crop(bb)).astype(np.float32), bb[0], bb[1]


# ---------------- 紙の映像を撮る ----------------
def block_hash(b, n):
    return hashlib.md5((b.get("html", "") + repr(b.get("video")) + str(n)).encode()).hexdigest()[:10]


def render_block(b, n, out, pg=None):
    """b: dict(name, html | video=(映像, 開始秒, テロップpng)), n: コマ数 → out（mp4・crf 14）"""
    tmp = out[:-4] + ".part.mp4"
    if "video" in b:
        src, ss, ovl = b["video"]
        ins = ["-ss", f"{ss:.3f}", "-stream_loop", "-1", "-i", src]
        fc = f"[0:v]scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080,fps={FPS},setsar=1[b]"
        if ovl:
            ins += ["-loop", "1", "-i", ovl]
            fc += ";[b][1:v]overlay[v]"
        else:
            fc += ";[b]null[v]"
        subprocess.run(["ffmpeg", "-v", "error", "-y"] + ins + ["-filter_complex", fc, "-map", "[v]", "-frames:v", str(n),
                        "-c:v", "libx264", "-preset", "ultrafast", "-crf", "14", "-pix_fmt", "yuv420p", "-r", str(FPS), "-an", tmp], check=True)
        os.replace(tmp, out)
        return
    with open("_block.html", "w", encoding="utf-8") as f:
        f.write(b["html"])
    pg.goto("file://" + os.path.abspath("_block.html"))
    pg.evaluate("document.fonts.ready")
    pg.wait_for_timeout(300)
    p = subprocess.Popen(["ffmpeg", "-v", "error", "-y", "-f", "image2pipe", "-framerate", str(FPS), "-c:v", "mjpeg", "-i", "-",
                          "-c:v", "libx264", "-preset", "ultrafast", "-crf", "14", "-pix_fmt", "yuv420p", "-r", str(FPS), "-an", tmp],
                         stdin=subprocess.PIPE)
    last, shots = None, 0
    for f in range(n):
        t = f / FPS
        if last is None or pg.evaluate(f"window.busy({t})"):
            pg.evaluate(f"window.setT({t})")
            last = pg.screenshot(type="jpeg", quality=93); shots += 1
        p.stdin.write(last)
    p.stdin.close()
    if p.wait():
        sys.exit(f"紙の映像の書き出しに失敗: {b['name']}")
    os.replace(tmp, out)
    print(f"  {b['name']}: {n}コマ（撮影 {shots}）", flush=True)


def still(b, t, pg, path):
    with open("_block.html", "w", encoding="utf-8") as f:
        f.write(b["html"])
    pg.goto("file://" + os.path.abspath("_block.html"))
    pg.evaluate("document.fonts.ready")
    pg.wait_for_timeout(200)
    pg.evaluate(f"window.setT({t})")
    pg.screenshot(path=path, type="jpeg", quality=90)


# ================= 重ねる（ハコブくん・エイイチ・字幕） =================
class Cast:
    """フレームごとのハコブくんとエイイチの姿を決めて、背景に重ねる"""

    def __init__(self, sc, last_i):
        self.sc, self.idx = sc, sc.tl.idx[:last_i + 1]
        idx = self.idx
        self.total = idx[-1]["start"] + idx[-1]["frames"]
        ei_lines = set(_ei_lines())
        self.ei_lines = ei_lines
        pcm = narration(idx)
        self.pcm = pcm
        # --- ハコブくん ---
        self.hu = hk.Hakobu()
        self.hf = hk.Hakobu(mode="full")
        bb = self.hf.body.getbbox()
        self.hf.w = self.hu.w
        self.hf.h = round((bb[3] - bb[1]) * self.hu.w / (bb[2] - bb[0]))
        self.hf._cache = {}
        tline = [(o["start"], o["start"] + o["frames"], o["start"] + o["speech"], o.get("face", "base"),
                  bool(o["text"]) and o["i"] not in ei_lines) for o in idx]
        mp = pcm.astype(np.float32) / 32768.0
        speak = np.zeros(self.total, bool)
        eispk = np.zeros(self.total, bool)
        for o in idx:
            a0, a1 = int(o["start"] / FPS * SR), int((o["start"] + o["frames"]) / FPS * SR)
            if o["i"] in ei_lines:
                mp[a0:a1] = 0
                eispk[o["start"]:o["start"] + o["speech"]] = True
            elif o["text"]:
                speak[o["start"]:o["start"] + o["speech"]] = True
        self.params = self.hu.animate(tline, mp)
        for t0, t1, f in sc.HK_FACE:   # 文の途中で替える表情
            ex = hk.EXPR[f]
            for i in range(max(0, int(t0 * FPS)), min(self.total, int(t1 * FPS))):
                eyes, brows, mouth, ring, dy = self.params[i]
                if not speak[i]:
                    mouth = ex["rest"]
                elif mouth == "closed" and "closed" in ex:
                    mouth = ex["closed"]
                self.params[i] = (ex["eyes"], ex["brows"], mouth, ring, dy)
        # --- エイイチ: 声に合わせて 4px（ドット1つ）弾む ---
        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(self.total)])
        ref = np.percentile(rms[rms > 0.01], 85) if (rms > 0.01).any() else 1.0
        bob = np.zeros(self.total, int)
        for i in range(0, self.total, 3):
            v = -4 if rms[i:i + 3].mean() > 0.45 * ref else 0
            bob[i:i + 3] = v
        self.bob = bob
        self.ei = Eiichi()
        self.subs = {}

    def ei_state(self, f):
        t = f / FPS
        pose = 1
        for a, p in self.sc.EI_POSE:
            if t >= a:
                pose = p
        place = "normal"
        for a, b, k in self.sc.EI_PLACE:
            if a <= t < b:
                place, pa, pb = k, a, b
        x, y, scale, glow, crop = 1490, 680, 4, False, 1.0
        if place == "hide":
            return None
        if place == "big":
            scale, glow, x, y = 5, True, 1920 - 60 - 500, 1080 - 500
        elif place == "buried":
            scale, crop = 3, 0.62
            x, y = 840, 870 - int(300 * 0.62)   # 紙の山の真ん中の黒い穴から顔を出す（字幕より上）
        elif place == "slide":
            pose = 3
            go = ease((t - pa) / 0.7)
            back = ease((t - (pb - 0.5)) / 0.5)
            x = int(1490 + (520 - 1490) * go + (1490 - 520) * back)
            x -= x % 4
        y += self.bob[f] if place in ("normal", "slide") else 0
        return pose, x, y, scale, glow, crop

    def hk_state(self, f):
        """→ (体, dx, dy, 回す角度, 回す中心) 体は upper / full"""
        t = f / FPS
        for m in self.sc.HK_MOVES:
            u = t - m["t0"]
            if m["kind"] == "jump" and 0 <= u < m["dur"]:
                y0 = self.hu.y
                y_peak = H - self.hf.h - 30
                if u < 0.32:
                    k = np.sin(u / 0.32 * np.pi / 2) * 1.12
                elif u < 0.5:
                    k = 1.12 - 0.12 * ease((u - 0.32) / 0.18)
                elif u < m["dur"] - 0.45:
                    k = 1.0 + 0.02 * np.sin((u - 0.5) * 6)
                else:
                    k = 1.0 - ease((u - (m["dur"] - 0.45)) / 0.45)
                ytop = y0 + (y_peak - y0) * k
                rot = -7 * min(1, k) + 2.5 * np.sin(u * 5) * min(1, k)
                return "full", 30 * min(1, k), ytop - self.hf.y, rot, (self.hf.w / 2, self.hf.h)
            if m["kind"] == "reflex" and 0 <= u < m["dur"]:
                e = ease(u / 0.1) * (1 - ease((u - 0.25) / (m["dur"] - 0.25)))
                # 上半身の絵を回すと、下の切り口が斜めに見える（2026-11-02 エイイチの指摘）。全身の絵を、上半身と同じ位置に置いて回す
                return "full", -24 * e, self.hu.y - self.hf.y, -16 * e, (0, self.hf.h)
            if m["kind"] == "fall":   # 全身を出してから、前のめりに倒れる（体が切れないように。2026-11-02 エイイチの指摘）
                y0 = self.hu.y
                ys = H - 14 - self.hf.h          # 全身が見える位置（足もとが画面の下から14px）
                piv = (self.hf.w * 0.95, self.hf.h)
                t1 = m["t1"]
                if 0 <= u < 0.18:
                    k = ease(u / 0.18)
                    return "full", 0, y0 + (ys - y0) * k - self.hf.y, -6 * k, piv
                if 0.18 <= u < 0.5:
                    r = -6 + 98 * ease((u - 0.18) / 0.32)
                    return "full", 0, ys - self.hf.y, r, piv
                if u >= 0.5 and t < t1:
                    r = 88 + 4 * np.exp(-(u - 0.5) * 8) * np.cos((u - 0.5) * 28)
                    return "full", 0, ys - self.hf.y, r, piv
                if t1 <= t < t1 + 0.45:
                    return "full", 0, ys - self.hf.y, 88 * (1 - ease((t - t1) / 0.45)), piv
                if t1 + 0.45 <= t < t1 + 0.75:
                    k = ease((t - t1 - 0.45) / 0.3)
                    return "full", 0, ys + (y0 - ys) * k - self.hf.y, 0, piv
        return "upper", 0, 0, 0, None

    def hk_alpha(self, f):
        t = f / FPS
        for a, b, al in self.sc.HK_ALPHA:
            if a <= t < b:
                return al
        return 1.0

    def sub_at(self, f):
        for o in self.idx:
            if o["start"] <= f < o["start"] + o["frames"]:
                if not o["text"]:
                    return None
                r = f - o["start"]
                for k, a, b in o["parts"]:
                    if a <= r < b:
                        key = (o["i"], k if k is not None else 0)
                        if key not in self.subs:
                            self.subs[key] = sub_layer(f"png/sub/{key[0]:03d}_{key[1]}.png")
                        return self.subs[key]
        return None

    def draw(self, bg, f):
        """bg: H×W×3 uint8（書き換える）"""
        st = self.ei_state(f)
        if st:
            pose, x, y, scale, glow, crop = st
            spr, off = self.ei.sprite(pose, scale, glow, crop)
            blend(bg, spr, x + off, y + off)
        body, dx, dy, rot, pivot = self.hk_state(f)
        hh = self.hu if body == "upper" else self.hf
        img = hh.frame(self.params[f])
        x, y = hh.x + int(round(dx)), hh.y + int(round(dy))
        if rot:
            img, (ox, oy) = rotate_about(img, rot, pivot)
            x, y = x + ox, y + oy
        blend(bg, np.asarray(img).astype(np.float32), x, y, self.hk_alpha(f))
        s = self.sub_at(f)
        if s is not None:
            blend(bg, s[0], s[1], s[2])
        return bg


def _ei_lines():
    import importlib.util as ilu
    sp = ilu.spec_from_file_location("_narr_here", os.path.join(os.getcwd(), "narration.py"))
    nm = ilu.module_from_spec(sp); sp.loader.exec_module(nm)
    return list(getattr(nm, "EI_LINES", []))


def prepare(sc, blocks, pg):
    os.makedirs("png/ovl", exist_ok=True); os.makedirs("blocks", exist_ok=True)
    for k, h in sc.OVERLAYS.items():
        pg.set_content(h); pg.wait_for_timeout(80)
        pg.screenshot(path=f"png/ovl/{k}.png", omit_background=True)
    last_i = blocks[-1]["sents"][-1]
    make_subs(sc.tl.idx[:last_i + 1], pg)


def block_frames(sc, b):
    idx = sc.tl.idx
    a = idx[b["sents"][0]]["start"]
    e = idx[b["sents"][-1]]["start"] + idx[b["sents"][-1]]["frames"]
    return a, e - a


def main():
    args = sys.argv[1:]
    cmd = args[0] if args else "sheet"
    import importlib
    sc = importlib.import_module("scenes_kabe")
    from playwright.sync_api import sync_playwright
    blocks = sc.BLOCKS
    if "--first-blocks" in args:
        blocks = blocks[:int(args[args.index("--first-blocks") + 1])]
    names = [a for a in args[1:] if not a.startswith("--") and not a.isdigit()]
    with sync_playwright() as p:
        br = p.chromium.launch()
        pg = br.new_page(viewport={"width": W, "height": H})
        prepare(sc, blocks, pg)
        if cmd == "sheet":
            cast = Cast(sc, blocks[-1]["sents"][-1])
            os.makedirs("png/sheet", exist_ok=True)
            shots = []
            for b in blocks:
                if names and b["name"] not in names:
                    continue
                a, n = block_frames(sc, b)
                ts = sc.SHEET.get(b["name"], [n / FPS - 0.4])
                for k, t in enumerate(ts):
                    f = min(a + int(t * FPS), a + n - 1)
                    path = f"png/sheet/{b['name']}_{k}.jpg"
                    if "video" in b:
                        src, ss, ovl = b["video"]
                        dur = float(subprocess.check_output(["ffprobe", "-v", "quiet", "-show_entries", "format=duration", "-of", "csv=p=0", src]))
                        subprocess.run(["ffmpeg", "-v", "error", "-y", "-ss", f"{(ss + t) % (dur - 0.2):.2f}", "-i", src, "-frames:v", "1",
                                        "-vf", "scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080", path], check=True)
                        im = Image.open(path).convert("RGBA")
                        im.alpha_composite(Image.open(ovl).convert("RGBA"))
                        im.convert("RGB").save(path)
                    else:
                        still(b, t, pg, path)
                    bg = np.array(Image.open(path).convert("RGB"))
                    Image.fromarray(cast.draw(bg, f)).save(path, quality=88)
                    shots.append((b["name"], k, f / FPS, path))
            # 一覧
            cols, tw = 4, 480
            th = tw * 9 // 16
            rows = (len(shots) + cols - 1) // cols
            from PIL import ImageDraw, ImageFont
            fnt = ImageFont.truetype("/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc", 22)
            sheet = Image.new("RGB", (cols * tw, rows * (th + 34)), (240, 238, 232))
            d = ImageDraw.Draw(sheet)
            for j, (nm, k, t, path) in enumerate(shots):
                x, y = (j % cols) * tw, (j // cols) * (th + 34)
                sheet.paste(Image.open(path).resize((tw - 6, th - 4)), (x + 3, y + 32))
                d.text((x + 8, y + 4), f"{nm}  {int(t // 60)}:{t % 60:04.1f}", font=fnt, fill=(30, 30, 30))
            sheet.save("一覧_場面.jpg", quality=85)
            print("一覧_場面.jpg", len(shots))
        if cmd in ("render", "build"):
            for b in blocks:
                if names and b["name"] not in names:
                    continue
                a, n = block_frames(sc, b)
                out = f"blocks/{b['name']}_{block_hash(b, n)}.mp4"
                b["mp4"] = out
                if not os.path.exists(out):
                    render_block(b, n, out, pg)
        br.close()
    if cmd != "build":
        return
    for b in blocks:
        a, n = block_frames(sc, b)
        b["mp4"] = f"blocks/{b['name']}_{block_hash(b, n)}.mp4"
    last_i = blocks[-1]["sents"][-1]
    cast = Cast(sc, last_i)
    idx = cast.idx
    write_wav("narration.wav", cast.pcm)
    audio_in = "narration.wav"
    if os.path.exists("sound.py"):
        from audio_mix import mix
        audio_in = mix(idx, cast.pcm)
    total = cast.total
    enc = subprocess.Popen(["ffmpeg", "-v", "error", "-y", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{W}x{H}", "-r", str(FPS), "-i", "-",
                            "-i", audio_in, "-map", "0:v", "-map", "1:a", "-c:v", "libx264", "-preset", "medium", "-crf", "20", "-pix_fmt", "yuv420p",
                            "-af", "loudnorm=I=-16:TP=-1.5:LRA=11", "-c:a", "aac", "-b:a", "192k", "-ar", "48000",
                            "-movflags", "+faststart", "-frames:v", str(total), "out.mp4"], stdin=subprocess.PIPE)
    f = 0
    for b in blocks:
        a, n = block_frames(sc, b)
        dec = subprocess.Popen(["ffmpeg", "-v", "error", "-i", b["mp4"], "-f", "rawvideo", "-pix_fmt", "rgb24", "-"], stdout=subprocess.PIPE)
        for k in range(n):
            raw = dec.stdout.read(W * H * 3)
            if len(raw) < W * H * 3:
                sys.exit(f"紙の映像が短い: {b['name']} {k}/{n}")
            bg = np.frombuffer(raw, np.uint8).reshape(H, W, 3).copy()
            enc.stdin.write(cast.draw(bg, a + k).tobytes())
            f += 1
            if f % 900 == 0:
                print(f"  {f}/{total}", flush=True)
        dec.stdout.close(); dec.wait()
    enc.stdin.close()
    if enc.wait():
        sys.exit("書き出しに失敗")
    # 字幕 SRT
    def ts(fr):
        ms = round(fr / FPS * 1000); h_, ms = divmod(ms, 3600000); m_, ms = divmod(ms, 60000); s_, ms = divmod(ms, 1000)
        return f"{h_:02d}:{m_:02d}:{s_:02d},{ms:03d}"
    srt, k = [], 1
    for o in idx:
        if not o["text"]:
            continue
        pages_ = sub_pages(o["text"])
        for (pk, a_, b_), ptxt in zip(o["parts"], pages_):
            end = min(b_, o["speech"]) if pk == len(pages_) - 1 else b_
            srt.append(f"{k}\n{ts(o['start'] + a_)} --> {ts(o['start'] + end)}\n{ptxt}\n"); k += 1
    open("out.srt", "w", encoding="utf-8").write("\n".join(srt))
    json.dump(idx, open("timeline.json", "w"), ensure_ascii=False)
    for crf in (29, 32, 35):
        subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", "out.mp4", "-c:v", "libx264", "-preset", "medium", "-crf", str(crf),
                        "-c:a", "aac", "-b:a", "112k", "-movflags", "+faststart", "out_確認用.mp4"], check=True)
        mb = os.path.getsize("out_確認用.mp4") / 1e6
        if mb <= 30:
            break
    print(f"frames {total} = {total / FPS:.1f}s  確認用 {mb:.1f}MB (crf {crf})")


if __name__ == "__main__":
    main()
```

### kabe_short.py（縦長ショートの版2）

```python
# -*- 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'<div class="vhead"><img src="{LOGO}"><div class="q">{head}</div></div>', VCSS)


def bg(img, size_h, pos_x, pos_y, t_veil):
    p = "file://" + os.path.abspath(img)
    return (f'<div class="bgimg" style="background-image:url(\'{p}\');background-size:auto {size_h}px;background-position:{pos_x}px {pos_y}px"></div>'
            f'<div style="position:absolute;inset:0;background:#141311;opacity:0;animation:veil .5s ease {t_veil:.3f}s forwards"></div>'
            '<div style="position:absolute;right:100px;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>')


def paper(inner, t_in):
    return f'<div class="vpaper" style="animation:pIn .45s cubic-bezier(.2,1.2,.4,1) {t_in:.3f}s forwards">{inner}</div>'


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'<div class="vstack" style="opacity:0;animation:{an}">{inner}</div>'


def line(text, t, size=40, weight=700, step=0.05, style=""):
    return f'<div style="{SANS}font-size:{size}px;font-weight:{weight};text-align:center;{style}">{ink(text, t, step)}</div>'


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'<span style="color:{col};animation:boom .32s cubic-bezier(.2,1.8,.4,1) {t0 + gap * k:.3f}s forwards, hide .2s linear {t_out:.3f}s forwards">{txt}</span>')
    return f'<div class="vbig" style="top:{top}px;font-size:{size}px;transform:rotate(-4deg)">{"<br>".join(out)}</div>'


def bigword(text, t, t_out, top, size=300, color="#c4472a", rot=-8):
    return (f'<div class="big" style="top:{top}px;font-size:{size}px;color:{color};-webkit-text-stroke:10px #fff;paint-order:stroke fill;'
            f'text-shadow:0 14px 0 #141311;transform:rotate({rot}deg);z-index:6"><span style="animation:boom .3s cubic-bezier(.2,1.8,.4,1) {t:.3f}s forwards, '
            f'hide .2s linear {t_out:.3f}s forwards">{text}</span></div>')


WHITE, GOLD = "#ffffff", "#ffd84a"


# ====================== 3本の中身（T(k, 秒) は k 番目の文の頭から。k=-1 は動画の頭） ======================
def short_uchu(T, S):
    tp = T(2, 0.0)   # 「宇宙は、今日は置いておきます」で、絵を暗くして紙を出す
    return vpage("ロボットのAI、<br>考えるのに18.6秒", (
        bg("src/bg/宇宙.jpg", 1920, -1500, 0, tp)
        # 誰の言葉かを先に見せる（2026-11-02 エイイチ「イーロン・マスク氏の話を伝えないと何か分からない」）
        + f'<div style="position:absolute;left:0;right:0;top:470px;text-align:center;z-index:6"><span style="display:inline-block;{SANS}background:#141311;color:#fff;'
          f'font-size:46px;font-weight:700;padding:10px 30px;opacity:0;animation:pop .35s ease-out 0.15s forwards, hide .2s linear {tp:.3f}s forwards">イーロン・マスクさんの X の投稿</span></div>'
        + words([("超知能の", WHITE), ("ほぼ100%は", WHITE), ("深宇宙で計算", GOLD)], T(0, 0.3), tp, 570, size=120, gap=0.9)
        + paper(grp(f'<div style="{SANS}font-size:32px;letter-spacing:.2em;">ハコブくん　考え中</div>'
                    + f'<div class="num" style="font-size:200px;line-height:1"><span id="cnt">0.0</span>{land("秒", T(4, 0.05), "font-size:84px")}</div>'
                    + 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本、<br>捨てられない？", (
        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")
                    + '<div class="row2" style="gap:24px">'
                    + card("fact", "記事の全文", T(3, 0.4), "海馬", w=330)
                    + card("fact", "整理した記事", T(3, 2.3), "大脳", w=330)
                    + '</div>', tp + 0.1), tp)))


def short_intai(T, S):
    tp = T(2, 0.0)   # 本題（何にでも使える大きなAIが…）に入ったら紙を出す
    return vpage("π0.5 は、<br>もう引退？", (
        bg("src/bg/引退式.jpg", 2300, -1933, 230, tp)
        + f'<div class="big" style="top:686px;font-size:104px;color:#141311;-webkit-text-stroke:0;text-shadow:none;transform:none">'
          f'<span style="animation:boom .35s cubic-bezier(.2,1.8,.4,1) 0.2s forwards, hide .2s linear {tp:.3f}s forwards">祝・引退</span></div>'
        + 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'<div class="serif" style="font-size:52px;font-weight:700">{ink("分けるしかない", T(4, 0.3), 0.07)}</div>'
                      + '<div class="row2" style="gap:30px">'
                      + f'<div class="card k-fact" style="width:300px;opacity:0;animation:pop .45s ease-out {T(4, 2.4):.3f}s forwards"><small>遅く考える</small><span class="serif" style="font-size:110px;font-weight:900;line-height:1.2">頭</span></div>'
                      + f'<div class="card k-fact" style="width:300px;opacity:0;animation:pop .45s ease-out {T(4, 3.6):.3f}s forwards"><small>速く直す</small><span class="serif" style="font-size:110px;font-weight:900;line-height:1.2">手</span></div>'
                      + '</div>', 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'<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"{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)
```

### sound.py（BGM と章の効果音。効果音の時刻は scenes_kabe.py の SE）

```python
# 音の置き方（audio_mix.py が読む）。効果音の時刻は scenes_kabe.py の SE に書く（場面の動きと同じ所で決めるため）
# 考察の場面は静かに、脱線の場面は効果音を大きめに（考察回の型）
from scenes_kabe import SE as _SE

BGM = [
    dict(file='sound/bgm_明るいB_2.mp3', start=0, end=None, level=-26, gap=-19, fade_in=0.6, fade_out=3.0),   # いつもの曲（ElevenLabs Music）を、考察回なのでいつもより3dB小さく
]

SE = list(_SE)

CHAPTER_SE = dict(file='se_chapter_marimba', files={'title': 'se_chapter_glock'}, level=-10, offset=-0.3,
                  scenes=['title', 'bring', 'think', 'split', 'fail_ei', 'log', 'sum', 'outro'])
```

### gemini_bg.py（Gemini の画像の API で、脱線の絵・全身のドット絵・サムネイルの背景を作る）

```python
#!/usr/bin/env python3
"""考察回の脱線の場面の背景を Gemini の画像の API で作る（2026-11-02）
使い方: python3 gemini_bg.py <プロンプト.txt> <出力の頭> [札=参照画像のパス ...]
16:9。鍵は KIOXIA/gemini_api_key.txt から読むだけ（表示しない）。もとは 4コマの道具 gemini_gen_labeled.py"""
import sys, json, base64, time, urllib.request, urllib.error, mimetypes, os, datetime
MODEL = "gemini-3.1-flash-image"
SIZE = os.environ.get("BG_SIZE", "2K")
KEY_PATHS = [os.path.expanduser("~/mnt/KIOXIA/gemini_api_key.txt"), "/Volumes/KIOXIA/gemini_api_key.txt"]
key = next(open(p).read().strip() for p in KEY_PATHS if os.path.exists(p))
prompt_path, out_head, refs = sys.argv[1], sys.argv[2], sys.argv[3:]
parts = []
for r in refs:
    label, path = r.split("=", 1)
    parts.append({"text": f"Reference image — {label}:"})
    parts.append({"inlineData": {"mimeType": mimetypes.guess_type(path)[0] or "image/png", "data": base64.b64encode(open(path, "rb").read()).decode()}})
parts.append({"text": open(prompt_path, encoding="utf-8").read()})
body = {"contents": [{"role": "user", "parts": parts}],
        "generationConfig": {"responseModalities": ["IMAGE"], "imageConfig": {"aspectRatio": os.environ.get("BG_ASPECT", "16:9"), "imageSize": SIZE}}}
req = urllib.request.Request(f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL}:generateContent",
        data=json.dumps(body).encode(), headers={"Content-Type": "application/json", "x-goog-api-key": key})
t = time.time()
try:
    res = json.load(urllib.request.urlopen(req, timeout=300))
except urllib.error.HTTPError as e:
    print("失敗", e.code, e.read().decode()[:800]); sys.exit(1)
el = time.time() - t
stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
img = None
for c in res.get("candidates", []):
    for p in c.get("content", {}).get("parts", []):
        if "inlineData" in p: img = p["inlineData"]
ext = ".jpg" if img and "jpeg" in img["mimeType"] else ".png"
base = f"{out_head}_{stamp}"
if img: open(base + ext, "wb").write(base64.b64decode(img["data"]))
for c in res.get("candidates", []):
    for p in c.get("content", {}).get("parts", []): p.pop("inlineData", None)
json.dump({"model": MODEL, "size": SIZE, "elapsed_s": round(el, 2), "prompt": prompt_path, "refs": refs, "response": res}, open(base + ".json", "w", encoding="utf-8"), ensure_ascii=False, indent=1)
u = res.get("usageMetadata", {})
print(os.path.basename(base + ext) if img else "画像なし", f"{el:.1f}秒", u.get("promptTokenCount"), u.get("candidatesTokenCount"), res.get("candidates", [{}])[0].get("finishReason"))
```

### 脱線の絵のプロンプト：宇宙（宇宙_プロンプト.txt）

```text
Photorealistic wide cinematic photograph, 16:9 landscape.
A colossal AI data center floating in deep space: a huge ring-shaped station packed with rows of glowing server racks, cooling fins and tiny blinking status lights, seen at a dramatic angle.
A giant blue planet fills the upper right, a dense star field and a faint purple nebula behind.
The mood is awe-inspiring and slightly absurd, like a movie poster that went too far.
Leave generous empty starry space in the center and left of the frame so that large white text can be placed over it. One continuous seamless photograph — no bands, no stripes, no split panels, no hard horizontal edges.
No text, no letters, no numbers, no logos, no people, no astronauts.
```

### 脱線の絵のプロンプト：引退式（引退式_プロンプト.txt・ハコブくんの正面と横の絵を参考に渡した）

```text
Black and white Japanese manga illustration, one wide 16:9 panel, clean ink line art with screentone, the artwork fills the whole image with no panel border and no frame line.

USE THE ATTACHED REFERENCE for the robot "Hakobu" — match its body shape, screen face, wheels, line weight and screentone exactly. Hakobu is a hospital delivery robot: a rounded box body with a screen for a face. Hakobu has NO arms and NO hands.

Scene: a retirement ceremony on a hospital break-room table.
- On the table stands a small white 6-axis desktop robot arm. It holds a bouquet of flowers in its gripper, slightly drooping, as if bowing in thanks.
- Above it hangs a paper garland and a blank ribbon banner with NO letters on it. Confetti falls.
- Hakobu stands beside the table, deeply moved: its screen face shows big tear drops streaming down and a wobbly happy smile. Small sparkle marks around it.
- Comedic, heartwarming, deliberately over-the-top, like a farewell party for a coworker.

No text, no letters, no numbers, no speech bubbles anywhere in the image. No people.
```

### 脱線の絵のプロンプト：割れたコップ（コップ_プロンプト.txt）

```text
Photorealistic high-speed photograph, 16:9 landscape.
A clear drinking glass shattering on a polished pale hospital corridor floor, water splashing up in a frozen crown shape, glass shards suspended in mid-air, every droplet sharp.
Low camera angle close to the floor, shallow depth of field, the corridor softly blurred behind.
Cool clinical lighting with one warm accent light, dramatic slow-motion feel, a little comedic in its seriousness.
Keep the upper center area relatively plain so text can be placed over it.
No people, no robots, no text, no letters, no logos.
```

### 脱線の絵のプロンプト：紙の山（紙の山_プロンプト.txt）

```text
Photorealistic wide photograph, 16:9 landscape.
A small Japanese home office completely buried under mountains of printed paper documents: towers of stacked A4 sheets reaching the ceiling, leaning piles, sheets sliding off and scattered across the floor, binders and sticky notes everywhere. A desk with a laptop and a small white robot arm is barely visible under the paper.
In the lower center, leave a clear dark gap in the paper pile, as if someone has just been buried there and could pop their head out.
Warm evening desk-lamp light mixed with cool light from a window, slightly comedic exaggeration, like a magazine photo of a hoarder of documents.
Keep the upper center area calm so text can be placed over it.
No people, no faces, no hands, no readable text on any paper, no logos.
```

### 作り手の全身のドット絵のプロンプト1：立つ（f1.txt）

```text
Pixel art, FULL BODY from head to shoes, the SAME character as the first attached reference image (the pixel-art bust): same face, same black messy hair with bangs, same thick black rectangular glasses, same warm orange skin tone with reddish-brown shading, same chunky pixel size, same limited color palette, same black outlines, no anti-aliasing. Outfit exactly as the person in the second attached photo: a loose dark charcoal gray tailored jacket, a black crew-neck T-shirt, loose wide black trousers, and light gray-white chunky sneakers with lavender-blue accents. Standing, the whole figure centered and fully visible with a small margin, feet at the bottom. Plain flat pure white background. No text, no letters, no shadow on the ground.
Pose and expression: relaxed standing pose with both hands in the trouser pockets, a calm slight smile, facing the viewer.
```

### 作り手の全身のドット絵のプロンプト2：ツッコミの手つき（f2.txt・出てきた絵は腕組み）

```text
Pixel art, FULL BODY from head to shoes, the SAME character as the first attached reference image (the pixel-art bust): same face, same black messy hair with bangs, same thick black rectangular glasses, same warm orange skin tone with reddish-brown shading, same chunky pixel size, same limited color palette, same black outlines, no anti-aliasing. Outfit exactly as the person in the second attached photo: a loose dark charcoal gray tailored jacket, a black crew-neck T-shirt, loose wide black trousers, and light gray-white chunky sneakers with lavender-blue accents. Standing, the whole figure centered and fully visible with a small margin, feet at the bottom. Plain flat pure white background. No text, no letters, no shadow on the ground.
Pose and expression: a comedic retort (tsukkomi): body turned slightly to the left, one hand thrust out to the left with the back of the hand facing out as if lightly hitting someone, eyes half-closed in an unimpressed look, a wry lopsided smile.
```

### 作り手の全身のドット絵のプロンプト3：驚く（f3.txt）

```text
Pixel art, FULL BODY from head to shoes, the SAME character as the first attached reference image (the pixel-art bust): same face, same black messy hair with bangs, same thick black rectangular glasses, same warm orange skin tone with reddish-brown shading, same chunky pixel size, same limited color palette, same black outlines, no anti-aliasing. Outfit exactly as the person in the second attached photo: a loose dark charcoal gray tailored jacket, a black crew-neck T-shirt, loose wide black trousers, and light gray-white chunky sneakers with lavender-blue accents. Standing, the whole figure centered and fully visible with a small margin, feet at the bottom. Plain flat pure white background. No text, no letters, no shadow on the ground.
Pose and expression: leaning back in big surprise, eyes wide open behind the glasses, eyebrows raised high, mouth open in an O shape, both hands raised with palms open, one foot slightly lifted.
```

### 作り手の全身のドット絵のプロンプト4：笑う（f4.txt）

```text
Pixel art, FULL BODY from head to shoes, the SAME character as the first attached reference image (the pixel-art bust): same face, same black messy hair with bangs, same thick black rectangular glasses, same warm orange skin tone with reddish-brown shading, same chunky pixel size, same limited color palette, same black outlines, no anti-aliasing. Outfit exactly as the person in the second attached photo: a loose dark charcoal gray tailored jacket, a black crew-neck T-shirt, loose wide black trousers, and light gray-white chunky sneakers with lavender-blue accents. Standing, the whole figure centered and fully visible with a small margin, feet at the bottom. Plain flat pure white background. No text, no letters, no shadow on the ground.
Pose and expression: bursting out laughing, eyes squinted shut with joy, shoulders raised, one hand covering the mouth, the other hand on the stomach, leaning forward slightly.
```

### サムネイルの背景のプロンプト：最初の案・写真のような脳（脳と腕_プロンプト.txt）

```text
A photorealistic, cinematic photograph for a YouTube thumbnail background. On the right third of the frame: a realistic anatomical model of a human brain seen from the side, with the cerebrum (large upper part) lit in warm amber light and the cerebellum (small lower back part) glowing in soft cyan light, so the two parts are clearly different. Next to it, a small white desktop robot arm reaching toward it. Dark charcoal background with soft studio lighting and slight depth of field. The left two-thirds of the image and the bottom quarter must be calm, dark, empty space (for text added later). Seamless single photograph, no bands or seams. No text, no letters, no numbers, no logos, no people, no watermark. 16:9.
```

### サムネイルの背景のプロンプト：つるっとした立体（脳_光_プロンプト.txt）

```text
A sleek, minimal 3D render for a YouTube thumbnail background, in the style of a modern tech product illustration (smooth matte plastic, not realistic, no anatomical detail, nothing fleshy or wet). On the right third: a smooth, simplified brain shape made of soft rounded matte material, split into two clearly separate pieces with a small gap between them: the large upper piece (cerebrum) in warm amber orange, the small lower back piece (cerebellum) in bright cyan, each softly glowing. Next to it, a small white robot arm in the same smooth matte style. Dark charcoal background with soft studio light. The left two-thirds and the bottom quarter must be empty, calm, dark space for text added later. No text, no letters, no numbers, no logos, no people, no watermark. 16:9.
```

### サムネイルの背景のプロンプト：平面（脳_平面_プロンプト.txt）

```text
A clean, friendly flat graphic illustration for a YouTube thumbnail background (not photorealistic, no anatomical detail, no wet or fleshy texture). On the right third: a simple, stylized brain icon seen from the side, drawn with smooth rounded shapes and only a few soft curved lines, made of two clearly separated parts: the large upper part (cerebrum) in warm amber orange, the small lower back part (cerebellum) in bright cyan. Thin soft glow around the shape. Next to it, a simple white robot arm drawn in the same flat style. Deep charcoal navy background with a subtle gradient. The left two-thirds and the bottom quarter must be empty, calm, dark space for text added later. No text, no letters, no numbers, no logos, no people, no watermark. 16:9.
```

### サムネイルの背景のプロンプト：右下に大きく（脳_光2_プロンプト.txt）

```text
A sleek, minimal 3D render for a YouTube thumbnail background, in the style of a modern tech product illustration (smooth matte plastic, not realistic, no anatomical detail, nothing fleshy or wet). Composition: the subject is LARGE and placed in the LOWER-RIGHT part of the frame, occupying the right 45% of the width and reaching down close to the bottom edge. The subject: a smooth, simplified brain shape made of soft rounded matte material, split into two clearly separate pieces with a small gap between them: the large upper piece (cerebrum) in warm amber orange, the small lower back piece (cerebellum) in bright cyan, each softly glowing. A small white robot arm stands at the far right, its gripper pointing at the cyan piece, fully inside the frame. Dark charcoal background with soft studio light. The left 55% of the image must be empty, calm, dark space for text added later. No text, no letters, no numbers, no logos, no people, no watermark. 16:9.
```

### thumb_A.py（サムネイル A 案の文言と背景）

```python
# サムネイル A 案（2026-11-02）: 背景は Gemini で作った「脳の模型とロボットアーム」（サムネ素材/脳と腕.jpg）
PHOTO = 'サムネ素材/脳と腕.jpg'
PHOTO_POS_FULL = '50% 50%'
HAKOBU = True
KICKER = '考察回'
LINES = ['ロボットの頭脳は', '<span class="red">脳のように</span>', '分かれる？']
LABEL = '考えるのに18.6秒'
VARIANTS = ['photo4']
SIZE_L1, SIZE_L2, SIZE_L3 = 120, 190, 150
TXT_TOP = 60
HK_H = 560
LABEL_BOTTOM = 250
```

### thumb_B.py（サムネイル B 案の文言と背景。この案を、背景を「右下に大きく」に替えて使った）

```python
# サムネイル B 案（2026-11-02）: タイトルの後ろ半分をそのまま大きく。大脳＝暖色、小脳＝水色（背景の模型の光と同じ色）
PHOTO = 'サムネ素材/脳と腕.jpg'
PHOTO_POS_FULL = '50% 50%'
HAKOBU = True
KICKER = 'ロボットの頭脳は、脳のように分かれる？'
LINES = ['大きなAIは<span style="color:#F5A04A">大脳</span>', '専用AIは<span style="color:#5BD8F0">小脳</span>', '']
LABEL = '僕はどっち？'
VARIANTS = ['photo4']
SIZE_L1, SIZE_L2, SIZE_L3 = 150, 150, 10
TXT_TOP = 60
HK_H = 560
LABEL_BOTTOM = 250
```

### 概要欄の下書き（概要欄_下書き.md）

```markdown
<!--
YouTube の概要欄の下書き（2026-11-02「頭脳は脳のように分かれる」＝考察回「壁打ちの型」の1本目・もとは LOG424）。{t:文番号} は、その文が始まる時間に置きかわる（文番号は 0 から数える）。
python3 <_制作ベース>/description.py で 概要欄.txt を作り、それを貼る。
-->
ロボットを動かすAIが1回考えるのに、専用のAIは0.05秒、何にでも使える大きなAIは18.6秒。エイイチが持ち込んだ話に、案内役のハコブくんが付き合う「考察回」です。答えを伝える回ではなく、考えている途中をそのまま見せます。人の脳が大脳・小脳・反射に分かれているように、ロボットのAIも分かれていくのか。速い手を育てるのは何か。

詳しい数字と考えた道すじは、サイトの記事にまとめています。
記事：ロボットのAIは脳のように分業する ― 小脳を育てるのは失敗の手直し https://pixelstomotors.com/record/robot-ai-divides-like-brain

▼目次
0:00 0.05秒と、18.6秒
{t:6} この番組について
{t:10} 持ち込み：ロボット専用の頭脳は、いらなくなる？
{t:21} やり取り1：遅い頭
{t:36} 人の脳も、分かれている
{t:49} やり取り2：失敗の手直し
{t:59} やり取り3：記憶の整理
{t:66} まとめ
{t:69} 残った問い
{t:74} エンディング

▼数字の出どころ
・0.05秒・18.6秒・11〜19秒（使い方による違い）：NVIDIA「Astranomy」（2026年10月14日）。π0.5 は 46.96ミリ秒、OpenAI の GPT-6 Astra は使い方によって 11.50秒〜18.56秒 https://research.nvidia.com/labs/srl/projects/astranomy/
・370回ほど：18.6秒 ÷ 0.05秒 で出した目安です
・海外の投資家の書き込み：Robonomics（Freda Duan）「Robotics' New Era After Astra」 https://robonomics.substack.com/p/robotics-new-era-after-astra ／ X の投稿 https://x.com/fredaduan/status/2116727410972590380
・イーロン・マスクさんの投稿（"~100% of SI will be deep space compute. Obviously." を訳しました） https://x.com/elonmusk/status/2114772074438988256
・小脳の神経細胞が、大脳の表面（大脳皮質）の4倍ほど：Suzana Herculano-Houzel の研究 https://www.frontiersin.org/journals/neuroanatomy/articles/10.3389/fnana.2010.00012/full ／ インタビュー https://intelligence.org/2014/04/22/suzana-herculano-houzel/

▼この動画について
・エイイチのセリフは、記事を書く前のやり取りで本人が話した言葉を、話し言葉に整えたものです。短い相づちやツッコミの一部は、本人が了承した案です
・「大きなAIは、賢くなるほど考える時間が長くなる方向に進んでいる」は、ハコブくん（Claude）の見立てです
・手直しの区間を分けて学ばせる方法は、まだ試していません。続きは机の上の実験で確かめます

▼この番組について
「フィジカルAI解説｜Pixels to Motors」は、フィジカルAIのニュースや実機の記録から、現場で使える形を考える番組です。
エイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。その中で分かったことと、そこから考えた見立てを、案内役のハコブくんがお伝えします。

・ナレーションは AI 音声（ElevenLabs）です。話しているのは Pixels to Motors のキャラクター、ハコブくんです
・エイイチの声も AI 音声で、本人の声ではありません
・引退式・宇宙・割れたコップ・紙の山の絵と、サムネイルの背景（脳の模型とロボットアーム）は、生成AI（Gemini）で作成しています
・実機の映像はエイイチが撮ったものです

▼ロボットの開発・導入のご相談
メール：pixelstomotors@adaptdesign.jp

▼リンク
Webサイト：https://pixelstomotors.com
4コママンガ『ハコブくん』：https://pixelstomotors.com/manga-archive
GitHub：https://github.com/PixelstoMotors
X：https://x.com/adaptdesignjp
LinkedIn：https://www.linkedin.com/in/eiichi-shimazu

チャンネル登録・高評価もよろしくお願いします。コメントは、エイイチが全部読んでいます。

#フィジカルAI #ロボット #AI
```

### ショートのタイトルと説明文（ショート_タイトルと説明文.txt）

```text
ショートのタイトルと説明文（本編「ロボットの頭脳は、脳のように分かれるのか｜大きなAIは大脳、専用AIは小脳」から作った3本・版2）
関連動画には本編を設定してください。

━━━━━━━━━━━━━━━━
ショート1_遅っ（24.3秒）
タイトル：
深宇宙で計算？ その前に、考えるのに18.6秒 #Shorts
説明文：
「超知能のほぼ100%は、深宇宙で計算される」。話がでかい。地上のロボットでは、何にでも使える大きなAIが1回考えるのに18.6秒。その間に、ロボット専用のAI（π0.5）は動きを370回ほど直せます。
数字の出どころ：NVIDIA「Astranomy」（2026年10月14日）・イーロン・マスクさんの X の投稿。370回は 18.6秒 ÷ 0.05秒 の目安です。
宇宙の絵は生成AI（Gemini）で作成しました。声は AI 音声です（エイイチの声も AI 音声で、本人の声ではありません）。
本編：ロボットの頭脳は、脳のように分かれるのか
#フィジカルAI #ロボット #AI

━━━━━━━━━━━━━━━━
ショート2_捨てられない人（24.0秒）
タイトル：
記事424本を全部残す人は、捨てられない人？ #Shorts
説明文：
記事を全部、全文で残しているのは、捨てられないからではなく、全部の積み重ねが「基幹情報」だから。ただ、情報が多いだけでは言葉や動きは良くならない。脳も、その日の出来事をいったん海馬に残し、大事なものを大脳へ移していきます。
紙の山の絵は生成AI（Gemini）で作成しました。声は AI 音声です（エイイチの声も AI 音声で、本人の声ではありません）。
本編：ロボットの頭脳は、脳のように分かれるのか
#フィジカルAI #ロボット #AI

━━━━━━━━━━━━━━━━
ショート3_勝手に引退させないで（26.8秒）
タイトル：
π0.5 は引退？「勝手に引退させないで」 #Shorts
説明文：
何にでも使える大きなAIが、追加の練習なしでブロックを器に入れた。では、ロボット専用のAIは引退？　答えは、組み合わせ。遅く考える頭と、速く直す手に分けます。
引退式の絵は生成AI（Gemini）で作成しました。声は AI 音声です（エイイチの声も AI 音声で、本人の声ではありません）。
本編：ロボットの頭脳は、脳のように分かれるのか
#フィジカルAI #ロボット #AI
```

### 書き出しの記録（kabe_kit.py build の出力・3版の本編）

```text
BGM 1本・効果音 35個 → mix.wav
   89.67秒  文013  se_whoosh  -10dB  「ということは、エイイチの研究室の」
   94.17秒  文014  se_pop  -8dB  「勝手に引退させないで。」
   94.77秒  文014  se_pop  -12dB  「勝手に引退させないで。」
  110.25秒  文018  se_whoosh  -10dB  「超知能の、ほぼ100%は、深宇宙」
  118.57秒  文020  se_whoosh  -14dB  「宇宙は、今日は置いておきます。」
  130.53秒  文023  se_pop  -8dB  「遅っ。」
  145.93秒  文026  se_whoosh  -10dB  「コップが手から滑り始めても、僕が」
  183.03秒  文034  se_pop  -12dB  「考えてみました。……半分、おっし」
  210.77秒  文037  se_pop  -8dB  「人も、小脳と大脳で役割が分かれて」
  213.23秒  文038  se_rise  -10dB  「……それです。」
  229.08秒  文041  se_pop  -10dB  「ちょっと実演します。」
  284.20秒  文052  se_whoosh  -12dB  「では僕も、小脳を鍛えるために、今」
  284.55秒  文052  se_pop  -6dB  「では僕も、小脳を鍛えるために、今」
  286.70秒  文053  se_pop  -12dB  「失敗だけ覚えちゃって、良くないね」
  330.00秒  文059  se_whoosh  -10dB  「記録といえば、エイイチは、記事を」
  333.45秒  文060  se_whoosh  -14dB  「LOGは、全部の積み重ね。基幹情」
  377.05秒  文068  se_pop  -14dB  「ちなみに、ボツになった3つは、全」
  377.45秒  文068  se_pop  -14dB  「ちなみに、ボツになった3つは、全」
  377.85秒  文068  se_pop  -14dB  「ちなみに、ボツになった3つは、全」
  404.80秒  文072  se_ding  -12dB  「今の返事、速かったね。」
  122.90秒  文021  se_tick  -16dB  「では、なぜ組み合わせになるのか。」
  123.90秒  文021  se_tick  -16dB  「では、なぜ組み合わせになるのか。」
  124.90秒  文021  se_tick  -16dB  「では、なぜ組み合わせになるのか。」
  125.90秒  文021  se_tick  -16dB  「では、なぜ組み合わせになるのか。」
  126.90秒  文021  se_tick  -16dB  「では、なぜ組み合わせになるのか。」
  127.90秒  文021  se_tick  -16dB  「では、なぜ組み合わせになるのか。」
   36.80秒  文006  se_chapter_glock  -10dB  「Pixels to Motors」
   65.60秒  文010  se_chapter_marimba  -10dB  「夜、エイイチから、画面写真が7枚」
  122.30秒  文021  se_chapter_marimba  -10dB  「では、なぜ組み合わせになるのか。」
  131.60秒  文024  se_chapter_marimba  -10dB  「大きなAIなら、ここで18.6秒」
  200.07秒  文036  se_chapter_marimba  -10dB  「だから、分けるしかありません。遅」
  267.13秒  文050  se_chapter_marimba  -10dB  「失敗を手直しするデータの積み重ね」
  322.50秒  文059  se_chapter_marimba  -10dB  「記録といえば、エイイチは、記事を」
  365.27秒  文066  se_chapter_marimba  -10dB  「ここまでを、1枚にまとめます。」
  408.97秒  文074  se_chapter_marimba  -10dB  「この動画が役に立ったら、高評価と」
  900/12761
  1800/12761
  2700/12761
  3600/12761
  4500/12761
  5400/12761
  6300/12761
  7200/12761
  8100/12761
  9000/12761
  9900/12761
  10800/12761
  11700/12761
  12600/12761
frames 12761 = 425.4s  確認用 19.3MB (crf 29)
```
