ひとつの看板に、ふたつの入口 ― フィジカルAIのサイトで、AI活用の相談も受ける設計
このサイトは、フィジカルAI(ロボットのように体を持って動く AI)を研究した記録を出す場所として作っています。トップの説明も、YouTube のチャンネル名も、SNS のプロフィールも、フィジカルAI に絞っています。ところが、直近の記事20本を中身で数え直すと、学習・推論・実機・画像認識・考察といった研究の記録は7本しかありませんでした。12本は、AI に手伝わせてサイトを別の仕組みへ移したり、解説動画の作り方を整えたりした記録です。看板はフィジカルAI なのに、中身の6割は研究の外側の作業でした。
もう一つ、シリーズの名前と中身もずれていました。「フィジカルAI研究室」というシリーズに入っていた11本のうち、AI の学習や推論を実際に回した回は4本だけで、残りの7本は解説動画の作り方の記録でした。研究室の名前の下に、研究ではない記録が並んでいたことになります。
この数字を出したきっかけは、「サイトの発信を変える必要があるか」という相談でした。研究を進めるかたわらで、AI を使って仕事を回す知見がたまっていく。Web 制作の仕事が減ったいま、その知見で受けられる相談もあるかもしれない。ただ、看板を AI活用に掛け替えたいわけではありません。本人の言葉ではこうです。
SNSや動画、サイトTOPに来る人にはフィジカルAI、ロボットの人と伝わればいいですが、別のところから入ってきた人にはAI活用のことも相談していい人なんだと伝わればいいです。
決めたのは、看板は変えずに、入口によって見せるものを変えることです。サイトのトップ・SNS・動画から来た人には、これまでどおりフィジカルAI の人として見せます。検索で AI活用の記事に直接来た人にだけ、記事の末尾で「この仕組みづくりも相談できます」と伝えます。AI活用を2本目の看板にすると、フィジカルAI の発信が薄まり、同じ話題を扱う人の多い場所に立つことになるからです。
| 入口 | 来る人 | 伝えること | 変えたこと |
|---|---|---|---|
| サイトのトップ・SNS・YouTube | ロボットやフィジカルAI に関心のある人 | フィジカルAI・ロボットの人 | トップの説明文を、いまの研究の中身に合わせて書き直した |
| 検索から AI活用の記事へ | サイト移行や SEO などを調べている人 | この仕組みづくりも相談できる | 記事の末尾に相談の案内を出した(15本) |
| 記事からシリーズのページへ | 記事を読んで回遊してきた人 | 研究を回すために作った仕組みの記録 | 「ロボティクスデザイン実務フロー」のページに説明文を書いた |
| メッセージフォーム | 相談したい人 | 相談の種類を選べる | 「ご相談の種類」を足し、届くメールの件名に入れた |
あわせて、動画の作り方の記録7本を「フィジカルAI研究室」から「ロボティクスデザイン実務フロー」へ移し、研究室には学習と推論を回した4本だけを残しました。最後に、このプロジェクトの判断の元になっている文書(MASTER)を見直し、「研究の外側の作業が続いたら研究に戻す」「AI活用の相談は看板にしない」などの決まりを書き足しています。
ここから下は作業ログです。AI(Claude)とのやり取りで出た指示と判断、数えたときのコマンドと出力、サイトに入れた変更の全量、MASTER の変更の全量を、作業した順に載せています。
作業ログ ― 相談は「サイトの発信を変える必要があるか分析してほしい」だった
依頼はこの2文でした。
サイトの発信内容はフィジカルAIに全振りしていますが、その一方でAI活用のこともやることが多いです。 サイトの発信を変える必要があるか分析してください。
AI は、プロジェクトの決まりをまとめた文書(MASTER)と、これまでの記録のメモを読んだうえで、公開中のトップページを読み込み、記事一覧に並ぶ直近の記事の題とシリーズ名を取り出しました。分析は、この一覧を中身で分けるところから始まっています。
直近20本(LOG325〜LOG344)を、公開時のシリーズ名と中身で分けた表
最初の返答では、記事一覧の題から中身を分けました。記事にするにあたって、変更を入れる前のサイトのデータ(commit f8bc5ef の時点)から、20本の公開時のシリーズ名を取り出し直しています。古い記事は Studio から移した src/data/cards.json に、新しい記事は Markdown の先頭の項目にシリーズ名が入っているので、両方を読んでいます。
$ cd /home/claude/p2m-site && python3 - <<'EOF'
import json,subprocess,re
def show(rev,path): return subprocess.run(['git','show',f'{rev}:{path}'],capture_output=True,text=True).stdout
rev='f8bc5ef'
cards=json.loads(show(rev,'src/data/cards.json'))
rows={}
for c in cards:
m=re.match(r'LOG(\d+)$',c['day'])
if m: rows[int(m.group(1))]=(c['series'],c['title'][:40],c['slug'])
for f in ['DAY334','DAY335','DAY336','DAY337','DAY338','DAY339','DAY340','DAY342','LOG341','LOG343','LOG344']:
t=show(rev,f'src/content/records/{f}.md')
d=int(re.search(r'^day: (\d+)',t,re.M).group(1)); s=re.search(r'^series: "(.*)"',t,re.M).group(1); ti=re.search(r'^title: "(.*)"',t,re.M).group(1)
sl=re.search(r'^slug: (\S+)',t,re.M).group(1)
rows[d]=(s,ti[:40],sl)
for n in range(325,347): print(n, rows.get(n))
EOF
325 ('画像認識・物体検出', '画像認識と物体検出を学び直す Log#11 | Decisions API は爪', 'decisions-api-grasp-check')
326 ('フィジカルAI研究室', 'フィジカルAI研究室を作る Log#2 | Physical Intellige', 'openpi-remote-inference-first-run')
327 ('フィジカルAI研究室', 'フィジカルAI研究室を作る Log#3 | Physical Intellige', 'pi05-libero-benchmark-first-run')
328 ('ロボティクスデザイン', 'ロボティクスを声に出して覚え直す ― 開発ログから用語164語と判断の型104枚', 'speak-to-relearn-robotics')
329 ('考察記事', 'MicroFactoryは市販アームFAIRINO FR3でロボットセルを作った', 'microfactory-fairino-fr3')
330 ('フィジカルAI研究室', '解説動画をAIと作る ― 型の分析から敵対的検証まで、試作1本で決めたこと', 'making-explainer-video-with-ai')
331 ('フィジカルAI研究室', '解説動画のナレーションをハコブくんに任せる ― 口と目と光をコードで動かし、文字', 'explainer-video-character-narration')
332 ('フィジカルAI研究室', 'YouTubeチャンネルの顔を作る ― チャンネル名・アイコン・バナーをハコブく', 'youtube-channel-identity')
333 ('フィジカルAI研究室', '解説動画の1本目を公開まで通す ― 声・BGM・間・直し・サムネイルを部品ごとに', 'first-video-end-to-end')
334 ('ロボティクスデザイン', 'サイトを Studio の外へ持ち出せる形で残す ― 533ページを1フォルダに', 'pixelstomotors-site-archive')
335 ('フィジカルAI研究室', '解説動画の台本15本 ― ネタ25本から絞った基準と、事実確認で直した29か所', 'youtube-script-curation')
336 ('考察記事', 'NVIDIAのAlphaNISTは二つ爪のグリッパーで組立を覚えた ― 成功か失', 'alphanist-binary-reward-assembly')
337 ('フィジカルAI研究室', 'フィジカルAI研究室を作る Log#4 | Physical Intellige', 'pi05-mecharm-demo-to-inference')
338 ('フィジカルAI研究室', '解説動画の作り方を2つ増やす ― 「試してみた回」の型と、画面の文字を大きくする', 'video-patterns-and-try-it')
339 ('フィジカルAI研究室', '何本も並行する動画づくりを見える化する ― 段階の自動判定と、PCは一覧・iPh', 'video-progress-board')
340 ('ロボティクスデザイン', 'Studio を出て、GitHub に push すると公開されるサイトへ ― ', 'studio-to-astro-cloudflare')
341 ('フィジカルAI研究室', 'フィジカルAI研究室を作る Log#5 | π0.5 を mechArm 270', 'pi05-mecharm-lora-2000-steps')
342 ('ロボティクスデザイン', 'サイトを pixelstomotors.com へ ― 動画が再生されなかった原', 'pixelstomotors-com-domain')
343 ('ロボティクスデザイン', 'Studio を閉じて、旧 URL は Cloudflare で転送する ― 公', 'studio-shutdown-cloudflare-redirect')
344 ('ロボティクスデザイン', 'AIに任せたSEO対策 ― 公開したサイトは「Googleに載せない」設定のまま', 'site-seo-by-ai')
345 None
346 None
これを中身で分けた表が次です。「研究」は、AI の学習・推論、実機のアーム、画像認識、ロボットの考察のどれかを扱った記録。「研究の外」は、AI を使ってサイトや動画の仕組みを作った記録。「学習用の道具」は、開発ログから作った用語の練習ページの記録(LOG328)です。
| LOG | 公開時のシリーズ | 中身の分類 | 何の記録か |
|---|---|---|---|
| 325 | 画像認識・物体検出 | 研究 | 爪の間のブロックを判定する画像認識の試験 |
| 326 | フィジカルAI研究室 | 研究 | π0.5 を借りた GPU で動かし、Mac から指示を届ける |
| 327 | フィジカルAI研究室 | 研究 | π0.5 をシミュレータで評価し、GPU のメモリを測る |
| 328 | ロボティクスデザイン | 学習用の道具 | ロボティクスの用語を声に出して覚え直すページ |
| 329 | 考察記事 | 研究 | MicroFactory のロボットセルの考察 |
| 330 | フィジカルAI研究室 | 研究の外 | 解説動画を AI と作る試作 |
| 331 | フィジカルAI研究室 | 研究の外 | 解説動画のナレーションをハコブくんに任せる |
| 332 | フィジカルAI研究室 | 研究の外 | YouTube チャンネルの名前・アイコン・バナー |
| 333 | フィジカルAI研究室 | 研究の外 | 解説動画の1本目を公開まで通す |
| 334 | ロボティクスデザイン | 研究の外 | サイトを Studio の外へ持ち出せる形で残す |
| 335 | フィジカルAI研究室 | 研究の外 | 解説動画の台本15本 |
| 336 | 考察記事 | 研究 | NVIDIA の AlphaNIST の考察 |
| 337 | フィジカルAI研究室 | 研究 | π0.5 を mechArm 270 のデモで追加学習する |
| 338 | フィジカルAI研究室 | 研究の外 | 解説動画の作り方を2つ増やす |
| 339 | フィジカルAI研究室 | 研究の外 | 並行する動画づくりの進み具合のページ |
| 340 | ロボティクスデザイン | 研究の外 | サイトを Astro と Cloudflare へ移す |
| 341 | フィジカルAI研究室 | 研究 | π0.5 を成功19本で2,000ステップ学習する |
| 342 | ロボティクスデザイン | 研究の外 | サイトを pixelstomotors.com へ |
| 343 | ロボティクスデザイン | 研究の外 | Studio を閉じて旧 URL を転送する |
| 344 | ロボティクスデザイン | 研究の外 | AI に任せた SEO 対策 |
| 中身の分類 | 本数 |
|---|---|
| 研究 | 7 |
| 研究の外 | 12 |
| 学習用の道具 | 1 |
| 合計 | 20 |
「フィジカルAI研究室」の名前が付いていたのは 326・327・330・331・332・333・335・337・338・339・341 の11本で、学習・推論を回したのは 326・327・337・341 の4本です。最初の返答では「10本のうち4本、残り6本が動画」と書いていましたが、表で数え直すと11本のうち4本、残り7本でした。移した記事も7本なので、表の数が正しい方です。
AI の最初の答え ― AI活用を2本目の看板にしない、ただし置き場所はある
AI の答えは「軸は変えない。直すのは AI活用の記事の置き場所と、トップの文言の2つ」でした。AI活用を看板に加えない理由として、次の3つを挙げています。
- AI活用は発信している人がとても多い話題で、「誰もいない場所に立つ」というこのプロジェクトの立ち位置と逆になる
- 看板として掲げることは、MASTER が向こう2年は禁じている「新しい肩書き・キャッチコピー」の追加にあたる
- 共同研究や R&D 契約を考える組織が見たいのは、学習・推論を自分で回している証拠で、サイト移行や動画づくりの記録はその判断材料になりにくい
一方で、AI活用の記録の居場所もあると答えています。MASTER の Mission(目的の3行)の2行目と3行目は「その知見を再現可能な形で公開する」「誰でも同じ環境から Physical AI 研究を始められる状態を増やす」です。ひとりで研究を回すために AI でどう仕組みを作ったかは、「同じ環境から始める」ための材料の一部と言えます。テーマとしてではなく「研究の回し方」として置けば、目的の内側に収まる、という整理です。
あわせて、直近の6割がサイトと動画の作業だったことについて、「研究を止めない。発信は研究の副産物」という決まりに照らすと、見直すべきなのは発信の方向より時間の配分かもしれない、と書いています。そのうえで、AI活用を発信したい理由が「Web 制作の代わりになる仕事を取りたい」からなのかを聞き返しました。そうであれば、サイトの見せ方ではなく、事業の範囲の話になるからです。
本人の答え ― 看板は変えず、別の入口から来た人にだけ相談できると伝わればいい
返ってきた答えはこうでした。
AI活用の仕事(Web制作の代わりになる案件)を取りたいから それもあります。現時点言うとすぐにロボットのUI/UX改善の仕事が増えて案件獲得に結びつきにくい(出来ないとは言わないがすぐには難しい)
AI活用の仕事を積極的に取りたいというより、フィジカルAIのことを進めていく中で、副産物としてAI活用が多いのでそれを活かす方法も探しておきたいといったところです。
SNSや動画、サイトTOPに来る人にはフィジカルAI、ロボットの人と伝わればいいですが、別のところから入ってきた人にはAI活用のことも相談していい人なんだと伝わればいいです。
ロボットの UI/UX の仕事はすぐには増えない。AI活用の仕事を積極的に取りに行くつもりはないが、研究の副産物として増えた知見を生かす道は作っておきたい。条件は、看板(トップ・SNS・動画)ではフィジカルAI の人として伝わること。この答えで、「看板を1つのまま、入口ごとに見せるものを変える」という形が決まりました。
入口ごとの見せ方 ― 4つの入口と、それぞれで伝えること
AI は、入口を4つに分けて、それぞれで何を伝えるかを表にしました。
| 入口 | 来る人 | 伝えること | 変更 |
|---|---|---|---|
| SNS・動画・サイトのトップ | ロボットに関心のある人 | フィジカルAI・ロボットの人 | なし(トップの古い文言の修正だけ) |
| 検索から AI活用の記事に来る | AI活用で調べている人 | この仕組みづくりも相談できる | 記事の末尾に一言と、相談への導線を足す |
| 実務フローのシリーズページ | 記事から回遊してきた人 | 研究を回すために作った仕組みの記録 | 冒頭の説明を書く |
| メッセージフォーム | 相談したい人 | 相談の種類を選べる | 選択肢を足す |
ポイントは、AI活用を「研究のために実際に作って使っている仕組み」として見せることです。机の上の空論ではなく動いている実例なので信頼されやすく、ロボットの研究もしている人という点で、AI活用の一般的な相談先とも違いが出ます。
やらないことも決めています。
- サイトのトップ、SNS のプロフィール、YouTube に AI活用を足さない
- 「AI活用コンサルタント」のような肩書きを作らない
フォームで相談の種類を選んでもらうと、どちらの入口から仕事の話が来ているかが記録に残ります。サイトにはアクセス解析を入れたばかりなので、数か月後に「AI活用の導線に意味があったか」を数字で判断できます。
この形で進めてよいかを聞き、「進めてください。MASTERには1行追加しておきます。」と返ってきたので、サイトの変更に入りました。
サイトに入れた変更 ― 記事7本の移動・相談の案内・シリーズの説明・相談の種類
サイトは GitHub の PixelstoMotors/p2m-site から作られていて、main に push すると約1分で公開されます(LOG340)。作業は Claude のクラウドの作業環境にこのリポジトリを取ってきて行いました。
$ git clone --depth 1 https://github.com/PixelstoMotors/p2m-site /home/claude/p2m-site
Cloning into '/home/claude/p2m-site'...
$ git -C /home/claude/p2m-site rev-parse HEAD
f8bc5ef2404a453d5ffc2b5a33bb1f44618f9b1c
記事は2種類の形で入っています。Studio から移した古い記事は、一覧用の src/data/cards.json と、記事ごとの src/data/records/{slug}.json。新しく Markdown で書いた記事は src/content/records/*.md で、先頭の項目にシリーズ名などが入っています。記事ページ(src/pages/record/[slug].astro)は、この両方からページを作ります。変更は、この2種類の両方に入れる必要がありました。
動画づくりの記録7本を「フィジカルAI研究室」から「ロボティクスデザイン実務フロー」へ移した
移したのは 330・331・332・333・335・338・339 の7本です。330〜333 は Studio から移した記事なので、cards.json と記事ごとの JSON の "series" を書き換えました。335・338・339 は Markdown なので、先頭の series: を書き換えています。サイトの中でのシリーズ名は、実務フローが ロボティクスデザイン、研究室が フィジカルAI研究室 です。
$ for f in DAY335 DAY338 DAY339; do sed -i 's/^series: "フィジカルAI研究室"$/series: "ロボティクスデザイン"/' src/content/records/$f.md; grep -n '^series' src/content/records/$f.md; done
8:series: "ロボティクスデザイン"
8:series: "ロボティクスデザイン"
8:series: "ロボティクスデザイン"
cards.json は1行ごとに整形された大きなファイルなので、読み込んで書き出し直すと、関係のない行まで変わってしまいます。記事1件分のかたまりごとに区切り、対象の4件の中の "series" だけを置き換えました。
$ python3 - <<'EOF'
import json,re
slugs=['making-explainer-video-with-ai','explainer-video-character-narration','youtube-channel-identity','first-video-end-to-end']
for sl in slugs:
p=f'src/data/records/{sl}.json'; s=open(p,encoding='utf-8').read()
old='"series": "フィジカルAI研究室"'
assert s.count(old)==1,(p,s.count(old))
open(p,'w',encoding='utf-8').write(s.replace(old,'"series": "ロボティクスデザイン"'))
p='src/data/cards.json'; s=open(p,encoding='utf-8').read()
objs=re.split(r'(?<=\n\})', s)
out=[];n=0
for o in objs:
m=re.search(r'"slug": "([^"]+)"',o)
if m and m.group(1) in slugs:
assert o.count('"series": "フィジカルAI研究室"')==1; o=o.replace('"series": "フィジカルAI研究室"','"series": "ロボティクスデザイン"'); n+=1
out.append(o)
open(p,'w',encoding='utf-8').write(''.join(out)); print('changed',n)
json.load(open(p))
EOF
changed 4
$ git diff --stat
src/content/records/DAY335.md | 2 +-
src/content/records/DAY338.md | 2 +-
src/content/records/DAY339.md | 2 +-
src/data/cards.json | 8 ++++----
src/data/records/explainer-video-character-narration.json | 2 +-
src/data/records/first-video-end-to-end.json | 2 +-
src/data/records/making-explainer-video-with-ai.json | 2 +-
src/data/records/youtube-channel-identity.json | 2 +-
8 files changed, 11 insertions(+), 11 deletions(-)
移したあとのフィジカルAI研究室のページです。学習と推論の回(LOG341・337・327・326)と、研究室を始めた回、学習データの置き場を決めた回、SO-101 のハードを選び直した回が残っています。

AI活用の記事の末尾に出す相談の案内 ― Markdown は ai_consult、Studio の記事は aiConsult.json
案内を出す記事は、Markdown の記事なら先頭の項目に ai_consult: true を書き、Studio から移した記事は src/data/aiConsult.json に slug を並べる形にしました。古い記事の JSON に項目を足すより、別の一覧で持つほうが、どの記事に出しているかを1か所で見られるからです。
最初に出したのは13本です。Markdown の 334・335・338・339・340・342・343・344 の8本と、Studio から移した 328・330・331・332・333 の5本。このあと別の作業で公開された LOG346 にも同じ案内が入り、この記事を合わせて15本になります。
$ for f in DAY334 DAY335 DAY338 DAY339 DAY340 DAY342 LOG343 LOG344; do python3 - "$f" <<'EOF'
import sys
p=f'src/content/records/{sys.argv[1]}.md'; s=open(p,encoding='utf-8').read()
head,rest=s.split('\n---\n',1)
assert head.startswith('---') and 'ai_consult' not in head
open(p,'w',encoding='utf-8').write(head+'\nai_consult: true\n---\n'+rest)
EOF
done; head -11 src/content/records/LOG344.md
---
title: "AIに任せたSEO対策 ― 公開したサイトは「Googleに載せない」設定のままだった。説明のない画像1万枚まで直した記録"
slug: site-seo-by-ai
date: 2026-10-17
day: 344
meta_description: "公開中のサイトに検索除外の設定が残っていた。AIが全557ページを点検し、サイトマップ・画像の説明1万枚・構造化データ・日英の対応・SNS用画像・アクセス解析までを直した記録。"
tags: [SEO, noindex, sitemap.xml, alt, 構造化データ, JSON-LD, hreflang, OGP, Google Search Console, Cloudflare Web Analytics, Astro, Playwright]
series: "ロボティクスデザイン"
ai_consult: true
---
案内の文は、決めた案のままです。ボタンの行き先は /message?topic=ai にして、フォームの「ご相談の種類」が「AI活用・仕組みづくり」で選ばれた状態で開くようにしました。

実務フローのシリーズページの説明文 ― SERIES_PAGES に intro を足した
シリーズのページは src/lib/records.ts の SERIES_PAGES に並んでいます。ここに intro(見出しの下に出す説明)を書けるようにし、実務フロー(series4)にだけ説明を入れました。説明があるシリーズは、検索結果に出る説明(meta description)にもその文を使います。

メッセージフォームの「ご相談の種類」と、届くメールの件名
フォームに「ご相談の種類」の選択を足しました。選択肢は「フィジカルAI・ロボット」「AI活用・仕組みづくり」「取材・登壇」「その他」の4つで、選ばないと送れません。送った内容を受け取る Cloudflare Workers の worker/index.js では、種類を保存する JSON に足し、届くメールの件名を [Pixels to Motors] メッセージ: 名前 から [Pixels to Motors] AI活用・仕組みづくり: 名前 のような形に変えました。件名を見るだけで、どちらの入口からの相談かが分かります。
種類の値の一覧は、フォームと worker/index.js の TOPICS の2か所にあります。種類を増やすときは両方を直す必要があるので、そのことを README に書きました。
worker/index.js では、最初は TOPICS[d.topic] で値を確かめていました。この書き方だと、送られてきた値が constructor のような、どのオブジェクトにも最初から付いている名前のときにも「ある」と判定されます。push する前に Object.hasOwn(TOPICS, d.topic) に直しています。
$ sed -i "s/ const topic = TOPICS\[d.topic\] ? String(d.topic) : '';/ const topic = Object.hasOwn(TOPICS, d.topic) ? d.topic : '';/; s/ const topicName = TOPICS\[m.topic\] || '未選択';/ const topicName = Object.hasOwn(TOPICS, m.topic) ? TOPICS[m.topic] : '未選択';/" worker/index.js && grep -n "hasOwn" worker/index.js
71: const topic = Object.hasOwn(TOPICS, d.topic) ? d.topic : '';
94: const topicName = Object.hasOwn(TOPICS, m.topic) ? TOPICS[m.topic] : '未選択';

ビルドして、変わったページを数えて確かめた
ビルドすると、609ページができました。
$ npm ci --no-audit --no-fund; echo ci=$?; npm run build; echo build=$?
ci=0
build=0
12:13:57 ✓ Completed in 2.40s.
12:13:57 [@astrojs/sitemap] `sitemap-index.xml` created at `dist`
12:13:57 [build] 609 page(s) built in 11.19s
12:13:57 [build] Complete!
できたページから、相談の案内が出た記事、2つのシリーズページに並ぶ記事の番号、シリーズページの説明、フォームの選択を数えました。
$ cd dist && echo "consult boxes:"; grep -l 'class="consult"' record/*.html | sed 's#record/##;s#\.html##' | tr '\n' ' '; echo; echo series4:; grep -o 'LOG3[0-9][0-9]' series4.html | sort -u | tr '\n' ' '; echo; echo series6:; grep -o 'LOG3[0-9][0-9]' series6.html | sort -u | tr '\n' ' '; echo; grep -o 's-intro">[^<]*' series4.html; grep -c 'msgTopic' message.html; grep -o '<meta name="description" content="[^"]*' series4.html
consult boxes:
explainer-video-character-narration first-video-end-to-end making-explainer-video-with-ai pixelstomotors-com-domain pixelstomotors-site-archive site-seo-by-ai speak-to-relearn-robotics studio-shutdown-cloudflare-redirect studio-to-astro-cloudflare video-patterns-and-try-it video-progress-board youtube-channel-identity youtube-script-curation
series4:
LOG303 LOG328 LOG330 LOG331 LOG332 LOG333 LOG334 LOG335 LOG338 LOG339 LOG340 LOG342 LOG343 LOG344
series6:
LOG326 LOG327 LOG337 LOG341
1
<meta name="description" content="Pixels to Motors のシリーズ「ロボティクスデザイン実務フロー」(Robotics Design Workflow)。フィジカルAIの研究をひとりで続けるために、AIと一緒に作ってきた仕組みの記録です。サイトの移行、動画の制作、記録の整理など、研究の外側の作業をどう回しているかを残しています。
series6 の行に出ている LOG3xx は4本です(LOG277・LOG234・LOG225 は LOG3 で始まらないので、この数え方には出ません)。s-intro の文字が出なかったのは、ページの組み立ての都合で class の後ろに属性が付くためで、説明の文は meta description と画面写真で確かめています。
画面は、手元でビルドしたページをブラウザで開いて、PC の幅(1280px)とスマートフォンの幅(390px)で写して確かめました。/message?topic=ai を開いたとき、種類が「ai」(AI活用・仕組みづくり)で選ばれていることも、2つの幅で確かめています。
$ python3 shot.py
pc ai
sp ai
最後に、フォームの入力例の文の頭に「「」が抜けていたので足しました。もとの文は 現在〇〇の現場で…知りたいです」「URDFデータは… と、閉じのかぎ括弧だけがある形でした。
commit e325bf6 を push して、公開されたページで確かめた
変えたファイルを1つずつ指定して commit し、push しました。
$ git fetch --depth 1 origin main -q && git log --oneline -1 origin/main && git rev-parse HEAD
f8bc5ef ハコブくんの知らせの記録に、週ごとの表を足す(週が変わると自動で行が増える)
f8bc5ef2404a453d5ffc2b5a33bb1f44618f9b1c
$ git add README.md src/content.config.ts src/content/records/DAY334.md src/content/records/DAY335.md src/content/records/DAY338.md src/content/records/DAY339.md src/content/records/DAY340.md src/content/records/DAY342.md src/content/records/LOG343.md src/content/records/LOG344.md src/data/cards.json src/data/records/explainer-video-character-narration.json src/data/records/first-video-end-to-end.json src/data/records/making-explainer-video-with-ai.json src/data/records/youtube-channel-identity.json src/lib/records.ts 'src/pages/[series].astro' src/pages/message.astro 'src/pages/record/[slug].astro' worker/index.js src/data/aiConsult.json
$ git commit -q -F - # commit の文は最後の節「コードの変更の全量」に載せている
$ git push -q origin HEAD:main && git log --oneline -1
e325bf6 AI活用の記事を実務フローへ移し、相談の導線を足す
30秒おいて、公開中のサイトから3つのページを取ってきて、変更が出ているかを確かめました。
$ sleep 30; for u in /series4 /record/site-seo-by-ai /message; do printf "%s " $u; curl -s https://pixelstomotors.com$u | grep -o 's-intro\|class="consult"\|msgTopic' | sort -u | tr '\n' ' '; echo; done
/series4 s-intro
/record/site-seo-by-ai class="consult"
/message msgTopic
トップの説明文を「HRIシミュレーション・実機はその先」から、いまの研究の中身に書き直した
最初の分析で、AI活用より先に直すべきだと挙げたのがトップの文言です。トップの説明は「テーマは HRI シミュレーション」「Unity で空間もシナリオもゼロから組み」「実機はその先」となっていて、半年以上前の状態のままでした。いまは実機のアームで集めたデータで AI(π0.5)を学習させ、推論まで回しています。
案を先に見せてから入れる約束だったので、AI は次の案を出しています。見出しのキャッチコピー(「人とロボットの関わりを、シミュレーションでデザインする」)と肩書き(Robotics UX & HRI Designer)は、変えると MASTER が禁じている新しい肩書き・キャッチコピーの追加にあたるので、触っていません。
| 場所 | 前 | 後 |
|---|---|---|
| トップの説明文 | 29年のデザイナー経験を、いまロボティクスに応用しています。テーマはHRIシミュレーション——人がロボットをどう感じ、どう関わるかを、実機の前にデジタル上で設計すること。Unityで空間もシナリオもゼロから組み、人の間を動いて運び・手渡すサービスロボットの振る舞いを、成否の基準を物理精度ではなく人の体験に置いてつくります。実機はその先。過程の失敗も含めて公開しています。 | 29年のデザイナー経験を、いまロボティクスに応用しています。机の上のロボットアームでお手本のデータを集め、AIに学習させ、動かして確かめる。その環境を自宅で組み、誰でも同じ手順で始められる形で公開しています。基準は物理精度ではなく、人がロボットをどう感じ、どう関わるかという体験の成立。過程の失敗も含めて公開しています。 |
| 事業所についての説明文 | 29年にわたるデザイナーとしての知見を土台に、人とロボットの関わり(HRI)をシミュレーションで設計するデザイン事務所です。Unityなどのツールで空間もシナリオもゼロから組み、AIエージェント(Claude Code)を設計と実装の相棒に、人がロボットをどう感じ・どう関わるかを実機の前にデザインします。基準は物理精度ではなく体験の成立。画面上の「Pixels(デジタル)」の設計を、やがて現実の「Motors(モーター)」へ——その前半、体験を確かめるフェーズを公開しています。 | 29年にわたるデザイナーとしての知見を土台に、人とロボットの関わり(HRI)と、ロボットが学習できる環境づくりに取り組むデザイン事務所です。Unityでのシミュレーション、実機のロボットアーム、借りたGPUでの学習・推論までを自分の手で回し、AIエージェント(Claude Code)を設計と実装の相棒にしています。基準は物理精度ではなく体験の成立。画面上の「Pixels(デジタル)」の設計を、現実の「Motors(モーター)」へ——その過程を公開しています。 |
| 研究・実践分野 | HRIシミュレーション / ロボティクスUX設計 / デジタルツイン / Unity / Physical AI / R2R2R | フィジカルAI(学習・推論) / HRI / ロボティクスUX設計 / デジタルツイン / 画像認識 / Unity / R2R2R |
| 検索結果に出る説明 | トップの説明文の前半(HRIシミュレーションの文) | トップの新しい説明文の前半 |
この案は、MASTER の見直し(次の節)を採用するという返事と一緒に了承されたので、そのまま入れました。
入れるときに一度つまずいています。手元の作業ブランチは、別の作業で入った LOG346 の commit の分だけ遅れていました。そこで git rebase をかけ、同じコマンドの続きでトップの文を書き換えてビルドしたところ、rebase は途中で止まり、ビルドも失敗しました。
$ cd /home/claude/p2m-site && git fetch --depth 1 origin main -q && git status -sb | head -2 && git rebase -q origin/main 2>&1 | tail -2; python3 - <<'EOF'
(トップの説明文・事業所の説明文・研究・実践分野・検索結果の説明を書き換える。中身は最後の節の commit 30069f8 と同じ)
EOF
$ grep -rn "HRIシミュレーション" src public/embeds 2>/dev/null | head; npm run build >/dev/null 2>&1; echo build=$?; grep -o '<meta name="description" content="[^"]*' dist/index.html
## main...origin/main [ahead 2, behind 1]
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply f8bc5ef... ハコブくんの知らせの記録に、週ごとの表を足す(週が変わると自動で行が増える)
src/content/records/LOG344.md:75:<meta name="description" content="29年のデザイナー経験を、いまロボティクスに応用しています。テーマはHRIシミュレーション——人がロボットをどう感じ、どう関わるかを、実機の前にデジタル上で設計すること。">
src/content/records/LOG344.md:2235: description="29年のデザイナー経験を、いまロボティクスに応用しています。テーマはHRIシミュレーション——人がロボットをどう感じ、どう関わるかを、実機の前にデジタル上で設計すること。"
src/content/records/LOG344.md:2241: <p>29年のデザイナー経験を、いまロボティクスに応用しています。テーマはHRIシミュレーション——人がロボットをどう感じ、どう関わるかを、実機の前にデジタル上で設計すること。Unityで空間もシナリオもゼロから組み、人の間を動いて運び・手渡すサービスロボットの振る舞いを、成否の基準を物理精度ではなく人の体験に置いてつくります。実機はその先。過程の失敗も含めて公開しています。</p>
src/content/records/LOG344.md:2292: <div class="wide"><dt>研究・実践分野</dt><dd>HRIシミュレーション / ロボティクスUX設計 / デジタルツイン / Unity / Physical AI / R2R2R</dd></div>
src/content/records/LOG343.md:618: description="29年のデザイナー経験を、いまロボティクスに応用しています。テーマはHRIシミュレーション——人がロボットをどう感じ、どう関わるかを、実機の前にデジタル上で設計すること。"
src/content/records/LOG343.md:624: <p>29年のデザイナー経験を、いまロボティクスに応用しています。テーマはHRIシミュレーション——人がロボットをどう感じ、どう関わるかを、実機の前にデジタル上で設計すること。Unityで空間もシナリオもゼロから組み、人の間を動いて運び・手渡すサービスロボットの振る舞いを、成否の基準を物理精度ではなく人の体験に置いてつくります。実機はその先。過程の失敗も含めて公開しています。</p>
src/content/records/LOG343.md:675: <div class="wide"><dt>研究・実践分野</dt><dd>HRIシミュレーション / ロボティクスUX設計 / デジタルツイン / Unity / Physical AI / R2R2R</dd></div>
src/content/records/DAY340.md:1445: description="29年のデザイナー経験を、いまロボティクスに応用しています。テーマはHRIシミュレーション——人がロボットをどう感じ、どう関わるかを、実機の前にデジタル上で設計すること。"
src/content/records/DAY340.md:1451: <p>29年のデザイナー経験を、いまロボティクスに応用しています。テーマはHRIシミュレーション——人がロボットをどう感じ、どう関わるかを、実機の前にデジタル上で設計すること。Unityで空間もシナリオもゼロから組み、人の間を動いて運び・手渡すサービスロボットの振る舞いを、成否の基準を物理精度ではなく人の体験に置いてつくります。実機はその先。過程の失敗も含めて公開しています。</p>
src/content/records/DAY340.md:1502: <div class="wide"><dt>研究・実践分野</dt><dd>HRIシミュレーション / ロボティクスUX設計 / デジタルツイン / Unity / Physical AI / R2R2R</dd></div>
build=1
<meta name="description" content="29年のデザイナー経験を、いまロボティクスに応用しています。テーマはHRIシミュレーション——人がロボットをどう感じ、どう関わるかを、実機の前にデジタル上で設計すること。
rebase が止まった途中の状態で書き換えとビルドが走ったので、どちらも当てになりません。最後の行の古い説明は、前回のビルドで残っていたページです。grep に出た古い文は、記事の本文(LOG340・343・344)がトップのコードを記録として写した箇所で、ページの文そのものではありません。
浅く取ってきたリポジトリ(--depth 1)だったので、手元と GitHub の2つの枝の共通の祖先が手元になく、git が同じ commit だと分からなかったのが止まった原因です。rebase をやめて履歴を深めに取り直すと、自分の commit e325bf6 はすでに origin/main に入っていることが分かりました。
$ git rebase --abort; git status -sb | head -5; git log --oneline -3; git log --oneline -3 origin/main
## main...origin/main [ahead 2, behind 1]
e325bf6 AI活用の記事を実務フローへ移し、相談の導線を足す
f8bc5ef ハコブくんの知らせの記録に、週ごとの表を足す(週が変わると自動で行が増える)
3268501 LOG346 の末尾に相談の案内を出す(サイトづくりの他の記事とそろえる)
$ git fetch -q --depth=50 origin main && git log --oneline -6 origin/main && git merge-base --is-ancestor e325bf6 origin/main && echo "mine is in origin"
3268501 LOG346 の末尾に相談の案内を出す(サイトづくりの他の記事とそろえる)
c4923b4 LOG346 を公開:課題地図をサイトに移し、週1回AIが更新する
e325bf6 AI活用の記事を実務フローへ移し、相談の導線を足す
f8bc5ef ハコブくんの知らせの記録に、週ごとの表を足す(週が変わると自動で行が増える)
6a9c856 更新を知らせるハコブくんの効果を記録する
7286a81 サイトに新しい更新があると、ハコブくんが画面の下を走り回って知らせる
mine is in origin
次に、手元の書き換えを退避(stash)してから、作業ブランチを origin/main に合わせようとしました。rebase をやめた時点で書き換えは消えていたので、退避するものがなく、このときのビルドも失敗しています。ビルドの出力を捨てていたので、この失敗の理由は記録に残っていません。
$ git diff --stat; git stash -q && git reset -q --hard origin/main && git stash pop -q && git status -sb | head -3 && npm run build >/dev/null 2>&1; echo build=$?; grep -o '<meta name="description" content="[^"]*' dist/index.html; grep -o '研究・実践分野</dt><dd>[^<]*' dist/index.html
No stash entries found.
build=1
<meta name="description" content="29年のデザイナー経験を、いまロボティクスに応用しています。テーマはHRIシミュレーション——人がロボットをどう感じ、どう関わるかを、実機の前にデジタル上で設計すること。
作業ブランチが origin/main と同じになったことを確かめ、ビルドの出力をファイルに残してもう一度ビルドすると、今度は通りました。そのうえで、トップの変更を当て直しています。
$ git status -sb | head -3; git log --oneline -1; npm run build > build2.log 2>&1; echo build=$?; grep -i "error" build2.log | head -5
## main...origin/main
3268501 LOG346 の末尾に相談の案内を出す(サイトづくりの他の記事とそろえる)
build=0
$ npm run build >/dev/null 2>&1; echo build=$?; grep -o '<meta name="description" content="[^"]*' dist/index.html; git diff --stat
build=0
<meta name="description" content="29年のデザイナー経験を、いまロボティクスに応用しています。机の上のロボットアームでお手本のデータを集め、AIに学習させ、動かして確かめる。その環境を自宅で組み、誰でも同じ手順で始められる形で公開しています。
src/pages/index.astro | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
$ git add src/pages/index.astro && git commit -q -F - # commit の文は最後の節に載せている
$ git push -q origin HEAD:main && git log --oneline -1
30069f8 トップの説明文を今の研究の中身に合わせる
$ sleep 45; curl -s https://pixelstomotors.com/ | grep -o 'ロボットが学習できる環境づくり\|フィジカルAI(学習・推論)\|テーマはHRIシミュレーション' | sort | uniq -c
1 フィジカルAI(学習・推論)
1 ロボットが学習できる環境づくり
古い「テーマはHRIシミュレーション」の文は、公開中のトップから出なくなりました。記事の本文(LOG340・343・344)には、トップのコードを写した箇所として古い文が残っていますが、これはその時点の記録なので変えていません。


MASTER(プロジェクトの判断の元になる文書)の見直し ― 軸は変えず、ずれた所を直した
サイトの変更の途中で、次の依頼が入りました。
1行追加も含めて今のMASTERの見直しも一緒に行ってください。 内容がずれていることや強化すべきポイントなども見直してください(軸はブレないことは重要です)
MASTER は、このプロジェクトのすべての判断の上に置いている文書です。目的の3行(Mission)、ブランドの柱、ただ一つの主指標(再現可能な環境数)、設備投資の決まり、データの保存先、AI ツールの分担などが書いてあります。AI は、目的・主指標・ブランドの柱・新しい肩書きの禁止といった軸には触れず、実態とずれている所と、今回の件で強めるべき所だけを直しました。
実態とずれていたので直したところ
| 直したところ | 前 | 後 | 理由 |
|---|---|---|---|
| サイトの住所 | adaptdesign.jp/pixelstomotors | pixelstomotors.com | 10月16日にサイトを移した |
| 記事の公開先 | GitHub r2r2r-content の articles/ | GitHub p2m-site(画像と動画は Cloudflare R2)。r2r2r-content は SNS 用 | サイトが p2m-site から自動で作られるようになった |
| 動画の保存先 | 表になかった | iCloud の 動画/{動画名}/(作り方)と KIOXIA の videos/{動画名}/(mp4 と上げる物) | すでに使っていたのに、「場所を増やすときは先に表へ書く」という決まりに反して載っていなかった |
| 「コピーのみ・移動しない」の対象 | 保存するもの全部 | 測定データ(runs/・datasets/ など) | 動画のフォルダ整理では mv で移動していて、決まりと食い違っていた |
| 報酬モデルの1行 | 基本情報に VLM-as-Reward の説明があった | 削除 | 「技術トピックは本文書に書かない」という自身の決まりに反していた |
| AI ツールの分担 | Claude は記事と設計 | サイトのコードと公開、動画の制作、クラウド GPU での学習・推論の実行を追記 | 実際の作業範囲と合っていなかった |
| 物理的なリスクのない変更の確かめ方 | すべての変更に外部レビューを挟む決まりだけ | サイト・動画・記録の整理はビルドと画面で確かめ、URL・転送・フォーム・データの消去のように戻しにくい変更だけ外部レビューを挟む | このままだと、サイトの小さな変更のたびに決まりに違反するか、毎回レビューが要るかのどちらかになる。今回のサイトの変更も外部レビューは通していない |
| 動画のルールの参照先 | 記事のルール(article_rules.md)だけ | 動画のルール(video_rules.md)も唯一の正として参照 | 動画のルールの文書が MASTER から見えていなかった |
| 研究の中身 | 課題地図がなかった | Projects に課題地図(週1回更新)を追加 | LOG346 でサイトに組み込んだ |
強めたところ ― 運用の上位ルールに3行
運用の上位ルールには、もともと「研究を止めない。発信は研究の副産物である」という1行がありました。直近の6割が研究の外だったのは、この1行に確かめる手段がなかったからでもあります。そこで次の3行を足しました。
- 研究の外側の作業(サイト・動画・発信の仕組みづくり)が続いたら、次の作業は研究に戻す。週に一度、直近の記録のうち研究の記録(学習・推論・実機・画像認識・考察)が半分を下回っていないかを見る
- シリーズの名前は中身と一致させる。フィジカルAI研究室には学習・推論を回した記録だけを載せる。研究の外側の作業の記録はロボティクスデザイン実務フローに置く
- AI活用の相談は研究の副産物として受ける。看板にはせず、AI活用の記事とメッセージフォームからの導線に限る(サイトのトップ・SNS・動画には出さない)
3行目は、本人が「MASTERには1行追加しておきます」と言っていた1行を、AI の案の形で入れたものです。
決めきれなかったこと ― 本人に返した4つの問い
| 問い | なぜ聞いたか |
|---|---|
| 「事業を主・転職を従」と「目標年 2030年」は今もこの順で合っているか | 更新履歴にだけあって本文にない。動画の目的には「正社員の声がけ」も入っている |
r2r2r-content は SNS 専用になったと考えてよいか | まだ articles/ に記事を入れているなら、記事の置き場が2つになり「1種類=1ルート」の決まりに反する |
| Studio から移すための備えだったサイトのアーカイブ(site_archive)を、今後も更新するか | 移行は終わっている |
| トップの見出しのキャッチコピーは Mission より前の言葉だが、どうするか | 変えると新しいキャッチコピーの追加にあたるので、手を付けていない |
返事は「これで進めてみましょう。サイトの情報が増えてきたらまた見直しを考えます。」でした。見直した MASTER はそのまま採用され、4つの問いは次に見直すときに持ち越しています。動画のルール(video_rules.md)には「MASTER の表に videos/ の行がない」という一文が残っていますが、この文書は iCloud の制作ベースにある写しが正なので、次に動画を作るときに両方そろえて直します。
MASTER の変更の全量
変更前の MASTER と変更後の MASTER の差分です(diff -u)。
--- MASTER_before.md
+++ MASTER_after.md
@@ -1,7 +1,7 @@
# Pixels to Motors — MASTER v2
バージョン: v2(実行フェーズ)
-最終更新: 2026-10-11
+最終更新: 2026-10-17
このドキュメントは Pixels to Motors 関連プロジェクト全てに複製される。
すべての判断の上位参照ソース。個別プロジェクトのルールよりこちらが優先する。
@@ -34,10 +34,11 @@
│ ├── フィジカルAI研究室(学習・推論を回す環境)
│ ├── mechArm 270-Pi 系列
│ ├── SO-101 系列
- │ └── ハコブくん
+ │ ├── ハコブくん
+ │ └── 課題地図(フィジカルAIの社会実装の前に解くべき課題・週1回更新)
│
└── Platforms(窓・外から見せる)
- ├── Website(adaptdesign.jp/pixelstomotors)
+ ├── Website(pixelstomotors.com)
├── GitHub
├── YouTube(@PixelstoMotors)
└── X / Facebook / LinkedIn
@@ -152,6 +153,9 @@
## 運用の上位ルール
- 研究を止めない。発信は研究の副産物である
+- 研究の外側の作業(サイト・動画・発信の仕組みづくり)が続いたら、次の作業は研究に戻す。週に一度、直近の記録のうち研究の記録(学習・推論・実機・画像認識・考察)が半分を下回っていないかを見る
+- シリーズの名前は中身と一致させる。フィジカルAI研究室には学習・推論を回した記録だけを載せる。研究の外側の作業の記録はロボティクスデザイン実務フローに置く
+- AI活用の相談は研究の副産物として受ける。看板にはせず、AI活用の記事とメッセージフォームからの導線に限る(サイトのトップ・SNS・動画には出さない)
- フォロワー数を主指標にしない
- 同じコンテンツを媒体間で単純流用しない(X / LinkedIn / Facebook で書き分け)
@@ -162,20 +166,20 @@
現在のシリーズ・機体の稼働状況・技術トピックは本文書に書かない(変わるため)。機体の定数・恒久ルールは arm_kinematics.md を参照する。
-シリーズの正は、公開されている記事(adaptdesign.jp/pixelstomotors)。タイトルは公開時に調整することがあるため、GitHub の articles/ や作業フォルダのタイトルと公開記事が違う場合は公開記事に合わせる。記事化の前に、GitHub の articles/ の直近記事と記事の作業フォルダ(シリーズ名のフォルダ)で最新の Log 番号を確認し、どのシリーズの何番目の Log かをエイイチに確認してから書く。引き継ぎ書(handoff)は使わない。
-
-報酬モデル: VLM-as-Reward 方式(実機動作 → iPhone 撮影 → API で成否判定 → 次試行へフィードバック)。ハード追加なし。
+シリーズの正は、公開されている記事(pixelstomotors.com)。タイトルは公開時に調整することがあるため、GitHub の articles/ や作業フォルダのタイトルと公開記事が違う場合は公開記事に合わせる。記事化の前に、GitHub の articles/ の直近記事と記事の作業フォルダ(シリーズ名のフォルダ)で最新の Log 番号を確認し、どのシリーズの何番目の Log かをエイイチに確認してから書く。引き継ぎ書(handoff)は使わない。
---
-## データ保管(git管理外の生成物・測定データ)
+## データ保管
- **コード** = git(`~/r2r2r-scripts/` ローカル)
-- **記事・SNS(公開用)** = GitHub(`PixelstoMotors/r2r2r-content`)
+- **公開サイト(記事・サムネイル・図解・ページのコード)** = GitHub(`PixelstoMotors/p2m-site`。main に push すると公開される)
+- **記事・動画の画像と動画(公開用)** = Cloudflare R2(`p2m-media`。GitHub には入れない)
+- **SNS(公開用)** = GitHub(`PixelstoMotors/r2r2r-content` の `sns/`)
- **記事の作業ファイル・記事に載せる画像** = iCloud Drive(`PixelstoMotors/デジタルツイン学習R2R2R/{シリーズ名}/`)
- **生成物・測定データ** = KIOXIA SSD(`/Volumes/KIOXIA/r2r2r-data/`)
-記事・SNS の GitHub への反映は、Claude が直接 commit・push する。Mac の `~/r2r2r-content` は普段開かない。Mac でこのフォルダを使って作業するときは、最初に `git pull` を実行する。
+記事・SNS の GitHub への反映は、Claude が直接 commit・push する。サイトの手順の正は p2m-site の README.md。Mac の `~/r2r2r-content` は普段開かない。Mac でこのフォルダを使って作業するときは、最初に `git pull` を実行する。
> 台帳(`pick_place_ledger_v3.csv`)の列・状態語・pose_id 連携・追記条件は `data_manifest_rules.md` を唯一の正とする。本節は保存先(種類→場所)を定める。
@@ -210,16 +214,20 @@
| 論文の原文 PDF(調査メモと対・同名) | `iCloud Drive/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/research/pdf/` |
| 記事の作業ファイル一式(記事MD・サムネイル・図解・SNS の txt/json) | `iCloud Drive/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/デジタルツイン学習R2R2R/{シリーズ名}/` |
| 記事に載せる画像(GitHub には入れない) | `iCloud Drive/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/デジタルツイン学習R2R2R/{シリーズ名}/DAY{NNN}_images/` |
-| 記事・SNS の公開用ファイル | GitHub `PixelstoMotors/r2r2r-content` の `articles/`(記事MD・サムネイル・図解)と `sns/`(txt・json) |
+| 記事の公開用ファイル(記事MD・サムネイル・図解) | GitHub `PixelstoMotors/p2m-site`(記事は `src/content/records/`。入れ方は `tools/add_article.py`) |
+| 記事・4コマに載せる画像と動画(公開用) | Cloudflare R2 `p2m-media`(Mac で `tools/upload_media.py`) |
+| SNS の公開用ファイル | GitHub `PixelstoMotors/r2r2r-content` の `sns/`(txt・json) |
| フィジカルAI研究室シリーズの実行ログ・動画(クラウドGPUでの推論・評価) | `lab/Log{NN}/` |
| 単体記事(考察記事など・シリーズに属さない記事)の作業ファイル一式 | `iCloud Drive/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/デジタルツイン学習R2R2R/記事/` |
| 単体記事に載せる画像(GitHub には入れない) | `iCloud Drive/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/デジタルツイン学習R2R2R/記事/DAY{NNN}_images/` |
| 公開サイト一式のアーカイブ(全ページMD・画像・動画・図解HTML・CMS原本・作業ファイルとコードのコピー・索引) | `iCloud Drive/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/site_archive/` |
+| 解説動画の作り方のファイル(台本・字幕・スクリプト。共通の部品は `動画/_制作ベース/`) | `iCloud Drive/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/デジタルツイン学習R2R2R/動画/{動画名}/` |
+| 解説動画の mp4・アップロード用一式・ショート(重いので iCloud に置かない) | `videos/{動画名}/`(フォルダの形は video_rules.md) |
### 保管ルール
- 書き込み前に必ずマウントを確認する(`ensure_ssd_mounted()`)。未マウントならローカルへ fallback せず中断して報告する
-- コピーのみ。既存ファイルの上書き・移動・削除はしない
+- 測定データ(`runs/`・`mecharm_calibration/`・`datasets/`・`checkpoints/` など)はコピーのみ。既存ファイルの上書き・移動・削除はしない
- 測定データは「データと対のコード」をセットで置く
- スクリプトの出力先は保存先テーブルで決め打ちする。出力先を新設するなら、まずテーブルを更新してから実装する
- 実機の通し完走スクリプトは、本線・派生を問わず、完走/中断時に必ず台帳へ1行追記する。台帳追記処理を持たない通し完走スクリプトを作らない(索引CSVは補助・台帳が正)
@@ -230,7 +238,7 @@
## AIツール分担(厳守)
-- Claude: 設計・戦略・ドキュメント・記事制作・レビュー指示文。記事・SNS の GitHub への反映、Mac 連携でのファイル保存、クラウド環境での手順の検証も行う(実機を動かすコードは書かない)
+- Claude: 設計・戦略・ドキュメント・記事と動画の制作・レビュー指示文。サイト(p2m-site)のコードと公開、記事・SNS の GitHub への反映、Mac 連携でのファイル保存、クラウド GPU での学習・推論の手順の実行と検証も行う(実機を動かすコードは書かない)
- Claude Code: 実機・Unity・r2r2r-scripts のコード実装
- codex / gemini: コードレビュー(実装後の外部チェック)
- エイイチ: 最終判断・実機操作・目視確認
@@ -242,6 +250,7 @@
設計(Claude)→ 実装(Claude Code)→ レビュー(codex / gemini テンプレート: code_review_request_template.md)→ 修正 → 実機テスト(エイイチ)→ 記事化・memory 反映
- 物理的リスクがある変更(関節制御・姿勢系)は必ずレビューを挟む。順序を飛ばさない
+- 物理的リスクのない変更(サイト・動画・記録の整理のコード)は、公開前にビルドと実際の画面で確かめる。外部レビューは、公開後に戻しにくい変更(URL・転送・フォーム・データの消去)のときに挟む
- 同じ書き手で書いて確認すると同じ盲点を持つ。外部の目を必ず通す
- 最終判断はエイイチにある。レビューはエイイチが判断するための材料であって、エイイチの決定を縛るものではない
@@ -267,9 +276,9 @@
---
-## 記事制作ルール
+## 記事・動画制作ルール
-記事・SNS 制作の詳細ルールは article_rules.md を唯一の正とする。MASTER では再記述しない。
+記事・SNS 制作の詳細ルールは article_rules.md を唯一の正とする。動画制作の詳細ルールは video_rules.md を唯一の正とする。MASTER では再記述しない。
**記事化を依頼されたら、まず article_rules.md を読む。** フルコード掲載・実機ログ全量掲載を含む絶対ルールも article_rules.md に集約し、記事化依頼時に自動適用する。
---
@@ -298,4 +307,5 @@
- 2026-10-11 (DAY326): 運用の実態に合わせて改訂。Projects に「フィジカルAI研究室(学習・推論を回す環境)」を追加。設備投資方針に「買う前に借りて測る」(クラウド GPU の時間借りで実測・型番と費用の記録・使用後は削除まで・削除前にログを手元へ)を追加。記事・SNS は Claude が GitHub(PixelstoMotors/r2r2r-content)へ直接反映し、Mac の ~/r2r2r-content は作業前に git pull する運用を明記。保存先テーブルに記事の作業ファイル・記事画像(iCloud のシリーズ名フォルダ、画像は GitHub に入れない)と公開用ファイル(GitHub の articles/・sns/)の3行を追加。引き継ぎ書(handoff)の運用を廃止し、シリーズの確認方法を「公開記事が正・GitHub の直近記事とシリーズフォルダで Log 番号を確認し、書く前にエイイチに確認」に一本化。機体の恒久ルールの参照先を arm_kinematics.md に一本化。KIOXIA への書き込み条件を「Mac 連携でフォルダが接続されているときのみ」に修正。AI ツール分担を実態に合わせ、Claude の作業範囲を追記、外部レビューを codex / gemini に。保管ルールに git の個別 add と git clean 禁止を追加(Unity の節の git clean 禁止はそのまま残す)。ブランド名の行の太字の閉じ忘れを修正。
- 2026-10-11: 保存先テーブルにフィジカルAI研究室シリーズの実行ログ・動画の行を追加(lab/Log{NN}/)。
- 2026-10-11 (DAY329): 保存先テーブルに単体記事の2行を追加(作業ファイル一式 → デジタルツイン学習R2R2R/記事/・画像 → 記事/DAY{NNN}_images/)。シリーズに属さない記事の置き場所が決まっていなかったため。
-- 2026-10-12: 保存先テーブルに公開サイト一式のアーカイブの行を追加(site_archive/)。Studio から将来移行するときに備え、サイトの全ページ・CMS原本・作業ファイル・コードを1フォルダにまとめたため。更新は tools/ のスクリプトで行い、入口は README.md。
\ No newline at end of file
+- 2026-10-12: 保存先テーブルに公開サイト一式のアーカイブの行を追加(site_archive/)。Studio から将来移行するときに備え、サイトの全ページ・CMS原本・作業ファイル・コードを1フォルダにまとめたため。更新は tools/ のスクリプトで行い、入口は README.md。
+- 2026-10-17: 運用の上位ルールに3行を追加(研究の外側の作業が続いたら研究に戻す・週1回、研究の記録が半分を下回っていないかを見る/フィジカルAI研究室には学習・推論の記録だけを載せる/AI活用の相談は研究の副産物として受け、看板にせず記事とフォームの導線に限る)。直近20本の記録のうち研究の記録が7本で、研究室の名前の下に動画制作の記録が入っていたため。サイトの移転に合わせ、Website と公開記事の正を pixelstomotors.com に、記事の公開先を p2m-site、画像と動画を R2 に改めた(r2r2r-content は SNS 用)。保存先テーブルに動画の2行を追加(既に使っていたのに未記載だった)。「コピーのみ」の対象を測定データに限定。Projects に課題地図を追加。基本情報から報酬モデルの行を削除(技術トピックは本文書に書かないという自身の決まりに反していたため)。AI ツール分担に Claude のサイト・動画・クラウド GPU の作業を追記。物理的リスクのない変更の確かめ方を明記。動画制作ルールの正として video_rules.md を参照に加えた。
\ No newline at end of file
決めたことと、見直す時期
| 決めたこと | 中身 |
|---|---|
| 看板 | フィジカルAI のまま変えない。トップ・SNS・YouTube に AI活用を足さない |
| AI活用の入口 | AI活用の記事の末尾の案内と、メッセージフォームの「ご相談の種類」だけ |
| シリーズの名前 | フィジカルAI研究室は学習・推論を回した記録だけ。研究の外の記録は実務フロー |
| トップの説明 | いまの研究の中身(データを集め、学習させ、動かして確かめる環境を公開)に合わせた |
| 研究の時間 | 研究の外の作業が続いたら研究に戻す。週に一度、研究の記録が半分を下回っていないかを見る |
| MASTER | 軸は変えず、実態とずれた所を直し、上の決まりを書き足した |
見直す目安は、アクセス解析の数字がたまる4週間ほど後です。見るのは2つで、AI活用の記事から相談が来ているか(フォームの種類で分かる)と、研究の記録が直近の半分を保てているかです。AI活用の記事から仕事の相談が来るようであれば、そのときに看板の扱いをもう一度考えます。
コードの変更の全量 ― commit e325bf6 と 30069f8
commit e325bf6 ― AI活用の記事を実務フローへ移し、相談の導線を足す
commit e325bf6ab1a881eec3a78b7ea5d442ebc1a45989
Author: Claude <noreply@anthropic.com>
Date: Sat Oct 17 12:14:53 2026 +0900
AI活用の記事を実務フローへ移し、相談の導線を足す
- 動画制作の記録7本を「フィジカルAI研究室」から「ロボティクスデザイン実務フロー」へ移す(研究室は学習・推論の回だけにする)
- AI活用の記事13本の末尾に、相談の案内(/message?topic=ai)を出す
- 実務フローのシリーズページに説明文を足す
- メッセージフォームに「ご相談の種類」を足し、保存とメールの件名に入れる
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8PyTPCaRx7Mg89h22HK6a
diff --git a/README.md b/README.md
index 5c1c9c9..9ac099e 100644
--- a/README.md
+++ b/README.md
@@ -31,4 +31,9 @@ GitHub の main に push すると、Cloudflare が自動で作り直して公
- 効果の記録:「出た・吹き出しを押した・押さずに7日以内に見に来た」を `/api/hk-event` が R2(p2m-messages の `hk-events/`)に1件ずつ残す。数は `/api/hk-stats`(週ごと・日ごと・更新ごと・画面の幅ごとの表。週ごとの行は週が変わると自動で増える)・`/api/hk-stats?format=json` で見る。見た人を見分ける情報は残さない
記事・4コマの画像と動画は GitHub に入れない。ただし `public/og/` の画像は、GitHub にあるサムネイルの HTML から書き出したものなので GitHub に入れる。サイトの `/media/ファイル名` を `worker/index.js` が R2 から返す。
-メッセージフォームの送信は R2(p2m-messages)に保存する。
+メッセージフォームの送信は R2(p2m-messages)に保存する。フォームには「ご相談の種類」(フィジカルAI・ロボット/AI活用・仕組みづくり/取材・登壇/その他)があり、保存する JSON とメールの件名に入る。`/message?topic=ai` のように開くと、その種類を選んだ状態になる。値の一覧はフォーム(`src/pages/message.astro`)と `worker/index.js` の `TOPICS` の2か所にあるので、増やすときは両方を直す。
+
+## AI活用の記事と相談の案内
+
+AI活用の記事(フィジカルAIの研究を回すために作った仕組みの記録)は、シリーズ「ロボティクスデザイン実務フロー」(`series: "ロボティクスデザイン"`)に置く。「フィジカルAI研究室」は学習・推論を回す回だけにする。
+AI活用の記事の末尾には、相談の案内(`/message?topic=ai` への導線)を出す。出す記事は、Markdown の記事なら先頭の項目に `ai_consult: true` を書く。Studio から移した記事は `src/data/aiConsult.json` に slug を足す。サイトのトップ・SNS・動画には AI活用の相談を出さない。
diff --git a/src/content.config.ts b/src/content.config.ts
index 565ceda..ba71872 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -12,6 +12,8 @@ const records = defineCollection({
series: z.string(),
meta_description: z.string().optional(),
tags: z.array(z.string()).optional(),
+ // AI活用の記事(研究を回すために作った仕組みの記録)。true なら記事の末尾に相談の案内を出す
+ ai_consult: z.boolean().optional(),
}),
});
// 課題地図(/issues)。課題1件=1ファイル。先頭の項目がカード(点数・分類など)の唯一の正
diff --git a/src/content/records/DAY334.md b/src/content/records/DAY334.md
index 42ca0a7..c4946cb 100644
--- a/src/content/records/DAY334.md
+++ b/src/content/records/DAY334.md
@@ -6,6 +6,7 @@ day: 334
meta_description: "Studio製のサイトの全533ページを、Studioがなくても読める形で1フォルダにまとめた。画像・図解・作業ファイル・コードも収め、別のAIのcodexに2回検証させて、変換の崩れと作業場所の取り違えを直した。"
tags: [サイトアーカイブ, Studio, CMS, Markdown, Nuxt, iCloud, codex, 敵対的検証, Python, JavaScript]
series: "ロボティクスデザイン"
+ai_consult: true
---
このサイト(adaptdesign.jp/pixelstomotors)は、Studio というノーコードのWeb制作サービスで作っています。記事やマンガは Studio の CMS(記事を項目ごとに入れておく仕組み)に入っていて、本文も画像も Studio の中にしかありません。いつか別の仕組みへ移るときに、手元に何もない状態から始めることになります。
diff --git a/src/content/records/DAY335.md b/src/content/records/DAY335.md
index 0c47449..68198ac 100644
--- a/src/content/records/DAY335.md
+++ b/src/content/records/DAY335.md
@@ -5,7 +5,8 @@ date: 2026-10-12
day: 335
meta_description: "フィジカルAIの課題をまとめたサイトから動画ネタを25本出し、見る前に気になるかで15本に絞った。完成済みの台本と同じ形式で15本を書き、別の担当の事実確認で29か所を直した。"
tags: [YouTube, 解説動画, 台本, ネタ出し, 事実確認, セキュリティ, ハコブくん, Claude]
-series: "フィジカルAI研究室"
+series: "ロボティクスデザイン"
+ai_consult: true
---
Pixels to Motors の YouTube チャンネルで出す解説動画の、ネタと台本を Claude と一緒に作りました。材料にしたのは、フィジカルAI(ロボットのように体を持って動くAI)が社会で使われるまでに解くべき課題を36件まとめた、自作のサイトです。
diff --git a/src/content/records/DAY338.md b/src/content/records/DAY338.md
index 7c64709..4b4a872 100644
--- a/src/content/records/DAY338.md
+++ b/src/content/records/DAY338.md
@@ -5,7 +5,8 @@ date: 2026-10-15
day: 338
meta_description: "YouTube解説動画の作り方を2つ足した。自分で試す回の台本の型と、画面の文字を名前・数字・一言の15字以内にする大文字型。大文字型で「ロボットの動きはなぜ怖い?」を11分50秒の本編とショート3本まで作った。"
tags: [YouTube, 解説動画, ハコブくん, 動画制作, 画面設計, 不気味の谷, codex, ElevenLabs, Python, Playwright]
-series: "フィジカルAI研究室"
+series: "ロボティクスデザイン"
+ai_consult: true
---
YouTube の解説動画の作り方を、2つ増やしました。
diff --git a/src/content/records/DAY339.md b/src/content/records/DAY339.md
index 964a714..7224305 100644
--- a/src/content/records/DAY339.md
+++ b/src/content/records/DAY339.md
@@ -5,7 +5,8 @@ date: 2026-10-16
day: 339
meta_description: "並行する解説動画21本の段階と誰の番かを、フォルダとYouTubeから自動で判定するページを作った。公開予約はYouTube Data APIで読み、PCは一覧、iPhoneはハコブくんのカードで1件ずつ片付ける形にした。"
tags: [YouTube, 解説動画, ハコブくん, 進捗管理, YouTube Data API, Claude Code, プラグイン, iPhone, Python, JavaScript]
-series: "フィジカルAI研究室"
+series: "ロボティクスデザイン"
+ai_consult: true
---
YouTube の解説動画を、何本も並行して作るようになりました。この記事を書いている時点で、作業フォルダのある動画は21本あります。そのうち12本が公開予約済み、6本が公開済み、3本がアップロード待ちです。まだ作業フォルダのない台本の候補も8本あります。
diff --git a/src/content/records/DAY340.md b/src/content/records/DAY340.md
index 3c3c73c..7a4664f 100644
--- a/src/content/records/DAY340.md
+++ b/src/content/records/DAY340.md
@@ -6,6 +6,7 @@ day: 340
meta_description: "Studio製のサイト553ページを、Astro+Cloudflare Workers で push すると約40秒で公開される形に移した。画像1,004点は R2 へ。本文・画像数・図解数を全ページ照合し一致。"
tags: [Studio, Astro, Cloudflare Workers, R2, GitHub, 自動公開, サイト移行, Cloudflare Access, Playwright, Python]
series: "ロボティクスデザイン"
+ai_consult: true
---
このサイト(adaptdesign.jp/pixelstomotors)は、Studio というノーコードの Web 制作サービスで作っています。記事は Claude が Markdown で書き、それを Studio の編集画面に貼り付け、画像を1枚ずつ入れ、図解の HTML を埋め込みブロックに貼って公開していました。1本あたり数十分の手作業です。前回(LOG334)でサイトの中身を Studio の外へ持ち出せる形にまとめたので、次はその手作業そのものをなくす番でした。
diff --git a/src/content/records/DAY342.md b/src/content/records/DAY342.md
index 1334b8a..a941060 100644
--- a/src/content/records/DAY342.md
+++ b/src/content/records/DAY342.md
@@ -6,6 +6,7 @@ day: 342
meta_description: "サイト用に pixelstomotors.com を取得し、URL から /pixelstomotors を外した。旧URL554件と内部リンク1万5千本余りを照合。動画が再生されない原因は Range 応答の NaN だった。"
tags: [Cloudflare Workers, Cloudflare Registrar, R2, HTTP Range, ドメイン, サイト移行, Astro, 301リダイレクト, アクセス解析, UX]
series: "ロボティクスデザイン"
+ai_consult: true
---
前回(LOG340)で、Studio で作っていたこのサイトの 553 ページを、Astro と Cloudflare Workers の仕組みに移しました。移したサイトは仮の住所(workers.dev)に置き、ログインした本人しか見られない鍵をかけていました。その続きです。
diff --git a/src/content/records/LOG343.md b/src/content/records/LOG343.md
index 74d2e94..fc5c3ca 100644
--- a/src/content/records/LOG343.md
+++ b/src/content/records/LOG343.md
@@ -6,6 +6,7 @@ day: 343
meta_description: "pixelstomotors.com を公開し、フォームの通知メール、adaptdesign.jp の DNS 移行と記事まで届く転送、Studio の非公開化、記事番号の DAY → LOG 改番までを行った記録。"
tags: [Cloudflare, Cloudflare Access, Email Routing, DNS, DNSSEC, Redirect Rules, Squarespace, Studio, サイト移行, Astro]
series: "ロボティクスデザイン"
+ai_consult: true
---
前回(LOG342)で、サイト用のドメイン pixelstomotors.com を取り、Cloudflare Workers で動く新しいサイトにつなぎました。ただ、ログインした本人しか見られない鍵がかかったままで、公開はしていませんでした。その続きです。
diff --git a/src/content/records/LOG344.md b/src/content/records/LOG344.md
index ac21d28..4c97d8b 100644
--- a/src/content/records/LOG344.md
+++ b/src/content/records/LOG344.md
@@ -6,6 +6,7 @@ day: 344
meta_description: "公開中のサイトに検索除外の設定が残っていた。AIが全557ページを点検し、サイトマップ・画像の説明1万枚・構造化データ・日英の対応・SNS用画像・アクセス解析までを直した記録。"
tags: [SEO, noindex, sitemap.xml, alt, 構造化データ, JSON-LD, hreflang, OGP, Google Search Console, Cloudflare Web Analytics, Astro, Playwright]
series: "ロボティクスデザイン"
+ai_consult: true
---
サイトを新しくして公開した翌朝、AI に「サイトの SEO 対策がちゃんとできているか、全部確認して」と頼みました。SEO は、Google などの検索結果に自分のページが出やすくなるようにする手入れのことです。
diff --git a/src/data/aiConsult.json b/src/data/aiConsult.json
new file mode 100644
index 0000000..72f0843
--- /dev/null
+++ b/src/data/aiConsult.json
@@ -0,0 +1,7 @@
+[
+"speak-to-relearn-robotics",
+"making-explainer-video-with-ai",
+"explainer-video-character-narration",
+"youtube-channel-identity",
+"first-video-end-to-end"
+]
diff --git a/src/data/cards.json b/src/data/cards.json
index c933abe..2ce9d88 100644
--- a/src/data/cards.json
+++ b/src/data/cards.json
@@ -1855,7 +1855,7 @@
},
{
"day": "LOG331",
-"series": "フィジカルAI研究室",
+"series": "ロボティクスデザイン",
"published": "2026-10-12T00:08:34.000Z",
"title": "解説動画のナレーションをハコブくんに任せる ― 口と目と光をコードで動かし、文字が重ならない区画を決めた",
"slug": "explainer-video-character-narration",
@@ -1909,7 +1909,7 @@
},
{
"day": "LOG333",
-"series": "フィジカルAI研究室",
+"series": "ロボティクスデザイン",
"published": "2026-10-12T07:58:00.000Z",
"title": "解説動画の1本目を公開まで通す ― 声・BGM・間・直し・サムネイルを部品ごとに決め、23手順にまとめた",
"slug": "first-video-end-to-end",
@@ -2134,7 +2134,7 @@
},
{
"day": "LOG330",
-"series": "フィジカルAI研究室",
+"series": "ロボティクスデザイン",
"published": "2026-10-11T13:46:46.000Z",
"title": "解説動画をAIと作る ― 型の分析から敵対的検証まで、試作1本で決めたこと",
"slug": "making-explainer-video-with-ai",
@@ -3061,7 +3061,7 @@
},
{
"day": "LOG332",
-"series": "フィジカルAI研究室",
+"series": "ロボティクスデザイン",
"published": "2026-10-12T00:28:45.000Z",
"title": "YouTubeチャンネルの顔を作る ― チャンネル名・アイコン・バナーをハコブくんにそろえるまで",
"slug": "youtube-channel-identity",
diff --git a/src/data/records/explainer-video-character-narration.json b/src/data/records/explainer-video-character-narration.json
index b17d4a3..019e433 100644
--- a/src/data/records/explainer-video-character-narration.json
+++ b/src/data/records/explainer-video-character-narration.json
@@ -1 +1 @@
-{"day": "LOG331", "series": "フィジカルAI研究室", "published": "2026-10-12T00:08:34.000Z", "title": "解説動画のナレーションをハコブくんに任せる ― 口と目と光をコードで動かし、文字が重ならない区画を決めた", "slug": "explainer-video-character-narration", "cover": null, "thumb": "/thumbs/r-explainer-video-character-narration.html", "lang": "JP", "description": "YouTube解説動画のナレーションを本人の声ではなくキャラクターのハコブくんに任せると決め、口・目・側面の光をコードで動かす試作を作った記録。字幕や図が重ならない区画と自動の重なり確認も決めた。", "updated": "2026-10-12T00:09:22.000Z", "body": "<!--TOC--><p></p><p>Pixels to Motors の YouTube 解説動画について、続きの作業を Claude と対話しながら進めました。前回(解説動画をAIと作る ― 型の分析から敵対的検証まで、試作1本で決めたこと)で試作1本と制作ルールができたので、今回は「何を話すか」と「誰が話すか」を決めています。</p><p>やったことは4つです。サイトの記事から、ロボティクスに関心のある経営者・PM・マネージャー・開発者に向けた動画の企画を5本立てる。ナレーションの声と、BGM・効果音の候補を、公式の利用条件で比べる。ナレーションを本人の声ではなく、Pixels to Motors のキャラクターのハコブくん(病院で物を運ぶロボット)にすると決める。ハコブくんを画面の左下に置き、口・目・側面の光をコードで動かす試作を作る。</p><p>試作は、フィードバックを受けて十数回作り直しました。口の形、目の動き、側面の光り方、頭の上の切り抜き、顔の傾き、置く大きさを、ひとつずつ直しています。本記事は、その過程で決めたことと、決めた理由の記録です。間違えた直し方(凸包で頭を埋めた、角の点で傾きを測った、など)も消さずに残しました。試作スクリプトの全文と実行ログは記事の後半に載せています。</p><p></p><h2 id=\"index_Wy0UmY2h\">動画の企画5本は、見た人が「この人に何を頼めるか」が分かる話を選んだ</h2><p>動画の目的は、見た人から仕事の相談や依頼をもらうことです。そこで、個人の体験談よりも、見る人が自分の会社やチームの判断に使える話を優先しました。サイトの記事を読み直し、経営者・PM・開発者に向けて5本の企画を立てています。</p><p>選ぶときの基準は、自分の机の上で実測した話が入っていることです。業界の解説だけなら、ほかの人にも作れます。実測の数字が入っていると、この人に頼むと何をしてくれるのかが見る人に伝わります。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>公開する順</p></th><th colspan=\"1\" rowspan=\"1\"><p>企画</p></th><th colspan=\"1\" rowspan=\"1\"><p>冒頭の一言</p></th><th colspan=\"1\" rowspan=\"1\"><p>元にした記事</p></th><th colspan=\"1\" rowspan=\"1\"><p>見た人に伝わること</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>AIエージェントにロボットのコードを書かせたら、原因を7回取り違えた</p></td><td colspan=\"1\" rowspan=\"1\"><p>テストは53件、全部通った。実機に出したら、アームの動きは前と何も変わっていなかった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG294、LOG273、LOG275、LOG170</p></td><td colspan=\"1\" rowspan=\"1\"><p>AIで書く・別のAIでレビューする・実機で小さく通す、という手順を組める</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィジカルAIの研究環境は、買う前に借りて測る</p></td><td colspan=\"1\" rowspan=\"1\"><p>GPUメモリは34,399MiB使われているように見えた。実際に使っていたのは8,486MiBだった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG326、LOG327</p></td><td colspan=\"1\" rowspan=\"1\"><p>本格導入の前の小さな検証(PoC)を、安く速く回せる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィジカルAIの導入は、ロボットを買う前の「仕事の切り出し」で止まっている</p></td><td colspan=\"1\" rowspan=\"1\"><p>清掃ロボットの管理画面に出る4項目は、故障を調べるためではなく、契約どおり掃除したことを示す請求書だった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG306、LOG314、LOG286</p></td><td colspan=\"1\" rowspan=\"1\"><p>導入前に、どの仕事をロボットに渡し、何を人に残すかを分けられる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>ロボットの「成功」を何で数えるか ― 完走したのに、ブロックを掴めていなかった</p></td><td colspan=\"1\" rowspan=\"1\"><p>アームは最後まで止まらずに動き切った。でも、ブロックは掴めていなかった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG246、LOG248、LOG268、LOG220</p></td><td colspan=\"1\" rowspan=\"1\"><p>評価の指標と、成否を記録する仕組みを設計できる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>ロボット企業の勝ち方は3つに分かれる ― 汎用モデル、環境の作り込み、用途の固定</p></td><td colspan=\"1\" rowspan=\"1\"><p>「人型ロボット40万台」と報じられたトヨタの計画は、既存の産業用ロボット40万台の頭脳を入れ替える計画だった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG315、LOG282、LOG277、LOG329、LOG291</p></td><td colspan=\"1\" rowspan=\"1\"><p>業界の動きを構造で整理し、相手の会社の選択肢に落とせる</p></td></tr></tbody></table></div><p>公開する順は、自分の実測が中心の2本(AIエージェントの回、借りて測る回)を先にしました。ほかの人には作れない動画だからです。仕事の切り出しの回は、相談や契約にいちばん直接つながる企画ですが、実績の動画を2本見せてから出す方が説得力が出ると考えて3本目に置きました。</p><p>企画を立てる途中で、注意が要る点も3つ見つかりました。2本目は、速度やメモリの数字を必ず GPU の型番(NVIDIA A40)と組で言う。4本目は「安全装置を外した実験」の話が入るので、実験の条件(速さ、周りに人がいたか、止める手段)を言わないと、安全を軽く見ているように聞こえる。5本目は自分の実測がない回なので、会社の自己申告の数字は「会社の発表では」と言い、最後に自分の机の上で同じ判断をした場面を1つ入れる。</p><p></p><h2 id=\"index__9qJii4D\">ナレーション・BGM・効果音は、2026年10月11日時点の公式の条件で比べた</h2><p>音声は映像と別に作る前提なので、ナレーションの声、BGM、効果音の候補を、それぞれの公式ページの利用条件で確かめました。料金や条件は変わるので、表は2026年10月11日に確認した時点のものです。</p><p>ナレーションの候補は3つでした。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>方法</p></th><th colspan=\"1\" rowspan=\"1\"><p>条件(確認した時点)</p></th><th colspan=\"1\" rowspan=\"1\"><p>経営者向けの動画に合うか</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>本人の声で録る</p></td><td colspan=\"1\" rowspan=\"1\"><p>字幕ファイルに1文ごとの時間が入っているので、それに合わせて録れば映像はそのまま使える</p></td><td colspan=\"1\" rowspan=\"1\"><p>仕事の相談は人を見て決まるので、最初はこれを勧めた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ElevenLabs で声を複製する</p></td><td colspan=\"1\" rowspan=\"1\"><p>Creator プランは月11ドルの表示(初月の割引の表記あり)。商用ライセンス付きで、本格的な声の複製(Professional Voice Cloning)が1つ作れる</p></td><td colspan=\"1\" rowspan=\"1\"><p>録る時間が取れない回の代わりになる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>VOICEVOX</p></td><td colspan=\"1\" rowspan=\"1\"><p>商用・非商用を問わず無料。VOICEVOX を使ったことが分かるクレジットの表記が必要で、キャラクターごとの規約にも従う</p></td><td colspan=\"1\" rowspan=\"1\"><p>キャラクターの声は娯楽系の解説動画の印象が強く、相談先として見てもらいにくい</p></td></tr></tbody></table></div><p>BGM と効果音は次の候補です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>用途</p></th><th colspan=\"1\" rowspan=\"1\"><p>サービス</p></th><th colspan=\"1\" rowspan=\"1\"><p>条件(確認した時点)</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM・効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>YouTube オーディオライブラリ</p></td><td colspan=\"1\" rowspan=\"1\"><p>収益化している動画で使え、権利者からの著作権の申し立て(Content ID)が来ない。YouTube が著作権上安全と認めているのはここの音源だけ。Creative Commons の曲は概要欄にクレジットが要る</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM</p></td><td colspan=\"1\" rowspan=\"1\"><p>DOVA-SYNDROME</p></td><td colspan=\"1\" rowspan=\"1\"><p>無料、商用OK、クレジット不要。作曲者ごとに別の条件がある場合はそちらが優先。音楽が主役の動画やリミックスは禁止</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM・効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>Epidemic Sound</p></td><td colspan=\"1\" rowspan=\"1\"><p>有料。使用料の追加払いはない。広告に使うには Pro 以上のプランが要る</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>効果音ラボ</p></td><td colspan=\"1\" rowspan=\"1\"><p>無料、商用OK、YouTube の収益化OK、クレジット不要。効果音だけを流す動画は再配布として禁止</p></td></tr></tbody></table></div><p>効果音は、自分のロボットアームのサーボが回る音やグリッパーが閉じる音を録って、章の扉の音にする案も出しました。権利の心配がなく、このチャンネルにしかない音になります。入れる場所は、動画のルールで「打ち出し」と決めている場面(1本に6〜10場面)だけにしぼります。</p><p></p><h2 id=\"index_F7VUaN1Y\">ナレーションは、本人の声でも声の複製でもなく、ハコブくんに決めた</h2><p>声の候補を並べたあと、ナレーションをハコブくんにしたいという相談がありました。</p><blockquote><p>音声はハコブくんをナレーションキャラにしようと思いますが、自分の声の複製とどちらが良いですか?(自分の声は別に良いとは思わない。声が通らない)</p></blockquote><p>ハコブくんの方が良い、と答えました。理由は3つです。声の複製は元の声の性質をそのまま写すので、声が通らないのが不満なら、複製しても通らない声を毎回使うことになります。ハコブくんはすでに Pixels to Motors の4コママンガに出ているキャラクターで、開発ログ(day_log)の発信戦略にも「実機展開以降、ハコブくんをR2R2Rの概念解説役として再登場させる」と書いてありました。もう1つは、本人の実機の話を「エイイチの机の上では、こうなりました」と三人称で言えることです。動画のルールにある「自分の実機の話は、自分の条件の話として言う」が、言い方だけで自然に守れます。</p><p>ただ、目的が仕事の相談をもらうことなので、キャラクターの後ろで本人が見えなくなると効果が落ちます。そこで次の3つを必ず入れると決めました。</p><ul><li><p>自己紹介の場面で、本人の名前のテロップと実機の映像を出す</p></li><li><p>意見を話す節は「ここからはエイイチの見立てです」と言ってから入る(意見が誰のものかをはっきりさせる)</p></li><li><p>概要欄にプロフィールと連絡先を置き、「ナレーションは AI 音声です」と書く</p></li></ul><p>声は、ほかのチャンネルと同じ声(VOICEVOX のキャラクターの声など)を使わず、ハコブくん専用の声にします。「ハコブくん=有名なキャラクターの声のロボット」と聞かれないようにするためです。作り方の候補は2つで、ElevenLabs の Voice Design(文章で声の特徴を書くと新しい声を作る機能)か、声優に頼んで作ってもらうかです。Voice Design は、日本語での出来と、作った声を商用で使う条件が公式の説明に書かれていなかったので、試して確かめる必要があります。話し方は、高すぎない声でゆっくり丁寧に、経営者が13分聞いて疲れないことを基準にしました。</p><p></p><h2 id=\"index_O4m3dwLv\">ハコブくんは画面の左下に置き、生成した10秒の動画の1コマから体を切り抜いた</h2><p>置き方のイメージとして、参考にしている解説動画(前回、型を分析した3本のうち、会社への侵入と暗数の回)の画面が共有されました。画面の左下でサングラスの猫のキャラクターがマイクの前で話していて、字幕は猫の右側から始まっています。</p><blockquote><p>画面の左端にこのような形で話しているハコブくんを追加するイメージです。</p></blockquote><p>あわせて、病室にいるハコブくんの生成動画(10秒)が共有されました。最初に考えたのは、この動画をそのまま左下で繰り返し流す方法ですが、やめました。動画の中の口は声と関係なく開閉するので、話していない間も口が動きます。10秒の動画を13分繰り返すと、同じ動きの繰り返しだと気づかれます。背景の病室を毎フレーム切り抜くと、縁が揺れます。</p><p>そこで、体は動画の1コマを切り抜いた静止画にし、顔だけをコードで描く方法にしました。ハコブくんの顔はディスプレイなので、画面の中の目と口を描き替えれば話しているように見せられます。</p><p>動画は 848×1056 ピクセル・24fps・10.04秒でした。1秒に4枚ずつ、40枚のコマを取り出して並べ、全身と側面の緑のリングが見えている5枚目を選んでいます。背景の切り抜きには rembg(画像から人や物の背景を消すライブラリ)の isnet-general-use というモデルを使いました。</p><p></p><figure><img src=\"/media/s-1920x956_v-frms_webp_dd2ed6ca-70c6-4449-8572-3287b85ed201.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_g2Fv9MC5\">顔の目と口は、元の絵を消してコードで描き直した</h2><p>切り抜いた体のディスプレイを測り、元の目と口の位置と色を取りました(測定の全量は記事後半の実行ログにあります)。ディスプレイは横183×縦151ピクセル、元の口の色は #57B5F8 の水色です。元の目と口はディスプレイごと黒(#131311)で塗りつぶし、その上に同じ位置で目と口を描きます。</p><p>口はナレーションの音量に合わせて動かします。音声を1フレーム(1/30秒)ずつ区切って音量を測り、大きいほど口を大きく開く形にしました。最初の試作は、目が白い楕円、口が「閉じた笑顔/小さく開く/大きく開く」の3段階で、体は左を向いたままでした。</p><p>【画像】DAY331_03_first_prototype_faces.png</p><p>体の向きは、すぐに直しが入りました。</p><blockquote><p>ハコブくんは右向きがいいですね。</p></blockquote><p>左下に置くと、右を向けば画面の中央(図と字幕の側)を向くことになります。体の画像を左右反転して右向きにしました。</p><p>ここで1つ間違えています。その少し前に13種類の表情のイメージ(表情集)が共有されていて、表情集の顔は目も口も同じ薄い水色でした。右向きに直すときに、目と口を両方その水色(#BDE8FB)に変えてしまいました。</p><blockquote><p>色は目は白、口は水色のままでいいですよ</p></blockquote><p>表情集は形の参考で、色は元の動画のままでよかった、ということです。目を白、口を元の水色に戻しました。</p><p></p><figure><img src=\"/media/s-1920x1216_v-frms_webp_0e7f1609-60a9-4748-b723-9db00d3fde2a.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_Ufl5ndIQ\">口は音量で4段階に切り替え、1回に1段階ずつしか動かさない</h2><p>最初の口は、閉じた口からいきなり大きい口に変わることがあり、角も立っていました。</p><blockquote><p>口はもっと柔らかさを出すために角を丸くした方がいいですね(小さい口を途中に入れたりすると動きが出てより良いです)</p></blockquote><p>口の形を4段階にしました。閉じた口は両端が丸い太い弧、小さい口は上が少し広く下が丸い形、中と大は角を丸めた三日月形です。角は、4倍の大きさで形を描いてから、半径5〜6ピクセル相当の丸で角を削っています(モルフォロジーのオープン処理。小さな丸が入り込めない角だけが削れる)。</p><p>切り替え方も変えました。音量から決めた段階に向かって、1回に1段階ずつしか動かしません。閉じた口から大きい口へ行くときは、必ず小と中を通ります。</p><p>次の指摘は速さでした。</p><blockquote><p>口の動きや目の動きをもう気持ちだけゆっくりに動かしたほうがいいね(今は早すぎる。話し方も早すぎなのかもだけど)</p></blockquote><p>口の形を変えるのを、毎フレーム(1秒に30回)から2フレームごと(1秒に15回)にしました。仮の音声の話す速さも1.08倍から1.0倍に落としています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>最初</p></th><th colspan=\"1\" rowspan=\"1\"><p>最後</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>口の段階</p></td><td colspan=\"1\" rowspan=\"1\"><p>3段階(閉じる・小・大)</p></td><td colspan=\"1\" rowspan=\"1\"><p>4段階(閉じる・小・中・大)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>口の切り替え</p></td><td colspan=\"1\" rowspan=\"1\"><p>毎フレーム、段階を飛ばしてよい</p></td><td colspan=\"1\" rowspan=\"1\"><p>2フレームごと、1回に1段階</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>口の角</p></td><td colspan=\"1\" rowspan=\"1\"><p>尖っている</p></td><td colspan=\"1\" rowspan=\"1\"><p>半径5〜6ピクセル相当で丸める</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>仮の音声の速さ</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.08倍</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.0倍</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>問いかけの文のあとの口</p></td><td colspan=\"1\" rowspan=\"1\"><p>「o」の口</p></td><td colspan=\"1\" rowspan=\"1\"><p>小さく開いた口のまま止める</p></td></tr></tbody></table></div><p></p><h2 id=\"index_V8JsKfRO\">目は2.8秒の周期で上下し、口より0.1秒遅れて上がる</h2><blockquote><p>目が少し上下しているように描写すると表情がイキイキして見えます。</p></blockquote><p>目に3つの動きを足しました。1つ目は、ゆっくりした上下の揺れです。最初は1.9秒の周期でしたが、速すぎるという指摘のあと2.8秒に延ばしました。2つ目は、口が大きく開くほど目が少し上がる動きです。口と同時に動くと機械的に見えるので、口より3フレーム(0.1秒)遅れて上がるようにしています。3つ目は、文の頭で目が0.3秒だけ小さく跳ねる動きです。</p><p>まばたきも、最初は3.2秒ごとの一定間隔でしたが、2〜4.5秒の間でばらつかせ、文を言い終わった直後にも1回入れました。</p><div class=\"embed\"><iframe src=\"/embeds/explainer-video-character-narration-QIb2qI10.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p></p><h2 id=\"index_W02dAGsw\">側面のリングは、元の絵を消して楕円を描き直し、顔と別の周期で光らせた</h2><p>側面の緑のリングは、最初は元の絵の画素を明るくしていただけでした。</p><blockquote><p>側面の光っている部分をもっと丁寧に描写(光方が汚く見える)</p></blockquote><p>元の絵のリングは、生成動画を縮小した画像なので画素が粗く、明るくすると粗さが目立ちます。リングの画素に楕円を当てはめて位置と大きさを測り、元のリングはまわりの色で塗りつぶして消しました(OpenCV の inpaint。消したい範囲を、まわりの画素から推測した色で埋める処理)。その上に、4倍の大きさで描いた楕円を縮めて置き、ぼかした光を重ねています。光は体の外には出しません。</p><p></p><figure><img src=\"/media/s-693x876_v-fs_webp_42809200-9e99-483a-85d0-4093607ecd3e.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>次の指摘は、光と表情が一緒に動いていることでした。</p><blockquote><p>側面の光と表情の動きが同じタイミングで繰り返しているので、ずらしたり、合わせたりして躍動感(生命感)を出すと良さそう</p></blockquote><p>普段はずらし、要所だけそろえる動きにしました。リングは、声に0.2秒遅れて明るくなり、話し終わったあとはゆっくり暗くなります(つくときは速く、消えるときは遅い)。話していない間も、息をするように少し明滅します。この明滅の周期は3.7秒で、目の揺れ(2.8秒)と周期を変えてあるので、目とリングが同じ組み合わせで繰り返しません。そろえるのは、文の頭と、口を大きく開いた瞬間だけです。</p><blockquote><p>側面の光は強く光時と、弱く光ときを混ぜ込むと良さそう(ランダム性があるといい)</p></blockquote><p>ばらつきを4か所に入れました。</p><ul><li><p>声への反応の強さを、文ごとに変える(強い文と弱い文が混ざる)</p></li><li><p>口を大きく開いた瞬間の光り方を毎回変え、3回に1回ほどは光らない</p></li><li><p>息をするような明滅の強さを、ゆっくり不規則に変える</p></li><li><p>話していない間も、ときどき弱くまたたく</p></li></ul><p>ばらつきは乱数の種を7に固定して作っているので、同じスクリプトを何度動かしても同じ光り方になります。</p><p></p><h2 id=\"index_tXF5W1d5\">目はカプセル形にし、顔はディスプレイの辺に直線を当てはめて傾けた</h2><p>目の形は、元の動画の顔に合わせる指示がありました。</p><blockquote><p>目のデザインはこれに合わせて欲しい。</p></blockquote><p>元の動画の目は、楕円ではなく、上下の端が半円になった縦長のカプセル形で、まわりに白い光がにじんでいます。カプセル形に変え、光のにじみを足しました。にじみはディスプレイの中だけに収めています。</p><p>続けて、傾きの指摘がありました。</p><blockquote><p>目は顔のディスプレイに合わせて少しだけ斜めにすると良さそう</p></blockquote><p>ハコブくんは斜め前から見た構図なので、ディスプレイも少し傾いて見えます。まっすぐ描いた顔を、ディスプレイの四隅に合わせて射影変換(四角形を別の四角形にはめ込む変形)しました。最初は、ディスプレイの輪郭を4つの角の点に近似して四隅を取りましたが、合いませんでした。</p><blockquote><p>ちょっと目と口の描写が顔のディスプレいと合ってないね。微妙な差だけど。</p></blockquote><p>ディスプレイは角が丸いので、角の点を直接取ると、丸みに引っぱられて位置がずれます。最初の四隅だと上の辺まで傾いていて、顔が回って見えていました。上下左右の辺の、まっすぐな中央4割の点にそれぞれ直線を当てはめ、直線どうしの交点を角にしました。測り直すと、上の辺は0.18度でほぼ水平、左右の辺は約6度(5.97度と5.67度)、下の辺は2.16度傾いていました。</p><p></p><figure><img src=\"/media/s-840x876_v-fs_webp_6637392f-3158-4208-b51c-0fa6fd52d0e6.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>目と口の間隔も2回、ごく少しだけ直しています。</p><blockquote><p>ほんの少しだけだけど、目と口が近い方がデザインが近くなるね(ごくごく少しだけ)</p></blockquote><blockquote><p>目と口の位置がほんの少しだけ上に(本当に少しだけ)</p></blockquote><p>1回目は、目を2ピクセル下げ、口を1.5ピクセル上げて、間を3.5ピクセル詰めました。2回目は、間隔を保ったまま両方を1.5ピクセル上げています(どちらも元の画像の大きさでのピクセル数で、動画上ではその約8割の動きです)。スクリプトでは <code>EYE_SHIFT = 0.5</code> と <code>MOUTH_SHIFT = -3.0</code> の2つの数字で調整できます。</p><p></p><h2 id=\"index_fNoBvJtt\">頭の上は、凸包で埋めると奥の段差が消えて枕まで塗っていた</h2><p>頭の上は、3回直しています。元の動画では、ハコブくんの白い頭が後ろの白い枕と重なっていて、切り抜くと頭の上が薄く欠けます。</p><blockquote><p>あとサンプルの頭の上が切れてしまっているのを修正してください。</p></blockquote><p>1回目は、欠けた部分を含めた頭の輪郭の凸包(輪郭の外側に輪ゴムをかけたような、へこみのない形)で埋めました。</p><blockquote><p>頭の描写をもっと綺麗に処理して(元のデザイン見直してください)</p></blockquote><p>2回目は、凸包の角を大きく丸めてなめらかにしました。</p><blockquote><p>まだ頭の処理が汚いです。</p></blockquote><p>ここで、欠けた部分を下の色から塗り延ばす案を出しましたが、実行する前に止められました。</p><blockquote><p>元の動画があるから見直してください!</p></blockquote><p>元の動画の40枚のコマを並べ、そのうち8枚で頭の上を拡大すると、ハコブくんの頭は、画面のすぐ上の縁が低く、奥側がもう一段持ち上がった形をしていました。凸包は、この段差を直線で結んでしまいます。結んだ内側には後ろの枕が入っていたので、枕まで頭として塗り、形が崩れて汚く見えていました。</p><p>3回目は、凸包をやめました。切り抜きがはっきり頭と判断した範囲(透明度を表すアルファ値が60以上)をそのまま輪郭に使い、縁のギザギザだけを、4倍に拡大してぼかしてから切り直す方法でなめらかにしています。輪郭の内側は頭そのものなので、色は元のコマの画素で戻しました。縁の半透明の画素も元の色で戻したので、2回目まで出ていた暗い点線も消えています。</p><p></p><figure><img src=\"/media/s-882x1224_v-fms_webp_7f743859-c942-49dc-afff-0603e1d0848a.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_boeVOUIR\">ハコブくんの区画に字幕や図が重なったら、書き出す前に止める</h2><p>左下にキャラクターを置くと、字幕や図とぶつかります。試作を作る前に、置き方の決め事が必要だという話になりました。</p><blockquote><p>左端の顔の部分にテキストがかからないように設定しておくことが必要ですね。</p></blockquote><p>ハコブくんの「区画」を決め、そこには何も重ねないことにしました。区画は、実際に置いたハコブくんの体の範囲に余白20ピクセルを足した四角です。動画のルールで決めていた画面の作りとは、次の点が変わります。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>もとの決まり</p></th><th colspan=\"1\" rowspan=\"1\"><p>ハコブくんを置いたあと</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの区画</p></td><td colspan=\"1\" rowspan=\"1\"><p>なし</p></td><td colspan=\"1\" rowspan=\"1\"><p>左下の x=0〜380、y=746〜1080(上半身版)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>字幕の位置</p></td><td colspan=\"1\" rowspan=\"1\"><p>画面の中央、1行31字まで</p></td><td colspan=\"1\" rowspan=\"1\"><p>区画の右側(x=400〜1780)の中央、1行26字まで</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>出典の位置</p></td><td colspan=\"1\" rowspan=\"1\"><p>左端から140ピクセル</p></td><td colspan=\"1\" rowspan=\"1\"><p>左端から420ピクセル</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>図の範囲</p></td><td colspan=\"1\" rowspan=\"1\"><p>左右140ピクセルの余白</p></td><td colspan=\"1\" rowspan=\"1\"><p>左右140ピクセルのまま。区画に重なる場面は自動で止める</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ロゴの位置</p></td><td colspan=\"1\" rowspan=\"1\"><p>右から140ピクセル、上から46ピクセル</p></td><td colspan=\"1\" rowspan=\"1\"><p>右から56ピクセル、上から44ピクセル</p></td></tr></tbody></table></div><p></p><figure><img src=\"/media/s-1024x648_v-fs_webp_79281675-7ed6-4030-9539-7b81072bb026.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>ロゴは、途中まで試作が「Pixels to Motors」の文字を自分で描いていました。</p><blockquote><p>それと動画のデザインルールを確認して欲しい(どこかにデータがある。ロゴも修正)</p></blockquote><p>iCloud の制作ベースのフォルダを開くと、本番は scenes.py(場面ごとの図の HTML と CSS)、render.py(字幕の画像)、logo_dark.png(ロゴの画像)で画面を作っていました。試作が自前で描いていた部分は、章のタグの書き方や、大きい数字の単位の色も本番と違っていたので、図と字幕は本番の scenes.py をそのまま読み込んで描く形に作り直しています。そのあと、ロゴの位置に指摘がありました。</p><blockquote><p>右上のロゴはもっと端に寄せたほうがいいね。パディング取りすぎ。</p></blockquote><p>重なりの確かめ方は、目で見るのをやめ、書き出しの前の自動の確認にしました。図と字幕はブラウザ(Playwright で動かす Chromium)で描いているので、描いたあとに、見出し・数字・出典・ロゴ・字幕の1行ずつの位置を、ブラウザから実際の四角として取り出します(getBoundingClientRect)。その四角がハコブくんの区画と1ピクセルでも重なったら、どの要素がどこで重なったかを出して、書き出しを止めます。</p><p>図の範囲をどうするかは、2つの案から選んでもらいました。図の左端を420ピクセルに寄せたままにするか、本番どおり140ピクセルにして場面ごとの自動の確認に任せるかです。</p><blockquote><p>場面ごとに自動の重なり確認に任せて広げる</p></blockquote><p>上半身版では、12個の要素すべてが区画に重なりませんでした。同じ場面を全身版で書き出すと、区画が高い位置(y=580)から始まるので、「起動時に確保された量の 約28%」の見出しが重なり、書き出しが止まりました。要素の四角で判定しているので、文字が短くても段落の幅いっぱいが判定の対象になります。止まった場面は、図を区画より上に収めるか、左端を420ピクセルに寄せて作り直します。</p><h2 id=\"index_ZOcuqpzA\">全身版より上半身版の方が、図を読む邪魔をしなかった</h2><p>側面のリングを直すときに、全身版と上半身版の2つを作るように言われ、両方を書き出しました。全身版は高さ460ピクセルで体全体を、上半身版は幅340ピクセルで引き出しの取っ手の下あたりまでを置き、画面の下端に接しています。</p><blockquote><p>上半身のデザインがいいですね。全身は少し邪魔しているように見えます。</p></blockquote><p>全身版は、体の下半分(引き出しや車輪)が図と字幕の高さまで伸びていて、視線がそちらに引っぱられます。上半身版は、顔と側面のリングだけが画面の端から出ているので、図を読む邪魔をしません。上半身版を基本にしました。</p><p></p><figure><img src=\"/media/s-811x1056_v-fs_webp_f590a47e-2382-46cb-ad00-ad8d63bd22a9.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>最後の画面はこの形です。</p><p></p><figure><img src=\"/media/s-1920x1080_v-frms_webp_4418024c-4b1e-44f2-a658-4ee6646c54bb.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_H1Z4xux4\">決めたことは video_rules.md に3-1節と4-7節として書き足した</h2><p>ここまで決めたことは、動画制作のルール(video_rules.md)に書き足しました。ルールの文書はプロジェクトの文書と、iCloud の制作ベースのフォルダに同じ内容の写しがあるので、両方を同じ内容に更新しています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>節</p></th><th colspan=\"1\" rowspan=\"1\"><p>書き足したこと</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3-1(新設)</p></td><td colspan=\"1\" rowspan=\"1\"><p>ナレーションはハコブくんが話す。本人を見せる3つの決まり。概要欄に AI 音声と書く。ほかのチャンネルと同じ声を使わない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3-2</p></td><td colspan=\"1\" rowspan=\"1\"><p>仮の音声の速さを1.08倍から1.0倍に下げた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4-1</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの区画(左下・上半身・右向き)。図の範囲は140ピクセルのまま、場面ごとの自動の確認に任せる。ロゴの位置</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4-2・4-5</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの体と顔の色(目の白・口の水色・リングの緑)は、「オレンジと青以外の色を足さない」の対象外。影は付けない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4-6</p></td><td colspan=\"1\" rowspan=\"1\"><p>出典の左端を420ピクセルに移す</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4-7(新設)</p></td><td colspan=\"1\" rowspan=\"1\"><p>体の作り方(頭の上の直し方を含む)、顔の描き方、口・目・まばたき・リングの動きの数値、表情の付け方と使い分けの目安</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>字幕は1行26字まで、区画の右側の中央にそろえる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>台本の一人称「僕」を、本人の名前の三人称に言い換える。自己紹介の固定の文言をハコブくんが話す形にする</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>8</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの声、体の画像、本番のスクリプトへの組み込み、表情の使い分けを未決として追加</p></td></tr></tbody></table></div><p>表情は、目と頬を1文ごとに台本で指定し、口は話している間はどの表情でも音量で動かす、という決まりにしました。表情集の「_」の口のまま話すと、声と口が合わなくなるからです。使い分けの目安は次の表で、経営者向けの動画なので1本で使う表情は6つ前後にとどめます。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>表情</p></th><th colspan=\"1\" rowspan=\"1\"><p>使う場面</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>丸い目+開いた口</p></td><td colspan=\"1\" rowspan=\"1\"><p>基本。説明の文はほぼこれ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>丸い目+小さな笑み</p></td><td colspan=\"1\" rowspan=\"1\"><p>間、聞き手に戻るところ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>驚いた目+「o」の口</p></td><td colspan=\"1\" rowspan=\"1\"><p>動画の冒頭の問い、意外な数字が出るところ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ジト目</p></td><td colspan=\"1\" rowspan=\"1\"><p>「本当にそうでしょうか」と反論を出すところ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>目を閉じる</p></td><td colspan=\"1\" rowspan=\"1\"><p>「まとめると」、考えている間</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>困り眉</p></td><td colspan=\"1\" rowspan=\"1\"><p>失敗や限界の話</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>>▽<</p></td><td colspan=\"1\" rowspan=\"1\"><p>結論が言えたところ、うまくいった実験</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>「III_III」</p></td><td colspan=\"1\" rowspan=\"1\"><p>計算中・データを読み込んでいる場面</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ウインク、キラキラ目、照れ、>_<、目が泳ぐ</p></td><td colspan=\"1\" rowspan=\"1\"><p>アウトロや軽い一言だけ</p></td></tr></tbody></table></div><p>自己紹介の固定の文言は、もとの一人称の文をハコブくんが話す形に言い換え、「この番組は、本業のデザイナーをしながらロボティクスの研究と開発をしているエイイチが、自宅の机の上でロボットアームを動かした記録をもとにしています。」としました。この文言でよいかは、本人の確認待ちです。</p><p>ファイルの置き場所は次のとおりです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>ファイル</p></th><th colspan=\"1\" rowspan=\"1\"><p>置き場所</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>video_rules.md</p></td><td colspan=\"1\" rowspan=\"1\"><p>iCloud <code>PixelstoMotors/デジタルツイン学習R2R2R/動画/_制作ベース/video_rules.md</code> と、プロジェクトの文書 <code>claude/video_rules.md</code></p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>試作のスクリプトと入力の画像</p></td><td colspan=\"1\" rowspan=\"1\"><p>iCloud <code>動画/_制作ベース/hakobu_試作/</code>(make_demo.py、render_static.py、measure_face.py、cut_raw.png、full/g_05.png、元の生成動画)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>試作の動画(mp4)</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>/Volumes/KIOXIA/r2r2r-data/videos/hakobu_試作/</code></p></td></tr></tbody></table></div><p></p><h2 id=\"index_OMOijwC5\">まだ決まっていないのは、ハコブくんの声・体の画像・本番への組み込み・表情の4つ</h2><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>いまの状態</p></th><th colspan=\"1\" rowspan=\"1\"><p>決めること</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの声</p></td><td colspan=\"1\" rowspan=\"1\"><p>Open JTalk の仮の男声</p></td><td colspan=\"1\" rowspan=\"1\"><p>ElevenLabs の Voice Design で作るか、声優に頼んで作るか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>体の画像</p></td><td colspan=\"1\" rowspan=\"1\"><p>生成動画の1コマを切り抜き、頭の上とリングを直して使っている</p></td><td colspan=\"1\" rowspan=\"1\"><p>同じ構図で、無地の背景・リングなしの画像を生成し直すか。生成し直せば、頭の上とリングの直しが要らなくなる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>本番のスクリプトへの組み込み</p></td><td colspan=\"1\" rowspan=\"1\"><p>試作のスクリプトで、動きと配置を確かめた段階</p></td><td colspan=\"1\" rowspan=\"1\"><p>render.py と assemble.py に組み込む。台本(narration.py)に「表情」の欄を足す。表情集の顔をコードで描く。scenes.py のロゴの位置を直す(いまは試作の中で上書きしているだけ)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>表情の使い分け</p></td><td colspan=\"1\" rowspan=\"1\"><p>目安の表だけ</p></td><td colspan=\"1\" rowspan=\"1\"><p>1本作って、使う表情と場面を確定する</p></td></tr></tbody></table></div><p>試作は体を描いてから左右反転しているので、ウインクのように左右で形が違う表情は、このままだと左右が逆になります。本番では、体だけを先に反転し、顔は向きどおりに描く順番にします。</p><p>この4項目のうち3つは、この記事の作業のあとで決まりました。本番への組み込みは hakobu.py として済み、声は ElevenLabs の Voice Design を1か月試すことに、体の画像はいまのまま使うことになっています。字幕の位置も、そのあと画面の中央にそろえる形に変わりました。経緯は video_rules.md の更新履歴にあります。</p><p></p><h2 id=\"index_AtrH00k0\">試作スクリプトの全文</h2><p>使ったもの: Python 3.13.16、opencv-python 5.0.0、Pillow 12.3.0、numpy 2.5.3、pyopenjtalk 0.4.1、rembg 2.0.85(onnxruntime 1.29.0)、playwright 1.56.0(Chromium)、ffmpeg 6.1.1。フォントは Noto Sans CJK JP。</p><p>試作は Claude の作業環境(クラウドの Linux)で動かしました。Mac で動かすときは、<code>hakobu_試作/</code> を作業フォルダにして実行します。render_static.py は、1つ上の <code>_制作ベース/</code> にある本番の scenes.py と logo_dark.png を読み込みます。</p><h3 id=\"index_q54toAvd\">準備のコマンド(コマの取り出しと、体の切り抜き)</h3><p>元の生成動画は、共有されたときのファイル名が文字化けしていたため、<code>hakobu_試作/source_hakobu_hospital.mp4</code> という名前で保存し直しています。</p><pre><code class=\"language-bash\"># 動画の情報を見る\nffprobe -v error -show_entries format=duration:stream=codec_type,width,height,r_frame_rate -of compact source_hakobu_hospital.mp4\n\n# 1秒に4枚ずつ、40枚のコマを取り出す\nmkdir -p full\nffmpeg -v error -y -i source_hakobu_hospital.mp4 -vf \"fps=4\" full/g_%02d.png\n\n# 40枚に番号を振って並べる(コマを選ぶため)\nffmpeg -v error -y -i full/g_%02d.png -vf \"scale=212:-1,drawtext=text='%{n}':x=5:y=5:fontsize=24:fontcolor=red,tile=10x4\" sheet.png\n\n# rembg と、関連するライブラリを入れる\npip install --break-system-packages pyopenjtalk rembg onnxruntime\n</code></pre><pre><code class=\"language-python\"># 5枚目のコマから背景を消す(cut_raw.png)\nfrom rembg import remove, new_session\nfrom PIL import Image\nim = Image.open('full/g_05.png').convert('RGB')\nout = remove(im, session=new_session('isnet-general-use'))\nout.save('cut_raw.png'); print(out.size, out.getbbox())\n</code></pre><h3 id=\"index_TKcoDqjv\">measure_face.py(ディスプレイ・目・口・リングを測る)</h3><pre><code class=\"language-python\">\"\"\"ハコブくんの顔のディスプレイを測る(make_demo.py に書いた座標の出どころ)\n入力: cut_raw.png(元の動画の5コマ目を rembg の isnet-general-use で切り抜いた画像)\n出力: ディスプレイ・目・口・リング・体の範囲、口の色、ディスプレイの四隅\n\"\"\"\nimport numpy as np, cv2\nfrom PIL import Image\n\nraw = np.array(Image.open(\"cut_raw.png\").convert(\"RGBA\"))\nrgb = raw[..., :3].astype(int)\na = raw[..., 3]\nlum = rgb.mean(2)\n\n# 1. ディスプレイ(黒い画面):顔のあたりで一番大きい暗い領域\ny0, y1, x0, x1 = 330, 520, 300, 530\ndark = np.zeros(lum.shape, np.uint8)\ndark[y0:y1, x0:x1] = (lum[y0:y1, x0:x1] < 75)\nn, lab, st, _ = cv2.connectedComponentsWithStats(dark)\nk = 1 + np.argmax(st[1:, 4])\nscr = (lab == k).astype(np.uint8)\nprint(\"ディスプレイの外接矩形 (x, y, 幅, 高さ):\", st[k][:4].tolist())\n\n# 2. 元の目(白)と口(水色)\nreg = rgb[y0:y1, x0:x1]\nrl = reg.mean(2)\nwhite = (rl > 200) & (reg.std(2) < 25)\nblue = (reg[..., 2] > 180) & (reg[..., 0] < 150)\nfor name, m in [(\"白\", white), (\"水色\", blue)]:\n n2, lab2, st2, _ = cv2.connectedComponentsWithStats(m.astype(np.uint8))\n for j in range(1, n2):\n if st2[j, 4] > 80:\n x, y, w, h, area = st2[j].tolist()\n print(f\"{name}の領域: x={x + x0} y={y + y0} 幅={w} 高さ={h} 面積={area}\")\nprint(\"口の色(中央値 RGB):\", np.median(reg[blue], 0).astype(int).tolist())\n\n# 3. 側面の緑のリングと、体全体の範囲\nring = (rgb[..., 1] > 150) & (rgb[..., 1] - rgb[..., 0] > 40) & (rgb[..., 1] - rgb[..., 2] > 20)\nys, xs = np.where(ring)\nprint(\"リングの範囲:\", xs.min(), ys.min(), xs.max(), ys.max(), \"画素数\", len(xs))\nys, xs = np.where(a > 128)\nprint(\"体の範囲:\", xs.min(), ys.min(), xs.max(), ys.max())\n\n# 4. 頭の上の切り抜きの濃さ(行ごとに、アルファが一定以上の画素の数)\nprint(\"頭の上の行ごとの画素数(アルファ>200 / >60 / >10):\")\nfor y in range(320, 360, 4):\n row = a[y, 300:650]\n print(f\" y={y}: {(row > 200).sum()} / {(row > 60).sum()} / {(row > 10).sum()}\")\n\n# 5. ディスプレイの四隅\n# 角が丸いので角の点は直接取らない。上下左右の辺の、まっすぐな中央4割の点に直線を当てはめ、交点を角にする\nc = max(cv2.findContours(scr, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[0], key=cv2.contourArea)\nc = c.reshape(-1, 2).astype(np.float32)\nbx, by, bw, bh = cv2.boundingRect(c.astype(np.int32))\ncx, cy = bx + bw / 2, by + bh / 2\nsides = {\n \"上\": c[(c[:, 1] < cy - bh * 0.3) & (np.abs(c[:, 0] - cx) < bw * 0.2)],\n \"下\": c[(c[:, 1] > cy + bh * 0.3) & (np.abs(c[:, 0] - cx) < bw * 0.2)],\n \"左\": c[(c[:, 0] < cx - bw * 0.3) & (np.abs(c[:, 1] - cy) < bh * 0.2)],\n \"右\": c[(c[:, 0] > cx + bw * 0.3) & (np.abs(c[:, 1] - cy) < bh * 0.2)],\n}\nlines = {}\nfor nm, p in sides.items():\n vx, vy, px, py = cv2.fitLine(p, cv2.DIST_L2, 0, 0.01, 0.01).ravel()\n lines[nm] = (np.array([px, py]), np.array([vx, vy]))\n ang = np.degrees(np.arctan2(vy, vx))\n if nm in (\"左\", \"右\"):\n ang = 90 - abs(ang) # 縦の辺は、垂直からの傾き\n print(f\"{nm}の辺: 点 {len(p)} 個、傾き {ang:.2f}°\")\n\n\ndef cross(s1, s2):\n (p1, d1), (p2, d2) = lines[s1], lines[s2]\n t = np.linalg.solve(np.array([d1, -d2]).T, p2 - p1)\n return p1 + t[0] * d1\n\n\nquad = [cross(\"上\", \"左\"), cross(\"上\", \"右\"), cross(\"下\", \"右\"), cross(\"下\", \"左\")]\nprint(\"ディスプレイの四隅(左上・右上・右下・左下):\", [[round(float(v), 1) for v in q] for q in quad])\n\n# 参考: 角の点をそのまま取った場合(最初に使って、傾きを読み違えた方法)\nhull = cv2.convexHull(c.astype(np.int32))\nfor eps in np.linspace(2, 30, 57):\n ap = cv2.approxPolyDP(hull, eps, True)\n if len(ap) == 4:\n print(f\"参考・角の点を直接取った四隅(eps={eps}):\", ap.reshape(-1, 2).tolist())\n break\n</code></pre><h3 id=\"index_nK_87CBf\">make_demo.py(体の加工・顔の描画・口と目とリングの動き・重なりの確認・書き出し)</h3><pre><code class=\"language-python\">\"\"\"ハコブくん ナレーション試作(13秒ほど)\n使い方: python3 make_demo.py upper (上半身・基本) / python3 make_demo.py full (全身)\n python3 make_demo.py upper --dump (口・目・まばたき・リングの時間の値だけを timing.json に書き出して終わる)\n入力: cut_raw.png(元の動画の5コマ目を rembg で切り抜いた画像)、full/g_05.png(元の動画の5コマ目そのもの)\n必要なもの: pyopenjtalk(仮音声)、playwright と Chromium(図と字幕)、opencv-python、numpy、Pillow、ffmpeg\n- 動画の1コマから切り抜いた体に、画面の顔をコードで描く\n- 仮の音声(Open JTalk)の音量で口を4段階に切り替え、目・まばたき・側面のリングを動かす\n- ハコブくんの区画に文字が重なっていないかを自動で確認する\n\"\"\"\nimport subprocess, sys\nimport numpy as np, cv2, pyopenjtalk\nfrom PIL import Image, ImageDraw, ImageFont, ImageEnhance\n\nW, H, FPS = 1920, 1080, 30\nSR = 48000\nBG = (245, 245, 240)\nINK = (13, 13, 13)\nGRAY = (136, 136, 136)\nORANGE = (196, 71, 31)\nFONT_JP = \"/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc\"\nFONT_BLACK = \"/usr/share/fonts/opentype/noto/NotoSansCJK-Black.ttc\"\nFONT_REG = \"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc\"\n\n# ハコブくんの区画(ここには何も重ねない)\nZONE = (0, 600, 380, 1080)\nSUB_X0, SUB_X1 = 400, 1780\nFIG_X0 = 420\n\n\ndef font(path, size):\n return ImageFont.truetype(path, size, index=0) # index 0 = JP\n\n\n# ---------- 1. 体の切り抜きを整える ----------\nraw = np.array(Image.open(\"cut_raw.png\").convert(\"RGBA\"))\nalpha = raw[..., 3].copy()\nalpha[alpha < 60] = 0\nn, lab, st, _ = cv2.connectedComponentsWithStats((alpha > 0).astype(np.uint8))\nkeep = 1 + np.argmax(st[1:, 4])\nalpha[lab != keep] = 0\n# 頭の上:白い頭が白い枕と重なっていて、切り抜きが薄く欠けている。\n# 元の動画のコマを見ると、頭の上の輪郭は「画面の上の手前側の縁(低い)→ 奥の角で持ち上がる → 奥の縁(高い)→ 右の角で丸く下がる」形。\n# 凸包で埋めるとこの段差が直線になり、枕まで頭に含めてしまう(汚れの原因)。\n# そこで、切り抜きがはっきり頭と判断した範囲(アルファ60以上)をそのまま輪郭に使い、ギザギザだけをなめらかにする。\na_soft = np.array(Image.open(\"cut_raw.png\"))[..., 3]\nHEAD_Y1 = 425 # 肩の段差より上だけを対象にする\nhead = (a_soft > 60).astype(np.uint8)\nhead[HEAD_Y1:] = 0\nhead[:, :300] = 0\nn_h, lab_h, st_h, _ = cv2.connectedComponentsWithStats(head)\nhead = (lab_h == 1 + np.argmax(st_h[1:, 4])).astype(np.uint8)\ncnts, _ = cv2.findContours(head, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)\nhead = np.zeros_like(head)\ncv2.drawContours(head, cnts, -1, 1, -1) # 穴を埋める\n# 4倍に拡大 → ぼかして0.5で切り直す(ギザギザを曲線に) → 小さく角を丸める → 元の大きさへ\n_S = 4\n_big = cv2.resize(head.astype(np.float32) * 255, None, fx=_S, fy=_S, interpolation=cv2.INTER_LINEAR)\n_pad = 20 * _S\n_bigp = cv2.copyMakeBorder(_big, 0, _pad, 0, 0, cv2.BORDER_REPLICATE) # 下の切れ目が丸まらないように下へ延ばす\n_bigp = cv2.GaussianBlur(_bigp, (0, 0), 2.5 * _S)\n_bigp = ((_bigp > 127) * 255).astype(np.uint8)\n_k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * 4 * _S + 1, 2 * 4 * _S + 1))\n_bigp = cv2.morphologyEx(_bigp, cv2.MORPH_OPEN, _k)[: _big.shape[0]].astype(np.float32)\nhull_soft = cv2.resize(_bigp, (head.shape[1], head.shape[0]), interpolation=cv2.INTER_AREA) # 縁は自然な中間値\nhull_mask = (hull_soft > 127).astype(np.uint8)\nhull_soft[HEAD_Y1 - 3:] = 0\nalpha = np.maximum(alpha, hull_soft.astype(np.uint8))\nalpha[: HEAD_Y1 - 20] = hull_soft[: HEAD_Y1 - 20].astype(np.uint8) # 頭の上はこの輪郭だけで描く\n# 輪郭の内側は頭そのものなので、色は元のコマの画素をそのまま使う(切り抜きで暗くなった部分を戻す)\norig_rgb = np.array(Image.open(\"full/g_05.png\").convert(\"RGB\"))\nrestore = (hull_soft > 0) & (a_soft < 245) # 縁の半透明の画素も含める(暗い点線が出ないように)\nrestore[HEAD_Y1:] = False\nraw[restore, :3] = orig_rgb[restore]\nraw[..., 3] = alpha\n\n# 画面(黒いディスプレイ)の領域を見つけて、元の目と口を消す\nrgb = raw[..., :3].astype(int)\nlum = rgb.mean(2)\ny0, y1, x0, x1 = 330, 520, 300, 530\ndark = np.zeros(lum.shape, np.uint8)\ndark[y0:y1, x0:x1] = (lum[y0:y1, x0:x1] < 75)\nn, lab, st, _ = cv2.connectedComponentsWithStats(dark)\nk = 1 + np.argmax(st[1:, 4])\nscr = (lab == k).astype(np.uint8)\ncnts, _ = cv2.findContours(scr, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)\nscreen_mask = np.zeros_like(scr)\ncv2.drawContours(screen_mask, cnts, -1, 1, -1)\nscreen_mask = cv2.erode(screen_mask, np.ones((3, 3), np.uint8), 1)\nSCREEN_COLOR = (19, 19, 17)\nraw[screen_mask == 1, :3] = SCREEN_COLOR\n\n# 横の緑のリング\n# 元のリングは画素が粗いので、一度消して、楕円をコードで描き直す\ng = raw[..., :3].astype(int)\nring_core = (g[..., 1] > 150) & (g[..., 1] - g[..., 0] > 40) & (g[..., 1] - g[..., 2] > 20)\nring_core &= ~(screen_mask.astype(bool))\nys, xs = np.where(ring_core)\nRING = cv2.fitEllipse(np.stack([xs, ys], 1).astype(np.float32)) # ((cx,cy),(長径,短径),角度)\n(rcx, rcy), (rw, rh), rang = RING\nperim = np.pi * (3 * (rw + rh) / 2 - np.sqrt((3 * rw / 2 + rh / 2) * (rw / 2 + 3 * rh / 2)))\nRING_T = max(3.5, min(7.0, ring_core.sum() / perim)) # リングの太さ(px)\n# 緑がにじんでいる範囲ごと塗り直して消す\nhalo = ((g[..., 1] - g[..., 0] > 15) & (g[..., 1] - g[..., 2] > 5)) | ring_core\nhalo = cv2.dilate(halo.astype(np.uint8), np.ones((7, 7), np.uint8), 1)\nhalo[: int(rcy - rh / 2 - 15)] = 0\nhalo[int(rcy + rh / 2 + 15):] = 0\nhalo[:, : int(rcx - rw / 2 - 15)] = 0\nhalo[:, int(rcx + rw / 2 + 15):] = 0\nbgr = cv2.cvtColor(raw[..., :3].copy(), cv2.COLOR_RGB2BGR)\nbgr = cv2.inpaint(bgr, halo, 7, cv2.INPAINT_TELEA)\nraw[..., :3] = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)\n\n\ndef draw_ring(img, on):\n \"\"\"リングを4倍の解像度で描き、にじむ光を重ねる\"\"\"\n m = 30\n bx0, by0 = int(rcx - rw / 2 - m), int(rcy - rh / 2 - m)\n bw, bh = int(rw + 2 * m), int(rh + 2 * m)\n big = np.zeros((bh * SS, bw * SS), np.uint8)\n cv2.ellipse(big, ((int((rcx - bx0) * SS), int((rcy - by0) * SS)),\n (int(rw * SS), int(rh * SS)), rang),\n 255, int(RING_T * SS), cv2.LINE_AA)\n core = cv2.resize(big, (bw, bh), interpolation=cv2.INTER_AREA).astype(np.float32) / 255\n on = float(on) # 0(話していない)〜1(話している)\n glow = cv2.GaussianBlur(core, (0, 0), 3.5 + 1.5 * on)\n glow = np.clip(glow * (1.4 + 0.8 * on), 0, 1)\n region = np.array(img.crop((bx0, by0, bx0 + bw, by0 + bh))).astype(np.float32)\n a = region[..., 3:4] / 255\n rgb = region[..., :3]\n glow_col = np.array([70, 225, 130], np.float32)\n core_col = np.array([160, 240, 185], np.float32) * (1 - on) + np.array([215, 255, 228], np.float32) * on\n gk = (glow * (0.45 + 0.3 * on))[..., None] * a # 体の外には光を出さない\n rgb = rgb * (1 - gk) + glow_col * gk\n ck = core[..., None] * a\n rgb = rgb * (1 - ck) + core_col * ck\n region[..., :3] = rgb\n img.paste(Image.fromarray(region.astype(np.uint8)), (bx0, by0))\n\n\nbase_body = Image.fromarray(raw)\nbody_bbox = base_body.getbbox()\n\n# ---------- 2. 顔を描く(元の位置を実測した座標) ----------\nEYE_L = (362, 395, 384, 441)\nEYE_R = (446, 396, 469, 444)\nMOUTH = (378, 450, 446, 490)\nEYE_SHIFT = 0.5 # 目の上下位置(元画像の px。+で下)。顔全体を1.5px上げた\nMOUTH_SHIFT = -3.0 # 口の上下位置(目と口の間は 3.5px 詰めたまま)\nBLUE = (87, 181, 248) # 口の水色(元の動画の色 #57B5F8)\nWHITE = (246, 248, 252) # 目の白\nSS = 4 # 4倍で描いて縮める(縁をなめらかにする)\n\n\nFX0, FY0, FX1, FY1 = 320, 370, 520, 500 # 顔を描く範囲(元画像の座標)\n\n\ndef _pts(xs, ys):\n return np.stack([(np.asarray(xs) - FX0) * SS, (np.asarray(ys) - FY0) * SS], 1).astype(np.int32)\n\n\ndef mouth_mask(kind):\n \"\"\"口の形を4倍の解像度で作り、角を丸めて返す(0/255 の配列)\"\"\"\n w, h = (FX1 - FX0) * SS, (FY1 - FY0) * SS\n m = np.zeros((h, w), np.uint8)\n cx = (MOUTH[0] + MOUTH[2]) / 2\n top = MOUTH[1] + 2 + MOUTH_SHIFT\n t = np.linspace(0, 1, 60)\n if kind in (\"mid\", \"large\"):\n # 三日月形:上の辺はゆるく下がり、下の辺は深い弧\n hw, depth, sag, rr = (34, 38, 7, 6) if kind == \"large\" else (27, 25, 5, 5)\n xs = cx - hw + 2 * hw * t\n y_top = top + sag * (1 - (2 * t - 1) ** 2)\n y_bot = top + depth * np.sqrt(np.clip(1 - (2 * t - 1) ** 2, 0, 1))\n poly = np.concatenate([_pts(xs, y_top), _pts(xs[::-1], y_bot[::-1])])\n cv2.fillPoly(m, [poly], 255)\n elif kind == \"small\":\n # 小さく開いた口:上が少し広く、下が丸い\n hw, rr = 14, 6\n xs = cx - hw + 2 * hw * t\n y_top = top + 4 + 2 * (1 - (2 * t - 1) ** 2)\n ang = np.linspace(0, np.pi, 60)\n bx = cx + hw * np.cos(ang)\n by = top + 12 + 13 * np.sin(ang)\n poly = np.concatenate([_pts(xs, y_top), _pts(bx, by)])\n cv2.fillPoly(m, [poly], 255)\n elif kind == \"closed\":\n # 閉じた笑顔:太さのある弧、両端は丸い\n hw, sag, thick = 25, 9, 7\n for tt in np.linspace(0, 1, 200):\n x = cx - hw + 2 * hw * tt\n y = top + 7 + sag * (1 - (2 * tt - 1) ** 2)\n p = _pts([x], [y])[0]\n cv2.circle(m, (int(p[0]), int(p[1])), int(thick / 2 * SS), 255, -1)\n return m\n k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * rr * SS + 1, 2 * rr * SS + 1))\n return cv2.morphologyEx(m, cv2.MORPH_OPEN, k) # 角を丸める\n\n\nMOUTH_CACHE = {}\n\n# ディスプレイの四隅:画面の上下左右の辺のまっすぐな部分(角の丸みを除いた中央4割)に直線を当てはめ、\n# その交点を角とした(角の丸みに引っぱられないように)。上の辺はほぼ水平(0.2°)、左右の辺は約6°傾いている\nSCREEN_QUAD = np.float32([[336.8, 348.8], [510.2, 349.3], [495.1, 501.2], [321.5, 494.6]]) # 左上・右上・右下・左下\n_c = SCREEN_QUAD.mean(0)\n_w = (np.linalg.norm(SCREEN_QUAD[1] - SCREEN_QUAD[0]) + np.linalg.norm(SCREEN_QUAD[2] - SCREEN_QUAD[3])) / 2\n_h = (np.linalg.norm(SCREEN_QUAD[3] - SCREEN_QUAD[0]) + np.linalg.norm(SCREEN_QUAD[2] - SCREEN_QUAD[1])) / 2\nFLAT_RECT = np.float32([[_c[0] - _w / 2, _c[1] - _h / 2], [_c[0] + _w / 2, _c[1] - _h / 2],\n [_c[0] + _w / 2, _c[1] + _h / 2], [_c[0] - _w / 2, _c[1] + _h / 2]])\nTILT = 1.0 # 1.0 = ディスプレイと同じだけ傾ける。0.5 なら半分\n_quad = FLAT_RECT + TILT * (SCREEN_QUAD - FLAT_RECT)\n_to_layer = lambda p: (p - np.float32([FX0, FY0])) * SS\nH_FACE = cv2.getPerspectiveTransform(_to_layer(FLAT_RECT), _to_layer(_quad))\n\n\ndef warp_to_screen(la):\n h, w = la.shape[:2]\n a = la[..., 3:4].astype(np.float32) / 255\n pre = np.concatenate([la[..., :3].astype(np.float32) * a, a * 255], 2) # 縁が黒ずまないよう乗算済みで変形\n out = cv2.warpPerspective(pre, H_FACE, (w, h), flags=cv2.INTER_LINEAR)\n aa = out[..., 3:4] / 255\n rgb = np.where(aa > 1e-4, out[..., :3] / np.maximum(aa, 1e-4), 0)\n return np.concatenate([np.clip(rgb, 0, 255), out[..., 3:4]], 2).astype(np.uint8)\n\n\ndef draw_face(img, mouth, eyes_closed, eye_dy=0.0):\n fx0, fy0, fx1, fy1 = FX0, FY0, FX1, FY1\n layer = Image.new(\"RGBA\", ((fx1 - fx0) * SS, (fy1 - fy0) * SS), (0, 0, 0, 0))\n d = ImageDraw.Draw(layer)\n\n def r(b):\n return [(b[0] - fx0) * SS, (b[1] - fy0) * SS, (b[2] - fx0) * SS, (b[3] - fy0) * SS]\n\n for e in (EYE_L, EYE_R):\n e = (e[0], e[1] + eye_dy + EYE_SHIFT, e[2], e[3] + eye_dy + EYE_SHIFT) # 目だけ少し上下させる\n if eyes_closed:\n cy = (e[1] + e[3]) / 2\n d.rounded_rectangle(r((e[0] - 1, cy - 2.5, e[2] + 1, cy + 2.5)), radius=2.5 * SS, fill=WHITE)\n else:\n # 縦長のカプセル形(上下の端が半円)\n w = e[2] - e[0]\n d.rounded_rectangle(r(e), radius=w / 2 * SS, fill=WHITE)\n if mouth not in MOUTH_CACHE:\n MOUTH_CACHE[mouth] = mouth_mask(mouth)\n la = np.array(layer)\n mm = MOUTH_CACHE[mouth] > 0\n la[mm, :3] = BLUE\n la[mm, 3] = 255\n # 顔ごとディスプレイの傾きに合わせて斜めにする(まっすぐ描いた顔を、画面の四隅へ射影する)\n la = warp_to_screen(la)\n layer = Image.fromarray(la)\n layer = layer.resize((fx1 - fx0, fy1 - fy0), Image.LANCZOS)\n # 画面の外にはみ出さないよう、画面の形で切る\n m = Image.fromarray((screen_mask[fy0:fy1, fx0:fx1] * 255).astype(np.uint8))\n la = np.array(layer)\n inside = (np.array(m) > 0)\n la[..., 3] = (la[..., 3].astype(int) * inside).astype(np.uint8)\n # 目のまわりのにじむ光(画面の中だけ)\n eye_a = la[..., 3].astype(np.float32) / 255 * (la[..., 0] > 200)\n glow = cv2.GaussianBlur(eye_a, (0, 0), 2.6) * 0.75 * inside\n glow_layer = np.zeros_like(la)\n glow_layer[..., :3] = (205, 225, 255)\n glow_layer[..., 3] = np.clip(glow * 255, 0, 255).astype(np.uint8)\n img.alpha_composite(Image.fromarray(glow_layer), (fx0, fy0))\n img.alpha_composite(Image.fromarray(la), (fx0, fy0))\n\n\nMODE = sys.argv[1] if len(sys.argv) > 1 else \"upper\" # upper=上半身(基本) / full=全身\nif MODE == \"full\":\n CROP = body_bbox\n ROBOT_H = 460\n scale = ROBOT_H / (CROP[3] - CROP[1])\n ROBOT_W = round((CROP[2] - CROP[0]) * scale)\n ROBOT_Y = ZONE[3] - 20 - ROBOT_H\nelse:\n # 上半身:引き出しの取っ手の下あたりで切り、画面の下端に接して置く\n CROP = (body_bbox[0], body_bbox[1], body_bbox[2], 660)\n ROBOT_W = 340\n scale = ROBOT_W / (CROP[2] - CROP[0])\n ROBOT_H = round((CROP[3] - CROP[1]) * scale)\n ROBOT_Y = H - ROBOT_H\nROBOT_X = (ZONE[0] + ZONE[2]) // 2 - ROBOT_W // 2\n# ハコブくんの区画は、実際に置いた体の範囲+余白20pxで決める(上半身版は全身版より低い位置から始まる)\nZONE = (0, ROBOT_Y - 20, ROBOT_X + ROBOT_W + 20, H)\ncache = {}\n\n\ndef robot(mouth, eyes_closed, ring_on, eye_dy=0.0):\n key = (mouth, eyes_closed, ring_on, eye_dy)\n if key not in cache:\n img = base_body.copy()\n draw_ring(img, ring_on)\n draw_face(img, mouth, eyes_closed, eye_dy)\n img = img.crop(CROP).resize((ROBOT_W, ROBOT_H), Image.LANCZOS)\n img = img.transpose(Image.FLIP_LEFT_RIGHT) # 右向き(画面の中央を向く)\n cache[key] = img\n return cache[key]\n\n\n# ---------- 3. 仮の音声 ----------\nSCRIPT = [\n # (字幕, 読み上げ用, 後ろの間[秒], 間の顔)\n (\"ハコブくんです。\", \"ハコブくんです。\", 0.35, \"closed\"),\n (\"高いGPUを、買う必要はあるのでしょうか。\",\n \"たかいジーピーユーを、買う必要は、あるのでしょうか。\", 0.7, \"o\"),\n (\"エイイチが借りて測ると、\\n使ったメモリは約3割でした。\",\n \"エイイチが借りて測ると、使ったメモリは、約3割でした。\", 0.6, \"closed\"),\n]\n\naudio = []\ntimeline = [] # (開始フレーム, 終了フレーム, 字幕, 間の顔, 発話終了フレーム)\nt = 0\nfor sub, read, gap, gap_face in SCRIPT:\n x, sr = pyopenjtalk.tts(read, speed=1.0, half_tone=-1.0) # 1.08 → 1.0 に落とした\n assert sr == SR\n x = x / 32768.0\n seg = np.concatenate([x, np.zeros(int(gap * SR))])\n f_start = round(t * FPS)\n f_speech_end = round((t + len(x) / SR) * FPS)\n t += len(seg) / SR\n f_end = round(t * FPS)\n audio.append(seg)\n timeline.append((f_start, f_end, sub, gap_face, f_speech_end))\naudio = np.concatenate([np.zeros(int(0.3 * SR)), np.concatenate(audio)])\nlead = round(0.3 * FPS)\ntimeline = [(a + lead, b + lead, s, gf, e + lead) for a, b, s, gf, e in timeline]\ntotal_frames = timeline[-1][1] + round(0.3 * FPS)\naudio = np.concatenate([audio, np.zeros(max(0, int(total_frames / FPS * SR) - len(audio)))])\npeak = np.abs(audio).max()\naudio = audio / peak * 0.8\nimport wave\nwith wave.open(\"narration.wav\", \"wb\") as wf:\n wf.setnchannels(1); wf.setsampwidth(2); wf.setframerate(SR)\n wf.writeframes((audio * 32767).astype(np.int16).tobytes())\n\n# 音量 → 口の4段階(閉じる/小/中/大)\nMOUTHS = [\"closed\", \"small\", \"mid\", \"large\"]\nhop = SR // FPS\nrms = np.array([np.sqrt(np.mean(audio[i * hop:(i + 1) * hop] ** 2)) if i * hop < len(audio) else 0\n for i in range(total_frames)])\nref = np.percentile(rms[rms > 0.01], 90)\nlvl = rms / ref\nlvl_s = np.convolve(lvl, np.ones(3) / 3, mode=\"same\") # 音量を少しならす\ntarget = np.digitize(lvl_s, [0.15, 0.35, 0.65]) # 0..3\n# ちらつき防止: 1フレームだけの変化は前に合わせる\nfor i in range(1, total_frames - 1):\n if target[i] != target[i - 1] and target[i] != target[i + 1]:\n target[i] = target[i - 1]\n# 1フレームに1段階ずつしか変えない → 閉じる→大 の間に必ず小・中が入る\nstep = np.zeros(total_frames, int)\nMOUTH_HOLD = 2 # 口は2フレームごと(毎秒15回)にだけ変える\nfor i in range(total_frames):\n prev = step[i - 1] if i else 0\n if i % MOUTH_HOLD == 0:\n step[i] = prev + int(np.clip(target[i] - prev, -1, 1))\n else:\n step[i] = prev\nmouth_seq = [MOUTHS[s] for s in step]\n# 間の顔(問いかけのあとは小さく開いた口のまま止める)\nfor a, b, s, gf, e in timeline:\n for i in range(e + 2, b):\n if gf == \"o\":\n mouth_seq[i] = \"small\"\n# 目の上下:ゆっくりした揺れ + 口が開くほど少し上がる(元画像の px。0.25px 刻みにまとめる)\ntt = np.arange(total_frames) / FPS\nrng = np.random.default_rng(7)\nstarts = [a for a, b, s, gf, e in timeline]\nends = [e for a, b, s, gf, e in timeline]\n\n# ---- 目:ゆっくりした揺れ + 口に少し遅れて上がる + 文の頭で小さく跳ねる ----\nlift = np.convolve(step / 3.0, np.ones(11) / 11, mode=\"same\")\nlift = np.concatenate([np.zeros(3), lift[:-3]]) # 口より3フレーム(0.1秒)遅れて動く\nhop = np.zeros(total_frames)\nfor a in starts: # 文の頭:目がぴょこっと上がって戻る(0.3秒)\n n = np.arange(9)\n hop[a:a + 9] += np.sin(np.pi * n / 8)[: len(hop[a:a + 9])]\neye_dy = 1.6 * np.sin(2 * np.pi * tt / 2.8) - 2.6 * lift - 2.0 * hop\neye_dy = np.round(eye_dy * 4) / 4\n\n# ---- まばたき:間隔をばらつかせ、文の終わりの直後にも入れる ----\nblink = np.zeros(total_frames, bool)\nt_b = 1.2\nwhile t_b * FPS < total_frames:\n blink[int(t_b * FPS):int(t_b * FPS) + 4] = True\n t_b += rng.uniform(2.0, 4.5)\nfor e in ends:\n if not blink[max(0, e - FPS):e + 8].any():\n blink[e + 4:e + 8] = True\n\n# ---- 側面の光:顔とは別の周期で息をするように明滅し、声には少し遅れて追いかける ----\n# 息をするような明滅。強さそのものもゆっくり不規則に変わる\nnoise = np.convolve(rng.normal(0, 1, total_frames + 90), np.ones(90) / 90, mode=\"valid\")[:total_frames]\nnoise = (noise - noise.min()) / (noise.max() - noise.min() + 1e-9) # 0〜1 のゆっくりした揺らぎ\nbreath = (0.5 + 0.5 * np.sin(2 * np.pi * tt / 3.7 + 1.3)) * (0.4 + 0.9 * noise) # 目の揺れ(2.8秒)と周期をずらす\ntalk = (lvl_s > 0.15).astype(float)\nenv = np.zeros(total_frames)\nfor i in range(1, total_frames): # 立ち上がりは速く、消えるのはゆっくり\n k = 0.30 if talk[i] > env[i - 1] else 0.05\n env[i] = env[i - 1] + k * (talk[i] - env[i - 1])\nenv = np.concatenate([np.zeros(6), env[:-6]]) # 声より0.2秒遅れて光る\ngain = np.full(total_frames, 0.5)\nfor a, b, s_, gf, e in timeline: # 文ごとに、声への反応の強さを変える(強い文・弱い文が混ざる)\n gain[a:b] = rng.uniform(0.3, 0.75)\ngain = np.convolve(gain, np.ones(15) / 15, mode=\"same\")\npulse = np.zeros(total_frames)\n\n\ndef add_pulse(i, amp, tau):\n n = np.arange(min(int(tau * 3), total_frames - i))\n pulse[i:i + len(n)] = np.maximum(pulse[i:i + len(n)], amp * np.exp(-n / tau))\n\n\nlast = -999\nfor i in range(total_frames):\n # 文の頭と、大きく口を開いた瞬間:顔とそろえて光る。強さは毎回ばらつき、ときどき光らない\n if i in starts:\n add_pulse(i, rng.uniform(0.25, 0.45), 5); last = i\n elif step[i] == 3 and i - last > 15:\n if rng.random() < 0.7:\n add_pulse(i, rng.uniform(0.08, 0.4), rng.uniform(3, 7))\n last = i\n# 話していないときも、ときどき弱くまたたく\nt_tw = rng.uniform(1.0, 2.5)\nwhile t_tw * FPS < total_frames:\n add_pulse(int(t_tw * FPS), rng.uniform(0.06, 0.18), rng.uniform(4, 9))\n t_tw += rng.uniform(1.5, 4.0)\nring_lv = np.clip(0.10 + 0.2 * breath + gain * env + pulse, 0, 1)\nring_lv = np.round(ring_lv * 20) / 20\n\nif \"--dump\" in sys.argv: # 図解用に、時間の値だけを書き出して終わる\n import json\n json.dump({\"fps\": FPS, \"frames\": int(total_frames),\n \"voice\": np.round(np.clip(lvl_s, 0, 1.5), 3).tolist(),\n \"mouth\": [int(v) for v in step], \"eye_dy\": eye_dy.tolist(),\n \"blink\": [int(b) for b in blink], \"ring\": ring_lv.tolist(),\n \"sentences\": [[int(a), int(e)] for a, b, s, gf, e in timeline]},\n open(\"timing.json\", \"w\"))\n print(\"timing.json に書き出しました\")\n sys.exit(0)\n\n# ---------- 4. 画面の文字と図(本番と同じ HTML/CSS で描く) ----------\nimport json\nsubprocess.run([sys.executable, \"render_static.py\", json.dumps([s for a, b, s, gf, e in timeline], ensure_ascii=False)], check=True)\nstatic = Image.open(\"png/static.png\").convert(\"RGB\")\nsub_layers = {s: Image.open(f\"png/sub_{k}.png\").convert(\"RGBA\") for k, (a, b, s, gf, e) in enumerate(timeline)}\nboxes = json.load(open(\"png/boxes.json\"))\n\n# ---------- 5. 重なりの自動確認 ----------\ndef overlap(b1, b2):\n return not (b1[2] <= b2[0] or b2[2] <= b1[0] or b1[3] <= b2[1] or b2[3] <= b1[1])\n\nbad = [(nm, bb) for nm, bb in boxes if overlap(bb, ZONE)]\nprint(\"ハコブくんの区画:\", ZONE, \" 体の位置:\", (ROBOT_X, ROBOT_Y, ROBOT_X + ROBOT_W, ROBOT_Y + ROBOT_H))\nif bad:\n for nm, bb in bad:\n print(\" 重なり:\", nm, bb)\n sys.exit(\"区画に重なる要素があるため中止しました\")\nprint(f\"重なり確認: 文字・図・ロゴ {len(boxes)} 個、区画との重なり 0 件\")\n\n# ---------- 6. 書き出し ----------\ncmd = [\"ffmpeg\", \"-v\", \"error\", \"-y\", \"-f\", \"rawvideo\", \"-pix_fmt\", \"rgb24\", \"-s\", f\"{W}x{H}\",\n \"-r\", str(FPS), \"-i\", \"-\", \"-i\", \"narration.wav\", \"-c:v\", \"libx264\", \"-crf\", \"20\",\n \"-pix_fmt\", \"yuv420p\", \"-c:a\", \"aac\", \"-b:a\", \"192k\", \"-shortest\", f\"hakobu_demo_{MODE}.mp4\"]\np = subprocess.Popen(cmd, stdin=subprocess.PIPE)\ncur_sub = None\nfor i in range(total_frames):\n frame = static.copy().convert(\"RGBA\")\n for a, b, s, gf, e in timeline:\n if a <= i < b:\n frame.alpha_composite(sub_layers[s])\n frame.alpha_composite(robot(mouth_seq[i], bool(blink[i]), float(ring_lv[i]), float(eye_dy[i])), (ROBOT_X, ROBOT_Y))\n if i == round(total_frames * 0.42):\n frame.convert(\"RGB\").save(f\"hakobu_demo_{MODE}_still.png\")\n p.stdin.write(frame.convert(\"RGB\").tobytes())\np.stdin.close(); p.wait()\nprint(\"frames\", total_frames, \"seconds\", round(total_frames / FPS, 2))</code></pre><h3 id=\"index_BgCE00BU\">render_static.py(図と字幕を本番の scenes.py で描き、要素の位置を測る)</h3><pre><code class=\"language-python\">\"\"\"図と字幕を、本番(_制作ベース の scenes.py / render.py)と同じ HTML/CSS で描く。\nハコブくんを左下に置くために本番から変えた点:\n - 図の範囲(.wrap)は本番と同じ左端 140px のまま。ハコブくんの区画に重なる場面は、書き出し前の自動確認で止める\n - 出典(.src)の左端 140px → 420px\n - 字幕の中央 960px → 1090px、1行の最大幅 1640px → 1380px(1行26字まで)\n - ロゴ(.brand)を右端に寄せる:右 140px → 56px、上 46px → 44px\n各要素の位置は getBoundingClientRect で測り、png/boxes.json に書き出す(重なり確認用)。\n\"\"\"\nimport sys, os, json, html\n# 本番の scenes.py と logo_dark.png を読む。_制作ベース/hakobu_試作/ に置いたときは1つ上の _制作ベース を、\n# それ以外の場所では base/ に置いた写しを使う\n_here = os.path.dirname(os.path.abspath(__file__))\n_parent = os.path.dirname(_here)\nsys.path.insert(0, _parent if os.path.exists(os.path.join(_parent, \"scenes.py\")) else os.path.join(_here, \"base\"))\nimport scenes\nfrom playwright.sync_api import sync_playwright\n\nsubs = json.loads(sys.argv[1])\nos.makedirs(\"png\", exist_ok=True)\n\nHAKOBU_CSS = \"\"\"\n.src{left:420px !important}\n.brand{right:56px !important;top:44px !important}\n\"\"\"\nscenes.CSS = scenes.CSS + HAKOBU_CSS\n\nscene = scenes.page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">π0.5 をシミュレータで動かしたときの GPU メモリ(実使用の最大)</div>\n <div class=\"hero\">9,735<small>MiB</small></div>\n <div class=\"h2\" style=\"margin-top:36px\">起動時に確保された量の <span class=\"red\">約28%</span></div>\n</div>\n<div class=\"src\">出典:Pixels to Motors 実測(GPU:NVIDIA A40、π0.5-LIBERO)</div>\"\"\",\n tag=\"<b>2</b>買う前に借りて測る\")\n\nSUBCSS = \"\"\"*{margin:0;padding:0}html,body{width:1920px;height:1080px;background:transparent}\nsvg{position:absolute;left:0;top:0}\n.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}\"\"\"\nSUB_CX, SUB_MAXW = 1090, 1380\n\nMEASURE = \"\"\"() => [...document.querySelectorAll('.tag,.brand,.kicker,.hero,.h1,.h2,.h3,.src,.wrap *')]\n .filter(e => e.getBoundingClientRect().width > 0)\n .map(e => { const r = e.getBoundingClientRect();\n return [e.className || e.tagName, [r.left, r.top, r.right, r.bottom]]; })\"\"\"\n\nboxes = []\nwith sync_playwright() as p:\n b = p.chromium.launch()\n pg = b.new_page(viewport={\"width\": 1920, \"height\": 1080})\n pg.set_content(scene)\n pg.wait_for_timeout(100)\n pg.screenshot(path=\"png/static.png\")\n boxes += [[f\"図:{n}\", r] for n, r in pg.evaluate(MEASURE)]\n for k, s in enumerate(subs):\n lines = s.split(\"\\n\")\n assert all(len(ln) <= 26 for ln in lines), s\n svg_lines = \"\".join(f'<text x=\"{SUB_CX}\" text-anchor=\"middle\" class=\"t\">{html.escape(ln)}</text>' for ln in lines)\n pg.set_content(f\"\"\"<!doctype html><html><head><meta charset=\"utf-8\"><style>{SUBCSS}</style></head><body>\n<svg id=\"s\" width=\"1920\" height=\"1080\" viewBox=\"0 0 1920 1080\">{svg_lines}</svg></body></html>\"\"\")\n rects = pg.evaluate(f\"\"\"() => {{\n const ts=[...document.querySelectorAll('text.t')]; const lh=72; const n=ts.length;\n 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)); }}\n ts.forEach((t,k)=>{{ t.style.fontSize=fs+'px'; t.setAttribute('y', 1080-62-(n-1-k)*lh); }});\n return ts.map(t => {{ const r=t.getBoundingClientRect(); return [r.left-6, r.top-6, r.right+6, r.bottom+6]; }});\n }}\"\"\")\n boxes += [[f\"字幕{k}-{j}\", r] for j, r in enumerate(rects)]\n pg.screenshot(path=f\"png/sub_{k}.png\", omit_background=True)\n b.close()\njson.dump(boxes, open(\"png/boxes.json\", \"w\"), ensure_ascii=False)</code></pre><h3 id=\"index_Fr7ec4rQ\">edit_rules.py(video_rules.md に書き足したスクリプト)</h3><p>ルールの文書は、文章を打ち直すと途中が抜けることがあるので、置き換える文が文書の中にちょうど1か所あることを確かめてから置き換えるスクリプトで書き足しました。このスクリプトを動かしたあと、試作のフォルダの説明の1行だけを、次の sed で置き換えています。</p><pre><code class=\"language-python\">\"\"\"video_rules.md にハコブくんのナレーションで決めたことを書き足す(置換は1か所ずつ一致を確かめる)\"\"\"\nimport sys\n\nsrc, dst = sys.argv[1], sys.argv[2]\ns = open(src, encoding=\"utf-8\").read()\n\n\ndef rep(old, new):\n global s\n assert s.count(old) == 1, f\"一致が1か所ではない: {old[:40]}\"\n s = s.replace(old, new)\n\n\n# --- 冒頭 ---\nrep(\"最終更新: 2026-10-11\\n状態: 試作1本目「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」を作りながら決めたことをまとめた初版。まだ決まっていない項目は8節にまとめている。\",\n \"最終更新: 2026-10-12\\n状態: 試作1本目「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」を作りながら決めたことをまとめた初版に、ナレーションをハコブくんにする決定(2026-10-12)を足した版。まだ決まっていない項目は8節にまとめている。\")\n\n# --- 1節 ---\nrep(\"| 1 | 自己紹介 | 〜0:55 | ロゴのテロップを出し、毎回同じ文言で名乗る(2節の文言) |\",\n \"| 1 | 自己紹介 | 〜0:55 | ロゴと、エイイチの名前のテロップを出す。ハコブくんが毎回同じ文言で番組とエイイチを紹介する(2節の文言) |\")\nrep(\"| 8 | 見立て | 〜12:30 | 「ここからが僕の一番言いたいことです」と言ってから意見を話す |\",\n \"| 8 | 見立て | 〜12:30 | 「ここからはエイイチの見立てです」と言ってから意見を話す(意見が誰のものかをはっきりさせる) |\")\nrep(\"| 自分の立ち位置を明かす | 3 結論と立場、または本編1節の頭 | 「僕自身、仕事で使っていて、今回は得をする側なんです」「僕は〜否定派じゃないです。技術としては本物」 |\",\n \"| 自分の立ち位置を明かす | 3 結論と立場、または本編1節の頭 | 「僕自身、仕事で使っていて、今回は得をする側なんです」「僕は〜否定派じゃないです。技術としては本物」。このチャンネルではハコブくんが話すので「エイイチ自身、〜」と言い換える |\")\nrep(\"数字の言い方は5本とも同じだった。\",\n \"台本はすべてハコブくんが話す前提で書く。エイイチの体験と意見は「エイイチは」「エイイチの机の上では」と三人称で言う。\\n\\n数字の言い方は5本とも同じだった。\")\n\n# --- 2節 ---\nrep(\"「僕の機体とソフトの組み合わせでは」を付ける。\", \"「エイイチの機体とソフトの組み合わせでは」を付ける。\")\nrep(\"「配送開始を示す公式発表は、僕が確認した範囲では見つかっていません」\",\n \"「配送開始を示す公式発表は、エイイチが確認した範囲では見つかっていません」\")\nrep(\"自己紹介の文言は「僕は本業のデザイナーをしながら、ロボティクスの研究と開発をしています。自宅の机の上でロボットアームを動かして、その記録を公開しています。」に固定する。「秋田」「29年」「毎日」は入れない。\",\n \"自己紹介の文言は、ハコブくんが話す形の「この番組は、本業のデザイナーをしながらロボティクスの研究と開発をしているエイイチが、自宅の机の上でロボットアームを動かした記録をもとにしています。」に固定する(2026-10-12 に、それまでの一人称の文言を言い換えた。文言を変えるときはエイイチが決める)。「秋田」「29年」「毎日」は入れない。\")\n\n# --- 3節 ---\nrep(\"\"\"## 3. ナレーション(現状は仮音声)\n\nいまは仮の合成音声で作っている。本番の声は未決(8節)。\n\n- **エンジン:** Open JTalk、声は nitech_jp_atr503_m001(男声)。設定は 速さ 1.08、音の高さ -1、声質 0.55\"\"\",\n\"\"\"## 3. ナレーション(ハコブくん・声は仮音声)\n\n### 3-1. ナレーションはハコブくんが話す(2026-10-12 決定)\n\n- ナレーションは、Pixels to Motors のキャラクターのハコブくん(病院で物を運ぶロボット)が担当する。エイイチ本人の声と、本人の声の複製は使わない(本人の声が通らないため。複製しても声の性質はそのまま写る)\n- 動画の目的は、見た人から仕事の相談や声がけをもらうこと。キャラクターの後ろで本人が見えなくならないよう、次の3つを必ず入れる\n - 自己紹介の場面で、エイイチの名前のテロップと実機の映像を出す\n - 見立ての節は「ここからはエイイチの見立てです」と言ってから入る\n - 概要欄にプロフィールと連絡先を置く\n- 概要欄に「ナレーションは AI 音声です」と書く\n- 声は、他のチャンネルと同じ声(VOICEVOX のキャラクターの声など)を使わず、ハコブくん専用の声にする。「ハコブくん=有名キャラクターの声のロボット」と聞かれないようにするため。候補は8節\n- 高すぎない声で、ゆっくり丁寧に話す。経営者が13分聞いて疲れないことを基準にする。キャラクターらしい決まった語尾は本編で使わない(使うならジングルと最初の一言だけ)\n\n### 3-2. 仮音声の設定(ハコブくんの声が決まるまで)\n\n- **エンジン:** Open JTalk、声は nitech_jp_atr503_m001(男声)。設定は 速さ 1.0(2026-10-12 に 1.08 から下げた。ハコブくんの口の動きと合わせると速すぎたため)、音の高さ -1、声質 0.55\"\"\")\n\n# --- 4-1 ---\nrep(\"\"\"- 余白は左右140px。下の250pxは字幕のために空ける\n- 左上に章のタグ(章番号はオレンジ、見出しは薄い文字)、右上にロゴ(高さ34px)\"\"\",\n\"\"\"- 余白は左右140px。下の250pxは字幕のために空ける\n- 左上に章のタグ(章番号はオレンジ、見出しは薄い文字。左端から140px)、右上にロゴ(高さ34px。右端から56px、上端から44px。2026-10-12 に右140px・上46pxから端へ寄せた)\n- **ハコブくんの区画:** 左下に、ハコブくんの上半身を右向きで置く(全身より図の邪魔にならないため、上半身を基本にする)。体は幅340pxで x=20〜360px、画面の下端に接して置く(上端は y=766px)。区画は体の範囲に余白20pxを足した x=0〜380px、y=746〜1080px。この区画には字幕・図・出典・テロップを重ねない。体の画像を差し替えたら、区画は置いた体の範囲から計算し直す(スクリプトが自動で計算する)\n- **図の範囲:** 左右140pxのまま使う。ハコブくんの区画に重なるかどうかは、場面ごとに書き出しの前に自動で確かめる。図・文字の位置はブラウザで描いたときの実際の位置を測り、区画と1pxでも重なった場面は、場面の名前を出して書き出しを止める。止まった場面は、図を区画より上に収めるか、左端を420pxに寄せて作り直す\"\"\")\n\n# --- 4-2 ---\nrep(\"- 新しい色を足すときは、色覚の違いがあっても見分けられるかを検証してから使う(dataviz スキルの validate_palette.js。背景色 #F5F5F0 を指定する)\",\n \"- 新しい色を足すときは、色覚の違いがあっても見分けられるかを検証してから使う(dataviz スキルの validate_palette.js。背景色 #F5F5F0 を指定する)\\n- ハコブくんの体と顔の色(目の白 #F6F8FC、口の水色 #57B5F8、側面のリングの緑)は、この表と4-5の「オレンジと青以外の色を足さない」の対象外。図や文字には使わない\")\n\n# --- 4-5 ---\nrep(\"- 影、グラデーション\\n\", \"- 影、グラデーション(ハコブくんにも影は付けない。目のまわりのにじむ光と側面のリングの光は、ハコブくん自身が光っている表現なので例外)\\n\")\n\n# --- 4-6 と 4-7 ---\nrep(\"数字を出す画面には、左下(字幕の上)に出典を24pxの薄い文字で必ず入れる。\",\n \"数字を出す画面には、字幕の上に出典を24pxの薄い文字で必ず入れる。位置は左端から420px(左下のハコブくんの区画を避ける。もとは140px)。\")\nrep(\"\"\"模式図には「図は模式図」と入れる。\n\n---\"\"\", \"\"\"模式図には「図は模式図」と入れる。\n\n### 4-7. ハコブくんの描き方と動き\n\n試作(2026-10-12、`_制作ベース/hakobu_試作/`)で決めた作り方。数値はスクリプトの先頭近くにまとめてある。\n\n**体**\n- 元の動画(ハコブくんが病室にいる生成動画)の1コマから体を切り抜き、左右反転して右向きにする(画面の中央を向く)\n- 頭の上は、後ろの白い枕と重なっていて切り抜きが欠ける。元の動画では、頭の上は「画面の上の縁が低く、奥側がもう一段持ち上がった」形をしている。切り抜きがはっきり頭と判断した範囲を輪郭に使い、縁のギザギザだけをなめらかにする。色は元のコマの画素で戻す(欠けた部分を直線や凸包で埋めると、奥の段差が消えて枕まで頭として塗ってしまう)\n- 側面の緑のリングは、元の絵の画素が粗いので一度消し、楕円をコードで描き直す\n\n**顔(すべてコードで描く)**\n- 目: 白(#F6F8FC)の縦長のカプセル形(上下の端が半円)。まわりに白い光が少しにじむ。光は画面の外に出さない\n- 口: 水色(#57B5F8)。閉じた口(両端が丸い太い弧)・小(上が広く下が丸い形)・中・大(角の丸い三日月形)の4段階\n- 顔は、ディスプレイの四隅に合わせて射影し、ディスプレイと同じ傾きで描く。四隅は、上下左右の辺のまっすぐな部分にそれぞれ直線を当てはめた交点で決める(角が丸いので、角の点を直接取ると傾きを読み違える)\n- 本番では、体だけを先に反転し、顔は向きどおりに描く(試作は顔を描いてから体ごと反転している。左右で形が違う表情が逆になるため)\n\n**動き**\n- 口: ナレーションの音量で4段階を選ぶ。口の形は2フレームごと(毎秒15回)に、1段階ずつしか変えない。閉じた口と大きい口の間に、必ず小と中がはさまる\n- 目: 2.8秒の周期でゆっくり上下する。口が開くと0.1秒遅れて少し上がる。文の頭で、0.3秒だけ小さく跳ねる\n- まばたき: 2〜4.5秒の間でばらつかせる。文を言い終わった直後にも1回入れる\n- 側面のリング: 顔とは別の3.7秒の周期で、息をするように明滅する(明滅の強さもゆっくり不規則に変える)。声には0.2秒遅れて明るくなり、話し終わるとゆっくり暗くなる。声への反応の強さは文ごとに変える。文の頭と、大きく口を開いた瞬間だけは顔とそろえて強く光る(大きく口を開いたときは、3回に1回ほど光らない)。話していない間も、ときどき弱くまたたく\n\n**表情**\n- 1文ごとに、台本で表情を指定する(narration.py の1文の行に「表情」の欄を足す)\n- 目と頬は表情で決める。口は、話している間はどの表情でも音量で動かし、話していない間だけその表情の口に戻す(「_」の口のまま話すと、声と口が合わなくなるため)\n- 使い分けの目安(1本作って確定する。8節)\n\n| 表情 | 使う場面 |\n|---|---|\n| 丸い目+開いた口 | 基本。説明の文はほぼこれ |\n| 丸い目+小さな笑み | 間、聞き手に戻るところ |\n| 驚いた目+「o」の口 | 今日の問い、意外な数字が出るところ |\n| ジト目 | 「本当にそうでしょうか」と反論を出すところ |\n| 目を閉じる | 「まとめると」、考えている間 |\n| 困り眉 | 失敗や限界の話 |\n| >▽< | 結論が言えたところ、うまくいった実験 |\n| 「III_III」 | 計算中・データを読み込んでいる場面 |\n| ウインク、キラキラ目、照れ、>_<、目が泳ぐ | アウトロや軽い一言だけ |\n\n経営者向けの動画なので、1本で使う表情は上の6つ前後にとどめる。\n\n---\"\"\")\n\n# --- 5節 ---\nrep(\"- 2行まで、1行31字以内。改行は読点の位置で入れる。読点がなければ助詞の後ろで割る\\n- 位置は画面の下から62px\",\n \"- 2行まで、1行26字以内(ハコブくんの区画を避けるため。もとは31字)。改行は読点の位置で入れる。読点がなければ助詞の後ろで割る\\n- 位置は画面の下から62px。横はハコブくんの区画の右側(x=400〜1780px)の中央(x=1090px)にそろえ、1行の幅は1380pxまで\")\n\n# --- 6節 ---\nrep(\"- 映像は 1920×1080 に拡大して切り抜く。字幕の後ろに黒50%の帯(高さ230px)を敷く\",\n \"- 映像は 1920×1080 に拡大して切り抜く。字幕の後ろに黒50%の帯(高さ230px)を敷く。帯はハコブくんの区画の右(x=380px から右)だけに敷く\")\nrep(\"- 名前のテロップ: 薄い地に、ロゴ(高さ30px)・名前60px・肩書き30px(「デザイナー/ロボティクスの研究・開発」)。左の太い線は付けない\",\n \"- 名前のテロップ: 薄い地に、ロゴ(高さ30px)・名前60px・肩書き30px(「デザイナー/ロボティクスの研究・開発」)。左の太い線は付けない。ハコブくんの区画の外に置く\")\n\n# --- 7節 ---\nrep(\"\"\" 5. `assemble.py` で1文ずつ映像を作ってつなぎ、音声と合わせる(字幕の srt も出る)\n 6. 確認用に圧縮する\"\"\",\n\"\"\" 5. `assemble.py` で1文ずつ映像を作ってつなぎ、音声と合わせる(字幕の srt も出る)\n 6. 確認用に圧縮する\n- **ハコブくんの試作(`_制作ベース/hakobu_試作/`):** `python3 make_demo.py upper`(全身版は `full`)で、体の切り抜き・顔の描画・口と目とリングの動き・区画との重なり確認・書き出しまでを行う。図と字幕は `render_static.py` が本番の `scenes.py` を読み込んで描く(`base/` に scenes.py と logo_dark.png の写しを置く)。本番の render.py・assemble.py への組み込みはまだ(8節)\"\"\")\n\n# --- 8節 ---\nrep(\"| ナレーションの声 | Open JTalk の仮音声。抑揚が機械的 | 本人の声で録るか、品質の高い音声合成(VOICEVOX など。利用規約とクレジット表記の条件を確認)にするか。srt に1文ごとの時間が入っているので、それに合わせて録れば映像はそのまま使える |\",\n \"| ハコブくんの声 | ナレーションはハコブくんに決定(3-1)。声は Open JTalk の仮音声 | ハコブくん専用の声を、ElevenLabs の Voice Design(文章で声の特徴を書くと新しい声を作る機能。日本語の出来と、作った声を商用で使う条件は試して確かめる)で作るか、声優に依頼して作るか(毎回読んでもらうか、本人の同意を取って声を複製する。声の権利が一番はっきりする) |\\n| ハコブくんの体の画像 | 生成動画の1コマを切り抜き、頭の上とリングを修整して使っている | 同じ構図(斜め・右向き)で、無地の背景・リングなしの画像を生成し直すか。生成し直せば頭とリングの修整が要らなくなる |\\n| ハコブくんの本番組み込み | 試作(`hakobu_試作/`)で動きと配置を確認した段階 | render.py・assemble.py に組み込む。narration.py に「表情」の欄を足す。表情集の顔をコードで描く |\\n| 表情の使い分け | 4-7の目安の表のみ | 1本作って、使う表情と場面を確定する |\")\nrep(\"| ジングル | 文言だけ(「Pixels to Motors。こんにちは、エイイチです。」) | 音とロゴの動きを付けるか |\",\n \"| ジングル | 文言だけ(「Pixels to Motors。こんにちは、エイイチです。」) | ハコブくんが話す形の文言に言い換える。音とロゴの動きを付けるか |\")\n\n# --- 更新履歴 ---\nrep(\"## 更新履歴\\n\\n\",\n \"## 更新履歴\\n\\n- 2026-10-12: ナレーションをハコブくんにする決定を反映。3-1(ハコブくんが話す・本人を見せる3つの決まり・AI音声の明記・専用の声)と4-7(ハコブくんの描き方と動き・表情)を新設。4-1 にハコブくんの区画(左下・上半身・右向き)と、図の範囲を140pxのまま場面ごとの自動の重なり確認に任せる決まりを追加。ロゴを右56px・上44pxへ寄せた。字幕を1行26字・中央x=1090pxへ、出典を左420pxへ移した。ハコブくんの色を4-2・4-5の対象外にした。台本の一人称(僕)をエイイチの三人称に言い換え、自己紹介の固定文言をハコブくんが話す形にした。仮音声の速さを1.08から1.0に下げた。8節にハコブくんの声・体の画像・本番組み込み・表情の使い分けを追加。\\n\")\n\nopen(dst, \"w\", encoding=\"utf-8\").write(s)\nprint(\"ok\", len(s))</code></pre><pre><code class=\"language-bash\">sed -i 's#図と字幕は `render_static.py` が本番の `scenes.py` を読み込んで描く(`base/` に scenes.py と logo_dark.png の写しを置く)。#図と字幕は `render_static.py` が本番の `scenes.py` と `logo_dark.png`(1つ上の `_制作ベース` にあるもの)を読み込んで描く。入力の画像(`cut_raw.png`・`full/g_05.png`)も同じフォルダに置いてある。#' rules/video_rules.md</code></pre><p></p><h2 id=\"index_NfGgHGVW\">実行ログ</h2><h3 id=\"index_s_TBaVYv\">元の生成動画の情報(ffprobe)</h3><pre><code>stream|codec_type=video|width=848|height=1056|r_frame_rate=24/1\nstream|codec_type=audio|r_frame_rate=0/0\nstream|codec_type=video|width=848|height=1056|r_frame_rate=90000/1\nformat|duration=10.041667</code></pre><h3 id=\"index_ts_1basB\">ディスプレイ・目・口・リングの測定(measure_face.py)</h3><pre><code>ディスプレイの外接矩形 (x, y, 幅, 高さ): [325, 349, 183, 151]\n白の領域: x=482 y=344 幅=29 高さ=52 面積=172\n白の領域: x=362 y=395 幅=22 高さ=46 面積=799\n白の領域: x=305 y=396 幅=170 高さ=124 面積=2301\n白の領域: x=446 y=396 幅=23 高さ=48 面積=885\n水色の領域: x=378 y=450 幅=68 高さ=40 面積=2115\n口の色(中央値 RGB): [87, 181, 248]\nリングの範囲: 576 548 629 681 画素数 1793\n体の範囲: 291 328 648 904\n頭の上の行ごとの画素数(アルファ>200 / >60 / >10):\n y=320: 0 / 0 / 0\n y=324: 0 / 0 / 36\n y=328: 0 / 42 / 151\n y=332: 33 / 77 / 183\n y=336: 56 / 95 / 212\n y=340: 70 / 167 / 232\n y=344: 198 / 237 / 249\n y=348: 240 / 250 / 261\n y=352: 251 / 257 / 264\n y=356: 258 / 264 / 270\n上の辺: 点 74 個、傾き 0.18°\n下の辺: 点 74 個、傾き 2.16°\n左の辺: 点 60 個、傾き 5.97°\n右の辺: 点 61 個、傾き 5.67°\nディスプレイの四隅(左上・右上・右下・左下): [[336.8, 348.8], [510.2, 349.3], [495.1, 501.2], [321.5, 494.6]]\n参考・角の点を直接取った四隅(eps=9.0): [[504, 360], [488, 496], [327, 485], [346, 354]]</code></pre><h3 id=\"index_wL1s2RcT\">上半身版の書き出し(python3 make_demo.py upper)</h3><pre><code>ハコブくんの区画: (0, 746, 380, 1080) 体の位置: (20, 766, 360, 1080)\n重なり確認: 文字・図・ロゴ 12 個、区画との重なり 0 件\nframes 398 seconds 13.27\n終了コード: 0</code></pre><h3 id=\"index_qWOd32_u\">全身版の書き出しが、重なりの確認で止まった記録(python3 make_demo.py full)</h3><pre><code>ハコブくんの区画: (0, 580, 353, 1080) 体の位置: (47, 600, 333, 1060)\n 重なり: 図:h2 [140, 589.40625, 1780, 672.59375]\n区画に重なる要素があるため中止しました\n終了コード: 1</code></pre><h3 id=\"index_0eceRLsx\">書き出した上半身版の動画の情報(ffprobe)</h3><pre><code>stream|codec_name=h264|width=1920|height=1080|r_frame_rate=30/1\nstream|codec_name=aac|sample_rate=48000|r_frame_rate=0/0\nformat|duration=13.266667|size=530991\n</code></pre><h3 id=\"index_C2dvPBTh\">図解用に時間の値を書き出した記録(python3 make_demo.py upper --dump)</h3><pre><code>timing.json に書き出しました\nフレーム数 398 / 文ごとの開始と発話終了のフレーム [[9, 51], [62, 195], [216, 371]] / リングの明るさの範囲 0.15 〜 1.0 / まばたきのフレーム数 24</code></pre><p></p><h2 id=\"index_Saxw8HVI\">参考にしたページ</h2><ul><li><p><a href=\"https://elevenlabs.io/pricing\" data-has-link=\"true\">ElevenLabs:Pricing</a></p></li><li><p><a href=\"https://elevenlabs.io/docs/product-guides/voices/voice-design\" data-has-link=\"true\">ElevenLabs:Voice Design</a></p></li><li><p><a href=\"https://voicevox.hiroshiba.jp/term/\" data-has-link=\"true\">VOICEVOX:ソフトウェア利用規約</a></p></li><li><p><a href=\"https://support.google.com/youtube/answer/3376882?hl=ja\" data-has-link=\"true\">YouTube ヘルプ:オーディオライブラリの音楽と効果音を使う</a></p></li><li><p><a href=\"https://dova-s.jp/_contents/license/\" data-has-link=\"true\">DOVA-SYNDROME:音源利用ライセンス</a></p></li><li><p><a href=\"https://soundeffect-lab.info/agreement/\" data-has-link=\"true\">効果音ラボ:利用規約</a></p></li><li><p><a href=\"https://www.epidemicsound.com/pricing/\" data-has-link=\"true\">Epidemic Sound:Pricing</a></p></li><li><p><a href=\"https://github.com/danielgatis/rembg\" data-has-link=\"true\">rembg(GitHub)</a></p></li></ul>", "toc": [{"id": "index_Wy0UmY2h", "text": "動画の企画5本は、見た人が「この人に何を頼めるか」が分かる話を選んだ"}, {"id": "index__9qJii4D", "text": "ナレーション・BGM・効果音は、2026年10月11日時点の公式の条件で比べた"}, {"id": "index_F7VUaN1Y", "text": "ナレーションは、本人の声でも声の複製でもなく、ハコブくんに決めた"}, {"id": "index_O4m3dwLv", "text": "ハコブくんは画面の左下に置き、生成した10秒の動画の1コマから体を切り抜いた"}, {"id": "index_g2Fv9MC5", "text": "顔の目と口は、元の絵を消してコードで描き直した"}, {"id": "index_Ufl5ndIQ", "text": "口は音量で4段階に切り替え、1回に1段階ずつしか動かさない"}, {"id": "index_V8JsKfRO", "text": "目は2.8秒の周期で上下し、口より0.1秒遅れて上がる"}, {"id": "index_W02dAGsw", "text": "側面のリングは、元の絵を消して楕円を描き直し、顔と別の周期で光らせた"}, {"id": "index_tXF5W1d5", "text": "目はカプセル形にし、顔はディスプレイの辺に直線を当てはめて傾けた"}, {"id": "index_fNoBvJtt", "text": "頭の上は、凸包で埋めると奥の段差が消えて枕まで塗っていた"}, {"id": "index_boeVOUIR", "text": "ハコブくんの区画に字幕や図が重なったら、書き出す前に止める"}, {"id": "index_ZOcuqpzA", "text": "全身版より上半身版の方が、図を読む邪魔をしなかった"}, {"id": "index_H1Z4xux4", "text": "決めたことは video_rules.md に3-1節と4-7節として書き足した"}, {"id": "index_OMOijwC5", "text": "まだ決まっていないのは、ハコブくんの声・体の画像・本番への組み込み・表情の4つ"}, {"id": "index_AtrH00k0", "text": "試作スクリプトの全文"}, {"id": "index_NfGgHGVW", "text": "実行ログ"}, {"id": "index_Saxw8HVI", "text": "参考にしたページ"}]}
\ No newline at end of file
+{"day": "LOG331", "series": "ロボティクスデザイン", "published": "2026-10-12T00:08:34.000Z", "title": "解説動画のナレーションをハコブくんに任せる ― 口と目と光をコードで動かし、文字が重ならない区画を決めた", "slug": "explainer-video-character-narration", "cover": null, "thumb": "/thumbs/r-explainer-video-character-narration.html", "lang": "JP", "description": "YouTube解説動画のナレーションを本人の声ではなくキャラクターのハコブくんに任せると決め、口・目・側面の光をコードで動かす試作を作った記録。字幕や図が重ならない区画と自動の重なり確認も決めた。", "updated": "2026-10-12T00:09:22.000Z", "body": "<!--TOC--><p></p><p>Pixels to Motors の YouTube 解説動画について、続きの作業を Claude と対話しながら進めました。前回(解説動画をAIと作る ― 型の分析から敵対的検証まで、試作1本で決めたこと)で試作1本と制作ルールができたので、今回は「何を話すか」と「誰が話すか」を決めています。</p><p>やったことは4つです。サイトの記事から、ロボティクスに関心のある経営者・PM・マネージャー・開発者に向けた動画の企画を5本立てる。ナレーションの声と、BGM・効果音の候補を、公式の利用条件で比べる。ナレーションを本人の声ではなく、Pixels to Motors のキャラクターのハコブくん(病院で物を運ぶロボット)にすると決める。ハコブくんを画面の左下に置き、口・目・側面の光をコードで動かす試作を作る。</p><p>試作は、フィードバックを受けて十数回作り直しました。口の形、目の動き、側面の光り方、頭の上の切り抜き、顔の傾き、置く大きさを、ひとつずつ直しています。本記事は、その過程で決めたことと、決めた理由の記録です。間違えた直し方(凸包で頭を埋めた、角の点で傾きを測った、など)も消さずに残しました。試作スクリプトの全文と実行ログは記事の後半に載せています。</p><p></p><h2 id=\"index_Wy0UmY2h\">動画の企画5本は、見た人が「この人に何を頼めるか」が分かる話を選んだ</h2><p>動画の目的は、見た人から仕事の相談や依頼をもらうことです。そこで、個人の体験談よりも、見る人が自分の会社やチームの判断に使える話を優先しました。サイトの記事を読み直し、経営者・PM・開発者に向けて5本の企画を立てています。</p><p>選ぶときの基準は、自分の机の上で実測した話が入っていることです。業界の解説だけなら、ほかの人にも作れます。実測の数字が入っていると、この人に頼むと何をしてくれるのかが見る人に伝わります。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>公開する順</p></th><th colspan=\"1\" rowspan=\"1\"><p>企画</p></th><th colspan=\"1\" rowspan=\"1\"><p>冒頭の一言</p></th><th colspan=\"1\" rowspan=\"1\"><p>元にした記事</p></th><th colspan=\"1\" rowspan=\"1\"><p>見た人に伝わること</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>AIエージェントにロボットのコードを書かせたら、原因を7回取り違えた</p></td><td colspan=\"1\" rowspan=\"1\"><p>テストは53件、全部通った。実機に出したら、アームの動きは前と何も変わっていなかった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG294、LOG273、LOG275、LOG170</p></td><td colspan=\"1\" rowspan=\"1\"><p>AIで書く・別のAIでレビューする・実機で小さく通す、という手順を組める</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィジカルAIの研究環境は、買う前に借りて測る</p></td><td colspan=\"1\" rowspan=\"1\"><p>GPUメモリは34,399MiB使われているように見えた。実際に使っていたのは8,486MiBだった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG326、LOG327</p></td><td colspan=\"1\" rowspan=\"1\"><p>本格導入の前の小さな検証(PoC)を、安く速く回せる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィジカルAIの導入は、ロボットを買う前の「仕事の切り出し」で止まっている</p></td><td colspan=\"1\" rowspan=\"1\"><p>清掃ロボットの管理画面に出る4項目は、故障を調べるためではなく、契約どおり掃除したことを示す請求書だった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG306、LOG314、LOG286</p></td><td colspan=\"1\" rowspan=\"1\"><p>導入前に、どの仕事をロボットに渡し、何を人に残すかを分けられる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>ロボットの「成功」を何で数えるか ― 完走したのに、ブロックを掴めていなかった</p></td><td colspan=\"1\" rowspan=\"1\"><p>アームは最後まで止まらずに動き切った。でも、ブロックは掴めていなかった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG246、LOG248、LOG268、LOG220</p></td><td colspan=\"1\" rowspan=\"1\"><p>評価の指標と、成否を記録する仕組みを設計できる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>ロボット企業の勝ち方は3つに分かれる ― 汎用モデル、環境の作り込み、用途の固定</p></td><td colspan=\"1\" rowspan=\"1\"><p>「人型ロボット40万台」と報じられたトヨタの計画は、既存の産業用ロボット40万台の頭脳を入れ替える計画だった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG315、LOG282、LOG277、LOG329、LOG291</p></td><td colspan=\"1\" rowspan=\"1\"><p>業界の動きを構造で整理し、相手の会社の選択肢に落とせる</p></td></tr></tbody></table></div><p>公開する順は、自分の実測が中心の2本(AIエージェントの回、借りて測る回)を先にしました。ほかの人には作れない動画だからです。仕事の切り出しの回は、相談や契約にいちばん直接つながる企画ですが、実績の動画を2本見せてから出す方が説得力が出ると考えて3本目に置きました。</p><p>企画を立てる途中で、注意が要る点も3つ見つかりました。2本目は、速度やメモリの数字を必ず GPU の型番(NVIDIA A40)と組で言う。4本目は「安全装置を外した実験」の話が入るので、実験の条件(速さ、周りに人がいたか、止める手段)を言わないと、安全を軽く見ているように聞こえる。5本目は自分の実測がない回なので、会社の自己申告の数字は「会社の発表では」と言い、最後に自分の机の上で同じ判断をした場面を1つ入れる。</p><p></p><h2 id=\"index__9qJii4D\">ナレーション・BGM・効果音は、2026年10月11日時点の公式の条件で比べた</h2><p>音声は映像と別に作る前提なので、ナレーションの声、BGM、効果音の候補を、それぞれの公式ページの利用条件で確かめました。料金や条件は変わるので、表は2026年10月11日に確認した時点のものです。</p><p>ナレーションの候補は3つでした。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>方法</p></th><th colspan=\"1\" rowspan=\"1\"><p>条件(確認した時点)</p></th><th colspan=\"1\" rowspan=\"1\"><p>経営者向けの動画に合うか</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>本人の声で録る</p></td><td colspan=\"1\" rowspan=\"1\"><p>字幕ファイルに1文ごとの時間が入っているので、それに合わせて録れば映像はそのまま使える</p></td><td colspan=\"1\" rowspan=\"1\"><p>仕事の相談は人を見て決まるので、最初はこれを勧めた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ElevenLabs で声を複製する</p></td><td colspan=\"1\" rowspan=\"1\"><p>Creator プランは月11ドルの表示(初月の割引の表記あり)。商用ライセンス付きで、本格的な声の複製(Professional Voice Cloning)が1つ作れる</p></td><td colspan=\"1\" rowspan=\"1\"><p>録る時間が取れない回の代わりになる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>VOICEVOX</p></td><td colspan=\"1\" rowspan=\"1\"><p>商用・非商用を問わず無料。VOICEVOX を使ったことが分かるクレジットの表記が必要で、キャラクターごとの規約にも従う</p></td><td colspan=\"1\" rowspan=\"1\"><p>キャラクターの声は娯楽系の解説動画の印象が強く、相談先として見てもらいにくい</p></td></tr></tbody></table></div><p>BGM と効果音は次の候補です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>用途</p></th><th colspan=\"1\" rowspan=\"1\"><p>サービス</p></th><th colspan=\"1\" rowspan=\"1\"><p>条件(確認した時点)</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM・効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>YouTube オーディオライブラリ</p></td><td colspan=\"1\" rowspan=\"1\"><p>収益化している動画で使え、権利者からの著作権の申し立て(Content ID)が来ない。YouTube が著作権上安全と認めているのはここの音源だけ。Creative Commons の曲は概要欄にクレジットが要る</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM</p></td><td colspan=\"1\" rowspan=\"1\"><p>DOVA-SYNDROME</p></td><td colspan=\"1\" rowspan=\"1\"><p>無料、商用OK、クレジット不要。作曲者ごとに別の条件がある場合はそちらが優先。音楽が主役の動画やリミックスは禁止</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM・効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>Epidemic Sound</p></td><td colspan=\"1\" rowspan=\"1\"><p>有料。使用料の追加払いはない。広告に使うには Pro 以上のプランが要る</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>効果音ラボ</p></td><td colspan=\"1\" rowspan=\"1\"><p>無料、商用OK、YouTube の収益化OK、クレジット不要。効果音だけを流す動画は再配布として禁止</p></td></tr></tbody></table></div><p>効果音は、自分のロボットアームのサーボが回る音やグリッパーが閉じる音を録って、章の扉の音にする案も出しました。権利の心配がなく、このチャンネルにしかない音になります。入れる場所は、動画のルールで「打ち出し」と決めている場面(1本に6〜10場面)だけにしぼります。</p><p></p><h2 id=\"index_F7VUaN1Y\">ナレーションは、本人の声でも声の複製でもなく、ハコブくんに決めた</h2><p>声の候補を並べたあと、ナレーションをハコブくんにしたいという相談がありました。</p><blockquote><p>音声はハコブくんをナレーションキャラにしようと思いますが、自分の声の複製とどちらが良いですか?(自分の声は別に良いとは思わない。声が通らない)</p></blockquote><p>ハコブくんの方が良い、と答えました。理由は3つです。声の複製は元の声の性質をそのまま写すので、声が通らないのが不満なら、複製しても通らない声を毎回使うことになります。ハコブくんはすでに Pixels to Motors の4コママンガに出ているキャラクターで、開発ログ(day_log)の発信戦略にも「実機展開以降、ハコブくんをR2R2Rの概念解説役として再登場させる」と書いてありました。もう1つは、本人の実機の話を「エイイチの机の上では、こうなりました」と三人称で言えることです。動画のルールにある「自分の実機の話は、自分の条件の話として言う」が、言い方だけで自然に守れます。</p><p>ただ、目的が仕事の相談をもらうことなので、キャラクターの後ろで本人が見えなくなると効果が落ちます。そこで次の3つを必ず入れると決めました。</p><ul><li><p>自己紹介の場面で、本人の名前のテロップと実機の映像を出す</p></li><li><p>意見を話す節は「ここからはエイイチの見立てです」と言ってから入る(意見が誰のものかをはっきりさせる)</p></li><li><p>概要欄にプロフィールと連絡先を置き、「ナレーションは AI 音声です」と書く</p></li></ul><p>声は、ほかのチャンネルと同じ声(VOICEVOX のキャラクターの声など)を使わず、ハコブくん専用の声にします。「ハコブくん=有名なキャラクターの声のロボット」と聞かれないようにするためです。作り方の候補は2つで、ElevenLabs の Voice Design(文章で声の特徴を書くと新しい声を作る機能)か、声優に頼んで作ってもらうかです。Voice Design は、日本語での出来と、作った声を商用で使う条件が公式の説明に書かれていなかったので、試して確かめる必要があります。話し方は、高すぎない声でゆっくり丁寧に、経営者が13分聞いて疲れないことを基準にしました。</p><p></p><h2 id=\"index_O4m3dwLv\">ハコブくんは画面の左下に置き、生成した10秒の動画の1コマから体を切り抜いた</h2><p>置き方のイメージとして、参考にしている解説動画(前回、型を分析した3本のうち、会社への侵入と暗数の回)の画面が共有されました。画面の左下でサングラスの猫のキャラクターがマイクの前で話していて、字幕は猫の右側から始まっています。</p><blockquote><p>画面の左端にこのような形で話しているハコブくんを追加するイメージです。</p></blockquote><p>あわせて、病室にいるハコブくんの生成動画(10秒)が共有されました。最初に考えたのは、この動画をそのまま左下で繰り返し流す方法ですが、やめました。動画の中の口は声と関係なく開閉するので、話していない間も口が動きます。10秒の動画を13分繰り返すと、同じ動きの繰り返しだと気づかれます。背景の病室を毎フレーム切り抜くと、縁が揺れます。</p><p>そこで、体は動画の1コマを切り抜いた静止画にし、顔だけをコードで描く方法にしました。ハコブくんの顔はディスプレイなので、画面の中の目と口を描き替えれば話しているように見せられます。</p><p>動画は 848×1056 ピクセル・24fps・10.04秒でした。1秒に4枚ずつ、40枚のコマを取り出して並べ、全身と側面の緑のリングが見えている5枚目を選んでいます。背景の切り抜きには rembg(画像から人や物の背景を消すライブラリ)の isnet-general-use というモデルを使いました。</p><p></p><figure><img src=\"/media/s-1920x956_v-frms_webp_dd2ed6ca-70c6-4449-8572-3287b85ed201.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_g2Fv9MC5\">顔の目と口は、元の絵を消してコードで描き直した</h2><p>切り抜いた体のディスプレイを測り、元の目と口の位置と色を取りました(測定の全量は記事後半の実行ログにあります)。ディスプレイは横183×縦151ピクセル、元の口の色は #57B5F8 の水色です。元の目と口はディスプレイごと黒(#131311)で塗りつぶし、その上に同じ位置で目と口を描きます。</p><p>口はナレーションの音量に合わせて動かします。音声を1フレーム(1/30秒)ずつ区切って音量を測り、大きいほど口を大きく開く形にしました。最初の試作は、目が白い楕円、口が「閉じた笑顔/小さく開く/大きく開く」の3段階で、体は左を向いたままでした。</p><p>【画像】DAY331_03_first_prototype_faces.png</p><p>体の向きは、すぐに直しが入りました。</p><blockquote><p>ハコブくんは右向きがいいですね。</p></blockquote><p>左下に置くと、右を向けば画面の中央(図と字幕の側)を向くことになります。体の画像を左右反転して右向きにしました。</p><p>ここで1つ間違えています。その少し前に13種類の表情のイメージ(表情集)が共有されていて、表情集の顔は目も口も同じ薄い水色でした。右向きに直すときに、目と口を両方その水色(#BDE8FB)に変えてしまいました。</p><blockquote><p>色は目は白、口は水色のままでいいですよ</p></blockquote><p>表情集は形の参考で、色は元の動画のままでよかった、ということです。目を白、口を元の水色に戻しました。</p><p></p><figure><img src=\"/media/s-1920x1216_v-frms_webp_0e7f1609-60a9-4748-b723-9db00d3fde2a.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_Ufl5ndIQ\">口は音量で4段階に切り替え、1回に1段階ずつしか動かさない</h2><p>最初の口は、閉じた口からいきなり大きい口に変わることがあり、角も立っていました。</p><blockquote><p>口はもっと柔らかさを出すために角を丸くした方がいいですね(小さい口を途中に入れたりすると動きが出てより良いです)</p></blockquote><p>口の形を4段階にしました。閉じた口は両端が丸い太い弧、小さい口は上が少し広く下が丸い形、中と大は角を丸めた三日月形です。角は、4倍の大きさで形を描いてから、半径5〜6ピクセル相当の丸で角を削っています(モルフォロジーのオープン処理。小さな丸が入り込めない角だけが削れる)。</p><p>切り替え方も変えました。音量から決めた段階に向かって、1回に1段階ずつしか動かしません。閉じた口から大きい口へ行くときは、必ず小と中を通ります。</p><p>次の指摘は速さでした。</p><blockquote><p>口の動きや目の動きをもう気持ちだけゆっくりに動かしたほうがいいね(今は早すぎる。話し方も早すぎなのかもだけど)</p></blockquote><p>口の形を変えるのを、毎フレーム(1秒に30回)から2フレームごと(1秒に15回)にしました。仮の音声の話す速さも1.08倍から1.0倍に落としています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>最初</p></th><th colspan=\"1\" rowspan=\"1\"><p>最後</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>口の段階</p></td><td colspan=\"1\" rowspan=\"1\"><p>3段階(閉じる・小・大)</p></td><td colspan=\"1\" rowspan=\"1\"><p>4段階(閉じる・小・中・大)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>口の切り替え</p></td><td colspan=\"1\" rowspan=\"1\"><p>毎フレーム、段階を飛ばしてよい</p></td><td colspan=\"1\" rowspan=\"1\"><p>2フレームごと、1回に1段階</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>口の角</p></td><td colspan=\"1\" rowspan=\"1\"><p>尖っている</p></td><td colspan=\"1\" rowspan=\"1\"><p>半径5〜6ピクセル相当で丸める</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>仮の音声の速さ</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.08倍</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.0倍</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>問いかけの文のあとの口</p></td><td colspan=\"1\" rowspan=\"1\"><p>「o」の口</p></td><td colspan=\"1\" rowspan=\"1\"><p>小さく開いた口のまま止める</p></td></tr></tbody></table></div><p></p><h2 id=\"index_V8JsKfRO\">目は2.8秒の周期で上下し、口より0.1秒遅れて上がる</h2><blockquote><p>目が少し上下しているように描写すると表情がイキイキして見えます。</p></blockquote><p>目に3つの動きを足しました。1つ目は、ゆっくりした上下の揺れです。最初は1.9秒の周期でしたが、速すぎるという指摘のあと2.8秒に延ばしました。2つ目は、口が大きく開くほど目が少し上がる動きです。口と同時に動くと機械的に見えるので、口より3フレーム(0.1秒)遅れて上がるようにしています。3つ目は、文の頭で目が0.3秒だけ小さく跳ねる動きです。</p><p>まばたきも、最初は3.2秒ごとの一定間隔でしたが、2〜4.5秒の間でばらつかせ、文を言い終わった直後にも1回入れました。</p><div class=\"embed\"><iframe src=\"/embeds/explainer-video-character-narration-QIb2qI10.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p></p><h2 id=\"index_W02dAGsw\">側面のリングは、元の絵を消して楕円を描き直し、顔と別の周期で光らせた</h2><p>側面の緑のリングは、最初は元の絵の画素を明るくしていただけでした。</p><blockquote><p>側面の光っている部分をもっと丁寧に描写(光方が汚く見える)</p></blockquote><p>元の絵のリングは、生成動画を縮小した画像なので画素が粗く、明るくすると粗さが目立ちます。リングの画素に楕円を当てはめて位置と大きさを測り、元のリングはまわりの色で塗りつぶして消しました(OpenCV の inpaint。消したい範囲を、まわりの画素から推測した色で埋める処理)。その上に、4倍の大きさで描いた楕円を縮めて置き、ぼかした光を重ねています。光は体の外には出しません。</p><p></p><figure><img src=\"/media/s-693x876_v-fs_webp_42809200-9e99-483a-85d0-4093607ecd3e.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>次の指摘は、光と表情が一緒に動いていることでした。</p><blockquote><p>側面の光と表情の動きが同じタイミングで繰り返しているので、ずらしたり、合わせたりして躍動感(生命感)を出すと良さそう</p></blockquote><p>普段はずらし、要所だけそろえる動きにしました。リングは、声に0.2秒遅れて明るくなり、話し終わったあとはゆっくり暗くなります(つくときは速く、消えるときは遅い)。話していない間も、息をするように少し明滅します。この明滅の周期は3.7秒で、目の揺れ(2.8秒)と周期を変えてあるので、目とリングが同じ組み合わせで繰り返しません。そろえるのは、文の頭と、口を大きく開いた瞬間だけです。</p><blockquote><p>側面の光は強く光時と、弱く光ときを混ぜ込むと良さそう(ランダム性があるといい)</p></blockquote><p>ばらつきを4か所に入れました。</p><ul><li><p>声への反応の強さを、文ごとに変える(強い文と弱い文が混ざる)</p></li><li><p>口を大きく開いた瞬間の光り方を毎回変え、3回に1回ほどは光らない</p></li><li><p>息をするような明滅の強さを、ゆっくり不規則に変える</p></li><li><p>話していない間も、ときどき弱くまたたく</p></li></ul><p>ばらつきは乱数の種を7に固定して作っているので、同じスクリプトを何度動かしても同じ光り方になります。</p><p></p><h2 id=\"index_tXF5W1d5\">目はカプセル形にし、顔はディスプレイの辺に直線を当てはめて傾けた</h2><p>目の形は、元の動画の顔に合わせる指示がありました。</p><blockquote><p>目のデザインはこれに合わせて欲しい。</p></blockquote><p>元の動画の目は、楕円ではなく、上下の端が半円になった縦長のカプセル形で、まわりに白い光がにじんでいます。カプセル形に変え、光のにじみを足しました。にじみはディスプレイの中だけに収めています。</p><p>続けて、傾きの指摘がありました。</p><blockquote><p>目は顔のディスプレイに合わせて少しだけ斜めにすると良さそう</p></blockquote><p>ハコブくんは斜め前から見た構図なので、ディスプレイも少し傾いて見えます。まっすぐ描いた顔を、ディスプレイの四隅に合わせて射影変換(四角形を別の四角形にはめ込む変形)しました。最初は、ディスプレイの輪郭を4つの角の点に近似して四隅を取りましたが、合いませんでした。</p><blockquote><p>ちょっと目と口の描写が顔のディスプレいと合ってないね。微妙な差だけど。</p></blockquote><p>ディスプレイは角が丸いので、角の点を直接取ると、丸みに引っぱられて位置がずれます。最初の四隅だと上の辺まで傾いていて、顔が回って見えていました。上下左右の辺の、まっすぐな中央4割の点にそれぞれ直線を当てはめ、直線どうしの交点を角にしました。測り直すと、上の辺は0.18度でほぼ水平、左右の辺は約6度(5.97度と5.67度)、下の辺は2.16度傾いていました。</p><p></p><figure><img src=\"/media/s-840x876_v-fs_webp_6637392f-3158-4208-b51c-0fa6fd52d0e6.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>目と口の間隔も2回、ごく少しだけ直しています。</p><blockquote><p>ほんの少しだけだけど、目と口が近い方がデザインが近くなるね(ごくごく少しだけ)</p></blockquote><blockquote><p>目と口の位置がほんの少しだけ上に(本当に少しだけ)</p></blockquote><p>1回目は、目を2ピクセル下げ、口を1.5ピクセル上げて、間を3.5ピクセル詰めました。2回目は、間隔を保ったまま両方を1.5ピクセル上げています(どちらも元の画像の大きさでのピクセル数で、動画上ではその約8割の動きです)。スクリプトでは <code>EYE_SHIFT = 0.5</code> と <code>MOUTH_SHIFT = -3.0</code> の2つの数字で調整できます。</p><p></p><h2 id=\"index_fNoBvJtt\">頭の上は、凸包で埋めると奥の段差が消えて枕まで塗っていた</h2><p>頭の上は、3回直しています。元の動画では、ハコブくんの白い頭が後ろの白い枕と重なっていて、切り抜くと頭の上が薄く欠けます。</p><blockquote><p>あとサンプルの頭の上が切れてしまっているのを修正してください。</p></blockquote><p>1回目は、欠けた部分を含めた頭の輪郭の凸包(輪郭の外側に輪ゴムをかけたような、へこみのない形)で埋めました。</p><blockquote><p>頭の描写をもっと綺麗に処理して(元のデザイン見直してください)</p></blockquote><p>2回目は、凸包の角を大きく丸めてなめらかにしました。</p><blockquote><p>まだ頭の処理が汚いです。</p></blockquote><p>ここで、欠けた部分を下の色から塗り延ばす案を出しましたが、実行する前に止められました。</p><blockquote><p>元の動画があるから見直してください!</p></blockquote><p>元の動画の40枚のコマを並べ、そのうち8枚で頭の上を拡大すると、ハコブくんの頭は、画面のすぐ上の縁が低く、奥側がもう一段持ち上がった形をしていました。凸包は、この段差を直線で結んでしまいます。結んだ内側には後ろの枕が入っていたので、枕まで頭として塗り、形が崩れて汚く見えていました。</p><p>3回目は、凸包をやめました。切り抜きがはっきり頭と判断した範囲(透明度を表すアルファ値が60以上)をそのまま輪郭に使い、縁のギザギザだけを、4倍に拡大してぼかしてから切り直す方法でなめらかにしています。輪郭の内側は頭そのものなので、色は元のコマの画素で戻しました。縁の半透明の画素も元の色で戻したので、2回目まで出ていた暗い点線も消えています。</p><p></p><figure><img src=\"/media/s-882x1224_v-fms_webp_7f743859-c942-49dc-afff-0603e1d0848a.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_boeVOUIR\">ハコブくんの区画に字幕や図が重なったら、書き出す前に止める</h2><p>左下にキャラクターを置くと、字幕や図とぶつかります。試作を作る前に、置き方の決め事が必要だという話になりました。</p><blockquote><p>左端の顔の部分にテキストがかからないように設定しておくことが必要ですね。</p></blockquote><p>ハコブくんの「区画」を決め、そこには何も重ねないことにしました。区画は、実際に置いたハコブくんの体の範囲に余白20ピクセルを足した四角です。動画のルールで決めていた画面の作りとは、次の点が変わります。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>もとの決まり</p></th><th colspan=\"1\" rowspan=\"1\"><p>ハコブくんを置いたあと</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの区画</p></td><td colspan=\"1\" rowspan=\"1\"><p>なし</p></td><td colspan=\"1\" rowspan=\"1\"><p>左下の x=0〜380、y=746〜1080(上半身版)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>字幕の位置</p></td><td colspan=\"1\" rowspan=\"1\"><p>画面の中央、1行31字まで</p></td><td colspan=\"1\" rowspan=\"1\"><p>区画の右側(x=400〜1780)の中央、1行26字まで</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>出典の位置</p></td><td colspan=\"1\" rowspan=\"1\"><p>左端から140ピクセル</p></td><td colspan=\"1\" rowspan=\"1\"><p>左端から420ピクセル</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>図の範囲</p></td><td colspan=\"1\" rowspan=\"1\"><p>左右140ピクセルの余白</p></td><td colspan=\"1\" rowspan=\"1\"><p>左右140ピクセルのまま。区画に重なる場面は自動で止める</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ロゴの位置</p></td><td colspan=\"1\" rowspan=\"1\"><p>右から140ピクセル、上から46ピクセル</p></td><td colspan=\"1\" rowspan=\"1\"><p>右から56ピクセル、上から44ピクセル</p></td></tr></tbody></table></div><p></p><figure><img src=\"/media/s-1024x648_v-fs_webp_79281675-7ed6-4030-9539-7b81072bb026.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>ロゴは、途中まで試作が「Pixels to Motors」の文字を自分で描いていました。</p><blockquote><p>それと動画のデザインルールを確認して欲しい(どこかにデータがある。ロゴも修正)</p></blockquote><p>iCloud の制作ベースのフォルダを開くと、本番は scenes.py(場面ごとの図の HTML と CSS)、render.py(字幕の画像)、logo_dark.png(ロゴの画像)で画面を作っていました。試作が自前で描いていた部分は、章のタグの書き方や、大きい数字の単位の色も本番と違っていたので、図と字幕は本番の scenes.py をそのまま読み込んで描く形に作り直しています。そのあと、ロゴの位置に指摘がありました。</p><blockquote><p>右上のロゴはもっと端に寄せたほうがいいね。パディング取りすぎ。</p></blockquote><p>重なりの確かめ方は、目で見るのをやめ、書き出しの前の自動の確認にしました。図と字幕はブラウザ(Playwright で動かす Chromium)で描いているので、描いたあとに、見出し・数字・出典・ロゴ・字幕の1行ずつの位置を、ブラウザから実際の四角として取り出します(getBoundingClientRect)。その四角がハコブくんの区画と1ピクセルでも重なったら、どの要素がどこで重なったかを出して、書き出しを止めます。</p><p>図の範囲をどうするかは、2つの案から選んでもらいました。図の左端を420ピクセルに寄せたままにするか、本番どおり140ピクセルにして場面ごとの自動の確認に任せるかです。</p><blockquote><p>場面ごとに自動の重なり確認に任せて広げる</p></blockquote><p>上半身版では、12個の要素すべてが区画に重なりませんでした。同じ場面を全身版で書き出すと、区画が高い位置(y=580)から始まるので、「起動時に確保された量の 約28%」の見出しが重なり、書き出しが止まりました。要素の四角で判定しているので、文字が短くても段落の幅いっぱいが判定の対象になります。止まった場面は、図を区画より上に収めるか、左端を420ピクセルに寄せて作り直します。</p><h2 id=\"index_ZOcuqpzA\">全身版より上半身版の方が、図を読む邪魔をしなかった</h2><p>側面のリングを直すときに、全身版と上半身版の2つを作るように言われ、両方を書き出しました。全身版は高さ460ピクセルで体全体を、上半身版は幅340ピクセルで引き出しの取っ手の下あたりまでを置き、画面の下端に接しています。</p><blockquote><p>上半身のデザインがいいですね。全身は少し邪魔しているように見えます。</p></blockquote><p>全身版は、体の下半分(引き出しや車輪)が図と字幕の高さまで伸びていて、視線がそちらに引っぱられます。上半身版は、顔と側面のリングだけが画面の端から出ているので、図を読む邪魔をしません。上半身版を基本にしました。</p><p></p><figure><img src=\"/media/s-811x1056_v-fs_webp_f590a47e-2382-46cb-ad00-ad8d63bd22a9.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>最後の画面はこの形です。</p><p></p><figure><img src=\"/media/s-1920x1080_v-frms_webp_4418024c-4b1e-44f2-a658-4ee6646c54bb.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_H1Z4xux4\">決めたことは video_rules.md に3-1節と4-7節として書き足した</h2><p>ここまで決めたことは、動画制作のルール(video_rules.md)に書き足しました。ルールの文書はプロジェクトの文書と、iCloud の制作ベースのフォルダに同じ内容の写しがあるので、両方を同じ内容に更新しています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>節</p></th><th colspan=\"1\" rowspan=\"1\"><p>書き足したこと</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3-1(新設)</p></td><td colspan=\"1\" rowspan=\"1\"><p>ナレーションはハコブくんが話す。本人を見せる3つの決まり。概要欄に AI 音声と書く。ほかのチャンネルと同じ声を使わない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3-2</p></td><td colspan=\"1\" rowspan=\"1\"><p>仮の音声の速さを1.08倍から1.0倍に下げた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4-1</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの区画(左下・上半身・右向き)。図の範囲は140ピクセルのまま、場面ごとの自動の確認に任せる。ロゴの位置</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4-2・4-5</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの体と顔の色(目の白・口の水色・リングの緑)は、「オレンジと青以外の色を足さない」の対象外。影は付けない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4-6</p></td><td colspan=\"1\" rowspan=\"1\"><p>出典の左端を420ピクセルに移す</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4-7(新設)</p></td><td colspan=\"1\" rowspan=\"1\"><p>体の作り方(頭の上の直し方を含む)、顔の描き方、口・目・まばたき・リングの動きの数値、表情の付け方と使い分けの目安</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>字幕は1行26字まで、区画の右側の中央にそろえる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>台本の一人称「僕」を、本人の名前の三人称に言い換える。自己紹介の固定の文言をハコブくんが話す形にする</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>8</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの声、体の画像、本番のスクリプトへの組み込み、表情の使い分けを未決として追加</p></td></tr></tbody></table></div><p>表情は、目と頬を1文ごとに台本で指定し、口は話している間はどの表情でも音量で動かす、という決まりにしました。表情集の「_」の口のまま話すと、声と口が合わなくなるからです。使い分けの目安は次の表で、経営者向けの動画なので1本で使う表情は6つ前後にとどめます。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>表情</p></th><th colspan=\"1\" rowspan=\"1\"><p>使う場面</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>丸い目+開いた口</p></td><td colspan=\"1\" rowspan=\"1\"><p>基本。説明の文はほぼこれ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>丸い目+小さな笑み</p></td><td colspan=\"1\" rowspan=\"1\"><p>間、聞き手に戻るところ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>驚いた目+「o」の口</p></td><td colspan=\"1\" rowspan=\"1\"><p>動画の冒頭の問い、意外な数字が出るところ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ジト目</p></td><td colspan=\"1\" rowspan=\"1\"><p>「本当にそうでしょうか」と反論を出すところ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>目を閉じる</p></td><td colspan=\"1\" rowspan=\"1\"><p>「まとめると」、考えている間</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>困り眉</p></td><td colspan=\"1\" rowspan=\"1\"><p>失敗や限界の話</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>>▽<</p></td><td colspan=\"1\" rowspan=\"1\"><p>結論が言えたところ、うまくいった実験</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>「III_III」</p></td><td colspan=\"1\" rowspan=\"1\"><p>計算中・データを読み込んでいる場面</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ウインク、キラキラ目、照れ、>_<、目が泳ぐ</p></td><td colspan=\"1\" rowspan=\"1\"><p>アウトロや軽い一言だけ</p></td></tr></tbody></table></div><p>自己紹介の固定の文言は、もとの一人称の文をハコブくんが話す形に言い換え、「この番組は、本業のデザイナーをしながらロボティクスの研究と開発をしているエイイチが、自宅の机の上でロボットアームを動かした記録をもとにしています。」としました。この文言でよいかは、本人の確認待ちです。</p><p>ファイルの置き場所は次のとおりです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>ファイル</p></th><th colspan=\"1\" rowspan=\"1\"><p>置き場所</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>video_rules.md</p></td><td colspan=\"1\" rowspan=\"1\"><p>iCloud <code>PixelstoMotors/デジタルツイン学習R2R2R/動画/_制作ベース/video_rules.md</code> と、プロジェクトの文書 <code>claude/video_rules.md</code></p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>試作のスクリプトと入力の画像</p></td><td colspan=\"1\" rowspan=\"1\"><p>iCloud <code>動画/_制作ベース/hakobu_試作/</code>(make_demo.py、render_static.py、measure_face.py、cut_raw.png、full/g_05.png、元の生成動画)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>試作の動画(mp4)</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>/Volumes/KIOXIA/r2r2r-data/videos/hakobu_試作/</code></p></td></tr></tbody></table></div><p></p><h2 id=\"index_OMOijwC5\">まだ決まっていないのは、ハコブくんの声・体の画像・本番への組み込み・表情の4つ</h2><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>いまの状態</p></th><th colspan=\"1\" rowspan=\"1\"><p>決めること</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの声</p></td><td colspan=\"1\" rowspan=\"1\"><p>Open JTalk の仮の男声</p></td><td colspan=\"1\" rowspan=\"1\"><p>ElevenLabs の Voice Design で作るか、声優に頼んで作るか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>体の画像</p></td><td colspan=\"1\" rowspan=\"1\"><p>生成動画の1コマを切り抜き、頭の上とリングを直して使っている</p></td><td colspan=\"1\" rowspan=\"1\"><p>同じ構図で、無地の背景・リングなしの画像を生成し直すか。生成し直せば、頭の上とリングの直しが要らなくなる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>本番のスクリプトへの組み込み</p></td><td colspan=\"1\" rowspan=\"1\"><p>試作のスクリプトで、動きと配置を確かめた段階</p></td><td colspan=\"1\" rowspan=\"1\"><p>render.py と assemble.py に組み込む。台本(narration.py)に「表情」の欄を足す。表情集の顔をコードで描く。scenes.py のロゴの位置を直す(いまは試作の中で上書きしているだけ)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>表情の使い分け</p></td><td colspan=\"1\" rowspan=\"1\"><p>目安の表だけ</p></td><td colspan=\"1\" rowspan=\"1\"><p>1本作って、使う表情と場面を確定する</p></td></tr></tbody></table></div><p>試作は体を描いてから左右反転しているので、ウインクのように左右で形が違う表情は、このままだと左右が逆になります。本番では、体だけを先に反転し、顔は向きどおりに描く順番にします。</p><p>この4項目のうち3つは、この記事の作業のあとで決まりました。本番への組み込みは hakobu.py として済み、声は ElevenLabs の Voice Design を1か月試すことに、体の画像はいまのまま使うことになっています。字幕の位置も、そのあと画面の中央にそろえる形に変わりました。経緯は video_rules.md の更新履歴にあります。</p><p></p><h2 id=\"index_AtrH00k0\">試作スクリプトの全文</h2><p>使ったもの: Python 3.13.16、opencv-python 5.0.0、Pillow 12.3.0、numpy 2.5.3、pyopenjtalk 0.4.1、rembg 2.0.85(onnxruntime 1.29.0)、playwright 1.56.0(Chromium)、ffmpeg 6.1.1。フォントは Noto Sans CJK JP。</p><p>試作は Claude の作業環境(クラウドの Linux)で動かしました。Mac で動かすときは、<code>hakobu_試作/</code> を作業フォルダにして実行します。render_static.py は、1つ上の <code>_制作ベース/</code> にある本番の scenes.py と logo_dark.png を読み込みます。</p><h3 id=\"index_q54toAvd\">準備のコマンド(コマの取り出しと、体の切り抜き)</h3><p>元の生成動画は、共有されたときのファイル名が文字化けしていたため、<code>hakobu_試作/source_hakobu_hospital.mp4</code> という名前で保存し直しています。</p><pre><code class=\"language-bash\"># 動画の情報を見る\nffprobe -v error -show_entries format=duration:stream=codec_type,width,height,r_frame_rate -of compact source_hakobu_hospital.mp4\n\n# 1秒に4枚ずつ、40枚のコマを取り出す\nmkdir -p full\nffmpeg -v error -y -i source_hakobu_hospital.mp4 -vf \"fps=4\" full/g_%02d.png\n\n# 40枚に番号を振って並べる(コマを選ぶため)\nffmpeg -v error -y -i full/g_%02d.png -vf \"scale=212:-1,drawtext=text='%{n}':x=5:y=5:fontsize=24:fontcolor=red,tile=10x4\" sheet.png\n\n# rembg と、関連するライブラリを入れる\npip install --break-system-packages pyopenjtalk rembg onnxruntime\n</code></pre><pre><code class=\"language-python\"># 5枚目のコマから背景を消す(cut_raw.png)\nfrom rembg import remove, new_session\nfrom PIL import Image\nim = Image.open('full/g_05.png').convert('RGB')\nout = remove(im, session=new_session('isnet-general-use'))\nout.save('cut_raw.png'); print(out.size, out.getbbox())\n</code></pre><h3 id=\"index_TKcoDqjv\">measure_face.py(ディスプレイ・目・口・リングを測る)</h3><pre><code class=\"language-python\">\"\"\"ハコブくんの顔のディスプレイを測る(make_demo.py に書いた座標の出どころ)\n入力: cut_raw.png(元の動画の5コマ目を rembg の isnet-general-use で切り抜いた画像)\n出力: ディスプレイ・目・口・リング・体の範囲、口の色、ディスプレイの四隅\n\"\"\"\nimport numpy as np, cv2\nfrom PIL import Image\n\nraw = np.array(Image.open(\"cut_raw.png\").convert(\"RGBA\"))\nrgb = raw[..., :3].astype(int)\na = raw[..., 3]\nlum = rgb.mean(2)\n\n# 1. ディスプレイ(黒い画面):顔のあたりで一番大きい暗い領域\ny0, y1, x0, x1 = 330, 520, 300, 530\ndark = np.zeros(lum.shape, np.uint8)\ndark[y0:y1, x0:x1] = (lum[y0:y1, x0:x1] < 75)\nn, lab, st, _ = cv2.connectedComponentsWithStats(dark)\nk = 1 + np.argmax(st[1:, 4])\nscr = (lab == k).astype(np.uint8)\nprint(\"ディスプレイの外接矩形 (x, y, 幅, 高さ):\", st[k][:4].tolist())\n\n# 2. 元の目(白)と口(水色)\nreg = rgb[y0:y1, x0:x1]\nrl = reg.mean(2)\nwhite = (rl > 200) & (reg.std(2) < 25)\nblue = (reg[..., 2] > 180) & (reg[..., 0] < 150)\nfor name, m in [(\"白\", white), (\"水色\", blue)]:\n n2, lab2, st2, _ = cv2.connectedComponentsWithStats(m.astype(np.uint8))\n for j in range(1, n2):\n if st2[j, 4] > 80:\n x, y, w, h, area = st2[j].tolist()\n print(f\"{name}の領域: x={x + x0} y={y + y0} 幅={w} 高さ={h} 面積={area}\")\nprint(\"口の色(中央値 RGB):\", np.median(reg[blue], 0).astype(int).tolist())\n\n# 3. 側面の緑のリングと、体全体の範囲\nring = (rgb[..., 1] > 150) & (rgb[..., 1] - rgb[..., 0] > 40) & (rgb[..., 1] - rgb[..., 2] > 20)\nys, xs = np.where(ring)\nprint(\"リングの範囲:\", xs.min(), ys.min(), xs.max(), ys.max(), \"画素数\", len(xs))\nys, xs = np.where(a > 128)\nprint(\"体の範囲:\", xs.min(), ys.min(), xs.max(), ys.max())\n\n# 4. 頭の上の切り抜きの濃さ(行ごとに、アルファが一定以上の画素の数)\nprint(\"頭の上の行ごとの画素数(アルファ>200 / >60 / >10):\")\nfor y in range(320, 360, 4):\n row = a[y, 300:650]\n print(f\" y={y}: {(row > 200).sum()} / {(row > 60).sum()} / {(row > 10).sum()}\")\n\n# 5. ディスプレイの四隅\n# 角が丸いので角の点は直接取らない。上下左右の辺の、まっすぐな中央4割の点に直線を当てはめ、交点を角にする\nc = max(cv2.findContours(scr, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[0], key=cv2.contourArea)\nc = c.reshape(-1, 2).astype(np.float32)\nbx, by, bw, bh = cv2.boundingRect(c.astype(np.int32))\ncx, cy = bx + bw / 2, by + bh / 2\nsides = {\n \"上\": c[(c[:, 1] < cy - bh * 0.3) & (np.abs(c[:, 0] - cx) < bw * 0.2)],\n \"下\": c[(c[:, 1] > cy + bh * 0.3) & (np.abs(c[:, 0] - cx) < bw * 0.2)],\n \"左\": c[(c[:, 0] < cx - bw * 0.3) & (np.abs(c[:, 1] - cy) < bh * 0.2)],\n \"右\": c[(c[:, 0] > cx + bw * 0.3) & (np.abs(c[:, 1] - cy) < bh * 0.2)],\n}\nlines = {}\nfor nm, p in sides.items():\n vx, vy, px, py = cv2.fitLine(p, cv2.DIST_L2, 0, 0.01, 0.01).ravel()\n lines[nm] = (np.array([px, py]), np.array([vx, vy]))\n ang = np.degrees(np.arctan2(vy, vx))\n if nm in (\"左\", \"右\"):\n ang = 90 - abs(ang) # 縦の辺は、垂直からの傾き\n print(f\"{nm}の辺: 点 {len(p)} 個、傾き {ang:.2f}°\")\n\n\ndef cross(s1, s2):\n (p1, d1), (p2, d2) = lines[s1], lines[s2]\n t = np.linalg.solve(np.array([d1, -d2]).T, p2 - p1)\n return p1 + t[0] * d1\n\n\nquad = [cross(\"上\", \"左\"), cross(\"上\", \"右\"), cross(\"下\", \"右\"), cross(\"下\", \"左\")]\nprint(\"ディスプレイの四隅(左上・右上・右下・左下):\", [[round(float(v), 1) for v in q] for q in quad])\n\n# 参考: 角の点をそのまま取った場合(最初に使って、傾きを読み違えた方法)\nhull = cv2.convexHull(c.astype(np.int32))\nfor eps in np.linspace(2, 30, 57):\n ap = cv2.approxPolyDP(hull, eps, True)\n if len(ap) == 4:\n print(f\"参考・角の点を直接取った四隅(eps={eps}):\", ap.reshape(-1, 2).tolist())\n break\n</code></pre><h3 id=\"index_nK_87CBf\">make_demo.py(体の加工・顔の描画・口と目とリングの動き・重なりの確認・書き出し)</h3><pre><code class=\"language-python\">\"\"\"ハコブくん ナレーション試作(13秒ほど)\n使い方: python3 make_demo.py upper (上半身・基本) / python3 make_demo.py full (全身)\n python3 make_demo.py upper --dump (口・目・まばたき・リングの時間の値だけを timing.json に書き出して終わる)\n入力: cut_raw.png(元の動画の5コマ目を rembg で切り抜いた画像)、full/g_05.png(元の動画の5コマ目そのもの)\n必要なもの: pyopenjtalk(仮音声)、playwright と Chromium(図と字幕)、opencv-python、numpy、Pillow、ffmpeg\n- 動画の1コマから切り抜いた体に、画面の顔をコードで描く\n- 仮の音声(Open JTalk)の音量で口を4段階に切り替え、目・まばたき・側面のリングを動かす\n- ハコブくんの区画に文字が重なっていないかを自動で確認する\n\"\"\"\nimport subprocess, sys\nimport numpy as np, cv2, pyopenjtalk\nfrom PIL import Image, ImageDraw, ImageFont, ImageEnhance\n\nW, H, FPS = 1920, 1080, 30\nSR = 48000\nBG = (245, 245, 240)\nINK = (13, 13, 13)\nGRAY = (136, 136, 136)\nORANGE = (196, 71, 31)\nFONT_JP = \"/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc\"\nFONT_BLACK = \"/usr/share/fonts/opentype/noto/NotoSansCJK-Black.ttc\"\nFONT_REG = \"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc\"\n\n# ハコブくんの区画(ここには何も重ねない)\nZONE = (0, 600, 380, 1080)\nSUB_X0, SUB_X1 = 400, 1780\nFIG_X0 = 420\n\n\ndef font(path, size):\n return ImageFont.truetype(path, size, index=0) # index 0 = JP\n\n\n# ---------- 1. 体の切り抜きを整える ----------\nraw = np.array(Image.open(\"cut_raw.png\").convert(\"RGBA\"))\nalpha = raw[..., 3].copy()\nalpha[alpha < 60] = 0\nn, lab, st, _ = cv2.connectedComponentsWithStats((alpha > 0).astype(np.uint8))\nkeep = 1 + np.argmax(st[1:, 4])\nalpha[lab != keep] = 0\n# 頭の上:白い頭が白い枕と重なっていて、切り抜きが薄く欠けている。\n# 元の動画のコマを見ると、頭の上の輪郭は「画面の上の手前側の縁(低い)→ 奥の角で持ち上がる → 奥の縁(高い)→ 右の角で丸く下がる」形。\n# 凸包で埋めるとこの段差が直線になり、枕まで頭に含めてしまう(汚れの原因)。\n# そこで、切り抜きがはっきり頭と判断した範囲(アルファ60以上)をそのまま輪郭に使い、ギザギザだけをなめらかにする。\na_soft = np.array(Image.open(\"cut_raw.png\"))[..., 3]\nHEAD_Y1 = 425 # 肩の段差より上だけを対象にする\nhead = (a_soft > 60).astype(np.uint8)\nhead[HEAD_Y1:] = 0\nhead[:, :300] = 0\nn_h, lab_h, st_h, _ = cv2.connectedComponentsWithStats(head)\nhead = (lab_h == 1 + np.argmax(st_h[1:, 4])).astype(np.uint8)\ncnts, _ = cv2.findContours(head, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)\nhead = np.zeros_like(head)\ncv2.drawContours(head, cnts, -1, 1, -1) # 穴を埋める\n# 4倍に拡大 → ぼかして0.5で切り直す(ギザギザを曲線に) → 小さく角を丸める → 元の大きさへ\n_S = 4\n_big = cv2.resize(head.astype(np.float32) * 255, None, fx=_S, fy=_S, interpolation=cv2.INTER_LINEAR)\n_pad = 20 * _S\n_bigp = cv2.copyMakeBorder(_big, 0, _pad, 0, 0, cv2.BORDER_REPLICATE) # 下の切れ目が丸まらないように下へ延ばす\n_bigp = cv2.GaussianBlur(_bigp, (0, 0), 2.5 * _S)\n_bigp = ((_bigp > 127) * 255).astype(np.uint8)\n_k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * 4 * _S + 1, 2 * 4 * _S + 1))\n_bigp = cv2.morphologyEx(_bigp, cv2.MORPH_OPEN, _k)[: _big.shape[0]].astype(np.float32)\nhull_soft = cv2.resize(_bigp, (head.shape[1], head.shape[0]), interpolation=cv2.INTER_AREA) # 縁は自然な中間値\nhull_mask = (hull_soft > 127).astype(np.uint8)\nhull_soft[HEAD_Y1 - 3:] = 0\nalpha = np.maximum(alpha, hull_soft.astype(np.uint8))\nalpha[: HEAD_Y1 - 20] = hull_soft[: HEAD_Y1 - 20].astype(np.uint8) # 頭の上はこの輪郭だけで描く\n# 輪郭の内側は頭そのものなので、色は元のコマの画素をそのまま使う(切り抜きで暗くなった部分を戻す)\norig_rgb = np.array(Image.open(\"full/g_05.png\").convert(\"RGB\"))\nrestore = (hull_soft > 0) & (a_soft < 245) # 縁の半透明の画素も含める(暗い点線が出ないように)\nrestore[HEAD_Y1:] = False\nraw[restore, :3] = orig_rgb[restore]\nraw[..., 3] = alpha\n\n# 画面(黒いディスプレイ)の領域を見つけて、元の目と口を消す\nrgb = raw[..., :3].astype(int)\nlum = rgb.mean(2)\ny0, y1, x0, x1 = 330, 520, 300, 530\ndark = np.zeros(lum.shape, np.uint8)\ndark[y0:y1, x0:x1] = (lum[y0:y1, x0:x1] < 75)\nn, lab, st, _ = cv2.connectedComponentsWithStats(dark)\nk = 1 + np.argmax(st[1:, 4])\nscr = (lab == k).astype(np.uint8)\ncnts, _ = cv2.findContours(scr, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)\nscreen_mask = np.zeros_like(scr)\ncv2.drawContours(screen_mask, cnts, -1, 1, -1)\nscreen_mask = cv2.erode(screen_mask, np.ones((3, 3), np.uint8), 1)\nSCREEN_COLOR = (19, 19, 17)\nraw[screen_mask == 1, :3] = SCREEN_COLOR\n\n# 横の緑のリング\n# 元のリングは画素が粗いので、一度消して、楕円をコードで描き直す\ng = raw[..., :3].astype(int)\nring_core = (g[..., 1] > 150) & (g[..., 1] - g[..., 0] > 40) & (g[..., 1] - g[..., 2] > 20)\nring_core &= ~(screen_mask.astype(bool))\nys, xs = np.where(ring_core)\nRING = cv2.fitEllipse(np.stack([xs, ys], 1).astype(np.float32)) # ((cx,cy),(長径,短径),角度)\n(rcx, rcy), (rw, rh), rang = RING\nperim = np.pi * (3 * (rw + rh) / 2 - np.sqrt((3 * rw / 2 + rh / 2) * (rw / 2 + 3 * rh / 2)))\nRING_T = max(3.5, min(7.0, ring_core.sum() / perim)) # リングの太さ(px)\n# 緑がにじんでいる範囲ごと塗り直して消す\nhalo = ((g[..., 1] - g[..., 0] > 15) & (g[..., 1] - g[..., 2] > 5)) | ring_core\nhalo = cv2.dilate(halo.astype(np.uint8), np.ones((7, 7), np.uint8), 1)\nhalo[: int(rcy - rh / 2 - 15)] = 0\nhalo[int(rcy + rh / 2 + 15):] = 0\nhalo[:, : int(rcx - rw / 2 - 15)] = 0\nhalo[:, int(rcx + rw / 2 + 15):] = 0\nbgr = cv2.cvtColor(raw[..., :3].copy(), cv2.COLOR_RGB2BGR)\nbgr = cv2.inpaint(bgr, halo, 7, cv2.INPAINT_TELEA)\nraw[..., :3] = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)\n\n\ndef draw_ring(img, on):\n \"\"\"リングを4倍の解像度で描き、にじむ光を重ねる\"\"\"\n m = 30\n bx0, by0 = int(rcx - rw / 2 - m), int(rcy - rh / 2 - m)\n bw, bh = int(rw + 2 * m), int(rh + 2 * m)\n big = np.zeros((bh * SS, bw * SS), np.uint8)\n cv2.ellipse(big, ((int((rcx - bx0) * SS), int((rcy - by0) * SS)),\n (int(rw * SS), int(rh * SS)), rang),\n 255, int(RING_T * SS), cv2.LINE_AA)\n core = cv2.resize(big, (bw, bh), interpolation=cv2.INTER_AREA).astype(np.float32) / 255\n on = float(on) # 0(話していない)〜1(話している)\n glow = cv2.GaussianBlur(core, (0, 0), 3.5 + 1.5 * on)\n glow = np.clip(glow * (1.4 + 0.8 * on), 0, 1)\n region = np.array(img.crop((bx0, by0, bx0 + bw, by0 + bh))).astype(np.float32)\n a = region[..., 3:4] / 255\n rgb = region[..., :3]\n glow_col = np.array([70, 225, 130], np.float32)\n core_col = np.array([160, 240, 185], np.float32) * (1 - on) + np.array([215, 255, 228], np.float32) * on\n gk = (glow * (0.45 + 0.3 * on))[..., None] * a # 体の外には光を出さない\n rgb = rgb * (1 - gk) + glow_col * gk\n ck = core[..., None] * a\n rgb = rgb * (1 - ck) + core_col * ck\n region[..., :3] = rgb\n img.paste(Image.fromarray(region.astype(np.uint8)), (bx0, by0))\n\n\nbase_body = Image.fromarray(raw)\nbody_bbox = base_body.getbbox()\n\n# ---------- 2. 顔を描く(元の位置を実測した座標) ----------\nEYE_L = (362, 395, 384, 441)\nEYE_R = (446, 396, 469, 444)\nMOUTH = (378, 450, 446, 490)\nEYE_SHIFT = 0.5 # 目の上下位置(元画像の px。+で下)。顔全体を1.5px上げた\nMOUTH_SHIFT = -3.0 # 口の上下位置(目と口の間は 3.5px 詰めたまま)\nBLUE = (87, 181, 248) # 口の水色(元の動画の色 #57B5F8)\nWHITE = (246, 248, 252) # 目の白\nSS = 4 # 4倍で描いて縮める(縁をなめらかにする)\n\n\nFX0, FY0, FX1, FY1 = 320, 370, 520, 500 # 顔を描く範囲(元画像の座標)\n\n\ndef _pts(xs, ys):\n return np.stack([(np.asarray(xs) - FX0) * SS, (np.asarray(ys) - FY0) * SS], 1).astype(np.int32)\n\n\ndef mouth_mask(kind):\n \"\"\"口の形を4倍の解像度で作り、角を丸めて返す(0/255 の配列)\"\"\"\n w, h = (FX1 - FX0) * SS, (FY1 - FY0) * SS\n m = np.zeros((h, w), np.uint8)\n cx = (MOUTH[0] + MOUTH[2]) / 2\n top = MOUTH[1] + 2 + MOUTH_SHIFT\n t = np.linspace(0, 1, 60)\n if kind in (\"mid\", \"large\"):\n # 三日月形:上の辺はゆるく下がり、下の辺は深い弧\n hw, depth, sag, rr = (34, 38, 7, 6) if kind == \"large\" else (27, 25, 5, 5)\n xs = cx - hw + 2 * hw * t\n y_top = top + sag * (1 - (2 * t - 1) ** 2)\n y_bot = top + depth * np.sqrt(np.clip(1 - (2 * t - 1) ** 2, 0, 1))\n poly = np.concatenate([_pts(xs, y_top), _pts(xs[::-1], y_bot[::-1])])\n cv2.fillPoly(m, [poly], 255)\n elif kind == \"small\":\n # 小さく開いた口:上が少し広く、下が丸い\n hw, rr = 14, 6\n xs = cx - hw + 2 * hw * t\n y_top = top + 4 + 2 * (1 - (2 * t - 1) ** 2)\n ang = np.linspace(0, np.pi, 60)\n bx = cx + hw * np.cos(ang)\n by = top + 12 + 13 * np.sin(ang)\n poly = np.concatenate([_pts(xs, y_top), _pts(bx, by)])\n cv2.fillPoly(m, [poly], 255)\n elif kind == \"closed\":\n # 閉じた笑顔:太さのある弧、両端は丸い\n hw, sag, thick = 25, 9, 7\n for tt in np.linspace(0, 1, 200):\n x = cx - hw + 2 * hw * tt\n y = top + 7 + sag * (1 - (2 * tt - 1) ** 2)\n p = _pts([x], [y])[0]\n cv2.circle(m, (int(p[0]), int(p[1])), int(thick / 2 * SS), 255, -1)\n return m\n k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * rr * SS + 1, 2 * rr * SS + 1))\n return cv2.morphologyEx(m, cv2.MORPH_OPEN, k) # 角を丸める\n\n\nMOUTH_CACHE = {}\n\n# ディスプレイの四隅:画面の上下左右の辺のまっすぐな部分(角の丸みを除いた中央4割)に直線を当てはめ、\n# その交点を角とした(角の丸みに引っぱられないように)。上の辺はほぼ水平(0.2°)、左右の辺は約6°傾いている\nSCREEN_QUAD = np.float32([[336.8, 348.8], [510.2, 349.3], [495.1, 501.2], [321.5, 494.6]]) # 左上・右上・右下・左下\n_c = SCREEN_QUAD.mean(0)\n_w = (np.linalg.norm(SCREEN_QUAD[1] - SCREEN_QUAD[0]) + np.linalg.norm(SCREEN_QUAD[2] - SCREEN_QUAD[3])) / 2\n_h = (np.linalg.norm(SCREEN_QUAD[3] - SCREEN_QUAD[0]) + np.linalg.norm(SCREEN_QUAD[2] - SCREEN_QUAD[1])) / 2\nFLAT_RECT = np.float32([[_c[0] - _w / 2, _c[1] - _h / 2], [_c[0] + _w / 2, _c[1] - _h / 2],\n [_c[0] + _w / 2, _c[1] + _h / 2], [_c[0] - _w / 2, _c[1] + _h / 2]])\nTILT = 1.0 # 1.0 = ディスプレイと同じだけ傾ける。0.5 なら半分\n_quad = FLAT_RECT + TILT * (SCREEN_QUAD - FLAT_RECT)\n_to_layer = lambda p: (p - np.float32([FX0, FY0])) * SS\nH_FACE = cv2.getPerspectiveTransform(_to_layer(FLAT_RECT), _to_layer(_quad))\n\n\ndef warp_to_screen(la):\n h, w = la.shape[:2]\n a = la[..., 3:4].astype(np.float32) / 255\n pre = np.concatenate([la[..., :3].astype(np.float32) * a, a * 255], 2) # 縁が黒ずまないよう乗算済みで変形\n out = cv2.warpPerspective(pre, H_FACE, (w, h), flags=cv2.INTER_LINEAR)\n aa = out[..., 3:4] / 255\n rgb = np.where(aa > 1e-4, out[..., :3] / np.maximum(aa, 1e-4), 0)\n return np.concatenate([np.clip(rgb, 0, 255), out[..., 3:4]], 2).astype(np.uint8)\n\n\ndef draw_face(img, mouth, eyes_closed, eye_dy=0.0):\n fx0, fy0, fx1, fy1 = FX0, FY0, FX1, FY1\n layer = Image.new(\"RGBA\", ((fx1 - fx0) * SS, (fy1 - fy0) * SS), (0, 0, 0, 0))\n d = ImageDraw.Draw(layer)\n\n def r(b):\n return [(b[0] - fx0) * SS, (b[1] - fy0) * SS, (b[2] - fx0) * SS, (b[3] - fy0) * SS]\n\n for e in (EYE_L, EYE_R):\n e = (e[0], e[1] + eye_dy + EYE_SHIFT, e[2], e[3] + eye_dy + EYE_SHIFT) # 目だけ少し上下させる\n if eyes_closed:\n cy = (e[1] + e[3]) / 2\n d.rounded_rectangle(r((e[0] - 1, cy - 2.5, e[2] + 1, cy + 2.5)), radius=2.5 * SS, fill=WHITE)\n else:\n # 縦長のカプセル形(上下の端が半円)\n w = e[2] - e[0]\n d.rounded_rectangle(r(e), radius=w / 2 * SS, fill=WHITE)\n if mouth not in MOUTH_CACHE:\n MOUTH_CACHE[mouth] = mouth_mask(mouth)\n la = np.array(layer)\n mm = MOUTH_CACHE[mouth] > 0\n la[mm, :3] = BLUE\n la[mm, 3] = 255\n # 顔ごとディスプレイの傾きに合わせて斜めにする(まっすぐ描いた顔を、画面の四隅へ射影する)\n la = warp_to_screen(la)\n layer = Image.fromarray(la)\n layer = layer.resize((fx1 - fx0, fy1 - fy0), Image.LANCZOS)\n # 画面の外にはみ出さないよう、画面の形で切る\n m = Image.fromarray((screen_mask[fy0:fy1, fx0:fx1] * 255).astype(np.uint8))\n la = np.array(layer)\n inside = (np.array(m) > 0)\n la[..., 3] = (la[..., 3].astype(int) * inside).astype(np.uint8)\n # 目のまわりのにじむ光(画面の中だけ)\n eye_a = la[..., 3].astype(np.float32) / 255 * (la[..., 0] > 200)\n glow = cv2.GaussianBlur(eye_a, (0, 0), 2.6) * 0.75 * inside\n glow_layer = np.zeros_like(la)\n glow_layer[..., :3] = (205, 225, 255)\n glow_layer[..., 3] = np.clip(glow * 255, 0, 255).astype(np.uint8)\n img.alpha_composite(Image.fromarray(glow_layer), (fx0, fy0))\n img.alpha_composite(Image.fromarray(la), (fx0, fy0))\n\n\nMODE = sys.argv[1] if len(sys.argv) > 1 else \"upper\" # upper=上半身(基本) / full=全身\nif MODE == \"full\":\n CROP = body_bbox\n ROBOT_H = 460\n scale = ROBOT_H / (CROP[3] - CROP[1])\n ROBOT_W = round((CROP[2] - CROP[0]) * scale)\n ROBOT_Y = ZONE[3] - 20 - ROBOT_H\nelse:\n # 上半身:引き出しの取っ手の下あたりで切り、画面の下端に接して置く\n CROP = (body_bbox[0], body_bbox[1], body_bbox[2], 660)\n ROBOT_W = 340\n scale = ROBOT_W / (CROP[2] - CROP[0])\n ROBOT_H = round((CROP[3] - CROP[1]) * scale)\n ROBOT_Y = H - ROBOT_H\nROBOT_X = (ZONE[0] + ZONE[2]) // 2 - ROBOT_W // 2\n# ハコブくんの区画は、実際に置いた体の範囲+余白20pxで決める(上半身版は全身版より低い位置から始まる)\nZONE = (0, ROBOT_Y - 20, ROBOT_X + ROBOT_W + 20, H)\ncache = {}\n\n\ndef robot(mouth, eyes_closed, ring_on, eye_dy=0.0):\n key = (mouth, eyes_closed, ring_on, eye_dy)\n if key not in cache:\n img = base_body.copy()\n draw_ring(img, ring_on)\n draw_face(img, mouth, eyes_closed, eye_dy)\n img = img.crop(CROP).resize((ROBOT_W, ROBOT_H), Image.LANCZOS)\n img = img.transpose(Image.FLIP_LEFT_RIGHT) # 右向き(画面の中央を向く)\n cache[key] = img\n return cache[key]\n\n\n# ---------- 3. 仮の音声 ----------\nSCRIPT = [\n # (字幕, 読み上げ用, 後ろの間[秒], 間の顔)\n (\"ハコブくんです。\", \"ハコブくんです。\", 0.35, \"closed\"),\n (\"高いGPUを、買う必要はあるのでしょうか。\",\n \"たかいジーピーユーを、買う必要は、あるのでしょうか。\", 0.7, \"o\"),\n (\"エイイチが借りて測ると、\\n使ったメモリは約3割でした。\",\n \"エイイチが借りて測ると、使ったメモリは、約3割でした。\", 0.6, \"closed\"),\n]\n\naudio = []\ntimeline = [] # (開始フレーム, 終了フレーム, 字幕, 間の顔, 発話終了フレーム)\nt = 0\nfor sub, read, gap, gap_face in SCRIPT:\n x, sr = pyopenjtalk.tts(read, speed=1.0, half_tone=-1.0) # 1.08 → 1.0 に落とした\n assert sr == SR\n x = x / 32768.0\n seg = np.concatenate([x, np.zeros(int(gap * SR))])\n f_start = round(t * FPS)\n f_speech_end = round((t + len(x) / SR) * FPS)\n t += len(seg) / SR\n f_end = round(t * FPS)\n audio.append(seg)\n timeline.append((f_start, f_end, sub, gap_face, f_speech_end))\naudio = np.concatenate([np.zeros(int(0.3 * SR)), np.concatenate(audio)])\nlead = round(0.3 * FPS)\ntimeline = [(a + lead, b + lead, s, gf, e + lead) for a, b, s, gf, e in timeline]\ntotal_frames = timeline[-1][1] + round(0.3 * FPS)\naudio = np.concatenate([audio, np.zeros(max(0, int(total_frames / FPS * SR) - len(audio)))])\npeak = np.abs(audio).max()\naudio = audio / peak * 0.8\nimport wave\nwith wave.open(\"narration.wav\", \"wb\") as wf:\n wf.setnchannels(1); wf.setsampwidth(2); wf.setframerate(SR)\n wf.writeframes((audio * 32767).astype(np.int16).tobytes())\n\n# 音量 → 口の4段階(閉じる/小/中/大)\nMOUTHS = [\"closed\", \"small\", \"mid\", \"large\"]\nhop = SR // FPS\nrms = np.array([np.sqrt(np.mean(audio[i * hop:(i + 1) * hop] ** 2)) if i * hop < len(audio) else 0\n for i in range(total_frames)])\nref = np.percentile(rms[rms > 0.01], 90)\nlvl = rms / ref\nlvl_s = np.convolve(lvl, np.ones(3) / 3, mode=\"same\") # 音量を少しならす\ntarget = np.digitize(lvl_s, [0.15, 0.35, 0.65]) # 0..3\n# ちらつき防止: 1フレームだけの変化は前に合わせる\nfor i in range(1, total_frames - 1):\n if target[i] != target[i - 1] and target[i] != target[i + 1]:\n target[i] = target[i - 1]\n# 1フレームに1段階ずつしか変えない → 閉じる→大 の間に必ず小・中が入る\nstep = np.zeros(total_frames, int)\nMOUTH_HOLD = 2 # 口は2フレームごと(毎秒15回)にだけ変える\nfor i in range(total_frames):\n prev = step[i - 1] if i else 0\n if i % MOUTH_HOLD == 0:\n step[i] = prev + int(np.clip(target[i] - prev, -1, 1))\n else:\n step[i] = prev\nmouth_seq = [MOUTHS[s] for s in step]\n# 間の顔(問いかけのあとは小さく開いた口のまま止める)\nfor a, b, s, gf, e in timeline:\n for i in range(e + 2, b):\n if gf == \"o\":\n mouth_seq[i] = \"small\"\n# 目の上下:ゆっくりした揺れ + 口が開くほど少し上がる(元画像の px。0.25px 刻みにまとめる)\ntt = np.arange(total_frames) / FPS\nrng = np.random.default_rng(7)\nstarts = [a for a, b, s, gf, e in timeline]\nends = [e for a, b, s, gf, e in timeline]\n\n# ---- 目:ゆっくりした揺れ + 口に少し遅れて上がる + 文の頭で小さく跳ねる ----\nlift = np.convolve(step / 3.0, np.ones(11) / 11, mode=\"same\")\nlift = np.concatenate([np.zeros(3), lift[:-3]]) # 口より3フレーム(0.1秒)遅れて動く\nhop = np.zeros(total_frames)\nfor a in starts: # 文の頭:目がぴょこっと上がって戻る(0.3秒)\n n = np.arange(9)\n hop[a:a + 9] += np.sin(np.pi * n / 8)[: len(hop[a:a + 9])]\neye_dy = 1.6 * np.sin(2 * np.pi * tt / 2.8) - 2.6 * lift - 2.0 * hop\neye_dy = np.round(eye_dy * 4) / 4\n\n# ---- まばたき:間隔をばらつかせ、文の終わりの直後にも入れる ----\nblink = np.zeros(total_frames, bool)\nt_b = 1.2\nwhile t_b * FPS < total_frames:\n blink[int(t_b * FPS):int(t_b * FPS) + 4] = True\n t_b += rng.uniform(2.0, 4.5)\nfor e in ends:\n if not blink[max(0, e - FPS):e + 8].any():\n blink[e + 4:e + 8] = True\n\n# ---- 側面の光:顔とは別の周期で息をするように明滅し、声には少し遅れて追いかける ----\n# 息をするような明滅。強さそのものもゆっくり不規則に変わる\nnoise = np.convolve(rng.normal(0, 1, total_frames + 90), np.ones(90) / 90, mode=\"valid\")[:total_frames]\nnoise = (noise - noise.min()) / (noise.max() - noise.min() + 1e-9) # 0〜1 のゆっくりした揺らぎ\nbreath = (0.5 + 0.5 * np.sin(2 * np.pi * tt / 3.7 + 1.3)) * (0.4 + 0.9 * noise) # 目の揺れ(2.8秒)と周期をずらす\ntalk = (lvl_s > 0.15).astype(float)\nenv = np.zeros(total_frames)\nfor i in range(1, total_frames): # 立ち上がりは速く、消えるのはゆっくり\n k = 0.30 if talk[i] > env[i - 1] else 0.05\n env[i] = env[i - 1] + k * (talk[i] - env[i - 1])\nenv = np.concatenate([np.zeros(6), env[:-6]]) # 声より0.2秒遅れて光る\ngain = np.full(total_frames, 0.5)\nfor a, b, s_, gf, e in timeline: # 文ごとに、声への反応の強さを変える(強い文・弱い文が混ざる)\n gain[a:b] = rng.uniform(0.3, 0.75)\ngain = np.convolve(gain, np.ones(15) / 15, mode=\"same\")\npulse = np.zeros(total_frames)\n\n\ndef add_pulse(i, amp, tau):\n n = np.arange(min(int(tau * 3), total_frames - i))\n pulse[i:i + len(n)] = np.maximum(pulse[i:i + len(n)], amp * np.exp(-n / tau))\n\n\nlast = -999\nfor i in range(total_frames):\n # 文の頭と、大きく口を開いた瞬間:顔とそろえて光る。強さは毎回ばらつき、ときどき光らない\n if i in starts:\n add_pulse(i, rng.uniform(0.25, 0.45), 5); last = i\n elif step[i] == 3 and i - last > 15:\n if rng.random() < 0.7:\n add_pulse(i, rng.uniform(0.08, 0.4), rng.uniform(3, 7))\n last = i\n# 話していないときも、ときどき弱くまたたく\nt_tw = rng.uniform(1.0, 2.5)\nwhile t_tw * FPS < total_frames:\n add_pulse(int(t_tw * FPS), rng.uniform(0.06, 0.18), rng.uniform(4, 9))\n t_tw += rng.uniform(1.5, 4.0)\nring_lv = np.clip(0.10 + 0.2 * breath + gain * env + pulse, 0, 1)\nring_lv = np.round(ring_lv * 20) / 20\n\nif \"--dump\" in sys.argv: # 図解用に、時間の値だけを書き出して終わる\n import json\n json.dump({\"fps\": FPS, \"frames\": int(total_frames),\n \"voice\": np.round(np.clip(lvl_s, 0, 1.5), 3).tolist(),\n \"mouth\": [int(v) for v in step], \"eye_dy\": eye_dy.tolist(),\n \"blink\": [int(b) for b in blink], \"ring\": ring_lv.tolist(),\n \"sentences\": [[int(a), int(e)] for a, b, s, gf, e in timeline]},\n open(\"timing.json\", \"w\"))\n print(\"timing.json に書き出しました\")\n sys.exit(0)\n\n# ---------- 4. 画面の文字と図(本番と同じ HTML/CSS で描く) ----------\nimport json\nsubprocess.run([sys.executable, \"render_static.py\", json.dumps([s for a, b, s, gf, e in timeline], ensure_ascii=False)], check=True)\nstatic = Image.open(\"png/static.png\").convert(\"RGB\")\nsub_layers = {s: Image.open(f\"png/sub_{k}.png\").convert(\"RGBA\") for k, (a, b, s, gf, e) in enumerate(timeline)}\nboxes = json.load(open(\"png/boxes.json\"))\n\n# ---------- 5. 重なりの自動確認 ----------\ndef overlap(b1, b2):\n return not (b1[2] <= b2[0] or b2[2] <= b1[0] or b1[3] <= b2[1] or b2[3] <= b1[1])\n\nbad = [(nm, bb) for nm, bb in boxes if overlap(bb, ZONE)]\nprint(\"ハコブくんの区画:\", ZONE, \" 体の位置:\", (ROBOT_X, ROBOT_Y, ROBOT_X + ROBOT_W, ROBOT_Y + ROBOT_H))\nif bad:\n for nm, bb in bad:\n print(\" 重なり:\", nm, bb)\n sys.exit(\"区画に重なる要素があるため中止しました\")\nprint(f\"重なり確認: 文字・図・ロゴ {len(boxes)} 個、区画との重なり 0 件\")\n\n# ---------- 6. 書き出し ----------\ncmd = [\"ffmpeg\", \"-v\", \"error\", \"-y\", \"-f\", \"rawvideo\", \"-pix_fmt\", \"rgb24\", \"-s\", f\"{W}x{H}\",\n \"-r\", str(FPS), \"-i\", \"-\", \"-i\", \"narration.wav\", \"-c:v\", \"libx264\", \"-crf\", \"20\",\n \"-pix_fmt\", \"yuv420p\", \"-c:a\", \"aac\", \"-b:a\", \"192k\", \"-shortest\", f\"hakobu_demo_{MODE}.mp4\"]\np = subprocess.Popen(cmd, stdin=subprocess.PIPE)\ncur_sub = None\nfor i in range(total_frames):\n frame = static.copy().convert(\"RGBA\")\n for a, b, s, gf, e in timeline:\n if a <= i < b:\n frame.alpha_composite(sub_layers[s])\n frame.alpha_composite(robot(mouth_seq[i], bool(blink[i]), float(ring_lv[i]), float(eye_dy[i])), (ROBOT_X, ROBOT_Y))\n if i == round(total_frames * 0.42):\n frame.convert(\"RGB\").save(f\"hakobu_demo_{MODE}_still.png\")\n p.stdin.write(frame.convert(\"RGB\").tobytes())\np.stdin.close(); p.wait()\nprint(\"frames\", total_frames, \"seconds\", round(total_frames / FPS, 2))</code></pre><h3 id=\"index_BgCE00BU\">render_static.py(図と字幕を本番の scenes.py で描き、要素の位置を測る)</h3><pre><code class=\"language-python\">\"\"\"図と字幕を、本番(_制作ベース の scenes.py / render.py)と同じ HTML/CSS で描く。\nハコブくんを左下に置くために本番から変えた点:\n - 図の範囲(.wrap)は本番と同じ左端 140px のまま。ハコブくんの区画に重なる場面は、書き出し前の自動確認で止める\n - 出典(.src)の左端 140px → 420px\n - 字幕の中央 960px → 1090px、1行の最大幅 1640px → 1380px(1行26字まで)\n - ロゴ(.brand)を右端に寄せる:右 140px → 56px、上 46px → 44px\n各要素の位置は getBoundingClientRect で測り、png/boxes.json に書き出す(重なり確認用)。\n\"\"\"\nimport sys, os, json, html\n# 本番の scenes.py と logo_dark.png を読む。_制作ベース/hakobu_試作/ に置いたときは1つ上の _制作ベース を、\n# それ以外の場所では base/ に置いた写しを使う\n_here = os.path.dirname(os.path.abspath(__file__))\n_parent = os.path.dirname(_here)\nsys.path.insert(0, _parent if os.path.exists(os.path.join(_parent, \"scenes.py\")) else os.path.join(_here, \"base\"))\nimport scenes\nfrom playwright.sync_api import sync_playwright\n\nsubs = json.loads(sys.argv[1])\nos.makedirs(\"png\", exist_ok=True)\n\nHAKOBU_CSS = \"\"\"\n.src{left:420px !important}\n.brand{right:56px !important;top:44px !important}\n\"\"\"\nscenes.CSS = scenes.CSS + HAKOBU_CSS\n\nscene = scenes.page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">π0.5 をシミュレータで動かしたときの GPU メモリ(実使用の最大)</div>\n <div class=\"hero\">9,735<small>MiB</small></div>\n <div class=\"h2\" style=\"margin-top:36px\">起動時に確保された量の <span class=\"red\">約28%</span></div>\n</div>\n<div class=\"src\">出典:Pixels to Motors 実測(GPU:NVIDIA A40、π0.5-LIBERO)</div>\"\"\",\n tag=\"<b>2</b>買う前に借りて測る\")\n\nSUBCSS = \"\"\"*{margin:0;padding:0}html,body{width:1920px;height:1080px;background:transparent}\nsvg{position:absolute;left:0;top:0}\n.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}\"\"\"\nSUB_CX, SUB_MAXW = 1090, 1380\n\nMEASURE = \"\"\"() => [...document.querySelectorAll('.tag,.brand,.kicker,.hero,.h1,.h2,.h3,.src,.wrap *')]\n .filter(e => e.getBoundingClientRect().width > 0)\n .map(e => { const r = e.getBoundingClientRect();\n return [e.className || e.tagName, [r.left, r.top, r.right, r.bottom]]; })\"\"\"\n\nboxes = []\nwith sync_playwright() as p:\n b = p.chromium.launch()\n pg = b.new_page(viewport={\"width\": 1920, \"height\": 1080})\n pg.set_content(scene)\n pg.wait_for_timeout(100)\n pg.screenshot(path=\"png/static.png\")\n boxes += [[f\"図:{n}\", r] for n, r in pg.evaluate(MEASURE)]\n for k, s in enumerate(subs):\n lines = s.split(\"\\n\")\n assert all(len(ln) <= 26 for ln in lines), s\n svg_lines = \"\".join(f'<text x=\"{SUB_CX}\" text-anchor=\"middle\" class=\"t\">{html.escape(ln)}</text>' for ln in lines)\n pg.set_content(f\"\"\"<!doctype html><html><head><meta charset=\"utf-8\"><style>{SUBCSS}</style></head><body>\n<svg id=\"s\" width=\"1920\" height=\"1080\" viewBox=\"0 0 1920 1080\">{svg_lines}</svg></body></html>\"\"\")\n rects = pg.evaluate(f\"\"\"() => {{\n const ts=[...document.querySelectorAll('text.t')]; const lh=72; const n=ts.length;\n 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)); }}\n ts.forEach((t,k)=>{{ t.style.fontSize=fs+'px'; t.setAttribute('y', 1080-62-(n-1-k)*lh); }});\n return ts.map(t => {{ const r=t.getBoundingClientRect(); return [r.left-6, r.top-6, r.right+6, r.bottom+6]; }});\n }}\"\"\")\n boxes += [[f\"字幕{k}-{j}\", r] for j, r in enumerate(rects)]\n pg.screenshot(path=f\"png/sub_{k}.png\", omit_background=True)\n b.close()\njson.dump(boxes, open(\"png/boxes.json\", \"w\"), ensure_ascii=False)</code></pre><h3 id=\"index_Fr7ec4rQ\">edit_rules.py(video_rules.md に書き足したスクリプト)</h3><p>ルールの文書は、文章を打ち直すと途中が抜けることがあるので、置き換える文が文書の中にちょうど1か所あることを確かめてから置き換えるスクリプトで書き足しました。このスクリプトを動かしたあと、試作のフォルダの説明の1行だけを、次の sed で置き換えています。</p><pre><code class=\"language-python\">\"\"\"video_rules.md にハコブくんのナレーションで決めたことを書き足す(置換は1か所ずつ一致を確かめる)\"\"\"\nimport sys\n\nsrc, dst = sys.argv[1], sys.argv[2]\ns = open(src, encoding=\"utf-8\").read()\n\n\ndef rep(old, new):\n global s\n assert s.count(old) == 1, f\"一致が1か所ではない: {old[:40]}\"\n s = s.replace(old, new)\n\n\n# --- 冒頭 ---\nrep(\"最終更新: 2026-10-11\\n状態: 試作1本目「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」を作りながら決めたことをまとめた初版。まだ決まっていない項目は8節にまとめている。\",\n \"最終更新: 2026-10-12\\n状態: 試作1本目「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」を作りながら決めたことをまとめた初版に、ナレーションをハコブくんにする決定(2026-10-12)を足した版。まだ決まっていない項目は8節にまとめている。\")\n\n# --- 1節 ---\nrep(\"| 1 | 自己紹介 | 〜0:55 | ロゴのテロップを出し、毎回同じ文言で名乗る(2節の文言) |\",\n \"| 1 | 自己紹介 | 〜0:55 | ロゴと、エイイチの名前のテロップを出す。ハコブくんが毎回同じ文言で番組とエイイチを紹介する(2節の文言) |\")\nrep(\"| 8 | 見立て | 〜12:30 | 「ここからが僕の一番言いたいことです」と言ってから意見を話す |\",\n \"| 8 | 見立て | 〜12:30 | 「ここからはエイイチの見立てです」と言ってから意見を話す(意見が誰のものかをはっきりさせる) |\")\nrep(\"| 自分の立ち位置を明かす | 3 結論と立場、または本編1節の頭 | 「僕自身、仕事で使っていて、今回は得をする側なんです」「僕は〜否定派じゃないです。技術としては本物」 |\",\n \"| 自分の立ち位置を明かす | 3 結論と立場、または本編1節の頭 | 「僕自身、仕事で使っていて、今回は得をする側なんです」「僕は〜否定派じゃないです。技術としては本物」。このチャンネルではハコブくんが話すので「エイイチ自身、〜」と言い換える |\")\nrep(\"数字の言い方は5本とも同じだった。\",\n \"台本はすべてハコブくんが話す前提で書く。エイイチの体験と意見は「エイイチは」「エイイチの机の上では」と三人称で言う。\\n\\n数字の言い方は5本とも同じだった。\")\n\n# --- 2節 ---\nrep(\"「僕の機体とソフトの組み合わせでは」を付ける。\", \"「エイイチの機体とソフトの組み合わせでは」を付ける。\")\nrep(\"「配送開始を示す公式発表は、僕が確認した範囲では見つかっていません」\",\n \"「配送開始を示す公式発表は、エイイチが確認した範囲では見つかっていません」\")\nrep(\"自己紹介の文言は「僕は本業のデザイナーをしながら、ロボティクスの研究と開発をしています。自宅の机の上でロボットアームを動かして、その記録を公開しています。」に固定する。「秋田」「29年」「毎日」は入れない。\",\n \"自己紹介の文言は、ハコブくんが話す形の「この番組は、本業のデザイナーをしながらロボティクスの研究と開発をしているエイイチが、自宅の机の上でロボットアームを動かした記録をもとにしています。」に固定する(2026-10-12 に、それまでの一人称の文言を言い換えた。文言を変えるときはエイイチが決める)。「秋田」「29年」「毎日」は入れない。\")\n\n# --- 3節 ---\nrep(\"\"\"## 3. ナレーション(現状は仮音声)\n\nいまは仮の合成音声で作っている。本番の声は未決(8節)。\n\n- **エンジン:** Open JTalk、声は nitech_jp_atr503_m001(男声)。設定は 速さ 1.08、音の高さ -1、声質 0.55\"\"\",\n\"\"\"## 3. ナレーション(ハコブくん・声は仮音声)\n\n### 3-1. ナレーションはハコブくんが話す(2026-10-12 決定)\n\n- ナレーションは、Pixels to Motors のキャラクターのハコブくん(病院で物を運ぶロボット)が担当する。エイイチ本人の声と、本人の声の複製は使わない(本人の声が通らないため。複製しても声の性質はそのまま写る)\n- 動画の目的は、見た人から仕事の相談や声がけをもらうこと。キャラクターの後ろで本人が見えなくならないよう、次の3つを必ず入れる\n - 自己紹介の場面で、エイイチの名前のテロップと実機の映像を出す\n - 見立ての節は「ここからはエイイチの見立てです」と言ってから入る\n - 概要欄にプロフィールと連絡先を置く\n- 概要欄に「ナレーションは AI 音声です」と書く\n- 声は、他のチャンネルと同じ声(VOICEVOX のキャラクターの声など)を使わず、ハコブくん専用の声にする。「ハコブくん=有名キャラクターの声のロボット」と聞かれないようにするため。候補は8節\n- 高すぎない声で、ゆっくり丁寧に話す。経営者が13分聞いて疲れないことを基準にする。キャラクターらしい決まった語尾は本編で使わない(使うならジングルと最初の一言だけ)\n\n### 3-2. 仮音声の設定(ハコブくんの声が決まるまで)\n\n- **エンジン:** Open JTalk、声は nitech_jp_atr503_m001(男声)。設定は 速さ 1.0(2026-10-12 に 1.08 から下げた。ハコブくんの口の動きと合わせると速すぎたため)、音の高さ -1、声質 0.55\"\"\")\n\n# --- 4-1 ---\nrep(\"\"\"- 余白は左右140px。下の250pxは字幕のために空ける\n- 左上に章のタグ(章番号はオレンジ、見出しは薄い文字)、右上にロゴ(高さ34px)\"\"\",\n\"\"\"- 余白は左右140px。下の250pxは字幕のために空ける\n- 左上に章のタグ(章番号はオレンジ、見出しは薄い文字。左端から140px)、右上にロゴ(高さ34px。右端から56px、上端から44px。2026-10-12 に右140px・上46pxから端へ寄せた)\n- **ハコブくんの区画:** 左下に、ハコブくんの上半身を右向きで置く(全身より図の邪魔にならないため、上半身を基本にする)。体は幅340pxで x=20〜360px、画面の下端に接して置く(上端は y=766px)。区画は体の範囲に余白20pxを足した x=0〜380px、y=746〜1080px。この区画には字幕・図・出典・テロップを重ねない。体の画像を差し替えたら、区画は置いた体の範囲から計算し直す(スクリプトが自動で計算する)\n- **図の範囲:** 左右140pxのまま使う。ハコブくんの区画に重なるかどうかは、場面ごとに書き出しの前に自動で確かめる。図・文字の位置はブラウザで描いたときの実際の位置を測り、区画と1pxでも重なった場面は、場面の名前を出して書き出しを止める。止まった場面は、図を区画より上に収めるか、左端を420pxに寄せて作り直す\"\"\")\n\n# --- 4-2 ---\nrep(\"- 新しい色を足すときは、色覚の違いがあっても見分けられるかを検証してから使う(dataviz スキルの validate_palette.js。背景色 #F5F5F0 を指定する)\",\n \"- 新しい色を足すときは、色覚の違いがあっても見分けられるかを検証してから使う(dataviz スキルの validate_palette.js。背景色 #F5F5F0 を指定する)\\n- ハコブくんの体と顔の色(目の白 #F6F8FC、口の水色 #57B5F8、側面のリングの緑)は、この表と4-5の「オレンジと青以外の色を足さない」の対象外。図や文字には使わない\")\n\n# --- 4-5 ---\nrep(\"- 影、グラデーション\\n\", \"- 影、グラデーション(ハコブくんにも影は付けない。目のまわりのにじむ光と側面のリングの光は、ハコブくん自身が光っている表現なので例外)\\n\")\n\n# --- 4-6 と 4-7 ---\nrep(\"数字を出す画面には、左下(字幕の上)に出典を24pxの薄い文字で必ず入れる。\",\n \"数字を出す画面には、字幕の上に出典を24pxの薄い文字で必ず入れる。位置は左端から420px(左下のハコブくんの区画を避ける。もとは140px)。\")\nrep(\"\"\"模式図には「図は模式図」と入れる。\n\n---\"\"\", \"\"\"模式図には「図は模式図」と入れる。\n\n### 4-7. ハコブくんの描き方と動き\n\n試作(2026-10-12、`_制作ベース/hakobu_試作/`)で決めた作り方。数値はスクリプトの先頭近くにまとめてある。\n\n**体**\n- 元の動画(ハコブくんが病室にいる生成動画)の1コマから体を切り抜き、左右反転して右向きにする(画面の中央を向く)\n- 頭の上は、後ろの白い枕と重なっていて切り抜きが欠ける。元の動画では、頭の上は「画面の上の縁が低く、奥側がもう一段持ち上がった」形をしている。切り抜きがはっきり頭と判断した範囲を輪郭に使い、縁のギザギザだけをなめらかにする。色は元のコマの画素で戻す(欠けた部分を直線や凸包で埋めると、奥の段差が消えて枕まで頭として塗ってしまう)\n- 側面の緑のリングは、元の絵の画素が粗いので一度消し、楕円をコードで描き直す\n\n**顔(すべてコードで描く)**\n- 目: 白(#F6F8FC)の縦長のカプセル形(上下の端が半円)。まわりに白い光が少しにじむ。光は画面の外に出さない\n- 口: 水色(#57B5F8)。閉じた口(両端が丸い太い弧)・小(上が広く下が丸い形)・中・大(角の丸い三日月形)の4段階\n- 顔は、ディスプレイの四隅に合わせて射影し、ディスプレイと同じ傾きで描く。四隅は、上下左右の辺のまっすぐな部分にそれぞれ直線を当てはめた交点で決める(角が丸いので、角の点を直接取ると傾きを読み違える)\n- 本番では、体だけを先に反転し、顔は向きどおりに描く(試作は顔を描いてから体ごと反転している。左右で形が違う表情が逆になるため)\n\n**動き**\n- 口: ナレーションの音量で4段階を選ぶ。口の形は2フレームごと(毎秒15回)に、1段階ずつしか変えない。閉じた口と大きい口の間に、必ず小と中がはさまる\n- 目: 2.8秒の周期でゆっくり上下する。口が開くと0.1秒遅れて少し上がる。文の頭で、0.3秒だけ小さく跳ねる\n- まばたき: 2〜4.5秒の間でばらつかせる。文を言い終わった直後にも1回入れる\n- 側面のリング: 顔とは別の3.7秒の周期で、息をするように明滅する(明滅の強さもゆっくり不規則に変える)。声には0.2秒遅れて明るくなり、話し終わるとゆっくり暗くなる。声への反応の強さは文ごとに変える。文の頭と、大きく口を開いた瞬間だけは顔とそろえて強く光る(大きく口を開いたときは、3回に1回ほど光らない)。話していない間も、ときどき弱くまたたく\n\n**表情**\n- 1文ごとに、台本で表情を指定する(narration.py の1文の行に「表情」の欄を足す)\n- 目と頬は表情で決める。口は、話している間はどの表情でも音量で動かし、話していない間だけその表情の口に戻す(「_」の口のまま話すと、声と口が合わなくなるため)\n- 使い分けの目安(1本作って確定する。8節)\n\n| 表情 | 使う場面 |\n|---|---|\n| 丸い目+開いた口 | 基本。説明の文はほぼこれ |\n| 丸い目+小さな笑み | 間、聞き手に戻るところ |\n| 驚いた目+「o」の口 | 今日の問い、意外な数字が出るところ |\n| ジト目 | 「本当にそうでしょうか」と反論を出すところ |\n| 目を閉じる | 「まとめると」、考えている間 |\n| 困り眉 | 失敗や限界の話 |\n| >▽< | 結論が言えたところ、うまくいった実験 |\n| 「III_III」 | 計算中・データを読み込んでいる場面 |\n| ウインク、キラキラ目、照れ、>_<、目が泳ぐ | アウトロや軽い一言だけ |\n\n経営者向けの動画なので、1本で使う表情は上の6つ前後にとどめる。\n\n---\"\"\")\n\n# --- 5節 ---\nrep(\"- 2行まで、1行31字以内。改行は読点の位置で入れる。読点がなければ助詞の後ろで割る\\n- 位置は画面の下から62px\",\n \"- 2行まで、1行26字以内(ハコブくんの区画を避けるため。もとは31字)。改行は読点の位置で入れる。読点がなければ助詞の後ろで割る\\n- 位置は画面の下から62px。横はハコブくんの区画の右側(x=400〜1780px)の中央(x=1090px)にそろえ、1行の幅は1380pxまで\")\n\n# --- 6節 ---\nrep(\"- 映像は 1920×1080 に拡大して切り抜く。字幕の後ろに黒50%の帯(高さ230px)を敷く\",\n \"- 映像は 1920×1080 に拡大して切り抜く。字幕の後ろに黒50%の帯(高さ230px)を敷く。帯はハコブくんの区画の右(x=380px から右)だけに敷く\")\nrep(\"- 名前のテロップ: 薄い地に、ロゴ(高さ30px)・名前60px・肩書き30px(「デザイナー/ロボティクスの研究・開発」)。左の太い線は付けない\",\n \"- 名前のテロップ: 薄い地に、ロゴ(高さ30px)・名前60px・肩書き30px(「デザイナー/ロボティクスの研究・開発」)。左の太い線は付けない。ハコブくんの区画の外に置く\")\n\n# --- 7節 ---\nrep(\"\"\" 5. `assemble.py` で1文ずつ映像を作ってつなぎ、音声と合わせる(字幕の srt も出る)\n 6. 確認用に圧縮する\"\"\",\n\"\"\" 5. `assemble.py` で1文ずつ映像を作ってつなぎ、音声と合わせる(字幕の srt も出る)\n 6. 確認用に圧縮する\n- **ハコブくんの試作(`_制作ベース/hakobu_試作/`):** `python3 make_demo.py upper`(全身版は `full`)で、体の切り抜き・顔の描画・口と目とリングの動き・区画との重なり確認・書き出しまでを行う。図と字幕は `render_static.py` が本番の `scenes.py` を読み込んで描く(`base/` に scenes.py と logo_dark.png の写しを置く)。本番の render.py・assemble.py への組み込みはまだ(8節)\"\"\")\n\n# --- 8節 ---\nrep(\"| ナレーションの声 | Open JTalk の仮音声。抑揚が機械的 | 本人の声で録るか、品質の高い音声合成(VOICEVOX など。利用規約とクレジット表記の条件を確認)にするか。srt に1文ごとの時間が入っているので、それに合わせて録れば映像はそのまま使える |\",\n \"| ハコブくんの声 | ナレーションはハコブくんに決定(3-1)。声は Open JTalk の仮音声 | ハコブくん専用の声を、ElevenLabs の Voice Design(文章で声の特徴を書くと新しい声を作る機能。日本語の出来と、作った声を商用で使う条件は試して確かめる)で作るか、声優に依頼して作るか(毎回読んでもらうか、本人の同意を取って声を複製する。声の権利が一番はっきりする) |\\n| ハコブくんの体の画像 | 生成動画の1コマを切り抜き、頭の上とリングを修整して使っている | 同じ構図(斜め・右向き)で、無地の背景・リングなしの画像を生成し直すか。生成し直せば頭とリングの修整が要らなくなる |\\n| ハコブくんの本番組み込み | 試作(`hakobu_試作/`)で動きと配置を確認した段階 | render.py・assemble.py に組み込む。narration.py に「表情」の欄を足す。表情集の顔をコードで描く |\\n| 表情の使い分け | 4-7の目安の表のみ | 1本作って、使う表情と場面を確定する |\")\nrep(\"| ジングル | 文言だけ(「Pixels to Motors。こんにちは、エイイチです。」) | 音とロゴの動きを付けるか |\",\n \"| ジングル | 文言だけ(「Pixels to Motors。こんにちは、エイイチです。」) | ハコブくんが話す形の文言に言い換える。音とロゴの動きを付けるか |\")\n\n# --- 更新履歴 ---\nrep(\"## 更新履歴\\n\\n\",\n \"## 更新履歴\\n\\n- 2026-10-12: ナレーションをハコブくんにする決定を反映。3-1(ハコブくんが話す・本人を見せる3つの決まり・AI音声の明記・専用の声)と4-7(ハコブくんの描き方と動き・表情)を新設。4-1 にハコブくんの区画(左下・上半身・右向き)と、図の範囲を140pxのまま場面ごとの自動の重なり確認に任せる決まりを追加。ロゴを右56px・上44pxへ寄せた。字幕を1行26字・中央x=1090pxへ、出典を左420pxへ移した。ハコブくんの色を4-2・4-5の対象外にした。台本の一人称(僕)をエイイチの三人称に言い換え、自己紹介の固定文言をハコブくんが話す形にした。仮音声の速さを1.08から1.0に下げた。8節にハコブくんの声・体の画像・本番組み込み・表情の使い分けを追加。\\n\")\n\nopen(dst, \"w\", encoding=\"utf-8\").write(s)\nprint(\"ok\", len(s))</code></pre><pre><code class=\"language-bash\">sed -i 's#図と字幕は `render_static.py` が本番の `scenes.py` を読み込んで描く(`base/` に scenes.py と logo_dark.png の写しを置く)。#図と字幕は `render_static.py` が本番の `scenes.py` と `logo_dark.png`(1つ上の `_制作ベース` にあるもの)を読み込んで描く。入力の画像(`cut_raw.png`・`full/g_05.png`)も同じフォルダに置いてある。#' rules/video_rules.md</code></pre><p></p><h2 id=\"index_NfGgHGVW\">実行ログ</h2><h3 id=\"index_s_TBaVYv\">元の生成動画の情報(ffprobe)</h3><pre><code>stream|codec_type=video|width=848|height=1056|r_frame_rate=24/1\nstream|codec_type=audio|r_frame_rate=0/0\nstream|codec_type=video|width=848|height=1056|r_frame_rate=90000/1\nformat|duration=10.041667</code></pre><h3 id=\"index_ts_1basB\">ディスプレイ・目・口・リングの測定(measure_face.py)</h3><pre><code>ディスプレイの外接矩形 (x, y, 幅, 高さ): [325, 349, 183, 151]\n白の領域: x=482 y=344 幅=29 高さ=52 面積=172\n白の領域: x=362 y=395 幅=22 高さ=46 面積=799\n白の領域: x=305 y=396 幅=170 高さ=124 面積=2301\n白の領域: x=446 y=396 幅=23 高さ=48 面積=885\n水色の領域: x=378 y=450 幅=68 高さ=40 面積=2115\n口の色(中央値 RGB): [87, 181, 248]\nリングの範囲: 576 548 629 681 画素数 1793\n体の範囲: 291 328 648 904\n頭の上の行ごとの画素数(アルファ>200 / >60 / >10):\n y=320: 0 / 0 / 0\n y=324: 0 / 0 / 36\n y=328: 0 / 42 / 151\n y=332: 33 / 77 / 183\n y=336: 56 / 95 / 212\n y=340: 70 / 167 / 232\n y=344: 198 / 237 / 249\n y=348: 240 / 250 / 261\n y=352: 251 / 257 / 264\n y=356: 258 / 264 / 270\n上の辺: 点 74 個、傾き 0.18°\n下の辺: 点 74 個、傾き 2.16°\n左の辺: 点 60 個、傾き 5.97°\n右の辺: 点 61 個、傾き 5.67°\nディスプレイの四隅(左上・右上・右下・左下): [[336.8, 348.8], [510.2, 349.3], [495.1, 501.2], [321.5, 494.6]]\n参考・角の点を直接取った四隅(eps=9.0): [[504, 360], [488, 496], [327, 485], [346, 354]]</code></pre><h3 id=\"index_wL1s2RcT\">上半身版の書き出し(python3 make_demo.py upper)</h3><pre><code>ハコブくんの区画: (0, 746, 380, 1080) 体の位置: (20, 766, 360, 1080)\n重なり確認: 文字・図・ロゴ 12 個、区画との重なり 0 件\nframes 398 seconds 13.27\n終了コード: 0</code></pre><h3 id=\"index_qWOd32_u\">全身版の書き出しが、重なりの確認で止まった記録(python3 make_demo.py full)</h3><pre><code>ハコブくんの区画: (0, 580, 353, 1080) 体の位置: (47, 600, 333, 1060)\n 重なり: 図:h2 [140, 589.40625, 1780, 672.59375]\n区画に重なる要素があるため中止しました\n終了コード: 1</code></pre><h3 id=\"index_0eceRLsx\">書き出した上半身版の動画の情報(ffprobe)</h3><pre><code>stream|codec_name=h264|width=1920|height=1080|r_frame_rate=30/1\nstream|codec_name=aac|sample_rate=48000|r_frame_rate=0/0\nformat|duration=13.266667|size=530991\n</code></pre><h3 id=\"index_C2dvPBTh\">図解用に時間の値を書き出した記録(python3 make_demo.py upper --dump)</h3><pre><code>timing.json に書き出しました\nフレーム数 398 / 文ごとの開始と発話終了のフレーム [[9, 51], [62, 195], [216, 371]] / リングの明るさの範囲 0.15 〜 1.0 / まばたきのフレーム数 24</code></pre><p></p><h2 id=\"index_Saxw8HVI\">参考にしたページ</h2><ul><li><p><a href=\"https://elevenlabs.io/pricing\" data-has-link=\"true\">ElevenLabs:Pricing</a></p></li><li><p><a href=\"https://elevenlabs.io/docs/product-guides/voices/voice-design\" data-has-link=\"true\">ElevenLabs:Voice Design</a></p></li><li><p><a href=\"https://voicevox.hiroshiba.jp/term/\" data-has-link=\"true\">VOICEVOX:ソフトウェア利用規約</a></p></li><li><p><a href=\"https://support.google.com/youtube/answer/3376882?hl=ja\" data-has-link=\"true\">YouTube ヘルプ:オーディオライブラリの音楽と効果音を使う</a></p></li><li><p><a href=\"https://dova-s.jp/_contents/license/\" data-has-link=\"true\">DOVA-SYNDROME:音源利用ライセンス</a></p></li><li><p><a href=\"https://soundeffect-lab.info/agreement/\" data-has-link=\"true\">効果音ラボ:利用規約</a></p></li><li><p><a href=\"https://www.epidemicsound.com/pricing/\" data-has-link=\"true\">Epidemic Sound:Pricing</a></p></li><li><p><a href=\"https://github.com/danielgatis/rembg\" data-has-link=\"true\">rembg(GitHub)</a></p></li></ul>", "toc": [{"id": "index_Wy0UmY2h", "text": "動画の企画5本は、見た人が「この人に何を頼めるか」が分かる話を選んだ"}, {"id": "index__9qJii4D", "text": "ナレーション・BGM・効果音は、2026年10月11日時点の公式の条件で比べた"}, {"id": "index_F7VUaN1Y", "text": "ナレーションは、本人の声でも声の複製でもなく、ハコブくんに決めた"}, {"id": "index_O4m3dwLv", "text": "ハコブくんは画面の左下に置き、生成した10秒の動画の1コマから体を切り抜いた"}, {"id": "index_g2Fv9MC5", "text": "顔の目と口は、元の絵を消してコードで描き直した"}, {"id": "index_Ufl5ndIQ", "text": "口は音量で4段階に切り替え、1回に1段階ずつしか動かさない"}, {"id": "index_V8JsKfRO", "text": "目は2.8秒の周期で上下し、口より0.1秒遅れて上がる"}, {"id": "index_W02dAGsw", "text": "側面のリングは、元の絵を消して楕円を描き直し、顔と別の周期で光らせた"}, {"id": "index_tXF5W1d5", "text": "目はカプセル形にし、顔はディスプレイの辺に直線を当てはめて傾けた"}, {"id": "index_fNoBvJtt", "text": "頭の上は、凸包で埋めると奥の段差が消えて枕まで塗っていた"}, {"id": "index_boeVOUIR", "text": "ハコブくんの区画に字幕や図が重なったら、書き出す前に止める"}, {"id": "index_ZOcuqpzA", "text": "全身版より上半身版の方が、図を読む邪魔をしなかった"}, {"id": "index_H1Z4xux4", "text": "決めたことは video_rules.md に3-1節と4-7節として書き足した"}, {"id": "index_OMOijwC5", "text": "まだ決まっていないのは、ハコブくんの声・体の画像・本番への組み込み・表情の4つ"}, {"id": "index_AtrH00k0", "text": "試作スクリプトの全文"}, {"id": "index_NfGgHGVW", "text": "実行ログ"}, {"id": "index_Saxw8HVI", "text": "参考にしたページ"}]}
\ No newline at end of file
diff --git a/src/data/records/first-video-end-to-end.json b/src/data/records/first-video-end-to-end.json
index dd29af0..e51bc70 100644
--- a/src/data/records/first-video-end-to-end.json
+++ b/src/data/records/first-video-end-to-end.json
@@ -1 +1 @@
-{"day": "LOG333", "series": "フィジカルAI研究室", "published": "2026-10-12T07:58:00.000Z", "title": "解説動画の1本目を公開まで通す ― 声・BGM・間・直し・サムネイルを部品ごとに決め、23手順にまとめた", "slug": "first-video-end-to-end", "cover": null, "thumb": "/thumbs/r-first-video-end-to-end.html", "lang": "JP", "description": "解説動画「フィジカルAIの現在地」をハコブくんの声で作り直し、YouTubeに公開した。声・BGM・効果音・間・不具合の直し・概要欄・サムネイルを部品ごとに決め、次の動画から使う23手順にまとめた。", "updated": "2026-10-12T23:58:54.000Z", "body": "<!--TOC--><p></p><hr><h2 id=\"index_5Npkigty\">title: \"解説動画の1本目を公開まで通す ― 声・BGM・間・直し・サムネイルを部品ごとに決め、23手順にまとめた\" slug: first-video-end-to-end date: 2026-10-12 day: 333 meta_description: \"解説動画「フィジカルAIの現在地」をハコブくんの声で作り直し、YouTubeに公開した。声・BGM・効果音・間・不具合の直し・概要欄・サムネイルを部品ごとに決め、次の動画から使う23手順にまとめた。\" tags: [YouTube, 解説動画, ハコブくん, ElevenLabs, BGM, 効果音, ffmpeg, Python, Playwright, サムネイル, 動画制作]</h2><p>試作で作った解説動画「フィジカルAIの現在地」を、ナレーション役のハコブくんの声で作り直し、YouTube に公開しました。公開したタイトルは「人型ロボットの出荷は4倍。ロボットにも「ChatGPTの瞬間」は来たのか|フィジカルAIの現在地」、長さは12分29秒です。</p><p>台本・図・字幕・ハコブくんの動きは、これまでの記事で作ってきました(構成と台本の検証は LOG330、ハコブくんの口と目の動きは LOG331、チャンネルの見た目は LOG332)。この記事は、そこから公開までに決めたことの記録です。声、BGM と効果音、文と文の間、見つかった不具合の直し、YouTube の概要欄、サムネイル、公開した直後に見えたこと、の順に書いています。最後に、次の動画から同じ順番で作れるようにまとめた23の手順を載せました。</p><p>作業は、Claude のクラウドの作業環境でスクリプトを動かし、ElevenLabs で声と音楽を作る所だけを手元の Mac で動かす形で進めました。ElevenLabs の API キーは Mac の中だけで使い、Claude には渡していません。API キーが写っている画面の写真は、この記事に載せていません。</p><p>作ったスクリプトは、記事の後半にすべて全文で載せています。書き出しのログと、測った数値も全量で載せています。</p><p></p><figure><img src=\"/media/s-1280x720_v-fms_webp_da0a9183-719d-40ae-8829-cf3de0ab3f27.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_R8Gtcv3i\">公開した動画は12分29秒・146文・効果音45個</h2><p>公開した動画の中身は、次のとおりです。数値は、YouTube に上げたファイルを ffprobe(動画の中身を調べるツール)と ffmpeg の音量測定で測ったものです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>値</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>長さ</p></td><td colspan=\"1\" rowspan=\"1\"><p>12分29秒(749.1秒・22,473コマ)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>文の数</p></td><td colspan=\"1\" rowspan=\"1\"><p>146文(字幕の字数で約4,300字)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ナレーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくん(ElevenLabs のボイスデザインで作った声・モデル Eleven v4)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM</p></td><td colspan=\"1\" rowspan=\"1\"><p>ElevenLabs Music で作った1曲(bgm_明るいB_2・60秒)を最初から最後までループ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>45個。場面の切り替えの「ホワーン」37個、大きな流れの変わり目の「ポロロン」7個、自己紹介の「キラリン」1個</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>音の大きさ</p></td><td colspan=\"1\" rowspan=\"1\"><p>-16.0 LUFS(YouTube で聞く大きさの目安にそろえた)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>映像</p></td><td colspan=\"1\" rowspan=\"1\"><p>H.264、1920×1080、30コマ/秒</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>上げたファイル</p></td><td colspan=\"1\" rowspan=\"1\"><p>65,655,407 バイト(約66MB)</p></td></tr></tbody></table></div><p>動画は、冒頭で「今年の上半期に出荷された人型ロボットは約4倍」という推計を出し、「ロボットにも ChatGPT のような瞬間はもう来たのか」を問いにしています。答えは「写真を見せて判断させる入口は簡単になった。物を確実に動かすところは、まだ手間がかかる」です。そのあと3つの章(出荷の中身、机の上のロボットアームで分かったこと、データの壁)を見て、ロボットのニュースを見るときの2つの問いで終わります。</p><p>チャンネルは <a href=\"https://www.youtube.com/@PixelstoMotors\" data-has-link=\"true\">Pixels to Motors の YouTube</a> です。</p><h2 id=\"index_iJ5F55aG\">ハコブくんの声は、ElevenLabs のボイスデザインで作った「候補6」に決めた</h2><p>ナレーションはハコブくんが話すと決めています(LOG331)。声は、ほかのチャンネルと同じ声を使わず、ハコブくん専用に作ります。ElevenLabs のボイスデザイン(声の特徴を文章で書くと、新しい声の候補を作る機能)を有料プランで1か月試すことにしました。</p><p>声の候補をいくつか作って聞き比べ、「候補6(明るい30代前半のお手伝いロボット)」の2つ目のパターンに決めました。落ち着いた30代の声(候補1)は、読みは正確でしたが、ハコブくんの見た目に合いませんでした。</p><p>読み上げのモデルは、冒頭の14文を Multilingual v2 と Eleven v4 で読ませて聞き比べ、Eleven v4 にしました。話す速さは既定のままです。1秒に約6.8字で、仮の音声(Open JTalk)の約5.2字より速く、動画は仮の音声の版より短くなりました。</p><p>声づくりは、手元の Mac で <code>tts_eleven.py</code> を動かして1文ずつ mp3 にし、それを Claude の作業環境で <code>tts_import.py</code> が書き出し用の wav に変える流れです。文の前後の無音は 0.05 秒を残して切り、文と文の間は台本に書いた「後ろの間」で決めます。</p><h2 id=\"index_qsSC6Gf9\">「エイイチ」の抑揚は、ひらがなの「えいいち」で渡すとそろった</h2><p>通しで聞くと、「エイイチ」という名前の抑揚が文ごとに違っていました。</p><blockquote><p>エイイチのイントネーションが揃っていないので、全て同じイントネーションに修正してください(語尾が下がる方が通常のイントネーションかな)</p></blockquote><p>ElevenLabs に渡す文の書き方で、読み方が変わります。そこで <code>tts_eleven.py</code> に、台本ではなく試しの文だけを音声にする <code>--try</code> を足し、同じ文を4通りの書き方で作りました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>番号</p></th><th colspan=\"1\" rowspan=\"1\"><p>ElevenLabs に渡した書き方</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>エイイチ(カタカナ)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>栄一(漢字)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>えいいち(ひらがな)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>エーイチ(伸ばす音)</p></td></tr></tbody></table></div><blockquote><p>3回が正しい発音でした。</p></blockquote><p>ひらがなの「えいいち」が、語尾の下がる読みでそろいました。台本 <code>narration.py</code> に <code>ELEVEN_READ</code>(ElevenLabs に渡すときだけ文字を置きかえる表)を作り、「エイイチ」を「えいいち」に置きかえています。字幕はカタカナのままです。</p><p>もう1か所、読み間違いが見つかりました。</p><blockquote><p>3分16秒あたりのセリフが、裏表裏になっています(テキストは合っている)</p></blockquote><p>37番の文で、「裏表」が「うらおもてうら」のように読まれていました。字幕の文字は合っていて、音声だけが違います。<code>ELEVEN_READ</code> に「裏表 → うらおもて」を足し、その文だけ作り直しました。仮の音声用の読み替え表 <code>READ</code> とは別に持っているのは、同じ語でも、エンジンによって間違え方が違うためです。</p><h2 id=\"index_NvWums4o\">BGM は、コードで作った音から ElevenLabs Music の生楽器の曲に替えた</h2><p>BGM は、最初にコードで仮の曲を作りました(<code>sound_synth.py</code>。やわらかい和音と小さな粒の音を数式で鳴らしたもの)。混ぜ方を確かめるための仮の曲です。</p><blockquote><p>音源はシンセサイザーっぽいのではなくて、ほんのもう少しだけ温かみのある音源が良さそう(テーマが真面目なので、少し音源には彩りを加えたい的な)</p></blockquote><p>コードでピアノ風・ギターとマリンバ風・ピッツィカート風の3曲を作り直しましたが、方向は変わりませんでした。</p><blockquote><p>音源はもっと生音っぽい方がいいかな。音源使えるのある?</p></blockquote><p>そこで、使える音源を調べて表にしました(<code>音源_候補.md</code>。全文は後半)。条件は、YouTube で収益化しても使えること、クレジット表記の条件を守れること、ほかのチャンネルと同じ曲になりにくいことです。第一候補を ElevenLabs Music(文章で曲の雰囲気を書くと、新しい曲を作る機能)にしました。声と同じプランで使え、新しく作る曲なので、ほかのチャンネルと同じ曲にならないためです。</p><p>ElevenLabs Music を API から使うと、最初は次のエラーで止まりました。</p><pre><code>HTTP 401 missing_permissions music_generation\n</code></pre><p>使っていた API キーに、音楽を作る許可がありませんでした。新しいキーを作り直し、「読み上げ・効果音・音楽生成」の3つだけを許可しました。前のキーは、以前に画面の写真に写ったことがあったため、削除を勧めました(削除したかは、この記事の時点で確かめていません)。キーは、Mac のターミナルで画面に出ない形で入れています。</p><pre><code>read -s ELEVENLABS_API_KEY && export ELEVENLABS_API_KEY\n</code></pre><p>曲は <code>sound_eleven.py</code> で60秒ずつ作りました。最初の4曲(冒頭用2曲・本編用2曲)は、聞くと暗く感じました。</p><blockquote><p>もう少し明るい曲調がいいかな。暗く聞こえる</p></blockquote><p>長調・明るめを指定して、2種類の書き方で2曲ずつ作り直しました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>曲</p></th><th colspan=\"1\" rowspan=\"1\"><p>指定した内容(要点)</p></th><th colspan=\"1\" rowspan=\"1\"><p>結果</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>bgm_冒頭_1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>生の楽器、ギター・ピアノ・ピッツィカート・マリンバ、100 BPM、好奇心と希望</p></td><td colspan=\"1\" rowspan=\"1\"><p>暗く聞こえた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>bgm_本編_1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>生の楽器、フェルトピアノ・ギター・チェロとバイオリン、90 BPM、落ち着いて真面目</p></td><td colspan=\"1\" rowspan=\"1\"><p>暗く聞こえた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>bgm_明るいA_1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>長調、ギター・明るいピアノ・鉄琴・手拍子、105 BPM、晴れた朝のように</p></td><td colspan=\"1\" rowspan=\"1\"><p>A_1 を冒頭で試した</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>bgm_明るいB_1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>長調、ピアノとギターが主、弦・ピッツィカート・鉄琴、100 BPM、前向きで落ち着いた明るさ</p></td><td colspan=\"1\" rowspan=\"1\"><p><strong>B_2 に決めた</strong></p></td></tr></tbody></table></div><blockquote><p>bgm_明るいB_2.mp3 bgm_明るいA_1.mp3 で動画作成してみて</p></blockquote><p>冒頭15秒を2曲それぞれで書き出して聞き比べ、B_2 に決まりました。指定した文章の全文は、後半の「作った曲の記録」にあります。</p><p>曲は60秒なので、12分29秒の動画ではループします。ElevenLabs の曲は最後の数秒で小さくなって終わるため、そのままつなぐと1分ごとに音が消えます。<code>audio_mix.py</code> で、曲のふつうの大きさより10dB以上小さくなる終わりの部分を切り、つなぎ目を2秒重ねてループさせました。</p><h2 id=\"index_845PdZbU\">BGM の大きさは、話している間 -23dB・0.8秒以上の間 -16dB にした</h2><p>BGM は、ハコブくんが話している間だけ自動で小さくし、話していない間で少し戻します。値はナレーションの大きさとの差で、-23dB はナレーションのおよそ14分の1の音圧です。</p><p></p><figure><img src=\"/media/s-1800x780_v-frms_webp_a3a3705d-d886-470c-8be2-f4a70eed6f10.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>この値に決まるまでに、4回動かしました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>段階</p></th><th colspan=\"1\" rowspan=\"1\"><p>話している間</p></th><th colspan=\"1\" rowspan=\"1\"><p>0.8秒以上の間</p></th><th colspan=\"1\" rowspan=\"1\"><p>きっかけ</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>-22dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>-15dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>最初の値</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>-15dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>-9dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>「音源が聞こえないね。もう少しだけ音量上げるか、音源だけ聴きたい」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>-22dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>-15dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>明るい曲に替えたら大きく聞こえた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>-23dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>-16dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>「音源の音量90%くらいにして」</p></td></tr></tbody></table></div><blockquote><p>曲調を変えたら音量が大きく聞こえてしまうようになったので、前の音量に戻してください。冒頭15秒_bgm_明るいB_2が良さそうです。</p></blockquote><blockquote><p>いいですね。音源の音量90%くらいにして、この動画を仕上げてみてください。</p></blockquote><p>同じ dB の値でも、曲が変わると聞こえ方が変わりました。明るい曲は高い音(鉄琴や明るいピアノ)が多く、低い音の多い曲よりナレーションの上に出て聞こえたためと考えています。90%は、音圧で1dB下げることにあたります(20×log10(0.9) ≒ -0.9dB)。</p><p>話している所の決め方は、次のとおりです。</p><ul><li><p>各文の読み上げの範囲を、前に0.15秒、後ろに0.3秒広げる</p></li><li><p>広げたあとの間が0.5秒未満なら、話している扱いのまま(もとの間で約0.8秒未満。BGM が文ごとに上下しないため)</p></li><li><p>0.3秒かけてなめらかに上げ下げする</p></li></ul><p>グラフのとおり、BGM が戻るのは、場面の切り替えの直前(1.2秒の間)、章の見出しのあと(1.7秒)、大きな流れの変わり目(1.8秒の間)だけです。ふつうの文の間(0.35秒)では、BGM は下がったままです。</p><p>後ろの間の長さを選ぶと、BGM の大きさと鳴る効果音がどう変わるかを、次の図で動かして確かめられます。</p><div class=\"embed\"><iframe src=\"/embeds/first-video-end-to-end-VBQ8Fbsp.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p></p><h2 id=\"index_SkptrlzU\">場面の切り替えは「ホワーン」、大きな流れの変わり目は「キラリン」と「ポロロン」</h2><p>効果音は、最初にコードで5種類作りました(ぽん・こつ・しゅっ・ちん・ひゅい)。冒頭15秒で試したあと、使い方を絞りました。</p><blockquote><p>最後のホワーンって効果音を、場面切り替えに入れてみたい。</p></blockquote><blockquote><p>ホワーンって効果音だけを、場面切り替えに使って</p></blockquote><blockquote><p>効果音はもう少し音量を大きく。</p></blockquote><p>効果音は「ホワーン」(se_rise)だけにし、ナレーションより12dB小さい値から6dB小さい値に上げました。場面(台本の1文ごとに書いた場面の名前)が変わる文の頭から0.15秒前に、自動で鳴らします。</p><p>次に、話の大きな区切りがどこか分かるようにしました。</p><blockquote><p>それと動画の流れの中で少し間があるポイントがあるといいので、大きな流れの変化の時にはその間を設定することはできる?</p></blockquote><blockquote><p>もしくはそのときは効果音を違うものにするか。</p></blockquote><p>大きな流れの変わり目(フック→自己紹介、自己紹介→「今日の問い」、各章の始まり、答え、エンディング)では、直前の文の後ろの間を1.8秒にし、ホワーンの代わりに別の音を鳴らすことにしました。別の音は、コードで6つ作りました(マリンバ・ハープ・ピアノ・鉄琴・ふくらむ和音・風)。</p><blockquote><p>5と2で動画に入れ込んでみて(自己紹介のあたりで入れると良さそう。音源だけでも可です)</p></blockquote><blockquote><p>自己紹介は5のキラリンがいいね。他の切り替えポイントで2のポロロン使ったりと使い分けが良さそうだね(とりあえず自己紹介は5で確定)</p></blockquote><p>自己紹介に入る所は「キラリン」(鉄琴の高い3音)、そのほかの変わり目は「ポロロン」(マリンバの上がる3音)にしました。ポロロンを場所ごとにどう使い分けるかは、まだ試している段階です。</p><p>そのあと、ふつうの場面の切り替えにも、もう少し間が欲しいという指摘がありました。</p><blockquote><p>ホワーンという中の切り替えの時にもう一拍だけ間がある方がいいな。</p></blockquote><p>ホワーンが鳴る所の直前の文の後ろの間を、1.2秒にしました。公開した台本の146文は、後ろの間で分けると次のとおりです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>どこ</p></th><th colspan=\"1\" rowspan=\"1\"><p>後ろの間</p></th><th colspan=\"1\" rowspan=\"1\"><p>文の数</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ふつうの文</p></td><td colspan=\"1\" rowspan=\"1\"><p>0.35秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>96</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>話の小さな区切り</p></td><td colspan=\"1\" rowspan=\"1\"><p>0.4〜0.6秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>4</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ふつうの場面の切り替え(ホワーンの前)</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.2秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>32</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>章の見出しを読む文(「1つ目、…」など)と、最後のあいさつ</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.7秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>5</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>大きな流れの変わり目(キラリン・ポロロンの前)</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.8秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>8</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>字幕のない最後の文(エンディングの余白)</p></td><td colspan=\"1\" rowspan=\"1\"><p>6.0秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>1</p></td></tr></tbody></table></div><p>章の見出しは、読んだすぐあとに場面が切り替わるので、見出しの1.2秒に、切り替えの一拍を足して1.7秒にしています。</p><p>どの文で何が鳴ったかは、書き出しのログに45行で出ています(後半に全量)。</p><h2 id=\"index_ttUdYCtI\">自己紹介の2文目を「研究と開発」と「見立て」に直した</h2><p>確認用の動画を見て、自己紹介の文と、字幕の後ろの帯に指摘がありました。</p><blockquote><p>ここの自己紹介おかしくないか? それとナレーションのテキストの下に薄い四角枠が敷かれているのが必要ないかな。</p></blockquote><blockquote><p>ロボットアームを動かした記録を紹介しているわけではないよ(もちろんそういう時もあるだろうけど)</p></blockquote><p>前の文は「題材は、本業のデザイナーをしながら、自宅の机の上でロボットアームを動かした記録です。」でした。これだと、毎回ロボットアームの記録を見せる番組だと受け取られます。この番組は、フィジカルAIを解説する番組です。Claude がいくつか言い方を出し、次の文に決めました。</p><blockquote><p>エイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。その中で分かったことと、そこから考えた見立てを、ハコブくんがお伝えします。こっちがいいかな。その下はすこし冗長だね。</p></blockquote><p>「結論」ではなく「見立て」にしたのは、今の時点での判断で、新しい事実が出たら変わるからです。</p><p>名前のテロップも分けました。ハコブくんが自分の紹介をしている間は「フィジカルAI解説/案内役:ハコブくん(AI音声)」、本人を紹介する文から「エイイチ」を出します。台本では、この文を「title」から「title_eiichi」という別の場面に分けて、テロップを替えています。映像は同じなので、この切り替えではホワーンを鳴らしません。</p><p>字幕の後ろの薄い黒い帯は、実機の映像の上で字幕を読みやすくするために敷いていたものです。字幕の文字には黒い縁があり、帯がなくても読めるため、外しました。</p><p></p><figure><img src=\"/media/s-1979x632_v-frms_webp_552cefd2-62a2-43d2-bba1-e1e5e23b08c9.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_jcwDZAwf\">右下のモヤモヤは、透かしを消した跡だった</h2><blockquote><p>55秒くらいに右端下がモヤモヤしているのを見つけました。同じようなモヤモヤが他にもないか調べて修正してください。</p></blockquote><p>自己紹介の場面の映像は、以前に動画編集サイト(Clideo)で書き出した版(DAY123.mp4)を使っていました。この版には右下にサイトの透かし(ロゴ)が入っていて、ffmpeg の delogo(指定した範囲を周りの色でぼかして埋める機能)で消していました。その跡が、マットの格子の上で縦の筋のように見えていました。</p><p>同じように透かし入りの版を使っていた映像は、もう1本(DAY124.mp4。章2の実機の場面)ありました。2本とも、透かしの入っていない元の撮影ファイル(DAY123.MOV・DAY124.mov)に替え、<code>scenes.py</code> の映像の指定で、透かしを消す処理を使わない設定(3つ目の値を False)にしました。</p><p><code>assemble.py</code> は、1文ずつ背景の映像を作り、中身が変わった文だけを作り直す仕組みです。どの文を作り直すかは、背景の中身から作った指紋(ハッシュ)で判断しています。この指紋に、使う映像のファイル名と開始秒が入っていませんでした。そのままでは、映像を替えても作り直されないため、指紋に映像の指定を足しました。</p><p></p><figure><img src=\"/media/s-1874x512_v-frms_webp_bca07aad-c04e-4174-a202-e8fd93911724.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_mZmow8P1\">30cm の図の説明を、図の下の2行に移した</h2><blockquote><p>30cm以上違うはのところは新しく内容を修正したので確認してください。前の動画か台本を確認して修正してください。</p></blockquote><p>章2の「計算ツールとデジタルツインで、爪先の位置が30cm以上食い違った」という図で、説明の行が右へはみ出し、「グリッパー」が「グリッパ」と「ー」の2行に折れていました。説明を図の下に移し、中央にそろえた2行にしました。1行目が「アームが外れた距離ではない。爪先の位置の決め方が違っていた」、2行目が「計算ツールには、グリッパーの形が入っていない」です。</p><p></p><figure><img src=\"/media/s-1897x632_v-frms_webp_e4c6b745-4cda-4da9-8bf1-663eb0d60dcb.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_RXyGoUij\">YouTube の概要欄は、目次の時間を書き出しから自動で入れた</h2><blockquote><p>YouTubeの動画内に入れる動画概要は追加情報などをまとめておいてください!</p></blockquote><p>概要欄は、動画の作業フォルダの <code>概要欄_下書き.md</code> に書き、<code>description.py</code> で貼り付け用の <code>概要欄.txt</code> にします。目次の時間は、下書きに <code>{t:26}</code> のように文番号で書くと、書き出しのときに記録した、その文が始まる時間に置きかわります。声や間を変えて時間がずれても、下書きを直す必要がありません。</p><p><code>description.py</code> は、YouTube の目次(チャプター)の条件を満たしているかも確かめます。最初が 0:00 で始まり、3つ以上あり、それぞれ10秒以上あることです。満たさないとき、5,000字を超えるとき、「【要記入】」が残っているときは止まります。</p><p>並べる順は、冒頭の数字、「今日の問い」、先に答え、この動画で見る3つ、目次、出典、この番組について、連絡先とリンク、チャンネル登録のお願いとハッシュタグ3つです。連絡先とリンクは、次のように決めました。</p><blockquote><p>メールアドレス、xとリンクドインかな。</p></blockquote><p>出典のリンクは、すべて開いて確かめてから載せました。Smart Analytics Global の推計は、公開されているページが見つからなかったため、リンクなしで名前と時期だけを書いています。1X NEO の「一般のお客さんへの配送開始」は、公式発表が見つからなかったことを、確かめた日付と一緒に書きました。</p><p>公開した概要欄の全文は、後半の「書き出した概要欄」にあります。</p><h2 id=\"index_WGBasXjY\">タイトルを決め、「子ども向け」は「いいえ」にした</h2><blockquote><p>修正したいところはまだまだありつつも、とりあえず合格点です!YouTubeにアップロードします!</p></blockquote><p>タイトルは、Claude が3案を出し、「人型ロボットの出荷は4倍。ロボットにも「ChatGPTの瞬間」は来たのか|フィジカルAIの現在地」に決めました。冒頭の数字、動画の問い、シリーズ名の順です。</p><p>YouTube に上げるときは、動画が子ども向けかどうかを必ず選びます。ハコブくんはキャラクターですが、動画は大人の視聴者に向けた解説なので「いいえ、子ども向けではありません」にしました。子ども向けにすると、コメントや、ほかの動画の紹介などの機能が使えなくなります。</p><p></p><figure><img src=\"/media/s-1270x654_v-fms_webp_24ce4e2f-fed4-448a-a044-254e3578f394.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_ua9kJv2c\">サムネイルは5回作り直した</h2><blockquote><p>それからサムネイルが必要ですね。ChatGptに作らせるので指示を考えてください。</p></blockquote><p>最初は、ChatGPT に画像を作らせるための指示文を Claude が書き、ChatGPT で作りました。</p><blockquote><p>chatGPTのサムネイルフォントが微妙だし、背景写真が違うしだね。</p></blockquote><blockquote><p>今の動画のフォント周りのデザインをベースにサムネイルの背景を入れる感じでまずは惹きつけられるサムネイルを目指したいです。</p></blockquote><p>画像を作る AI は、日本語の文字の形が崩れやすく、背景のロボットアームも実物と違う形になりました。そこで、文字は画像の AI に任せず、<code>thumbnail.py</code> がコードで組む形にしました。文字と色は動画と同じもの(Noto Sans CJK JP の太字、オレンジ #C4471F、紙色 #F5F5F0、黒 #0D0D0D)を使い、HTML で組んで Playwright(ブラウザを自動で動かす道具)で画像にします。背景は、実機の映像の1コマです。文字をコードで組めば、誤字や崩れた文字は出ません。</p><p></p><figure><img src=\"/media/s-1864x1225_v-frms_webp_4be2ec7d-a345-4e3d-b8e9-88f8d4885fa8.png\" alt=\"\"><figcaption></figcaption></figure><p></p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>回</p></th><th colspan=\"1\" rowspan=\"1\"><p>作ったもの</p></th><th colspan=\"1\" rowspan=\"1\"><p>指摘</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>ChatGPT で作った案</p></td><td colspan=\"1\" rowspan=\"1\"><p>文字の形と、背景の写真が違う</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>案A(紙色と写真)・案B(黒と写真)・案C(大きなハコブくん)</p></td><td colspan=\"1\" rowspan=\"1\"><p>「これがまぁまぁ良さそうだけど。ハコブくんが小さいの修正と右向きがいいね。レイアウト別バージョンを生成」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>案Bの別の組み方3つ(B1・B2・B3)。最初は元の体のまま</p></td><td colspan=\"1\" rowspan=\"1\"><p>「ハコブくんの頭凹んでいるよ!」「最初から作り直したら?」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3'</p></td><td colspan=\"1\" rowspan=\"1\"><p>体を作り直して B1〜B3 を出し直した</p></td><td colspan=\"1\" rowspan=\"1\"><p>「ハコブくん大きすぎ。ハコブくんの顔が変(口と目の配置や大きさ確認)背景を全部写真に変更。テキストを大きく目立つように。写真の上にテキストを置くので、黒縁をつけること。」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>案D(全面写真・黒い縁の大きな文字・ハコブくん右下)</p></td><td colspan=\"1\" rowspan=\"1\"><p>「ハコブくんは左下済み(大きさ少しだけ大きく)人型ロボット出荷 約4倍の部分は黒縁無し。」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>案E(写真のコマを替え、ハコブくん左下)</p></td><td colspan=\"1\" rowspan=\"1\"><p>公開に使った</p></td></tr></tbody></table></div><h2 id=\"index_kbIhNmO_\">ハコブくんの体は、ChatGPT で無地の背景から作り直した</h2><p>サムネイルでハコブくんを大きく出すと、頭の上がへこんで見えました。動画で使っている体の画像は、生成動画の1コマから切り抜いたもので、頭の上が背景と見分けにくく、切り抜きが欠けていたためです。動画では小さく出すので目立ちませんでしたが、サムネイルの大きさでは分かりました。</p><blockquote><p>最初から作り直したら?</p></blockquote><p>ChatGPT で、無地の背景・画面は真っ黒・側面の緑の光の輪はそのまま、という条件でハコブくんの体を作り直しました。</p><p></p><figure><img src=\"/media/s-1328x612_v-fms_webp_86d9d594-b517-4b6d-99b5-8485b97925a6.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>作り直した体は、次の手順でサムネイル用の画像にしました。</p><ol><li><p>切り抜き: OpenCV の GrabCut(指定した四角の中で、前景と背景を色の分布から分ける方法)を、体を囲む四角(GrabCut に渡した値は 230, 170, 680, 1040)で8回くり返して前景を出した。いちばん大きなかたまりだけを残し、中の穴を埋めた。最初に試した、背景の色から塗りつぶす方法は、体の白い部分まで背景として消えた</p></li><li><p>画面の四隅: 黒い画面の四隅の位置を測った(左上 356,262・右上 592,260・右下 569,491・左下 314,485)</p></li><li><p>顔: 画面に、白い目と青い口を描いた</p></li><li><p>向き: 透明でない範囲で切り、左右を反転して右向きにした</p></li></ol><p></p><figure><img src=\"/media/s-1277x420_v-fms_webp_f178bd99-0535-4ee4-a1e8-bca01a47d139.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>3の顔は、最初、動画のハコブくんを見ながら、目と口の位置と大きさを画面の割合で決めて描きました。目が縦に長く上に寄り、口が画面の半分の幅で大きく開いていて、動画のハコブくんと違う顔になりました。</p><p>顔を描き直すとき、目と口を自前で描くのをやめました。動画の <code>hakobu.py</code> が持っている目と口の描き方(<code>eye_mask</code>・<code>mouth_mask</code>)でそのまま描き、動画の体の画面を長方形にならしたものから、新しい体の画面の四隅へ写しています。画面の大きさに対する目と口の大きさと位置が、動画のハコブくんと同じになります。<code>make_new_face.py</code>(全文は後半)がこの処理です。</p><p>1の切り抜きと2の四隅の測定は、その場でコマンドとして動かしたため、スクリプトのファイルが残っていません。結果の <code>cut_new.png</code> と <code>screen_quad_new.npy</code>(四隅の値)は <code>_制作ベース/hakobu/</code> に置いてあり、3と4は <code>make_new_face.py</code> と、そのあとに切り抜きと反転をしたコマンドで作っています。</p><h2 id=\"index_gTZdlqKn\">サムネイルの背景は、ハコブくんがアームに重ならないコマを選んだ</h2><p>ハコブくんを左下に置くと、最初に使っていた写真(DAY124.mov の28秒、アームが赤いブロックを掴む瞬間)では、ちょうど左下に爪とブロックがあり、ハコブくんで隠れます。写真を拡大したり、ずらしたりしても、爪は左下から出ませんでした。</p><p>同じ映像の別のコマを2秒ごとに並べて探し、39.8秒(アームがブロックを箱に置く瞬間)を選びました。このコマは左下がマットだけで、爪とブロックと箱が画面の中央から右下に入ります。</p><p></p><figure><img src=\"/media/s-1980x632_v-frms_webp_c0aa477f-7805-473d-acb6-7dfc5d782633.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>公開したサムネイル(案E)の組み方は、次のとおりです。</p><ul><li><p>背景は写真を全面に敷き、右上だけを少し暗くして文字を読みやすくする</p></li><li><p>「ロボットにも/ChatGPTの瞬間/もう来た?」は、1920×1080 のとき150・200・170px の大きさで、24px の黒い縁を付ける</p></li><li><p>上の札「人型ロボット出荷 約4倍」は、オレンジの札に白い文字。黒い縁は付けない</p></li><li><p>ハコブくんは左下の隅に、高さ600px で右(アーム)を向けて置く。「本当に?」の吹き出しは右側</p></li><li><p>1920×1080 で作って 1280×720 に縮め、YouTube の上限の2MB 以下にする(公開版は約1MB)</p></li></ul><h2 id=\"index_NqzL8c9Y\">公開した直後に見えた2つのこと:字幕が2重に出る、画質が低い</h2><blockquote><p>アップしました!</p></blockquote><p>公開したページを開くと、YouTube の字幕(黒い帯に白い文字)が、動画に焼き込んだ字幕の上に重なって、同じ文が2重に出ていました。</p><p></p><figure><img src=\"/media/s-1932x1606_v-frms_webp_e2dcc461-2987-4422-86aa-134d6e44985c.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>動画には字幕が焼き込んであります。字幕のファイル(srt)を YouTube に字幕として登録していると、視聴者が字幕をオンにしたときに2重になります。登録しておくと検索や、音を出せない人への字幕として役に立つ一方、焼き込んだ字幕と重なります。登録を残すかどうかは、これから決めます。</p><blockquote><p>動画の画質こんなもん?アップしてすぐだから?</p></blockquote><p></p><figure><img src=\"/media/s-3024x1912_v-frms_webp_3eb563d7-8909-410f-9e2a-a4a74c38cc55.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>YouTube は、上げた直後は低い画質の版を先に公開し、HD(1080p)の版はあとから作ります。文字のふちがにじんでいたのは、このためと考えています。再生画面の歯車の「画質」に 1080p が出ていれば、HD の版はできています。</p><p>一方、上げたファイルの映像は、測ると約0.49Mbps(488,631 ビット/秒)でした。<code>assemble.py</code> は crf 20(画質を一定に保つ設定。数字が小さいほど高画質)で書き出していて、図と無地の背景が多い動画なので、この値でも書き出したファイルの文字はくっきりしています。ただ YouTube は上げたファイルをもう一度圧縮するので、HD の版でも文字が甘く見えるようなら、2560×1440 に拡大して書き出し直す方法を試す候補にしています。YouTube は 1440p 以上の動画には、画質のよい圧縮方式を使うためです。HD の版での見え方は、まだ確かめていません。</p><h2 id=\"index_CTcfDe5k\">チャンネル名は「フィジカルAI解説|Pixels to Motors」、まだ反映されていない</h2><blockquote><p>チャンネルの名前なんだっけ?(更新されていない)</p></blockquote><p>チャンネル名は「フィジカルAI解説|Pixels to Motors」に決めています(LOG332)。公開した時点では、YouTube ではまだ「Pixels to Motors」と表示されていました。</p><blockquote><p>何度も修正しすぎて反映されなくなってるかも・</p></blockquote><p>YouTube のチャンネル名は、14日間に2回までしか変えられず、変えた名前が表示されるまでに数日かかることもあります(<a href=\"https://network.mobile.rakuten.co.jp/sumakatsu/contents/articles/2026/00533/\" data-has-link=\"true\">楽天モバイルの解説記事</a>)。YouTube Studio の「カスタマイズ」→「基本情報」の名前の欄が新しい名前になっていれば、表示が追いつくのを待ちます。古いままで保存できなければ、2回の上限に当たっています。どちらだったかは、この記事の時点で確かめていません。</p><h2 id=\"index_qiv3oQpt\">X と LinkedIn の投稿文は、数字と机の上の実感を先に置いた</h2><blockquote><p>Xとlinkdinに投稿するので、投稿内容を考えてください。わかりやすくかつ、興味を持ちやすい投稿内容になるように注意してください。</p></blockquote><p>X は140字(URL を含む)に収めるため、冒頭の数字(約4倍)、動画の問い、机の上のロボットアームで650回以上試した実感(写真で判断させるのは簡単、物を確実に動かすのはまだ手間)の3つだけにしました。X の数え方では、日本語1字が2、URL が23で、280以内に収まれば140字以内です。作った文は266でした。</p><p>LinkedIn は英語のあとに日本語を並べ、動画の中の具体的な話(計算ツールとデジタルツインで爪先の位置が30cm以上食い違ったこと、肩の関節に90度ちょっとを超える値を送るとエラーなしで動かないことがあったこと)を入れました。30cm は「アームが30cm外れた」と読まれやすいので、コメント欄への補足で、爪先の位置の決め方が違っていた話だと書き足しています。ハッシュタグは、記事の投稿で使う #R2R2R ではなく、動画の話題で探す人が使う #フィジカルAI・#PhysicalAI にしました。</p><h2 id=\"index_YMC72TM8\">Claude の返事が英語になり、2回聞き返された</h2><p>作業の途中で、Claude の返事が英語になったことが2回ありました。</p><blockquote><p>英語で書いてあってわからないよ?</p></blockquote><blockquote><p>英語だよ?</p></blockquote><p>1回目は、使える音源の候補を調べて返したとき、2回目は、公開したページを見たあとの返事です。どちらも日本語で書き直しました。このプロジェクトの返事は日本語で書くと決まっています。</p><h2 id=\"index_ZKdeas3Q\">Mac へ送ったファイルが、途中で変わることがあった</h2><p>作ったファイルは、Claude の作業環境から、Mac とつながった経路で iCloud と外付けの SSD(KIOXIA)に送っています。この経路で、3つのことが起きました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>起きたこと</p></th><th colspan=\"1\" rowspan=\"1\"><p>どう分かったか</p></th><th colspan=\"1\" rowspan=\"1\"><p>どうしたか</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>約28MB を超えるファイルが送れない(HTTP 400)</p></td><td colspan=\"1\" rowspan=\"1\"><p>送る道具がエラーを返した</p></td><td colspan=\"1\" rowspan=\"1\"><p>10MB ずつに分けて送り、Mac でつないだ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>分けた動画の先頭の1つ、透明な部分のある PNG(ハコブくんの切り抜きなど)が、届いたときに中身が変わっていた</p></td><td colspan=\"1\" rowspan=\"1\"><p>送る前と届いたあとの指紋(md5。ファイルの中身から作る32桁の値)が違った。PNG は届いた方が大きかった</p></td><td colspan=\"1\" rowspan=\"1\"><p>gzip で圧縮して送り、Mac で戻した。戻したあとの指紋が一致するのを確かめた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Mac にあるスクリプト(assemble.py・2本の scenes.py)が、古い版に戻っていた</p></td><td colspan=\"1\" rowspan=\"1\"><p>Mac と作業環境のファイルの指紋を比べた</p></td><td colspan=\"1\" rowspan=\"1\"><p>送り直して、指紋が一致するのを確かめた</p></td></tr></tbody></table></div><p>YouTube に上げた動画ファイルは、作業環境の指紋 <code>d9f6def3f1a01d9aee5c76c9d6516bc0</code> と、Mac でつないだあとの指紋が一致しています。送ったあとは、毎回指紋を比べることにしました。</p><h2 id=\"index_lMTio9tY\">次の動画から使う「1本を作る流れ」23手順</h2><blockquote><p>動画の作り方の手順をまとめてありますか?他の台本を使って動画を量産していきますよ。</p></blockquote><p>作る手順は、動画のルール <code>video_rules.md</code> の中に分かれて書かれていて、サムネイル・アップロード・投稿の手順はどこにもありませんでした。そこで <code>video_rules.md</code> の先頭に「0節 1本を作る流れ」を足し、台本から投稿までを23の手順にまとめました。「フィジカルAIの現在地」で実際に通した順番です。担当の <strong>エ</strong> は本人、<strong>C</strong> は Claude です。追記した0節を、ファイルのまま載せます。</p><pre><code class=\"language-markdown\">### 準備(1本ごとに1回)\n\n1. **エ** どの台本を動画にするか決めて、Claude に伝える\n2. **C** iCloud の `動画/` に作業フォルダ `{動画名}/` を作る。前の動画の作業フォルダから `narration.py`・`scenes.py`・`sound.py`・`thumb.py`・`概要欄_下書き.md` を写して、書き換える元にする\n\n### 台本(2節・3節)\n\n3. **C** 台本を `narration.py` に書く。1文ごとに(場面の名前、表情、字幕の文、後ろの間)。自己紹介の2文(2節10)と、間の取り方(2節12)は前の動画と同じにする\n4. **C** 読みの直しを書く。`READ`(仮音声用)と `ELEVEN_READ`(ElevenLabs 用)。「エイイチ」は ElevenLabs には「えいいち」で渡す(3-1)\n5. **C** `make_docs.py` で読み物の台本と codex の依頼を作る。**エ** 内容を確かめる。数字と出典は、公開前に元のページを開いて確かめる\n6. **エ** 台本を確定する(ここで決まった文が、声のお金を使う単位になる)\n\n### 声(3-1)\n\n7. **エ** Mac のターミナルで、作業フォルダに移って声を作る:\n `read -s ELEVENLABS_API_KEY && export ELEVENLABS_API_KEY`(キーを貼って Enter。画面には出ない)\n `python3 ../_制作ベース/tts_eleven.py --voice gWJpc8whMBqnF7Y0kc9f --name ハコブ`\n 読み方を比べたい語があるときは `--try \"文1|文2\"` で試しの音声だけ作る\n8. **C** `tts_import.py voice_eleven/ハコブ` で取り込む。読み間違い(例: 裏表が「うらおもてうら」)が見つかったら、`ELEVEN_READ` を直し、**エ** がその文だけ `--lines 番号` で作り直す\n\n### 音(8節の BGM・効果音の行)\n\n9. **C** `sound.py` に BGM と効果音の置き方を書く。いまの決まり: BGM は `bgm_明るいB_2` を最初から最後までループ、話している間 -23dB・間 -16dB。場面の切り替えはホワーン(-6dB)、自己紹介はキラリン、大きな流れの変わり目はポロロン\n10. **C** `audio_mix.py --first N` で音だけ書き出し、**エ** が冒頭を聞いて大きさを確かめる\n\n### 図と字幕(4節・5節・6節)\n\n11. **C** `scenes.py` に場面ごとの図を書く。実機映像は `src/` に置き、`VIDEO` に場面名と開始秒を書く。透かし入りの映像は使わない(6節)\n12. **C** `render.py` で図・テロップ・字幕を作る。ハコブくんの区画と重なったら止まるので、直して通す\n13. **C** `preview.py 文番号…` で、気になる文の画面を静止画で出す。**エ** 確かめる\n\n### 書き出しと確認(7節)\n\n14. **C** `assemble.py` で全体を書き出す(14分で30分ほど)。確認用(30MB 以下)をチャットに送り、KIOXIA の `videos/{動画名}/` にも置く\n15. **エ** 通しで見て、直す所を時間つきで伝える(例:「3分16秒の読みがおかしい」「55秒の右下がモヤモヤ」)\n16. **C** 直して書き出し直す(中身が変わった文だけ作り直すので、2回目からは速い)。15〜16 を合格まで繰り返す\n17. **C** アップロード用(crf 20)を KIOXIA に置く。大きいファイルは分けて送り、Mac でつなぎ、指紋(md5)が一致するのを確かめる\n\n### YouTube に出すもの(7節)\n\n18. **C** `概要欄_下書き.md` を書き、`description.py` で `概要欄.txt` を作る。目次の時間は自動で入る。リンクは開いて確かめてから載せる\n19. **C** タイトル案を3つ出す。**エ** 決める\n20. **C** `thumb.py` に文言と写真を書き、`thumbnail.py` でサムネイルを作る(8節のサムネイルの行)。写真は、ハコブくんを左下に置いてもアームや物に重ならないコマを実機映像から選ぶ。**エ** 確かめる\n21. **エ** YouTube にアップロードする。設定: 子ども向けは「いいえ」。字幕ファイル(srt)は、映像に字幕が入っているので、上げると字幕をオンにした人には2重に出る(上げるかはエイイチが決める)。公開直後は低い画質で再生され、HD(1080p)は数十分〜1時間後に出る\n\n### 公開のあと\n\n22. **C** X と LinkedIn の投稿文を作る(書き方は article_rules.md の SNS 節。X は URL 込み140字・LinkedIn は英語→日本語と、コメント欄への補足)。**エ** 動画の URL を入れて投稿する\n23. **C** この動画で決めたこと・直したことを、本ファイルの該当の節と更新履歴に書く。iCloud の `_制作ベース` とプロジェクトの `claude/video_rules.md` の両方に保存する\n\n### 前の動画でつまずいた所(先に気をつける)\n\n- ElevenLabs の API キーには「音声の読み上げ・効果音・音楽生成」の3つの許可が要る(音楽生成がないと BGM が作れない)\n- Claude から Mac へ送ったファイルが途中で変わることがある(大きい動画・透明部分のある PNG)。送ったあとは指紋(md5)を比べ、違えば圧縮して送り直す\n- Mac 側のファイルが古い版に戻っていたことがある。書き出しの前に、Mac と Claude の作業環境でスクリプトの指紋がそろっているか確かめる\n</code></pre><h2 id=\"index_B9rTkmMC\">まだ決まっていないこと</h2><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>いまの状態</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ポロロンの使い分け</p></td><td colspan=\"1\" rowspan=\"1\"><p>大きな流れの変わり目7か所すべてで同じポロロンを鳴らしている。場所ごとに音を変えるかは試し中</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ジングルとアウトロの文言</p></td><td colspan=\"1\" rowspan=\"1\"><p>「Pixels to Motors。案内役のハコブくんです。」とエンディングの文は、毎回同じにするか未定</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>字幕ファイルの登録</p></td><td colspan=\"1\" rowspan=\"1\"><p>焼き込んだ字幕と2重になる。登録を残すかは未定</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>HD の版の画質</p></td><td colspan=\"1\" rowspan=\"1\"><p>1080p の版での文字の見え方をまだ見ていない。甘ければ 2560×1440 で書き出し直す</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>チャンネル名の反映</p></td><td colspan=\"1\" rowspan=\"1\"><p>「フィジカルAI解説|Pixels to Motors」が表示されていない。上限に当たったかを確かめる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>動画のハコブくんの体</p></td><td colspan=\"1\" rowspan=\"1\"><p>作り直した体は、サムネイルだけで使っている。動画にも使うなら、hakobu.py の画面の四隅・頭の位置・光の輪の位置を測り直し、表情を一覧で確かめる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1X NEO の配送</p></td><td colspan=\"1\" rowspan=\"1\"><p>「配送開始を示す公式発表が見つからない」は 2026年10月12日に確かめた内容。公式発表が出たら、概要欄を直す</p></td></tr></tbody></table></div><h2 id=\"index_XNI1szIP\">この記事で作った・直したスクリプトの全文</h2><p>動画ごとの作業フォルダ(<code>動画/フィジカルAIの現在地/</code>)のファイルと、どの動画でも使う部品を置く <code>_制作ベース/</code> のファイルです。<code>_制作ベース/</code> のうち、この記事の作業で新しく作ったもの、または直したものを載せています。図とハコブくんの描き方(<code>scene_kit.py</code>・<code>hakobu.py</code>・<code>render.py</code>・<code>subtitle.py</code>)は、LOG331 から変えていないので載せていません。</p><h3 id=\"index_OykTUSdQ\">_制作ベース/timing.py</h3><p>新規。文ごとの尺(コマ数)と、ナレーションのつなぎ方を決める。assemble.py と audio_mix.py が同じ規則を使うために分けた(51行)。</p><pre><code class=\"language-python\">\"\"\"文ごとの尺(フレーム)と、ナレーションの音をつなぐ規則。assemble.py と audio_mix.py が同じものを使う。\"\"\"\nimport json, wave\nimport numpy as np\nfrom subtitle import pages as sub_pages\n\nFPS, SR = 30, 48000\n\n\ndef load(first=None):\n idx = json.load(open('tts/index.json'))\n return idx[:first] if first else idx\n\n\ndef layout(idx):\n \"\"\"各文に frames(間を含む尺)・speech(読み上げの長さ)・start(開始フレーム)・parts(字幕の枚の切り替え)を足す。全体のフレーム数を返す\"\"\"\n t = 0\n for o in idx:\n o['frames'] = max(1, round((o['dur'] + o['pause']) * FPS))\n o['speech'] = round(o['dur'] * FPS)\n o['start'] = t\n t += o['frames']\n # 「|」で分けた字幕は、読み上げの長さを字数で割って切り替える\n pages = sub_pages(o['text']) if o['text'] else []\n cuts, acc, total = [0], 0, sum(len(p) for p in pages)\n for p in pages[:-1]:\n acc += len(p)\n cuts.append(round(o['speech'] * acc / total))\n cuts.append(o['frames'])\n o['parts'] = [(k, cuts[k], cuts[k + 1]) for k in range(len(pages))] or [(None, 0, o['frames'])]\n return t\n\n\ndef narration(idx):\n \"\"\"各文の音声を、尺ちょうどに合わせてつないだ int16 モノラル\"\"\"\n parts = []\n for o in idx:\n need = o['frames'] * SR // FPS\n if o['text']:\n with wave.open(f\"tts/{o['i']:03d}.wav\") as w:\n a = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16)\n else:\n a = np.zeros(0, dtype=np.int16)\n a = a[:need]\n parts.append(np.concatenate([a, np.zeros(need - len(a), dtype=np.int16)]))\n return np.concatenate(parts)\n\n\ndef write_wav(path, a, ch=1):\n \"\"\"a は int16(モノラルは1次元、ステレオは (n, 2))\"\"\"\n with wave.open(path, 'wb') as w:\n w.setnchannels(ch); w.setsampwidth(2); w.setframerate(SR); w.writeframes(np.ascontiguousarray(a).tobytes())\n</code></pre><h3 id=\"index_P6iLbpu7\">_制作ベース/audio_mix.py</h3><p>新規。ナレーションに BGM と効果音を混ぜる。話している間は BGM を下げ、0.8秒以上の間で戻す。曲の終わりを切ってループする(173行)。</p><pre><code class=\"language-python\">\"\"\"ナレーションに BGM と効果音を混ぜる。音の置き方は、動画の作業フォルダの sound.py に書く。\n使い方(動画の作業フォルダで)\n python3 <_制作ベース>/audio_mix.py [--first N] 音だけ先に確かめる → 音の確認.m4a(映像は作らない)\n assemble.py は sound.py があると、これを呼んで mix.wav を使う\n\nsound.py の書き方\n BGM = [dict(file='bgm_仮', start=0, end=None, level=-22, gap=-15, fade_in=0.8, fade_out=2.0, offset=0.0)]\n file 作業フォルダからの相対パス(sound/bgm_main.mp3 など)。見つからなければ <_制作ベース>/sound/{file}.wav\n start 流し始める文の番号。end は止める文の番号(その文は含まない。None は最後まで)\n level 話している間の大きさ。ナレーションより何 dB 小さくするか(-22 なら、ナレーションの約 1/12 の音圧)\n gap 話していない間(0.8 秒以上の間)の大きさ。少し上げて、間が空っぽに聞こえないようにする\n fade_in / fade_out 入りと終わりのフェード(秒)。offset は BGM のどこから流すか(秒)\n 曲の終わりの小さくなっていく所は切り、BGM が区間より短いときは 2 秒重ねてループする\n SE = [(文番号, 文の頭から何秒後, 音, ナレーションより何dB)]\n 音は file と同じ探し方(se_pop など)。例: (2, 0.0, 'se_pop', -8)\n SCENE_SE = dict(file='se_rise', level=-12, offset=-0.15)\n 場面(narration.py の場面の名前)が変わる文ごとに、自動で同じ音を入れる(動画の最初の文は入れない)。\n offset は場面が変わる瞬間から何秒ずらすか(マイナスで少し前から鳴らし、音の山を切り替えに合わせる)\n skip=['場面名', ...] に書いた場面へ切り替わるときは鳴らさない(同じ映像のままテロップだけ替える場面など)\n CHAPTER_SE = dict(file='se_chapter', level=-8, offset=-0.3, scenes=['c1', 'c2', ...])\n scenes に書いた場面(章の始まりなど、大きな流れの変わり目)へ切り替わるときは、SCENE_SE の代わりにこの音を鳴らす\n files={'場面名': '音', ...} を書くと、その場面だけ別の音にする(例: 自己紹介だけキラリン)\n\"\"\"\nimport os, subprocess, sys\nimport numpy as np\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, '.')\nsys.path.insert(1, HERE)\nfrom timing import FPS, SR, load, layout, narration, write_wav\n\nWIN = int(0.05 * SR)\n\n\ndef find(name):\n for p in (name, os.path.join(HERE, 'sound', name), os.path.join(HERE, 'sound', name + '.wav')):\n if os.path.isfile(p):\n return p\n sys.exit(f'音のファイルが見つかりません: {name}(作業フォルダからの相対パスか、_制作ベース/sound/ の名前で書く)')\n\n\ndef read(path):\n \"\"\"どの形式でも 48kHz ステレオの float にする\"\"\"\n raw = subprocess.run(['ffmpeg', '-v', 'error', '-i', path, '-ac', '2', '-ar', str(SR), '-f', 's16le', '-'],\n capture_output=True, check=True).stdout\n return np.frombuffer(raw, dtype=np.int16).reshape(-1, 2).astype(np.float32) / 32768.0\n\n\ndef win_rms(a):\n m = a.mean(axis=1) if a.ndim == 2 else a\n n = len(m) // WIN\n if n == 0:\n return np.array([np.sqrt(np.mean(m ** 2)) + 1e-9])\n return np.sqrt((m[:n * WIN].reshape(n, WIN) ** 2).mean(axis=1)) + 1e-9\n\n\ndef db(x):\n return 10 ** (x / 20)\n\n\ndef cut_tail(a):\n \"\"\"曲の終わりで音が小さくなっていく所(ふつうの大きさより10dB以上小さい所)を切る。ループでつなぐとき、途中で音が消えないようにする\"\"\"\n r = win_rms(a)\n loud = np.where(r > np.median(r[r > 1e-3]) * db(-10))[0]\n if len(loud) == 0:\n return a\n return a[:min(len(a), (loud[-1] + 1) * WIN)]\n\n\ndef loop_to(a, n, xf=SR):\n \"\"\"a を n サンプルまでループで伸ばす(つなぎ目は xf サンプル重ねる)\"\"\"\n if len(a) >= n:\n return a[:n]\n xf = min(xf, len(a) // 4)\n out = a.copy()\n ramp = np.linspace(0, 1, xf)[:, None]\n while len(out) < n:\n head = out[:-xf]\n cross = out[-xf:] * (1 - ramp) + a[:xf] * ramp\n out = np.concatenate([head, cross, a[xf:]])\n return out[:n]\n\n\ndef smooth(x, sec):\n k = max(1, int(sec * 100))\n return np.convolve(np.pad(x, (k, k), mode='edge'), np.ones(k) / k, mode='same')[k:-k]\n\n\ndef mix(idx, pcm, out='mix.wav'):\n ns = {}\n exec(open('sound.py', encoding='utf-8').read(), ns)\n BGM, SE = ns.get('BGM', []), list(ns.get('SE', []))\n sc = ns.get('SCENE_SE')\n ch = ns.get('CHAPTER_SE') or {}\n if sc or ch:\n for prev, o in zip(idx, idx[1:]):\n if o['scene'] == prev['scene']:\n continue\n if o['scene'] in ch.get('scenes', []): # 大きな流れの変わり目は、別の音にする\n f = ch.get('files', {}).get(o['scene'], ch['file']) # 場面ごとに音を替えられる(files={'title': 'se_chapter_glock'})\n SE.append((o['i'], ch.get('offset', -0.3), f, ch.get('level', -8)))\n elif sc and o['scene'] not in sc.get('skip', []):\n SE.append((o['i'], sc.get('offset', -0.15), sc['file'], sc.get('level', -12)))\n voice = pcm.astype(np.float32) / 32768.0\n n = len(voice)\n ref = np.percentile(win_rms(voice), 90) # ナレーションの「ふつうに話している」大きさ\n total = n / SR\n by_i = {o['i']: o for o in idx}\n\n # 話している所(各文の読み上げ)を、前0.15秒・後0.3秒広げる。0.8 秒未満の間は話している扱いのまま(BGM が細かく上下しない)\n ctl = np.zeros(int(total * 100) + 1)\n for o in idx:\n if o['text']:\n a = max(0, int((o['start'] / FPS - 0.15) * 100)); b = int(((o['start'] + o['speech']) / FPS + 0.3) * 100)\n ctl[a:b] = 1\n gaps = np.where(np.diff(np.concatenate([[1], ctl, [1]])) != 0)[0].reshape(-1, 2)\n for a, b in gaps:\n if (b - a) < 50: # 0.5 秒(広げた後)= もとの間で約 0.8〜0.95 秒未満\n ctl[a:b] = 1\n ctl = smooth(ctl, 0.3)\n\n bus = np.zeros((n, 2), dtype=np.float32)\n for m in BGM:\n src = read(find(m['file']))\n src = src[int(m.get('offset', 0) * SR):]\n g = ref / np.median(win_rms(src)[win_rms(src) > 1e-3])\n src = cut_tail(src)\n s0 = by_i[m.get('start', 0)]['start'] * SR // FPS if m.get('start', 0) in by_i else 0\n e = m.get('end')\n e0 = by_i[e]['start'] * SR // FPS if e is not None and e in by_i else n\n seg = loop_to(src, e0 - s0, xf=2 * SR) * g\n # 話している間は level、間は gap(控えめに 100 Hz で決めてから、サンプルに伸ばす)\n t = (np.arange(s0, e0) / SR * 100).astype(int).clip(0, len(ctl) - 1)\n lv = db(m.get('level', -22)) * ctl[t] + db(m.get('gap', -15)) * (1 - ctl[t])\n fi, fo = int(m.get('fade_in', 0.8) * SR), int(m.get('fade_out', 2.0) * SR)\n fade = np.ones(e0 - s0, dtype=np.float32)\n if fi:\n fade[:fi] = np.linspace(0, 1, min(fi, len(fade)))[:len(fade[:fi])]\n if fo:\n fade[-fo:] = np.minimum(fade[-fo:], np.linspace(1, 0, min(fo, len(fade))))\n bus[s0:e0] += seg * (lv * fade)[:, None]\n\n log = []\n for line, off, name, level in SE:\n if line not in by_i:\n continue\n src = read(find(name))\n g = ref / win_rms(src).max() * db(level)\n s0 = max(0, int((by_i[line]['start'] / FPS + off) * SR))\n k = min(len(src), n - s0)\n if k > 0:\n bus[s0:s0 + k] += src[:k] * g\n log.append(f\" {s0 / SR:6.2f}秒 文{line:03d} {name} {level}dB 「{by_i[line]['text'][:16]}」\")\n\n out_a = voice[:, None] + bus\n peak = np.abs(out_a).max()\n if peak > 0.98:\n out_a *= 0.98 / peak # 割れないように全体を下げる(最後の loudnorm で大きさはそろう)\n write_wav(out, (out_a * 32767).astype(np.int16), ch=2)\n print(f'BGM {len(BGM)}本・効果音 {len(log)}個 → {out}')\n print('\\n'.join(log))\n return out\n\n\nif __name__ == '__main__':\n args = sys.argv[1:]\n first = int(args[args.index('--first') + 1]) if '--first' in args else None\n idx = load(first)\n layout(idx)\n pcm = narration(idx)\n f = mix(idx, pcm)\n subprocess.run(['ffmpeg', '-v', 'error', '-y', '-i', f, '-af', 'loudnorm=I=-16:TP=-1.5:LRA=11',\n '-c:a', 'aac', '-b:a', '160k', '-ar', '48000', '音の確認.m4a'], check=True)\n print('音の確認.m4a')\n</code></pre><h3 id=\"index_F7ztL1Qa\">_制作ベース/assemble.py</h3><p>直した。sound.py があれば audio_mix.py を呼ぶ。字幕の後ろの帯をやめた。映像の指定を背景の指紋に入れた。前の場面の映像の続きから流す指定(開始秒 None)を足した(144行)。</p><pre><code class=\"language-python\">\"\"\"1文ずつ背景の映像を作ってつなぎ、ハコブくんを重ねて、音声と合わせる。\n使い方: 動画の作業フォルダで(render.py のあと)\n python3 <_制作ベース>/assemble.py [--first N] [--force] [--name 動画名]\n --first N 最初の N 文だけ書き出す(確かめ用)\n --force 背景の映像を作り直す(ふだんは、できているものは使い回す)\n音: 作業フォルダに sound.py があれば、BGM・効果音を混ぜる(audio_mix.py。音だけ先に確かめるときは audio_mix.py を単独で動かす)\n出力: out.mp4(作業用・crf 20・-16 LUFS)、out_確認用.mp4(crf 29・30MB以下)、out.srt、timeline.json\n\"\"\"\nimport json, subprocess, os, sys, wave, hashlib\nimport numpy as np\nfrom concurrent.futures import ThreadPoolExecutor\nfrom PIL import Image\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, '.')\nsys.path.insert(1, HERE)\nos.environ.setdefault(\"P2M_BASE\", HERE)\nfrom scenes import VIDEO\nfrom hakobu import Hakobu\nfrom subtitle import pages as sub_pages\nfrom timing import FPS, SR, load, layout, narration, write_wav\n\nargs = sys.argv[1:]\nfirst = int(args[args.index('--first') + 1]) if '--first' in args else None\nforce = '--force' in args\nidx = load(first)\nos.makedirs('chunks', exist_ok=True); os.makedirs('comp', exist_ok=True)\nhk = Hakobu()\n\n# ---------- 尺(フレーム単位)と字幕の枚(timing.py。audio_mix.py と同じ規則) ----------\ntotal_frames = layout(idx)\n\n# 実機の映像の場面: 同じ場面が続く間は、続きから再生する。開始秒が None の場面は、前の場面の映像の続きから再生する\n# (同じ映像のまま、テロップだけ替えたいとき。2026-10-12)\nprev, off = None, 0.0\nfor o in idx:\n sc = o['scene']\n if sc in VIDEO:\n if sc != prev and VIDEO[sc][1] is not None:\n off = VIDEO[sc][1]\n o['voff'] = off\n off += o['frames'] / FPS\n prev = sc\n\n# 背景(字幕まで入れて、ハコブくんは入れない)。最後に1回だけ圧縮し直すので、ここは画質を高めに保つ\nENC = ['-c:v', 'libx264', '-preset', 'ultrafast', '-crf', '12', '-pix_fmt', 'yuv420p', '-r', str(FPS), '-an']\njobs = []\nfor o in idx:\n for k, a, b in o['parts']:\n jobs.append((o, k, a, b))\n\n\ndef job(j):\n o, k, a, b = j\n tag = f\"{o['i']:03d}\" + (f\"_{k}\" if k is not None else \"\")\n # 中身(場面・字幕・尺・映像の位置・図と字幕の画像)が変わったら作り直すよう、ファイル名に指紋を入れる\n sub_png = f\"png/sub/{o['i']:03d}_{k}.png\" if k is not None else \"\"\n fp = [o['scene'], o['text'], a, b, round(o.get('voff', 0), 3), VIDEO.get(o['scene'])] # 映像のファイルや透かしの設定が変わっても作り直す\n for f in (f\"png/scene/{o['scene']}.png\", f\"png/ovl/{o['scene']}.png\", sub_png):\n if f and os.path.exists(f):\n fp.append(hashlib.md5(open(f, 'rb').read()).hexdigest())\n out = f\"chunks/{tag}_{hashlib.md5(repr(fp).encode()).hexdigest()[:8]}.mp4\"\n if os.path.exists(out) and not force:\n return out\n sub = f\"png/sub/{o['i']:03d}_{k}.png\" if k is not None else None\n sc, n = o['scene'], str(b - a)\n if sc in VIDEO:\n src, _, wm = VIDEO[sc]\n dl = 'delogo=x=2740:y=1840:w=1010:h=220,' if wm else ''\n ins = ['-ss', f\"{o['voff'] + a / FPS:.3f}\", '-stream_loop', '-1', '-i', src, '-loop', '1', '-i', f'png/ovl/{sc}.png']\n fc = (f\"[0:v]{dl}scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080,fps={FPS},setsar=1[b];\" # 字幕の後ろの黒い帯は敷かない(2026-10-12 エイイチの指示)\n f\"[b][1:v]overlay[c]\")\n if sub:\n ins += ['-loop', '1', '-i', sub]\n fc += \";[c][2:v]overlay[v]\"\n else:\n fc += \";[c]null[v]\"\n cmd = ['ffmpeg', '-v', 'error', '-y'] + ins + ['-filter_complex', fc, '-map', '[v]', '-frames:v', n] + ENC + [out]\n else:\n comp = f\"comp/{tag}.png\"\n im = Image.open(f'png/scene/{sc}.png').convert('RGBA')\n if sub:\n im.alpha_composite(Image.open(sub).convert('RGBA'))\n im.convert('RGB').save(comp)\n cmd = ['ffmpeg', '-v', 'error', '-y', '-loop', '1', '-framerate', str(FPS), '-i', comp, '-frames:v', n,\n '-tune', 'stillimage'] + ENC + [out]\n subprocess.run(cmd, check=True)\n return out\n\n\nwith ThreadPoolExecutor(max(1, min(3, os.cpu_count() or 1))) as ex:\n outs = list(ex.map(job, jobs))\nopen('chunks/list.txt', 'w').write(''.join(f\"file '{os.path.basename(x)}'\\n\" for x in outs))\nsubprocess.run(['ffmpeg', '-v', 'error', '-y', '-f', 'concat', '-safe', '0', '-i', 'chunks/list.txt', '-c', 'copy', 'bg.mp4'], check=True)\n\n# ---------- 音声: 各文を尺ちょうどに合わせて連結。sound.py があれば BGM・効果音を混ぜる(audio_mix.py) ----------\npcm = narration(idx)\nwrite_wav('narration.wav', pcm)\naudio_in = 'narration.wav'\nif os.path.exists('sound.py'):\n from audio_mix import mix\n audio_in = mix(idx, pcm) # mix.wav(ステレオ)。ハコブくんの口はナレーションだけで動かす\n\n# ---------- 字幕 SRT(「|」で分けた枚は別の行にする) ----------\ndef ts(f):\n ms = round(f / FPS * 1000); h, ms = divmod(ms, 3600000); m, ms = divmod(ms, 60000); s, ms = divmod(ms, 1000)\n return f\"{h:02d}:{m:02d}:{s:02d},{ms:03d}\"\nsrt, k = [], 1\nfor o in idx:\n if not o['text']:\n continue\n pages = sub_pages(o['text'])\n for (pk, a, b), ptxt in zip(o['parts'], pages):\n end = min(b, o['speech']) if pk == len(pages) - 1 else b\n srt.append(f\"{k}\\n{ts(o['start'] + a)} --> {ts(o['start'] + end)}\\n{ptxt}\\n\"); k += 1\nopen('out.srt', 'w', encoding='utf-8').write('\\n'.join(srt))\njson.dump(idx, open('timeline.json', 'w'), ensure_ascii=False)\n\n# ---------- ハコブくん: 表情と口・目・リングの動きを決めて、背景に重ねる ----------\ntimeline = [(o['start'], o['start'] + o['frames'], o['start'] + o['speech'], o.get('face', 'base'), bool(o['text'])) for o in idx]\nparams = hk.animate(timeline, pcm.astype(np.float32) / 32768.0)\ncmd = ['ffmpeg', '-v', 'error', '-y', '-i', 'bg.mp4',\n '-f', 'rawvideo', '-pix_fmt', 'rgba', '-s', f'{hk.w}x{hk.h}', '-r', str(FPS), '-i', '-',\n '-i', audio_in,\n '-filter_complex', f'[0:v][1:v]overlay=x={hk.x}:y={hk.y}:eof_action=pass[v]',\n '-map', '[v]', '-map', '2:a', '-c:v', 'libx264', '-preset', 'medium', '-crf', '20', '-pix_fmt', 'yuv420p',\n '-af', 'loudnorm=I=-16:TP=-1.5:LRA=11', '-c:a', 'aac', '-b:a', '192k', '-ar', '48000',\n '-movflags', '+faststart', '-frames:v', str(total_frames), 'out.mp4']\np = subprocess.Popen(cmd, stdin=subprocess.PIPE)\nfor i in range(total_frames):\n p.stdin.write(hk.frame(params[i]).tobytes())\np.stdin.close()\nif p.wait():\n sys.exit('ハコブくんを重ねる書き出しに失敗しました')\nstill = round(total_frames * 0.42)\nsubprocess.run(['ffmpeg', '-v', 'error', '-y', '-ss', f'{still / FPS:.2f}', '-i', 'out.mp4', '-frames:v', '1', 'out_still.png'], check=True)\n\n# ---------- 確認用(チャットで送る方): crf 29・音声 112kbps・30MB 以下 ----------\nfor crf in (29, 32, 35):\n subprocess.run(['ffmpeg', '-v', 'error', '-y', '-i', 'out.mp4', '-c:v', 'libx264', '-preset', 'medium', '-crf', str(crf),\n '-c:a', 'aac', '-b:a', '112k', '-movflags', '+faststart', 'out_確認用.mp4'], check=True)\n mb = os.path.getsize('out_確認用.mp4') / 1e6\n if mb <= 30:\n break\nprint(f'frames {total_frames} = {total_frames / FPS:.1f}s 確認用 {mb:.1f}MB (crf {crf})')\n</code></pre><h3 id=\"index_WrI3e7rF\">_制作ベース/preview.py</h3><p>直した。字幕の後ろの帯をやめ、前の場面の映像の続き(開始秒 None)を、書き出しの記録から読むようにした(54行)。</p><pre><code class=\"language-python\">\"\"\"確かめ用の静止画。動画を書き出さずに、指定した文の画面(図または実機映像の1コマ+字幕+ハコブくん)を1枚ずつ出す。\n使い方: 動画の作業フォルダで(render.py のあと) python3 <_制作ベース>/preview.py 8 9 12\n出力: preview/{文番号}_{字幕の枚}.png と、並べた preview/一覧.png\n\"\"\"\nimport json, os, sys, subprocess\nfrom PIL import Image\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, '.')\nsys.path.insert(1, HERE)\nos.environ.setdefault(\"P2M_BASE\", HERE)\nfrom scenes import VIDEO\nfrom hakobu import Hakobu, EXPR\nfrom subtitle import pages\n\nidx = {o['i']: o for o in json.load(open('tts/index.json'))}\nnums = [int(x) for x in sys.argv[1:]] or [0]\nos.makedirs('preview', exist_ok=True)\nhk = Hakobu()\nouts = []\nfor i in nums:\n o = idx[i]\n sc = o['scene']\n if sc in VIDEO:\n src, start, wm = VIDEO[sc]\n start = o.get('voff', start) if start is None else start # 続きから再生する場面は、書き出しのときの位置(timeline.json)を使う\n if start is None:\n tl = {t['i']: t for t in json.load(open('timeline.json'))} if os.path.exists('timeline.json') else {}\n start = tl.get(i, {}).get('voff', 0.0)\n tmp = f'preview/_v{i}.png'\n subprocess.run(['ffmpeg', '-v', 'error', '-y', '-ss', f'{start:.2f}', '-i', src, '-frames:v', '1',\n '-vf', ('delogo=x=2740:y=1840:w=1010:h=220,' if wm else '') + 'scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080', tmp], check=True)\n bg = Image.open(tmp).convert('RGBA')\n bg.alpha_composite(Image.open(f'png/ovl/{sc}.png').convert('RGBA'))\n else:\n bg = Image.open(f'png/scene/{sc}.png').convert('RGBA')\n ex = EXPR[o.get('face', 'base')]\n face = hk.frame((ex['eyes'], ex['brows'], 'mid', 0.4, 0.0))\n n = len(pages(o['text'])) if o['text'] else 1\n for k in range(n):\n im = bg.copy()\n sub = f'png/sub/{i:03d}_{k}.png'\n if os.path.exists(sub):\n im.alpha_composite(Image.open(sub).convert('RGBA'))\n im.alpha_composite(face, (hk.x, hk.y))\n path = f'preview/{i:03d}_{k}.png'\n im.convert('RGB').save(path)\n outs.append(path)\ncols = 2\nw, h = 960, 540\nsheet = Image.new('RGB', (w * cols, h * ((len(outs) + cols - 1) // cols)), 'white')\nfor k, p in enumerate(outs):\n sheet.paste(Image.open(p).resize((w, h)), ((k % cols) * w, (k // cols) * h))\nsheet.save('preview/一覧.png')\nprint('\\n'.join(outs))\n</code></pre><h3 id=\"index_Y7zJS_Te\">_制作ベース/sound_synth.py</h3><p>新規。効果音(ホワーン・キラリン・ポロロンなど)と、仮の BGM をコードで作る(386行)。</p><pre><code class=\"language-python\">\"\"\"効果音と仮のBGMを、コードで作る(音源サイトに頼らない。だれでも同じ音を作り直せる)。\n使い方: python3 <_制作ベース>/sound_synth.py → <_制作ベース>/sound/ に wav を書く(warm を付けると温かみの BGM、bgm3 を付けると BGM の候補3種を作る)\n se_pop.wav ぽん(図・数字が出る。いちばんよく使う)\n se_tick.wav こつ(小さな切り替え。字幕の枚が変わる・箇条書きが1つ出る)\n se_whoosh.wav しゅっ(章や場面が大きく変わる)\n se_ding.wav ちん(結論・大事な数字)\n se_rise.wav ひゅい(驚き・問いかけ)。エイイチは「ホワーン」と呼ぶ。場面の切り替えに使う\n se_chapter.wav ポロン(章の始まりなど、大きな流れの変わり目。2026-10-12 追加)\n se_chapter_marimba.wav ポロロン(マリンバの上がる3音)。se_chapter の候補\n se_chapter_harp.wav シャララン(ハープ風に弦をはじいて上がる)。se_chapter の候補\n se_chapter_piano.wav ポーン(やわらかいピアノの和音を1つ)。se_chapter の候補\n se_chapter_glock.wav キラリン(鉄琴の高い3音)。se_chapter の候補\n se_chapter_swell.wav フワァ(弦の和音がふくらんで消える)。se_chapter の候補\n se_chapter_wind.wav シャラン(ウィンドチャイム)。se_chapter の候補\n bgm_仮.wav 仮のBGM(電子音。32秒で切れ目なくループ)\n bgm_仮_温かみ.wav 仮のBGM(はじく弦・やわらかいピアノ・弦の和音・ウッドベース風。2026-10-12 エイイチの「もう少し温かみ」の指示で追加)\n ElevenLabs Music などの本番のBGMができるまでの、方向を確かめる用\n bgm_1_ピアノ / bgm_2_ギターとマリンバ / bgm_3_ピッツィカート BGM の候補3種(2026-10-12)\n音の高さは、ナレーション(男声・おおむね100〜300Hz)とぶつからないよう、効果音は 500Hz より上を中心にしている。\n\"\"\"\nimport os, sys\nimport numpy as np\nfrom timing import SR, write_wav\n\nHERE = os.path.dirname(os.path.abspath(__file__))\nOUT = os.path.join(HERE, 'sound')\nrng = np.random.default_rng(7)\n\n\ndef t_(dur):\n return np.arange(int(dur * SR)) / SR\n\n\ndef env(n, attack=0.004, decay=0.15):\n t = np.arange(n) / SR\n return np.minimum(1, t / attack) * np.exp(-t / decay)\n\n\ndef norm(a, peak=0.9):\n return a / (np.abs(a).max() + 1e-9) * peak\n\n\ndef lowpass(a, cutoff):\n \"\"\"1次のローパス(やわらかくするだけ)\"\"\"\n k = np.exp(-2 * np.pi * cutoff / SR)\n out = np.empty_like(a); y = 0.0\n for i, x in enumerate(a):\n y = (1 - k) * x + k * y; out[i] = y\n return out\n\n\ndef pop():\n t = t_(0.12)\n f = 520 + 520 * np.exp(-t / 0.018) # 高い所から素早く下がる\n a = np.sin(2 * np.pi * np.cumsum(f) / SR) * env(len(t), 0.002, 0.035)\n a += 0.25 * np.sin(2 * np.pi * 2 * np.cumsum(f) / SR) * env(len(t), 0.001, 0.015)\n return norm(a)\n\n\ndef tick():\n t = t_(0.05)\n a = np.sin(2 * np.pi * 1800 * t) * env(len(t), 0.0008, 0.008)\n a += 0.4 * lowpass(rng.standard_normal(len(t)), 4000) * env(len(t), 0.0005, 0.004)\n return norm(a, 0.7)\n\n\ndef whoosh():\n d = 0.55; t = t_(d); n = len(t)\n noise = rng.standard_normal(n)\n # 帯域を低→高へ動かす(2つのローパスの差で帯域を作る)\n out = np.empty(n); y1 = y2 = 0.0\n for i in range(n):\n p = i / n\n c1 = 300 + 3200 * p ** 1.5; c2 = c1 * 0.35\n k1 = np.exp(-2 * np.pi * c1 / SR); k2 = np.exp(-2 * np.pi * c2 / SR)\n y1 = (1 - k1) * noise[i] + k1 * y1; y2 = (1 - k2) * noise[i] + k2 * y2\n out[i] = y1 - y2\n shape = np.sin(np.pi * np.minimum(1, t / d)) ** 2 * (t / d) ** 0.4\n return norm(out * shape, 0.8)\n\n\ndef ding():\n t = t_(1.2)\n f0 = 1046.5 # C6\n a = (np.sin(2 * np.pi * f0 * t) * np.exp(-t / 0.45)\n + 0.35 * np.sin(2 * np.pi * f0 * 2.0 * t) * np.exp(-t / 0.2)\n + 0.12 * np.sin(2 * np.pi * f0 * 3.0 * t) * np.exp(-t / 0.08))\n a *= np.minimum(1, t / 0.003)\n return norm(a, 0.8)\n\n\ndef rise():\n t = t_(0.35)\n f = 600 * (2 ** (t / 0.35 * 1.0)) # 1オクターブ上がる\n a = np.sin(2 * np.pi * np.cumsum(f) / SR)\n a *= np.sin(np.pi * t / 0.35) ** 1.5\n return norm(a, 0.7)\n\n\ndef chapter():\n \"\"\"章の変わり目の「ポロン」。やわらかい鐘の音を2つ(ミ→ラ)、少しずらして鳴らす。ホワーンより長く、落ち着いた音\"\"\"\n d = 1.6; t = t_(d); out = np.zeros(len(t))\n for k, (f0, at, g) in enumerate([(659.3, 0.0, 1.0), (880.0, 0.13, 0.85)]):\n tt = np.clip(t - at, 0, None); on = (t >= at)\n a = (np.sin(2 * np.pi * f0 * tt) * np.exp(-tt / 0.55) + 0.3 * np.sin(2 * np.pi * f0 * 2 * tt) * np.exp(-tt / 0.25)\n + 0.1 * np.sin(2 * np.pi * f0 * 3.01 * tt) * np.exp(-tt / 0.1))\n out += g * a * on * np.minimum(1, tt / 0.004)\n return norm(lowpass(out, 6000), 0.8)\n\n\ndef bgm_placeholder():\n \"\"\"やわらかいパッドと、小さな粒の音(アルペジオ)。テンポ 96・4小節×2=32秒・ループで切れ目が出ないよう、はみ出した余韻は頭に回す\"\"\"\n bpm = 96; beat = 60 / bpm; bars = 8; L = int(bars * 4 * beat * SR)\n buf = np.zeros((L, 2))\n chords = [[57, 60, 64, 67], [53, 57, 60, 64], [48, 52, 55, 59], [55, 59, 62, 64]] # Am7 Fmaj7 Cmaj7 G6(MIDI番号)\n hz = lambda m: 440 * 2 ** ((m - 69) / 12)\n\n def add(sig, start, pan=0.0):\n i0 = int(start * SR); idx = (np.arange(len(sig)) + i0) % L\n np.add.at(buf[:, 0], idx, sig * (1 - pan) * 0.5 + sig * 0.5 * (1 - abs(pan)))\n np.add.at(buf[:, 1], idx, sig * (1 + pan) * 0.5 + sig * 0.5 * (1 - abs(pan)))\n\n for b in range(bars):\n ch = chords[b % 4]\n d = 4 * beat\n t = t_(d + 1.5)\n pad_env = np.minimum(1, t / 0.9) * np.where(t < d, 1, np.exp(-(t - d) / 0.6))\n for k, m in enumerate(ch):\n f = hz(m)\n s = sum(np.sin(2 * np.pi * f * (1 + det) * t + ph) for det, ph in ((-0.003, 0), (0.003, 1.3)))\n s += 0.2 * np.sin(2 * np.pi * 2 * f * t)\n add(0.05 * s * pad_env, b * d, pan=(k - 1.5) * 0.25)\n bass = hz(ch[0] - 12)\n tb = t_(d)\n add(0.10 * np.sin(2 * np.pi * bass * tb) * np.minimum(1, tb / 0.05) * np.exp(-tb / 2.5), b * d)\n # 8分音符の粒(上の2音を行き来する)\n for s8 in range(8):\n m = ch[[1, 2, 3, 2, 1, 2, 3, 2][s8]] + 12\n tp = t_(0.6)\n note = np.sin(2 * np.pi * hz(m) * tp) * np.exp(-tp / 0.18) * np.minimum(1, tp / 0.004)\n vel = 0.05 if s8 % 2 == 0 else 0.032\n add(vel * note, b * d + s8 * beat / 2, pan=0.35 if s8 % 2 else -0.35)\n buf[:, 0] = lowpass(buf[:, 0], 5000); buf[:, 1] = lowpass(buf[:, 1], 5000)\n return buf / (np.abs(buf).max() + 1e-9) * 0.8\n\n\ndef pluck(f, dur, bright=0.5):\n \"\"\"はじいた弦(Karplus-Strong)。bright が小さいほど丸い音\"\"\"\n n = int(dur * SR); p = max(2, int(SR / f))\n buf = lowpass(rng.uniform(-1, 1, p), 1500 + 5000 * bright)\n out = np.empty(n)\n for i in range(n):\n out[i] = buf[i % p]\n buf[i % p] = 0.996 * 0.5 * (buf[i % p] + buf[(i + 1) % p])\n return out * np.minimum(1, np.arange(n) / (0.002 * SR))\n\n\ndef felt_piano(f, dur):\n \"\"\"フェルトを挟んだピアノ風(倍音を少し伸ばしてずらし、高い倍音ほど早く消す)\"\"\"\n t = t_(dur)\n a = sum((0.6 ** k) * np.sin(2 * np.pi * f * (k + 1) * (1 + 0.0004 * k * k) * t) * np.exp(-t / (1.8 / (k + 1) ** 0.8))\n for k in range(6))\n return a * np.minimum(1, t / 0.006)\n\n\ndef strings(f, dur):\n \"\"\"弦の和音のパッド(ゆっくり入り、少しビブラート)\"\"\"\n t = t_(dur)\n vib = 1 + 0.003 * np.sin(2 * np.pi * 5.2 * t)\n a = sum(np.sin(2 * np.pi * f * k * vib * t + k) / k ** 1.3 for k in range(1, 7))\n a += 0.6 * sum(np.sin(2 * np.pi * f * 1.002 * k * vib * t) / k ** 1.3 for k in range(1, 7))\n return a * np.minimum(1, t / 1.2) * np.minimum(1, (dur - t) / 0.8)\n\n\ndef bgm_warm():\n \"\"\"テンポ 88・8小節。ギターの分散和音、ピアノの和音、弦、ウッドベース風。ループで切れ目が出ないよう余韻は頭に回す\"\"\"\n bpm = 88; beat = 60 / bpm; bars = 8; L = int(bars * 4 * beat * SR)\n buf = np.zeros((L, 2))\n hz = lambda m: 440 * 2 ** ((m - 69) / 12)\n prog = [[48, 55, 64, 67, 72], [45, 52, 60, 64, 69], [41, 53, 57, 60, 65], [43, 55, 59, 62, 67]] # C Am F G(ギターの押さえ方に近い並び)\n\n def add(sig, start, pan=0.0, gain=1.0):\n i0 = int(start * SR); idx = (np.arange(len(sig)) + i0) % L\n np.add.at(buf[:, 0], idx, sig * gain * (1 - max(pan, 0)))\n np.add.at(buf[:, 1], idx, sig * gain * (1 + min(pan, 0)))\n\n for b in range(bars):\n ch = prog[b % 4]; d = 4 * beat; t0 = b * d\n # ギター: 8分音符の分散和音(指ではじく)\n pat = [0, 2, 3, 4, 1, 3, 2, 4]\n for k, j in enumerate(pat):\n vel = 0.22 if k % 2 == 0 else 0.15\n add(pluck(hz(ch[j]), 1.6, bright=0.35), t0 + k * beat / 2 + rng.uniform(0, 0.008), pan=-0.35, gain=vel)\n # ピアノ: 小節の頭と3拍目に、高い所で和音を置く\n for beat_at, g in ((0, 0.10), (2, 0.06)):\n for m in ch[2:]:\n add(felt_piano(hz(m + 12), 2.4), t0 + beat_at * beat + 0.01, pan=0.3, gain=g)\n # 弦: 和音の3音を長く\n for m in ch[1:4]:\n add(lowpass(strings(hz(m), d + 0.6), 2500), t0, pan=0.0, gain=0.018)\n # ウッドベース風: 1拍目と3拍目\n for beat_at, m in ((0, ch[0] - 12), (2, ch[0] - 12 + (7 if b % 2 else 0))):\n add(pluck(hz(m), 1.2, bright=0.1), t0 + beat_at * beat, gain=0.5)\n buf[:, 0] = lowpass(buf[:, 0], 6000); buf[:, 1] = lowpass(buf[:, 1], 6000)\n return buf / (np.abs(buf).max() + 1e-9) * 0.8\n\n# ---------- BGM の候補(2026-10-12 エイイチの指示で3種。どれも電子音を避け、楽器に近い音で組む) ----------\nHZ = lambda m: 440 * 2 ** ((m - 69) / 12)\n\n\nclass Loop:\n \"\"\"小節数ぶんの輪(ループ)の入れ物。はみ出した余韻は頭に回すので、つなぎ目が出ない\"\"\"\n def __init__(self, bpm, bars):\n self.beat = 60 / bpm; self.bar = 4 * self.beat\n self.L = int(bars * self.bar * SR); self.buf = np.zeros((self.L, 2))\n\n def add(self, sig, start, pan=0.0, gain=1.0):\n i0 = int(start * SR); idx = (np.arange(len(sig)) + i0) % self.L\n np.add.at(self.buf[:, 0], idx, sig * gain * (1 - max(pan, 0)))\n np.add.at(self.buf[:, 1], idx, sig * gain * (1 + min(pan, 0)))\n\n def out(self, cutoff=6500):\n b = self.buf\n b[:, 0] = lowpass(b[:, 0], cutoff); b[:, 1] = lowpass(b[:, 1], cutoff)\n return b / (np.abs(b).max() + 1e-9) * 0.8\n\n\ndef marimba(f, dur=1.0):\n \"\"\"木の鍵盤(マリンバ)。倍音が 1・3.9・9.2 倍にあり、すぐ消える\"\"\"\n t = t_(dur)\n a = (np.sin(2 * np.pi * f * t) * np.exp(-t / 0.35) + 0.25 * np.sin(2 * np.pi * f * 3.9 * t) * np.exp(-t / 0.06)\n + 0.08 * np.sin(2 * np.pi * f * 9.2 * t) * np.exp(-t / 0.02))\n return a * np.minimum(1, t / 0.002)\n\n\ndef pizz(f, dur=0.7):\n \"\"\"弦をはじく(ピッツィカート)。ギターより短く丸い\"\"\"\n a = pluck(f, dur, bright=0.2)\n return a * np.exp(-t_(dur)[:len(a)] / 0.18)\n\n\ndef shaker(dur=0.09):\n t = t_(dur)\n n = rng.standard_normal(len(t))\n return (n - lowpass(n, 5000)) * np.sin(np.pi * t / dur) ** 2\n\n\ndef bgm_piano():\n \"\"\"1. ピアノ中心・ゆったり(テンポ76)。ピアノの分散和音に弦がうすく重なる。考える場面向き。\n 分散和音はナレーションの声の高さ(男声 100〜300Hz)とぶつからないよう、1オクターブ上で弾く\"\"\"\n lp = Loop(76, 8)\n prog = [[41, 53, 57, 60, 64], [40, 52, 55, 59, 62], [38, 50, 53, 57, 60], [36, 48, 52, 55, 59]] # Fmaj7 Em7 Dm7 Cmaj7\n mel = [72, None, 71, None, 69, None, 67, None] # 小節の頭にだけ置く、ゆっくり下がる音\n for b in range(8):\n ch = prog[b % 4]; t0 = b * lp.bar\n for k, j in enumerate([1, 2, 3, 4, 3, 2, 3, 4]):\n lp.add(felt_piano(HZ(ch[j] + 12), 2.6), t0 + k * lp.beat / 2 + rng.uniform(0, 0.01), pan=0.15, gain=0.09 if k % 2 == 0 else 0.065)\n lp.add(felt_piano(HZ(ch[0]), 3.5), t0, gain=0.12)\n if mel[b]:\n lp.add(felt_piano(HZ(mel[b] + 12), 3.0), t0 + 0.02, pan=-0.2, gain=0.06)\n for m in ch[1:4]:\n lp.add(lowpass(strings(HZ(m), lp.bar + 0.8), 2200), t0, gain=0.014)\n return lp.out(6000)\n\n\ndef bgm_guitar_marimba():\n \"\"\"2. ギターとマリンバ・明るめ(テンポ96)。ギターの刻みに、木の鍵盤の短いフレーズ。フック・導入向き\"\"\"\n lp = Loop(96, 8)\n prog = [[48, 55, 64, 67, 72], [47, 55, 62, 67, 71], [45, 52, 60, 64, 69], [41, 53, 57, 60, 65]] # C G/B Am F\n riff = [(0, 0), (0.75, 2), (1.5, 1), (2.5, 2), (3, 3)] # (拍, 和音の何番目) マリンバ\n for b in range(8):\n ch = prog[b % 4]; t0 = b * lp.bar\n for k, j in enumerate([0, 2, 3, 4, 2, 3, 4, 3]):\n lp.add(pluck(HZ(ch[j]), 1.4, bright=0.4), t0 + k * lp.beat / 2 + rng.uniform(0, 0.006), pan=-0.4, gain=0.2 if k % 2 == 0 else 0.13)\n if b % 2 == 1:\n for at, j in riff:\n lp.add(marimba(HZ(ch[2:][j % 3] + 12)), t0 + at * lp.beat, pan=0.4, gain=0.16)\n for at in (0, 2):\n lp.add(pluck(HZ(ch[0] - 12), 1.1, bright=0.1), t0 + at * lp.beat, gain=0.5)\n for k in range(8):\n lp.add(shaker(), t0 + k * lp.beat / 2 + lp.beat / 4, pan=0.2, gain=0.05 if k % 2 else 0.03)\n return lp.out(4000) # はじく音は高い成分が多いので、丸めて温かくする\n\n\ndef bgm_pizzicato():\n \"\"\"3. 弦のピッツィカートとピアノ・少し先が気になる感じ(テンポ104)。ドキュメンタリーの解説向き\"\"\"\n lp = Loop(104, 8)\n prog = [[45, 57, 60, 64], [41, 57, 60, 65], [36, 55, 60, 64], [43, 55, 59, 62]] # Am F C G\n for b in range(8):\n ch = prog[b % 4]; t0 = b * lp.bar\n for k, j in enumerate([1, 2, 3, 2, 1, 2, 3, 2]):\n lp.add(pizz(HZ(ch[j])), t0 + k * lp.beat / 2, pan=-0.3 if k % 2 else 0.3, gain=0.22 if k % 2 == 0 else 0.15)\n for at, m in ((0, ch[0] - 12), (1.5, ch[0] - 12), (2, ch[0] - 5)):\n lp.add(pizz(HZ(m), 0.9), t0 + at * lp.beat, gain=0.45)\n for m in ch[1:]:\n lp.add(felt_piano(HZ(m + 12), 2.2), t0 + 0.01, pan=0.1, gain=0.05)\n if b >= 4: # 後半だけ弦の和音が入り、少しふくらむ\n for m in ch[1:]:\n lp.add(lowpass(strings(HZ(m), lp.bar + 0.6), 2400), t0, gain=0.012)\n return lp.out(3800)\n\n\nBGMS = {'bgm_1_ピアノ': bgm_piano, 'bgm_2_ギターとマリンバ': bgm_guitar_marimba, 'bgm_3_ピッツィカート': bgm_pizzicato}\n\n\n# ---------- 大きな流れの変わり目の音の候補(2026-10-12 エイイチの指示で3つ) ----------\ndef chapter_marimba():\n \"\"\"ポロロン: マリンバで ド・ミ・ソ と上がる\"\"\"\n out = np.zeros(int(1.6 * SR))\n for k, f in enumerate([523.3, 659.3, 784.0]):\n a = marimba(f, 1.2) * (0.8 if k < 2 else 1.0)\n i = int(k * 0.09 * SR); out[i:i + len(a)] += a[:len(out) - i]\n return norm(lowpass(out, 7000), 0.8)\n\n\ndef chapter_harp():\n \"\"\"シャララン: 弦をはじく音を6つ、ハープのように素早く上がる\"\"\"\n out = np.zeros(int(2.0 * SR))\n for k, m in enumerate([60, 64, 67, 72, 76, 79]):\n a = pluck(440 * 2 ** ((m - 69) / 12), 1.6, bright=0.3) * (0.6 + 0.08 * k)\n i = int(k * 0.055 * SR); out[i:i + len(a)] += a[:len(out) - i]\n return norm(lowpass(out, 6000), 0.8)\n\n\ndef chapter_piano():\n \"\"\"ポーン: やわらかいピアノで ド・ソ・ミ・ソ の和音を1つ(低い音から少しずつずらして弾く)\"\"\"\n out = np.zeros(int(2.2 * SR))\n for k, m in enumerate([48, 55, 64, 67]):\n a = felt_piano(440 * 2 ** ((m - 69) / 12), 2.2) * (1.0 if k == 0 else 0.7)\n i = int(k * 0.025 * SR); out[i:i + len(a)] += a[:len(out) - i]\n return norm(lowpass(out, 5000), 0.8)\n\n\nCHAPTER_CANDS = {'se_chapter_marimba': chapter_marimba, 'se_chapter_harp': chapter_harp, 'se_chapter_piano': chapter_piano}\n\n\ndef chapter_glock():\n \"\"\"キラリン: 鉄琴(グロッケン)で ミ・ソ・ド と上がる。倍音は 1・2.76・5.4 倍(金属の棒の音)\"\"\"\n out = np.zeros(int(1.8 * SR)); t = t_(1.6)\n for k, f in enumerate([1318.5, 1568.0, 2093.0]):\n a = (np.sin(2 * np.pi * f * t) * np.exp(-t / 0.7) + 0.25 * np.sin(2 * np.pi * f * 2.76 * t) * np.exp(-t / 0.15)\n + 0.08 * np.sin(2 * np.pi * f * 5.4 * t) * np.exp(-t / 0.05)) * np.minimum(1, t / 0.001)\n i = int(k * 0.08 * SR); out[i:i + len(a)] += a[:len(out) - i] * (0.7 + 0.15 * k)\n return norm(lowpass(out, 9000), 0.7)\n\n\ndef chapter_swell():\n \"\"\"フワァ: 弦の和音(ド・ミ・ソ・ド)が0.8秒でふくらみ、1秒で消える\"\"\"\n d = 1.9; t = t_(d)\n a = sum(lowpass(strings(440 * 2 ** ((m - 69) / 12), d), 3000) for m in [60, 64, 67, 72])\n shape = np.where(t < 0.8, np.sin(np.pi / 2 * t / 0.8) ** 2, np.exp(-(t - 0.8) / 0.35))\n return norm(a * shape, 0.8)\n\n\ndef chapter_wind():\n \"\"\"シャラン: 細い金属の筒(ウィンドチャイム)が、0.5秒の間に9回ふれあう\"\"\"\n out = np.zeros(int(2.0 * SR)); t = t_(1.4); r = np.random.default_rng(11)\n scale = [1760.0, 1975.5, 2217.5, 2637.0, 2960.0, 3520.0]\n for k in range(9):\n f = scale[r.integers(len(scale))]\n a = (np.sin(2 * np.pi * f * t) + 0.3 * np.sin(2 * np.pi * f * 2.7 * t) * np.exp(-t / 0.2)) * np.exp(-t / 0.6) * np.minimum(1, t / 0.001)\n i = int((k * 0.055 + r.uniform(0, 0.02)) * SR); out[i:i + len(a)] += a[:len(out) - i] * r.uniform(0.5, 1.0)\n return norm(lowpass(out, 10000), 0.6)\n\n\nCHAPTER_CANDS.update({'se_chapter_glock': chapter_glock, 'se_chapter_swell': chapter_swell, 'se_chapter_wind': chapter_wind})\n\n\nif __name__ == '__main__':\n os.makedirs(OUT, exist_ok=True)\n for name, f in [('se_pop', pop), ('se_tick', tick), ('se_whoosh', whoosh), ('se_ding', ding), ('se_rise', rise), ('se_chapter', chapter)]:\n write_wav(os.path.join(OUT, f'{name}.wav'), (f() * 32767).astype(np.int16))\n print(name)\n for name, f in CHAPTER_CANDS.items():\n write_wav(os.path.join(OUT, f'{name}.wav'), (f() * 32767).astype(np.int16))\n print(name)\n if 'bgm3' in sys.argv:\n for name, f in BGMS.items():\n write_wav(os.path.join(OUT, f'{name}.wav'), (f() * 32767).astype(np.int16), ch=2)\n print(name)\n sys.exit()\n if 'warm' not in sys.argv:\n write_wav(os.path.join(OUT, 'bgm_仮.wav'), (bgm_placeholder() * 32767).astype(np.int16), ch=2)\n print('bgm_仮')\n write_wav(os.path.join(OUT, 'bgm_仮_温かみ.wav'), (bgm_warm() * 32767).astype(np.int16), ch=2)\n print('bgm_仮_温かみ')\n</code></pre><h3 id=\"index_0alA5Ix0\">_制作ベース/sound_eleven.py</h3><p>新規。ElevenLabs で BGM と効果音を作る。API キーを使うので Mac で動かす(71行)。</p><pre><code class=\"language-python\">\"\"\"BGM と効果音を ElevenLabs で作る。エイイチの Mac で動かす(API キーを Claude に渡さないため。tts_eleven.py と同じ)。\n\n準備: ターミナルで read -s ELEVENLABS_API_KEY && export ELEVENLABS_API_KEY\n (キーを貼って Enter。画面には出ない)\n\n使い方(動画の作業フォルダで)\n BGM: python3 ../_制作ベース/sound_eleven.py music --name bgm_hook --sec 60 --prompt \"(説明文)\"\n 効果音: python3 ../_制作ベース/sound_eleven.py sfx --name se_pop2 --sec 0.5 --prompt \"(説明文)\"\n --name 保存する名前(sound/{名前}.mp3 になる。sound.py の file にこの名前のパスを書く)\n --sec 長さ(秒)。BGM は 3〜600、効果音は 0.5〜30\n --prompt どんな音か(英語の方が安定する。説明文の候補は 音源_候補.md)\n --count 同じ説明文で何本作るか(既定 1。2 以上だと bgm_hook_1.mp3, bgm_hook_2.mp3…)\n --loop 効果音を切れ目なくループする音にする(環境音など)\nBGM は歌なし(force_instrumental)で作る。\nStarter プランの決まり(2026-10-12 に ElevenLabs の Eleven Music の規約で確認):\n 商用に使える(映画・テレビ・ラジオ・ゲームの一部を除く)。クレジット表記はいらない。\n 作れる曲の長さは月 17 分まで。曲そのものを音楽配信サービスに出すのは禁止(動画の BGM に使うのはこれに当たらない)\n有料プランの間に作った音は、解約したあとも商用に使える(無料プランに戻ってから作った音は使えない)\n\"\"\"\nimport argparse, json, os, sys, time, urllib.request, urllib.error\n\nap = argparse.ArgumentParser()\nap.add_argument('kind', choices=['music', 'sfx'])\nap.add_argument('--name', required=True)\nap.add_argument('--sec', type=float, required=True)\nap.add_argument('--prompt', required=True)\nap.add_argument('--count', type=int, default=1)\nap.add_argument('--loop', action='store_true')\nap.add_argument('--model', default='')\nargs = ap.parse_args()\nkey = os.environ.get('ELEVENLABS_API_KEY')\nif not key:\n sys.exit('ELEVENLABS_API_KEY が入っていません。ターミナルで read -s ELEVENLABS_API_KEY && export ELEVENLABS_API_KEY を実行してから動かしてください')\n\nos.makedirs('sound', exist_ok=True)\nif args.kind == 'music':\n if not 3 <= args.sec <= 600:\n sys.exit('BGM の長さは 3〜600 秒')\n url = 'https://api.elevenlabs.io/v1/music?output_format=mp3_44100_128'\n body = {'prompt': args.prompt, 'music_length_ms': int(args.sec * 1000), 'force_instrumental': True}\nelse:\n if not 0.5 <= args.sec <= 30:\n sys.exit('効果音の長さは 0.5〜30 秒')\n url = 'https://api.elevenlabs.io/v1/sound-generation?output_format=mp3_44100_128'\n body = {'text': args.prompt, 'duration_seconds': args.sec, 'prompt_influence': 0.5}\n if args.loop:\n body['loop'] = True\nif args.model:\n body['model_id'] = args.model\n\nlog = os.path.join('sound', '作った記録.jsonl')\nfor k in range(args.count):\n name = args.name if args.count == 1 else f'{args.name}_{k + 1}'\n path = os.path.join('sound', f'{name}.mp3')\n if os.path.exists(path):\n sys.exit(f'{path} はもうあります(上書きしない)。--name を変えてください')\n req = urllib.request.Request(url, data=json.dumps(body).encode(),\n headers={'xi-api-key': key, 'Content-Type': 'application/json'})\n try:\n with urllib.request.urlopen(req, timeout=600) as r:\n open(path, 'wb').write(r.read())\n extra = {h: r.headers.get(h) for h in ('song-id', 'character-cost') if r.headers.get(h)}\n except urllib.error.HTTPError as e:\n sys.exit(f'{name} で止まりました: {e.code} {e.read().decode(errors=\"ignore\")[:300]}')\n except urllib.error.URLError as e:\n sys.exit(f'{name} で止まりました(ElevenLabs につながらない): {e.reason}')\n with open(log, 'a', encoding='utf-8') as f:\n f.write(json.dumps(dict(file=path, kind=args.kind, sec=args.sec, prompt=args.prompt, loop=args.loop,\n model=args.model or '既定', time=time.strftime('%Y-%m-%d %H:%M'), **extra),\n ensure_ascii=False) + '\\n')\n print(f'できあがり: {path}')\n</code></pre><h3 id=\"index_ZuLtrq6q\">_制作ベース/tts_eleven.py</h3><p>直した。読みを確かめる試しの文だけを作る --try を足した。Mac で動かす(114行)。</p><pre><code class=\"language-python\">\"\"\"ハコブくんの声(ElevenLabs)で、台本を1文ずつ音声にする。エイイチの Mac で動かす(API キーを Claude に渡さないため)。\n\n準備(1回だけ)\n 1. ElevenLabs の有料プランに入る(無料プランの音声は商用に使えない)\n 2. Voice Design で声を作って保存し、その声の Voice ID を控える(Voices の一覧で声を開くと出る)\n 3. ターミナルで API キーを入れる(キーは画面やファイルに残さない): export ELEVENLABS_API_KEY=(キー)\n\n使い方(動画の作業フォルダで。例: 動画/AIエージェントの取り違え/)\n python3 ../_制作ベース/tts_eleven.py --voice VOICE_ID --name 候補A --lines 0-12\n --lines 作る文の番号(0 から。「0-12」「5,8,9」のように書く。省略するとすべて)\n --model eleven_v4(既定。2026-10-12 に決定)/eleven_multilingual_v2/eleven_v3\n --seed 同じ値なら、なるべく同じ読み方になる(既定 7)\n --try 台本ではなく、試しの文を作る(「|」で区切って何文でも。例: --try \"エイイチは…。|栄一は…。\")\n 出力は voice_eleven/{名前}/試し_01.mp3 … と、どの文かの一覧 試し.txt\n出力: voice_eleven/{名前}/{番号}.mp3 と、作った設定の記録 voice_eleven/{名前}/設定.json\nこのあと Claude が tts_import.py で 48kHz の wav にして、口の動きと字幕に使う。\n使う文字数(クレジット)は、文の字数の合計。1本約4,100字。作り直した分も数える\n\"\"\"\nimport argparse, json, os, sys, time, urllib.request, urllib.error\nsys.path.insert(0, '.')\nfrom narration import SEGS\ntry:\n from narration import parse\nexcept ImportError:\n def parse(s):\n return s[0], \"base\", s[1], (s[2] if len(s) > 2 else 0.35)\ntry:\n from narration import ELEVEN_READ # ElevenLabs が読み間違えた語だけ、ここで読みを直す(字幕は変えない)\nexcept ImportError:\n ELEVEN_READ = []\n\nap = argparse.ArgumentParser()\nap.add_argument('--voice', required=True)\nap.add_argument('--name', required=True)\nap.add_argument('--lines', default='')\nap.add_argument('--model', default='eleven_v4') # 2026-10-12 エイイチが v4 に決めた(冒頭14文で v2 と聞き比べ)\nap.add_argument('--seed', type=int, default=7)\nap.add_argument('--stability', type=float, default=0.55)\nap.add_argument('--similarity', type=float, default=0.75)\nap.add_argument('--try', dest='trial', default='')\nargs = ap.parse_args()\nkey = os.environ.get('ELEVENLABS_API_KEY')\nif not key:\n sys.exit('ELEVENLABS_API_KEY が入っていません。ターミナルで export ELEVENLABS_API_KEY=(キー) を実行してから動かしてください')\n\n\ndef nums(spec, n):\n if not spec:\n return list(range(n))\n out = []\n for part in spec.split(','):\n if '-' in part:\n a, b = part.split('-'); out += list(range(int(a), int(b) + 1))\n else:\n out.append(int(part))\n return [i for i in out if 0 <= i < n]\n\n\ndef speak_text(t):\n t = t.replace('|', '').replace('「', '').replace('」', '')\n for a, b in ELEVEN_READ:\n t = t.replace(a, b)\n return t\n\n\ntexts = [parse(s)[2] for s in SEGS]\noutdir = os.path.join('voice_eleven', args.name)\nif args.trial: # 試しの文だけを作る(台本の読みの直し ELEVEN_READ は使わない。書いたとおりに読ませて聞き比べる)\n os.makedirs(outdir, exist_ok=True)\n lines = [t for t in args.trial.split('|') if t]\n with open(os.path.join(outdir, '試し.txt'), 'w', encoding='utf-8') as f:\n for k, t in enumerate(lines, 1):\n f.write(f'試し_{k:02d}: {t}\\n')\n for k, t in enumerate(lines, 1):\n body = {'text': t, 'model_id': args.model, 'seed': args.seed,\n 'voice_settings': {'stability': args.stability, 'similarity_boost': args.similarity}}\n req = urllib.request.Request(\n f'https://api.elevenlabs.io/v1/text-to-speech/{args.voice}?output_format=mp3_44100_128',\n data=json.dumps(body).encode(), headers={'xi-api-key': key, 'Content-Type': 'application/json'})\n try:\n with urllib.request.urlopen(req, timeout=120) as r:\n open(os.path.join(outdir, f'試し_{k:02d}.mp3'), 'wb').write(r.read())\n except urllib.error.HTTPError as e:\n sys.exit(f'試し_{k:02d} で止まりました: {e.code} {e.read().decode(errors=\"ignore\")[:300]}')\n print(f'試し_{k:02d} {t}')\n print(f'できあがり: {outdir} 使った文字数 約{sum(len(t) for t in lines)}字')\n sys.exit(0)\nos.makedirs(outdir, exist_ok=True)\njson.dump(dict(vars(args), time=time.strftime('%Y-%m-%d %H:%M')), open(os.path.join(outdir, '設定.json'), 'w'),\n ensure_ascii=False, indent=1)\nused = 0\nfor i in nums(args.lines, len(SEGS)):\n if not texts[i]:\n continue\n prev = next((speak_text(texts[j]) for j in range(i - 1, -1, -1) if texts[j]), None)\n nxt = next((speak_text(texts[j]) for j in range(i + 1, len(texts)) if texts[j]), None)\n body = {'text': speak_text(texts[i]), 'model_id': args.model, 'seed': args.seed,\n 'voice_settings': {'stability': args.stability, 'similarity_boost': args.similarity}}\n if args.model.startswith('eleven_multilingual'): # 前後の文を渡すと、文のつながりの抑揚が自然になる(対応を確かめたモデルだけ)\n body['previous_text'] = prev\n body['next_text'] = nxt\n req = urllib.request.Request(\n f'https://api.elevenlabs.io/v1/text-to-speech/{args.voice}?output_format=mp3_44100_128',\n data=json.dumps(body).encode(), headers={'xi-api-key': key, 'Content-Type': 'application/json'})\n try:\n with urllib.request.urlopen(req, timeout=120) as r:\n open(os.path.join(outdir, f'{i:03d}.mp3'), 'wb').write(r.read())\n except urllib.error.HTTPError as e:\n sys.exit(f'{i:03d} で止まりました: {e.code} {e.read().decode(errors=\"ignore\")[:300]}')\n except urllib.error.URLError as e:\n sys.exit(f'{i:03d} で止まりました(ElevenLabs につながらない): {e.reason}')\n used += len(body['text'])\n print(f'{i:03d} {body[\"text\"][:30]}')\nprint(f'できあがり: {outdir} 使った文字数 約{used}字')\n</code></pre><h3 id=\"index_HLGEVABj\">_制作ベース/tts_import.py</h3><p>ElevenLabs の mp3 を、書き出し用の 48kHz の wav にする(44行)。</p><pre><code class=\"language-python\">\"\"\"ElevenLabs などで作った1文ずつの音声を、書き出しで使う形(tts/{番号}.wav・48kHz モノラル と tts/index.json)にする。\n使い方: 動画の作業フォルダで python3 <_制作ベース>/tts_import.py voice_eleven/候補A\n- その文の音声がないときは、いまの tts/ の音声(仮音声)をそのまま使う。声の聞き比べで、一部の文だけ差し替えられる\n- 文の前後の無音は 0.05 秒を残して切る(間は narration.py の「後ろの間」で決めるため)\n\"\"\"\nimport json, os, subprocess, sys, wave\nimport numpy as np\nsys.path.insert(0, '.')\nfrom narration import SEGS\ntry:\n from narration import parse\nexcept ImportError:\n def parse(s):\n return s[0], \"base\", s[1], (s[2] if len(s) > 2 else 0.35)\n\nSR = 48000\nsrc = sys.argv[1]\nos.makedirs('tts', exist_ok=True)\nold = {o['i']: o for o in json.load(open('tts/index.json'))} if os.path.exists('tts/index.json') else {}\nout, replaced = [], 0\nfor i, s in enumerate(SEGS):\n scene, face, text, pause = parse(s)\n mp3 = os.path.join(src, f'{i:03d}.mp3')\n d, note = 0.0, ''\n if text and os.path.exists(mp3):\n raw = subprocess.run(['ffmpeg', '-v', 'error', '-i', mp3, '-ac', '1', '-ar', str(SR), '-f', 's16le', '-'],\n capture_output=True, check=True).stdout\n a = np.frombuffer(raw, dtype=np.int16)\n loud = np.where(np.abs(a) > 300)[0]\n if len(loud):\n pad = int(0.05 * SR)\n a = a[max(0, loud[0] - pad):min(len(a), loud[-1] + pad)]\n with wave.open(f'tts/{i:03d}.wav', 'wb') as w:\n w.setnchannels(1); w.setsampwidth(2); w.setframerate(SR); w.writeframes(a.tobytes())\n d, note = len(a) / SR, f'voice:{os.path.basename(src.rstrip(\"/\"))}'\n replaced += 1\n elif text and i in old and old[i]['text'] == text:\n d, note = old[i]['dur'], old[i].get('note', '')\n elif text:\n sys.exit(f'{i:03d} の音声がありません({mp3} も、同じ文の仮音声もない)。tts2.py で仮音声を作ってから取り込む')\n out.append(dict(i=i, scene=scene, face=face, text=text, dur=d, pause=pause, note=note))\njson.dump(out, open('tts/index.json', 'w'), ensure_ascii=False, indent=0)\ntot = sum(o['dur'] + o['pause'] for o in out)\nprint(f'差し替えた文 {replaced} 全体 {int(tot // 60)}:{int(tot % 60):02d}')\n</code></pre><h3 id=\"index_zP2G4G0g\">_制作ベース/description.py</h3><p>新規。概要欄の下書きの {t:文番号} を時間に置きかえ、目次の条件を確かめる(40行)。</p><pre><code class=\"language-python\">\"\"\"YouTube の概要欄を、書き出しの時間に合わせて仕上げる。\n使い方(動画の作業フォルダで、assemble.py のあと)\n python3 <_制作ベース>/description.py (--draft を付けると、【要記入】が残っていても 概要欄_確認用.txt に書く)\n読むもの: 概要欄_下書き.md(目次の時間を {t:文番号} と書く)と timeline.json(assemble.py が書く)\n書くもの: 概要欄.txt(YouTube にそのまま貼る。{t:9} が、9番の文が始まる時間「0:56」などに置きかわる)\n- YouTube の目次(チャプター)は、最初が 0:00 で、3つ以上・それぞれ10秒以上が条件。満たさないときは止まる\n- 概要欄は 5,000 字まで\n- 「【要記入:…】」が残っていると止まる(連絡先や SNS の URL など、エイイチが決めて書く所)\n\"\"\"\nimport json, re, sys\n\nFPS = 30\ntl = {o['i']: o for o in json.load(open('timeline.json'))}\nsrc = open('概要欄_下書き.md', encoding='utf-8').read()\n# 「<!-- -->」で囲んだメモは貼らない\nsrc = re.sub(r'<!--.*?-->\\n?', '', src, flags=re.S)\n\n\ndef ts(i):\n if i not in tl:\n sys.exit(f'{i}番の文が timeline.json にありません(全体を書き出してから動かす)')\n s = int(tl[i]['start'] / FPS)\n return f'{s // 3600}:{s % 3600 // 60:02d}:{s % 60:02d}' if s >= 3600 else f'{s // 60}:{s % 60:02d}'\n\n\nout = re.sub(r'\\{t:(\\d+)\\}', lambda m: ts(int(m.group(1))), src).strip() + '\\n'\n# 目次の条件を確かめる\ntimes = [t for t in re.findall(r'^(\\d+:\\d{2}(?::\\d{2})?) ', out, flags=re.M)]\nsecs = [sum(int(x) * 60 ** k for k, x in enumerate(reversed(t.split(':')))) for t in times]\nif times:\n if secs[0] != 0 or len(secs) < 3 or any(b - a < 10 for a, b in zip(secs, secs[1:])):\n sys.exit(f'目次が YouTube の条件を満たしていません: {times}')\ndraft = '--draft' in sys.argv # 「【要記入】」が残っていても、確かめ用に 概要欄_確認用.txt を書く\nif '【' in out and '要記入' in out and not draft:\n sys.exit('「【要記入】」が残っています: ' + ', '.join(re.findall(r'【要記入[^】]*】', out)))\nif len(out) > 5000:\n sys.exit(f'概要欄が {len(out)} 字あります(5,000 字まで)')\nname = '概要欄_確認用.txt' if draft else '概要欄.txt'\nopen(name, 'w', encoding='utf-8').write(out)\nprint(f'{name} を書きました({len(out)} 字・目次 {len(times)} 個)')\n</code></pre><h3 id=\"index_3UslsPCD\">_制作ベース/thumbnail.py</h3><p>新規。サムネイルを動画と同じ文字と色で、HTML で組んで画像にする(214行)。</p><pre><code class=\"language-python\">\"\"\"YouTube のサムネイルを、動画と同じ文字・色(scene_kit.py)で作る。文字は画像生成 AI に任せず、ここで組む(文字の間違いが起きない)。\n使い方(動画の作業フォルダで)\n python3 <_制作ベース>/thumbnail.py\n読むもの: thumb.py(文言・背景の写真・案の指定)\n書くもの: thumb/{案の名前}.png(1280×720。YouTube の上限 2MB 以下)と thumb/一覧.png(並べて比べる用・スマホの小さい表示の大きさも添える)\n\nthumb.py の書き方\n PHOTO = 'サムネ素材/素材2_実機のアーム.png' # 背景の写真(1920×1080 以上)\n HAKOBU = True # 左下にハコブくんを置く\n LABEL = '本当に?' # ハコブくんの横の小さな札\n KICKER = '人型ロボット出荷 約4倍' # 上の小さな一言(オレンジ)\n LINES = ['ロボットにも', '<span class=\"red\">ChatGPTの瞬間</span>', 'もう来た?']\n VARIANTS = ['split', 'dark', 'bold'] # 作る案(split 紙色と写真/dark 黒と写真/bold 大きなハコブくんと縁取り文字)\n BOLD_LINES = ['人型ロボット出荷 約4倍', 'ChatGPTの瞬間', 'もう来た?'] # bold 案の3行(なければ KICKER と LINES)\n\"\"\"\nimport base64, os, sys, subprocess\nsys.path.insert(0, '.')\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(1, HERE)\nfrom scene_kit import BG, INK, RED, SUB, LOGO\nfrom PIL import Image\nfrom playwright.sync_api import sync_playwright\n\nns = {}\nexec(open('thumb.py', encoding='utf-8').read(), ns)\nos.makedirs('thumb', exist_ok=True)\n\n\ndef data_uri(path):\n ext = os.path.splitext(path)[1].lower().lstrip('.')\n return f\"data:image/{'jpeg' if ext in ('jpg', 'jpeg') else 'png'};base64,\" + base64.b64encode(open(path, 'rb').read()).decode()\n\n\nphoto = data_uri(ns['PHOTO'])\ndef _hakobu_uri():\n \"\"\"ハコブくんの体(右向き)。2026-10-12 から、作り直した体に顔を描いた hakobu/thumb_body_right.png を使う\n (元の体は頭の上が切り抜けておらず、大きく出すと頭がへこんで見えた)。なければ動画と同じ体を使う\"\"\"\n import io\n new = os.path.join(HERE, 'hakobu', 'thumb_body_right.png')\n if os.path.exists(new):\n return data_uri(new)\n from hakobu import Hakobu, FX0, FY0\n H = Hakobu(mode='full')\n eyes = ('open', 'open')\n b = H.body.copy(); H._ring(b, 0.7)\n img = b.crop(H.crop_draw)\n gl, la = H._face_layers(eyes, None, 'mid', 0.0)\n ox, oy = FX0 - H.crop_draw[0], FY0 - H.crop_draw[1]\n img.alpha_composite(gl, (ox, oy)); img.alpha_composite(la, (ox, oy))\n img = img.crop((H.crop[0] - H.crop_draw[0], H.crop[1] - H.crop_draw[1], H.crop[2] - H.crop_draw[0], H.crop[3] - H.crop_draw[1]))\n img = img.transpose(Image.FLIP_LEFT_RIGHT)\n img = img.crop(img.getchannel('A').point(lambda v: 255 if v > 60 else 0).getbbox())\n img.save('thumb/_hakobu.png')\n buf = io.BytesIO(); img.save(buf, 'PNG')\n return 'data:image/png;base64,' + base64.b64encode(buf.getvalue()).decode()\n\n\nhk = _hakobu_uri() if ns.get('HAKOBU', True) else None\nkicker, lines, label = ns.get('KICKER', ''), ns['LINES'], ns.get('LABEL', '')\npos = ns.get('PHOTO_POS', '20% 50%') # 写真のどこを見せるか(CSS の background-position)\n\nBASE = f\"\"\"*{{margin:0;padding:0;box-sizing:border-box}}\nhtml,body{{width:1920px;height:1080px;overflow:hidden;font-family:'Noto Sans CJK JP',sans-serif;color:{INK}}}\n.red{{color:{RED}}}\n.kicker{{display:inline-block;font-size:56px;font-weight:900;color:#fff;background:{RED};padding:10px 30px;border-radius:12px;letter-spacing:.02em}}\n.main{{font-weight:900;line-height:1.16;letter-spacing:-.02em}}\n.hk{{position:absolute;left:24px;bottom:-90px;height:430px}}\n.label{{position:absolute;left:350px;bottom:250px;background:#fff;color:{INK};font-size:48px;font-weight:900;padding:12px 26px;border-radius:14px;box-shadow:0 6px 24px rgba(0,0,0,.25)}}\n.label:after{{content:'';position:absolute;left:-22px;bottom:22px;border:14px solid transparent;border-right:14px solid #fff}}\n.brand{{position:absolute;right:56px;top:44px;height:40px}}\n\"\"\"\n\n\ndef hakobu_html():\n if not hk:\n return ''\n lab = f'<div class=\"label\">{label}</div>' if label else ''\n return f'<img class=\"hk\" src=\"{hk}\">{lab}'\n\n\ndef html(kind):\n k = f'<div class=\"kicker\">{kicker}</div>' if kicker else ''\n body = '<br>'.join(lines)\n if kind == 'split': # 左に動画と同じ紙色の面と文字、右に写真\n return f\"\"\"<style>{BASE}\nbody{{background:{BG}}}\n.ph{{position:absolute;right:0;top:0;width:820px;height:1080px;background:url({photo}) {pos}/cover}}\n.txt{{position:absolute;left:96px;top:84px;width:1060px}}\n.main{{font-size:136px;margin-top:28px}}\n</style><div class=\"ph\"></div><div class=\"txt\">{k}<div class=\"main\">{body}</div></div>{hakobu_html()}<img class=\"brand\" src=\"{LOGO}\">\"\"\"\n if kind == 'card': # 写真を全面に敷き、動画の名前テロップと同じ薄い紙色の札に文字\n return f\"\"\"<style>{BASE}\nbody{{background:url({photo}) {pos}/cover}}\n.txt{{position:absolute;left:70px;top:70px;background:rgba(245,245,240,.95);border-radius:24px;padding:44px 60px 52px}}\n.main{{font-size:124px;margin-top:24px}}\n</style><div class=\"txt\">{k}<div class=\"main\">{body}</div></div>{hakobu_html()}\"\"\"\n if kind == 'dark': # 案B: 写真を右へずらして敷き、左は黒に溶かして白い文字。ハコブくんは左下で大きめ、右(アーム)を向く\n return f\"\"\"<style>{BASE}\nbody{{background:#111}}\n.ph{{position:absolute;left:720px;top:0;width:1920px;height:1080px;background:url({photo}) 0 50%/cover}}\n.shade{{position:absolute;inset:0;background:linear-gradient(90deg,rgba(13,13,13,1) 0%,rgba(13,13,13,1) 38%,rgba(13,13,13,.6) 48%,rgba(13,13,13,0) 64%)}}\n.txt{{position:absolute;left:96px;top:70px;width:1150px}}\n.main{{font-size:122px;margin-top:22px;color:#fff;text-shadow:0 4px 18px rgba(0,0,0,.6)}}\n.main .red{{color:#E25A2C}}\n.hk2{{position:absolute;left:40px;bottom:-120px;height:540px}}\n.label{{left:380px;bottom:300px}}\n</style><div class=\"ph\"></div><div class=\"shade\"></div><div class=\"txt\">{k}<div class=\"main\">{body}</div></div><img class=\"hk2\" src=\"{hk}\"><div class=\"label\">{label}</div>\"\"\"\n if kind == 'dark2': # 案B2: ハコブくんを左に大きく(画面の高さいっぱい)。文字は右上、写真は右下\n return f\"\"\"<style>{BASE}\nbody{{background:#111}}\n.ph{{position:absolute;left:560px;top:120px;width:1920px;height:1080px;background:url({photo}) 0 50%/cover}}\n.shade{{position:absolute;inset:0;background:linear-gradient(90deg,rgba(13,13,13,1) 0%,rgba(13,13,13,1) 30%,rgba(13,13,13,0) 46%),linear-gradient(180deg,rgba(13,13,13,1) 0%,rgba(13,13,13,.92) 30%,rgba(13,13,13,0) 58%)}}\n.txt{{position:absolute;right:70px;top:60px;text-align:right}}\n.kicker{{font-size:52px}}\n.main{{font-size:128px;margin-top:20px;color:#fff;text-shadow:0 4px 18px rgba(0,0,0,.7)}}\n.main .red{{color:#E25A2C}}\n.hk2{{position:absolute;left:30px;bottom:-110px;height:900px}}\n.label{{left:90px;top:40px;bottom:auto}}\n.label:after{{left:60px;bottom:-26px;border:14px solid transparent;border-top:14px solid #fff}}\n</style><div class=\"ph\"></div><div class=\"shade\"></div><img class=\"hk2\" src=\"{hk}\"><div class=\"label\">{label}</div><div class=\"txt\">{k}<div class=\"main\">{body}</div></div>\"\"\"\n if kind == 'dark3': # 案B3: 上に横いっぱいの文字(黒地)、下に写真。ハコブくんは左下で大きく、アームを見る\n L = ns.get('BOLD_LINES', [kicker] + lines)\n return f\"\"\"<style>{BASE}\nbody{{background:#111}}\n.ph{{position:absolute;left:470px;top:380px;width:1620px;height:911px;background:url({photo}) 0 40%/cover}}\n.shade{{position:absolute;left:0;right:0;top:380px;height:220px;background:linear-gradient(180deg,#111 0%,rgba(17,17,17,0) 100%)}}\n.shadeL{{position:absolute;left:470px;top:380px;width:260px;bottom:0;background:linear-gradient(90deg,#111 0%,rgba(17,17,17,0) 100%)}}\n.top{{position:absolute;left:0;right:0;top:46px;text-align:center;color:#fff;font-weight:900;line-height:1.12}}\n.top .a{{font-size:78px}} .top .b{{font-size:150px;letter-spacing:-.02em}} .top .b .red{{color:#E25A2C}}\n.hk2{{position:absolute;left:40px;bottom:-90px;height:700px}}\n.label{{left:360px;bottom:520px}}\n</style><div class=\"ph\"></div><div class=\"shade\"></div><div class=\"shadeL\"></div>\n<div class=\"top\"><div class=\"a\"><span style=\"background:{RED};padding:4px 26px;border-radius:12px;font-size:60px\">{L[0]}</span></div>\n<div class=\"b\"><span class=\"red\">{L[1]}</span> {L[2]}</div></div><img class=\"hk2\" src=\"{hk}\"><div class=\"label\">{label}</div>\"\"\"\n if kind in ('photo', 'photo2', 'photo3'):\n # 2026-10-12 エイイチの指示: 背景は全部写真。文字は大きく、写真の上に置くので黒い縁を付ける(上の札は縁なし)\n # photo : 文字は左上、ハコブくんは右下で左(アーム)を向く\n # photo2: 文字は右上、ハコブくんは右下で左を向く\n # photo3: 文字は右上、ハコブくんは左下の隅で右(アーム)を向く。写真は PHOTO3(ハコブくんがアームと箱に重ならないコマ)\n L = [kicker] + lines\n right = kind != 'photo'\n left_hk = kind == 'photo3'\n ph = data_uri(ns['PHOTO3']) if left_hk and ns.get('PHOTO3') else photo\n OUT = f\"-webkit-text-stroke:24px {INK};paint-order:stroke fill;text-shadow:0 6px 18px rgba(0,0,0,.55)\"\n if left_hk:\n HK = f\".hk3{{{{position:absolute;left:10px;bottom:-190px;height:{ns.get('HK_H', 600)}px}}}}\"\n LB = f\"\"\".label{{{{left:{ns.get('HK_H', 600) * 0.62 + 10:.0f}px;bottom:300px;border:6px solid {INK};font-size:54px}}}}\n.label:after{{{{left:-30px;bottom:24px;border:16px solid transparent;border-right:16px solid {INK}}}}}\"\"\"\n else:\n HK = f\".hk3{{{{position:absolute;right:{'30' if right else '60'}px;bottom:-170px;height:{'520' if right else '640'}px;transform:scaleX(-1)}}}}\"\n LB = f\"\"\".label{{{{left:auto;right:{'330' if right else '420'}px;bottom:{'190' if right else '360'}px;border:6px solid {INK};font-size:54px}}}}\n.label:after{{{{left:auto;right:-30px;bottom:24px;border:16px solid transparent;border-left:16px solid {INK}}}}}\"\"\"\n HK, LB = HK.replace('{{', '{').replace('}}', '}'), LB.replace('{{', '{').replace('}}', '}')\n return f\"\"\"<style>{BASE}\nbody{{background:url({ph}) {ns.get('PHOTO_POS_FULL', '50% 50%')}/cover}}\n.dim{{position:absolute;inset:0;background:linear-gradient({'200deg' if right else '160deg'},rgba(0,0,0,.45) 0%,rgba(0,0,0,.15) 45%,rgba(0,0,0,0) 70%)}}\n.txt{{position:absolute;{'right:56px;text-align:right' if right else 'left:60px'};top:40px;font-weight:900;color:#fff;line-height:1.1;letter-spacing:-.02em}}\n.kick{{display:inline-block;font-size:66px;background:{RED};color:#fff;padding:8px 30px 12px;border-radius:14px;letter-spacing:.02em;box-shadow:0 6px 18px rgba(0,0,0,.35)}}\n.l1{{font-size:150px;margin-top:22px;{OUT}}}\n.l2{{font-size:{'200' if right else '212'}px;{OUT}}}\n.l2 .red{{color:#F0602C}}\n.l3{{font-size:{'170' if right else '176'}px;{OUT}}}\n{HK}\n{LB}\n</style><div class=\"dim\"></div>\n<div class=\"txt\"><div class=\"kick\">{L[0]}</div><div class=\"l1\">{L[1]}</div><div class=\"l2\">{L[2]}</div><div class=\"l3\">{L[3]}</div></div>\n<img class=\"hk3\" src=\"{hk}\"><div class=\"label\">{label}</div>\"\"\"\n if kind == 'bold': # ChatGPT 案の構図(左に大きなハコブくんと吹き出し、右に縁取りの太い文字)を、本物の写真と動画の文字で作る\n L = ns.get('BOLD_LINES', [kicker] + lines)\n rows = ''.join(f'<div class=\"r{i}\">{t}</div>' for i, t in enumerate(L))\n return f\"\"\"<style>{BASE}\nbody{{background:#151515}}\n.ph{{position:absolute;left:700px;top:-30px;width:2040px;height:1148px;background:url({photo}) 0 50%/cover}}\n.fade{{position:absolute;inset:0;background:linear-gradient(90deg,#151515 0%,#151515 36%,rgba(21,21,21,0) 50%),linear-gradient(180deg,rgba(0,0,0,.35) 0%,rgba(0,0,0,0) 55%)}}\n.txt{{position:absolute;right:50px;top:70px;text-align:right;font-weight:900;line-height:1.08;letter-spacing:-.01em;color:#fff;\n -webkit-text-stroke:26px {INK};paint-order:stroke fill}}\n.r0{{font-size:92px}} .r1{{font-size:156px;color:{RED};margin-top:14px}} .r2{{font-size:190px;margin-top:6px}}\n.r1 .red{{color:{RED}}}\n.big{{position:absolute;left:30px;bottom:-50px;height:760px}}\n.bubble{{position:absolute;left:110px;top:70px;background:#fff;border:8px solid {INK};border-radius:22px;padding:14px 34px;font-size:76px;font-weight:900;color:{INK}}}\n.bubble:after{{content:'';position:absolute;left:150px;bottom:-44px;border:22px solid transparent;border-top:22px solid {INK}}}\n</style><div class=\"ph\"></div><div class=\"fade\"></div><img class=\"big\" src=\"{hk}\"><div class=\"bubble\">{label}</div><div class=\"txt\">{rows}</div>\"\"\"\n raise SystemExit(f'案の名前が分かりません: {kind}')\n\n\nouts = []\nwith sync_playwright() as p:\n b = p.chromium.launch()\n pg = b.new_page(viewport={'width': 1920, 'height': 1080})\n for kind in ns.get('VARIANTS', ['split', 'card', 'dark']):\n pg.set_content('<!doctype html><html><head><meta charset=\"utf-8\"></head><body>' + html(kind) + '</body></html>')\n pg.wait_for_timeout(300)\n big = f'thumb/_{kind}_1920.png'\n pg.screenshot(path=big)\n im = Image.open(big).convert('RGB').resize((1280, 720), Image.LANCZOS)\n out = f'thumb/{kind}.png'\n im.save(out, optimize=True)\n if os.path.getsize(out) > 2_000_000:\n out = f'thumb/{kind}.jpg'; im.save(out, quality=92)\n os.remove(big)\n outs.append((kind, out))\n b.close()\n\n# 一覧: 原寸の半分と、スマホの一覧で出る大きさ(幅 168px 前後)を並べる\nW = 640 + 24 + 168\nsheet = Image.new('RGB', (W, len(outs) * (360 + 24)), (30, 30, 30))\nfor k, (kind, out) in enumerate(outs):\n im = Image.open(out)\n y = k * (360 + 24)\n sheet.paste(im.resize((640, 360), Image.LANCZOS), (0, y))\n sheet.paste(im.resize((168, 94), Image.LANCZOS), (640 + 24, y))\nsheet.save('thumb/一覧.png')\nfor kind, out in outs:\n print(kind, out, f'{os.path.getsize(out) / 1e6:.2f}MB')\n</code></pre><h3 id=\"index_TxNsgURJ\">_制作ベース/hakobu/make_new_face.py</h3><p>新規。作り直した体の画面に、動画と同じ描き方の顔を写す(64行)。</p><pre><code class=\"language-python\">\"\"\"新しい体(cut_new.png)のディスプレイに、動画と同じ顔を描く。サムネイル用(2026-10-12)\n2026-10-12 エイイチの指摘「顔が変(口と目の配置や大きさ)」で作り直し:\n 自前で描いた目と口をやめ、動画の hakobu.py と同じ描き方(eye_mask / mouth_mask)で描いた顔を、\n 動画のディスプレイ(SCREEN_QUAD を長方形にならしたもの)から新しい体のディスプレイ(screen_quad_new.npy)へ写す。\n 画面の大きさに対する目と口の大きさ・位置は、動画のハコブくんとそのまま同じになる\n使い方: python3 make_new_face.py [目 口] 例: python3 make_new_face.py open mid(既定)\n\"\"\"\nimport os, sys\nimport numpy as np, cv2\nfrom PIL import Image\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, os.path.dirname(HERE))\nimport hakobu as hk\n\n\ndef face_on_new_body(eyes=('open', 'open'), mouth='mid', out='body_new_face.png'):\n im = np.array(Image.open(os.path.join(HERE, 'cut_new.png'))).astype(np.float32)\n q = np.load(os.path.join(HERE, 'screen_quad_new.npy')).astype(np.float32) # 左上・右上・右下・左下\n SS, FX0, FY0, FX1, FY1 = hk.SS, hk.FX0, hk.FY0, hk.FX1, hk.FY1\n hh, ww = (FY1 - FY0) * SS, (FX1 - FX0) * SS\n # 1. 動画と同じ描き方で、顔を画面での向きに描く\n white = np.zeros((hh, ww), np.uint8)\n for kind, box, side in ((eyes[0], hk.V_EYE_LEFT, -1), (eyes[1], hk.V_EYE_RIGHT, 1)):\n em = np.zeros_like(white)\n hk.eye_mask(em, kind, box, side, 0.0)\n white = np.maximum(white, em)\n blue = hk.mouth_mask(mouth)\n la = np.zeros((hh, ww, 4), np.float32)\n la[white > 0, :3] = hk.WHITE\n la[..., 3] = white\n la[blue > 0, :3] = hk.BLUE\n la[..., 3] = np.maximum(la[..., 3], blue)\n la = np.ascontiguousarray(la[:, ::-1]) # 動画と同じく反転して、体の元の向きに戻す\n # 2. 動画のディスプレイをならした長方形(hakobu.py の flat)→ 新しい体のディスプレイ\n S = hk.SCREEN_QUAD\n c = S.mean(0)\n w_ = (np.linalg.norm(S[1] - S[0]) + np.linalg.norm(S[2] - S[3])) / 2\n h_ = (np.linalg.norm(S[3] - S[0]) + np.linalg.norm(S[2] - S[1])) / 2\n flat = np.float32([[c[0] - w_ / 2, c[1] - h_ / 2], [c[0] + w_ / 2, c[1] - h_ / 2],\n [c[0] + w_ / 2, c[1] + h_ / 2], [c[0] - w_ / 2, c[1] + h_ / 2]])\n src = (flat - np.float32([FX0, FY0])) * SS\n M = cv2.getPerspectiveTransform(src, q)\n h, w = im.shape[:2]\n a = la[..., 3:4] / 255\n pre = np.concatenate([la[..., :3] * a, a * 255], 2)\n wl = cv2.warpPerspective(pre, M, (w, h), flags=cv2.INTER_AREA)\n aa = wl[..., 3:4] / 255\n rgb = np.where(aa > 1e-4, wl[..., :3] / np.maximum(aa, 1e-4), 0)\n # 3. 目のまわりのにじむ光(動画と同じ色。体の大きさに合わせてぼかしを広げる)\n scale = np.linalg.norm(q[1] - q[0]) / w_\n eye_a = aa[..., 0] * (rgb[..., 0] > 200)\n glow = cv2.GaussianBlur(eye_a, (0, 0), 2.6 * scale) * 0.75\n for col, al in ((np.float32([205, 225, 255]), glow[..., None]), (rgb, aa)):\n im[..., :3] = im[..., :3] * (1 - al) + col * al\n Image.fromarray(im.clip(0, 255).astype(np.uint8)).save(os.path.join(HERE, out))\n return out\n\n\nif __name__ == '__main__':\n args = sys.argv[1:]\n if len(args) == 2:\n print(face_on_new_body((args[0], args[0]), args[1]))\n else:\n print(face_on_new_body())\n</code></pre><h3 id=\"index_zd9AxR5a\">フィジカルAIの現在地/narration.py</h3><p>台本。1文ごとに(場面、表情、字幕、後ろの間)。読み替えの表 READ と ELEVEN_READ もここ(186行)。</p><pre><code class=\"language-python\"># -*- coding: utf-8 -*-\n# 台本「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」ハコブくん版\n# もとは DAY330 の試作(codex の敵対的検証を反映した版)。2026-10-12 に、ハコブくんが話す形(エイイチは三人称)に直した。\n# 変えたのは、一人称→三人称、自己紹介(確定した文言)、見立ての入り方、NEO の確認日(10月12日)、アウトロ、1文ごとの表情。事実と数字は変えていない\n# 各要素: (場面の名前, 表情, 字幕の文, 後ろの間[秒]) 間を省略すると 0.35 秒\n# 字幕の文の中の「|」: 読み上げは1文のまま、字幕だけをそこで次の枚に替える\nFACES = (\"base\", \"smile\", \"surprise\", \"doubt\", \"think\", \"trouble\", \"joy\", \"wink\")\n\nSEGS = [\n ('hook_ship', 'base', '調査会社Counterpointの推計では、今年の上半期に出荷された人型ロボットは、'),\n ('hook_ship', 'base', '世界で2万2000台を超えました。前の年の同じ時期の、およそ4倍です。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('hook_forecast', 'surprise', '年末までには、5万台を超えると予想されています。'),\n ('hook_forecast', 'base', '家事をする人型ロボットの予約販売も、去年の秋から始まっています。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('hook_question', 'base', 'こういうニュースを見ると、ChatGPTが出たときみたいに、'),\n ('hook_question', 'doubt', 'ロボットも一気に賢くなって、家に来るのはもうすぐ、って感じませんか。', 1.8), # 大きな流れの変わり目の間\n ('title', 'smile', 'Pixels to Motors。案内役のハコブくんです。'),\n ('title', 'base', 'この番組では、フィジカルAIを、現場で使える形にするまでを解説します。'),\n ('title_eiichi', 'base', 'エイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。|その中で分かったことと、そこから考えた見立てを、ハコブくんがお伝えします。', 1.8), # 大きな流れの変わり目の間\n ('question', 'surprise', '今日の問いは、ロボットにもChatGPTのような瞬間は、もう来たのか、です。'),\n ('question', 'base', 'ここでいうChatGPTの瞬間は、専門家じゃなくても、'),\n ('question', 'base', 'いろんな仕事にすぐ使えるようになる転換点、という意味です。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('define', 'base', 'フィジカルAIっていうのは、現実の世界を認識して、行動するAIのことです。'),\n ('define', 'base', '自動運転もここに入りますが、'),\n ('define', 'base', '今日は主に、アームや人型ロボットが物を扱う話に絞ります。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('answer', 'base', '先に結論を言っちゃうと、写真を見せて判断させる入口は、ぐっと簡単になりました。'),\n ('answer', 'trouble', 'でも、物を確実に動かすところは、まだ手間がかかるんですよ。'),\n ('answer', 'base', 'その手間は大きく2つあります。'),\n ('answer', 'base', '動作と結びついたデータが集めにくいこと。そして、機体や環境とのすり合わせです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('stance', 'base', '先に言っておくと、フィジカルAIはまだ使えない、と言いたいわけじゃないんです。'),\n ('stance', 'base', 'どこまで来ていて、どこで手間がかかっているのかを、分けて見たいんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('toc', 'base', '大きく3つあります。'),\n ('toc', 'base', '出荷が4倍になったロボットは、何をしているのか。'),\n ('toc', 'base', 'エイイチの机の上のロボットアームで、何が簡単で、何が難しかったのか。'),\n ('toc', 'base', 'そして、データの壁はどれくらい高くて、誰がどう越えようとしているのか。'),\n ('toc', 'base', '最後に、エイイチの見立てを話します。', 1.8), # 大きな流れの変わり目の間\n ('c1', 'base', '1つ目、出荷が4倍になったロボットは、何をしているのか、です。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('use', 'base', 'Counterpointの推計では、出荷された人型ロボットの6割以上が、'),\n ('use', 'base', '展示やイベントで動きを見せる用途と、研究やデータ生産の用途なんですよ。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('datacollect', 'base', 'データ生産っていうのは、ロボットが学ぶための動きの記録を作ることです。'),\n ('datacollect', 'base', '人が遠隔で動かして記録するやり方も、自律で動いた記録を残すやり方もあります。'),\n ('datacollect', 'doubt', 'ただ、研究向けの台数が全部データ集めに使われる、という意味ではありません。'),\n ('datacollect', 'base', 'それでも、用途の分類に「データ生産」という項目があること自体が、'),\n ('datacollect', 'base', '今の段階をよく表していると思います。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('conflict', 'base', '一方で、別の調査会社のSmart Analytics Globalは、'),\n ('conflict', 'base', '同じ半年の出荷を約1万9100台と推計していて、'),\n ('conflict', 'base', '産業・商業用途が7割を超えた、としています。'),\n ('conflict', 'base', '総数も分類も違うので、この2つは、単純な裏表としては比べられません。'),\n ('conflict', 'doubt', 'それに、出荷台数は、そのまま自律的に働いている台数でもないんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('china', 'surprise', 'もう1つ、SAGの推計では、出荷の97%以上が中国のメーカーです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('neo', 'base', '家のほうも見てみます。'),\n ('neo', 'base', 'アメリカの1Xという会社は、家事をする人型ロボット、NEOの予約を、去年の10月に始めました。'),\n ('neo', 'base', '2万ドルの早期購入枠は、今年配送の予定で、月499ドルのプランは、その後です。'),\n ('neo', 'base', '最初は基本的な自律機能だけで、知らない作業は、'),\n ('neo', 'base', '予約した時間に、1Xの担当者が遠隔で手伝う仕組みなんですよ。'),\n ('neo', 'base', '10月12日の時点で、一般のお客さんへの配送開始を示す公式発表は、'),\n ('neo', 'base', 'エイイチが確認した範囲では、見つかっていません。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('neo_point', 'surprise', 'これ、ちょっと考えさせられませんか。'),\n ('neo_point', 'think', '売っているのはロボットだけじゃなくて、遠隔で手伝う人と、'),\n ('neo_point', 'think', 'そこで動きを覚えていく仕組みごとなんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('bridge1', 'base', '工場でも家でも、使いながらデータを集める動きが出てきているんですよ。'),\n ('bridge1', 'doubt', 'じゃあ、物を動かすのは、何がそんなに大変なのか。'),\n ('bridge1', 'base', 'まず、エイイチの机の上の話をします。', 1.8), # 大きな流れの変わり目の間\n ('c2', 'base', '2つ目、エイイチの机の上のロボットアームで、何が簡単で、何が難しかったのか、です。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('v_real', 'base', 'ここは、エイイチ自身の話から始めます。'),\n ('v_real', 'base', 'エイイチは自宅の机で、mechArm 270という小さなロボットアームを動かしています。'),\n ('v_real', 'base', '赤いブロックを掴んで、箱に置く。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('trials', 'base', 'それだけの作業を、失敗も含めて650回以上試して、全部台帳に残しています。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('easy', 'smile', 'まず、簡単だったほうから話します。掴めたかどうかの判定です。'),\n ('easy', 'smile', 'iPhoneで撮った写真をAIに見せて、「ブロックを掴めていますか」と聞くと、'),\n ('easy', 'joy', 'はい、いいえで答えが返ってくる。この入口は、驚くほど簡単でした。'),\n ('easy', 'doubt', 'ただ、答えが返ることと、毎回正しいことは別です。'),\n ('easy', 'smile', 'どこまで任せられるかは、失敗例も含めて確かめる必要があります。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('hard', 'trouble', '難しかったのは、動かすほうでした。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gap', 'base', 'ロボットアームの爪先がどこに来るかは、普通は関節の角度から計算で出します。'),\n ('gap', 'base', 'ところが、ikpyという計算ツールで出した爪先の位置と、', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('v_twin', 'base', 'パソコンの中に作ったアームの分身、デジタルツインで動かした位置が、', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gap', 'surprise', 'エイイチの設定では、同じ関節の角度なのに、30cm以上食い違ったんです。'),\n ('gap', 'doubt', 'これは、アームが30cm外れた、という意味ではありません。'),\n ('gap', 'base', '計算ツールには、グリッパーの形も、デジタルツインの物理も入っていなくて、爪先の位置の決め方そのものが違っていたんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('v_scan', 'base', 'そこでエイイチは、分身の中でアームを動かして、'),\n ('v_scan', 'base', '関節の角度と爪先の位置の対応表を作りました。'),\n ('v_scan', 'base', 'これは計算の表で、実機で全部の点を測ったわけではありません。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('silent', 'trouble', 'もう1つ困ったのが、黙る失敗です。'),\n ('silent', 'trouble', 'エイイチの機体とソフトの組み合わせでは、肩の関節に、90度ちょっとを超える値を送ると、'),\n ('silent', 'trouble', 'エラーが出ないのに、アームが動かないことがありました。'),\n ('silent', 'trouble', 'ログを1行ずつ見て、やっと気づきました。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('point2', 'surprise', 'でも、ポイントはここなんです。'),\n ('point2', 'think', 'この2つは、AIの学習データが足りない、という話とは別です。'),\n ('point2', 'think', 'モデルや制御が、エイイチの機体と噛み合っているかを確かめる、すり合わせの仕事でした。'),\n ('point2', 'think', 'ほかの環境で学んだことを持ってくる研究も進んでいます。'),\n ('point2', 'think', 'それでも、エイイチの構成でちゃんと動くかは、ここで確かめるしかなかったんです。', 0.6),\n ('point2', 'think', 'じゃあ、もう1つの手間、データのほうは、業界全体でどうなっているのか。', 1.8), # 大きな流れの変わり目の間\n ('c3', 'base', '3つ目、データの壁はどれくらい高くて、誰がどう越えようとしているのか、です。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('years', 'base', 'カリフォルニア大学バークレー校のロボット研究者、'),\n ('years', 'base', 'ケン・ゴールドバーグさんが、去年、こんな概算を紹介しています。'),\n ('years', 'base', '画像と言葉を扱うAIの学習資料を、'),\n ('years', 'base', '人が読んだり見たりする時間に換算すると、約10万年規模になる。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('wall', 'trouble', 'それに比べて、ロボットが手を動かすための、動作と結びついた経験は、集めにくい。'),\n ('wall', 'trouble', 'ゴールドバーグさんは、これを「10万年のデータの壁」と呼んでいます。'),\n ('wall', 'trouble', 'ただ、ロボットにも同じ量が必要だと分かったわけではありません。'),\n ('wall', 'trouble', '集めにくい、という問題提起なんです。', 0.6),\n ('wall', 'doubt', 'ここで、じゃあロボットにChatGPTの瞬間は当分来ない、'),\n ('wall', 'smile', 'と言いたくなるじゃないですか。でも、ちょっと待ってほしいんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gen', 'base', 'アメリカのGeneralistという会社は、人が手で作業する様子を、'),\n ('gen', 'base', '安い装着型の機材で記録して集めています。'),\n ('gen', 'surprise', '会社の発表では、去年の11月に27万時間、今年の4月に50万時間を超えました。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gen2', 'base', 'さらに8月には、簡単で短い作業なら、'),\n ('gen2', 'base', '数秒の実演を見せるだけで、新しい作業をこなす例も発表しています。'),\n ('gen2', 'doubt', 'ただ、本人たちも、作業は簡単で短く、成功率はまだ控えめだと書いています。'),\n ('gen2', 'base', 'それでも、「動かすには毎回大量の専用データが必要」とは、言い切れなくなってきました。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('compare', 'think', 'ここで1つ、注意があります。'),\n ('compare', 'base', '作業を記録した時間と、10万年という数字は、そのまま比べられません。'),\n ('compare', 'base', '何倍足りないかより、いろんな場面の経験を、どれだけ広く集められるかが問題なんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('dirt', 'think', 'つまり、壁はまだ高い。でも、壁の前でみんなが土を積み始めている。'),\n ('dirt', 'think', '1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、'),\n ('dirt', 'think', 'Generalistが人の手作業を記録しているのも、同じ壁に向かっている動きなんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('notall', 'smile', 'ただ、ここで「なんだ、まだ全然ダメなのか」で'),\n ('notall', 'smile', '片付けてしまうのも違うんです。'),\n ('notall', 'smile', '写真を見せて判断させる入口は、エイイチの机の上でも、もう普通に使えています。', 0.6),\n ('notall', 'doubt', 'じゃあ、この2つが混ざっている今、ロボットのニュースの何を見ればいいのか。', 1.8), # 大きな流れの変わり目の間\n ('c4', 'base', 'ここからは、エイイチの見立てです。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('q', 'surprise', 'ロボットのニュースを見たときの問いが、2つに変わるんですよ。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('q1', 'base', '1つは、そのロボットは自律で動いているのか、人が遠隔で動かしているのか、です。'),\n ('q1', 'base', 'デモ動画を見るときは、自律か遠隔操作か、'),\n ('q1', 'base', '再生速度や、人が手を出した条件が書かれているかを見ます。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('q2', 'base', 'もう1つは、その仕事は「見て判断する」で済むのか、'),\n ('q2', 'base', '「触れて動かす」精度が要るのか、です。'),\n ('q2', 'base', '棚の商品が足りているかを見るのは、前者です。'),\n ('q2', 'base', '柔らかい物を潰さずに掴むのは、後者です。'),\n ('q2', 'base', '前者は、手軽に試せるところまで来ています。ただ、どこまで任せられるかは、現場で確かめる必要がある。'),\n ('q2', 'base', '後者は、その現場の機体と環境に合わせて、すり合わせる仕事が、まだ残っています。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gofe', 'base', 'ゴールドバーグさんは、昔ながらのエンジニアリングと学習を組み合わせて、'),\n ('gofe', 'base', '現場で役に立つ仕事をしながら、データを集めていくのが近道だ、と言っています。'),\n ('gofe', 'base', 'エイイチの机の上も、AIと従来の制御を組み合わせる点では通じています。'),\n ('gofe', 'base', 'ただ、現場で働きながらデータが増える仕組みとは、まだ規模も段階も違います。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('motion', 'base', '作る側から見ると、もう1つあります。'),\n ('motion', 'base', 'データの話と並行して、もう1つ要るのが「どう動くか」の設計です。'),\n ('motion', 'base', '例えばロボットが人に物を渡すとき、'),\n ('motion', 'base', '何秒待って手を開けば、人は安心して受け取れるのか。'),\n ('motion', 'base', 'これは、成功か失敗かの判定だけでは出てこない。'),\n ('motion', 'base', '人が安心できる動きを設計する仕事も、同時に必要だと、エイイチは見ています。', 1.8), # 大きな流れの変わり目の間\n ('answer2', 'base', 'じゃあ、冒頭の問いに答えますね。', 0.4),\n ('answer2', 'base', 'エイイチの机の上では、写真を見せて判断させる入口は、ずっと簡単になりました。'),\n ('answer2', 'base', '一方、物を確実に動かすには、機体や環境とのすり合わせが、まだ必要でした。'),\n ('answer2', 'base', 'その負担を減らす研究は、急速に進んでいます。'),\n ('answer2', 'trouble', 'でも、いろんな家で、安全に家事を任せられる転換点まで来たかは、まだ判断できません。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('action', 'base', 'ロボットのデモ動画を見たら、'),\n ('action', 'base', '「これは自律なのか、何回やって、何回うまくいったのか」を、'),\n ('action', 'base', '一度だけ考えてみてください。'),\n ('action', 'smile', 'それだけで、同じ動画の見え方が、かなり変わると思います。', 1.8), # 大きな流れの変わり目の間\n ('outro', 'smile', '今日はここまでです。'),\n ('outro', 'base', 'この動画が気に入ったら、高評価とチャンネル登録をお願いします。'),\n ('outro', 'base', 'ロボットの開発や導入の相談は、概要欄の連絡先から、エイイチに直接どうぞ。'),\n ('outro', 'wink', 'コメントは、エイイチが全部読んでいます。それではまた。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('sources', 'smile', '', 6.0),\n]\n\n# 仮音声(Open JTalk)用の読み(字幕は元の表記のまま)\nREAD = [('Pixels to Motors', 'ピクセルズ トゥー モーターズ'), ('Smart Analytics Global', 'スマート アナリティクス グローバル'), ('Counterpoint', 'カウンターポイント'), ('ChatGPT', 'チャットジーピーティー'), ('Generalist', 'ジェネラリスト'), ('mechArm 270', 'メックアーム ニーナナゼロ'), ('iPhone', 'アイフォーン'), ('ikpy', 'アイケーパイ'), ('NEO', 'ネオ'), ('1X', 'ワンエックス'), ('SAG', 'エスエージー'), ('AI', 'エーアイ'), ('30cm', 'さんじゅっセンチ'), ('爪先', 'つめさき'), ('1万9100台', 'いちまんきゅうせんひゃくだい'), ('値を送る', 'あたいを送る'), ('フィジカルAI', 'フィジカルエーアイ'), ('「', ''), ('」', ''), ('|', '')]\n\n# ElevenLabs の声で読み間違えた語だけ、読みを直す(tts_eleven.py が使う。字幕は変えない)\nELEVEN_READ = [\n ('エイイチ', 'えいいち'), # 2026-10-12 カタカナだと文ごとに抑揚が変わった。ひらがなで語尾が下がる読みにそろう(エイイチが4通りを聞き比べて決めた)\n ('裏表', 'うらおもて'), # 2026-10-12 37番で「うらおもてうら」のように読まれた(エイイチが見つけた)\n]\n\n# 仮音声の間延び対策:ひと息が長い文だけ、読み上げ用に読点を足す(字幕は変えない)\nOVERRIDE = {'同じ半年の出荷を約1万9100台と推計していて、': '同じ半年の出荷を、約1万9100台と、推計していて、', '工場でも家でも、使いながらデータを集める動きが出てきているんですよ。': '工場でも家でも、使いながら、データを集める動きが、出てきているんですよ。', 'ほかの環境で学んだことを持ってくる研究も進んでいます。': 'ほかの環境で学んだことを、持ってくる研究も、進んでいます。', 'ここで、じゃあロボットにChatGPTの瞬間は当分来ない、': 'ここで、じゃあ、ロボットに、ChatGPTの瞬間は、当分来ない、', '1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、': '1Xが、家の中で、遠隔で手伝いながら、動きを覚えさせているのも、'}\n\n\ndef reading(t):\n t = OVERRIDE.get(t, t)\n for a, b in READ:\n t = t.replace(a, b)\n return t\n\n\ndef parse(seg):\n \"\"\"1行を (場面, 表情, 字幕, 間) にそろえる\"\"\"\n if len(seg) >= 3 and seg[1] in FACES:\n scene, face, text = seg[0], seg[1], seg[2]\n pause = seg[3] if len(seg) > 3 else 0.35\n else:\n scene, face, text = seg[0], \"base\", seg[1]\n pause = seg[2] if len(seg) > 2 else 0.35\n return scene, face, text, pause\n</code></pre><h3 id=\"index__EpedxB3\">フィジカルAIの現在地/scenes.py</h3><p>場面ごとの図と、実機の映像の指定(VIDEO)(363行)。</p><pre><code class=\"language-python\"># -*- coding: utf-8 -*-\n# 動画「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」ハコブくん版の場面。共通部品は _制作ベース/scene_kit.py\n# 2026-10-12: DAY330 の試作の場面を、ハコブくんの区画(左下)と三人称の台本に合わせて直した\nimport os as _os, sys as _sys\n_here = _os.path.dirname(_os.path.abspath(__file__))\nfor _c in (_os.environ.get(\"P2M_BASE\", \"\"), _os.path.join(_here, \"..\", \"base\"), _os.path.join(_here, \"..\", \"_制作ベース\")):\n if _c and _os.path.exists(_os.path.join(_c, \"scene_kit.py\")):\n _sys.path.insert(0, _c); break\nfrom scene_kit import *\n\nT1 = \"<b>1</b>出荷されたロボットは何をしているのか\"\nT2 = \"<b>2</b>机の上のロボットアームで、簡単だったこと・難しかったこと\"\nT3 = \"<b>3</b>データの壁の高さと、越えようとしている会社\"\nT4 = \"<b>4</b>エイイチの見立て\"\n\nS = {}\n\nS[\"hook_ship\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期 人型ロボットの世界出荷(推計)</div>\n <div class=\"hero\">22,000<small>台超</small></div>\n <div class=\"h2\" style=\"margin-top:36px\">前年同期の <span class=\"red\">約4倍</span></div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)</div>\"\"\")\n\nS[\"hook_forecast\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年通年の予想</div>\n <div class=\"hero\">50,000<small>台超</small></div>\n <div class=\"h3\" style=\"margin-top:44px\">家事をする人型ロボットの予約販売も、2025年秋から</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)/1X</div>\"\"\")\n\nS[\"hook_question\"] = page(\"\"\"\n<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div class=\"h1\">ロボットも、ChatGPTのときみたいに<br><span class=\"red\">一気に賢くなる?</span></div>\n</div>\"\"\")\n\nS[\"question\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">今日の問い</div>\n <div class=\"h1\">ロボットにも<span class=\"red\">ChatGPTの瞬間</span>は<br>もう来たのか</div>\n <div class=\"h3 muted\" style=\"margin-top:44px\">ChatGPTの瞬間=専門家でなくても、<br>いろんな仕事にすぐ使えるようになる転換点</div>\n</div>\"\"\")\n\nS[\"define\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">フィジカルAI とは</div>\n <div class=\"h2\">現実の世界を認識して、<span class=\"red\">行動する</span>AI</div>\n <div class=\"row\" style=\"margin-top:56px;gap:30px\">\n <div class=\"box\" style=\"border:4px solid #0D0D0D\">ロボットアーム</div>\n <div class=\"box\" style=\"border:4px solid #0D0D0D\">人型ロボット</div>\n <div class=\"box muted\" style=\"background:transparent;border:2px dashed #D6D6D1\">自動運転<br><span style=\"font-size:30px\">今日は扱わない</span></div>\n </div>\n</div>\"\"\")\n\nS[\"answer\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">先に結論</div>\n {two(\"写真を見せて判断させる入口\", \"ぐっと簡単になった\", \"物を確実に動かすところ\", \"まだ手間がかかる\")}\n <div class=\"h3\" style=\"margin-top:56px\">手間は2つ<br>① <span class=\"red\">動作と結びついたデータ</span>が集めにくい<br>② <span class=\"red\">機体や環境とのすり合わせ</span></div>\n</div>\"\"\")\n\nS[\"stance\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2 muted\" style=\"text-decoration:line-through;text-decoration-thickness:6px\">フィジカルAIはまだ使えない</div>\n <div class=\"h1\" style=\"margin-top:48px\">どこまで来ていて、<br>どこで<span class=\"red\">手間がかかっているのか</span>を、<br>分けて見る</div>\n</div>\"\"\")\n\nS[\"toc\"] = page(\"\"\"\n<div class=\"wrap\">\n <ul class=\"list\">\n <li><span class=\"n\">1</span>出荷が4倍になったロボットは、何をしているのか</li>\n <li><span class=\"n\">2</span>机の上のロボットアームで、何が簡単で何が難しかったか</li>\n <li><span class=\"n\">3</span>データの壁はどれくらい高く、誰がどう越えようとしているか</li>\n <li class=\"dim\"><span class=\"n\">+</span>エイイチの見立て</li>\n </ul>\n</div>\"\"\")\n\nS[\"c1\"] = chapter(\"1\", \"出荷が4倍になったロボットは、<br>何をしているのか\")\nS[\"c2\"] = chapter(\"2\", \"机の上のロボットアームで、<br>何が簡単で、何が難しかったのか\")\nS[\"c3\"] = chapter(\"3\", \"データの壁はどれくらい高くて、<br>誰がどう越えようとしているのか\")\nS[\"c4\"] = chapter(\"4\", \"エイイチの見立て\")\n\nS[\"use\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期の人型ロボット出荷の用途(Counterpointの推計)</div>\n {hbar([(\"展示・イベント + 研究・データ生産 6割以上\", 62, RED, \"#fff\"), (\"その他\", 38, \"#D3D3CE\", INK)])}\n <div class=\"h3\" style=\"margin-top:44px\">出荷の <span class=\"red\">6割以上</span> が、<br>動きを見せる用途と、研究・データ生産の用途</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)。帯の長さは「6割以上」を表す目安</div>\"\"\", tag=T1)\n\nS[\"datacollect\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">データ生産 = ロボットが学ぶための、動きの記録を作ること</div>\n <div class=\"row\" style=\"gap:30px\">\n <div class=\"box\">人が遠隔で動かして<br>記録する</div>\n <div class=\"box\">自律で動いた記録を<br>残す</div>\n </div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">※研究向けの台数が、全部データ集めに使われるわけではない</div>\n <div class=\"h3\" style=\"margin-top:20px\">分類に「データ生産」がある = 今の段階を表している</div>\n</div>\"\"\", tag=T1)\n\nS[\"conflict\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">同じ2026年上半期の推計</div>\n {two(\"Counterpoint\", \"見せる用途+研究・データ生産<br>6割以上\", \"Smart Analytics Global(SAG)\", \"産業・商業用途<br>7割超\", c1=\"\", c2=\"\", size=52, note1=\"出荷 22,000台超\", note2=\"出荷 約19,100台\")}\n <div class=\"h3\" style=\"margin-top:46px\">総数も分類も違う → <span class=\"red\">単純な裏表としては比べられない</span></div>\n <div class=\"h3 muted\" style=\"margin-top:10px\">出荷台数 ≠ 自律的に働いている台数</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)/Smart Analytics Global(2026年8月)</div>\"\"\", tag=T1)\n\nS[\"china\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期の人型ロボット出荷のうち、中国メーカーの割合(SAGの推計)</div>\n <div class=\"hero\">97<small>%以上</small></div>\n</div>\n<div class=\"src\">出典:Smart Analytics Global(2026年8月)</div>\"\"\", tag=T1)\n\nS[\"neo\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">1X「NEO」 家事をする人型ロボット(2025年10月 予約開始)</div>\n {two(\"値段と配送\", \"2万ドル:2026年配送予定<br>月499ドル:その後\", \"動き方\", \"最初は基本的な自律機能<br>知らない作業は<br>担当者が遠隔で手伝う\", c1=\"\", c2=\"\", size=46)}\n <div class=\"h3\" style=\"margin-top:40px\">2026年10月12日時点:一般顧客への配送開始の公式発表は<span class=\"red\">確認できず</span></div>\n</div>\n<div class=\"src\">出典:1X 公式の発表・注文ページ(2026年10月12日確認)</div>\"\"\", tag=T1)\n\nS[\"neo_point\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">売っているのは</div>\n <div class=\"flow\">\n <div class=\"box\">ロボット</div><div class=\"arrow\">+</div>\n <div class=\"box\">遠隔で<br>手伝う人</div><div class=\"arrow\">+</div>\n <div class=\"box\" style=\"border:4px solid {RED}\">動きを<br>覚えていく仕組み</div>\n </div>\n</div>\"\"\", tag=T1)\n\nS[\"bridge1\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">工場でも家でも、<br><span class=\"red\">使いながらデータを集める</span>動き</div>\n <div class=\"h2 muted\" style=\"margin-top:56px\">では、物を動かすのは<br>何がそんなに大変なのか</div>\n</div>\"\"\", tag=T1)\n\nS[\"trials\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">赤いブロックを掴んで、箱に置く</div>\n <div class=\"hero\">650<small>回以上</small></div>\n <div class=\"h3\" style=\"margin-top:36px\">失敗も含めた試行回数。全部台帳に記録</div>\n</div>\n<div class=\"src\">PICK→PLACE の通し試行(台帳 pick_place_ledger_v3.csv、2026年6月18日〜7月20日で656回)</div>\"\"\", tag=T2)\n\nS[\"easy\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{BLUE}\">簡単だった:掴めたかの判定</div>\n <div class=\"flow\">\n <div class=\"box\">iPhoneで<br>撮った写真</div><div class=\"arrow\">→</div>\n <div class=\"box\" style=\"flex:1.5\">AIに聞く<br><span style=\"font-size:32px;white-space:nowrap\" class=\"muted\">「ブロックを掴めていますか」</span></div><div class=\"arrow\">→</div>\n <div class=\"box\" style=\"border:4px solid {BLUE}\"><span class=\"blue\">はい</span>/いいえ</div>\n </div>\n <div class=\"h3\" style=\"margin-top:48px\">ただし、<span class=\"red\">答えが返る ≠ 毎回正しい</span></div>\n</div>\"\"\", tag=T2)\n\nS[\"hard\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">難しかった</div>\n <div class=\"h1\">動かすほう</div>\n</div>\"\"\", tag=T2)\n\nS[\"gap\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">エイイチの設定で、同じ関節の角度を入れたときの爪先の位置</div>\n <div style=\"position:relative;height:400px;margin-top:10px\">\n <div style=\"position:absolute;left:60px;top:250px;right:60px;height:4px;background:{RULE}\"></div>\n <div style=\"position:absolute;left:150px;top:218px;width:68px;height:68px;border-radius:50%;background:#fff;border:8px solid {SUB}\"></div>\n <div style=\"position:absolute;left:4px;top:310px;width:440px;text-align:center;font-size:36px;font-weight:700;color:{SUB}\">計算ツール(ikpy)</div>\n <div style=\"position:absolute;right:150px;top:218px;width:68px;height:68px;border-radius:50%;background:{RED}\"></div>\n <div style=\"position:absolute;right:4px;top:310px;width:440px;text-align:center;font-size:36px;font-weight:700;color:{RED}\">デジタルツイン</div>\n <div style=\"position:absolute;left:184px;right:184px;top:140px;height:4px;background:{INK}\"></div>\n <div style=\"position:absolute;left:182px;top:120px;width:4px;height:44px;background:{INK}\"></div>\n <div style=\"position:absolute;right:182px;top:120px;width:4px;height:44px;background:{INK}\"></div>\n <div style=\"position:absolute;left:0;right:0;top:20px;text-align:center;font-size:72px;font-weight:900\"><span class=\"red\">30cm以上</span> 食い違う</div>\n </div>\n <!-- 2026-10-12 説明の1行が右にはみ出して「グリッパ/ー」で折り返していたので、図の下に2行で中央にそろえた(DAY330 で足した説明。内容は同じ) -->\n <div class=\"h3\" style=\"text-align:center;font-size:38px;margin-top:-10px\">アームが外れた距離ではない。<span class=\"red\">爪先の位置の決め方</span>が違っていた</div>\n <div class=\"h3 muted\" style=\"text-align:center;font-size:32px;margin-top:10px\">計算ツールには、グリッパーの形が入っていない</div>\n</div>\n<div class=\"src\">図は模式図</div>\"\"\", tag=T2)\n\nS[\"silent\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">黙る失敗</div>\n <div class=\"flow\">\n <div class=\"box\">肩の関節に<br><span class=\"red\">90度ちょっと</span>を超える値</div><div class=\"arrow\">→</div>\n <div class=\"box\">エラーは出ない<br>アームも動かない</div>\n </div>\n <div class=\"h3 muted\" style=\"margin-top:44px\">エイイチの機体とソフトの組み合わせで起きたこと</div>\n</div>\"\"\", tag=T2)\n\nS[\"point2\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">この2つは</div>\n <div class=\"h2 muted\" style=\"text-decoration:line-through;text-decoration-thickness:5px\">AIの学習データが足りない</div>\n <div class=\"h1\" style=\"margin-top:30px\"><span class=\"red\">すり合わせ</span>の仕事</div>\n <div class=\"h3\" style=\"margin-top:20px\">モデルや制御が、自分の機体と噛み合っているかを確かめる</div>\n</div>\"\"\", tag=T2)\n\nS[\"years\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">画像と言葉を扱うAIの学習資料を、人が読む・見る時間に換算すると</div>\n <div class=\"hero\">約10<small>万年規模</small></div>\n</div>\n<div class=\"src\">出典:Ken Goldberg, Science Robotics(2025年8月, Editorial)</div>\"\"\", tag=T3)\n\nS[\"wall\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"h1\">10万年の<span class=\"red\">データの壁</span></div>\n <div style=\"margin-top:50px\">{two(\"画像と言葉\", \"大量にある\", \"動作と結びついた経験\", \"集めにくい\", size=58)}</div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">※ロボットにも同じ量が必要だと分かったわけではない</div>\n</div>\n<div class=\"src\">出典:Ken Goldberg, Science Robotics(2025年8月)</div>\"\"\", tag=T3)\n\nS[\"gen\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">Generalist が集めた、人の手作業の記録(会社の発表)</div>\n <div style=\"display:flex;flex-direction:column;gap:30px;margin-top:10px\">\n <div style=\"display:flex;align-items:center;gap:30px\"><div style=\"width:260px;font-size:36px;font-weight:700;color:{SUB}\">2025年11月</div>\n <div style=\"width:{int(1060*27/50)}px;height:110px;background:#D3D3CE;border-radius:0 6px 6px 0\"></div><div style=\"font-size:52px;font-weight:900;white-space:nowrap\">27万時間</div></div>\n <div style=\"display:flex;align-items:center;gap:30px\"><div style=\"width:260px;font-size:36px;font-weight:700;color:{SUB}\">2026年4月</div>\n <div style=\"width:1060px;height:110px;background:{RED};border-radius:0 6px 6px 0\"></div><div style=\"font-size:52px;font-weight:900;white-space:nowrap\">50万時間超</div></div>\n </div>\n <div class=\"h3\" style=\"margin-top:44px\">安い装着型の機材で記録</div>\n</div>\n<div class=\"src\">出典:Generalist(GEN-0 2025年11月/GEN-1 2026年4月)</div>\"\"\", tag=T3)\n\nS[\"gen2\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">Generalist GEN-1.5(2026年8月)</div>\n <div class=\"h2\">簡単で短い作業なら、<br><span class=\"red\">数秒の実演</span>で新しい作業をこなす例</div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">本人たちも:作業は簡単で短く、成功率はまだ控えめ</div>\n</div>\n<div class=\"src\">出典:Generalist「GEN-1.5」(2026年8月19日)</div>\"\"\", tag=T3)\n\nS[\"compare\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">作業を記録した時間と、10万年は<br><span class=\"red\">そのまま比べられない</span></div>\n <div class=\"h3\" style=\"margin-top:50px\"><span class=\"muted\">何倍足りないか</span> より <span class=\"red\">どれだけ広く集められるか</span></div>\n</div>\"\"\", tag=T3)\n\nS[\"dirt\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">壁はまだ高い。でも、<br><span class=\"red\">壁の前で土を積み始めている</span></div>\n <ul class=\"list\" style=\"margin-top:30px\">\n <li style=\"font-size:40px\"><span class=\"n\">・</span>1X:家の中で遠隔で手伝いながら、動きを覚えさせる</li>\n <li style=\"font-size:40px\"><span class=\"n\">・</span>Generalist:人の手作業を装着型の機材で記録する</li>\n </ul>\n</div>\"\"\", tag=T3)\n\nS[\"notall\"] = page(f\"\"\"\n<div class=\"wrap\">\n {two(\"写真を見せて判断させる入口\", \"もう普通に使える\", \"物を確実に動かす\", \"すり合わせとデータが<br>まだ要る\", size=56)}\n <div class=\"h3\" style=\"margin-top:48px\">「まだ全然ダメ」でも「もう来た」でもない</div>\n</div>\"\"\", tag=T3)\n\nS[\"q\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h1\">ロボットのニュースを見る<br><span class=\"red\">問いが2つ</span>に変わる</div>\n</div>\"\"\", tag=T4)\n\nS[\"q1\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">問い 1</div>\n <div class=\"h1\">自律で動いているのか、<br>人が遠隔で動かしているのか</div>\n <div class=\"h3 muted\" style=\"margin-top:36px\">デモ動画で見るところ:自律か遠隔か/再生速度/人が手を出した条件</div>\n</div>\"\"\", tag=T4)\n\nS[\"q2\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">問い 2</div>\n <div class=\"h2\" style=\"margin-bottom:40px\">その仕事には、どちらが要るのか</div>\n {two(\"見て判断する\", \"手軽に試せる<br><span style='font-size:34px;color:#888888'>任せる範囲は現場で確かめる</span>\", \"触れて動かす精度\", \"現場の機体と環境に<br>すり合わせる仕事が残る\", size=48, note1=\"例:棚の商品が足りているか見る\", note2=\"例:柔らかい物を潰さずに掴む\")}\n</div>\"\"\", tag=T4)\n\nS[\"gofe\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">ゴールドバーグさんの近道</div>\n <div class=\"flow\">\n <div class=\"box\">昔ながらの工学<br>+学習</div><div class=\"arrow\">→</div>\n <div class=\"box\">現場で役に立つ<br>仕事をする</div><div class=\"arrow\">→</div>\n <div class=\"box\">働きながら<br>データが増える</div>\n </div>\n <div class=\"h3\" style=\"margin-top:48px\">エイイチの机の上:組み合わせる点は通じる。<span class=\"muted\">規模も段階もまだ違う</span></div>\n</div>\"\"\", tag=T4)\n\nS[\"motion\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">データの話と並行して要るもの</div>\n <div class=\"h1\"><span class=\"red\">どう動くか</span>の設計</div>\n <div class=\"h3\" style=\"margin-top:36px\">ロボットが人に物を渡すとき、<br>何秒待って手を開けば、人は安心して受け取れるのか</div>\n <div class=\"h3 muted\" style=\"margin-top:28px\">成功か失敗かの判定だけでは出てこない</div>\n</div>\"\"\", tag=T4)\n\nS[\"answer2\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">冒頭の問いへの答え</div>\n {two(\"写真を見せて判断させる入口\", \"ずっと簡単になった\", \"物を確実に動かす\", \"すり合わせが<br>まだ必要\", size=58)}\n <div class=\"h3\" style=\"margin-top:44px\">負担を減らす研究は急速に進んでいる。<br>家で家事を任せられる転換点かは、<span class=\"red\">まだ判断できない</span></div>\n</div>\"\"\")\n\nS[\"action\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">ロボットのデモ動画を見たら</div>\n <div class=\"h1\">自律なのか、何回やって、<br><span class=\"red\">何回うまくいった</span>のか</div>\n</div>\"\"\")\n\nS[\"sources\"] = page(\"\"\"\n<div class=\"wrap\" style=\"left:420px;bottom:90px;justify-content:flex-start;top:120px\">\n <div class=\"h2\" style=\"margin-bottom:30px\">出典</div>\n <div class=\"p\" style=\"font-size:29px;line-height:1.75\">\n 人型ロボットの出荷:Counterpoint Research(2026年8月)/Smart Analytics Global(2026年8月)<br>\n 1X NEO:1X 公式の発表・注文ページ(2026年10月12日確認)<br>\n 10万年のデータの壁:Ken Goldberg, Science Robotics(2025年8月, Editorial)<br>\n Generalist:GEN-0(2025年11月)、GEN-1(2026年4月)、GEN-1.5(2026年8月)の公式発表<br>\n ほかの機体から学ぶ研究の例:Gemini Robotics 1.5 技術報告(Google DeepMind)<br>\n 実機の数字:Pixels to Motors の台帳(pick_place_ledger_v3.csv)、公開記事 DAY257<br>\n <span class=\"muted\">ナレーションは AI 音声(ElevenLabs)。話しているのは Pixels to Motors のキャラクター、ハコブくん</span>\n </div>\n</div>\"\"\")\n\n\n# --- 強弱をつける場面の上書き(2026-10-11)---\nS[\"hook_question\"] = punch(\"こういうニュースを見ると\", [\"ロボットも\", \"一気に賢くなる?\"])\nS[\"answer\"] = punch(\"先に結論\", ['見て判断する入口は<span class=\"blue\">簡単に</span>', '物を動かすのは<span class=\"red\">まだ手間</span>'], sub=\"手間は2つ:動作と結びついたデータ/機体や環境とのすり合わせ\", size=110)\nS[\"c1\"] = chapter2(\"1つ目\", \"出荷の中身\", \"出荷が4倍になったロボットは、何をしているのか\")\nS[\"c2\"] = chapter2(\"2つ目\", \"机の上の実感\", \"何が簡単で、何が難しかったのか\")\nS[\"c3\"] = chapter2(\"3つ目\", \"データの壁\", \"どれくらい高くて、誰がどう越えようとしているのか\")\nS[\"c4\"] = chapter2(\"最後に\", \"ニュースの見方\", \"エイイチの見立て\")\nS[\"china\"] = dots(97, \"2026年上半期の人型ロボット出荷のうち\", \"中国メーカー\", \"97%\", note=\"SAGの推計では「97%以上」\", src=\"出典:Smart Analytics Global(2026年8月)\", tag=T1)\nS[\"point2\"] = punch(\"この2つは\", [\"データ不足じゃない\", '<span class=\"red\">すり合わせ</span>の仕事'], sub=\"モデルや制御が、自分の機体と噛み合っているかを確かめる\", tag=T2)\nS[\"wall\"] = punch(\"ゴールドバーグさんの問題提起\", ['10万年の', '<span class=\"red\">データの壁</span>'], sub=\"動作と結びついた経験は集めにくい(同じ量が必要と分かったわけではない)\", kc=\"muted\", tag=T3)\nS[\"compare\"] = punch(\"ひとつ注意\", ['<span class=\"muted\">何倍足りないか</span>より', '<span class=\"red\">どれだけ広く</span>集めるか'], sub=\"作業を記録した時間と10万年は、そのまま比べられない\", size=110, tag=T3)\nS[\"answer2\"] = punch(\"冒頭の問いへの答え\", ['判断する入口は<span class=\"blue\">来た</span>', '家事を任せるのは<span class=\"red\">まだ</span>'], sub=\"負担を減らす研究は急速に進んでいる\", size=110)\n\nO = {\n # 2026-10-12 ハコブくんが話す間は番組とハコブくんの名前、エイイチを紹介する文からエイイチの名前を出す(エイイチの指示)\n \"title\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">フィジカルAI解説</div><div class=\"c\">案内役:ハコブくん(AI音声)</div></div>'),\n \"title_eiichi\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">エイイチ</div><div class=\"c\">デザイナー/ロボティクスの研究・開発</div></div>'),\n \"v_real\": overlay('<div class=\"cap\">実機:mechArm 270-Pi 赤いブロックを掴んで箱に置く</div>'),\n \"v_twin\": overlay('<div class=\"cap\">パソコンの中のアームの分身(Unity のデジタルツイン)</div>'),\n \"v_scan\": overlay('<div class=\"cap\">デジタルツインの中で動かして、関節の角度と爪先の位置の対応表を作る</div>'),\n \"outro\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">相談は概要欄の連絡先から</div><div class=\"c\">高評価・チャンネル登録/コメントはエイイチが全部読んでいます</div></div>'),\n}\n\n# 動画シーン: (ソースファイル, 開始秒, 透かし消しの有無)\n# src/ の中身(元は iCloud の 動画/ にある。2026-10-12 前の版の動画と1コマずつ照らして確かめた):\n# c1.mov = DAY123.MOV / c2.mov = DAY124.mov(どちらも撮ったままの元の映像。DAY123.mp4・DAY124.mp4 は Clideo の透かし入りで、透かしを消すと右下がモヤモヤになったので使わない。2026-10-12)/ c3.mov = デジタルツイン_260416.mov(アームが並ぶ画面)/ c4.mov = ロボットアーム失敗_260416.mov(アーム1台と箱)\nVIDEO = {\n \"title\": (\"src/c1.mov\", 0.0, False),\n \"title_eiichi\": (\"src/c1.mov\", None, False), # None = 前の場面(title)の映像の続きから\n \"v_real\": (\"src/c2.mov\", 22.0, False),\n \"v_twin\": (\"src/c4.mov\", 0.0, False),\n \"v_scan\": (\"src/c3.mov\", 0.0, False),\n \"outro\": (\"src/c1.mov\", 8.0, False),\n}\n</code></pre><h3 id=\"index_53PHekBl\">フィジカルAIの現在地/sound.py</h3><p>BGM と効果音の置き方(23行)。</p><pre><code class=\"language-python\"># 音の置き方(audio_mix.py が読む。書き方は audio_mix.py の先頭)\n# 2026-10-12 冒頭の BGM は ElevenLabs Music で作った bgm_明るいB_2(エイイチが選んだ。説明文は sound/作った記録.jsonl)\n\n# 大きさ: 話している間はナレーションより23dB小さく、間は16dB小さく(2026-10-12 エイイチの指示。-22/-15 から約90%に下げた。明るい曲では -15/-9 だと大きすぎた)\nBGM = [\n # 2026-10-12 仕上げ版: 最初から最後まで同じ曲をループで流す(本編・ジングル用の曲は未定)\n dict(file='sound/bgm_明るいB_2.mp3', start=0, end=None, level=-23, gap=-16, fade_in=0.6, fade_out=3.0),\n]\n\nSE = [\n # (文番号, 文の頭から何秒後, 音, ナレーションより何dB)\n # 2026-10-12 エイイチの指示で、効果音は場面切り替えのホワーンだけにした(始まりの「しゅっ」は外した)\n]\n\n# 場面が切り替わるたびに「ホワーン」(se_rise)を入れる。効果音はこれだけ(2026-10-12 エイイチの指示)\nSCENE_SE = dict(file='se_rise', level=-6, offset=-0.15, skip=['title_eiichi']) # 2026-10-12 エイイチの指示で -12 から 6dB 上げた。title→title_eiichi は同じ映像が続くので鳴らさない\n\n# 大きな流れの変わり目(自己紹介・今日の問い・各章の始まり・答え・アウトロ)は、ホワーンの代わりに別の音を鳴らす。\n# 直前の文の後ろに 1.8 秒の間がある(narration.py)。2026-10-12 エイイチの指示\n# 自己紹介(title): キラリン(se_chapter_glock。鉄琴の高い3音)… 2026-10-12 エイイチが決定\n# そのほか: ポロロン(se_chapter_marimba。マリンバの上がる3音)… 試し中。場所ごとの使い分けはこれから決める\nCHAPTER_SE = dict(file='se_chapter_marimba', files={'title': 'se_chapter_glock'}, level=-8, offset=-0.3,\n scenes=['title', 'question', 'c1', 'c2', 'c3', 'c4', 'answer2', 'outro'])\n</code></pre><h3 id=\"index_uVDSAdpJ\">フィジカルAIの現在地/thumb.py</h3><p>サムネイルの文言と写真(14行)。</p><pre><code class=\"language-python\"># サムネイルの文言と背景(thumbnail.py が読む。書き方は thumbnail.py の先頭)\n# 2026-10-12 動画と同じ文字・色で作る(エイイチの指示)。文言はタイトル案1に合わせた\nPHOTO = 'サムネ素材/素材2_実機のアーム.png' # 実機のアームが赤いブロックを掴む瞬間(DAY124.mov の28秒)\nPHOTO_POS = '6% 50%' # split の右の面で、ブロックを掴む爪先からアームの根元までが入る位置\nHAKOBU = True\nLABEL = '本当に?'\nKICKER = '人型ロボット出荷 約4倍'\nLINES = ['ロボットにも', '<span class=\"red\">ChatGPTの瞬間</span>', 'もう来た?']\nVARIANTS = ['photo3'] # 2026-10-12 背景を全部写真に(エイイチの指示。dark/dark2/dark3 → photo2 → photo3)\n# photo3 の写真: ハコブくんを左下の隅に置くので、アームが箱の上にいるコマ(左下のマットが空く)。DAY124.mov の39.8秒\nPHOTO3 = 'サムネ素材/素材3_箱に置く瞬間.png'\nHK_H = 600 # ハコブくんの高さ(1920×1080 のとき。photo2 の 520 より少し大きく)\nPHOTO_POS_FULL = '50% 50%'\nBOLD_LINES = ['人型ロボット出荷 約4倍', 'ChatGPTの瞬間', 'もう来た?']\n</code></pre><h3 id=\"index_Wfwum3Uk\">フィジカルAIの現在地/概要欄_下書き.md</h3><p>概要欄の下書き(67行)。</p><pre><code class=\"language-markdown\"><!--\nYouTube の概要欄の下書き(2026-10-12)。{t:文番号} は、その文が始まる時間に置きかわる。\n全体を書き出したあと、python3 <_制作ベース>/description.py で 概要欄.txt を作り、それを貼る。\n「【要記入:…】」はエイイチが決めて書く。\n-->\n今年の上半期に出荷された人型ロボットは、世界で2万2000台を超えました。前の年の同じ時期の、およそ4倍です(Counterpoint の推計)。\n\nロボットにも、ChatGPT のような瞬間はもう来たのか。\n\n先に答えを言うと、写真を見せて判断させる入口は、ぐっと簡単になりました。でも、物を確実に動かすところは、まだ手間がかかります。手間は2つ。動作と結びついたデータが集めにくいこと。そして、機体や環境とのすり合わせです。\n\nこの動画では、\n\n・出荷が4倍になったロボットは、何をしているのか\n・机の上のロボットアームで、何が簡単で、何が難しかったのか\n・データの壁はどれくらい高くて、誰がどう越えようとしているのか\n\nの3つを見たうえで、ロボットのニュースを見るときの2つの問いを解説します。\n\n▼目次\n0:00 人型ロボットの出荷は4倍。ロボットにもChatGPTの瞬間?\n{t:6} この番組について\n{t:9} 今日の問いと、先に結論\n{t:26} 1. 出荷の中身:4倍になったロボットは何をしているのか\n{t:53} 2. 机の上の実感:何が簡単で、何が難しかったのか\n{t:83} 3. データの壁:どれくらい高く、誰がどう越えようとしているのか\n{t:111} エイイチの見立て:ニュースを見るときの2つの問い\n{t:132} 冒頭の問いへの答え\n{t:141} エンディング\n\n▼出典\n・人型ロボットの出荷:Counterpoint Research(2026年8月20日)の推計\nhttps://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026\n・同じく出荷の推計:Smart Analytics Global(2026年8月)\n・1X NEO:1X 公式の製品・注文ページ(2026年10月12日に確認。一般のお客さんへの配送開始を示す公式発表は、この日の時点で見つかっていない)\nhttps://www.1x.tech/neo\n・10万年のデータの壁:Ken Goldberg「GOFE can close the 100,000-year robot data gap」Science Robotics(2025年8月, Editorial)\nhttps://doi.org/10.1126/scirobotics.aea7390\n・Generalist の会社の公式発表:GEN-0(2025年11月)、GEN-1(2026年4月)、GEN-1.5(2026年8月)\nhttps://generalistai.com/blog/gen-0\nhttps://generalistai.com/blog/gen-1\nhttps://generalistai.com/blog/gen-1.5\n・ほかの機体から学ぶ研究の例:Gemini Robotics 1.5 技術報告(Google DeepMind)\nhttps://arxiv.org/abs/2510.03342\n・実機の数字:Pixels to Motors の試行の台帳(pick_place_ledger_v3.csv)、公開記事 DAY257\nhttps://adaptdesign.jp/pixelstomotors/record/why-z-correction-failed-twin-poses\n<!-- 2026-10-12 リンクを確認: Counterpoint・1X・Generalist の3本・arXiv はページが開くことを確かめた。Science Robotics は DOI(バークレーの研究室が公開している PDF のファイル名と同じ番号)。Smart Analytics Global は公式の発表ページが見つからなかったので、リンクなし -->\n\n▼この番組について\n「フィジカルAI解説|Pixels to Motors」は、フィジカルAIを、現場で使える形にするまでを解説する番組です。\nエイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。その中で分かったことと、そこから考えた見立てを、案内役のハコブくんがお伝えします。\n\n・ナレーションは AI 音声(ElevenLabs)です。話しているのは Pixels to Motors のキャラクター、ハコブくんです\n・BGM は ElevenLabs Music で作った曲、効果音はコードで作った音です\n\n▼ロボットの開発・導入のご相談\nメール:pixelstomotors@adaptdesign.jp\n\n▼リンク\nWebサイト:https://adaptdesign.jp/pixelstomotors\nGitHub:https://github.com/PixelstoMotors\nX:https://x.com/adaptdesignjp\nLinkedIn:https://www.linkedin.com/in/eiichi-shimazu\n\nチャンネル登録・高評価もよろしくお願いします。コメントは、エイイチが全部読んでいます。\n\n#フィジカルAI #人型ロボット #ロボット\n</code></pre><h3 id=\"index_cfdrc8jp\">フィジカルAIの現在地/plot_ducking.py</h3><p>この記事の BGM のグラフを描いたスクリプト(69行)。</p><pre><code class=\"language-python\">\"\"\"記事 DAY333 の図: 20〜80秒のナレーションの波形と、audio_mix.py が決める BGM の大きさを並べて描く。\n使い方: 動画の作業フォルダで python3 plot_ducking.py\n\"\"\"\nimport sys\nimport numpy as np\nsys.path.insert(0, '.')\nsys.path.insert(1, '../_制作ベース')\nsys.path.insert(2, '../base')\nfrom timing import load, layout, narration, FPS, SR\nimport audio_mix as am\nimport matplotlib\nmatplotlib.use('Agg')\nimport matplotlib.pyplot as plt\nfrom matplotlib import font_manager\n\nFONT = '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc'\nfont_manager.fontManager.addfont(FONT)\nplt.rcParams['font.family'] = font_manager.FontProperties(fname=FONT).get_name()\n\nidx = load(None)\nlayout(idx)\nvoice = narration(idx).astype(np.float32) / 32768\ntotal = len(voice) / SR\n\n# audio_mix.py の mix() と同じ手順で「話している所」を決める\nctl = np.zeros(int(total * 100) + 1)\nfor o in idx:\n if o['text']:\n a = max(0, int((o['start'] / FPS - 0.15) * 100))\n b = int(((o['start'] + o['speech']) / FPS + 0.3) * 100)\n ctl[a:b] = 1\ngaps = np.where(np.diff(np.concatenate([[1], ctl, [1]])) != 0)[0].reshape(-1, 2)\nfor a, b in gaps:\n if b - a < 50:\n ctl[a:b] = 1\nctl = am.smooth(ctl, 0.3)\nlv = 20 * np.log10(am.db(-23) * ctl + am.db(-16) * (1 - ctl))\n\nt0, t1 = 20, 80\nt = np.arange(len(ctl)) / 100\nm = (t >= t0) & (t <= t1)\nfig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 5.2), sharex=True, gridspec_kw={'height_ratios': [1, 1.2]})\ns0, s1 = int(t0 * SR), int(t1 * SR)\nv = voice[s0:s1]\nstep = 200\nenv = np.abs(v[:len(v) // step * step].reshape(-1, step)).max(1)\ntv = t0 + np.arange(len(env)) * step / SR\nax1.fill_between(tv, -env, env, color='#0d0d0d', lw=0)\nax1.set_ylabel('ナレーション')\nax1.set_yticks([])\nax2.plot(t[m], lv[m], color='#C4471F', lw=2)\nax2.set_ylim(-25, -14)\nax2.set_yticks([-23, -16])\nax2.set_yticklabels(['-23dB 話している間', '-16dB 0.8秒以上の間'])\n# 効果音の位置(assemble_full.log の時刻)\nses = [(22.78, 'ホワーン'), (33.40, 'キラリン'), (55.20, 'ポロロン'), (71.38, 'ホワーン')]\nfor x, l in ses:\n if t0 <= x <= t1:\n for a in (ax1, ax2):\n a.axvline(x, color='#1A6C98', lw=1, ls='--')\n ax2.text(x + 0.4, -14.6, l, color='#1A6C98', fontsize=10)\nax2.set_xlabel('動画の時間(秒)')\nfor a in (ax1, ax2):\n for sp in ('top', 'right'):\n a.spines[sp].set_visible(False)\nfig.suptitle('「フィジカルAIの現在地」20〜80秒:話している間は BGM を下げ、間で戻す(audio_mix.py)', fontsize=12)\nfig.tight_layout()\nfig.savefig('DAY333_02_bgm_ducking.png', dpi=150, facecolor='#F5F5F0')\nprint('DAY333_02_bgm_ducking.png')\n</code></pre><p></p><h2 id=\"index_MBHw9jHD\">書き出しのログと測った数値</h2><h3 id=\"index_Q1bHZ06h\">assemble.py の書き出しのログ(全量)</h3><p>最後に全体を書き出したときのログです。1行目が BGM と効果音の数、そのあとが効果音を鳴らした時刻・文番号・音・大きさ・その文の先頭16字、最後の行が全体のコマ数と確認用ファイルの大きさです。145番の文は、字幕のないエンディングの文です。</p><pre><code>BGM 1本・効果音 45個 → mix.wav\n 13.25秒 文002 se_rise -6dB 「年末までには、5万台を超えると予」\n 22.78秒 文004 se_rise -6dB 「こういうニュースを見ると、Cha」\n 33.40秒 文006 se_chapter_glock -8dB 「Pixels to Motors」\n 55.20秒 文009 se_chapter_marimba -8dB 「今日の問いは、ロボットにもCha」\n 71.38秒 文012 se_rise -6dB 「フィジカルAIっていうのは、現実」\n 85.32秒 文015 se_rise -6dB 「先に結論を言っちゃうと、写真を見」\n 106.02秒 文019 se_rise -6dB 「先に言っておくと、フィジカルAI」\n 117.05秒 文021 se_rise -6dB 「大きく3つあります。」\n 137.90秒 文026 se_chapter_marimba -8dB 「1つ目、出荷が4倍になったロボッ」\n 144.62秒 文027 se_rise -6dB 「Counterpointの推計で」\n 155.95秒 文029 se_rise -6dB 「データ生産っていうのは、ロボット」\n 181.92秒 文034 se_rise -6dB 「一方で、別の調査会社のSmart」\n 207.78秒 文039 se_rise -6dB 「もう1つ、SAGの推計では、出荷」\n 215.22秒 文040 se_rise -6dB 「家のほうも見てみます。」\n 251.52秒 文047 se_rise -6dB 「これ、ちょっと考えさせられません」\n 262.98秒 文050 se_rise -6dB 「工場でも家でも、使いながらデータ」\n 276.63秒 文053 se_chapter_marimba -8dB 「2つ目、エイイチの机の上のロボッ」\n 285.45秒 文054 se_rise -6dB 「ここは、エイイチ自身の話から始め」\n 298.85秒 文057 se_rise -6dB 「それだけの作業を、失敗も含めて6」\n 306.35秒 文058 se_rise -6dB 「まず、簡単だったほうから話します」\n 332.85秒 文063 se_rise -6dB 「難しかったのは、動かすほうでした」\n 336.58秒 文064 se_rise -6dB 「ロボットアームの爪先がどこに来る」\n 347.92秒 文066 se_rise -6dB 「パソコンの中に作ったアームの分身」\n 353.82秒 文067 se_rise -6dB 「エイイチの設定では、同じ関節の角」\n 373.52秒 文070 se_rise -6dB 「そこでエイイチは、分身の中でアー」\n 387.08秒 文073 se_rise -6dB 「もう1つ困ったのが、黙る失敗です」\n 404.65秒 文077 se_rise -6dB 「でも、ポイントはここなんです。」\n 434.47秒 文083 se_chapter_marimba -8dB 「3つ目、データの壁はどれくらい高」\n 442.28秒 文084 se_rise -6dB 「カリフォルニア大学バークレー校の」\n 459.62秒 文088 se_rise -6dB 「それに比べて、ロボットが手を動か」\n 488.68秒 文094 se_rise -6dB 「アメリカのGeneralistと」\n 505.52秒 文097 se_rise -6dB 「さらに8月には、簡単で短い作業な」\n 527.92秒 文101 se_rise -6dB 「ここで1つ、注意があります。」\n 542.65秒 文104 se_rise -6dB 「つまり、壁はまだ高い。でも、壁の」\n 561.08秒 文107 se_rise -6dB 「ただ、ここで「なんだ、まだ全然ダ」\n 581.20秒 文111 se_chapter_marimba -8dB 「ここからは、エイイチの見立てです」\n 585.28秒 文112 se_rise -6dB 「ロボットのニュースを見たときの問」\n 590.05秒 文113 se_rise -6dB 「1つは、そのロボットは自律で動い」\n 605.52秒 文116 se_rise -6dB 「もう1つは、その仕事は「見て判断」\n 634.32秒 文122 se_rise -6dB 「ゴールドバーグさんは、昔ながらの」\n 657.38秒 文126 se_rise -6dB 「作る側から見ると、もう1つありま」\n 684.30秒 文132 se_chapter_marimba -8dB 「じゃあ、冒頭の問いに答えますね。」\n 710.45秒 文137 se_rise -6dB 「ロボットのデモ動画を見たら、」\n 725.13秒 文141 se_chapter_marimba -8dB 「今日はここまでです。」\n 742.95秒 文145 se_rise -6dB 「」\nframes 22473 = 749.1s 確認用 29.4MB (crf 29)\n</code></pre><h3 id=\"index_vKASe5en\">上げたファイルの中身(ffprobe)</h3><pre><code>$ ffprobe -v error -show_entries format=duration,bit_rate:stream=codec_name,width,height,r_frame_rate,sample_rate,bit_rate -of default=nw=1 out.mp4\ncodec_name=h264\nwidth=1920\nheight=1080\nr_frame_rate=30/1\nbit_rate=488631\ncodec_name=aac\nsample_rate=48000\nr_frame_rate=0/0\nbit_rate=203703\nduration=749.100000\nbit_rate=701165\n</code></pre><p>1つ目の codec_name から bit_rate までが映像(488,631 ビット/秒)、2つ目が音声(AAC・48kHz・203,703 ビット/秒)、最後の duration と bit_rate がファイル全体です。</p><h3 id=\"index_gpWY2NxQ\">音の大きさ(ffmpeg の ebur128)</h3><pre><code>$ ffmpeg -nostats -i out.mp4 -af ebur128 -f null - 2>&1 | grep -A8 \"Summary\"\n[Parsed_ebur128_0 @ 0x5569ec070940] Summary:\n\n Integrated loudness:\n I: -16.0 LUFS\n Threshold: -26.4 LUFS\n\n Loudness range:\n LRA: 3.1 LU\n Threshold: -36.4 LUFS\n</code></pre><h3 id=\"index_DJoi5KGb\">ファイルの指紋(md5)</h3><pre><code>$ md5sum out.mp4 out_確認用.mp4\nd9f6def3f1a01d9aee5c76c9d6516bc0 out.mp4\n06c2afe18606354bd366a2e4224fab31 out_確認用.mp4\n</code></pre><h3 id=\"index_uLT3z9rw\">作った曲の記録(sound/作った記録.jsonl・全量)</h3><p><code>sound_eleven.py</code> が、曲を作るたびに1行ずつ書き足す記録です。prompt が ElevenLabs Music に渡した文章です。</p><pre><code>{\"file\": \"sound/bgm_本編_1.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Warm, gentle instrumental background music recorded with real acoustic instruments, for a thoughtful explainer about robots and AI. Felt piano and fingerpicked acoustic guitar, soft cello and violin, upright bass, very quiet brushed drums. Natural room sound, like a small live session. Around 90 BPM, calm and hopeful, a touch of color but serious in tone. No synthesizers, no electronic sounds, no vocals. A simple melody that stays out of the narrator's way.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:26\", \"song-id\": \"angU6pw2wgogpMCVOVzl\"}\n{\"file\": \"sound/bgm_本編_2.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Warm, gentle instrumental background music recorded with real acoustic instruments, for a thoughtful explainer about robots and AI. Felt piano and fingerpicked acoustic guitar, soft cello and violin, upright bass, very quiet brushed drums. Natural room sound, like a small live session. Around 90 BPM, calm and hopeful, a touch of color but serious in tone. No synthesizers, no electronic sounds, no vocals. A simple melody that stays out of the narrator's way.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:26\", \"song-id\": \"rGaTcW0xZPfECyYFmCtW\"}\n{\"file\": \"sound/bgm_冒頭_1.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Bright but gentle instrumental music played on real acoustic instruments, for the opening of a serious technology explainer. Acoustic guitar, piano, light pizzicato strings and soft marimba, natural room sound. Around 100 BPM, curious and hopeful, not cheesy. No synthesizers, no electronic sounds, no vocals, no dramatic hits.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:26\", \"song-id\": \"B0CIEZhLC948zw7GsAcP\"}\n{\"file\": \"sound/bgm_冒頭_2.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Bright but gentle instrumental music played on real acoustic instruments, for the opening of a serious technology explainer. Acoustic guitar, piano, light pizzicato strings and soft marimba, natural room sound. Around 100 BPM, curious and hopeful, not cheesy. No synthesizers, no electronic sounds, no vocals, no dramatic hits.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:27\", \"song-id\": \"5NbSMGUTgt0DXRjOTTB1\"}\n{\"file\": \"sound/bgm_明るいA_1.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Bright, warm and uplifting instrumental music played on real acoustic instruments, in a major key, for a friendly technology explainer about robots. Strummed and fingerpicked acoustic guitar, bright piano, glockenspiel accents, light hand claps and soft shaker, warm upright bass. Natural room sound. Around 105 BPM, cheerful and optimistic like a sunny morning, but not childish or cheesy. No synthesizers, no electronic sounds, no vocals. A simple melody that stays out of the narrator's way.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:31\", \"song-id\": \"Hd4Qnr113KBdkYe8qE9k\"}\n{\"file\": \"sound/bgm_明るいA_2.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Bright, warm and uplifting instrumental music played on real acoustic instruments, in a major key, for a friendly technology explainer about robots. Strummed and fingerpicked acoustic guitar, bright piano, glockenspiel accents, light hand claps and soft shaker, warm upright bass. Natural room sound. Around 105 BPM, cheerful and optimistic like a sunny morning, but not childish or cheesy. No synthesizers, no electronic sounds, no vocals. A simple melody that stays out of the narrator's way.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:31\", \"song-id\": \"bQUshWUbu4Y7vH4a43QI\"}\n{\"file\": \"sound/bgm_明るいB_1.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Hopeful, bright instrumental music on real acoustic instruments, in a major key, for a positive and curious explainer about the future of robots. Piano and acoustic guitar leading, warm strings, light pizzicato, a few glockenspiel notes, soft percussion. Natural room sound. Around 100 BPM, optimistic and forward-moving, bright but calm enough to sit under narration. No synthesizers, no electronic sounds, no vocals, no big build-ups.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:31\", \"song-id\": \"qyMIv2n4wM3BGvVoNqKR\"}\n{\"file\": \"sound/bgm_明るいB_2.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Hopeful, bright instrumental music on real acoustic instruments, in a major key, for a positive and curious explainer about the future of robots. Piano and acoustic guitar leading, warm strings, light pizzicato, a few glockenspiel notes, soft percussion. Natural room sound. Around 100 BPM, optimistic and forward-moving, bright but calm enough to sit under narration. No synthesizers, no electronic sounds, no vocals, no big build-ups.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:31\", \"song-id\": \"19pIxHwtXfiNptitO2FF\"}\n</code></pre><h2 id=\"index_azDryZvY\">書き出した概要欄(概要欄.txt・全量)</h2><p><code>description.py</code> が書き出し、YouTube に貼ったものです。</p><pre><code>今年の上半期に出荷された人型ロボットは、世界で2万2000台を超えました。前の年の同じ時期の、およそ4倍です(Counterpoint の推計)。\n\nロボットにも、ChatGPT のような瞬間はもう来たのか。\n\n先に答えを言うと、写真を見せて判断させる入口は、ぐっと簡単になりました。でも、物を確実に動かすところは、まだ手間がかかります。手間は2つ。動作と結びついたデータが集めにくいこと。そして、機体や環境とのすり合わせです。\n\nこの動画では、\n\n・出荷が4倍になったロボットは、何をしているのか\n・机の上のロボットアームで、何が簡単で、何が難しかったのか\n・データの壁はどれくらい高くて、誰がどう越えようとしているのか\n\nの3つを見たうえで、ロボットのニュースを見るときの2つの問いを解説します。\n\n▼目次\n0:00 人型ロボットの出荷は4倍。ロボットにもChatGPTの瞬間?\n0:33 この番組について\n0:55 今日の問いと、先に結論\n2:18 1. 出荷の中身:4倍になったロボットは何をしているのか\n4:36 2. 机の上の実感:何が簡単で、何が難しかったのか\n7:14 3. データの壁:どれくらい高く、誰がどう越えようとしているのか\n9:41 エイイチの見立て:ニュースを見るときの2つの問い\n11:24 冒頭の問いへの答え\n12:05 エンディング\n\n▼出典\n・人型ロボットの出荷:Counterpoint Research(2026年8月20日)の推計\nhttps://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026\n・同じく出荷の推計:Smart Analytics Global(2026年8月)\n・1X NEO:1X 公式の製品・注文ページ(2026年10月12日に確認。一般のお客さんへの配送開始を示す公式発表は、この日の時点で見つかっていない)\nhttps://www.1x.tech/neo\n・10万年のデータの壁:Ken Goldberg「GOFE can close the 100,000-year robot data gap」Science Robotics(2025年8月, Editorial)\nhttps://doi.org/10.1126/scirobotics.aea7390\n・Generalist の会社の公式発表:GEN-0(2025年11月)、GEN-1(2026年4月)、GEN-1.5(2026年8月)\nhttps://generalistai.com/blog/gen-0\nhttps://generalistai.com/blog/gen-1\nhttps://generalistai.com/blog/gen-1.5\n・ほかの機体から学ぶ研究の例:Gemini Robotics 1.5 技術報告(Google DeepMind)\nhttps://arxiv.org/abs/2510.03342\n・実機の数字:Pixels to Motors の試行の台帳(pick_place_ledger_v3.csv)、公開記事 DAY257\nhttps://adaptdesign.jp/pixelstomotors/record/why-z-correction-failed-twin-poses\n\n▼この番組について\n「フィジカルAI解説|Pixels to Motors」は、フィジカルAIを、現場で使える形にするまでを解説する番組です。\nエイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。その中で分かったことと、そこから考えた見立てを、案内役のハコブくんがお伝えします。\n\n・ナレーションは AI 音声(ElevenLabs)です。話しているのは Pixels to Motors のキャラクター、ハコブくんです\n・BGM は ElevenLabs Music で作った曲、効果音はコードで作った音です\n\n▼ロボットの開発・導入のご相談\nメール:pixelstomotors@adaptdesign.jp\n\n▼リンク\nWebサイト:https://adaptdesign.jp/pixelstomotors\nGitHub:https://github.com/PixelstoMotors\nX:https://x.com/adaptdesignjp\nLinkedIn:https://www.linkedin.com/in/eiichi-shimazu\n\nチャンネル登録・高評価もよろしくお願いします。コメントは、エイイチが全部読んでいます。\n\n#フィジカルAI #人型ロボット #ロボット\n</code></pre><h2 id=\"index_zetfem2o\">BGM と効果音の候補を調べた表(音源_候補.md・全量)</h2><p>規約は 2026年10月12日に公式のページで確かめた内容です。規約は変わるので、使う前にもう一度確かめます。</p><pre><code># BGM と効果音:どこから持ってくるか\n\n2026-10-12 作成。video_rules.md 8節「BGM・効果音」の判断材料。\n\n## 条件\n\n- YouTube で収益化しても使える\n- クレジット表記の条件を守れる(できれば表記なしで使える)\n- ほかのチャンネルと同じ曲になりにくい(声と同じ考え方。3-1)\n- ナレーションが聞き取れる音量に下げる。話している間は BGM を下げ、間で少し戻す(audio_mix.py が自動でやる)\n\n## 候補(規約は 2026-10-12 に公式のページで確かめた。変わるので、使う前にもう一度見る)\n\n| 用途 | 候補 | 収益化 | クレジット | 気をつけること |\n|---|---|---|---|---|\n| BGM(第一候補) | ElevenLabs Music(いまの Starter プランで使える) | 使える(映画・テレビ・ラジオ・ゲームの一部を除く) | Starter 以上はいらない(無料プランは必要) | 作れるのは月17分まで。曲そのものを音楽配信に出すのは禁止。新しく作る曲なので、ほかのチャンネルと同じ曲にならない |\n| BGM(安全側の控え) | YouTube オーディオライブラリ(YouTube Studio の中) | 使える。ここの曲には Content ID の申し立てが来ないと YouTube が書いている | 「帰属表示が不要」で絞り込めば、いらない | YouTube 以外(X・LinkedIn に切り抜きを載せるなど)での扱いは、ヘルプに書かれていない。多くのチャンネルが使うので、同じ曲になりやすい |\n| BGM(生の演奏・有料) | Audiostock(日本のサービス。人が演奏・録音した曲が多い) | YouTube で使える(動画配信者プランは本人のアカウントだけ) | 公式のライセンスのページに記載なし(使う前に確かめる) | 単品 1,210円〜(買い切り)。定額の動画配信者プランは月858円(年払い)。値段は公式ページの 2024年12月1日時点の表示。多くの曲が Content ID に登録されているが、ふつうは警告が出ない設定とのこと。定額で使った曲は、解約後に新しい動画では使えない(単品なら使える) |\n| BGM(控え) | OpenTracks(旧 DOVA-SYNDROME。2026年9月15日に名前が変わった) | 背景音楽としてなら商用も可 | いらない | 海外の団体が「音楽出版権」を不正に主張して、Content ID の申し立てが来る例があるとサイト自身が書いている(広告が付かない・収益の一部を取られることがある)。そのたびに異議を出す手間がかかる |\n| 効果音(第一候補) | コードで作る(sound_synth.py。もう作った) | 問題なし(自分で作った音) | いらない | 単純な電子音だけ。だれでも同じ音を作り直せるので、「再現できる」の考え方に合う |\n| 効果音(控え) | 効果音ラボ | 使える(「YouTube(収益化しても可)」と明記) | いらない | 効果音を Content ID に登録するのは禁止(他の人の利用に影響するため)。効果音だけの動画は作らない |\n| 効果音(控え) | ElevenLabs の効果音(sound_eleven.py sfx) | 有料プランで作った音は商用可 | いらない | 現実の音(モーター音・机に物を置く音など)が欲しいとき向き |\n\n外したもの: Pixabay の音楽(無料・クレジット不要だが、Content ID の扱いが公式のライセンス要約に書かれていない)。魔王魂・甘茶の音楽工房などのほかの無料サイトは、今回は調べていない。上の候補で足りなければ調べる。\n\n2026-10-12 エイイチ: BGM は生の楽器の音に近い方がよい。コードで作った BGM(bgm_1〜3・bgm_仮_温かみ)は方向を確かめる用にとどめる。\n\n## コードで作った効果音(_制作ベース/sound/。2026-10-12)\n\n| 名前 | 音 | 使う所 |\n|---|---|---|\n| se_pop | ぽん | 図・数字が出る。いちばんよく使う |\n| se_tick | こつ | 小さな切り替え。箇条書きが1つ出る |\n| se_whoosh | しゅっ | 章や場面が大きく変わる |\n| se_ding | ちん | 結論・大事な数字 |\n| se_rise | ひゅい | 驚き・問いかけ |\n| bgm_仮 | やわらかい和音と小さな粒の音(テンポ96・32秒でループ) | 本番の BGM ができるまでの仮。混ぜ方を確かめるためのもの |\n\n効果音は、ナレーションの声(おおむね 100〜300Hz)とぶつからないよう、500Hz より上を中心にしている。入れすぎると疲れるので、1分に2〜3個を目安にする。\n\n## ElevenLabs Music で作る BGM の説明文(英語と日本語訳)\n\nMac で、動画の作業フォルダから(キーの入れ方は tts_eleven.py と同じ):\n\n```\npython3 ../_制作ベース/sound_eleven.py music --name bgm_hook --sec 60 --count 2 --prompt \"(下の英文)\"\n```\n\n### A. 本編の後ろで流す曲(いちばん長く流れる)\n\n> Calm, curious instrumental background music for a technology explainer video about robots and AI. Soft electric piano and light plucked synth arpeggios, warm pads, gentle low bass, very soft minimal drums. Around 95 BPM, steady and unobtrusive. No strong melody that would compete with a narrator's voice. No vocals. Clean modern mix.\n\n日本語訳:ロボットとAIの解説動画の後ろで流す、落ち着いて、少し先が気になる感じの曲。やわらかいエレクトリックピアノ、軽くはじく電子音のアルペジオ、温かい和音、低くやさしいベース、ごく小さなドラム。テンポ95前後で一定、目立たない。ナレーションとぶつかる強いメロディは入れない。歌なし。\n\n### B. フック(動画の最初の数十秒)\n\n> Short instrumental intro bed for a tech news explainer, slightly brighter and more forward than background music. Pulsing soft synth, light percussion, a sense of something starting. Around 100 BPM. No vocals, no big drops, no dramatic trailer hits.\n\n日本語訳:技術ニュースの解説の冒頭で流す、短めの曲。本編の曲より少し明るく前に出る。脈打つやわらかい電子音、軽い打楽器、何かが始まる感じ。テンポ100前後。歌なし。盛り上げの落差や、映画の予告編のような強い音は入れない。\n\n### C. ジングル(「Pixels to Motors。案内役のハコブくんです。」の後ろ)\n\n> A 4-second friendly logo sting for a robotics channel. Soft synth bell chord with a gentle rising shimmer, ends cleanly on a warm chord. No vocals.\n\n日本語訳:ロボットのチャンネルの、4秒の親しみのあるロゴの音。やわらかい電子のベルの和音に、ゆっくり上がるきらめき。温かい和音できれいに終わる。歌なし。\n\n(C は --sec 4。A・B は 60 秒で作り、短ければ audio_mix.py がループでつなぐ)\n\n## 決める基準\n\n1. ナレーションの後ろで、言葉が聞き取りにくくならないか(音の確認.m4a をイヤホンとスピーカーの両方で聞く)\n2. 13分流して、うるさく感じないか\n3. ハコブくんの見た目と番組の落ち着いた調子に合うか</code></pre><p></p><h2 id=\"index_QVHbh5dK\">参照した情報</h2><ul><li><p><a href=\"https://network.mobile.rakuten.co.jp/sumakatsu/contents/articles/2026/00533/\" data-has-link=\"true\">YouTube のチャンネル名の変更方法(楽天モバイル スマ活)</a></p></li><li><p><a href=\"https://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026\" data-has-link=\"true\">Counterpoint Research:2026年上半期の人型ロボット出荷の推計(2026年8月20日)</a></p></li><li><p><a href=\"https://www.youtube.com/@PixelstoMotors\" data-has-link=\"true\">Pixels to Motors の YouTube チャンネル</a></p></li><li><p><a href=\"/\" data-has-link=\"true\">Pixels to Motors</a></p></li></ul>", "toc": [{"id": "index_5Npkigty", "text": "title: \"解説動画の1本目を公開まで通す ― 声・BGM・間・直し・サムネイルを部品ごとに決め、23手順にまとめた\" slug: first-video-end-to-end date: 2026-10-12 day: 333 meta_description: \"解説動画「フィジカルAIの現在地」をハコブくんの声で作り直し、YouTubeに公開した。声・BGM・効果音・間・不具合の直し・概要欄・サムネイルを部品ごとに決め、次の動画から使う23手順にまとめた。\" tags: [YouTube, 解説動画, ハコブくん, ElevenLabs, BGM, 効果音, ffmpeg, Python, Playwright, サムネイル, 動画制作]"}, {"id": "index_R8Gtcv3i", "text": "公開した動画は12分29秒・146文・効果音45個"}, {"id": "index_iJ5F55aG", "text": "ハコブくんの声は、ElevenLabs のボイスデザインで作った「候補6」に決めた"}, {"id": "index_qsSC6Gf9", "text": "「エイイチ」の抑揚は、ひらがなの「えいいち」で渡すとそろった"}, {"id": "index_NvWums4o", "text": "BGM は、コードで作った音から ElevenLabs Music の生楽器の曲に替えた"}, {"id": "index_845PdZbU", "text": "BGM の大きさは、話している間 -23dB・0.8秒以上の間 -16dB にした"}, {"id": "index_SkptrlzU", "text": "場面の切り替えは「ホワーン」、大きな流れの変わり目は「キラリン」と「ポロロン」"}, {"id": "index_ttUdYCtI", "text": "自己紹介の2文目を「研究と開発」と「見立て」に直した"}, {"id": "index_jcwDZAwf", "text": "右下のモヤモヤは、透かしを消した跡だった"}, {"id": "index_mZmow8P1", "text": "30cm の図の説明を、図の下の2行に移した"}, {"id": "index_RXyGoUij", "text": "YouTube の概要欄は、目次の時間を書き出しから自動で入れた"}, {"id": "index_WGBasXjY", "text": "タイトルを決め、「子ども向け」は「いいえ」にした"}, {"id": "index_ua9kJv2c", "text": "サムネイルは5回作り直した"}, {"id": "index_kbIhNmO_", "text": "ハコブくんの体は、ChatGPT で無地の背景から作り直した"}, {"id": "index_gTZdlqKn", "text": "サムネイルの背景は、ハコブくんがアームに重ならないコマを選んだ"}, {"id": "index_NqzL8c9Y", "text": "公開した直後に見えた2つのこと:字幕が2重に出る、画質が低い"}, {"id": "index_CTcfDe5k", "text": "チャンネル名は「フィジカルAI解説|Pixels to Motors」、まだ反映されていない"}, {"id": "index_qiv3oQpt", "text": "X と LinkedIn の投稿文は、数字と机の上の実感を先に置いた"}, {"id": "index_YMC72TM8", "text": "Claude の返事が英語になり、2回聞き返された"}, {"id": "index_ZKdeas3Q", "text": "Mac へ送ったファイルが、途中で変わることがあった"}, {"id": "index_lMTio9tY", "text": "次の動画から使う「1本を作る流れ」23手順"}, {"id": "index_B9rTkmMC", "text": "まだ決まっていないこと"}, {"id": "index_XNI1szIP", "text": "この記事で作った・直したスクリプトの全文"}, {"id": "index_MBHw9jHD", "text": "書き出しのログと測った数値"}, {"id": "index_azDryZvY", "text": "書き出した概要欄(概要欄.txt・全量)"}, {"id": "index_zetfem2o", "text": "BGM と効果音の候補を調べた表(音源_候補.md・全量)"}, {"id": "index_QVHbh5dK", "text": "参照した情報"}]}
\ No newline at end of file
+{"day": "LOG333", "series": "ロボティクスデザイン", "published": "2026-10-12T07:58:00.000Z", "title": "解説動画の1本目を公開まで通す ― 声・BGM・間・直し・サムネイルを部品ごとに決め、23手順にまとめた", "slug": "first-video-end-to-end", "cover": null, "thumb": "/thumbs/r-first-video-end-to-end.html", "lang": "JP", "description": "解説動画「フィジカルAIの現在地」をハコブくんの声で作り直し、YouTubeに公開した。声・BGM・効果音・間・不具合の直し・概要欄・サムネイルを部品ごとに決め、次の動画から使う23手順にまとめた。", "updated": "2026-10-12T23:58:54.000Z", "body": "<!--TOC--><p></p><hr><h2 id=\"index_5Npkigty\">title: \"解説動画の1本目を公開まで通す ― 声・BGM・間・直し・サムネイルを部品ごとに決め、23手順にまとめた\" slug: first-video-end-to-end date: 2026-10-12 day: 333 meta_description: \"解説動画「フィジカルAIの現在地」をハコブくんの声で作り直し、YouTubeに公開した。声・BGM・効果音・間・不具合の直し・概要欄・サムネイルを部品ごとに決め、次の動画から使う23手順にまとめた。\" tags: [YouTube, 解説動画, ハコブくん, ElevenLabs, BGM, 効果音, ffmpeg, Python, Playwright, サムネイル, 動画制作]</h2><p>試作で作った解説動画「フィジカルAIの現在地」を、ナレーション役のハコブくんの声で作り直し、YouTube に公開しました。公開したタイトルは「人型ロボットの出荷は4倍。ロボットにも「ChatGPTの瞬間」は来たのか|フィジカルAIの現在地」、長さは12分29秒です。</p><p>台本・図・字幕・ハコブくんの動きは、これまでの記事で作ってきました(構成と台本の検証は LOG330、ハコブくんの口と目の動きは LOG331、チャンネルの見た目は LOG332)。この記事は、そこから公開までに決めたことの記録です。声、BGM と効果音、文と文の間、見つかった不具合の直し、YouTube の概要欄、サムネイル、公開した直後に見えたこと、の順に書いています。最後に、次の動画から同じ順番で作れるようにまとめた23の手順を載せました。</p><p>作業は、Claude のクラウドの作業環境でスクリプトを動かし、ElevenLabs で声と音楽を作る所だけを手元の Mac で動かす形で進めました。ElevenLabs の API キーは Mac の中だけで使い、Claude には渡していません。API キーが写っている画面の写真は、この記事に載せていません。</p><p>作ったスクリプトは、記事の後半にすべて全文で載せています。書き出しのログと、測った数値も全量で載せています。</p><p></p><figure><img src=\"/media/s-1280x720_v-fms_webp_da0a9183-719d-40ae-8829-cf3de0ab3f27.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_R8Gtcv3i\">公開した動画は12分29秒・146文・効果音45個</h2><p>公開した動画の中身は、次のとおりです。数値は、YouTube に上げたファイルを ffprobe(動画の中身を調べるツール)と ffmpeg の音量測定で測ったものです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>値</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>長さ</p></td><td colspan=\"1\" rowspan=\"1\"><p>12分29秒(749.1秒・22,473コマ)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>文の数</p></td><td colspan=\"1\" rowspan=\"1\"><p>146文(字幕の字数で約4,300字)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ナレーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくん(ElevenLabs のボイスデザインで作った声・モデル Eleven v4)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM</p></td><td colspan=\"1\" rowspan=\"1\"><p>ElevenLabs Music で作った1曲(bgm_明るいB_2・60秒)を最初から最後までループ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>45個。場面の切り替えの「ホワーン」37個、大きな流れの変わり目の「ポロロン」7個、自己紹介の「キラリン」1個</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>音の大きさ</p></td><td colspan=\"1\" rowspan=\"1\"><p>-16.0 LUFS(YouTube で聞く大きさの目安にそろえた)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>映像</p></td><td colspan=\"1\" rowspan=\"1\"><p>H.264、1920×1080、30コマ/秒</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>上げたファイル</p></td><td colspan=\"1\" rowspan=\"1\"><p>65,655,407 バイト(約66MB)</p></td></tr></tbody></table></div><p>動画は、冒頭で「今年の上半期に出荷された人型ロボットは約4倍」という推計を出し、「ロボットにも ChatGPT のような瞬間はもう来たのか」を問いにしています。答えは「写真を見せて判断させる入口は簡単になった。物を確実に動かすところは、まだ手間がかかる」です。そのあと3つの章(出荷の中身、机の上のロボットアームで分かったこと、データの壁)を見て、ロボットのニュースを見るときの2つの問いで終わります。</p><p>チャンネルは <a href=\"https://www.youtube.com/@PixelstoMotors\" data-has-link=\"true\">Pixels to Motors の YouTube</a> です。</p><h2 id=\"index_iJ5F55aG\">ハコブくんの声は、ElevenLabs のボイスデザインで作った「候補6」に決めた</h2><p>ナレーションはハコブくんが話すと決めています(LOG331)。声は、ほかのチャンネルと同じ声を使わず、ハコブくん専用に作ります。ElevenLabs のボイスデザイン(声の特徴を文章で書くと、新しい声の候補を作る機能)を有料プランで1か月試すことにしました。</p><p>声の候補をいくつか作って聞き比べ、「候補6(明るい30代前半のお手伝いロボット)」の2つ目のパターンに決めました。落ち着いた30代の声(候補1)は、読みは正確でしたが、ハコブくんの見た目に合いませんでした。</p><p>読み上げのモデルは、冒頭の14文を Multilingual v2 と Eleven v4 で読ませて聞き比べ、Eleven v4 にしました。話す速さは既定のままです。1秒に約6.8字で、仮の音声(Open JTalk)の約5.2字より速く、動画は仮の音声の版より短くなりました。</p><p>声づくりは、手元の Mac で <code>tts_eleven.py</code> を動かして1文ずつ mp3 にし、それを Claude の作業環境で <code>tts_import.py</code> が書き出し用の wav に変える流れです。文の前後の無音は 0.05 秒を残して切り、文と文の間は台本に書いた「後ろの間」で決めます。</p><h2 id=\"index_qsSC6Gf9\">「エイイチ」の抑揚は、ひらがなの「えいいち」で渡すとそろった</h2><p>通しで聞くと、「エイイチ」という名前の抑揚が文ごとに違っていました。</p><blockquote><p>エイイチのイントネーションが揃っていないので、全て同じイントネーションに修正してください(語尾が下がる方が通常のイントネーションかな)</p></blockquote><p>ElevenLabs に渡す文の書き方で、読み方が変わります。そこで <code>tts_eleven.py</code> に、台本ではなく試しの文だけを音声にする <code>--try</code> を足し、同じ文を4通りの書き方で作りました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>番号</p></th><th colspan=\"1\" rowspan=\"1\"><p>ElevenLabs に渡した書き方</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>エイイチ(カタカナ)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>栄一(漢字)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>えいいち(ひらがな)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>エーイチ(伸ばす音)</p></td></tr></tbody></table></div><blockquote><p>3回が正しい発音でした。</p></blockquote><p>ひらがなの「えいいち」が、語尾の下がる読みでそろいました。台本 <code>narration.py</code> に <code>ELEVEN_READ</code>(ElevenLabs に渡すときだけ文字を置きかえる表)を作り、「エイイチ」を「えいいち」に置きかえています。字幕はカタカナのままです。</p><p>もう1か所、読み間違いが見つかりました。</p><blockquote><p>3分16秒あたりのセリフが、裏表裏になっています(テキストは合っている)</p></blockquote><p>37番の文で、「裏表」が「うらおもてうら」のように読まれていました。字幕の文字は合っていて、音声だけが違います。<code>ELEVEN_READ</code> に「裏表 → うらおもて」を足し、その文だけ作り直しました。仮の音声用の読み替え表 <code>READ</code> とは別に持っているのは、同じ語でも、エンジンによって間違え方が違うためです。</p><h2 id=\"index_NvWums4o\">BGM は、コードで作った音から ElevenLabs Music の生楽器の曲に替えた</h2><p>BGM は、最初にコードで仮の曲を作りました(<code>sound_synth.py</code>。やわらかい和音と小さな粒の音を数式で鳴らしたもの)。混ぜ方を確かめるための仮の曲です。</p><blockquote><p>音源はシンセサイザーっぽいのではなくて、ほんのもう少しだけ温かみのある音源が良さそう(テーマが真面目なので、少し音源には彩りを加えたい的な)</p></blockquote><p>コードでピアノ風・ギターとマリンバ風・ピッツィカート風の3曲を作り直しましたが、方向は変わりませんでした。</p><blockquote><p>音源はもっと生音っぽい方がいいかな。音源使えるのある?</p></blockquote><p>そこで、使える音源を調べて表にしました(<code>音源_候補.md</code>。全文は後半)。条件は、YouTube で収益化しても使えること、クレジット表記の条件を守れること、ほかのチャンネルと同じ曲になりにくいことです。第一候補を ElevenLabs Music(文章で曲の雰囲気を書くと、新しい曲を作る機能)にしました。声と同じプランで使え、新しく作る曲なので、ほかのチャンネルと同じ曲にならないためです。</p><p>ElevenLabs Music を API から使うと、最初は次のエラーで止まりました。</p><pre><code>HTTP 401 missing_permissions music_generation\n</code></pre><p>使っていた API キーに、音楽を作る許可がありませんでした。新しいキーを作り直し、「読み上げ・効果音・音楽生成」の3つだけを許可しました。前のキーは、以前に画面の写真に写ったことがあったため、削除を勧めました(削除したかは、この記事の時点で確かめていません)。キーは、Mac のターミナルで画面に出ない形で入れています。</p><pre><code>read -s ELEVENLABS_API_KEY && export ELEVENLABS_API_KEY\n</code></pre><p>曲は <code>sound_eleven.py</code> で60秒ずつ作りました。最初の4曲(冒頭用2曲・本編用2曲)は、聞くと暗く感じました。</p><blockquote><p>もう少し明るい曲調がいいかな。暗く聞こえる</p></blockquote><p>長調・明るめを指定して、2種類の書き方で2曲ずつ作り直しました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>曲</p></th><th colspan=\"1\" rowspan=\"1\"><p>指定した内容(要点)</p></th><th colspan=\"1\" rowspan=\"1\"><p>結果</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>bgm_冒頭_1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>生の楽器、ギター・ピアノ・ピッツィカート・マリンバ、100 BPM、好奇心と希望</p></td><td colspan=\"1\" rowspan=\"1\"><p>暗く聞こえた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>bgm_本編_1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>生の楽器、フェルトピアノ・ギター・チェロとバイオリン、90 BPM、落ち着いて真面目</p></td><td colspan=\"1\" rowspan=\"1\"><p>暗く聞こえた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>bgm_明るいA_1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>長調、ギター・明るいピアノ・鉄琴・手拍子、105 BPM、晴れた朝のように</p></td><td colspan=\"1\" rowspan=\"1\"><p>A_1 を冒頭で試した</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>bgm_明るいB_1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>長調、ピアノとギターが主、弦・ピッツィカート・鉄琴、100 BPM、前向きで落ち着いた明るさ</p></td><td colspan=\"1\" rowspan=\"1\"><p><strong>B_2 に決めた</strong></p></td></tr></tbody></table></div><blockquote><p>bgm_明るいB_2.mp3 bgm_明るいA_1.mp3 で動画作成してみて</p></blockquote><p>冒頭15秒を2曲それぞれで書き出して聞き比べ、B_2 に決まりました。指定した文章の全文は、後半の「作った曲の記録」にあります。</p><p>曲は60秒なので、12分29秒の動画ではループします。ElevenLabs の曲は最後の数秒で小さくなって終わるため、そのままつなぐと1分ごとに音が消えます。<code>audio_mix.py</code> で、曲のふつうの大きさより10dB以上小さくなる終わりの部分を切り、つなぎ目を2秒重ねてループさせました。</p><h2 id=\"index_845PdZbU\">BGM の大きさは、話している間 -23dB・0.8秒以上の間 -16dB にした</h2><p>BGM は、ハコブくんが話している間だけ自動で小さくし、話していない間で少し戻します。値はナレーションの大きさとの差で、-23dB はナレーションのおよそ14分の1の音圧です。</p><p></p><figure><img src=\"/media/s-1800x780_v-frms_webp_a3a3705d-d886-470c-8be2-f4a70eed6f10.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>この値に決まるまでに、4回動かしました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>段階</p></th><th colspan=\"1\" rowspan=\"1\"><p>話している間</p></th><th colspan=\"1\" rowspan=\"1\"><p>0.8秒以上の間</p></th><th colspan=\"1\" rowspan=\"1\"><p>きっかけ</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>-22dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>-15dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>最初の値</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>-15dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>-9dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>「音源が聞こえないね。もう少しだけ音量上げるか、音源だけ聴きたい」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>-22dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>-15dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>明るい曲に替えたら大きく聞こえた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>-23dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>-16dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>「音源の音量90%くらいにして」</p></td></tr></tbody></table></div><blockquote><p>曲調を変えたら音量が大きく聞こえてしまうようになったので、前の音量に戻してください。冒頭15秒_bgm_明るいB_2が良さそうです。</p></blockquote><blockquote><p>いいですね。音源の音量90%くらいにして、この動画を仕上げてみてください。</p></blockquote><p>同じ dB の値でも、曲が変わると聞こえ方が変わりました。明るい曲は高い音(鉄琴や明るいピアノ)が多く、低い音の多い曲よりナレーションの上に出て聞こえたためと考えています。90%は、音圧で1dB下げることにあたります(20×log10(0.9) ≒ -0.9dB)。</p><p>話している所の決め方は、次のとおりです。</p><ul><li><p>各文の読み上げの範囲を、前に0.15秒、後ろに0.3秒広げる</p></li><li><p>広げたあとの間が0.5秒未満なら、話している扱いのまま(もとの間で約0.8秒未満。BGM が文ごとに上下しないため)</p></li><li><p>0.3秒かけてなめらかに上げ下げする</p></li></ul><p>グラフのとおり、BGM が戻るのは、場面の切り替えの直前(1.2秒の間)、章の見出しのあと(1.7秒)、大きな流れの変わり目(1.8秒の間)だけです。ふつうの文の間(0.35秒)では、BGM は下がったままです。</p><p>後ろの間の長さを選ぶと、BGM の大きさと鳴る効果音がどう変わるかを、次の図で動かして確かめられます。</p><div class=\"embed\"><iframe src=\"/embeds/first-video-end-to-end-VBQ8Fbsp.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p></p><h2 id=\"index_SkptrlzU\">場面の切り替えは「ホワーン」、大きな流れの変わり目は「キラリン」と「ポロロン」</h2><p>効果音は、最初にコードで5種類作りました(ぽん・こつ・しゅっ・ちん・ひゅい)。冒頭15秒で試したあと、使い方を絞りました。</p><blockquote><p>最後のホワーンって効果音を、場面切り替えに入れてみたい。</p></blockquote><blockquote><p>ホワーンって効果音だけを、場面切り替えに使って</p></blockquote><blockquote><p>効果音はもう少し音量を大きく。</p></blockquote><p>効果音は「ホワーン」(se_rise)だけにし、ナレーションより12dB小さい値から6dB小さい値に上げました。場面(台本の1文ごとに書いた場面の名前)が変わる文の頭から0.15秒前に、自動で鳴らします。</p><p>次に、話の大きな区切りがどこか分かるようにしました。</p><blockquote><p>それと動画の流れの中で少し間があるポイントがあるといいので、大きな流れの変化の時にはその間を設定することはできる?</p></blockquote><blockquote><p>もしくはそのときは効果音を違うものにするか。</p></blockquote><p>大きな流れの変わり目(フック→自己紹介、自己紹介→「今日の問い」、各章の始まり、答え、エンディング)では、直前の文の後ろの間を1.8秒にし、ホワーンの代わりに別の音を鳴らすことにしました。別の音は、コードで6つ作りました(マリンバ・ハープ・ピアノ・鉄琴・ふくらむ和音・風)。</p><blockquote><p>5と2で動画に入れ込んでみて(自己紹介のあたりで入れると良さそう。音源だけでも可です)</p></blockquote><blockquote><p>自己紹介は5のキラリンがいいね。他の切り替えポイントで2のポロロン使ったりと使い分けが良さそうだね(とりあえず自己紹介は5で確定)</p></blockquote><p>自己紹介に入る所は「キラリン」(鉄琴の高い3音)、そのほかの変わり目は「ポロロン」(マリンバの上がる3音)にしました。ポロロンを場所ごとにどう使い分けるかは、まだ試している段階です。</p><p>そのあと、ふつうの場面の切り替えにも、もう少し間が欲しいという指摘がありました。</p><blockquote><p>ホワーンという中の切り替えの時にもう一拍だけ間がある方がいいな。</p></blockquote><p>ホワーンが鳴る所の直前の文の後ろの間を、1.2秒にしました。公開した台本の146文は、後ろの間で分けると次のとおりです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>どこ</p></th><th colspan=\"1\" rowspan=\"1\"><p>後ろの間</p></th><th colspan=\"1\" rowspan=\"1\"><p>文の数</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ふつうの文</p></td><td colspan=\"1\" rowspan=\"1\"><p>0.35秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>96</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>話の小さな区切り</p></td><td colspan=\"1\" rowspan=\"1\"><p>0.4〜0.6秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>4</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ふつうの場面の切り替え(ホワーンの前)</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.2秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>32</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>章の見出しを読む文(「1つ目、…」など)と、最後のあいさつ</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.7秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>5</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>大きな流れの変わり目(キラリン・ポロロンの前)</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.8秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>8</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>字幕のない最後の文(エンディングの余白)</p></td><td colspan=\"1\" rowspan=\"1\"><p>6.0秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>1</p></td></tr></tbody></table></div><p>章の見出しは、読んだすぐあとに場面が切り替わるので、見出しの1.2秒に、切り替えの一拍を足して1.7秒にしています。</p><p>どの文で何が鳴ったかは、書き出しのログに45行で出ています(後半に全量)。</p><h2 id=\"index_ttUdYCtI\">自己紹介の2文目を「研究と開発」と「見立て」に直した</h2><p>確認用の動画を見て、自己紹介の文と、字幕の後ろの帯に指摘がありました。</p><blockquote><p>ここの自己紹介おかしくないか? それとナレーションのテキストの下に薄い四角枠が敷かれているのが必要ないかな。</p></blockquote><blockquote><p>ロボットアームを動かした記録を紹介しているわけではないよ(もちろんそういう時もあるだろうけど)</p></blockquote><p>前の文は「題材は、本業のデザイナーをしながら、自宅の机の上でロボットアームを動かした記録です。」でした。これだと、毎回ロボットアームの記録を見せる番組だと受け取られます。この番組は、フィジカルAIを解説する番組です。Claude がいくつか言い方を出し、次の文に決めました。</p><blockquote><p>エイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。その中で分かったことと、そこから考えた見立てを、ハコブくんがお伝えします。こっちがいいかな。その下はすこし冗長だね。</p></blockquote><p>「結論」ではなく「見立て」にしたのは、今の時点での判断で、新しい事実が出たら変わるからです。</p><p>名前のテロップも分けました。ハコブくんが自分の紹介をしている間は「フィジカルAI解説/案内役:ハコブくん(AI音声)」、本人を紹介する文から「エイイチ」を出します。台本では、この文を「title」から「title_eiichi」という別の場面に分けて、テロップを替えています。映像は同じなので、この切り替えではホワーンを鳴らしません。</p><p>字幕の後ろの薄い黒い帯は、実機の映像の上で字幕を読みやすくするために敷いていたものです。字幕の文字には黒い縁があり、帯がなくても読めるため、外しました。</p><p></p><figure><img src=\"/media/s-1979x632_v-frms_webp_552cefd2-62a2-43d2-bba1-e1e5e23b08c9.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_jcwDZAwf\">右下のモヤモヤは、透かしを消した跡だった</h2><blockquote><p>55秒くらいに右端下がモヤモヤしているのを見つけました。同じようなモヤモヤが他にもないか調べて修正してください。</p></blockquote><p>自己紹介の場面の映像は、以前に動画編集サイト(Clideo)で書き出した版(DAY123.mp4)を使っていました。この版には右下にサイトの透かし(ロゴ)が入っていて、ffmpeg の delogo(指定した範囲を周りの色でぼかして埋める機能)で消していました。その跡が、マットの格子の上で縦の筋のように見えていました。</p><p>同じように透かし入りの版を使っていた映像は、もう1本(DAY124.mp4。章2の実機の場面)ありました。2本とも、透かしの入っていない元の撮影ファイル(DAY123.MOV・DAY124.mov)に替え、<code>scenes.py</code> の映像の指定で、透かしを消す処理を使わない設定(3つ目の値を False)にしました。</p><p><code>assemble.py</code> は、1文ずつ背景の映像を作り、中身が変わった文だけを作り直す仕組みです。どの文を作り直すかは、背景の中身から作った指紋(ハッシュ)で判断しています。この指紋に、使う映像のファイル名と開始秒が入っていませんでした。そのままでは、映像を替えても作り直されないため、指紋に映像の指定を足しました。</p><p></p><figure><img src=\"/media/s-1874x512_v-frms_webp_bca07aad-c04e-4174-a202-e8fd93911724.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_mZmow8P1\">30cm の図の説明を、図の下の2行に移した</h2><blockquote><p>30cm以上違うはのところは新しく内容を修正したので確認してください。前の動画か台本を確認して修正してください。</p></blockquote><p>章2の「計算ツールとデジタルツインで、爪先の位置が30cm以上食い違った」という図で、説明の行が右へはみ出し、「グリッパー」が「グリッパ」と「ー」の2行に折れていました。説明を図の下に移し、中央にそろえた2行にしました。1行目が「アームが外れた距離ではない。爪先の位置の決め方が違っていた」、2行目が「計算ツールには、グリッパーの形が入っていない」です。</p><p></p><figure><img src=\"/media/s-1897x632_v-frms_webp_e4c6b745-4cda-4da9-8bf1-663eb0d60dcb.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_RXyGoUij\">YouTube の概要欄は、目次の時間を書き出しから自動で入れた</h2><blockquote><p>YouTubeの動画内に入れる動画概要は追加情報などをまとめておいてください!</p></blockquote><p>概要欄は、動画の作業フォルダの <code>概要欄_下書き.md</code> に書き、<code>description.py</code> で貼り付け用の <code>概要欄.txt</code> にします。目次の時間は、下書きに <code>{t:26}</code> のように文番号で書くと、書き出しのときに記録した、その文が始まる時間に置きかわります。声や間を変えて時間がずれても、下書きを直す必要がありません。</p><p><code>description.py</code> は、YouTube の目次(チャプター)の条件を満たしているかも確かめます。最初が 0:00 で始まり、3つ以上あり、それぞれ10秒以上あることです。満たさないとき、5,000字を超えるとき、「【要記入】」が残っているときは止まります。</p><p>並べる順は、冒頭の数字、「今日の問い」、先に答え、この動画で見る3つ、目次、出典、この番組について、連絡先とリンク、チャンネル登録のお願いとハッシュタグ3つです。連絡先とリンクは、次のように決めました。</p><blockquote><p>メールアドレス、xとリンクドインかな。</p></blockquote><p>出典のリンクは、すべて開いて確かめてから載せました。Smart Analytics Global の推計は、公開されているページが見つからなかったため、リンクなしで名前と時期だけを書いています。1X NEO の「一般のお客さんへの配送開始」は、公式発表が見つからなかったことを、確かめた日付と一緒に書きました。</p><p>公開した概要欄の全文は、後半の「書き出した概要欄」にあります。</p><h2 id=\"index_WGBasXjY\">タイトルを決め、「子ども向け」は「いいえ」にした</h2><blockquote><p>修正したいところはまだまだありつつも、とりあえず合格点です!YouTubeにアップロードします!</p></blockquote><p>タイトルは、Claude が3案を出し、「人型ロボットの出荷は4倍。ロボットにも「ChatGPTの瞬間」は来たのか|フィジカルAIの現在地」に決めました。冒頭の数字、動画の問い、シリーズ名の順です。</p><p>YouTube に上げるときは、動画が子ども向けかどうかを必ず選びます。ハコブくんはキャラクターですが、動画は大人の視聴者に向けた解説なので「いいえ、子ども向けではありません」にしました。子ども向けにすると、コメントや、ほかの動画の紹介などの機能が使えなくなります。</p><p></p><figure><img src=\"/media/s-1270x654_v-fms_webp_24ce4e2f-fed4-448a-a044-254e3578f394.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_ua9kJv2c\">サムネイルは5回作り直した</h2><blockquote><p>それからサムネイルが必要ですね。ChatGptに作らせるので指示を考えてください。</p></blockquote><p>最初は、ChatGPT に画像を作らせるための指示文を Claude が書き、ChatGPT で作りました。</p><blockquote><p>chatGPTのサムネイルフォントが微妙だし、背景写真が違うしだね。</p></blockquote><blockquote><p>今の動画のフォント周りのデザインをベースにサムネイルの背景を入れる感じでまずは惹きつけられるサムネイルを目指したいです。</p></blockquote><p>画像を作る AI は、日本語の文字の形が崩れやすく、背景のロボットアームも実物と違う形になりました。そこで、文字は画像の AI に任せず、<code>thumbnail.py</code> がコードで組む形にしました。文字と色は動画と同じもの(Noto Sans CJK JP の太字、オレンジ #C4471F、紙色 #F5F5F0、黒 #0D0D0D)を使い、HTML で組んで Playwright(ブラウザを自動で動かす道具)で画像にします。背景は、実機の映像の1コマです。文字をコードで組めば、誤字や崩れた文字は出ません。</p><p></p><figure><img src=\"/media/s-1864x1225_v-frms_webp_4be2ec7d-a345-4e3d-b8e9-88f8d4885fa8.png\" alt=\"\"><figcaption></figcaption></figure><p></p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>回</p></th><th colspan=\"1\" rowspan=\"1\"><p>作ったもの</p></th><th colspan=\"1\" rowspan=\"1\"><p>指摘</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>ChatGPT で作った案</p></td><td colspan=\"1\" rowspan=\"1\"><p>文字の形と、背景の写真が違う</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>案A(紙色と写真)・案B(黒と写真)・案C(大きなハコブくん)</p></td><td colspan=\"1\" rowspan=\"1\"><p>「これがまぁまぁ良さそうだけど。ハコブくんが小さいの修正と右向きがいいね。レイアウト別バージョンを生成」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>案Bの別の組み方3つ(B1・B2・B3)。最初は元の体のまま</p></td><td colspan=\"1\" rowspan=\"1\"><p>「ハコブくんの頭凹んでいるよ!」「最初から作り直したら?」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3'</p></td><td colspan=\"1\" rowspan=\"1\"><p>体を作り直して B1〜B3 を出し直した</p></td><td colspan=\"1\" rowspan=\"1\"><p>「ハコブくん大きすぎ。ハコブくんの顔が変(口と目の配置や大きさ確認)背景を全部写真に変更。テキストを大きく目立つように。写真の上にテキストを置くので、黒縁をつけること。」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>案D(全面写真・黒い縁の大きな文字・ハコブくん右下)</p></td><td colspan=\"1\" rowspan=\"1\"><p>「ハコブくんは左下済み(大きさ少しだけ大きく)人型ロボット出荷 約4倍の部分は黒縁無し。」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>案E(写真のコマを替え、ハコブくん左下)</p></td><td colspan=\"1\" rowspan=\"1\"><p>公開に使った</p></td></tr></tbody></table></div><h2 id=\"index_kbIhNmO_\">ハコブくんの体は、ChatGPT で無地の背景から作り直した</h2><p>サムネイルでハコブくんを大きく出すと、頭の上がへこんで見えました。動画で使っている体の画像は、生成動画の1コマから切り抜いたもので、頭の上が背景と見分けにくく、切り抜きが欠けていたためです。動画では小さく出すので目立ちませんでしたが、サムネイルの大きさでは分かりました。</p><blockquote><p>最初から作り直したら?</p></blockquote><p>ChatGPT で、無地の背景・画面は真っ黒・側面の緑の光の輪はそのまま、という条件でハコブくんの体を作り直しました。</p><p></p><figure><img src=\"/media/s-1328x612_v-fms_webp_86d9d594-b517-4b6d-99b5-8485b97925a6.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>作り直した体は、次の手順でサムネイル用の画像にしました。</p><ol><li><p>切り抜き: OpenCV の GrabCut(指定した四角の中で、前景と背景を色の分布から分ける方法)を、体を囲む四角(GrabCut に渡した値は 230, 170, 680, 1040)で8回くり返して前景を出した。いちばん大きなかたまりだけを残し、中の穴を埋めた。最初に試した、背景の色から塗りつぶす方法は、体の白い部分まで背景として消えた</p></li><li><p>画面の四隅: 黒い画面の四隅の位置を測った(左上 356,262・右上 592,260・右下 569,491・左下 314,485)</p></li><li><p>顔: 画面に、白い目と青い口を描いた</p></li><li><p>向き: 透明でない範囲で切り、左右を反転して右向きにした</p></li></ol><p></p><figure><img src=\"/media/s-1277x420_v-fms_webp_f178bd99-0535-4ee4-a1e8-bca01a47d139.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>3の顔は、最初、動画のハコブくんを見ながら、目と口の位置と大きさを画面の割合で決めて描きました。目が縦に長く上に寄り、口が画面の半分の幅で大きく開いていて、動画のハコブくんと違う顔になりました。</p><p>顔を描き直すとき、目と口を自前で描くのをやめました。動画の <code>hakobu.py</code> が持っている目と口の描き方(<code>eye_mask</code>・<code>mouth_mask</code>)でそのまま描き、動画の体の画面を長方形にならしたものから、新しい体の画面の四隅へ写しています。画面の大きさに対する目と口の大きさと位置が、動画のハコブくんと同じになります。<code>make_new_face.py</code>(全文は後半)がこの処理です。</p><p>1の切り抜きと2の四隅の測定は、その場でコマンドとして動かしたため、スクリプトのファイルが残っていません。結果の <code>cut_new.png</code> と <code>screen_quad_new.npy</code>(四隅の値)は <code>_制作ベース/hakobu/</code> に置いてあり、3と4は <code>make_new_face.py</code> と、そのあとに切り抜きと反転をしたコマンドで作っています。</p><h2 id=\"index_gTZdlqKn\">サムネイルの背景は、ハコブくんがアームに重ならないコマを選んだ</h2><p>ハコブくんを左下に置くと、最初に使っていた写真(DAY124.mov の28秒、アームが赤いブロックを掴む瞬間)では、ちょうど左下に爪とブロックがあり、ハコブくんで隠れます。写真を拡大したり、ずらしたりしても、爪は左下から出ませんでした。</p><p>同じ映像の別のコマを2秒ごとに並べて探し、39.8秒(アームがブロックを箱に置く瞬間)を選びました。このコマは左下がマットだけで、爪とブロックと箱が画面の中央から右下に入ります。</p><p></p><figure><img src=\"/media/s-1980x632_v-frms_webp_c0aa477f-7805-473d-acb6-7dfc5d782633.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>公開したサムネイル(案E)の組み方は、次のとおりです。</p><ul><li><p>背景は写真を全面に敷き、右上だけを少し暗くして文字を読みやすくする</p></li><li><p>「ロボットにも/ChatGPTの瞬間/もう来た?」は、1920×1080 のとき150・200・170px の大きさで、24px の黒い縁を付ける</p></li><li><p>上の札「人型ロボット出荷 約4倍」は、オレンジの札に白い文字。黒い縁は付けない</p></li><li><p>ハコブくんは左下の隅に、高さ600px で右(アーム)を向けて置く。「本当に?」の吹き出しは右側</p></li><li><p>1920×1080 で作って 1280×720 に縮め、YouTube の上限の2MB 以下にする(公開版は約1MB)</p></li></ul><h2 id=\"index_NqzL8c9Y\">公開した直後に見えた2つのこと:字幕が2重に出る、画質が低い</h2><blockquote><p>アップしました!</p></blockquote><p>公開したページを開くと、YouTube の字幕(黒い帯に白い文字)が、動画に焼き込んだ字幕の上に重なって、同じ文が2重に出ていました。</p><p></p><figure><img src=\"/media/s-1932x1606_v-frms_webp_e2dcc461-2987-4422-86aa-134d6e44985c.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>動画には字幕が焼き込んであります。字幕のファイル(srt)を YouTube に字幕として登録していると、視聴者が字幕をオンにしたときに2重になります。登録しておくと検索や、音を出せない人への字幕として役に立つ一方、焼き込んだ字幕と重なります。登録を残すかどうかは、これから決めます。</p><blockquote><p>動画の画質こんなもん?アップしてすぐだから?</p></blockquote><p></p><figure><img src=\"/media/s-3024x1912_v-frms_webp_3eb563d7-8909-410f-9e2a-a4a74c38cc55.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>YouTube は、上げた直後は低い画質の版を先に公開し、HD(1080p)の版はあとから作ります。文字のふちがにじんでいたのは、このためと考えています。再生画面の歯車の「画質」に 1080p が出ていれば、HD の版はできています。</p><p>一方、上げたファイルの映像は、測ると約0.49Mbps(488,631 ビット/秒)でした。<code>assemble.py</code> は crf 20(画質を一定に保つ設定。数字が小さいほど高画質)で書き出していて、図と無地の背景が多い動画なので、この値でも書き出したファイルの文字はくっきりしています。ただ YouTube は上げたファイルをもう一度圧縮するので、HD の版でも文字が甘く見えるようなら、2560×1440 に拡大して書き出し直す方法を試す候補にしています。YouTube は 1440p 以上の動画には、画質のよい圧縮方式を使うためです。HD の版での見え方は、まだ確かめていません。</p><h2 id=\"index_CTcfDe5k\">チャンネル名は「フィジカルAI解説|Pixels to Motors」、まだ反映されていない</h2><blockquote><p>チャンネルの名前なんだっけ?(更新されていない)</p></blockquote><p>チャンネル名は「フィジカルAI解説|Pixels to Motors」に決めています(LOG332)。公開した時点では、YouTube ではまだ「Pixels to Motors」と表示されていました。</p><blockquote><p>何度も修正しすぎて反映されなくなってるかも・</p></blockquote><p>YouTube のチャンネル名は、14日間に2回までしか変えられず、変えた名前が表示されるまでに数日かかることもあります(<a href=\"https://network.mobile.rakuten.co.jp/sumakatsu/contents/articles/2026/00533/\" data-has-link=\"true\">楽天モバイルの解説記事</a>)。YouTube Studio の「カスタマイズ」→「基本情報」の名前の欄が新しい名前になっていれば、表示が追いつくのを待ちます。古いままで保存できなければ、2回の上限に当たっています。どちらだったかは、この記事の時点で確かめていません。</p><h2 id=\"index_qiv3oQpt\">X と LinkedIn の投稿文は、数字と机の上の実感を先に置いた</h2><blockquote><p>Xとlinkdinに投稿するので、投稿内容を考えてください。わかりやすくかつ、興味を持ちやすい投稿内容になるように注意してください。</p></blockquote><p>X は140字(URL を含む)に収めるため、冒頭の数字(約4倍)、動画の問い、机の上のロボットアームで650回以上試した実感(写真で判断させるのは簡単、物を確実に動かすのはまだ手間)の3つだけにしました。X の数え方では、日本語1字が2、URL が23で、280以内に収まれば140字以内です。作った文は266でした。</p><p>LinkedIn は英語のあとに日本語を並べ、動画の中の具体的な話(計算ツールとデジタルツインで爪先の位置が30cm以上食い違ったこと、肩の関節に90度ちょっとを超える値を送るとエラーなしで動かないことがあったこと)を入れました。30cm は「アームが30cm外れた」と読まれやすいので、コメント欄への補足で、爪先の位置の決め方が違っていた話だと書き足しています。ハッシュタグは、記事の投稿で使う #R2R2R ではなく、動画の話題で探す人が使う #フィジカルAI・#PhysicalAI にしました。</p><h2 id=\"index_YMC72TM8\">Claude の返事が英語になり、2回聞き返された</h2><p>作業の途中で、Claude の返事が英語になったことが2回ありました。</p><blockquote><p>英語で書いてあってわからないよ?</p></blockquote><blockquote><p>英語だよ?</p></blockquote><p>1回目は、使える音源の候補を調べて返したとき、2回目は、公開したページを見たあとの返事です。どちらも日本語で書き直しました。このプロジェクトの返事は日本語で書くと決まっています。</p><h2 id=\"index_ZKdeas3Q\">Mac へ送ったファイルが、途中で変わることがあった</h2><p>作ったファイルは、Claude の作業環境から、Mac とつながった経路で iCloud と外付けの SSD(KIOXIA)に送っています。この経路で、3つのことが起きました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>起きたこと</p></th><th colspan=\"1\" rowspan=\"1\"><p>どう分かったか</p></th><th colspan=\"1\" rowspan=\"1\"><p>どうしたか</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>約28MB を超えるファイルが送れない(HTTP 400)</p></td><td colspan=\"1\" rowspan=\"1\"><p>送る道具がエラーを返した</p></td><td colspan=\"1\" rowspan=\"1\"><p>10MB ずつに分けて送り、Mac でつないだ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>分けた動画の先頭の1つ、透明な部分のある PNG(ハコブくんの切り抜きなど)が、届いたときに中身が変わっていた</p></td><td colspan=\"1\" rowspan=\"1\"><p>送る前と届いたあとの指紋(md5。ファイルの中身から作る32桁の値)が違った。PNG は届いた方が大きかった</p></td><td colspan=\"1\" rowspan=\"1\"><p>gzip で圧縮して送り、Mac で戻した。戻したあとの指紋が一致するのを確かめた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Mac にあるスクリプト(assemble.py・2本の scenes.py)が、古い版に戻っていた</p></td><td colspan=\"1\" rowspan=\"1\"><p>Mac と作業環境のファイルの指紋を比べた</p></td><td colspan=\"1\" rowspan=\"1\"><p>送り直して、指紋が一致するのを確かめた</p></td></tr></tbody></table></div><p>YouTube に上げた動画ファイルは、作業環境の指紋 <code>d9f6def3f1a01d9aee5c76c9d6516bc0</code> と、Mac でつないだあとの指紋が一致しています。送ったあとは、毎回指紋を比べることにしました。</p><h2 id=\"index_lMTio9tY\">次の動画から使う「1本を作る流れ」23手順</h2><blockquote><p>動画の作り方の手順をまとめてありますか?他の台本を使って動画を量産していきますよ。</p></blockquote><p>作る手順は、動画のルール <code>video_rules.md</code> の中に分かれて書かれていて、サムネイル・アップロード・投稿の手順はどこにもありませんでした。そこで <code>video_rules.md</code> の先頭に「0節 1本を作る流れ」を足し、台本から投稿までを23の手順にまとめました。「フィジカルAIの現在地」で実際に通した順番です。担当の <strong>エ</strong> は本人、<strong>C</strong> は Claude です。追記した0節を、ファイルのまま載せます。</p><pre><code class=\"language-markdown\">### 準備(1本ごとに1回)\n\n1. **エ** どの台本を動画にするか決めて、Claude に伝える\n2. **C** iCloud の `動画/` に作業フォルダ `{動画名}/` を作る。前の動画の作業フォルダから `narration.py`・`scenes.py`・`sound.py`・`thumb.py`・`概要欄_下書き.md` を写して、書き換える元にする\n\n### 台本(2節・3節)\n\n3. **C** 台本を `narration.py` に書く。1文ごとに(場面の名前、表情、字幕の文、後ろの間)。自己紹介の2文(2節10)と、間の取り方(2節12)は前の動画と同じにする\n4. **C** 読みの直しを書く。`READ`(仮音声用)と `ELEVEN_READ`(ElevenLabs 用)。「エイイチ」は ElevenLabs には「えいいち」で渡す(3-1)\n5. **C** `make_docs.py` で読み物の台本と codex の依頼を作る。**エ** 内容を確かめる。数字と出典は、公開前に元のページを開いて確かめる\n6. **エ** 台本を確定する(ここで決まった文が、声のお金を使う単位になる)\n\n### 声(3-1)\n\n7. **エ** Mac のターミナルで、作業フォルダに移って声を作る:\n `read -s ELEVENLABS_API_KEY && export ELEVENLABS_API_KEY`(キーを貼って Enter。画面には出ない)\n `python3 ../_制作ベース/tts_eleven.py --voice gWJpc8whMBqnF7Y0kc9f --name ハコブ`\n 読み方を比べたい語があるときは `--try \"文1|文2\"` で試しの音声だけ作る\n8. **C** `tts_import.py voice_eleven/ハコブ` で取り込む。読み間違い(例: 裏表が「うらおもてうら」)が見つかったら、`ELEVEN_READ` を直し、**エ** がその文だけ `--lines 番号` で作り直す\n\n### 音(8節の BGM・効果音の行)\n\n9. **C** `sound.py` に BGM と効果音の置き方を書く。いまの決まり: BGM は `bgm_明るいB_2` を最初から最後までループ、話している間 -23dB・間 -16dB。場面の切り替えはホワーン(-6dB)、自己紹介はキラリン、大きな流れの変わり目はポロロン\n10. **C** `audio_mix.py --first N` で音だけ書き出し、**エ** が冒頭を聞いて大きさを確かめる\n\n### 図と字幕(4節・5節・6節)\n\n11. **C** `scenes.py` に場面ごとの図を書く。実機映像は `src/` に置き、`VIDEO` に場面名と開始秒を書く。透かし入りの映像は使わない(6節)\n12. **C** `render.py` で図・テロップ・字幕を作る。ハコブくんの区画と重なったら止まるので、直して通す\n13. **C** `preview.py 文番号…` で、気になる文の画面を静止画で出す。**エ** 確かめる\n\n### 書き出しと確認(7節)\n\n14. **C** `assemble.py` で全体を書き出す(14分で30分ほど)。確認用(30MB 以下)をチャットに送り、KIOXIA の `videos/{動画名}/` にも置く\n15. **エ** 通しで見て、直す所を時間つきで伝える(例:「3分16秒の読みがおかしい」「55秒の右下がモヤモヤ」)\n16. **C** 直して書き出し直す(中身が変わった文だけ作り直すので、2回目からは速い)。15〜16 を合格まで繰り返す\n17. **C** アップロード用(crf 20)を KIOXIA に置く。大きいファイルは分けて送り、Mac でつなぎ、指紋(md5)が一致するのを確かめる\n\n### YouTube に出すもの(7節)\n\n18. **C** `概要欄_下書き.md` を書き、`description.py` で `概要欄.txt` を作る。目次の時間は自動で入る。リンクは開いて確かめてから載せる\n19. **C** タイトル案を3つ出す。**エ** 決める\n20. **C** `thumb.py` に文言と写真を書き、`thumbnail.py` でサムネイルを作る(8節のサムネイルの行)。写真は、ハコブくんを左下に置いてもアームや物に重ならないコマを実機映像から選ぶ。**エ** 確かめる\n21. **エ** YouTube にアップロードする。設定: 子ども向けは「いいえ」。字幕ファイル(srt)は、映像に字幕が入っているので、上げると字幕をオンにした人には2重に出る(上げるかはエイイチが決める)。公開直後は低い画質で再生され、HD(1080p)は数十分〜1時間後に出る\n\n### 公開のあと\n\n22. **C** X と LinkedIn の投稿文を作る(書き方は article_rules.md の SNS 節。X は URL 込み140字・LinkedIn は英語→日本語と、コメント欄への補足)。**エ** 動画の URL を入れて投稿する\n23. **C** この動画で決めたこと・直したことを、本ファイルの該当の節と更新履歴に書く。iCloud の `_制作ベース` とプロジェクトの `claude/video_rules.md` の両方に保存する\n\n### 前の動画でつまずいた所(先に気をつける)\n\n- ElevenLabs の API キーには「音声の読み上げ・効果音・音楽生成」の3つの許可が要る(音楽生成がないと BGM が作れない)\n- Claude から Mac へ送ったファイルが途中で変わることがある(大きい動画・透明部分のある PNG)。送ったあとは指紋(md5)を比べ、違えば圧縮して送り直す\n- Mac 側のファイルが古い版に戻っていたことがある。書き出しの前に、Mac と Claude の作業環境でスクリプトの指紋がそろっているか確かめる\n</code></pre><h2 id=\"index_B9rTkmMC\">まだ決まっていないこと</h2><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>いまの状態</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ポロロンの使い分け</p></td><td colspan=\"1\" rowspan=\"1\"><p>大きな流れの変わり目7か所すべてで同じポロロンを鳴らしている。場所ごとに音を変えるかは試し中</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ジングルとアウトロの文言</p></td><td colspan=\"1\" rowspan=\"1\"><p>「Pixels to Motors。案内役のハコブくんです。」とエンディングの文は、毎回同じにするか未定</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>字幕ファイルの登録</p></td><td colspan=\"1\" rowspan=\"1\"><p>焼き込んだ字幕と2重になる。登録を残すかは未定</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>HD の版の画質</p></td><td colspan=\"1\" rowspan=\"1\"><p>1080p の版での文字の見え方をまだ見ていない。甘ければ 2560×1440 で書き出し直す</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>チャンネル名の反映</p></td><td colspan=\"1\" rowspan=\"1\"><p>「フィジカルAI解説|Pixels to Motors」が表示されていない。上限に当たったかを確かめる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>動画のハコブくんの体</p></td><td colspan=\"1\" rowspan=\"1\"><p>作り直した体は、サムネイルだけで使っている。動画にも使うなら、hakobu.py の画面の四隅・頭の位置・光の輪の位置を測り直し、表情を一覧で確かめる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1X NEO の配送</p></td><td colspan=\"1\" rowspan=\"1\"><p>「配送開始を示す公式発表が見つからない」は 2026年10月12日に確かめた内容。公式発表が出たら、概要欄を直す</p></td></tr></tbody></table></div><h2 id=\"index_XNI1szIP\">この記事で作った・直したスクリプトの全文</h2><p>動画ごとの作業フォルダ(<code>動画/フィジカルAIの現在地/</code>)のファイルと、どの動画でも使う部品を置く <code>_制作ベース/</code> のファイルです。<code>_制作ベース/</code> のうち、この記事の作業で新しく作ったもの、または直したものを載せています。図とハコブくんの描き方(<code>scene_kit.py</code>・<code>hakobu.py</code>・<code>render.py</code>・<code>subtitle.py</code>)は、LOG331 から変えていないので載せていません。</p><h3 id=\"index_OykTUSdQ\">_制作ベース/timing.py</h3><p>新規。文ごとの尺(コマ数)と、ナレーションのつなぎ方を決める。assemble.py と audio_mix.py が同じ規則を使うために分けた(51行)。</p><pre><code class=\"language-python\">\"\"\"文ごとの尺(フレーム)と、ナレーションの音をつなぐ規則。assemble.py と audio_mix.py が同じものを使う。\"\"\"\nimport json, wave\nimport numpy as np\nfrom subtitle import pages as sub_pages\n\nFPS, SR = 30, 48000\n\n\ndef load(first=None):\n idx = json.load(open('tts/index.json'))\n return idx[:first] if first else idx\n\n\ndef layout(idx):\n \"\"\"各文に frames(間を含む尺)・speech(読み上げの長さ)・start(開始フレーム)・parts(字幕の枚の切り替え)を足す。全体のフレーム数を返す\"\"\"\n t = 0\n for o in idx:\n o['frames'] = max(1, round((o['dur'] + o['pause']) * FPS))\n o['speech'] = round(o['dur'] * FPS)\n o['start'] = t\n t += o['frames']\n # 「|」で分けた字幕は、読み上げの長さを字数で割って切り替える\n pages = sub_pages(o['text']) if o['text'] else []\n cuts, acc, total = [0], 0, sum(len(p) for p in pages)\n for p in pages[:-1]:\n acc += len(p)\n cuts.append(round(o['speech'] * acc / total))\n cuts.append(o['frames'])\n o['parts'] = [(k, cuts[k], cuts[k + 1]) for k in range(len(pages))] or [(None, 0, o['frames'])]\n return t\n\n\ndef narration(idx):\n \"\"\"各文の音声を、尺ちょうどに合わせてつないだ int16 モノラル\"\"\"\n parts = []\n for o in idx:\n need = o['frames'] * SR // FPS\n if o['text']:\n with wave.open(f\"tts/{o['i']:03d}.wav\") as w:\n a = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16)\n else:\n a = np.zeros(0, dtype=np.int16)\n a = a[:need]\n parts.append(np.concatenate([a, np.zeros(need - len(a), dtype=np.int16)]))\n return np.concatenate(parts)\n\n\ndef write_wav(path, a, ch=1):\n \"\"\"a は int16(モノラルは1次元、ステレオは (n, 2))\"\"\"\n with wave.open(path, 'wb') as w:\n w.setnchannels(ch); w.setsampwidth(2); w.setframerate(SR); w.writeframes(np.ascontiguousarray(a).tobytes())\n</code></pre><h3 id=\"index_P6iLbpu7\">_制作ベース/audio_mix.py</h3><p>新規。ナレーションに BGM と効果音を混ぜる。話している間は BGM を下げ、0.8秒以上の間で戻す。曲の終わりを切ってループする(173行)。</p><pre><code class=\"language-python\">\"\"\"ナレーションに BGM と効果音を混ぜる。音の置き方は、動画の作業フォルダの sound.py に書く。\n使い方(動画の作業フォルダで)\n python3 <_制作ベース>/audio_mix.py [--first N] 音だけ先に確かめる → 音の確認.m4a(映像は作らない)\n assemble.py は sound.py があると、これを呼んで mix.wav を使う\n\nsound.py の書き方\n BGM = [dict(file='bgm_仮', start=0, end=None, level=-22, gap=-15, fade_in=0.8, fade_out=2.0, offset=0.0)]\n file 作業フォルダからの相対パス(sound/bgm_main.mp3 など)。見つからなければ <_制作ベース>/sound/{file}.wav\n start 流し始める文の番号。end は止める文の番号(その文は含まない。None は最後まで)\n level 話している間の大きさ。ナレーションより何 dB 小さくするか(-22 なら、ナレーションの約 1/12 の音圧)\n gap 話していない間(0.8 秒以上の間)の大きさ。少し上げて、間が空っぽに聞こえないようにする\n fade_in / fade_out 入りと終わりのフェード(秒)。offset は BGM のどこから流すか(秒)\n 曲の終わりの小さくなっていく所は切り、BGM が区間より短いときは 2 秒重ねてループする\n SE = [(文番号, 文の頭から何秒後, 音, ナレーションより何dB)]\n 音は file と同じ探し方(se_pop など)。例: (2, 0.0, 'se_pop', -8)\n SCENE_SE = dict(file='se_rise', level=-12, offset=-0.15)\n 場面(narration.py の場面の名前)が変わる文ごとに、自動で同じ音を入れる(動画の最初の文は入れない)。\n offset は場面が変わる瞬間から何秒ずらすか(マイナスで少し前から鳴らし、音の山を切り替えに合わせる)\n skip=['場面名', ...] に書いた場面へ切り替わるときは鳴らさない(同じ映像のままテロップだけ替える場面など)\n CHAPTER_SE = dict(file='se_chapter', level=-8, offset=-0.3, scenes=['c1', 'c2', ...])\n scenes に書いた場面(章の始まりなど、大きな流れの変わり目)へ切り替わるときは、SCENE_SE の代わりにこの音を鳴らす\n files={'場面名': '音', ...} を書くと、その場面だけ別の音にする(例: 自己紹介だけキラリン)\n\"\"\"\nimport os, subprocess, sys\nimport numpy as np\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, '.')\nsys.path.insert(1, HERE)\nfrom timing import FPS, SR, load, layout, narration, write_wav\n\nWIN = int(0.05 * SR)\n\n\ndef find(name):\n for p in (name, os.path.join(HERE, 'sound', name), os.path.join(HERE, 'sound', name + '.wav')):\n if os.path.isfile(p):\n return p\n sys.exit(f'音のファイルが見つかりません: {name}(作業フォルダからの相対パスか、_制作ベース/sound/ の名前で書く)')\n\n\ndef read(path):\n \"\"\"どの形式でも 48kHz ステレオの float にする\"\"\"\n raw = subprocess.run(['ffmpeg', '-v', 'error', '-i', path, '-ac', '2', '-ar', str(SR), '-f', 's16le', '-'],\n capture_output=True, check=True).stdout\n return np.frombuffer(raw, dtype=np.int16).reshape(-1, 2).astype(np.float32) / 32768.0\n\n\ndef win_rms(a):\n m = a.mean(axis=1) if a.ndim == 2 else a\n n = len(m) // WIN\n if n == 0:\n return np.array([np.sqrt(np.mean(m ** 2)) + 1e-9])\n return np.sqrt((m[:n * WIN].reshape(n, WIN) ** 2).mean(axis=1)) + 1e-9\n\n\ndef db(x):\n return 10 ** (x / 20)\n\n\ndef cut_tail(a):\n \"\"\"曲の終わりで音が小さくなっていく所(ふつうの大きさより10dB以上小さい所)を切る。ループでつなぐとき、途中で音が消えないようにする\"\"\"\n r = win_rms(a)\n loud = np.where(r > np.median(r[r > 1e-3]) * db(-10))[0]\n if len(loud) == 0:\n return a\n return a[:min(len(a), (loud[-1] + 1) * WIN)]\n\n\ndef loop_to(a, n, xf=SR):\n \"\"\"a を n サンプルまでループで伸ばす(つなぎ目は xf サンプル重ねる)\"\"\"\n if len(a) >= n:\n return a[:n]\n xf = min(xf, len(a) // 4)\n out = a.copy()\n ramp = np.linspace(0, 1, xf)[:, None]\n while len(out) < n:\n head = out[:-xf]\n cross = out[-xf:] * (1 - ramp) + a[:xf] * ramp\n out = np.concatenate([head, cross, a[xf:]])\n return out[:n]\n\n\ndef smooth(x, sec):\n k = max(1, int(sec * 100))\n return np.convolve(np.pad(x, (k, k), mode='edge'), np.ones(k) / k, mode='same')[k:-k]\n\n\ndef mix(idx, pcm, out='mix.wav'):\n ns = {}\n exec(open('sound.py', encoding='utf-8').read(), ns)\n BGM, SE = ns.get('BGM', []), list(ns.get('SE', []))\n sc = ns.get('SCENE_SE')\n ch = ns.get('CHAPTER_SE') or {}\n if sc or ch:\n for prev, o in zip(idx, idx[1:]):\n if o['scene'] == prev['scene']:\n continue\n if o['scene'] in ch.get('scenes', []): # 大きな流れの変わり目は、別の音にする\n f = ch.get('files', {}).get(o['scene'], ch['file']) # 場面ごとに音を替えられる(files={'title': 'se_chapter_glock'})\n SE.append((o['i'], ch.get('offset', -0.3), f, ch.get('level', -8)))\n elif sc and o['scene'] not in sc.get('skip', []):\n SE.append((o['i'], sc.get('offset', -0.15), sc['file'], sc.get('level', -12)))\n voice = pcm.astype(np.float32) / 32768.0\n n = len(voice)\n ref = np.percentile(win_rms(voice), 90) # ナレーションの「ふつうに話している」大きさ\n total = n / SR\n by_i = {o['i']: o for o in idx}\n\n # 話している所(各文の読み上げ)を、前0.15秒・後0.3秒広げる。0.8 秒未満の間は話している扱いのまま(BGM が細かく上下しない)\n ctl = np.zeros(int(total * 100) + 1)\n for o in idx:\n if o['text']:\n a = max(0, int((o['start'] / FPS - 0.15) * 100)); b = int(((o['start'] + o['speech']) / FPS + 0.3) * 100)\n ctl[a:b] = 1\n gaps = np.where(np.diff(np.concatenate([[1], ctl, [1]])) != 0)[0].reshape(-1, 2)\n for a, b in gaps:\n if (b - a) < 50: # 0.5 秒(広げた後)= もとの間で約 0.8〜0.95 秒未満\n ctl[a:b] = 1\n ctl = smooth(ctl, 0.3)\n\n bus = np.zeros((n, 2), dtype=np.float32)\n for m in BGM:\n src = read(find(m['file']))\n src = src[int(m.get('offset', 0) * SR):]\n g = ref / np.median(win_rms(src)[win_rms(src) > 1e-3])\n src = cut_tail(src)\n s0 = by_i[m.get('start', 0)]['start'] * SR // FPS if m.get('start', 0) in by_i else 0\n e = m.get('end')\n e0 = by_i[e]['start'] * SR // FPS if e is not None and e in by_i else n\n seg = loop_to(src, e0 - s0, xf=2 * SR) * g\n # 話している間は level、間は gap(控えめに 100 Hz で決めてから、サンプルに伸ばす)\n t = (np.arange(s0, e0) / SR * 100).astype(int).clip(0, len(ctl) - 1)\n lv = db(m.get('level', -22)) * ctl[t] + db(m.get('gap', -15)) * (1 - ctl[t])\n fi, fo = int(m.get('fade_in', 0.8) * SR), int(m.get('fade_out', 2.0) * SR)\n fade = np.ones(e0 - s0, dtype=np.float32)\n if fi:\n fade[:fi] = np.linspace(0, 1, min(fi, len(fade)))[:len(fade[:fi])]\n if fo:\n fade[-fo:] = np.minimum(fade[-fo:], np.linspace(1, 0, min(fo, len(fade))))\n bus[s0:e0] += seg * (lv * fade)[:, None]\n\n log = []\n for line, off, name, level in SE:\n if line not in by_i:\n continue\n src = read(find(name))\n g = ref / win_rms(src).max() * db(level)\n s0 = max(0, int((by_i[line]['start'] / FPS + off) * SR))\n k = min(len(src), n - s0)\n if k > 0:\n bus[s0:s0 + k] += src[:k] * g\n log.append(f\" {s0 / SR:6.2f}秒 文{line:03d} {name} {level}dB 「{by_i[line]['text'][:16]}」\")\n\n out_a = voice[:, None] + bus\n peak = np.abs(out_a).max()\n if peak > 0.98:\n out_a *= 0.98 / peak # 割れないように全体を下げる(最後の loudnorm で大きさはそろう)\n write_wav(out, (out_a * 32767).astype(np.int16), ch=2)\n print(f'BGM {len(BGM)}本・効果音 {len(log)}個 → {out}')\n print('\\n'.join(log))\n return out\n\n\nif __name__ == '__main__':\n args = sys.argv[1:]\n first = int(args[args.index('--first') + 1]) if '--first' in args else None\n idx = load(first)\n layout(idx)\n pcm = narration(idx)\n f = mix(idx, pcm)\n subprocess.run(['ffmpeg', '-v', 'error', '-y', '-i', f, '-af', 'loudnorm=I=-16:TP=-1.5:LRA=11',\n '-c:a', 'aac', '-b:a', '160k', '-ar', '48000', '音の確認.m4a'], check=True)\n print('音の確認.m4a')\n</code></pre><h3 id=\"index_F7ztL1Qa\">_制作ベース/assemble.py</h3><p>直した。sound.py があれば audio_mix.py を呼ぶ。字幕の後ろの帯をやめた。映像の指定を背景の指紋に入れた。前の場面の映像の続きから流す指定(開始秒 None)を足した(144行)。</p><pre><code class=\"language-python\">\"\"\"1文ずつ背景の映像を作ってつなぎ、ハコブくんを重ねて、音声と合わせる。\n使い方: 動画の作業フォルダで(render.py のあと)\n python3 <_制作ベース>/assemble.py [--first N] [--force] [--name 動画名]\n --first N 最初の N 文だけ書き出す(確かめ用)\n --force 背景の映像を作り直す(ふだんは、できているものは使い回す)\n音: 作業フォルダに sound.py があれば、BGM・効果音を混ぜる(audio_mix.py。音だけ先に確かめるときは audio_mix.py を単独で動かす)\n出力: out.mp4(作業用・crf 20・-16 LUFS)、out_確認用.mp4(crf 29・30MB以下)、out.srt、timeline.json\n\"\"\"\nimport json, subprocess, os, sys, wave, hashlib\nimport numpy as np\nfrom concurrent.futures import ThreadPoolExecutor\nfrom PIL import Image\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, '.')\nsys.path.insert(1, HERE)\nos.environ.setdefault(\"P2M_BASE\", HERE)\nfrom scenes import VIDEO\nfrom hakobu import Hakobu\nfrom subtitle import pages as sub_pages\nfrom timing import FPS, SR, load, layout, narration, write_wav\n\nargs = sys.argv[1:]\nfirst = int(args[args.index('--first') + 1]) if '--first' in args else None\nforce = '--force' in args\nidx = load(first)\nos.makedirs('chunks', exist_ok=True); os.makedirs('comp', exist_ok=True)\nhk = Hakobu()\n\n# ---------- 尺(フレーム単位)と字幕の枚(timing.py。audio_mix.py と同じ規則) ----------\ntotal_frames = layout(idx)\n\n# 実機の映像の場面: 同じ場面が続く間は、続きから再生する。開始秒が None の場面は、前の場面の映像の続きから再生する\n# (同じ映像のまま、テロップだけ替えたいとき。2026-10-12)\nprev, off = None, 0.0\nfor o in idx:\n sc = o['scene']\n if sc in VIDEO:\n if sc != prev and VIDEO[sc][1] is not None:\n off = VIDEO[sc][1]\n o['voff'] = off\n off += o['frames'] / FPS\n prev = sc\n\n# 背景(字幕まで入れて、ハコブくんは入れない)。最後に1回だけ圧縮し直すので、ここは画質を高めに保つ\nENC = ['-c:v', 'libx264', '-preset', 'ultrafast', '-crf', '12', '-pix_fmt', 'yuv420p', '-r', str(FPS), '-an']\njobs = []\nfor o in idx:\n for k, a, b in o['parts']:\n jobs.append((o, k, a, b))\n\n\ndef job(j):\n o, k, a, b = j\n tag = f\"{o['i']:03d}\" + (f\"_{k}\" if k is not None else \"\")\n # 中身(場面・字幕・尺・映像の位置・図と字幕の画像)が変わったら作り直すよう、ファイル名に指紋を入れる\n sub_png = f\"png/sub/{o['i']:03d}_{k}.png\" if k is not None else \"\"\n fp = [o['scene'], o['text'], a, b, round(o.get('voff', 0), 3), VIDEO.get(o['scene'])] # 映像のファイルや透かしの設定が変わっても作り直す\n for f in (f\"png/scene/{o['scene']}.png\", f\"png/ovl/{o['scene']}.png\", sub_png):\n if f and os.path.exists(f):\n fp.append(hashlib.md5(open(f, 'rb').read()).hexdigest())\n out = f\"chunks/{tag}_{hashlib.md5(repr(fp).encode()).hexdigest()[:8]}.mp4\"\n if os.path.exists(out) and not force:\n return out\n sub = f\"png/sub/{o['i']:03d}_{k}.png\" if k is not None else None\n sc, n = o['scene'], str(b - a)\n if sc in VIDEO:\n src, _, wm = VIDEO[sc]\n dl = 'delogo=x=2740:y=1840:w=1010:h=220,' if wm else ''\n ins = ['-ss', f\"{o['voff'] + a / FPS:.3f}\", '-stream_loop', '-1', '-i', src, '-loop', '1', '-i', f'png/ovl/{sc}.png']\n fc = (f\"[0:v]{dl}scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080,fps={FPS},setsar=1[b];\" # 字幕の後ろの黒い帯は敷かない(2026-10-12 エイイチの指示)\n f\"[b][1:v]overlay[c]\")\n if sub:\n ins += ['-loop', '1', '-i', sub]\n fc += \";[c][2:v]overlay[v]\"\n else:\n fc += \";[c]null[v]\"\n cmd = ['ffmpeg', '-v', 'error', '-y'] + ins + ['-filter_complex', fc, '-map', '[v]', '-frames:v', n] + ENC + [out]\n else:\n comp = f\"comp/{tag}.png\"\n im = Image.open(f'png/scene/{sc}.png').convert('RGBA')\n if sub:\n im.alpha_composite(Image.open(sub).convert('RGBA'))\n im.convert('RGB').save(comp)\n cmd = ['ffmpeg', '-v', 'error', '-y', '-loop', '1', '-framerate', str(FPS), '-i', comp, '-frames:v', n,\n '-tune', 'stillimage'] + ENC + [out]\n subprocess.run(cmd, check=True)\n return out\n\n\nwith ThreadPoolExecutor(max(1, min(3, os.cpu_count() or 1))) as ex:\n outs = list(ex.map(job, jobs))\nopen('chunks/list.txt', 'w').write(''.join(f\"file '{os.path.basename(x)}'\\n\" for x in outs))\nsubprocess.run(['ffmpeg', '-v', 'error', '-y', '-f', 'concat', '-safe', '0', '-i', 'chunks/list.txt', '-c', 'copy', 'bg.mp4'], check=True)\n\n# ---------- 音声: 各文を尺ちょうどに合わせて連結。sound.py があれば BGM・効果音を混ぜる(audio_mix.py) ----------\npcm = narration(idx)\nwrite_wav('narration.wav', pcm)\naudio_in = 'narration.wav'\nif os.path.exists('sound.py'):\n from audio_mix import mix\n audio_in = mix(idx, pcm) # mix.wav(ステレオ)。ハコブくんの口はナレーションだけで動かす\n\n# ---------- 字幕 SRT(「|」で分けた枚は別の行にする) ----------\ndef ts(f):\n ms = round(f / FPS * 1000); h, ms = divmod(ms, 3600000); m, ms = divmod(ms, 60000); s, ms = divmod(ms, 1000)\n return f\"{h:02d}:{m:02d}:{s:02d},{ms:03d}\"\nsrt, k = [], 1\nfor o in idx:\n if not o['text']:\n continue\n pages = sub_pages(o['text'])\n for (pk, a, b), ptxt in zip(o['parts'], pages):\n end = min(b, o['speech']) if pk == len(pages) - 1 else b\n srt.append(f\"{k}\\n{ts(o['start'] + a)} --> {ts(o['start'] + end)}\\n{ptxt}\\n\"); k += 1\nopen('out.srt', 'w', encoding='utf-8').write('\\n'.join(srt))\njson.dump(idx, open('timeline.json', 'w'), ensure_ascii=False)\n\n# ---------- ハコブくん: 表情と口・目・リングの動きを決めて、背景に重ねる ----------\ntimeline = [(o['start'], o['start'] + o['frames'], o['start'] + o['speech'], o.get('face', 'base'), bool(o['text'])) for o in idx]\nparams = hk.animate(timeline, pcm.astype(np.float32) / 32768.0)\ncmd = ['ffmpeg', '-v', 'error', '-y', '-i', 'bg.mp4',\n '-f', 'rawvideo', '-pix_fmt', 'rgba', '-s', f'{hk.w}x{hk.h}', '-r', str(FPS), '-i', '-',\n '-i', audio_in,\n '-filter_complex', f'[0:v][1:v]overlay=x={hk.x}:y={hk.y}:eof_action=pass[v]',\n '-map', '[v]', '-map', '2:a', '-c:v', 'libx264', '-preset', 'medium', '-crf', '20', '-pix_fmt', 'yuv420p',\n '-af', 'loudnorm=I=-16:TP=-1.5:LRA=11', '-c:a', 'aac', '-b:a', '192k', '-ar', '48000',\n '-movflags', '+faststart', '-frames:v', str(total_frames), 'out.mp4']\np = subprocess.Popen(cmd, stdin=subprocess.PIPE)\nfor i in range(total_frames):\n p.stdin.write(hk.frame(params[i]).tobytes())\np.stdin.close()\nif p.wait():\n sys.exit('ハコブくんを重ねる書き出しに失敗しました')\nstill = round(total_frames * 0.42)\nsubprocess.run(['ffmpeg', '-v', 'error', '-y', '-ss', f'{still / FPS:.2f}', '-i', 'out.mp4', '-frames:v', '1', 'out_still.png'], check=True)\n\n# ---------- 確認用(チャットで送る方): crf 29・音声 112kbps・30MB 以下 ----------\nfor crf in (29, 32, 35):\n subprocess.run(['ffmpeg', '-v', 'error', '-y', '-i', 'out.mp4', '-c:v', 'libx264', '-preset', 'medium', '-crf', str(crf),\n '-c:a', 'aac', '-b:a', '112k', '-movflags', '+faststart', 'out_確認用.mp4'], check=True)\n mb = os.path.getsize('out_確認用.mp4') / 1e6\n if mb <= 30:\n break\nprint(f'frames {total_frames} = {total_frames / FPS:.1f}s 確認用 {mb:.1f}MB (crf {crf})')\n</code></pre><h3 id=\"index_WrI3e7rF\">_制作ベース/preview.py</h3><p>直した。字幕の後ろの帯をやめ、前の場面の映像の続き(開始秒 None)を、書き出しの記録から読むようにした(54行)。</p><pre><code class=\"language-python\">\"\"\"確かめ用の静止画。動画を書き出さずに、指定した文の画面(図または実機映像の1コマ+字幕+ハコブくん)を1枚ずつ出す。\n使い方: 動画の作業フォルダで(render.py のあと) python3 <_制作ベース>/preview.py 8 9 12\n出力: preview/{文番号}_{字幕の枚}.png と、並べた preview/一覧.png\n\"\"\"\nimport json, os, sys, subprocess\nfrom PIL import Image\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, '.')\nsys.path.insert(1, HERE)\nos.environ.setdefault(\"P2M_BASE\", HERE)\nfrom scenes import VIDEO\nfrom hakobu import Hakobu, EXPR\nfrom subtitle import pages\n\nidx = {o['i']: o for o in json.load(open('tts/index.json'))}\nnums = [int(x) for x in sys.argv[1:]] or [0]\nos.makedirs('preview', exist_ok=True)\nhk = Hakobu()\nouts = []\nfor i in nums:\n o = idx[i]\n sc = o['scene']\n if sc in VIDEO:\n src, start, wm = VIDEO[sc]\n start = o.get('voff', start) if start is None else start # 続きから再生する場面は、書き出しのときの位置(timeline.json)を使う\n if start is None:\n tl = {t['i']: t for t in json.load(open('timeline.json'))} if os.path.exists('timeline.json') else {}\n start = tl.get(i, {}).get('voff', 0.0)\n tmp = f'preview/_v{i}.png'\n subprocess.run(['ffmpeg', '-v', 'error', '-y', '-ss', f'{start:.2f}', '-i', src, '-frames:v', '1',\n '-vf', ('delogo=x=2740:y=1840:w=1010:h=220,' if wm else '') + 'scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080', tmp], check=True)\n bg = Image.open(tmp).convert('RGBA')\n bg.alpha_composite(Image.open(f'png/ovl/{sc}.png').convert('RGBA'))\n else:\n bg = Image.open(f'png/scene/{sc}.png').convert('RGBA')\n ex = EXPR[o.get('face', 'base')]\n face = hk.frame((ex['eyes'], ex['brows'], 'mid', 0.4, 0.0))\n n = len(pages(o['text'])) if o['text'] else 1\n for k in range(n):\n im = bg.copy()\n sub = f'png/sub/{i:03d}_{k}.png'\n if os.path.exists(sub):\n im.alpha_composite(Image.open(sub).convert('RGBA'))\n im.alpha_composite(face, (hk.x, hk.y))\n path = f'preview/{i:03d}_{k}.png'\n im.convert('RGB').save(path)\n outs.append(path)\ncols = 2\nw, h = 960, 540\nsheet = Image.new('RGB', (w * cols, h * ((len(outs) + cols - 1) // cols)), 'white')\nfor k, p in enumerate(outs):\n sheet.paste(Image.open(p).resize((w, h)), ((k % cols) * w, (k // cols) * h))\nsheet.save('preview/一覧.png')\nprint('\\n'.join(outs))\n</code></pre><h3 id=\"index_Y7zJS_Te\">_制作ベース/sound_synth.py</h3><p>新規。効果音(ホワーン・キラリン・ポロロンなど)と、仮の BGM をコードで作る(386行)。</p><pre><code class=\"language-python\">\"\"\"効果音と仮のBGMを、コードで作る(音源サイトに頼らない。だれでも同じ音を作り直せる)。\n使い方: python3 <_制作ベース>/sound_synth.py → <_制作ベース>/sound/ に wav を書く(warm を付けると温かみの BGM、bgm3 を付けると BGM の候補3種を作る)\n se_pop.wav ぽん(図・数字が出る。いちばんよく使う)\n se_tick.wav こつ(小さな切り替え。字幕の枚が変わる・箇条書きが1つ出る)\n se_whoosh.wav しゅっ(章や場面が大きく変わる)\n se_ding.wav ちん(結論・大事な数字)\n se_rise.wav ひゅい(驚き・問いかけ)。エイイチは「ホワーン」と呼ぶ。場面の切り替えに使う\n se_chapter.wav ポロン(章の始まりなど、大きな流れの変わり目。2026-10-12 追加)\n se_chapter_marimba.wav ポロロン(マリンバの上がる3音)。se_chapter の候補\n se_chapter_harp.wav シャララン(ハープ風に弦をはじいて上がる)。se_chapter の候補\n se_chapter_piano.wav ポーン(やわらかいピアノの和音を1つ)。se_chapter の候補\n se_chapter_glock.wav キラリン(鉄琴の高い3音)。se_chapter の候補\n se_chapter_swell.wav フワァ(弦の和音がふくらんで消える)。se_chapter の候補\n se_chapter_wind.wav シャラン(ウィンドチャイム)。se_chapter の候補\n bgm_仮.wav 仮のBGM(電子音。32秒で切れ目なくループ)\n bgm_仮_温かみ.wav 仮のBGM(はじく弦・やわらかいピアノ・弦の和音・ウッドベース風。2026-10-12 エイイチの「もう少し温かみ」の指示で追加)\n ElevenLabs Music などの本番のBGMができるまでの、方向を確かめる用\n bgm_1_ピアノ / bgm_2_ギターとマリンバ / bgm_3_ピッツィカート BGM の候補3種(2026-10-12)\n音の高さは、ナレーション(男声・おおむね100〜300Hz)とぶつからないよう、効果音は 500Hz より上を中心にしている。\n\"\"\"\nimport os, sys\nimport numpy as np\nfrom timing import SR, write_wav\n\nHERE = os.path.dirname(os.path.abspath(__file__))\nOUT = os.path.join(HERE, 'sound')\nrng = np.random.default_rng(7)\n\n\ndef t_(dur):\n return np.arange(int(dur * SR)) / SR\n\n\ndef env(n, attack=0.004, decay=0.15):\n t = np.arange(n) / SR\n return np.minimum(1, t / attack) * np.exp(-t / decay)\n\n\ndef norm(a, peak=0.9):\n return a / (np.abs(a).max() + 1e-9) * peak\n\n\ndef lowpass(a, cutoff):\n \"\"\"1次のローパス(やわらかくするだけ)\"\"\"\n k = np.exp(-2 * np.pi * cutoff / SR)\n out = np.empty_like(a); y = 0.0\n for i, x in enumerate(a):\n y = (1 - k) * x + k * y; out[i] = y\n return out\n\n\ndef pop():\n t = t_(0.12)\n f = 520 + 520 * np.exp(-t / 0.018) # 高い所から素早く下がる\n a = np.sin(2 * np.pi * np.cumsum(f) / SR) * env(len(t), 0.002, 0.035)\n a += 0.25 * np.sin(2 * np.pi * 2 * np.cumsum(f) / SR) * env(len(t), 0.001, 0.015)\n return norm(a)\n\n\ndef tick():\n t = t_(0.05)\n a = np.sin(2 * np.pi * 1800 * t) * env(len(t), 0.0008, 0.008)\n a += 0.4 * lowpass(rng.standard_normal(len(t)), 4000) * env(len(t), 0.0005, 0.004)\n return norm(a, 0.7)\n\n\ndef whoosh():\n d = 0.55; t = t_(d); n = len(t)\n noise = rng.standard_normal(n)\n # 帯域を低→高へ動かす(2つのローパスの差で帯域を作る)\n out = np.empty(n); y1 = y2 = 0.0\n for i in range(n):\n p = i / n\n c1 = 300 + 3200 * p ** 1.5; c2 = c1 * 0.35\n k1 = np.exp(-2 * np.pi * c1 / SR); k2 = np.exp(-2 * np.pi * c2 / SR)\n y1 = (1 - k1) * noise[i] + k1 * y1; y2 = (1 - k2) * noise[i] + k2 * y2\n out[i] = y1 - y2\n shape = np.sin(np.pi * np.minimum(1, t / d)) ** 2 * (t / d) ** 0.4\n return norm(out * shape, 0.8)\n\n\ndef ding():\n t = t_(1.2)\n f0 = 1046.5 # C6\n a = (np.sin(2 * np.pi * f0 * t) * np.exp(-t / 0.45)\n + 0.35 * np.sin(2 * np.pi * f0 * 2.0 * t) * np.exp(-t / 0.2)\n + 0.12 * np.sin(2 * np.pi * f0 * 3.0 * t) * np.exp(-t / 0.08))\n a *= np.minimum(1, t / 0.003)\n return norm(a, 0.8)\n\n\ndef rise():\n t = t_(0.35)\n f = 600 * (2 ** (t / 0.35 * 1.0)) # 1オクターブ上がる\n a = np.sin(2 * np.pi * np.cumsum(f) / SR)\n a *= np.sin(np.pi * t / 0.35) ** 1.5\n return norm(a, 0.7)\n\n\ndef chapter():\n \"\"\"章の変わり目の「ポロン」。やわらかい鐘の音を2つ(ミ→ラ)、少しずらして鳴らす。ホワーンより長く、落ち着いた音\"\"\"\n d = 1.6; t = t_(d); out = np.zeros(len(t))\n for k, (f0, at, g) in enumerate([(659.3, 0.0, 1.0), (880.0, 0.13, 0.85)]):\n tt = np.clip(t - at, 0, None); on = (t >= at)\n a = (np.sin(2 * np.pi * f0 * tt) * np.exp(-tt / 0.55) + 0.3 * np.sin(2 * np.pi * f0 * 2 * tt) * np.exp(-tt / 0.25)\n + 0.1 * np.sin(2 * np.pi * f0 * 3.01 * tt) * np.exp(-tt / 0.1))\n out += g * a * on * np.minimum(1, tt / 0.004)\n return norm(lowpass(out, 6000), 0.8)\n\n\ndef bgm_placeholder():\n \"\"\"やわらかいパッドと、小さな粒の音(アルペジオ)。テンポ 96・4小節×2=32秒・ループで切れ目が出ないよう、はみ出した余韻は頭に回す\"\"\"\n bpm = 96; beat = 60 / bpm; bars = 8; L = int(bars * 4 * beat * SR)\n buf = np.zeros((L, 2))\n chords = [[57, 60, 64, 67], [53, 57, 60, 64], [48, 52, 55, 59], [55, 59, 62, 64]] # Am7 Fmaj7 Cmaj7 G6(MIDI番号)\n hz = lambda m: 440 * 2 ** ((m - 69) / 12)\n\n def add(sig, start, pan=0.0):\n i0 = int(start * SR); idx = (np.arange(len(sig)) + i0) % L\n np.add.at(buf[:, 0], idx, sig * (1 - pan) * 0.5 + sig * 0.5 * (1 - abs(pan)))\n np.add.at(buf[:, 1], idx, sig * (1 + pan) * 0.5 + sig * 0.5 * (1 - abs(pan)))\n\n for b in range(bars):\n ch = chords[b % 4]\n d = 4 * beat\n t = t_(d + 1.5)\n pad_env = np.minimum(1, t / 0.9) * np.where(t < d, 1, np.exp(-(t - d) / 0.6))\n for k, m in enumerate(ch):\n f = hz(m)\n s = sum(np.sin(2 * np.pi * f * (1 + det) * t + ph) for det, ph in ((-0.003, 0), (0.003, 1.3)))\n s += 0.2 * np.sin(2 * np.pi * 2 * f * t)\n add(0.05 * s * pad_env, b * d, pan=(k - 1.5) * 0.25)\n bass = hz(ch[0] - 12)\n tb = t_(d)\n add(0.10 * np.sin(2 * np.pi * bass * tb) * np.minimum(1, tb / 0.05) * np.exp(-tb / 2.5), b * d)\n # 8分音符の粒(上の2音を行き来する)\n for s8 in range(8):\n m = ch[[1, 2, 3, 2, 1, 2, 3, 2][s8]] + 12\n tp = t_(0.6)\n note = np.sin(2 * np.pi * hz(m) * tp) * np.exp(-tp / 0.18) * np.minimum(1, tp / 0.004)\n vel = 0.05 if s8 % 2 == 0 else 0.032\n add(vel * note, b * d + s8 * beat / 2, pan=0.35 if s8 % 2 else -0.35)\n buf[:, 0] = lowpass(buf[:, 0], 5000); buf[:, 1] = lowpass(buf[:, 1], 5000)\n return buf / (np.abs(buf).max() + 1e-9) * 0.8\n\n\ndef pluck(f, dur, bright=0.5):\n \"\"\"はじいた弦(Karplus-Strong)。bright が小さいほど丸い音\"\"\"\n n = int(dur * SR); p = max(2, int(SR / f))\n buf = lowpass(rng.uniform(-1, 1, p), 1500 + 5000 * bright)\n out = np.empty(n)\n for i in range(n):\n out[i] = buf[i % p]\n buf[i % p] = 0.996 * 0.5 * (buf[i % p] + buf[(i + 1) % p])\n return out * np.minimum(1, np.arange(n) / (0.002 * SR))\n\n\ndef felt_piano(f, dur):\n \"\"\"フェルトを挟んだピアノ風(倍音を少し伸ばしてずらし、高い倍音ほど早く消す)\"\"\"\n t = t_(dur)\n a = sum((0.6 ** k) * np.sin(2 * np.pi * f * (k + 1) * (1 + 0.0004 * k * k) * t) * np.exp(-t / (1.8 / (k + 1) ** 0.8))\n for k in range(6))\n return a * np.minimum(1, t / 0.006)\n\n\ndef strings(f, dur):\n \"\"\"弦の和音のパッド(ゆっくり入り、少しビブラート)\"\"\"\n t = t_(dur)\n vib = 1 + 0.003 * np.sin(2 * np.pi * 5.2 * t)\n a = sum(np.sin(2 * np.pi * f * k * vib * t + k) / k ** 1.3 for k in range(1, 7))\n a += 0.6 * sum(np.sin(2 * np.pi * f * 1.002 * k * vib * t) / k ** 1.3 for k in range(1, 7))\n return a * np.minimum(1, t / 1.2) * np.minimum(1, (dur - t) / 0.8)\n\n\ndef bgm_warm():\n \"\"\"テンポ 88・8小節。ギターの分散和音、ピアノの和音、弦、ウッドベース風。ループで切れ目が出ないよう余韻は頭に回す\"\"\"\n bpm = 88; beat = 60 / bpm; bars = 8; L = int(bars * 4 * beat * SR)\n buf = np.zeros((L, 2))\n hz = lambda m: 440 * 2 ** ((m - 69) / 12)\n prog = [[48, 55, 64, 67, 72], [45, 52, 60, 64, 69], [41, 53, 57, 60, 65], [43, 55, 59, 62, 67]] # C Am F G(ギターの押さえ方に近い並び)\n\n def add(sig, start, pan=0.0, gain=1.0):\n i0 = int(start * SR); idx = (np.arange(len(sig)) + i0) % L\n np.add.at(buf[:, 0], idx, sig * gain * (1 - max(pan, 0)))\n np.add.at(buf[:, 1], idx, sig * gain * (1 + min(pan, 0)))\n\n for b in range(bars):\n ch = prog[b % 4]; d = 4 * beat; t0 = b * d\n # ギター: 8分音符の分散和音(指ではじく)\n pat = [0, 2, 3, 4, 1, 3, 2, 4]\n for k, j in enumerate(pat):\n vel = 0.22 if k % 2 == 0 else 0.15\n add(pluck(hz(ch[j]), 1.6, bright=0.35), t0 + k * beat / 2 + rng.uniform(0, 0.008), pan=-0.35, gain=vel)\n # ピアノ: 小節の頭と3拍目に、高い所で和音を置く\n for beat_at, g in ((0, 0.10), (2, 0.06)):\n for m in ch[2:]:\n add(felt_piano(hz(m + 12), 2.4), t0 + beat_at * beat + 0.01, pan=0.3, gain=g)\n # 弦: 和音の3音を長く\n for m in ch[1:4]:\n add(lowpass(strings(hz(m), d + 0.6), 2500), t0, pan=0.0, gain=0.018)\n # ウッドベース風: 1拍目と3拍目\n for beat_at, m in ((0, ch[0] - 12), (2, ch[0] - 12 + (7 if b % 2 else 0))):\n add(pluck(hz(m), 1.2, bright=0.1), t0 + beat_at * beat, gain=0.5)\n buf[:, 0] = lowpass(buf[:, 0], 6000); buf[:, 1] = lowpass(buf[:, 1], 6000)\n return buf / (np.abs(buf).max() + 1e-9) * 0.8\n\n# ---------- BGM の候補(2026-10-12 エイイチの指示で3種。どれも電子音を避け、楽器に近い音で組む) ----------\nHZ = lambda m: 440 * 2 ** ((m - 69) / 12)\n\n\nclass Loop:\n \"\"\"小節数ぶんの輪(ループ)の入れ物。はみ出した余韻は頭に回すので、つなぎ目が出ない\"\"\"\n def __init__(self, bpm, bars):\n self.beat = 60 / bpm; self.bar = 4 * self.beat\n self.L = int(bars * self.bar * SR); self.buf = np.zeros((self.L, 2))\n\n def add(self, sig, start, pan=0.0, gain=1.0):\n i0 = int(start * SR); idx = (np.arange(len(sig)) + i0) % self.L\n np.add.at(self.buf[:, 0], idx, sig * gain * (1 - max(pan, 0)))\n np.add.at(self.buf[:, 1], idx, sig * gain * (1 + min(pan, 0)))\n\n def out(self, cutoff=6500):\n b = self.buf\n b[:, 0] = lowpass(b[:, 0], cutoff); b[:, 1] = lowpass(b[:, 1], cutoff)\n return b / (np.abs(b).max() + 1e-9) * 0.8\n\n\ndef marimba(f, dur=1.0):\n \"\"\"木の鍵盤(マリンバ)。倍音が 1・3.9・9.2 倍にあり、すぐ消える\"\"\"\n t = t_(dur)\n a = (np.sin(2 * np.pi * f * t) * np.exp(-t / 0.35) + 0.25 * np.sin(2 * np.pi * f * 3.9 * t) * np.exp(-t / 0.06)\n + 0.08 * np.sin(2 * np.pi * f * 9.2 * t) * np.exp(-t / 0.02))\n return a * np.minimum(1, t / 0.002)\n\n\ndef pizz(f, dur=0.7):\n \"\"\"弦をはじく(ピッツィカート)。ギターより短く丸い\"\"\"\n a = pluck(f, dur, bright=0.2)\n return a * np.exp(-t_(dur)[:len(a)] / 0.18)\n\n\ndef shaker(dur=0.09):\n t = t_(dur)\n n = rng.standard_normal(len(t))\n return (n - lowpass(n, 5000)) * np.sin(np.pi * t / dur) ** 2\n\n\ndef bgm_piano():\n \"\"\"1. ピアノ中心・ゆったり(テンポ76)。ピアノの分散和音に弦がうすく重なる。考える場面向き。\n 分散和音はナレーションの声の高さ(男声 100〜300Hz)とぶつからないよう、1オクターブ上で弾く\"\"\"\n lp = Loop(76, 8)\n prog = [[41, 53, 57, 60, 64], [40, 52, 55, 59, 62], [38, 50, 53, 57, 60], [36, 48, 52, 55, 59]] # Fmaj7 Em7 Dm7 Cmaj7\n mel = [72, None, 71, None, 69, None, 67, None] # 小節の頭にだけ置く、ゆっくり下がる音\n for b in range(8):\n ch = prog[b % 4]; t0 = b * lp.bar\n for k, j in enumerate([1, 2, 3, 4, 3, 2, 3, 4]):\n lp.add(felt_piano(HZ(ch[j] + 12), 2.6), t0 + k * lp.beat / 2 + rng.uniform(0, 0.01), pan=0.15, gain=0.09 if k % 2 == 0 else 0.065)\n lp.add(felt_piano(HZ(ch[0]), 3.5), t0, gain=0.12)\n if mel[b]:\n lp.add(felt_piano(HZ(mel[b] + 12), 3.0), t0 + 0.02, pan=-0.2, gain=0.06)\n for m in ch[1:4]:\n lp.add(lowpass(strings(HZ(m), lp.bar + 0.8), 2200), t0, gain=0.014)\n return lp.out(6000)\n\n\ndef bgm_guitar_marimba():\n \"\"\"2. ギターとマリンバ・明るめ(テンポ96)。ギターの刻みに、木の鍵盤の短いフレーズ。フック・導入向き\"\"\"\n lp = Loop(96, 8)\n prog = [[48, 55, 64, 67, 72], [47, 55, 62, 67, 71], [45, 52, 60, 64, 69], [41, 53, 57, 60, 65]] # C G/B Am F\n riff = [(0, 0), (0.75, 2), (1.5, 1), (2.5, 2), (3, 3)] # (拍, 和音の何番目) マリンバ\n for b in range(8):\n ch = prog[b % 4]; t0 = b * lp.bar\n for k, j in enumerate([0, 2, 3, 4, 2, 3, 4, 3]):\n lp.add(pluck(HZ(ch[j]), 1.4, bright=0.4), t0 + k * lp.beat / 2 + rng.uniform(0, 0.006), pan=-0.4, gain=0.2 if k % 2 == 0 else 0.13)\n if b % 2 == 1:\n for at, j in riff:\n lp.add(marimba(HZ(ch[2:][j % 3] + 12)), t0 + at * lp.beat, pan=0.4, gain=0.16)\n for at in (0, 2):\n lp.add(pluck(HZ(ch[0] - 12), 1.1, bright=0.1), t0 + at * lp.beat, gain=0.5)\n for k in range(8):\n lp.add(shaker(), t0 + k * lp.beat / 2 + lp.beat / 4, pan=0.2, gain=0.05 if k % 2 else 0.03)\n return lp.out(4000) # はじく音は高い成分が多いので、丸めて温かくする\n\n\ndef bgm_pizzicato():\n \"\"\"3. 弦のピッツィカートとピアノ・少し先が気になる感じ(テンポ104)。ドキュメンタリーの解説向き\"\"\"\n lp = Loop(104, 8)\n prog = [[45, 57, 60, 64], [41, 57, 60, 65], [36, 55, 60, 64], [43, 55, 59, 62]] # Am F C G\n for b in range(8):\n ch = prog[b % 4]; t0 = b * lp.bar\n for k, j in enumerate([1, 2, 3, 2, 1, 2, 3, 2]):\n lp.add(pizz(HZ(ch[j])), t0 + k * lp.beat / 2, pan=-0.3 if k % 2 else 0.3, gain=0.22 if k % 2 == 0 else 0.15)\n for at, m in ((0, ch[0] - 12), (1.5, ch[0] - 12), (2, ch[0] - 5)):\n lp.add(pizz(HZ(m), 0.9), t0 + at * lp.beat, gain=0.45)\n for m in ch[1:]:\n lp.add(felt_piano(HZ(m + 12), 2.2), t0 + 0.01, pan=0.1, gain=0.05)\n if b >= 4: # 後半だけ弦の和音が入り、少しふくらむ\n for m in ch[1:]:\n lp.add(lowpass(strings(HZ(m), lp.bar + 0.6), 2400), t0, gain=0.012)\n return lp.out(3800)\n\n\nBGMS = {'bgm_1_ピアノ': bgm_piano, 'bgm_2_ギターとマリンバ': bgm_guitar_marimba, 'bgm_3_ピッツィカート': bgm_pizzicato}\n\n\n# ---------- 大きな流れの変わり目の音の候補(2026-10-12 エイイチの指示で3つ) ----------\ndef chapter_marimba():\n \"\"\"ポロロン: マリンバで ド・ミ・ソ と上がる\"\"\"\n out = np.zeros(int(1.6 * SR))\n for k, f in enumerate([523.3, 659.3, 784.0]):\n a = marimba(f, 1.2) * (0.8 if k < 2 else 1.0)\n i = int(k * 0.09 * SR); out[i:i + len(a)] += a[:len(out) - i]\n return norm(lowpass(out, 7000), 0.8)\n\n\ndef chapter_harp():\n \"\"\"シャララン: 弦をはじく音を6つ、ハープのように素早く上がる\"\"\"\n out = np.zeros(int(2.0 * SR))\n for k, m in enumerate([60, 64, 67, 72, 76, 79]):\n a = pluck(440 * 2 ** ((m - 69) / 12), 1.6, bright=0.3) * (0.6 + 0.08 * k)\n i = int(k * 0.055 * SR); out[i:i + len(a)] += a[:len(out) - i]\n return norm(lowpass(out, 6000), 0.8)\n\n\ndef chapter_piano():\n \"\"\"ポーン: やわらかいピアノで ド・ソ・ミ・ソ の和音を1つ(低い音から少しずつずらして弾く)\"\"\"\n out = np.zeros(int(2.2 * SR))\n for k, m in enumerate([48, 55, 64, 67]):\n a = felt_piano(440 * 2 ** ((m - 69) / 12), 2.2) * (1.0 if k == 0 else 0.7)\n i = int(k * 0.025 * SR); out[i:i + len(a)] += a[:len(out) - i]\n return norm(lowpass(out, 5000), 0.8)\n\n\nCHAPTER_CANDS = {'se_chapter_marimba': chapter_marimba, 'se_chapter_harp': chapter_harp, 'se_chapter_piano': chapter_piano}\n\n\ndef chapter_glock():\n \"\"\"キラリン: 鉄琴(グロッケン)で ミ・ソ・ド と上がる。倍音は 1・2.76・5.4 倍(金属の棒の音)\"\"\"\n out = np.zeros(int(1.8 * SR)); t = t_(1.6)\n for k, f in enumerate([1318.5, 1568.0, 2093.0]):\n a = (np.sin(2 * np.pi * f * t) * np.exp(-t / 0.7) + 0.25 * np.sin(2 * np.pi * f * 2.76 * t) * np.exp(-t / 0.15)\n + 0.08 * np.sin(2 * np.pi * f * 5.4 * t) * np.exp(-t / 0.05)) * np.minimum(1, t / 0.001)\n i = int(k * 0.08 * SR); out[i:i + len(a)] += a[:len(out) - i] * (0.7 + 0.15 * k)\n return norm(lowpass(out, 9000), 0.7)\n\n\ndef chapter_swell():\n \"\"\"フワァ: 弦の和音(ド・ミ・ソ・ド)が0.8秒でふくらみ、1秒で消える\"\"\"\n d = 1.9; t = t_(d)\n a = sum(lowpass(strings(440 * 2 ** ((m - 69) / 12), d), 3000) for m in [60, 64, 67, 72])\n shape = np.where(t < 0.8, np.sin(np.pi / 2 * t / 0.8) ** 2, np.exp(-(t - 0.8) / 0.35))\n return norm(a * shape, 0.8)\n\n\ndef chapter_wind():\n \"\"\"シャラン: 細い金属の筒(ウィンドチャイム)が、0.5秒の間に9回ふれあう\"\"\"\n out = np.zeros(int(2.0 * SR)); t = t_(1.4); r = np.random.default_rng(11)\n scale = [1760.0, 1975.5, 2217.5, 2637.0, 2960.0, 3520.0]\n for k in range(9):\n f = scale[r.integers(len(scale))]\n a = (np.sin(2 * np.pi * f * t) + 0.3 * np.sin(2 * np.pi * f * 2.7 * t) * np.exp(-t / 0.2)) * np.exp(-t / 0.6) * np.minimum(1, t / 0.001)\n i = int((k * 0.055 + r.uniform(0, 0.02)) * SR); out[i:i + len(a)] += a[:len(out) - i] * r.uniform(0.5, 1.0)\n return norm(lowpass(out, 10000), 0.6)\n\n\nCHAPTER_CANDS.update({'se_chapter_glock': chapter_glock, 'se_chapter_swell': chapter_swell, 'se_chapter_wind': chapter_wind})\n\n\nif __name__ == '__main__':\n os.makedirs(OUT, exist_ok=True)\n for name, f in [('se_pop', pop), ('se_tick', tick), ('se_whoosh', whoosh), ('se_ding', ding), ('se_rise', rise), ('se_chapter', chapter)]:\n write_wav(os.path.join(OUT, f'{name}.wav'), (f() * 32767).astype(np.int16))\n print(name)\n for name, f in CHAPTER_CANDS.items():\n write_wav(os.path.join(OUT, f'{name}.wav'), (f() * 32767).astype(np.int16))\n print(name)\n if 'bgm3' in sys.argv:\n for name, f in BGMS.items():\n write_wav(os.path.join(OUT, f'{name}.wav'), (f() * 32767).astype(np.int16), ch=2)\n print(name)\n sys.exit()\n if 'warm' not in sys.argv:\n write_wav(os.path.join(OUT, 'bgm_仮.wav'), (bgm_placeholder() * 32767).astype(np.int16), ch=2)\n print('bgm_仮')\n write_wav(os.path.join(OUT, 'bgm_仮_温かみ.wav'), (bgm_warm() * 32767).astype(np.int16), ch=2)\n print('bgm_仮_温かみ')\n</code></pre><h3 id=\"index_0alA5Ix0\">_制作ベース/sound_eleven.py</h3><p>新規。ElevenLabs で BGM と効果音を作る。API キーを使うので Mac で動かす(71行)。</p><pre><code class=\"language-python\">\"\"\"BGM と効果音を ElevenLabs で作る。エイイチの Mac で動かす(API キーを Claude に渡さないため。tts_eleven.py と同じ)。\n\n準備: ターミナルで read -s ELEVENLABS_API_KEY && export ELEVENLABS_API_KEY\n (キーを貼って Enter。画面には出ない)\n\n使い方(動画の作業フォルダで)\n BGM: python3 ../_制作ベース/sound_eleven.py music --name bgm_hook --sec 60 --prompt \"(説明文)\"\n 効果音: python3 ../_制作ベース/sound_eleven.py sfx --name se_pop2 --sec 0.5 --prompt \"(説明文)\"\n --name 保存する名前(sound/{名前}.mp3 になる。sound.py の file にこの名前のパスを書く)\n --sec 長さ(秒)。BGM は 3〜600、効果音は 0.5〜30\n --prompt どんな音か(英語の方が安定する。説明文の候補は 音源_候補.md)\n --count 同じ説明文で何本作るか(既定 1。2 以上だと bgm_hook_1.mp3, bgm_hook_2.mp3…)\n --loop 効果音を切れ目なくループする音にする(環境音など)\nBGM は歌なし(force_instrumental)で作る。\nStarter プランの決まり(2026-10-12 に ElevenLabs の Eleven Music の規約で確認):\n 商用に使える(映画・テレビ・ラジオ・ゲームの一部を除く)。クレジット表記はいらない。\n 作れる曲の長さは月 17 分まで。曲そのものを音楽配信サービスに出すのは禁止(動画の BGM に使うのはこれに当たらない)\n有料プランの間に作った音は、解約したあとも商用に使える(無料プランに戻ってから作った音は使えない)\n\"\"\"\nimport argparse, json, os, sys, time, urllib.request, urllib.error\n\nap = argparse.ArgumentParser()\nap.add_argument('kind', choices=['music', 'sfx'])\nap.add_argument('--name', required=True)\nap.add_argument('--sec', type=float, required=True)\nap.add_argument('--prompt', required=True)\nap.add_argument('--count', type=int, default=1)\nap.add_argument('--loop', action='store_true')\nap.add_argument('--model', default='')\nargs = ap.parse_args()\nkey = os.environ.get('ELEVENLABS_API_KEY')\nif not key:\n sys.exit('ELEVENLABS_API_KEY が入っていません。ターミナルで read -s ELEVENLABS_API_KEY && export ELEVENLABS_API_KEY を実行してから動かしてください')\n\nos.makedirs('sound', exist_ok=True)\nif args.kind == 'music':\n if not 3 <= args.sec <= 600:\n sys.exit('BGM の長さは 3〜600 秒')\n url = 'https://api.elevenlabs.io/v1/music?output_format=mp3_44100_128'\n body = {'prompt': args.prompt, 'music_length_ms': int(args.sec * 1000), 'force_instrumental': True}\nelse:\n if not 0.5 <= args.sec <= 30:\n sys.exit('効果音の長さは 0.5〜30 秒')\n url = 'https://api.elevenlabs.io/v1/sound-generation?output_format=mp3_44100_128'\n body = {'text': args.prompt, 'duration_seconds': args.sec, 'prompt_influence': 0.5}\n if args.loop:\n body['loop'] = True\nif args.model:\n body['model_id'] = args.model\n\nlog = os.path.join('sound', '作った記録.jsonl')\nfor k in range(args.count):\n name = args.name if args.count == 1 else f'{args.name}_{k + 1}'\n path = os.path.join('sound', f'{name}.mp3')\n if os.path.exists(path):\n sys.exit(f'{path} はもうあります(上書きしない)。--name を変えてください')\n req = urllib.request.Request(url, data=json.dumps(body).encode(),\n headers={'xi-api-key': key, 'Content-Type': 'application/json'})\n try:\n with urllib.request.urlopen(req, timeout=600) as r:\n open(path, 'wb').write(r.read())\n extra = {h: r.headers.get(h) for h in ('song-id', 'character-cost') if r.headers.get(h)}\n except urllib.error.HTTPError as e:\n sys.exit(f'{name} で止まりました: {e.code} {e.read().decode(errors=\"ignore\")[:300]}')\n except urllib.error.URLError as e:\n sys.exit(f'{name} で止まりました(ElevenLabs につながらない): {e.reason}')\n with open(log, 'a', encoding='utf-8') as f:\n f.write(json.dumps(dict(file=path, kind=args.kind, sec=args.sec, prompt=args.prompt, loop=args.loop,\n model=args.model or '既定', time=time.strftime('%Y-%m-%d %H:%M'), **extra),\n ensure_ascii=False) + '\\n')\n print(f'できあがり: {path}')\n</code></pre><h3 id=\"index_ZuLtrq6q\">_制作ベース/tts_eleven.py</h3><p>直した。読みを確かめる試しの文だけを作る --try を足した。Mac で動かす(114行)。</p><pre><code class=\"language-python\">\"\"\"ハコブくんの声(ElevenLabs)で、台本を1文ずつ音声にする。エイイチの Mac で動かす(API キーを Claude に渡さないため)。\n\n準備(1回だけ)\n 1. ElevenLabs の有料プランに入る(無料プランの音声は商用に使えない)\n 2. Voice Design で声を作って保存し、その声の Voice ID を控える(Voices の一覧で声を開くと出る)\n 3. ターミナルで API キーを入れる(キーは画面やファイルに残さない): export ELEVENLABS_API_KEY=(キー)\n\n使い方(動画の作業フォルダで。例: 動画/AIエージェントの取り違え/)\n python3 ../_制作ベース/tts_eleven.py --voice VOICE_ID --name 候補A --lines 0-12\n --lines 作る文の番号(0 から。「0-12」「5,8,9」のように書く。省略するとすべて)\n --model eleven_v4(既定。2026-10-12 に決定)/eleven_multilingual_v2/eleven_v3\n --seed 同じ値なら、なるべく同じ読み方になる(既定 7)\n --try 台本ではなく、試しの文を作る(「|」で区切って何文でも。例: --try \"エイイチは…。|栄一は…。\")\n 出力は voice_eleven/{名前}/試し_01.mp3 … と、どの文かの一覧 試し.txt\n出力: voice_eleven/{名前}/{番号}.mp3 と、作った設定の記録 voice_eleven/{名前}/設定.json\nこのあと Claude が tts_import.py で 48kHz の wav にして、口の動きと字幕に使う。\n使う文字数(クレジット)は、文の字数の合計。1本約4,100字。作り直した分も数える\n\"\"\"\nimport argparse, json, os, sys, time, urllib.request, urllib.error\nsys.path.insert(0, '.')\nfrom narration import SEGS\ntry:\n from narration import parse\nexcept ImportError:\n def parse(s):\n return s[0], \"base\", s[1], (s[2] if len(s) > 2 else 0.35)\ntry:\n from narration import ELEVEN_READ # ElevenLabs が読み間違えた語だけ、ここで読みを直す(字幕は変えない)\nexcept ImportError:\n ELEVEN_READ = []\n\nap = argparse.ArgumentParser()\nap.add_argument('--voice', required=True)\nap.add_argument('--name', required=True)\nap.add_argument('--lines', default='')\nap.add_argument('--model', default='eleven_v4') # 2026-10-12 エイイチが v4 に決めた(冒頭14文で v2 と聞き比べ)\nap.add_argument('--seed', type=int, default=7)\nap.add_argument('--stability', type=float, default=0.55)\nap.add_argument('--similarity', type=float, default=0.75)\nap.add_argument('--try', dest='trial', default='')\nargs = ap.parse_args()\nkey = os.environ.get('ELEVENLABS_API_KEY')\nif not key:\n sys.exit('ELEVENLABS_API_KEY が入っていません。ターミナルで export ELEVENLABS_API_KEY=(キー) を実行してから動かしてください')\n\n\ndef nums(spec, n):\n if not spec:\n return list(range(n))\n out = []\n for part in spec.split(','):\n if '-' in part:\n a, b = part.split('-'); out += list(range(int(a), int(b) + 1))\n else:\n out.append(int(part))\n return [i for i in out if 0 <= i < n]\n\n\ndef speak_text(t):\n t = t.replace('|', '').replace('「', '').replace('」', '')\n for a, b in ELEVEN_READ:\n t = t.replace(a, b)\n return t\n\n\ntexts = [parse(s)[2] for s in SEGS]\noutdir = os.path.join('voice_eleven', args.name)\nif args.trial: # 試しの文だけを作る(台本の読みの直し ELEVEN_READ は使わない。書いたとおりに読ませて聞き比べる)\n os.makedirs(outdir, exist_ok=True)\n lines = [t for t in args.trial.split('|') if t]\n with open(os.path.join(outdir, '試し.txt'), 'w', encoding='utf-8') as f:\n for k, t in enumerate(lines, 1):\n f.write(f'試し_{k:02d}: {t}\\n')\n for k, t in enumerate(lines, 1):\n body = {'text': t, 'model_id': args.model, 'seed': args.seed,\n 'voice_settings': {'stability': args.stability, 'similarity_boost': args.similarity}}\n req = urllib.request.Request(\n f'https://api.elevenlabs.io/v1/text-to-speech/{args.voice}?output_format=mp3_44100_128',\n data=json.dumps(body).encode(), headers={'xi-api-key': key, 'Content-Type': 'application/json'})\n try:\n with urllib.request.urlopen(req, timeout=120) as r:\n open(os.path.join(outdir, f'試し_{k:02d}.mp3'), 'wb').write(r.read())\n except urllib.error.HTTPError as e:\n sys.exit(f'試し_{k:02d} で止まりました: {e.code} {e.read().decode(errors=\"ignore\")[:300]}')\n print(f'試し_{k:02d} {t}')\n print(f'できあがり: {outdir} 使った文字数 約{sum(len(t) for t in lines)}字')\n sys.exit(0)\nos.makedirs(outdir, exist_ok=True)\njson.dump(dict(vars(args), time=time.strftime('%Y-%m-%d %H:%M')), open(os.path.join(outdir, '設定.json'), 'w'),\n ensure_ascii=False, indent=1)\nused = 0\nfor i in nums(args.lines, len(SEGS)):\n if not texts[i]:\n continue\n prev = next((speak_text(texts[j]) for j in range(i - 1, -1, -1) if texts[j]), None)\n nxt = next((speak_text(texts[j]) for j in range(i + 1, len(texts)) if texts[j]), None)\n body = {'text': speak_text(texts[i]), 'model_id': args.model, 'seed': args.seed,\n 'voice_settings': {'stability': args.stability, 'similarity_boost': args.similarity}}\n if args.model.startswith('eleven_multilingual'): # 前後の文を渡すと、文のつながりの抑揚が自然になる(対応を確かめたモデルだけ)\n body['previous_text'] = prev\n body['next_text'] = nxt\n req = urllib.request.Request(\n f'https://api.elevenlabs.io/v1/text-to-speech/{args.voice}?output_format=mp3_44100_128',\n data=json.dumps(body).encode(), headers={'xi-api-key': key, 'Content-Type': 'application/json'})\n try:\n with urllib.request.urlopen(req, timeout=120) as r:\n open(os.path.join(outdir, f'{i:03d}.mp3'), 'wb').write(r.read())\n except urllib.error.HTTPError as e:\n sys.exit(f'{i:03d} で止まりました: {e.code} {e.read().decode(errors=\"ignore\")[:300]}')\n except urllib.error.URLError as e:\n sys.exit(f'{i:03d} で止まりました(ElevenLabs につながらない): {e.reason}')\n used += len(body['text'])\n print(f'{i:03d} {body[\"text\"][:30]}')\nprint(f'できあがり: {outdir} 使った文字数 約{used}字')\n</code></pre><h3 id=\"index_HLGEVABj\">_制作ベース/tts_import.py</h3><p>ElevenLabs の mp3 を、書き出し用の 48kHz の wav にする(44行)。</p><pre><code class=\"language-python\">\"\"\"ElevenLabs などで作った1文ずつの音声を、書き出しで使う形(tts/{番号}.wav・48kHz モノラル と tts/index.json)にする。\n使い方: 動画の作業フォルダで python3 <_制作ベース>/tts_import.py voice_eleven/候補A\n- その文の音声がないときは、いまの tts/ の音声(仮音声)をそのまま使う。声の聞き比べで、一部の文だけ差し替えられる\n- 文の前後の無音は 0.05 秒を残して切る(間は narration.py の「後ろの間」で決めるため)\n\"\"\"\nimport json, os, subprocess, sys, wave\nimport numpy as np\nsys.path.insert(0, '.')\nfrom narration import SEGS\ntry:\n from narration import parse\nexcept ImportError:\n def parse(s):\n return s[0], \"base\", s[1], (s[2] if len(s) > 2 else 0.35)\n\nSR = 48000\nsrc = sys.argv[1]\nos.makedirs('tts', exist_ok=True)\nold = {o['i']: o for o in json.load(open('tts/index.json'))} if os.path.exists('tts/index.json') else {}\nout, replaced = [], 0\nfor i, s in enumerate(SEGS):\n scene, face, text, pause = parse(s)\n mp3 = os.path.join(src, f'{i:03d}.mp3')\n d, note = 0.0, ''\n if text and os.path.exists(mp3):\n raw = subprocess.run(['ffmpeg', '-v', 'error', '-i', mp3, '-ac', '1', '-ar', str(SR), '-f', 's16le', '-'],\n capture_output=True, check=True).stdout\n a = np.frombuffer(raw, dtype=np.int16)\n loud = np.where(np.abs(a) > 300)[0]\n if len(loud):\n pad = int(0.05 * SR)\n a = a[max(0, loud[0] - pad):min(len(a), loud[-1] + pad)]\n with wave.open(f'tts/{i:03d}.wav', 'wb') as w:\n w.setnchannels(1); w.setsampwidth(2); w.setframerate(SR); w.writeframes(a.tobytes())\n d, note = len(a) / SR, f'voice:{os.path.basename(src.rstrip(\"/\"))}'\n replaced += 1\n elif text and i in old and old[i]['text'] == text:\n d, note = old[i]['dur'], old[i].get('note', '')\n elif text:\n sys.exit(f'{i:03d} の音声がありません({mp3} も、同じ文の仮音声もない)。tts2.py で仮音声を作ってから取り込む')\n out.append(dict(i=i, scene=scene, face=face, text=text, dur=d, pause=pause, note=note))\njson.dump(out, open('tts/index.json', 'w'), ensure_ascii=False, indent=0)\ntot = sum(o['dur'] + o['pause'] for o in out)\nprint(f'差し替えた文 {replaced} 全体 {int(tot // 60)}:{int(tot % 60):02d}')\n</code></pre><h3 id=\"index_zP2G4G0g\">_制作ベース/description.py</h3><p>新規。概要欄の下書きの {t:文番号} を時間に置きかえ、目次の条件を確かめる(40行)。</p><pre><code class=\"language-python\">\"\"\"YouTube の概要欄を、書き出しの時間に合わせて仕上げる。\n使い方(動画の作業フォルダで、assemble.py のあと)\n python3 <_制作ベース>/description.py (--draft を付けると、【要記入】が残っていても 概要欄_確認用.txt に書く)\n読むもの: 概要欄_下書き.md(目次の時間を {t:文番号} と書く)と timeline.json(assemble.py が書く)\n書くもの: 概要欄.txt(YouTube にそのまま貼る。{t:9} が、9番の文が始まる時間「0:56」などに置きかわる)\n- YouTube の目次(チャプター)は、最初が 0:00 で、3つ以上・それぞれ10秒以上が条件。満たさないときは止まる\n- 概要欄は 5,000 字まで\n- 「【要記入:…】」が残っていると止まる(連絡先や SNS の URL など、エイイチが決めて書く所)\n\"\"\"\nimport json, re, sys\n\nFPS = 30\ntl = {o['i']: o for o in json.load(open('timeline.json'))}\nsrc = open('概要欄_下書き.md', encoding='utf-8').read()\n# 「<!-- -->」で囲んだメモは貼らない\nsrc = re.sub(r'<!--.*?-->\\n?', '', src, flags=re.S)\n\n\ndef ts(i):\n if i not in tl:\n sys.exit(f'{i}番の文が timeline.json にありません(全体を書き出してから動かす)')\n s = int(tl[i]['start'] / FPS)\n return f'{s // 3600}:{s % 3600 // 60:02d}:{s % 60:02d}' if s >= 3600 else f'{s // 60}:{s % 60:02d}'\n\n\nout = re.sub(r'\\{t:(\\d+)\\}', lambda m: ts(int(m.group(1))), src).strip() + '\\n'\n# 目次の条件を確かめる\ntimes = [t for t in re.findall(r'^(\\d+:\\d{2}(?::\\d{2})?) ', out, flags=re.M)]\nsecs = [sum(int(x) * 60 ** k for k, x in enumerate(reversed(t.split(':')))) for t in times]\nif times:\n if secs[0] != 0 or len(secs) < 3 or any(b - a < 10 for a, b in zip(secs, secs[1:])):\n sys.exit(f'目次が YouTube の条件を満たしていません: {times}')\ndraft = '--draft' in sys.argv # 「【要記入】」が残っていても、確かめ用に 概要欄_確認用.txt を書く\nif '【' in out and '要記入' in out and not draft:\n sys.exit('「【要記入】」が残っています: ' + ', '.join(re.findall(r'【要記入[^】]*】', out)))\nif len(out) > 5000:\n sys.exit(f'概要欄が {len(out)} 字あります(5,000 字まで)')\nname = '概要欄_確認用.txt' if draft else '概要欄.txt'\nopen(name, 'w', encoding='utf-8').write(out)\nprint(f'{name} を書きました({len(out)} 字・目次 {len(times)} 個)')\n</code></pre><h3 id=\"index_3UslsPCD\">_制作ベース/thumbnail.py</h3><p>新規。サムネイルを動画と同じ文字と色で、HTML で組んで画像にする(214行)。</p><pre><code class=\"language-python\">\"\"\"YouTube のサムネイルを、動画と同じ文字・色(scene_kit.py)で作る。文字は画像生成 AI に任せず、ここで組む(文字の間違いが起きない)。\n使い方(動画の作業フォルダで)\n python3 <_制作ベース>/thumbnail.py\n読むもの: thumb.py(文言・背景の写真・案の指定)\n書くもの: thumb/{案の名前}.png(1280×720。YouTube の上限 2MB 以下)と thumb/一覧.png(並べて比べる用・スマホの小さい表示の大きさも添える)\n\nthumb.py の書き方\n PHOTO = 'サムネ素材/素材2_実機のアーム.png' # 背景の写真(1920×1080 以上)\n HAKOBU = True # 左下にハコブくんを置く\n LABEL = '本当に?' # ハコブくんの横の小さな札\n KICKER = '人型ロボット出荷 約4倍' # 上の小さな一言(オレンジ)\n LINES = ['ロボットにも', '<span class=\"red\">ChatGPTの瞬間</span>', 'もう来た?']\n VARIANTS = ['split', 'dark', 'bold'] # 作る案(split 紙色と写真/dark 黒と写真/bold 大きなハコブくんと縁取り文字)\n BOLD_LINES = ['人型ロボット出荷 約4倍', 'ChatGPTの瞬間', 'もう来た?'] # bold 案の3行(なければ KICKER と LINES)\n\"\"\"\nimport base64, os, sys, subprocess\nsys.path.insert(0, '.')\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(1, HERE)\nfrom scene_kit import BG, INK, RED, SUB, LOGO\nfrom PIL import Image\nfrom playwright.sync_api import sync_playwright\n\nns = {}\nexec(open('thumb.py', encoding='utf-8').read(), ns)\nos.makedirs('thumb', exist_ok=True)\n\n\ndef data_uri(path):\n ext = os.path.splitext(path)[1].lower().lstrip('.')\n return f\"data:image/{'jpeg' if ext in ('jpg', 'jpeg') else 'png'};base64,\" + base64.b64encode(open(path, 'rb').read()).decode()\n\n\nphoto = data_uri(ns['PHOTO'])\ndef _hakobu_uri():\n \"\"\"ハコブくんの体(右向き)。2026-10-12 から、作り直した体に顔を描いた hakobu/thumb_body_right.png を使う\n (元の体は頭の上が切り抜けておらず、大きく出すと頭がへこんで見えた)。なければ動画と同じ体を使う\"\"\"\n import io\n new = os.path.join(HERE, 'hakobu', 'thumb_body_right.png')\n if os.path.exists(new):\n return data_uri(new)\n from hakobu import Hakobu, FX0, FY0\n H = Hakobu(mode='full')\n eyes = ('open', 'open')\n b = H.body.copy(); H._ring(b, 0.7)\n img = b.crop(H.crop_draw)\n gl, la = H._face_layers(eyes, None, 'mid', 0.0)\n ox, oy = FX0 - H.crop_draw[0], FY0 - H.crop_draw[1]\n img.alpha_composite(gl, (ox, oy)); img.alpha_composite(la, (ox, oy))\n img = img.crop((H.crop[0] - H.crop_draw[0], H.crop[1] - H.crop_draw[1], H.crop[2] - H.crop_draw[0], H.crop[3] - H.crop_draw[1]))\n img = img.transpose(Image.FLIP_LEFT_RIGHT)\n img = img.crop(img.getchannel('A').point(lambda v: 255 if v > 60 else 0).getbbox())\n img.save('thumb/_hakobu.png')\n buf = io.BytesIO(); img.save(buf, 'PNG')\n return 'data:image/png;base64,' + base64.b64encode(buf.getvalue()).decode()\n\n\nhk = _hakobu_uri() if ns.get('HAKOBU', True) else None\nkicker, lines, label = ns.get('KICKER', ''), ns['LINES'], ns.get('LABEL', '')\npos = ns.get('PHOTO_POS', '20% 50%') # 写真のどこを見せるか(CSS の background-position)\n\nBASE = f\"\"\"*{{margin:0;padding:0;box-sizing:border-box}}\nhtml,body{{width:1920px;height:1080px;overflow:hidden;font-family:'Noto Sans CJK JP',sans-serif;color:{INK}}}\n.red{{color:{RED}}}\n.kicker{{display:inline-block;font-size:56px;font-weight:900;color:#fff;background:{RED};padding:10px 30px;border-radius:12px;letter-spacing:.02em}}\n.main{{font-weight:900;line-height:1.16;letter-spacing:-.02em}}\n.hk{{position:absolute;left:24px;bottom:-90px;height:430px}}\n.label{{position:absolute;left:350px;bottom:250px;background:#fff;color:{INK};font-size:48px;font-weight:900;padding:12px 26px;border-radius:14px;box-shadow:0 6px 24px rgba(0,0,0,.25)}}\n.label:after{{content:'';position:absolute;left:-22px;bottom:22px;border:14px solid transparent;border-right:14px solid #fff}}\n.brand{{position:absolute;right:56px;top:44px;height:40px}}\n\"\"\"\n\n\ndef hakobu_html():\n if not hk:\n return ''\n lab = f'<div class=\"label\">{label}</div>' if label else ''\n return f'<img class=\"hk\" src=\"{hk}\">{lab}'\n\n\ndef html(kind):\n k = f'<div class=\"kicker\">{kicker}</div>' if kicker else ''\n body = '<br>'.join(lines)\n if kind == 'split': # 左に動画と同じ紙色の面と文字、右に写真\n return f\"\"\"<style>{BASE}\nbody{{background:{BG}}}\n.ph{{position:absolute;right:0;top:0;width:820px;height:1080px;background:url({photo}) {pos}/cover}}\n.txt{{position:absolute;left:96px;top:84px;width:1060px}}\n.main{{font-size:136px;margin-top:28px}}\n</style><div class=\"ph\"></div><div class=\"txt\">{k}<div class=\"main\">{body}</div></div>{hakobu_html()}<img class=\"brand\" src=\"{LOGO}\">\"\"\"\n if kind == 'card': # 写真を全面に敷き、動画の名前テロップと同じ薄い紙色の札に文字\n return f\"\"\"<style>{BASE}\nbody{{background:url({photo}) {pos}/cover}}\n.txt{{position:absolute;left:70px;top:70px;background:rgba(245,245,240,.95);border-radius:24px;padding:44px 60px 52px}}\n.main{{font-size:124px;margin-top:24px}}\n</style><div class=\"txt\">{k}<div class=\"main\">{body}</div></div>{hakobu_html()}\"\"\"\n if kind == 'dark': # 案B: 写真を右へずらして敷き、左は黒に溶かして白い文字。ハコブくんは左下で大きめ、右(アーム)を向く\n return f\"\"\"<style>{BASE}\nbody{{background:#111}}\n.ph{{position:absolute;left:720px;top:0;width:1920px;height:1080px;background:url({photo}) 0 50%/cover}}\n.shade{{position:absolute;inset:0;background:linear-gradient(90deg,rgba(13,13,13,1) 0%,rgba(13,13,13,1) 38%,rgba(13,13,13,.6) 48%,rgba(13,13,13,0) 64%)}}\n.txt{{position:absolute;left:96px;top:70px;width:1150px}}\n.main{{font-size:122px;margin-top:22px;color:#fff;text-shadow:0 4px 18px rgba(0,0,0,.6)}}\n.main .red{{color:#E25A2C}}\n.hk2{{position:absolute;left:40px;bottom:-120px;height:540px}}\n.label{{left:380px;bottom:300px}}\n</style><div class=\"ph\"></div><div class=\"shade\"></div><div class=\"txt\">{k}<div class=\"main\">{body}</div></div><img class=\"hk2\" src=\"{hk}\"><div class=\"label\">{label}</div>\"\"\"\n if kind == 'dark2': # 案B2: ハコブくんを左に大きく(画面の高さいっぱい)。文字は右上、写真は右下\n return f\"\"\"<style>{BASE}\nbody{{background:#111}}\n.ph{{position:absolute;left:560px;top:120px;width:1920px;height:1080px;background:url({photo}) 0 50%/cover}}\n.shade{{position:absolute;inset:0;background:linear-gradient(90deg,rgba(13,13,13,1) 0%,rgba(13,13,13,1) 30%,rgba(13,13,13,0) 46%),linear-gradient(180deg,rgba(13,13,13,1) 0%,rgba(13,13,13,.92) 30%,rgba(13,13,13,0) 58%)}}\n.txt{{position:absolute;right:70px;top:60px;text-align:right}}\n.kicker{{font-size:52px}}\n.main{{font-size:128px;margin-top:20px;color:#fff;text-shadow:0 4px 18px rgba(0,0,0,.7)}}\n.main .red{{color:#E25A2C}}\n.hk2{{position:absolute;left:30px;bottom:-110px;height:900px}}\n.label{{left:90px;top:40px;bottom:auto}}\n.label:after{{left:60px;bottom:-26px;border:14px solid transparent;border-top:14px solid #fff}}\n</style><div class=\"ph\"></div><div class=\"shade\"></div><img class=\"hk2\" src=\"{hk}\"><div class=\"label\">{label}</div><div class=\"txt\">{k}<div class=\"main\">{body}</div></div>\"\"\"\n if kind == 'dark3': # 案B3: 上に横いっぱいの文字(黒地)、下に写真。ハコブくんは左下で大きく、アームを見る\n L = ns.get('BOLD_LINES', [kicker] + lines)\n return f\"\"\"<style>{BASE}\nbody{{background:#111}}\n.ph{{position:absolute;left:470px;top:380px;width:1620px;height:911px;background:url({photo}) 0 40%/cover}}\n.shade{{position:absolute;left:0;right:0;top:380px;height:220px;background:linear-gradient(180deg,#111 0%,rgba(17,17,17,0) 100%)}}\n.shadeL{{position:absolute;left:470px;top:380px;width:260px;bottom:0;background:linear-gradient(90deg,#111 0%,rgba(17,17,17,0) 100%)}}\n.top{{position:absolute;left:0;right:0;top:46px;text-align:center;color:#fff;font-weight:900;line-height:1.12}}\n.top .a{{font-size:78px}} .top .b{{font-size:150px;letter-spacing:-.02em}} .top .b .red{{color:#E25A2C}}\n.hk2{{position:absolute;left:40px;bottom:-90px;height:700px}}\n.label{{left:360px;bottom:520px}}\n</style><div class=\"ph\"></div><div class=\"shade\"></div><div class=\"shadeL\"></div>\n<div class=\"top\"><div class=\"a\"><span style=\"background:{RED};padding:4px 26px;border-radius:12px;font-size:60px\">{L[0]}</span></div>\n<div class=\"b\"><span class=\"red\">{L[1]}</span> {L[2]}</div></div><img class=\"hk2\" src=\"{hk}\"><div class=\"label\">{label}</div>\"\"\"\n if kind in ('photo', 'photo2', 'photo3'):\n # 2026-10-12 エイイチの指示: 背景は全部写真。文字は大きく、写真の上に置くので黒い縁を付ける(上の札は縁なし)\n # photo : 文字は左上、ハコブくんは右下で左(アーム)を向く\n # photo2: 文字は右上、ハコブくんは右下で左を向く\n # photo3: 文字は右上、ハコブくんは左下の隅で右(アーム)を向く。写真は PHOTO3(ハコブくんがアームと箱に重ならないコマ)\n L = [kicker] + lines\n right = kind != 'photo'\n left_hk = kind == 'photo3'\n ph = data_uri(ns['PHOTO3']) if left_hk and ns.get('PHOTO3') else photo\n OUT = f\"-webkit-text-stroke:24px {INK};paint-order:stroke fill;text-shadow:0 6px 18px rgba(0,0,0,.55)\"\n if left_hk:\n HK = f\".hk3{{{{position:absolute;left:10px;bottom:-190px;height:{ns.get('HK_H', 600)}px}}}}\"\n LB = f\"\"\".label{{{{left:{ns.get('HK_H', 600) * 0.62 + 10:.0f}px;bottom:300px;border:6px solid {INK};font-size:54px}}}}\n.label:after{{{{left:-30px;bottom:24px;border:16px solid transparent;border-right:16px solid {INK}}}}}\"\"\"\n else:\n HK = f\".hk3{{{{position:absolute;right:{'30' if right else '60'}px;bottom:-170px;height:{'520' if right else '640'}px;transform:scaleX(-1)}}}}\"\n LB = f\"\"\".label{{{{left:auto;right:{'330' if right else '420'}px;bottom:{'190' if right else '360'}px;border:6px solid {INK};font-size:54px}}}}\n.label:after{{{{left:auto;right:-30px;bottom:24px;border:16px solid transparent;border-left:16px solid {INK}}}}}\"\"\"\n HK, LB = HK.replace('{{', '{').replace('}}', '}'), LB.replace('{{', '{').replace('}}', '}')\n return f\"\"\"<style>{BASE}\nbody{{background:url({ph}) {ns.get('PHOTO_POS_FULL', '50% 50%')}/cover}}\n.dim{{position:absolute;inset:0;background:linear-gradient({'200deg' if right else '160deg'},rgba(0,0,0,.45) 0%,rgba(0,0,0,.15) 45%,rgba(0,0,0,0) 70%)}}\n.txt{{position:absolute;{'right:56px;text-align:right' if right else 'left:60px'};top:40px;font-weight:900;color:#fff;line-height:1.1;letter-spacing:-.02em}}\n.kick{{display:inline-block;font-size:66px;background:{RED};color:#fff;padding:8px 30px 12px;border-radius:14px;letter-spacing:.02em;box-shadow:0 6px 18px rgba(0,0,0,.35)}}\n.l1{{font-size:150px;margin-top:22px;{OUT}}}\n.l2{{font-size:{'200' if right else '212'}px;{OUT}}}\n.l2 .red{{color:#F0602C}}\n.l3{{font-size:{'170' if right else '176'}px;{OUT}}}\n{HK}\n{LB}\n</style><div class=\"dim\"></div>\n<div class=\"txt\"><div class=\"kick\">{L[0]}</div><div class=\"l1\">{L[1]}</div><div class=\"l2\">{L[2]}</div><div class=\"l3\">{L[3]}</div></div>\n<img class=\"hk3\" src=\"{hk}\"><div class=\"label\">{label}</div>\"\"\"\n if kind == 'bold': # ChatGPT 案の構図(左に大きなハコブくんと吹き出し、右に縁取りの太い文字)を、本物の写真と動画の文字で作る\n L = ns.get('BOLD_LINES', [kicker] + lines)\n rows = ''.join(f'<div class=\"r{i}\">{t}</div>' for i, t in enumerate(L))\n return f\"\"\"<style>{BASE}\nbody{{background:#151515}}\n.ph{{position:absolute;left:700px;top:-30px;width:2040px;height:1148px;background:url({photo}) 0 50%/cover}}\n.fade{{position:absolute;inset:0;background:linear-gradient(90deg,#151515 0%,#151515 36%,rgba(21,21,21,0) 50%),linear-gradient(180deg,rgba(0,0,0,.35) 0%,rgba(0,0,0,0) 55%)}}\n.txt{{position:absolute;right:50px;top:70px;text-align:right;font-weight:900;line-height:1.08;letter-spacing:-.01em;color:#fff;\n -webkit-text-stroke:26px {INK};paint-order:stroke fill}}\n.r0{{font-size:92px}} .r1{{font-size:156px;color:{RED};margin-top:14px}} .r2{{font-size:190px;margin-top:6px}}\n.r1 .red{{color:{RED}}}\n.big{{position:absolute;left:30px;bottom:-50px;height:760px}}\n.bubble{{position:absolute;left:110px;top:70px;background:#fff;border:8px solid {INK};border-radius:22px;padding:14px 34px;font-size:76px;font-weight:900;color:{INK}}}\n.bubble:after{{content:'';position:absolute;left:150px;bottom:-44px;border:22px solid transparent;border-top:22px solid {INK}}}\n</style><div class=\"ph\"></div><div class=\"fade\"></div><img class=\"big\" src=\"{hk}\"><div class=\"bubble\">{label}</div><div class=\"txt\">{rows}</div>\"\"\"\n raise SystemExit(f'案の名前が分かりません: {kind}')\n\n\nouts = []\nwith sync_playwright() as p:\n b = p.chromium.launch()\n pg = b.new_page(viewport={'width': 1920, 'height': 1080})\n for kind in ns.get('VARIANTS', ['split', 'card', 'dark']):\n pg.set_content('<!doctype html><html><head><meta charset=\"utf-8\"></head><body>' + html(kind) + '</body></html>')\n pg.wait_for_timeout(300)\n big = f'thumb/_{kind}_1920.png'\n pg.screenshot(path=big)\n im = Image.open(big).convert('RGB').resize((1280, 720), Image.LANCZOS)\n out = f'thumb/{kind}.png'\n im.save(out, optimize=True)\n if os.path.getsize(out) > 2_000_000:\n out = f'thumb/{kind}.jpg'; im.save(out, quality=92)\n os.remove(big)\n outs.append((kind, out))\n b.close()\n\n# 一覧: 原寸の半分と、スマホの一覧で出る大きさ(幅 168px 前後)を並べる\nW = 640 + 24 + 168\nsheet = Image.new('RGB', (W, len(outs) * (360 + 24)), (30, 30, 30))\nfor k, (kind, out) in enumerate(outs):\n im = Image.open(out)\n y = k * (360 + 24)\n sheet.paste(im.resize((640, 360), Image.LANCZOS), (0, y))\n sheet.paste(im.resize((168, 94), Image.LANCZOS), (640 + 24, y))\nsheet.save('thumb/一覧.png')\nfor kind, out in outs:\n print(kind, out, f'{os.path.getsize(out) / 1e6:.2f}MB')\n</code></pre><h3 id=\"index_TxNsgURJ\">_制作ベース/hakobu/make_new_face.py</h3><p>新規。作り直した体の画面に、動画と同じ描き方の顔を写す(64行)。</p><pre><code class=\"language-python\">\"\"\"新しい体(cut_new.png)のディスプレイに、動画と同じ顔を描く。サムネイル用(2026-10-12)\n2026-10-12 エイイチの指摘「顔が変(口と目の配置や大きさ)」で作り直し:\n 自前で描いた目と口をやめ、動画の hakobu.py と同じ描き方(eye_mask / mouth_mask)で描いた顔を、\n 動画のディスプレイ(SCREEN_QUAD を長方形にならしたもの)から新しい体のディスプレイ(screen_quad_new.npy)へ写す。\n 画面の大きさに対する目と口の大きさ・位置は、動画のハコブくんとそのまま同じになる\n使い方: python3 make_new_face.py [目 口] 例: python3 make_new_face.py open mid(既定)\n\"\"\"\nimport os, sys\nimport numpy as np, cv2\nfrom PIL import Image\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, os.path.dirname(HERE))\nimport hakobu as hk\n\n\ndef face_on_new_body(eyes=('open', 'open'), mouth='mid', out='body_new_face.png'):\n im = np.array(Image.open(os.path.join(HERE, 'cut_new.png'))).astype(np.float32)\n q = np.load(os.path.join(HERE, 'screen_quad_new.npy')).astype(np.float32) # 左上・右上・右下・左下\n SS, FX0, FY0, FX1, FY1 = hk.SS, hk.FX0, hk.FY0, hk.FX1, hk.FY1\n hh, ww = (FY1 - FY0) * SS, (FX1 - FX0) * SS\n # 1. 動画と同じ描き方で、顔を画面での向きに描く\n white = np.zeros((hh, ww), np.uint8)\n for kind, box, side in ((eyes[0], hk.V_EYE_LEFT, -1), (eyes[1], hk.V_EYE_RIGHT, 1)):\n em = np.zeros_like(white)\n hk.eye_mask(em, kind, box, side, 0.0)\n white = np.maximum(white, em)\n blue = hk.mouth_mask(mouth)\n la = np.zeros((hh, ww, 4), np.float32)\n la[white > 0, :3] = hk.WHITE\n la[..., 3] = white\n la[blue > 0, :3] = hk.BLUE\n la[..., 3] = np.maximum(la[..., 3], blue)\n la = np.ascontiguousarray(la[:, ::-1]) # 動画と同じく反転して、体の元の向きに戻す\n # 2. 動画のディスプレイをならした長方形(hakobu.py の flat)→ 新しい体のディスプレイ\n S = hk.SCREEN_QUAD\n c = S.mean(0)\n w_ = (np.linalg.norm(S[1] - S[0]) + np.linalg.norm(S[2] - S[3])) / 2\n h_ = (np.linalg.norm(S[3] - S[0]) + np.linalg.norm(S[2] - S[1])) / 2\n flat = np.float32([[c[0] - w_ / 2, c[1] - h_ / 2], [c[0] + w_ / 2, c[1] - h_ / 2],\n [c[0] + w_ / 2, c[1] + h_ / 2], [c[0] - w_ / 2, c[1] + h_ / 2]])\n src = (flat - np.float32([FX0, FY0])) * SS\n M = cv2.getPerspectiveTransform(src, q)\n h, w = im.shape[:2]\n a = la[..., 3:4] / 255\n pre = np.concatenate([la[..., :3] * a, a * 255], 2)\n wl = cv2.warpPerspective(pre, M, (w, h), flags=cv2.INTER_AREA)\n aa = wl[..., 3:4] / 255\n rgb = np.where(aa > 1e-4, wl[..., :3] / np.maximum(aa, 1e-4), 0)\n # 3. 目のまわりのにじむ光(動画と同じ色。体の大きさに合わせてぼかしを広げる)\n scale = np.linalg.norm(q[1] - q[0]) / w_\n eye_a = aa[..., 0] * (rgb[..., 0] > 200)\n glow = cv2.GaussianBlur(eye_a, (0, 0), 2.6 * scale) * 0.75\n for col, al in ((np.float32([205, 225, 255]), glow[..., None]), (rgb, aa)):\n im[..., :3] = im[..., :3] * (1 - al) + col * al\n Image.fromarray(im.clip(0, 255).astype(np.uint8)).save(os.path.join(HERE, out))\n return out\n\n\nif __name__ == '__main__':\n args = sys.argv[1:]\n if len(args) == 2:\n print(face_on_new_body((args[0], args[0]), args[1]))\n else:\n print(face_on_new_body())\n</code></pre><h3 id=\"index_zd9AxR5a\">フィジカルAIの現在地/narration.py</h3><p>台本。1文ごとに(場面、表情、字幕、後ろの間)。読み替えの表 READ と ELEVEN_READ もここ(186行)。</p><pre><code class=\"language-python\"># -*- coding: utf-8 -*-\n# 台本「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」ハコブくん版\n# もとは DAY330 の試作(codex の敵対的検証を反映した版)。2026-10-12 に、ハコブくんが話す形(エイイチは三人称)に直した。\n# 変えたのは、一人称→三人称、自己紹介(確定した文言)、見立ての入り方、NEO の確認日(10月12日)、アウトロ、1文ごとの表情。事実と数字は変えていない\n# 各要素: (場面の名前, 表情, 字幕の文, 後ろの間[秒]) 間を省略すると 0.35 秒\n# 字幕の文の中の「|」: 読み上げは1文のまま、字幕だけをそこで次の枚に替える\nFACES = (\"base\", \"smile\", \"surprise\", \"doubt\", \"think\", \"trouble\", \"joy\", \"wink\")\n\nSEGS = [\n ('hook_ship', 'base', '調査会社Counterpointの推計では、今年の上半期に出荷された人型ロボットは、'),\n ('hook_ship', 'base', '世界で2万2000台を超えました。前の年の同じ時期の、およそ4倍です。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('hook_forecast', 'surprise', '年末までには、5万台を超えると予想されています。'),\n ('hook_forecast', 'base', '家事をする人型ロボットの予約販売も、去年の秋から始まっています。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('hook_question', 'base', 'こういうニュースを見ると、ChatGPTが出たときみたいに、'),\n ('hook_question', 'doubt', 'ロボットも一気に賢くなって、家に来るのはもうすぐ、って感じませんか。', 1.8), # 大きな流れの変わり目の間\n ('title', 'smile', 'Pixels to Motors。案内役のハコブくんです。'),\n ('title', 'base', 'この番組では、フィジカルAIを、現場で使える形にするまでを解説します。'),\n ('title_eiichi', 'base', 'エイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。|その中で分かったことと、そこから考えた見立てを、ハコブくんがお伝えします。', 1.8), # 大きな流れの変わり目の間\n ('question', 'surprise', '今日の問いは、ロボットにもChatGPTのような瞬間は、もう来たのか、です。'),\n ('question', 'base', 'ここでいうChatGPTの瞬間は、専門家じゃなくても、'),\n ('question', 'base', 'いろんな仕事にすぐ使えるようになる転換点、という意味です。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('define', 'base', 'フィジカルAIっていうのは、現実の世界を認識して、行動するAIのことです。'),\n ('define', 'base', '自動運転もここに入りますが、'),\n ('define', 'base', '今日は主に、アームや人型ロボットが物を扱う話に絞ります。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('answer', 'base', '先に結論を言っちゃうと、写真を見せて判断させる入口は、ぐっと簡単になりました。'),\n ('answer', 'trouble', 'でも、物を確実に動かすところは、まだ手間がかかるんですよ。'),\n ('answer', 'base', 'その手間は大きく2つあります。'),\n ('answer', 'base', '動作と結びついたデータが集めにくいこと。そして、機体や環境とのすり合わせです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('stance', 'base', '先に言っておくと、フィジカルAIはまだ使えない、と言いたいわけじゃないんです。'),\n ('stance', 'base', 'どこまで来ていて、どこで手間がかかっているのかを、分けて見たいんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('toc', 'base', '大きく3つあります。'),\n ('toc', 'base', '出荷が4倍になったロボットは、何をしているのか。'),\n ('toc', 'base', 'エイイチの机の上のロボットアームで、何が簡単で、何が難しかったのか。'),\n ('toc', 'base', 'そして、データの壁はどれくらい高くて、誰がどう越えようとしているのか。'),\n ('toc', 'base', '最後に、エイイチの見立てを話します。', 1.8), # 大きな流れの変わり目の間\n ('c1', 'base', '1つ目、出荷が4倍になったロボットは、何をしているのか、です。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('use', 'base', 'Counterpointの推計では、出荷された人型ロボットの6割以上が、'),\n ('use', 'base', '展示やイベントで動きを見せる用途と、研究やデータ生産の用途なんですよ。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('datacollect', 'base', 'データ生産っていうのは、ロボットが学ぶための動きの記録を作ることです。'),\n ('datacollect', 'base', '人が遠隔で動かして記録するやり方も、自律で動いた記録を残すやり方もあります。'),\n ('datacollect', 'doubt', 'ただ、研究向けの台数が全部データ集めに使われる、という意味ではありません。'),\n ('datacollect', 'base', 'それでも、用途の分類に「データ生産」という項目があること自体が、'),\n ('datacollect', 'base', '今の段階をよく表していると思います。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('conflict', 'base', '一方で、別の調査会社のSmart Analytics Globalは、'),\n ('conflict', 'base', '同じ半年の出荷を約1万9100台と推計していて、'),\n ('conflict', 'base', '産業・商業用途が7割を超えた、としています。'),\n ('conflict', 'base', '総数も分類も違うので、この2つは、単純な裏表としては比べられません。'),\n ('conflict', 'doubt', 'それに、出荷台数は、そのまま自律的に働いている台数でもないんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('china', 'surprise', 'もう1つ、SAGの推計では、出荷の97%以上が中国のメーカーです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('neo', 'base', '家のほうも見てみます。'),\n ('neo', 'base', 'アメリカの1Xという会社は、家事をする人型ロボット、NEOの予約を、去年の10月に始めました。'),\n ('neo', 'base', '2万ドルの早期購入枠は、今年配送の予定で、月499ドルのプランは、その後です。'),\n ('neo', 'base', '最初は基本的な自律機能だけで、知らない作業は、'),\n ('neo', 'base', '予約した時間に、1Xの担当者が遠隔で手伝う仕組みなんですよ。'),\n ('neo', 'base', '10月12日の時点で、一般のお客さんへの配送開始を示す公式発表は、'),\n ('neo', 'base', 'エイイチが確認した範囲では、見つかっていません。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('neo_point', 'surprise', 'これ、ちょっと考えさせられませんか。'),\n ('neo_point', 'think', '売っているのはロボットだけじゃなくて、遠隔で手伝う人と、'),\n ('neo_point', 'think', 'そこで動きを覚えていく仕組みごとなんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('bridge1', 'base', '工場でも家でも、使いながらデータを集める動きが出てきているんですよ。'),\n ('bridge1', 'doubt', 'じゃあ、物を動かすのは、何がそんなに大変なのか。'),\n ('bridge1', 'base', 'まず、エイイチの机の上の話をします。', 1.8), # 大きな流れの変わり目の間\n ('c2', 'base', '2つ目、エイイチの机の上のロボットアームで、何が簡単で、何が難しかったのか、です。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('v_real', 'base', 'ここは、エイイチ自身の話から始めます。'),\n ('v_real', 'base', 'エイイチは自宅の机で、mechArm 270という小さなロボットアームを動かしています。'),\n ('v_real', 'base', '赤いブロックを掴んで、箱に置く。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('trials', 'base', 'それだけの作業を、失敗も含めて650回以上試して、全部台帳に残しています。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('easy', 'smile', 'まず、簡単だったほうから話します。掴めたかどうかの判定です。'),\n ('easy', 'smile', 'iPhoneで撮った写真をAIに見せて、「ブロックを掴めていますか」と聞くと、'),\n ('easy', 'joy', 'はい、いいえで答えが返ってくる。この入口は、驚くほど簡単でした。'),\n ('easy', 'doubt', 'ただ、答えが返ることと、毎回正しいことは別です。'),\n ('easy', 'smile', 'どこまで任せられるかは、失敗例も含めて確かめる必要があります。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('hard', 'trouble', '難しかったのは、動かすほうでした。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gap', 'base', 'ロボットアームの爪先がどこに来るかは、普通は関節の角度から計算で出します。'),\n ('gap', 'base', 'ところが、ikpyという計算ツールで出した爪先の位置と、', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('v_twin', 'base', 'パソコンの中に作ったアームの分身、デジタルツインで動かした位置が、', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gap', 'surprise', 'エイイチの設定では、同じ関節の角度なのに、30cm以上食い違ったんです。'),\n ('gap', 'doubt', 'これは、アームが30cm外れた、という意味ではありません。'),\n ('gap', 'base', '計算ツールには、グリッパーの形も、デジタルツインの物理も入っていなくて、爪先の位置の決め方そのものが違っていたんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('v_scan', 'base', 'そこでエイイチは、分身の中でアームを動かして、'),\n ('v_scan', 'base', '関節の角度と爪先の位置の対応表を作りました。'),\n ('v_scan', 'base', 'これは計算の表で、実機で全部の点を測ったわけではありません。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('silent', 'trouble', 'もう1つ困ったのが、黙る失敗です。'),\n ('silent', 'trouble', 'エイイチの機体とソフトの組み合わせでは、肩の関節に、90度ちょっとを超える値を送ると、'),\n ('silent', 'trouble', 'エラーが出ないのに、アームが動かないことがありました。'),\n ('silent', 'trouble', 'ログを1行ずつ見て、やっと気づきました。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('point2', 'surprise', 'でも、ポイントはここなんです。'),\n ('point2', 'think', 'この2つは、AIの学習データが足りない、という話とは別です。'),\n ('point2', 'think', 'モデルや制御が、エイイチの機体と噛み合っているかを確かめる、すり合わせの仕事でした。'),\n ('point2', 'think', 'ほかの環境で学んだことを持ってくる研究も進んでいます。'),\n ('point2', 'think', 'それでも、エイイチの構成でちゃんと動くかは、ここで確かめるしかなかったんです。', 0.6),\n ('point2', 'think', 'じゃあ、もう1つの手間、データのほうは、業界全体でどうなっているのか。', 1.8), # 大きな流れの変わり目の間\n ('c3', 'base', '3つ目、データの壁はどれくらい高くて、誰がどう越えようとしているのか、です。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('years', 'base', 'カリフォルニア大学バークレー校のロボット研究者、'),\n ('years', 'base', 'ケン・ゴールドバーグさんが、去年、こんな概算を紹介しています。'),\n ('years', 'base', '画像と言葉を扱うAIの学習資料を、'),\n ('years', 'base', '人が読んだり見たりする時間に換算すると、約10万年規模になる。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('wall', 'trouble', 'それに比べて、ロボットが手を動かすための、動作と結びついた経験は、集めにくい。'),\n ('wall', 'trouble', 'ゴールドバーグさんは、これを「10万年のデータの壁」と呼んでいます。'),\n ('wall', 'trouble', 'ただ、ロボットにも同じ量が必要だと分かったわけではありません。'),\n ('wall', 'trouble', '集めにくい、という問題提起なんです。', 0.6),\n ('wall', 'doubt', 'ここで、じゃあロボットにChatGPTの瞬間は当分来ない、'),\n ('wall', 'smile', 'と言いたくなるじゃないですか。でも、ちょっと待ってほしいんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gen', 'base', 'アメリカのGeneralistという会社は、人が手で作業する様子を、'),\n ('gen', 'base', '安い装着型の機材で記録して集めています。'),\n ('gen', 'surprise', '会社の発表では、去年の11月に27万時間、今年の4月に50万時間を超えました。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gen2', 'base', 'さらに8月には、簡単で短い作業なら、'),\n ('gen2', 'base', '数秒の実演を見せるだけで、新しい作業をこなす例も発表しています。'),\n ('gen2', 'doubt', 'ただ、本人たちも、作業は簡単で短く、成功率はまだ控えめだと書いています。'),\n ('gen2', 'base', 'それでも、「動かすには毎回大量の専用データが必要」とは、言い切れなくなってきました。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('compare', 'think', 'ここで1つ、注意があります。'),\n ('compare', 'base', '作業を記録した時間と、10万年という数字は、そのまま比べられません。'),\n ('compare', 'base', '何倍足りないかより、いろんな場面の経験を、どれだけ広く集められるかが問題なんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('dirt', 'think', 'つまり、壁はまだ高い。でも、壁の前でみんなが土を積み始めている。'),\n ('dirt', 'think', '1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、'),\n ('dirt', 'think', 'Generalistが人の手作業を記録しているのも、同じ壁に向かっている動きなんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('notall', 'smile', 'ただ、ここで「なんだ、まだ全然ダメなのか」で'),\n ('notall', 'smile', '片付けてしまうのも違うんです。'),\n ('notall', 'smile', '写真を見せて判断させる入口は、エイイチの机の上でも、もう普通に使えています。', 0.6),\n ('notall', 'doubt', 'じゃあ、この2つが混ざっている今、ロボットのニュースの何を見ればいいのか。', 1.8), # 大きな流れの変わり目の間\n ('c4', 'base', 'ここからは、エイイチの見立てです。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('q', 'surprise', 'ロボットのニュースを見たときの問いが、2つに変わるんですよ。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('q1', 'base', '1つは、そのロボットは自律で動いているのか、人が遠隔で動かしているのか、です。'),\n ('q1', 'base', 'デモ動画を見るときは、自律か遠隔操作か、'),\n ('q1', 'base', '再生速度や、人が手を出した条件が書かれているかを見ます。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('q2', 'base', 'もう1つは、その仕事は「見て判断する」で済むのか、'),\n ('q2', 'base', '「触れて動かす」精度が要るのか、です。'),\n ('q2', 'base', '棚の商品が足りているかを見るのは、前者です。'),\n ('q2', 'base', '柔らかい物を潰さずに掴むのは、後者です。'),\n ('q2', 'base', '前者は、手軽に試せるところまで来ています。ただ、どこまで任せられるかは、現場で確かめる必要がある。'),\n ('q2', 'base', '後者は、その現場の機体と環境に合わせて、すり合わせる仕事が、まだ残っています。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gofe', 'base', 'ゴールドバーグさんは、昔ながらのエンジニアリングと学習を組み合わせて、'),\n ('gofe', 'base', '現場で役に立つ仕事をしながら、データを集めていくのが近道だ、と言っています。'),\n ('gofe', 'base', 'エイイチの机の上も、AIと従来の制御を組み合わせる点では通じています。'),\n ('gofe', 'base', 'ただ、現場で働きながらデータが増える仕組みとは、まだ規模も段階も違います。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('motion', 'base', '作る側から見ると、もう1つあります。'),\n ('motion', 'base', 'データの話と並行して、もう1つ要るのが「どう動くか」の設計です。'),\n ('motion', 'base', '例えばロボットが人に物を渡すとき、'),\n ('motion', 'base', '何秒待って手を開けば、人は安心して受け取れるのか。'),\n ('motion', 'base', 'これは、成功か失敗かの判定だけでは出てこない。'),\n ('motion', 'base', '人が安心できる動きを設計する仕事も、同時に必要だと、エイイチは見ています。', 1.8), # 大きな流れの変わり目の間\n ('answer2', 'base', 'じゃあ、冒頭の問いに答えますね。', 0.4),\n ('answer2', 'base', 'エイイチの机の上では、写真を見せて判断させる入口は、ずっと簡単になりました。'),\n ('answer2', 'base', '一方、物を確実に動かすには、機体や環境とのすり合わせが、まだ必要でした。'),\n ('answer2', 'base', 'その負担を減らす研究は、急速に進んでいます。'),\n ('answer2', 'trouble', 'でも、いろんな家で、安全に家事を任せられる転換点まで来たかは、まだ判断できません。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('action', 'base', 'ロボットのデモ動画を見たら、'),\n ('action', 'base', '「これは自律なのか、何回やって、何回うまくいったのか」を、'),\n ('action', 'base', '一度だけ考えてみてください。'),\n ('action', 'smile', 'それだけで、同じ動画の見え方が、かなり変わると思います。', 1.8), # 大きな流れの変わり目の間\n ('outro', 'smile', '今日はここまでです。'),\n ('outro', 'base', 'この動画が気に入ったら、高評価とチャンネル登録をお願いします。'),\n ('outro', 'base', 'ロボットの開発や導入の相談は、概要欄の連絡先から、エイイチに直接どうぞ。'),\n ('outro', 'wink', 'コメントは、エイイチが全部読んでいます。それではまた。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('sources', 'smile', '', 6.0),\n]\n\n# 仮音声(Open JTalk)用の読み(字幕は元の表記のまま)\nREAD = [('Pixels to Motors', 'ピクセルズ トゥー モーターズ'), ('Smart Analytics Global', 'スマート アナリティクス グローバル'), ('Counterpoint', 'カウンターポイント'), ('ChatGPT', 'チャットジーピーティー'), ('Generalist', 'ジェネラリスト'), ('mechArm 270', 'メックアーム ニーナナゼロ'), ('iPhone', 'アイフォーン'), ('ikpy', 'アイケーパイ'), ('NEO', 'ネオ'), ('1X', 'ワンエックス'), ('SAG', 'エスエージー'), ('AI', 'エーアイ'), ('30cm', 'さんじゅっセンチ'), ('爪先', 'つめさき'), ('1万9100台', 'いちまんきゅうせんひゃくだい'), ('値を送る', 'あたいを送る'), ('フィジカルAI', 'フィジカルエーアイ'), ('「', ''), ('」', ''), ('|', '')]\n\n# ElevenLabs の声で読み間違えた語だけ、読みを直す(tts_eleven.py が使う。字幕は変えない)\nELEVEN_READ = [\n ('エイイチ', 'えいいち'), # 2026-10-12 カタカナだと文ごとに抑揚が変わった。ひらがなで語尾が下がる読みにそろう(エイイチが4通りを聞き比べて決めた)\n ('裏表', 'うらおもて'), # 2026-10-12 37番で「うらおもてうら」のように読まれた(エイイチが見つけた)\n]\n\n# 仮音声の間延び対策:ひと息が長い文だけ、読み上げ用に読点を足す(字幕は変えない)\nOVERRIDE = {'同じ半年の出荷を約1万9100台と推計していて、': '同じ半年の出荷を、約1万9100台と、推計していて、', '工場でも家でも、使いながらデータを集める動きが出てきているんですよ。': '工場でも家でも、使いながら、データを集める動きが、出てきているんですよ。', 'ほかの環境で学んだことを持ってくる研究も進んでいます。': 'ほかの環境で学んだことを、持ってくる研究も、進んでいます。', 'ここで、じゃあロボットにChatGPTの瞬間は当分来ない、': 'ここで、じゃあ、ロボットに、ChatGPTの瞬間は、当分来ない、', '1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、': '1Xが、家の中で、遠隔で手伝いながら、動きを覚えさせているのも、'}\n\n\ndef reading(t):\n t = OVERRIDE.get(t, t)\n for a, b in READ:\n t = t.replace(a, b)\n return t\n\n\ndef parse(seg):\n \"\"\"1行を (場面, 表情, 字幕, 間) にそろえる\"\"\"\n if len(seg) >= 3 and seg[1] in FACES:\n scene, face, text = seg[0], seg[1], seg[2]\n pause = seg[3] if len(seg) > 3 else 0.35\n else:\n scene, face, text = seg[0], \"base\", seg[1]\n pause = seg[2] if len(seg) > 2 else 0.35\n return scene, face, text, pause\n</code></pre><h3 id=\"index__EpedxB3\">フィジカルAIの現在地/scenes.py</h3><p>場面ごとの図と、実機の映像の指定(VIDEO)(363行)。</p><pre><code class=\"language-python\"># -*- coding: utf-8 -*-\n# 動画「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」ハコブくん版の場面。共通部品は _制作ベース/scene_kit.py\n# 2026-10-12: DAY330 の試作の場面を、ハコブくんの区画(左下)と三人称の台本に合わせて直した\nimport os as _os, sys as _sys\n_here = _os.path.dirname(_os.path.abspath(__file__))\nfor _c in (_os.environ.get(\"P2M_BASE\", \"\"), _os.path.join(_here, \"..\", \"base\"), _os.path.join(_here, \"..\", \"_制作ベース\")):\n if _c and _os.path.exists(_os.path.join(_c, \"scene_kit.py\")):\n _sys.path.insert(0, _c); break\nfrom scene_kit import *\n\nT1 = \"<b>1</b>出荷されたロボットは何をしているのか\"\nT2 = \"<b>2</b>机の上のロボットアームで、簡単だったこと・難しかったこと\"\nT3 = \"<b>3</b>データの壁の高さと、越えようとしている会社\"\nT4 = \"<b>4</b>エイイチの見立て\"\n\nS = {}\n\nS[\"hook_ship\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期 人型ロボットの世界出荷(推計)</div>\n <div class=\"hero\">22,000<small>台超</small></div>\n <div class=\"h2\" style=\"margin-top:36px\">前年同期の <span class=\"red\">約4倍</span></div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)</div>\"\"\")\n\nS[\"hook_forecast\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年通年の予想</div>\n <div class=\"hero\">50,000<small>台超</small></div>\n <div class=\"h3\" style=\"margin-top:44px\">家事をする人型ロボットの予約販売も、2025年秋から</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)/1X</div>\"\"\")\n\nS[\"hook_question\"] = page(\"\"\"\n<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div class=\"h1\">ロボットも、ChatGPTのときみたいに<br><span class=\"red\">一気に賢くなる?</span></div>\n</div>\"\"\")\n\nS[\"question\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">今日の問い</div>\n <div class=\"h1\">ロボットにも<span class=\"red\">ChatGPTの瞬間</span>は<br>もう来たのか</div>\n <div class=\"h3 muted\" style=\"margin-top:44px\">ChatGPTの瞬間=専門家でなくても、<br>いろんな仕事にすぐ使えるようになる転換点</div>\n</div>\"\"\")\n\nS[\"define\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">フィジカルAI とは</div>\n <div class=\"h2\">現実の世界を認識して、<span class=\"red\">行動する</span>AI</div>\n <div class=\"row\" style=\"margin-top:56px;gap:30px\">\n <div class=\"box\" style=\"border:4px solid #0D0D0D\">ロボットアーム</div>\n <div class=\"box\" style=\"border:4px solid #0D0D0D\">人型ロボット</div>\n <div class=\"box muted\" style=\"background:transparent;border:2px dashed #D6D6D1\">自動運転<br><span style=\"font-size:30px\">今日は扱わない</span></div>\n </div>\n</div>\"\"\")\n\nS[\"answer\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">先に結論</div>\n {two(\"写真を見せて判断させる入口\", \"ぐっと簡単になった\", \"物を確実に動かすところ\", \"まだ手間がかかる\")}\n <div class=\"h3\" style=\"margin-top:56px\">手間は2つ<br>① <span class=\"red\">動作と結びついたデータ</span>が集めにくい<br>② <span class=\"red\">機体や環境とのすり合わせ</span></div>\n</div>\"\"\")\n\nS[\"stance\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2 muted\" style=\"text-decoration:line-through;text-decoration-thickness:6px\">フィジカルAIはまだ使えない</div>\n <div class=\"h1\" style=\"margin-top:48px\">どこまで来ていて、<br>どこで<span class=\"red\">手間がかかっているのか</span>を、<br>分けて見る</div>\n</div>\"\"\")\n\nS[\"toc\"] = page(\"\"\"\n<div class=\"wrap\">\n <ul class=\"list\">\n <li><span class=\"n\">1</span>出荷が4倍になったロボットは、何をしているのか</li>\n <li><span class=\"n\">2</span>机の上のロボットアームで、何が簡単で何が難しかったか</li>\n <li><span class=\"n\">3</span>データの壁はどれくらい高く、誰がどう越えようとしているか</li>\n <li class=\"dim\"><span class=\"n\">+</span>エイイチの見立て</li>\n </ul>\n</div>\"\"\")\n\nS[\"c1\"] = chapter(\"1\", \"出荷が4倍になったロボットは、<br>何をしているのか\")\nS[\"c2\"] = chapter(\"2\", \"机の上のロボットアームで、<br>何が簡単で、何が難しかったのか\")\nS[\"c3\"] = chapter(\"3\", \"データの壁はどれくらい高くて、<br>誰がどう越えようとしているのか\")\nS[\"c4\"] = chapter(\"4\", \"エイイチの見立て\")\n\nS[\"use\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期の人型ロボット出荷の用途(Counterpointの推計)</div>\n {hbar([(\"展示・イベント + 研究・データ生産 6割以上\", 62, RED, \"#fff\"), (\"その他\", 38, \"#D3D3CE\", INK)])}\n <div class=\"h3\" style=\"margin-top:44px\">出荷の <span class=\"red\">6割以上</span> が、<br>動きを見せる用途と、研究・データ生産の用途</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)。帯の長さは「6割以上」を表す目安</div>\"\"\", tag=T1)\n\nS[\"datacollect\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">データ生産 = ロボットが学ぶための、動きの記録を作ること</div>\n <div class=\"row\" style=\"gap:30px\">\n <div class=\"box\">人が遠隔で動かして<br>記録する</div>\n <div class=\"box\">自律で動いた記録を<br>残す</div>\n </div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">※研究向けの台数が、全部データ集めに使われるわけではない</div>\n <div class=\"h3\" style=\"margin-top:20px\">分類に「データ生産」がある = 今の段階を表している</div>\n</div>\"\"\", tag=T1)\n\nS[\"conflict\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">同じ2026年上半期の推計</div>\n {two(\"Counterpoint\", \"見せる用途+研究・データ生産<br>6割以上\", \"Smart Analytics Global(SAG)\", \"産業・商業用途<br>7割超\", c1=\"\", c2=\"\", size=52, note1=\"出荷 22,000台超\", note2=\"出荷 約19,100台\")}\n <div class=\"h3\" style=\"margin-top:46px\">総数も分類も違う → <span class=\"red\">単純な裏表としては比べられない</span></div>\n <div class=\"h3 muted\" style=\"margin-top:10px\">出荷台数 ≠ 自律的に働いている台数</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)/Smart Analytics Global(2026年8月)</div>\"\"\", tag=T1)\n\nS[\"china\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期の人型ロボット出荷のうち、中国メーカーの割合(SAGの推計)</div>\n <div class=\"hero\">97<small>%以上</small></div>\n</div>\n<div class=\"src\">出典:Smart Analytics Global(2026年8月)</div>\"\"\", tag=T1)\n\nS[\"neo\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">1X「NEO」 家事をする人型ロボット(2025年10月 予約開始)</div>\n {two(\"値段と配送\", \"2万ドル:2026年配送予定<br>月499ドル:その後\", \"動き方\", \"最初は基本的な自律機能<br>知らない作業は<br>担当者が遠隔で手伝う\", c1=\"\", c2=\"\", size=46)}\n <div class=\"h3\" style=\"margin-top:40px\">2026年10月12日時点:一般顧客への配送開始の公式発表は<span class=\"red\">確認できず</span></div>\n</div>\n<div class=\"src\">出典:1X 公式の発表・注文ページ(2026年10月12日確認)</div>\"\"\", tag=T1)\n\nS[\"neo_point\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">売っているのは</div>\n <div class=\"flow\">\n <div class=\"box\">ロボット</div><div class=\"arrow\">+</div>\n <div class=\"box\">遠隔で<br>手伝う人</div><div class=\"arrow\">+</div>\n <div class=\"box\" style=\"border:4px solid {RED}\">動きを<br>覚えていく仕組み</div>\n </div>\n</div>\"\"\", tag=T1)\n\nS[\"bridge1\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">工場でも家でも、<br><span class=\"red\">使いながらデータを集める</span>動き</div>\n <div class=\"h2 muted\" style=\"margin-top:56px\">では、物を動かすのは<br>何がそんなに大変なのか</div>\n</div>\"\"\", tag=T1)\n\nS[\"trials\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">赤いブロックを掴んで、箱に置く</div>\n <div class=\"hero\">650<small>回以上</small></div>\n <div class=\"h3\" style=\"margin-top:36px\">失敗も含めた試行回数。全部台帳に記録</div>\n</div>\n<div class=\"src\">PICK→PLACE の通し試行(台帳 pick_place_ledger_v3.csv、2026年6月18日〜7月20日で656回)</div>\"\"\", tag=T2)\n\nS[\"easy\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{BLUE}\">簡単だった:掴めたかの判定</div>\n <div class=\"flow\">\n <div class=\"box\">iPhoneで<br>撮った写真</div><div class=\"arrow\">→</div>\n <div class=\"box\" style=\"flex:1.5\">AIに聞く<br><span style=\"font-size:32px;white-space:nowrap\" class=\"muted\">「ブロックを掴めていますか」</span></div><div class=\"arrow\">→</div>\n <div class=\"box\" style=\"border:4px solid {BLUE}\"><span class=\"blue\">はい</span>/いいえ</div>\n </div>\n <div class=\"h3\" style=\"margin-top:48px\">ただし、<span class=\"red\">答えが返る ≠ 毎回正しい</span></div>\n</div>\"\"\", tag=T2)\n\nS[\"hard\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">難しかった</div>\n <div class=\"h1\">動かすほう</div>\n</div>\"\"\", tag=T2)\n\nS[\"gap\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">エイイチの設定で、同じ関節の角度を入れたときの爪先の位置</div>\n <div style=\"position:relative;height:400px;margin-top:10px\">\n <div style=\"position:absolute;left:60px;top:250px;right:60px;height:4px;background:{RULE}\"></div>\n <div style=\"position:absolute;left:150px;top:218px;width:68px;height:68px;border-radius:50%;background:#fff;border:8px solid {SUB}\"></div>\n <div style=\"position:absolute;left:4px;top:310px;width:440px;text-align:center;font-size:36px;font-weight:700;color:{SUB}\">計算ツール(ikpy)</div>\n <div style=\"position:absolute;right:150px;top:218px;width:68px;height:68px;border-radius:50%;background:{RED}\"></div>\n <div style=\"position:absolute;right:4px;top:310px;width:440px;text-align:center;font-size:36px;font-weight:700;color:{RED}\">デジタルツイン</div>\n <div style=\"position:absolute;left:184px;right:184px;top:140px;height:4px;background:{INK}\"></div>\n <div style=\"position:absolute;left:182px;top:120px;width:4px;height:44px;background:{INK}\"></div>\n <div style=\"position:absolute;right:182px;top:120px;width:4px;height:44px;background:{INK}\"></div>\n <div style=\"position:absolute;left:0;right:0;top:20px;text-align:center;font-size:72px;font-weight:900\"><span class=\"red\">30cm以上</span> 食い違う</div>\n </div>\n <!-- 2026-10-12 説明の1行が右にはみ出して「グリッパ/ー」で折り返していたので、図の下に2行で中央にそろえた(DAY330 で足した説明。内容は同じ) -->\n <div class=\"h3\" style=\"text-align:center;font-size:38px;margin-top:-10px\">アームが外れた距離ではない。<span class=\"red\">爪先の位置の決め方</span>が違っていた</div>\n <div class=\"h3 muted\" style=\"text-align:center;font-size:32px;margin-top:10px\">計算ツールには、グリッパーの形が入っていない</div>\n</div>\n<div class=\"src\">図は模式図</div>\"\"\", tag=T2)\n\nS[\"silent\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">黙る失敗</div>\n <div class=\"flow\">\n <div class=\"box\">肩の関節に<br><span class=\"red\">90度ちょっと</span>を超える値</div><div class=\"arrow\">→</div>\n <div class=\"box\">エラーは出ない<br>アームも動かない</div>\n </div>\n <div class=\"h3 muted\" style=\"margin-top:44px\">エイイチの機体とソフトの組み合わせで起きたこと</div>\n</div>\"\"\", tag=T2)\n\nS[\"point2\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">この2つは</div>\n <div class=\"h2 muted\" style=\"text-decoration:line-through;text-decoration-thickness:5px\">AIの学習データが足りない</div>\n <div class=\"h1\" style=\"margin-top:30px\"><span class=\"red\">すり合わせ</span>の仕事</div>\n <div class=\"h3\" style=\"margin-top:20px\">モデルや制御が、自分の機体と噛み合っているかを確かめる</div>\n</div>\"\"\", tag=T2)\n\nS[\"years\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">画像と言葉を扱うAIの学習資料を、人が読む・見る時間に換算すると</div>\n <div class=\"hero\">約10<small>万年規模</small></div>\n</div>\n<div class=\"src\">出典:Ken Goldberg, Science Robotics(2025年8月, Editorial)</div>\"\"\", tag=T3)\n\nS[\"wall\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"h1\">10万年の<span class=\"red\">データの壁</span></div>\n <div style=\"margin-top:50px\">{two(\"画像と言葉\", \"大量にある\", \"動作と結びついた経験\", \"集めにくい\", size=58)}</div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">※ロボットにも同じ量が必要だと分かったわけではない</div>\n</div>\n<div class=\"src\">出典:Ken Goldberg, Science Robotics(2025年8月)</div>\"\"\", tag=T3)\n\nS[\"gen\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">Generalist が集めた、人の手作業の記録(会社の発表)</div>\n <div style=\"display:flex;flex-direction:column;gap:30px;margin-top:10px\">\n <div style=\"display:flex;align-items:center;gap:30px\"><div style=\"width:260px;font-size:36px;font-weight:700;color:{SUB}\">2025年11月</div>\n <div style=\"width:{int(1060*27/50)}px;height:110px;background:#D3D3CE;border-radius:0 6px 6px 0\"></div><div style=\"font-size:52px;font-weight:900;white-space:nowrap\">27万時間</div></div>\n <div style=\"display:flex;align-items:center;gap:30px\"><div style=\"width:260px;font-size:36px;font-weight:700;color:{SUB}\">2026年4月</div>\n <div style=\"width:1060px;height:110px;background:{RED};border-radius:0 6px 6px 0\"></div><div style=\"font-size:52px;font-weight:900;white-space:nowrap\">50万時間超</div></div>\n </div>\n <div class=\"h3\" style=\"margin-top:44px\">安い装着型の機材で記録</div>\n</div>\n<div class=\"src\">出典:Generalist(GEN-0 2025年11月/GEN-1 2026年4月)</div>\"\"\", tag=T3)\n\nS[\"gen2\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">Generalist GEN-1.5(2026年8月)</div>\n <div class=\"h2\">簡単で短い作業なら、<br><span class=\"red\">数秒の実演</span>で新しい作業をこなす例</div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">本人たちも:作業は簡単で短く、成功率はまだ控えめ</div>\n</div>\n<div class=\"src\">出典:Generalist「GEN-1.5」(2026年8月19日)</div>\"\"\", tag=T3)\n\nS[\"compare\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">作業を記録した時間と、10万年は<br><span class=\"red\">そのまま比べられない</span></div>\n <div class=\"h3\" style=\"margin-top:50px\"><span class=\"muted\">何倍足りないか</span> より <span class=\"red\">どれだけ広く集められるか</span></div>\n</div>\"\"\", tag=T3)\n\nS[\"dirt\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">壁はまだ高い。でも、<br><span class=\"red\">壁の前で土を積み始めている</span></div>\n <ul class=\"list\" style=\"margin-top:30px\">\n <li style=\"font-size:40px\"><span class=\"n\">・</span>1X:家の中で遠隔で手伝いながら、動きを覚えさせる</li>\n <li style=\"font-size:40px\"><span class=\"n\">・</span>Generalist:人の手作業を装着型の機材で記録する</li>\n </ul>\n</div>\"\"\", tag=T3)\n\nS[\"notall\"] = page(f\"\"\"\n<div class=\"wrap\">\n {two(\"写真を見せて判断させる入口\", \"もう普通に使える\", \"物を確実に動かす\", \"すり合わせとデータが<br>まだ要る\", size=56)}\n <div class=\"h3\" style=\"margin-top:48px\">「まだ全然ダメ」でも「もう来た」でもない</div>\n</div>\"\"\", tag=T3)\n\nS[\"q\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h1\">ロボットのニュースを見る<br><span class=\"red\">問いが2つ</span>に変わる</div>\n</div>\"\"\", tag=T4)\n\nS[\"q1\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">問い 1</div>\n <div class=\"h1\">自律で動いているのか、<br>人が遠隔で動かしているのか</div>\n <div class=\"h3 muted\" style=\"margin-top:36px\">デモ動画で見るところ:自律か遠隔か/再生速度/人が手を出した条件</div>\n</div>\"\"\", tag=T4)\n\nS[\"q2\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">問い 2</div>\n <div class=\"h2\" style=\"margin-bottom:40px\">その仕事には、どちらが要るのか</div>\n {two(\"見て判断する\", \"手軽に試せる<br><span style='font-size:34px;color:#888888'>任せる範囲は現場で確かめる</span>\", \"触れて動かす精度\", \"現場の機体と環境に<br>すり合わせる仕事が残る\", size=48, note1=\"例:棚の商品が足りているか見る\", note2=\"例:柔らかい物を潰さずに掴む\")}\n</div>\"\"\", tag=T4)\n\nS[\"gofe\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">ゴールドバーグさんの近道</div>\n <div class=\"flow\">\n <div class=\"box\">昔ながらの工学<br>+学習</div><div class=\"arrow\">→</div>\n <div class=\"box\">現場で役に立つ<br>仕事をする</div><div class=\"arrow\">→</div>\n <div class=\"box\">働きながら<br>データが増える</div>\n </div>\n <div class=\"h3\" style=\"margin-top:48px\">エイイチの机の上:組み合わせる点は通じる。<span class=\"muted\">規模も段階もまだ違う</span></div>\n</div>\"\"\", tag=T4)\n\nS[\"motion\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">データの話と並行して要るもの</div>\n <div class=\"h1\"><span class=\"red\">どう動くか</span>の設計</div>\n <div class=\"h3\" style=\"margin-top:36px\">ロボットが人に物を渡すとき、<br>何秒待って手を開けば、人は安心して受け取れるのか</div>\n <div class=\"h3 muted\" style=\"margin-top:28px\">成功か失敗かの判定だけでは出てこない</div>\n</div>\"\"\", tag=T4)\n\nS[\"answer2\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">冒頭の問いへの答え</div>\n {two(\"写真を見せて判断させる入口\", \"ずっと簡単になった\", \"物を確実に動かす\", \"すり合わせが<br>まだ必要\", size=58)}\n <div class=\"h3\" style=\"margin-top:44px\">負担を減らす研究は急速に進んでいる。<br>家で家事を任せられる転換点かは、<span class=\"red\">まだ判断できない</span></div>\n</div>\"\"\")\n\nS[\"action\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">ロボットのデモ動画を見たら</div>\n <div class=\"h1\">自律なのか、何回やって、<br><span class=\"red\">何回うまくいった</span>のか</div>\n</div>\"\"\")\n\nS[\"sources\"] = page(\"\"\"\n<div class=\"wrap\" style=\"left:420px;bottom:90px;justify-content:flex-start;top:120px\">\n <div class=\"h2\" style=\"margin-bottom:30px\">出典</div>\n <div class=\"p\" style=\"font-size:29px;line-height:1.75\">\n 人型ロボットの出荷:Counterpoint Research(2026年8月)/Smart Analytics Global(2026年8月)<br>\n 1X NEO:1X 公式の発表・注文ページ(2026年10月12日確認)<br>\n 10万年のデータの壁:Ken Goldberg, Science Robotics(2025年8月, Editorial)<br>\n Generalist:GEN-0(2025年11月)、GEN-1(2026年4月)、GEN-1.5(2026年8月)の公式発表<br>\n ほかの機体から学ぶ研究の例:Gemini Robotics 1.5 技術報告(Google DeepMind)<br>\n 実機の数字:Pixels to Motors の台帳(pick_place_ledger_v3.csv)、公開記事 DAY257<br>\n <span class=\"muted\">ナレーションは AI 音声(ElevenLabs)。話しているのは Pixels to Motors のキャラクター、ハコブくん</span>\n </div>\n</div>\"\"\")\n\n\n# --- 強弱をつける場面の上書き(2026-10-11)---\nS[\"hook_question\"] = punch(\"こういうニュースを見ると\", [\"ロボットも\", \"一気に賢くなる?\"])\nS[\"answer\"] = punch(\"先に結論\", ['見て判断する入口は<span class=\"blue\">簡単に</span>', '物を動かすのは<span class=\"red\">まだ手間</span>'], sub=\"手間は2つ:動作と結びついたデータ/機体や環境とのすり合わせ\", size=110)\nS[\"c1\"] = chapter2(\"1つ目\", \"出荷の中身\", \"出荷が4倍になったロボットは、何をしているのか\")\nS[\"c2\"] = chapter2(\"2つ目\", \"机の上の実感\", \"何が簡単で、何が難しかったのか\")\nS[\"c3\"] = chapter2(\"3つ目\", \"データの壁\", \"どれくらい高くて、誰がどう越えようとしているのか\")\nS[\"c4\"] = chapter2(\"最後に\", \"ニュースの見方\", \"エイイチの見立て\")\nS[\"china\"] = dots(97, \"2026年上半期の人型ロボット出荷のうち\", \"中国メーカー\", \"97%\", note=\"SAGの推計では「97%以上」\", src=\"出典:Smart Analytics Global(2026年8月)\", tag=T1)\nS[\"point2\"] = punch(\"この2つは\", [\"データ不足じゃない\", '<span class=\"red\">すり合わせ</span>の仕事'], sub=\"モデルや制御が、自分の機体と噛み合っているかを確かめる\", tag=T2)\nS[\"wall\"] = punch(\"ゴールドバーグさんの問題提起\", ['10万年の', '<span class=\"red\">データの壁</span>'], sub=\"動作と結びついた経験は集めにくい(同じ量が必要と分かったわけではない)\", kc=\"muted\", tag=T3)\nS[\"compare\"] = punch(\"ひとつ注意\", ['<span class=\"muted\">何倍足りないか</span>より', '<span class=\"red\">どれだけ広く</span>集めるか'], sub=\"作業を記録した時間と10万年は、そのまま比べられない\", size=110, tag=T3)\nS[\"answer2\"] = punch(\"冒頭の問いへの答え\", ['判断する入口は<span class=\"blue\">来た</span>', '家事を任せるのは<span class=\"red\">まだ</span>'], sub=\"負担を減らす研究は急速に進んでいる\", size=110)\n\nO = {\n # 2026-10-12 ハコブくんが話す間は番組とハコブくんの名前、エイイチを紹介する文からエイイチの名前を出す(エイイチの指示)\n \"title\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">フィジカルAI解説</div><div class=\"c\">案内役:ハコブくん(AI音声)</div></div>'),\n \"title_eiichi\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">エイイチ</div><div class=\"c\">デザイナー/ロボティクスの研究・開発</div></div>'),\n \"v_real\": overlay('<div class=\"cap\">実機:mechArm 270-Pi 赤いブロックを掴んで箱に置く</div>'),\n \"v_twin\": overlay('<div class=\"cap\">パソコンの中のアームの分身(Unity のデジタルツイン)</div>'),\n \"v_scan\": overlay('<div class=\"cap\">デジタルツインの中で動かして、関節の角度と爪先の位置の対応表を作る</div>'),\n \"outro\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">相談は概要欄の連絡先から</div><div class=\"c\">高評価・チャンネル登録/コメントはエイイチが全部読んでいます</div></div>'),\n}\n\n# 動画シーン: (ソースファイル, 開始秒, 透かし消しの有無)\n# src/ の中身(元は iCloud の 動画/ にある。2026-10-12 前の版の動画と1コマずつ照らして確かめた):\n# c1.mov = DAY123.MOV / c2.mov = DAY124.mov(どちらも撮ったままの元の映像。DAY123.mp4・DAY124.mp4 は Clideo の透かし入りで、透かしを消すと右下がモヤモヤになったので使わない。2026-10-12)/ c3.mov = デジタルツイン_260416.mov(アームが並ぶ画面)/ c4.mov = ロボットアーム失敗_260416.mov(アーム1台と箱)\nVIDEO = {\n \"title\": (\"src/c1.mov\", 0.0, False),\n \"title_eiichi\": (\"src/c1.mov\", None, False), # None = 前の場面(title)の映像の続きから\n \"v_real\": (\"src/c2.mov\", 22.0, False),\n \"v_twin\": (\"src/c4.mov\", 0.0, False),\n \"v_scan\": (\"src/c3.mov\", 0.0, False),\n \"outro\": (\"src/c1.mov\", 8.0, False),\n}\n</code></pre><h3 id=\"index_53PHekBl\">フィジカルAIの現在地/sound.py</h3><p>BGM と効果音の置き方(23行)。</p><pre><code class=\"language-python\"># 音の置き方(audio_mix.py が読む。書き方は audio_mix.py の先頭)\n# 2026-10-12 冒頭の BGM は ElevenLabs Music で作った bgm_明るいB_2(エイイチが選んだ。説明文は sound/作った記録.jsonl)\n\n# 大きさ: 話している間はナレーションより23dB小さく、間は16dB小さく(2026-10-12 エイイチの指示。-22/-15 から約90%に下げた。明るい曲では -15/-9 だと大きすぎた)\nBGM = [\n # 2026-10-12 仕上げ版: 最初から最後まで同じ曲をループで流す(本編・ジングル用の曲は未定)\n dict(file='sound/bgm_明るいB_2.mp3', start=0, end=None, level=-23, gap=-16, fade_in=0.6, fade_out=3.0),\n]\n\nSE = [\n # (文番号, 文の頭から何秒後, 音, ナレーションより何dB)\n # 2026-10-12 エイイチの指示で、効果音は場面切り替えのホワーンだけにした(始まりの「しゅっ」は外した)\n]\n\n# 場面が切り替わるたびに「ホワーン」(se_rise)を入れる。効果音はこれだけ(2026-10-12 エイイチの指示)\nSCENE_SE = dict(file='se_rise', level=-6, offset=-0.15, skip=['title_eiichi']) # 2026-10-12 エイイチの指示で -12 から 6dB 上げた。title→title_eiichi は同じ映像が続くので鳴らさない\n\n# 大きな流れの変わり目(自己紹介・今日の問い・各章の始まり・答え・アウトロ)は、ホワーンの代わりに別の音を鳴らす。\n# 直前の文の後ろに 1.8 秒の間がある(narration.py)。2026-10-12 エイイチの指示\n# 自己紹介(title): キラリン(se_chapter_glock。鉄琴の高い3音)… 2026-10-12 エイイチが決定\n# そのほか: ポロロン(se_chapter_marimba。マリンバの上がる3音)… 試し中。場所ごとの使い分けはこれから決める\nCHAPTER_SE = dict(file='se_chapter_marimba', files={'title': 'se_chapter_glock'}, level=-8, offset=-0.3,\n scenes=['title', 'question', 'c1', 'c2', 'c3', 'c4', 'answer2', 'outro'])\n</code></pre><h3 id=\"index_uVDSAdpJ\">フィジカルAIの現在地/thumb.py</h3><p>サムネイルの文言と写真(14行)。</p><pre><code class=\"language-python\"># サムネイルの文言と背景(thumbnail.py が読む。書き方は thumbnail.py の先頭)\n# 2026-10-12 動画と同じ文字・色で作る(エイイチの指示)。文言はタイトル案1に合わせた\nPHOTO = 'サムネ素材/素材2_実機のアーム.png' # 実機のアームが赤いブロックを掴む瞬間(DAY124.mov の28秒)\nPHOTO_POS = '6% 50%' # split の右の面で、ブロックを掴む爪先からアームの根元までが入る位置\nHAKOBU = True\nLABEL = '本当に?'\nKICKER = '人型ロボット出荷 約4倍'\nLINES = ['ロボットにも', '<span class=\"red\">ChatGPTの瞬間</span>', 'もう来た?']\nVARIANTS = ['photo3'] # 2026-10-12 背景を全部写真に(エイイチの指示。dark/dark2/dark3 → photo2 → photo3)\n# photo3 の写真: ハコブくんを左下の隅に置くので、アームが箱の上にいるコマ(左下のマットが空く)。DAY124.mov の39.8秒\nPHOTO3 = 'サムネ素材/素材3_箱に置く瞬間.png'\nHK_H = 600 # ハコブくんの高さ(1920×1080 のとき。photo2 の 520 より少し大きく)\nPHOTO_POS_FULL = '50% 50%'\nBOLD_LINES = ['人型ロボット出荷 約4倍', 'ChatGPTの瞬間', 'もう来た?']\n</code></pre><h3 id=\"index_Wfwum3Uk\">フィジカルAIの現在地/概要欄_下書き.md</h3><p>概要欄の下書き(67行)。</p><pre><code class=\"language-markdown\"><!--\nYouTube の概要欄の下書き(2026-10-12)。{t:文番号} は、その文が始まる時間に置きかわる。\n全体を書き出したあと、python3 <_制作ベース>/description.py で 概要欄.txt を作り、それを貼る。\n「【要記入:…】」はエイイチが決めて書く。\n-->\n今年の上半期に出荷された人型ロボットは、世界で2万2000台を超えました。前の年の同じ時期の、およそ4倍です(Counterpoint の推計)。\n\nロボットにも、ChatGPT のような瞬間はもう来たのか。\n\n先に答えを言うと、写真を見せて判断させる入口は、ぐっと簡単になりました。でも、物を確実に動かすところは、まだ手間がかかります。手間は2つ。動作と結びついたデータが集めにくいこと。そして、機体や環境とのすり合わせです。\n\nこの動画では、\n\n・出荷が4倍になったロボットは、何をしているのか\n・机の上のロボットアームで、何が簡単で、何が難しかったのか\n・データの壁はどれくらい高くて、誰がどう越えようとしているのか\n\nの3つを見たうえで、ロボットのニュースを見るときの2つの問いを解説します。\n\n▼目次\n0:00 人型ロボットの出荷は4倍。ロボットにもChatGPTの瞬間?\n{t:6} この番組について\n{t:9} 今日の問いと、先に結論\n{t:26} 1. 出荷の中身:4倍になったロボットは何をしているのか\n{t:53} 2. 机の上の実感:何が簡単で、何が難しかったのか\n{t:83} 3. データの壁:どれくらい高く、誰がどう越えようとしているのか\n{t:111} エイイチの見立て:ニュースを見るときの2つの問い\n{t:132} 冒頭の問いへの答え\n{t:141} エンディング\n\n▼出典\n・人型ロボットの出荷:Counterpoint Research(2026年8月20日)の推計\nhttps://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026\n・同じく出荷の推計:Smart Analytics Global(2026年8月)\n・1X NEO:1X 公式の製品・注文ページ(2026年10月12日に確認。一般のお客さんへの配送開始を示す公式発表は、この日の時点で見つかっていない)\nhttps://www.1x.tech/neo\n・10万年のデータの壁:Ken Goldberg「GOFE can close the 100,000-year robot data gap」Science Robotics(2025年8月, Editorial)\nhttps://doi.org/10.1126/scirobotics.aea7390\n・Generalist の会社の公式発表:GEN-0(2025年11月)、GEN-1(2026年4月)、GEN-1.5(2026年8月)\nhttps://generalistai.com/blog/gen-0\nhttps://generalistai.com/blog/gen-1\nhttps://generalistai.com/blog/gen-1.5\n・ほかの機体から学ぶ研究の例:Gemini Robotics 1.5 技術報告(Google DeepMind)\nhttps://arxiv.org/abs/2510.03342\n・実機の数字:Pixels to Motors の試行の台帳(pick_place_ledger_v3.csv)、公開記事 DAY257\nhttps://adaptdesign.jp/pixelstomotors/record/why-z-correction-failed-twin-poses\n<!-- 2026-10-12 リンクを確認: Counterpoint・1X・Generalist の3本・arXiv はページが開くことを確かめた。Science Robotics は DOI(バークレーの研究室が公開している PDF のファイル名と同じ番号)。Smart Analytics Global は公式の発表ページが見つからなかったので、リンクなし -->\n\n▼この番組について\n「フィジカルAI解説|Pixels to Motors」は、フィジカルAIを、現場で使える形にするまでを解説する番組です。\nエイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。その中で分かったことと、そこから考えた見立てを、案内役のハコブくんがお伝えします。\n\n・ナレーションは AI 音声(ElevenLabs)です。話しているのは Pixels to Motors のキャラクター、ハコブくんです\n・BGM は ElevenLabs Music で作った曲、効果音はコードで作った音です\n\n▼ロボットの開発・導入のご相談\nメール:pixelstomotors@adaptdesign.jp\n\n▼リンク\nWebサイト:https://adaptdesign.jp/pixelstomotors\nGitHub:https://github.com/PixelstoMotors\nX:https://x.com/adaptdesignjp\nLinkedIn:https://www.linkedin.com/in/eiichi-shimazu\n\nチャンネル登録・高評価もよろしくお願いします。コメントは、エイイチが全部読んでいます。\n\n#フィジカルAI #人型ロボット #ロボット\n</code></pre><h3 id=\"index_cfdrc8jp\">フィジカルAIの現在地/plot_ducking.py</h3><p>この記事の BGM のグラフを描いたスクリプト(69行)。</p><pre><code class=\"language-python\">\"\"\"記事 DAY333 の図: 20〜80秒のナレーションの波形と、audio_mix.py が決める BGM の大きさを並べて描く。\n使い方: 動画の作業フォルダで python3 plot_ducking.py\n\"\"\"\nimport sys\nimport numpy as np\nsys.path.insert(0, '.')\nsys.path.insert(1, '../_制作ベース')\nsys.path.insert(2, '../base')\nfrom timing import load, layout, narration, FPS, SR\nimport audio_mix as am\nimport matplotlib\nmatplotlib.use('Agg')\nimport matplotlib.pyplot as plt\nfrom matplotlib import font_manager\n\nFONT = '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc'\nfont_manager.fontManager.addfont(FONT)\nplt.rcParams['font.family'] = font_manager.FontProperties(fname=FONT).get_name()\n\nidx = load(None)\nlayout(idx)\nvoice = narration(idx).astype(np.float32) / 32768\ntotal = len(voice) / SR\n\n# audio_mix.py の mix() と同じ手順で「話している所」を決める\nctl = np.zeros(int(total * 100) + 1)\nfor o in idx:\n if o['text']:\n a = max(0, int((o['start'] / FPS - 0.15) * 100))\n b = int(((o['start'] + o['speech']) / FPS + 0.3) * 100)\n ctl[a:b] = 1\ngaps = np.where(np.diff(np.concatenate([[1], ctl, [1]])) != 0)[0].reshape(-1, 2)\nfor a, b in gaps:\n if b - a < 50:\n ctl[a:b] = 1\nctl = am.smooth(ctl, 0.3)\nlv = 20 * np.log10(am.db(-23) * ctl + am.db(-16) * (1 - ctl))\n\nt0, t1 = 20, 80\nt = np.arange(len(ctl)) / 100\nm = (t >= t0) & (t <= t1)\nfig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 5.2), sharex=True, gridspec_kw={'height_ratios': [1, 1.2]})\ns0, s1 = int(t0 * SR), int(t1 * SR)\nv = voice[s0:s1]\nstep = 200\nenv = np.abs(v[:len(v) // step * step].reshape(-1, step)).max(1)\ntv = t0 + np.arange(len(env)) * step / SR\nax1.fill_between(tv, -env, env, color='#0d0d0d', lw=0)\nax1.set_ylabel('ナレーション')\nax1.set_yticks([])\nax2.plot(t[m], lv[m], color='#C4471F', lw=2)\nax2.set_ylim(-25, -14)\nax2.set_yticks([-23, -16])\nax2.set_yticklabels(['-23dB 話している間', '-16dB 0.8秒以上の間'])\n# 効果音の位置(assemble_full.log の時刻)\nses = [(22.78, 'ホワーン'), (33.40, 'キラリン'), (55.20, 'ポロロン'), (71.38, 'ホワーン')]\nfor x, l in ses:\n if t0 <= x <= t1:\n for a in (ax1, ax2):\n a.axvline(x, color='#1A6C98', lw=1, ls='--')\n ax2.text(x + 0.4, -14.6, l, color='#1A6C98', fontsize=10)\nax2.set_xlabel('動画の時間(秒)')\nfor a in (ax1, ax2):\n for sp in ('top', 'right'):\n a.spines[sp].set_visible(False)\nfig.suptitle('「フィジカルAIの現在地」20〜80秒:話している間は BGM を下げ、間で戻す(audio_mix.py)', fontsize=12)\nfig.tight_layout()\nfig.savefig('DAY333_02_bgm_ducking.png', dpi=150, facecolor='#F5F5F0')\nprint('DAY333_02_bgm_ducking.png')\n</code></pre><p></p><h2 id=\"index_MBHw9jHD\">書き出しのログと測った数値</h2><h3 id=\"index_Q1bHZ06h\">assemble.py の書き出しのログ(全量)</h3><p>最後に全体を書き出したときのログです。1行目が BGM と効果音の数、そのあとが効果音を鳴らした時刻・文番号・音・大きさ・その文の先頭16字、最後の行が全体のコマ数と確認用ファイルの大きさです。145番の文は、字幕のないエンディングの文です。</p><pre><code>BGM 1本・効果音 45個 → mix.wav\n 13.25秒 文002 se_rise -6dB 「年末までには、5万台を超えると予」\n 22.78秒 文004 se_rise -6dB 「こういうニュースを見ると、Cha」\n 33.40秒 文006 se_chapter_glock -8dB 「Pixels to Motors」\n 55.20秒 文009 se_chapter_marimba -8dB 「今日の問いは、ロボットにもCha」\n 71.38秒 文012 se_rise -6dB 「フィジカルAIっていうのは、現実」\n 85.32秒 文015 se_rise -6dB 「先に結論を言っちゃうと、写真を見」\n 106.02秒 文019 se_rise -6dB 「先に言っておくと、フィジカルAI」\n 117.05秒 文021 se_rise -6dB 「大きく3つあります。」\n 137.90秒 文026 se_chapter_marimba -8dB 「1つ目、出荷が4倍になったロボッ」\n 144.62秒 文027 se_rise -6dB 「Counterpointの推計で」\n 155.95秒 文029 se_rise -6dB 「データ生産っていうのは、ロボット」\n 181.92秒 文034 se_rise -6dB 「一方で、別の調査会社のSmart」\n 207.78秒 文039 se_rise -6dB 「もう1つ、SAGの推計では、出荷」\n 215.22秒 文040 se_rise -6dB 「家のほうも見てみます。」\n 251.52秒 文047 se_rise -6dB 「これ、ちょっと考えさせられません」\n 262.98秒 文050 se_rise -6dB 「工場でも家でも、使いながらデータ」\n 276.63秒 文053 se_chapter_marimba -8dB 「2つ目、エイイチの机の上のロボッ」\n 285.45秒 文054 se_rise -6dB 「ここは、エイイチ自身の話から始め」\n 298.85秒 文057 se_rise -6dB 「それだけの作業を、失敗も含めて6」\n 306.35秒 文058 se_rise -6dB 「まず、簡単だったほうから話します」\n 332.85秒 文063 se_rise -6dB 「難しかったのは、動かすほうでした」\n 336.58秒 文064 se_rise -6dB 「ロボットアームの爪先がどこに来る」\n 347.92秒 文066 se_rise -6dB 「パソコンの中に作ったアームの分身」\n 353.82秒 文067 se_rise -6dB 「エイイチの設定では、同じ関節の角」\n 373.52秒 文070 se_rise -6dB 「そこでエイイチは、分身の中でアー」\n 387.08秒 文073 se_rise -6dB 「もう1つ困ったのが、黙る失敗です」\n 404.65秒 文077 se_rise -6dB 「でも、ポイントはここなんです。」\n 434.47秒 文083 se_chapter_marimba -8dB 「3つ目、データの壁はどれくらい高」\n 442.28秒 文084 se_rise -6dB 「カリフォルニア大学バークレー校の」\n 459.62秒 文088 se_rise -6dB 「それに比べて、ロボットが手を動か」\n 488.68秒 文094 se_rise -6dB 「アメリカのGeneralistと」\n 505.52秒 文097 se_rise -6dB 「さらに8月には、簡単で短い作業な」\n 527.92秒 文101 se_rise -6dB 「ここで1つ、注意があります。」\n 542.65秒 文104 se_rise -6dB 「つまり、壁はまだ高い。でも、壁の」\n 561.08秒 文107 se_rise -6dB 「ただ、ここで「なんだ、まだ全然ダ」\n 581.20秒 文111 se_chapter_marimba -8dB 「ここからは、エイイチの見立てです」\n 585.28秒 文112 se_rise -6dB 「ロボットのニュースを見たときの問」\n 590.05秒 文113 se_rise -6dB 「1つは、そのロボットは自律で動い」\n 605.52秒 文116 se_rise -6dB 「もう1つは、その仕事は「見て判断」\n 634.32秒 文122 se_rise -6dB 「ゴールドバーグさんは、昔ながらの」\n 657.38秒 文126 se_rise -6dB 「作る側から見ると、もう1つありま」\n 684.30秒 文132 se_chapter_marimba -8dB 「じゃあ、冒頭の問いに答えますね。」\n 710.45秒 文137 se_rise -6dB 「ロボットのデモ動画を見たら、」\n 725.13秒 文141 se_chapter_marimba -8dB 「今日はここまでです。」\n 742.95秒 文145 se_rise -6dB 「」\nframes 22473 = 749.1s 確認用 29.4MB (crf 29)\n</code></pre><h3 id=\"index_vKASe5en\">上げたファイルの中身(ffprobe)</h3><pre><code>$ ffprobe -v error -show_entries format=duration,bit_rate:stream=codec_name,width,height,r_frame_rate,sample_rate,bit_rate -of default=nw=1 out.mp4\ncodec_name=h264\nwidth=1920\nheight=1080\nr_frame_rate=30/1\nbit_rate=488631\ncodec_name=aac\nsample_rate=48000\nr_frame_rate=0/0\nbit_rate=203703\nduration=749.100000\nbit_rate=701165\n</code></pre><p>1つ目の codec_name から bit_rate までが映像(488,631 ビット/秒)、2つ目が音声(AAC・48kHz・203,703 ビット/秒)、最後の duration と bit_rate がファイル全体です。</p><h3 id=\"index_gpWY2NxQ\">音の大きさ(ffmpeg の ebur128)</h3><pre><code>$ ffmpeg -nostats -i out.mp4 -af ebur128 -f null - 2>&1 | grep -A8 \"Summary\"\n[Parsed_ebur128_0 @ 0x5569ec070940] Summary:\n\n Integrated loudness:\n I: -16.0 LUFS\n Threshold: -26.4 LUFS\n\n Loudness range:\n LRA: 3.1 LU\n Threshold: -36.4 LUFS\n</code></pre><h3 id=\"index_DJoi5KGb\">ファイルの指紋(md5)</h3><pre><code>$ md5sum out.mp4 out_確認用.mp4\nd9f6def3f1a01d9aee5c76c9d6516bc0 out.mp4\n06c2afe18606354bd366a2e4224fab31 out_確認用.mp4\n</code></pre><h3 id=\"index_uLT3z9rw\">作った曲の記録(sound/作った記録.jsonl・全量)</h3><p><code>sound_eleven.py</code> が、曲を作るたびに1行ずつ書き足す記録です。prompt が ElevenLabs Music に渡した文章です。</p><pre><code>{\"file\": \"sound/bgm_本編_1.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Warm, gentle instrumental background music recorded with real acoustic instruments, for a thoughtful explainer about robots and AI. Felt piano and fingerpicked acoustic guitar, soft cello and violin, upright bass, very quiet brushed drums. Natural room sound, like a small live session. Around 90 BPM, calm and hopeful, a touch of color but serious in tone. No synthesizers, no electronic sounds, no vocals. A simple melody that stays out of the narrator's way.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:26\", \"song-id\": \"angU6pw2wgogpMCVOVzl\"}\n{\"file\": \"sound/bgm_本編_2.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Warm, gentle instrumental background music recorded with real acoustic instruments, for a thoughtful explainer about robots and AI. Felt piano and fingerpicked acoustic guitar, soft cello and violin, upright bass, very quiet brushed drums. Natural room sound, like a small live session. Around 90 BPM, calm and hopeful, a touch of color but serious in tone. No synthesizers, no electronic sounds, no vocals. A simple melody that stays out of the narrator's way.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:26\", \"song-id\": \"rGaTcW0xZPfECyYFmCtW\"}\n{\"file\": \"sound/bgm_冒頭_1.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Bright but gentle instrumental music played on real acoustic instruments, for the opening of a serious technology explainer. Acoustic guitar, piano, light pizzicato strings and soft marimba, natural room sound. Around 100 BPM, curious and hopeful, not cheesy. No synthesizers, no electronic sounds, no vocals, no dramatic hits.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:26\", \"song-id\": \"B0CIEZhLC948zw7GsAcP\"}\n{\"file\": \"sound/bgm_冒頭_2.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Bright but gentle instrumental music played on real acoustic instruments, for the opening of a serious technology explainer. Acoustic guitar, piano, light pizzicato strings and soft marimba, natural room sound. Around 100 BPM, curious and hopeful, not cheesy. No synthesizers, no electronic sounds, no vocals, no dramatic hits.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:27\", \"song-id\": \"5NbSMGUTgt0DXRjOTTB1\"}\n{\"file\": \"sound/bgm_明るいA_1.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Bright, warm and uplifting instrumental music played on real acoustic instruments, in a major key, for a friendly technology explainer about robots. Strummed and fingerpicked acoustic guitar, bright piano, glockenspiel accents, light hand claps and soft shaker, warm upright bass. Natural room sound. Around 105 BPM, cheerful and optimistic like a sunny morning, but not childish or cheesy. No synthesizers, no electronic sounds, no vocals. A simple melody that stays out of the narrator's way.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:31\", \"song-id\": \"Hd4Qnr113KBdkYe8qE9k\"}\n{\"file\": \"sound/bgm_明るいA_2.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Bright, warm and uplifting instrumental music played on real acoustic instruments, in a major key, for a friendly technology explainer about robots. Strummed and fingerpicked acoustic guitar, bright piano, glockenspiel accents, light hand claps and soft shaker, warm upright bass. Natural room sound. Around 105 BPM, cheerful and optimistic like a sunny morning, but not childish or cheesy. No synthesizers, no electronic sounds, no vocals. A simple melody that stays out of the narrator's way.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:31\", \"song-id\": \"bQUshWUbu4Y7vH4a43QI\"}\n{\"file\": \"sound/bgm_明るいB_1.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Hopeful, bright instrumental music on real acoustic instruments, in a major key, for a positive and curious explainer about the future of robots. Piano and acoustic guitar leading, warm strings, light pizzicato, a few glockenspiel notes, soft percussion. Natural room sound. Around 100 BPM, optimistic and forward-moving, bright but calm enough to sit under narration. No synthesizers, no electronic sounds, no vocals, no big build-ups.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:31\", \"song-id\": \"qyMIv2n4wM3BGvVoNqKR\"}\n{\"file\": \"sound/bgm_明るいB_2.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Hopeful, bright instrumental music on real acoustic instruments, in a major key, for a positive and curious explainer about the future of robots. Piano and acoustic guitar leading, warm strings, light pizzicato, a few glockenspiel notes, soft percussion. Natural room sound. Around 100 BPM, optimistic and forward-moving, bright but calm enough to sit under narration. No synthesizers, no electronic sounds, no vocals, no big build-ups.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:31\", \"song-id\": \"19pIxHwtXfiNptitO2FF\"}\n</code></pre><h2 id=\"index_azDryZvY\">書き出した概要欄(概要欄.txt・全量)</h2><p><code>description.py</code> が書き出し、YouTube に貼ったものです。</p><pre><code>今年の上半期に出荷された人型ロボットは、世界で2万2000台を超えました。前の年の同じ時期の、およそ4倍です(Counterpoint の推計)。\n\nロボットにも、ChatGPT のような瞬間はもう来たのか。\n\n先に答えを言うと、写真を見せて判断させる入口は、ぐっと簡単になりました。でも、物を確実に動かすところは、まだ手間がかかります。手間は2つ。動作と結びついたデータが集めにくいこと。そして、機体や環境とのすり合わせです。\n\nこの動画では、\n\n・出荷が4倍になったロボットは、何をしているのか\n・机の上のロボットアームで、何が簡単で、何が難しかったのか\n・データの壁はどれくらい高くて、誰がどう越えようとしているのか\n\nの3つを見たうえで、ロボットのニュースを見るときの2つの問いを解説します。\n\n▼目次\n0:00 人型ロボットの出荷は4倍。ロボットにもChatGPTの瞬間?\n0:33 この番組について\n0:55 今日の問いと、先に結論\n2:18 1. 出荷の中身:4倍になったロボットは何をしているのか\n4:36 2. 机の上の実感:何が簡単で、何が難しかったのか\n7:14 3. データの壁:どれくらい高く、誰がどう越えようとしているのか\n9:41 エイイチの見立て:ニュースを見るときの2つの問い\n11:24 冒頭の問いへの答え\n12:05 エンディング\n\n▼出典\n・人型ロボットの出荷:Counterpoint Research(2026年8月20日)の推計\nhttps://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026\n・同じく出荷の推計:Smart Analytics Global(2026年8月)\n・1X NEO:1X 公式の製品・注文ページ(2026年10月12日に確認。一般のお客さんへの配送開始を示す公式発表は、この日の時点で見つかっていない)\nhttps://www.1x.tech/neo\n・10万年のデータの壁:Ken Goldberg「GOFE can close the 100,000-year robot data gap」Science Robotics(2025年8月, Editorial)\nhttps://doi.org/10.1126/scirobotics.aea7390\n・Generalist の会社の公式発表:GEN-0(2025年11月)、GEN-1(2026年4月)、GEN-1.5(2026年8月)\nhttps://generalistai.com/blog/gen-0\nhttps://generalistai.com/blog/gen-1\nhttps://generalistai.com/blog/gen-1.5\n・ほかの機体から学ぶ研究の例:Gemini Robotics 1.5 技術報告(Google DeepMind)\nhttps://arxiv.org/abs/2510.03342\n・実機の数字:Pixels to Motors の試行の台帳(pick_place_ledger_v3.csv)、公開記事 DAY257\nhttps://adaptdesign.jp/pixelstomotors/record/why-z-correction-failed-twin-poses\n\n▼この番組について\n「フィジカルAI解説|Pixels to Motors」は、フィジカルAIを、現場で使える形にするまでを解説する番組です。\nエイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。その中で分かったことと、そこから考えた見立てを、案内役のハコブくんがお伝えします。\n\n・ナレーションは AI 音声(ElevenLabs)です。話しているのは Pixels to Motors のキャラクター、ハコブくんです\n・BGM は ElevenLabs Music で作った曲、効果音はコードで作った音です\n\n▼ロボットの開発・導入のご相談\nメール:pixelstomotors@adaptdesign.jp\n\n▼リンク\nWebサイト:https://adaptdesign.jp/pixelstomotors\nGitHub:https://github.com/PixelstoMotors\nX:https://x.com/adaptdesignjp\nLinkedIn:https://www.linkedin.com/in/eiichi-shimazu\n\nチャンネル登録・高評価もよろしくお願いします。コメントは、エイイチが全部読んでいます。\n\n#フィジカルAI #人型ロボット #ロボット\n</code></pre><h2 id=\"index_zetfem2o\">BGM と効果音の候補を調べた表(音源_候補.md・全量)</h2><p>規約は 2026年10月12日に公式のページで確かめた内容です。規約は変わるので、使う前にもう一度確かめます。</p><pre><code># BGM と効果音:どこから持ってくるか\n\n2026-10-12 作成。video_rules.md 8節「BGM・効果音」の判断材料。\n\n## 条件\n\n- YouTube で収益化しても使える\n- クレジット表記の条件を守れる(できれば表記なしで使える)\n- ほかのチャンネルと同じ曲になりにくい(声と同じ考え方。3-1)\n- ナレーションが聞き取れる音量に下げる。話している間は BGM を下げ、間で少し戻す(audio_mix.py が自動でやる)\n\n## 候補(規約は 2026-10-12 に公式のページで確かめた。変わるので、使う前にもう一度見る)\n\n| 用途 | 候補 | 収益化 | クレジット | 気をつけること |\n|---|---|---|---|---|\n| BGM(第一候補) | ElevenLabs Music(いまの Starter プランで使える) | 使える(映画・テレビ・ラジオ・ゲームの一部を除く) | Starter 以上はいらない(無料プランは必要) | 作れるのは月17分まで。曲そのものを音楽配信に出すのは禁止。新しく作る曲なので、ほかのチャンネルと同じ曲にならない |\n| BGM(安全側の控え) | YouTube オーディオライブラリ(YouTube Studio の中) | 使える。ここの曲には Content ID の申し立てが来ないと YouTube が書いている | 「帰属表示が不要」で絞り込めば、いらない | YouTube 以外(X・LinkedIn に切り抜きを載せるなど)での扱いは、ヘルプに書かれていない。多くのチャンネルが使うので、同じ曲になりやすい |\n| BGM(生の演奏・有料) | Audiostock(日本のサービス。人が演奏・録音した曲が多い) | YouTube で使える(動画配信者プランは本人のアカウントだけ) | 公式のライセンスのページに記載なし(使う前に確かめる) | 単品 1,210円〜(買い切り)。定額の動画配信者プランは月858円(年払い)。値段は公式ページの 2024年12月1日時点の表示。多くの曲が Content ID に登録されているが、ふつうは警告が出ない設定とのこと。定額で使った曲は、解約後に新しい動画では使えない(単品なら使える) |\n| BGM(控え) | OpenTracks(旧 DOVA-SYNDROME。2026年9月15日に名前が変わった) | 背景音楽としてなら商用も可 | いらない | 海外の団体が「音楽出版権」を不正に主張して、Content ID の申し立てが来る例があるとサイト自身が書いている(広告が付かない・収益の一部を取られることがある)。そのたびに異議を出す手間がかかる |\n| 効果音(第一候補) | コードで作る(sound_synth.py。もう作った) | 問題なし(自分で作った音) | いらない | 単純な電子音だけ。だれでも同じ音を作り直せるので、「再現できる」の考え方に合う |\n| 効果音(控え) | 効果音ラボ | 使える(「YouTube(収益化しても可)」と明記) | いらない | 効果音を Content ID に登録するのは禁止(他の人の利用に影響するため)。効果音だけの動画は作らない |\n| 効果音(控え) | ElevenLabs の効果音(sound_eleven.py sfx) | 有料プランで作った音は商用可 | いらない | 現実の音(モーター音・机に物を置く音など)が欲しいとき向き |\n\n外したもの: Pixabay の音楽(無料・クレジット不要だが、Content ID の扱いが公式のライセンス要約に書かれていない)。魔王魂・甘茶の音楽工房などのほかの無料サイトは、今回は調べていない。上の候補で足りなければ調べる。\n\n2026-10-12 エイイチ: BGM は生の楽器の音に近い方がよい。コードで作った BGM(bgm_1〜3・bgm_仮_温かみ)は方向を確かめる用にとどめる。\n\n## コードで作った効果音(_制作ベース/sound/。2026-10-12)\n\n| 名前 | 音 | 使う所 |\n|---|---|---|\n| se_pop | ぽん | 図・数字が出る。いちばんよく使う |\n| se_tick | こつ | 小さな切り替え。箇条書きが1つ出る |\n| se_whoosh | しゅっ | 章や場面が大きく変わる |\n| se_ding | ちん | 結論・大事な数字 |\n| se_rise | ひゅい | 驚き・問いかけ |\n| bgm_仮 | やわらかい和音と小さな粒の音(テンポ96・32秒でループ) | 本番の BGM ができるまでの仮。混ぜ方を確かめるためのもの |\n\n効果音は、ナレーションの声(おおむね 100〜300Hz)とぶつからないよう、500Hz より上を中心にしている。入れすぎると疲れるので、1分に2〜3個を目安にする。\n\n## ElevenLabs Music で作る BGM の説明文(英語と日本語訳)\n\nMac で、動画の作業フォルダから(キーの入れ方は tts_eleven.py と同じ):\n\n```\npython3 ../_制作ベース/sound_eleven.py music --name bgm_hook --sec 60 --count 2 --prompt \"(下の英文)\"\n```\n\n### A. 本編の後ろで流す曲(いちばん長く流れる)\n\n> Calm, curious instrumental background music for a technology explainer video about robots and AI. Soft electric piano and light plucked synth arpeggios, warm pads, gentle low bass, very soft minimal drums. Around 95 BPM, steady and unobtrusive. No strong melody that would compete with a narrator's voice. No vocals. Clean modern mix.\n\n日本語訳:ロボットとAIの解説動画の後ろで流す、落ち着いて、少し先が気になる感じの曲。やわらかいエレクトリックピアノ、軽くはじく電子音のアルペジオ、温かい和音、低くやさしいベース、ごく小さなドラム。テンポ95前後で一定、目立たない。ナレーションとぶつかる強いメロディは入れない。歌なし。\n\n### B. フック(動画の最初の数十秒)\n\n> Short instrumental intro bed for a tech news explainer, slightly brighter and more forward than background music. Pulsing soft synth, light percussion, a sense of something starting. Around 100 BPM. No vocals, no big drops, no dramatic trailer hits.\n\n日本語訳:技術ニュースの解説の冒頭で流す、短めの曲。本編の曲より少し明るく前に出る。脈打つやわらかい電子音、軽い打楽器、何かが始まる感じ。テンポ100前後。歌なし。盛り上げの落差や、映画の予告編のような強い音は入れない。\n\n### C. ジングル(「Pixels to Motors。案内役のハコブくんです。」の後ろ)\n\n> A 4-second friendly logo sting for a robotics channel. Soft synth bell chord with a gentle rising shimmer, ends cleanly on a warm chord. No vocals.\n\n日本語訳:ロボットのチャンネルの、4秒の親しみのあるロゴの音。やわらかい電子のベルの和音に、ゆっくり上がるきらめき。温かい和音できれいに終わる。歌なし。\n\n(C は --sec 4。A・B は 60 秒で作り、短ければ audio_mix.py がループでつなぐ)\n\n## 決める基準\n\n1. ナレーションの後ろで、言葉が聞き取りにくくならないか(音の確認.m4a をイヤホンとスピーカーの両方で聞く)\n2. 13分流して、うるさく感じないか\n3. ハコブくんの見た目と番組の落ち着いた調子に合うか</code></pre><p></p><h2 id=\"index_QVHbh5dK\">参照した情報</h2><ul><li><p><a href=\"https://network.mobile.rakuten.co.jp/sumakatsu/contents/articles/2026/00533/\" data-has-link=\"true\">YouTube のチャンネル名の変更方法(楽天モバイル スマ活)</a></p></li><li><p><a href=\"https://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026\" data-has-link=\"true\">Counterpoint Research:2026年上半期の人型ロボット出荷の推計(2026年8月20日)</a></p></li><li><p><a href=\"https://www.youtube.com/@PixelstoMotors\" data-has-link=\"true\">Pixels to Motors の YouTube チャンネル</a></p></li><li><p><a href=\"/\" data-has-link=\"true\">Pixels to Motors</a></p></li></ul>", "toc": [{"id": "index_5Npkigty", "text": "title: \"解説動画の1本目を公開まで通す ― 声・BGM・間・直し・サムネイルを部品ごとに決め、23手順にまとめた\" slug: first-video-end-to-end date: 2026-10-12 day: 333 meta_description: \"解説動画「フィジカルAIの現在地」をハコブくんの声で作り直し、YouTubeに公開した。声・BGM・効果音・間・不具合の直し・概要欄・サムネイルを部品ごとに決め、次の動画から使う23手順にまとめた。\" tags: [YouTube, 解説動画, ハコブくん, ElevenLabs, BGM, 効果音, ffmpeg, Python, Playwright, サムネイル, 動画制作]"}, {"id": "index_R8Gtcv3i", "text": "公開した動画は12分29秒・146文・効果音45個"}, {"id": "index_iJ5F55aG", "text": "ハコブくんの声は、ElevenLabs のボイスデザインで作った「候補6」に決めた"}, {"id": "index_qsSC6Gf9", "text": "「エイイチ」の抑揚は、ひらがなの「えいいち」で渡すとそろった"}, {"id": "index_NvWums4o", "text": "BGM は、コードで作った音から ElevenLabs Music の生楽器の曲に替えた"}, {"id": "index_845PdZbU", "text": "BGM の大きさは、話している間 -23dB・0.8秒以上の間 -16dB にした"}, {"id": "index_SkptrlzU", "text": "場面の切り替えは「ホワーン」、大きな流れの変わり目は「キラリン」と「ポロロン」"}, {"id": "index_ttUdYCtI", "text": "自己紹介の2文目を「研究と開発」と「見立て」に直した"}, {"id": "index_jcwDZAwf", "text": "右下のモヤモヤは、透かしを消した跡だった"}, {"id": "index_mZmow8P1", "text": "30cm の図の説明を、図の下の2行に移した"}, {"id": "index_RXyGoUij", "text": "YouTube の概要欄は、目次の時間を書き出しから自動で入れた"}, {"id": "index_WGBasXjY", "text": "タイトルを決め、「子ども向け」は「いいえ」にした"}, {"id": "index_ua9kJv2c", "text": "サムネイルは5回作り直した"}, {"id": "index_kbIhNmO_", "text": "ハコブくんの体は、ChatGPT で無地の背景から作り直した"}, {"id": "index_gTZdlqKn", "text": "サムネイルの背景は、ハコブくんがアームに重ならないコマを選んだ"}, {"id": "index_NqzL8c9Y", "text": "公開した直後に見えた2つのこと:字幕が2重に出る、画質が低い"}, {"id": "index_CTcfDe5k", "text": "チャンネル名は「フィジカルAI解説|Pixels to Motors」、まだ反映されていない"}, {"id": "index_qiv3oQpt", "text": "X と LinkedIn の投稿文は、数字と机の上の実感を先に置いた"}, {"id": "index_YMC72TM8", "text": "Claude の返事が英語になり、2回聞き返された"}, {"id": "index_ZKdeas3Q", "text": "Mac へ送ったファイルが、途中で変わることがあった"}, {"id": "index_lMTio9tY", "text": "次の動画から使う「1本を作る流れ」23手順"}, {"id": "index_B9rTkmMC", "text": "まだ決まっていないこと"}, {"id": "index_XNI1szIP", "text": "この記事で作った・直したスクリプトの全文"}, {"id": "index_MBHw9jHD", "text": "書き出しのログと測った数値"}, {"id": "index_azDryZvY", "text": "書き出した概要欄(概要欄.txt・全量)"}, {"id": "index_zetfem2o", "text": "BGM と効果音の候補を調べた表(音源_候補.md・全量)"}, {"id": "index_QVHbh5dK", "text": "参照した情報"}]}
\ No newline at end of file
diff --git a/src/data/records/making-explainer-video-with-ai.json b/src/data/records/making-explainer-video-with-ai.json
index 9625695..2d08b5a 100644
--- a/src/data/records/making-explainer-video-with-ai.json
+++ b/src/data/records/making-explainer-video-with-ai.json
@@ -1 +1 @@
-{"day": "LOG330", "series": "フィジカルAI研究室", "published": "2026-10-11T13:46:46.000Z", "title": "解説動画をAIと作る ― 型の分析から敵対的検証まで、試作1本で決めたこと", "slug": "making-explainer-video-with-ai", "cover": null, "thumb": "/thumbs/r-making-explainer-video-with-ai.html", "lang": "JP", "description": "YouTube解説動画の試作1本を、参考動画の型の分析、台本、音声合成と図解の自動組み立て、codexの敵対的検証、デザイン修正まで進めた記録。決めたルールを量産のベースとして残した。", "updated": "2026-10-12T00:09:46.000Z", "body": "<!--TOC--><p></p><p>Pixels to Motors の YouTube で解説動画を出すために、Claude と対話しながら試作を1本作りました。題材は「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」で、尺は13分50秒です。</p><p>進め方は次の順番でした。参考にしたい解説動画3本の文字起こしから、構成・話し方・補足説明の型を取り出す。型を台本テンプレートにまとめ、テンプレートに沿って台本を書く。台本を、読み上げ音声・図解・字幕・実機の映像に分けてコードで組み立て、MP4 に書き出す。できた台本を codex(OpenAI のコーディング AI)に渡して、論破するつもりで検証してもらう。指摘とデザインの修正を重ねる。最後に、量産するときのベースとしてルール文書とスクリプト一式を残す。</p><p>本記事は、その過程で決めたことと、決めた理由の記録です。間違えたところ(根拠のない数字を書いた、30cm の例が誤解を招く、など)も消さずに残しています。制作スクリプトの全文、実行ログ、codex の検証結果の全文は記事の後半に載せています。</p><div class=\"video\"><video controls playsinline preload=\"metadata\" src=\"/media/s-1920x1080_b273c60b-57bf-4356-b2b2-1b5a4023565f.mp4\"></video></div><p></p><h2 id=\"index_D_tmh6uh\">参考にした3本の解説動画は、冒頭1分半で問い・結論・目次を全部出していた</h2><p>参考にしたのは、時事ニュースを統計と自分の体験で検証する形式の解説動画3本です(YouTube「海外Yパパラジオ」の、会社への侵入と暗数の回、AI とサイバー攻撃の回、溺れている人を AI に見つけさせる実験の回)。動画のページは Claude からは開けなかったため、文字起こしを貼って分析しました。</p><p>3本とも、骨格が同じでした。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>時間の目安</p></th><th colspan=\"1\" rowspan=\"1\"><p>中身</p></th><th colspan=\"1\" rowspan=\"1\"><p>実例</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>0:00〜0:45</p></td><td colspan=\"1\" rowspan=\"1\"><p>挨拶より先に、時事ネタか視聴者のコメントから入る</p></td><td colspan=\"1\" rowspan=\"1\"><p>「最近漏洩多くないですか?」というコメント</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>〜0:50</p></td><td colspan=\"1\" rowspan=\"1\"><p>決まったジングルと自己紹介</p></td><td colspan=\"1\" rowspan=\"1\"><p>住んでいる場所から世界を見ている、と名乗る</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>〜1:10</p></td><td colspan=\"1\" rowspan=\"1\"><p>その回の問いを疑問文1つで言う</p></td><td colspan=\"1\" rowspan=\"1\"><p>「会社への侵入は本当に急に増えたのか」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>〜1:30</p></td><td colspan=\"1\" rowspan=\"1\"><p>結論か鍵になる言葉を先に出し、自分の立場を言う</p></td><td colspan=\"1\" rowspan=\"1\"><p>「変わったのは手口じゃなく攻撃の値段」「増えていないと言いたいわけじゃない」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>〜1:50</p></td><td colspan=\"1\" rowspan=\"1\"><p>「大きく○つあります」で目次を言う</p></td><td colspan=\"1\" rowspan=\"1\"><p>3〜5項目</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>本編</p></td><td colspan=\"1\" rowspan=\"1\"><p>「1つ目、○○です」で区切る。各節の最後の問いが次の節の題になる</p></td><td colspan=\"1\" rowspan=\"1\"><p>「じゃあ一斉に調べ始めたら何が出てくるのか」→ 次の節</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>終盤</p></td><td colspan=\"1\" rowspan=\"1\"><p>「ここからが僕の一番言いたいこと」と口で言ってから意見を話す</p></td><td colspan=\"1\" rowspan=\"1\"><p>事実と意見の境目を視聴者に示す</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>締め</p></td><td colspan=\"1\" rowspan=\"1\"><p>冒頭の問いに答え、次にニュースを見たときに考える問いを1つ渡す</p></td><td colspan=\"1\" rowspan=\"1\"><p>「起きた数か、見つかった数か、一度だけ考えてみて」</p></td></tr></tbody></table></div><p>冒頭の1分半で「何の話か・何に答えるか・どの立場で話すか・何章あるか」がそろいます。ほかに共通していたのは、視聴者の思い込みを1つ置いてデータで崩し、崩したあとに逆方向の早合点も止めること。数字は必ず比較で言い直すこと(「15倍ですよ」)。専門用語は使った直後に1文で説明すること。限界を自分から言うこと(「動画2本だけなので、性能の評価というより最初の手触り」)。</p><p>溺れている人の回だけは型が少し違い、道具の説明 → なぜ難しいか → 試した結果 → 失敗の深掘り → 見せ方を変えて再実験 → 見立て、の順でした。山場が「失敗が見つかる場面」にある実験回です。</p><p></p><h2 id=\"index_0LUCW9SF\">台本テンプレートは「解説回」と「実験回」の2種類に分けた</h2><p>型を、パートごとの秒数とセリフの穴埋め(例「先に結論を言っちゃうと、変わったのは{A}じゃなくて{B}なんですよ」)にして、解説回と実験回の2種類のテンプレートにしました。収録前に確認するチェックリスト(専門用語は初出で1文の説明があるか、数字に出典の名前が付いているか、など)も付けています。</p><p>試作の題材は「フィジカルAIの現在地」で、問いの形にして「ロボットにもChatGPTの瞬間はもう来たのか」としました。解説回のテンプレートに当てはめ、外部の数字(人型ロボットの出荷台数、1X の家庭用ロボット NEO、ゴールドバーグさんの「10万年のデータの壁」、Generalist の学習データ量)は Web で一次情報を確認して入れました。</p><p></p><h2 id=\"index_bTgs9pio\">台本に入れた自分の数字は、台帳と公開記事で裏を取った</h2><p>台本の節2は、自宅の机でロボットアーム(mechArm 270-Pi)を動かしてきた体験談です。ここに入れた数字は、Mac のフォルダと外付け SSD(KIOXIA)をつないでもらい、記録で確かめました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>台本の数字</p></th><th colspan=\"1\" rowspan=\"1\"><p>確かめた記録</p></th><th colspan=\"1\" rowspan=\"1\"><p>結果</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>掴んで置く作業の試行回数</p></td><td colspan=\"1\" rowspan=\"1\"><p>台帳 pick_place_ledger_v3.csv</p></td><td colspan=\"1\" rowspan=\"1\"><p>657行。テスト1行を除いて656回(2026年6月18日〜7月20日)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>爪先の位置の対応表の点数</p></td><td colspan=\"1\" rowspan=\"1\"><p>公開記事 LOG283</p></td><td colspan=\"1\" rowspan=\"1\"><p>約170万点</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>計算ツールとデジタルツインの爪先の食い違い</p></td><td colspan=\"1\" rowspan=\"1\"><p>arm_kinematics.md、公開記事 LOG222</p></td><td colspan=\"1\" rowspan=\"1\"><p>同じ関節角で300mm以上</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>黙って捨てられる指令</p></td><td colspan=\"1\" rowspan=\"1\"><p>公開記事 LOG257</p></td><td colspan=\"1\" rowspan=\"1\"><p>j1(肩)が約90.7度を超えると send_angles がエラーなしで無視される</p></td></tr></tbody></table></div><p>1つ、間違いがありました。試行回数を最初「600回以上」と書いていましたが、これは記録を見ずに書いた数字でした。記事を検索した段階では LOG255 の「100試行ぶん」しか見つからず、いったん「100回以上」に直しました。そのあと台帳を数えたら656回あり、「650回以上」に直しています。結果として最初の数字は大きく外れていませんでしたが、根拠なしに書いた数字だったことに変わりはありません。台帳の集計結果は、後半の実行ログに全量で載せています。</p><p></p><h2 id=\"index_vjneh5nj\">動画は、Open JTalk・Playwright・ffmpeg で1文ずつ組み立てた</h2><p>台本から動画までを、クラウドの作業環境でコードにして組み立てました。</p><div class=\"embed\"><iframe src=\"/embeds/making-explainer-video-with-ai-vpZFxoYF.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p></p><ol><li><p>narration.py に台本を書く。1文ごとに「場面の名前・字幕の文・後ろの間(秒)」の組にする</p></li><li><p>tts2.py で、1文ずつ読み上げ音声を作る</p></li><li><p>scenes.py に、場面ごとの図解を HTML で書く</p></li><li><p>render.py で、図解・テロップ・字幕を Playwright(ブラウザを自動で動かす道具)で画像にする</p></li><li><p>assemble.py で、1文ごとに「図解か実機映像」+「字幕」を ffmpeg で数秒の映像にし、全部つないで音声と合わせる。字幕ファイル(srt)も同時に出す</p></li></ol><p>1文を1つの映像のかたまりにしたのは、あとで1文だけ直したときに、その文の映像だけ作り直せばよいからです。実際、自己紹介の1文を差し替えたときは、作り直したのは3つのかたまりだけでした。</p><p>読み上げ音声には、Open JTalk(日本語の音声合成エンジン)の男声を使いました。より自然な音声エンジン(VOICEVOX など)は、作業環境から配布元の GitHub にアクセスできず、取り込めませんでした。Open JTalk は Ubuntu のパッケージで入れられたので、仮の声として使っています。抑揚は機械的で、公開するなら本人の声で録り直す前提です。</p><p>実機の映像は、Mac の動画フォルダにあった5本から、赤いブロックを掴んで箱に置く実機の映像(LOG124)、アーム単体の動き(LOG123)、Unity のデジタルツインの映像2本を選びました。LOG123 と LOG124 の右下には、以前に使った編集ツールの透かし(clideo.com)が入っていたため、ffmpeg の delogo でぼかし、字幕の後ろに敷く黒い帯の下に隠しています。</p><p></p><h2 id=\"index_YxOgXNy5\">Open JTalk は、ひと息が長い文の最後を1音0.47秒に引き伸ばしていた</h2><p>最初に書き出した動画(13分33秒)を見てもらったところ、次の指摘がありました。</p><blockquote><p>動画の中で音声が間延びしたところが何箇所か存在します。 それだけ修正を行うことはできますか?</p></blockquote><p>Open JTalk は、音声と一緒に「どの音を何秒出したか」の記録を出せます。これを全153文について調べると、6文で、文の後半の音が1つあたり約0.47秒になっていました。普段は0.1秒前後なので、3〜4倍に引き伸ばされています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>文</p></th><th colspan=\"1\" rowspan=\"1\"><p>引き伸ばされていた部分</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>現実の世界で物に触れて動かす技術のことなんですよ。</p></td><td colspan=\"1\" rowspan=\"1\"><p>「…なんですよ」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ChatGPTのようなAIの学習に使われたインターネットの文章を、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「…使われた」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ChatGPTは10万年分の教科書で育ったけれど、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「育ったけれど」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ここで、じゃあロボットにChatGPTの瞬間は当分来ない、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「当分来ない」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1Xが家の中で遠隔操作しながらデータを集めているのも、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「いるのも」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>出荷されたロボットの多くがデータ集めに回っているのも、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「のも」</p></td></tr></tbody></table></div><p>共通していたのは、読点なしでひと息に読む区間が長いことでした。ひと息の区間が長いと、その区間の最後の言葉が引き伸ばされます。</p><p>直し方は、読み上げ用の原稿だけに読点を足して区切ることです。字幕の文は変えません。最初は読点の位置で自動的に区切りましたが、「遠隔操作しなが/ら」のように単語の途中で切れた文があり、区切る位置を手で指定し直しました。その後、台本を書き直したときに、作った音声をすべて検査し、1音が0.3秒を超える文を見つけたら区切って作り直す処理を tts2.py に入れました。区切っても直らない文は、narration.py の OVERRIDE に「読み上げ用の読点の位置」を書いて直しています。</p><p></p><h2 id=\"index_0fVKV772\">codex の敵対的検証で、台本の結論は「成立していない」と判定された</h2><p>台本を公開前に codex に渡し、「この台本を論破するつもりで」検証してもらいました。依頼ファイルには、台本の全文に文番号(S001〜S152)を振り、使った出典の表と、検証してほしい観点(事実と数字・論理・抜けている反論・言い方)を付けています。依頼ファイルの全文と、返ってきた検証結果の全文は、後半に載せています。</p><p>判定は「直してから公開」、結論の評価は「現状では成立していない」でした。重大とされたのは次の4点です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>codex の指摘</p></th><th colspan=\"1\" rowspan=\"1\"><p>中身</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>「ChatGPTの瞬間」が未定義</p></td><td colspan=\"1\" rowspan=\"1\"><p>写真に質問して答えが返った体験1つから、「見て判断する部分には来た」と言い切っている</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>異種データの「1700分の1」比較</p></td><td colspan=\"1\" rowspan=\"1\"><p>人が文章を読む時間(10万年)と、作業を記録した時間(57年)は、意味も単位も違う</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>実機の問題を「データ不足」の例にしている</p></td><td colspan=\"1\" rowspan=\"1\"><p>計算ツールとデジタルツインの食い違いや、指令が無視される問題は、学習データの不足とは別の話</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Generalist の最新発表の抜け</p></td><td colspan=\"1\" rowspan=\"1\"><p>4月の GEN-1 を最新として話していたが、8月に GEN-1.5 が出ていた</p></td></tr></tbody></table></div><p>結論そのものについて、codex は次の形なら条件つきで成立すると書いていました。</p><blockquote><p>僕の机では、画像に質問する入口はずっと簡単になりました。一方、物を確実に動かすには、機体や環境とのすり合わせがまだ必要でした。その負担を減らす研究は急速に進んでいます。でも、いろんな家庭で安全に家事を任せられる転換点まで来たかは、まだ判断できません。</p></blockquote><p>台本の結論は、この形に差し替えました。</p><p></p><h2 id=\"index_t6LAP1jb\">codex が新しく挙げた事実は、一次情報で確かめてから採用した</h2><p>codex の指摘には、台本になかった新しい事実が含まれていました。採用する前に、それぞれの一次情報を開いて確かめています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>codex が挙げた事実</p></th><th colspan=\"1\" rowspan=\"1\"><p>確かめた一次情報</p></th><th colspan=\"1\" rowspan=\"1\"><p>確かめた内容</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Generalist が8月に GEN-1.5 を発表</p></td><td colspan=\"1\" rowspan=\"1\"><p>Generalist 公式ブログ(2026年8月19日)</p></td><td colspan=\"1\" rowspan=\"1\"><p>3〜12秒の実演1回から新しい作業をこなす。10種類の作業で平均成功率59%。本人たちも「作業は簡単で短く、成功率は控えめ」と書いている</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>SAG の分類は「工場や店舗向け」ではない</p></td><td colspan=\"1\" rowspan=\"1\"><p>Smart Analytics Global の発表</p></td><td colspan=\"1\" rowspan=\"1\"><p>原文は「Industrial and commercial applications」(産業・商業用途)。出荷の総数も約19,100台で、Counterpoint の22,000台超とは違う</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1X NEO の月499ドルのプランは配送時期が別</p></td><td colspan=\"1\" rowspan=\"1\"><p>1X の注文ページ</p></td><td colspan=\"1\" rowspan=\"1\"><p>2万ドルの早期購入枠は「US Deliveries start 2026」。月499ドルは「Standard Delivery」で日付の記載なし。最初は基本的な自律機能で、知らない作業は予約した時間に担当者が遠隔で支援する</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ゴールドバーグさんの10万年は「文章だけ」ではない</p></td><td colspan=\"1\" rowspan=\"1\"><p>Science Robotics の論説(2025年8月)</p></td><td colspan=\"1\" rowspan=\"1\"><p>現代の画像と言葉を扱う AI(VLM)の学習資料を、人が読む・見る時間に換算した概算</p></td></tr></tbody></table></div><p>確認できた4点はすべて台本に反映しました。台本の書き直しでは、問いの中の言葉(ChatGPTの瞬間)を冒頭で定義し、話の範囲(自動運転は扱わず、アームと人型ロボットが物を扱う話に絞る)を言い、自分の机の上の話は「僕の機体とソフトの組み合わせでは」と条件を付けて話す形にしています。</p><p></p><h2 id=\"index_YhZ9JBp8\">根拠の弱い数字は足さずに削った</h2><p>codex の指摘を反映する前に、次の注意がありました。</p><blockquote><p>チェックして修正点を検討して反映したいです(過剰なデータは特に問題になりやすいので注意が必要です)</p></blockquote><p>そこで、指摘に答えるために数字を足すのではなく、論旨に必要でないものは削る方針にしました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>削った数字</p></th><th colspan=\"1\" rowspan=\"1\"><p>削った理由</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1700分の1、約57年</p></td><td colspan=\"1\" rowspan=\"1\"><p>意味と単位の違う数字の割り算で、比較として成り立たない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>成功率99%超、ロボットのデータ約1時間</p></td><td colspan=\"1\" rowspan=\"1\"><p>会社の自己申告で、評価の条件を説明しないと誤解される</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>爪先の対応表の約170万点</p></td><td colspan=\"1\" rowspan=\"1\"><p>シミュレーターの中で作った表なのに、実機で170万回測ったように聞こえる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>AGIBOT 44%・Unitree 31% の内訳</p></td><td colspan=\"1\" rowspan=\"1\"><p>なくても話が変わらない</p></td></tr></tbody></table></div><p>残した数字には「推計では」「会社の発表では」を口で付けています。調査会社2社で用途の数字が逆向きに見える件は、「総数も分類も違うので、単純な裏表としては比べられません」と言う形にしました。</p><p></p><h2 id=\"index_gc6toY6y\">読み上げは「爪先」を「つまさき」と読んでいた</h2><blockquote><p>あと「爪先」をつまさきと言っていますが、アームの場合は「つめさき」の方の読み方が適切かと思います(つまさきだと、足の指先に思ってしまう)</p></blockquote><p>Open JTalk の辞書では「爪先」はツマサキでした。読み上げ用の読み替え表に「爪先 → つめさき」を足しています。同じ確認で、「90度ちょっとを超える値を送ると」の「値」がネと読まれていたことも見つけ、「値を送る → あたいを送る」を足しました。ほかに直した読みは「4分の3」(ヨンプンノサン → よんぶんのさん)、「1700分の1」(センナナヒャップンノイチ → せんななひゃくぶんのいち)、英語の名前(ChatGPT、Counterpoint、ikpy など)です。新しい固有名や数字を入れたら、エンジンの解析結果のカタカナを出して目で確認する手順にしました。</p><p></p><h2 id=\"index_HCnc4FWz\">カード上の太線と、角の立った字幕の縁取りをやめた</h2><blockquote><p>それとデザイン面で直したいのがカードの上に太いラインがあるのはいわゆるAIっぽいこなしなので違う表現にしたいです。</p></blockquote><p>最初の図解は、2つのものを対比するとき、白いカードの上辺に青とオレンジの太い線を引いていました。これをやめ、2列の間を細い縦線(2px)で区切り、色は文字の色だけで見分ける形にしました。同じ理由で、名前テロップの左端の太い線も外しています。</p><p></p><figure><img src=\"/media/s-1944x604_v-frms_webp_1ae69cf2-9b0a-420a-a0b8-a54b8fd6b7d8.png\" alt=\"\"><figcaption></figcaption></figure><p></p><blockquote><p>さらにフォントですが、フォントの黒縁が角が立っていているところが散見されます。 角を落とした黒縁に変更してください。</p></blockquote><p>字幕は CSS の文字の縁取り(-webkit-text-stroke)で作っていました。この方式は、線の角を丸める指定ができず、「宅」や「机」の角に尖った黒い角が出ます。字幕を SVG の文字に作り替え、線の角を丸くする指定(stroke-linejoin: round)を入れました。</p><p></p><figure><img src=\"/media/s-1100x610_v-fs_webp_3b59dae8-f9d7-4d46-ab1e-9cf7a3b11305.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_mlpaKj8r\">自己紹介から「秋田」「29年」「毎日」を外し、ロゴを差し替えた</h2><blockquote><p>動画の自己紹介のところで、29年秋田で、というところは削除で。 本業のデザイナーをしながらロボティクスの研究、開発を行っている、などの紹介に修正してください。</p></blockquote><blockquote><p>その記録を毎日公開、だと毎日ロボットを触っているかのような説明なので、「毎日」は削除した方がいい。</p></blockquote><p>自己紹介の文は「僕は本業のデザイナーをしながら、ロボティクスの研究と開発をしています。自宅の机の上でロボットアームを動かして、その記録を公開しています。」に固定しました。動画の中だけでなく、台本のドキュメントと codex への依頼ファイルにも同じ文言が残っていたので、そちらも直しています。</p><p>画面右上とテロップの「PIXELS TO MOTORS」の文字は、Pixels to Motors のロゴ画像に差し替えました。もらったロゴ画像(灰色の地に黒)から、地の明るさとの差で透明度を計算して切り抜き、文字の色をスライドの黒にそろえています。</p><p></p><figure><img src=\"/media/s-1920x1080_v-frms_webp_b7837ab7-c1c0-4d01-943b-1d3aa04345db.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_9szChvZM\">背景と文字の色を、記事サムネイルの #F5F5F0 と #0D0D0D に合わせた</h2><blockquote><p>動画の背景色やテキスト色を記事のサムネイルと同じ色に変更してください(差し色で使っているオレンジや水色は少し濃くするくらいでカラーを使うのはOKです) 色だけの話です。他は修正しないでください。</p></blockquote><p>記事のサムネイル画像から色を拾い、次の色に置き換えました。レイアウトと文言には手を付けていません。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>用途</p></th><th colspan=\"1\" rowspan=\"1\"><p>変更前</p></th><th colspan=\"1\" rowspan=\"1\"><p>変更後</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>背景</p></td><td colspan=\"1\" rowspan=\"1\"><p>#F4F2ED</p></td><td colspan=\"1\" rowspan=\"1\"><p>#F5F5F0</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>文字</p></td><td colspan=\"1\" rowspan=\"1\"><p>#17191E</p></td><td colspan=\"1\" rowspan=\"1\"><p>#0D0D0D</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>薄い文字</p></td><td colspan=\"1\" rowspan=\"1\"><p>#5F636B</p></td><td colspan=\"1\" rowspan=\"1\"><p>#888888</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>区切り線</p></td><td colspan=\"1\" rowspan=\"1\"><p>#CFCAC0</p></td><td colspan=\"1\" rowspan=\"1\"><p>#D6D6D1</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>オレンジ(強調)</p></td><td colspan=\"1\" rowspan=\"1\"><p>#D9542B</p></td><td colspan=\"1\" rowspan=\"1\"><p>#C4471F</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>青(対比のもう一方)</p></td><td colspan=\"1\" rowspan=\"1\"><p>#1F7FB0</p></td><td colspan=\"1\" rowspan=\"1\"><p>#1A6C98</p></td></tr></tbody></table></div><p>オレンジと青は、色覚の違いがあっても見分けられるかを検証するスクリプト(validate_palette.js)にかけ、新しい背景色の上で全項目を通過することを確かめてから使っています。検証の出力は後半の実行ログにあります。</p><p></p><h2 id=\"index_msVyMuQC\">30cmの食い違いは、アームが外れた距離ではなく、爪先の位置の決め方の違いだった</h2><blockquote><p>30cmも違うって合っている? 30mmとかじゃなくて?なんか例が良くない?</p></blockquote><p>数字そのものは記録どおりでした。公開記事 LOG222 に「同じ関節角を入れても、先端位置が300mm以上違う」とあります。ただ、同じ記事に理由も書かれていました。ikpy(関節の角度から先端の位置を計算する一般的なライブラリ)には、グリッパーの3D形状もデジタルツインの物理も入っていません。一方、デジタルツインで作った表の爪先の位置は、Unity の中でグリッパー先端の3Dモデルが実際に来た場所です。2つは、そもそも爪先の位置の決め方が違っていました。</p><p>mechArm 270 の腕の長さは約27cmです。「30cm食い違う」とだけ聞くと、腕の長さより大きく外れたように受け取れてしまいます。差し替え候補として、LOG222 にある実機のずれ(赤いブロックが6回とも同じ向きに、箱の中心から約5cmずれて落ちた件)も挙げましたが、30cm のまま説明を足す形を選んでもらいました。台本に「これは、アームが30cm外れた、という意味ではありません」「計算ツールには、グリッパーの形も、デジタルツインの物理も入っていなくて、爪先の位置の決め方そのものが違っていたんです」の2文を足し、図にも同じ説明を1行入れています。</p><p></p><figure><img src=\"/media/s-1280x720_v-fms_webp_89ef024b-47c1-4439-9d46-42759eb70eaa.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_Apnn9rBf\">打ち出し・説明・補足の3段階で、画面に強弱をつけた</h2><blockquote><p>全部じゃなくていいのですが、動画内の説明テキストの内容を強弱をつけるようにしたいです。 打ち出しを強くしたい、簡潔にわかりやすく内容を伝えたい、タイトルをして使いたいなどの時はテキストを大きくわかりやすくしたり、わかりやすい図解を入れ込んだりして動画内の内容がうまく伝わるように変化をつけたいです。</p></blockquote><p>参考にした動画の画面を見ると、小さな色付きの前置き(例「韓国の甲状腺がん」)の下に、1〜2行の大きな文(「診断は15倍/亡くなる人は増えず」)を置いていました。章の始まりには「今日の話 → 2つ目 → 見つからない」のように大きなキーワードを置き、割合は点の並びで見せていました。</p><p>これを、背景・文字・差し色はそのままで取り入れ、40ある場面のうち11場面だけ切り替えました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>段階</p></th><th colspan=\"1\" rowspan=\"1\"><p>使った場面</p></th><th colspan=\"1\" rowspan=\"1\"><p>見た目</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>打ち出し</p></td><td colspan=\"1\" rowspan=\"1\"><p>冒頭の問いかけ、結論、「すり合わせの仕事」、データの壁、比べ方の注意、最後の答え(6場面)</p></td><td colspan=\"1\" rowspan=\"1\"><p>色付きの前置き(44px)+ 1〜2行の大きな文(110〜150px)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>章の扉</p></td><td colspan=\"1\" rowspan=\"1\"><p>4つの章の始まり</p></td><td colspan=\"1\" rowspan=\"1\"><p>「今日の話」→「1つ目」→ 大きなキーワード(200px・オレンジ)→ 章の問い</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>点の並び</p></td><td colspan=\"1\" rowspan=\"1\"><p>中国メーカーの割合</p></td><td colspan=\"1\" rowspan=\"1\"><p>100個の点のうち97個に色を付ける</p></td></tr></tbody></table></div><p>打ち出しの文は、字幕をそのまま大きくするのではなく、その場面で一番残したい一言に縮めています。たとえば字幕が「この2つは、AIの学習データが足りない、という話とは別です。」の場面は、「データ不足じゃない/すり合わせの仕事」にしました。</p><p></p><figure><img src=\"/media/s-1944x604_v-frms_webp_cf142cf2-c7a5-4f0f-88d3-ae65e0605315.png\" alt=\"\"><figcaption></figcaption></figure><p></p><figure><img src=\"/media/s-1944x604_v-frms_webp_077bbf3b-9bcf-4932-bd31-a613c2b4d0eb.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_sOZrkg72\">動画ファイルは、iCloud ではなく KIOXIA の videos/ に置いた</h2><blockquote><p>動画はデータが重くなるのでSSDの容量を圧迫するので別のフォルダに保管するようにしてください。</p></blockquote><p>動画ファイル(mp4)は、外付け SSD の KIOXIA に <code>r2r2r-data/videos/{動画名}/</code> を作って置き、iCloud からは消しました。字幕の srt と codex への依頼ファイルは軽いので iCloud に残しています。KIOXIA には、YouTube に上げる高画質の作業用(約47MB)と、確認用の圧縮版(約30MB)の2本を置いています。</p><p>ここで1つ問題が起きました。作業環境から Mac へ一度に送れるファイルは30MBまでなので、作業用の動画を12MBずつ4つに分けて送り、Mac 側でつなぎ直しました。ところが、つないだファイルのチェック値(md5)が元と一致しません。調べると、動画の先頭部分を含む1つ目の断片だけが、元より5,875バイト大きくなり、中身も化けていました。2〜4つ目の断片は元と一致していました。1つ目の断片だけを gzip で圧縮した形で送り直し、Mac 側で展開してからつなぐと、チェック値が元と一致しました。転送の途中で、動画の先頭部分だけが何らかの変換を受けたと考えていますが、原因までは確かめていません。</p><p></p><h2 id=\"index_52jeog_s\">量産のベースとして video_rules.md と制作スクリプト一式を残した</h2><p>ここまでに決めたことを、動画制作ルール video_rules.md にまとめました。記事の article_rules.md と同じく、動画を作るときに最初に読む正本です。プロジェクトの資料と、iCloud の <code>動画/_制作ベース/</code> の両方に置き、同じフォルダに制作スクリプト一式とロゴの透過 PNG を入れています。</p><p>video_rules.md は8つの節でできています。動画の構成(解説回の型と時間配分)、台本のルール(codex の検証で直した点から作った11項目)、ナレーション(読み替え表と間延びの検査)、画面デザイン(色・文字の大きさ・図の型・強弱の3段階・やらない表現)、字幕、実機の映像とテロップ、書き出しと保存、まだ決まっていない項目、です。全文は後半に載せています。</p><p>MASTER(プロジェクト全体の方針文書)の保存先テーブルと、KIOXIA の README.md のフォルダ構成には、videos/ と _制作ベース の行がまだありません。この2か所の追記は、本人が行う予定です。</p><p></p><h2 id=\"index_3J7_uzPW\">まだ決まっていない8項目は、未決のまま残した</h2><p>量産の前に決めることが残っています。video_rules.md の8節に、いまの状態と決めることを並べています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>いまの状態</p></th><th colspan=\"1\" rowspan=\"1\"><p>決めること</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ナレーションの声</p></td><td colspan=\"1\" rowspan=\"1\"><p>Open JTalk の仮音声。抑揚が機械的</p></td><td colspan=\"1\" rowspan=\"1\"><p>本人の声で録るか、品質の高い音声合成にするか。srt に1文ごとの時間が入っているので、それに合わせて録れば映像はそのまま使える</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>図解の動き</p></td><td colspan=\"1\" rowspan=\"1\"><p>すべて静止画</p></td><td colspan=\"1\" rowspan=\"1\"><p>数字のカウントアップや矢印が伸びるなどの動きを入れるか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>背景の映像</p></td><td colspan=\"1\" rowspan=\"1\"><p>実機映像は自己紹介・節2・エンディングだけ</p></td><td colspan=\"1\" rowspan=\"1\"><p>解説中も実機映像を薄く敷くか、図解だけにするか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM・効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>なし</p></td><td colspan=\"1\" rowspan=\"1\"><p>使うか。使うなら YouTube で収益化できる音源から選ぶ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ジングル</p></td><td colspan=\"1\" rowspan=\"1\"><p>文言だけ</p></td><td colspan=\"1\" rowspan=\"1\"><p>音とロゴの動きを付けるか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>サムネイル</p></td><td colspan=\"1\" rowspan=\"1\"><p>まだない</p></td><td colspan=\"1\" rowspan=\"1\"><p>記事サムネイルと同じ配色・線画のトーンで作るか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>番組名</p></td><td colspan=\"1\" rowspan=\"1\"><p>未定</p></td><td colspan=\"1\" rowspan=\"1\"><p>決める</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>概要欄のチャプター</p></td><td colspan=\"1\" rowspan=\"1\"><p>まだない</p></td><td colspan=\"1\" rowspan=\"1\"><p>srt の節の開始時刻から作れる</p></td></tr></tbody></table></div><p></p><h2 id=\"index_sWhPqJ9y\">制作スクリプトの全文</h2><p>作業環境(Ubuntu、Python 3.13、ffmpeg 6.1.1、Playwright の Chromium、Noto Sans CJK JP)で動かした最終版です。Open JTalk は <code>apt-get install open-jtalk open-jtalk-mecab-naist-jdic hts-voice-nitech-jp-atr503-m001</code> で入れています。実機の映像は <code>src/c1.mp4</code>(LOG123)、<code>src/c2.mp4</code>(LOG124)、<code>src/c3.mov</code>(デジタルツインの並列スキャン)、<code>src/c4.mov</code>(デジタルツイン)として置き、ロゴは <code>logo_dark.png</code> として同じフォルダに置いて動かします。</p><h3 id=\"index_ZLxuvA9o\">narration.py(台本・読み替え表・読み上げ用の読点)</h3><pre><code class=\"language-python\"># -*- coding: utf-8 -*-\n# 台本「ロボットにもChatGPTの瞬間はもう来たのか」読み上げ原稿(codex 検証反映版)\n# 各要素: (scene_id, 字幕テキスト, 後ろの間[秒]) 間を省略すると 0.35 秒\nSEGS = [\n # 0 フック\n (\"hook_ship\", \"調査会社Counterpointの推計では、今年の上半期に出荷された人型ロボットは、\"),\n (\"hook_ship\", \"世界で2万2000台を超えました。前の年の同じ時期の、およそ4倍です。\", 0.6),\n (\"hook_forecast\", \"年末までには、5万台を超えると予想されています。\"),\n (\"hook_forecast\", \"家事をする人型ロボットの予約販売も、去年の秋から始まっています。\", 0.6),\n (\"hook_question\", \"こういうニュースを見ると、ChatGPTが出たときみたいに、\"),\n (\"hook_question\", \"ロボットも一気に賢くなって、家に来るのはもうすぐ、って感じませんか。\", 0.9),\n # 1 自己紹介\n (\"title\", \"Pixels to Motors。こんにちは、エイイチです。\"),\n (\"title\", \"僕は本業のデザイナーをしながら、ロボティクスの研究と開発をしています。\"),\n (\"title\", \"自宅の机の上でロボットアームを動かして、その記録を公開しています。\"),\n (\"title\", \"作る側の机の上から見える、フィジカルAIの話をします。\", 0.8),\n # 2 今日の問い\n (\"question\", \"今日の問いは、ロボットにもChatGPTのような瞬間は、もう来たのか、です。\"),\n (\"question\", \"ここでいうChatGPTの瞬間は、専門家じゃなくても、\"),\n (\"question\", \"いろんな仕事にすぐ使えるようになる転換点、という意味です。\", 0.7),\n (\"define\", \"フィジカルAIっていうのは、現実の世界を認識して、行動するAIのことです。\"),\n (\"define\", \"自動運転もここに入りますが、\"),\n (\"define\", \"今日は主に、アームや人型ロボットが物を扱う話に絞ります。\", 0.6),\n # 3 結論と立場\n (\"answer\", \"先に結論を言っちゃうと、写真を見せて判断させる入口は、ぐっと簡単になりました。\"),\n (\"answer\", \"でも、物を確実に動かすところは、まだ手間がかかるんですよ。\"),\n (\"answer\", \"その手間は大きく2つあります。\"),\n (\"answer\", \"動作と結びついたデータが集めにくいこと。そして、機体や環境とのすり合わせです。\", 0.6),\n (\"stance\", \"先に言っておくと、フィジカルAIはまだ使えない、と言いたいわけじゃないんです。\"),\n (\"stance\", \"どこまで来ていて、どこで手間がかかっているのかを、分けて見たいんです。\", 0.6),\n # 4 目次\n (\"toc\", \"大きく3つあります。\"),\n (\"toc\", \"出荷が4倍になったロボットは、何をしているのか。\"),\n (\"toc\", \"僕の机の上のロボットアームで、何が簡単で、何が難しかったのか。\"),\n (\"toc\", \"そして、データの壁はどれくらい高くて、誰がどう越えようとしているのか。\"),\n (\"toc\", \"最後に僕の見立てを話します。\", 0.9),\n # 5 節1\n (\"c1\", \"1つ目、出荷が4倍になったロボットは、何をしているのか、です。\", 0.7),\n (\"use\", \"Counterpointの推計では、出荷された人型ロボットの6割以上が、\"),\n (\"use\", \"展示やイベントで動きを見せる用途と、研究やデータ生産の用途なんですよ。\", 0.6),\n (\"datacollect\", \"データ生産っていうのは、ロボットが学ぶための動きの記録を作ることです。\"),\n (\"datacollect\", \"人が遠隔で動かして記録するやり方も、自律で動いた記録を残すやり方もあります。\"),\n (\"datacollect\", \"ただ、研究向けの台数が全部データ集めに使われる、という意味ではありません。\"),\n (\"datacollect\", \"それでも、用途の分類に「データ生産」という項目があること自体が、\"),\n (\"datacollect\", \"今の段階をよく表していると思います。\", 0.7),\n (\"conflict\", \"一方で、別の調査会社のSmart Analytics Globalは、\"),\n (\"conflict\", \"同じ半年の出荷を約1万9100台と推計していて、\"),\n (\"conflict\", \"産業・商業用途が7割を超えた、としています。\"),\n (\"conflict\", \"総数も分類も違うので、この2つは、単純な裏表としては比べられません。\"),\n (\"conflict\", \"それに、出荷台数は、そのまま自律的に働いている台数でもないんです。\", 0.7),\n (\"china\", \"もう1つ、SAGの推計では、出荷の97%以上が中国のメーカーです。\", 0.7),\n (\"neo\", \"家のほうも見てみます。\"),\n (\"neo\", \"アメリカの1Xという会社は、家事をする人型ロボット、NEOの予約を、去年の10月に始めました。\"),\n (\"neo\", \"2万ドルの早期購入枠は、今年配送の予定で、月499ドルのプランは、その後です。\"),\n (\"neo\", \"最初は基本的な自律機能だけで、知らない作業は、\"),\n (\"neo\", \"予約した時間に、1Xの担当者が遠隔で手伝う仕組みなんですよ。\"),\n (\"neo\", \"10月11日の時点で、一般のお客さんへの配送開始を示す公式発表は、\"),\n (\"neo\", \"僕が確認した範囲では、見つかっていません。\", 0.7),\n (\"neo_point\", \"これ、ちょっと考えさせられませんか。\"),\n (\"neo_point\", \"売っているのはロボットだけじゃなくて、遠隔で手伝う人と、\"),\n (\"neo_point\", \"そこで動きを覚えていく仕組みごとなんです。\", 0.7),\n (\"bridge1\", \"工場でも家でも、使いながらデータを集める動きが出てきているんですよ。\"),\n (\"bridge1\", \"じゃあ、物を動かすのは、何がそんなに大変なのか。\"),\n (\"bridge1\", \"まず、僕の机の上の話をさせてください。\", 0.9),\n # 6 節2\n (\"c2\", \"2つ目、僕の机の上のロボットアームで、何が簡単で、何が難しかったのか、です。\", 0.7),\n (\"v_real\", \"ここは僕自身の話から始めさせてください。\"),\n (\"v_real\", \"僕は自宅の机で、mechArm 270という小さなロボットアームを動かしています。\"),\n (\"v_real\", \"赤いブロックを掴んで、箱に置く。\"),\n (\"trials\", \"それだけの作業を、失敗も含めて650回以上試して、全部台帳に残しています。\", 0.7),\n (\"easy\", \"まず、簡単だったほうから話します。掴めたかどうかの判定です。\"),\n (\"easy\", \"iPhoneで撮った写真をAIに見せて、「ブロックを掴めていますか」と聞くと、\"),\n (\"easy\", \"はい、いいえで答えが返ってくる。この入口は、驚くほど簡単でした。\"),\n (\"easy\", \"ただ、答えが返ることと、毎回正しいことは別です。\"),\n (\"easy\", \"どこまで任せられるかは、失敗例も含めて確かめる必要があります。\", 0.7),\n (\"hard\", \"難しかったのは、動かすほうでした。\", 0.6),\n (\"gap\", \"ロボットアームの爪先がどこに来るかは、普通は関節の角度から計算で出します。\"),\n (\"gap\", \"ところが、ikpyという計算ツールで出した爪先の位置と、\"),\n (\"v_twin\", \"パソコンの中に作ったアームの分身、デジタルツインで動かした位置が、\"),\n (\"gap\", \"僕の設定では、同じ関節の角度なのに、30cm以上食い違ったんです。\"),\n (\"gap\", \"これは、アームが30cm外れた、という意味ではありません。\"),\n (\"gap\", \"計算ツールには、グリッパーの形も、デジタルツインの物理も入っていなくて、爪先の位置の決め方そのものが違っていたんです。\", 0.7),\n (\"v_scan\", \"だから僕は、分身の中でアームを動かして、\"),\n (\"v_scan\", \"関節の角度と爪先の位置の対応表を作りました。\"),\n (\"v_scan\", \"これは計算の表で、実機で全部の点を測ったわけではありません。\", 0.7),\n (\"silent\", \"もう1つ困ったのが、黙る失敗です。\"),\n (\"silent\", \"僕の機体とソフトの組み合わせでは、肩の関節に、90度ちょっとを超える値を送ると、\"),\n (\"silent\", \"エラーが出ないのに、アームが動かないことがありました。\"),\n (\"silent\", \"ログを1行ずつ見て、やっと気づきました。\", 0.7),\n (\"point2\", \"でも、ポイントはここなんです。\"),\n (\"point2\", \"この2つは、AIの学習データが足りない、という話とは別です。\"),\n (\"point2\", \"モデルや制御が、僕の機体と噛み合っているかを確かめる、すり合わせの仕事でした。\"),\n (\"point2\", \"ほかの環境で学んだことを持ってくる研究も進んでいます。\"),\n (\"point2\", \"それでも、僕の構成でちゃんと動くかは、ここで確かめるしかなかったんです。\", 0.6),\n (\"point2\", \"じゃあ、もう1つの手間、データのほうは、業界全体でどうなっているのか。\", 0.9),\n # 7 節3\n (\"c3\", \"3つ目、データの壁はどれくらい高くて、誰がどう越えようとしているのか、です。\", 0.7),\n (\"years\", \"カリフォルニア大学バークレー校のロボット研究者、\"),\n (\"years\", \"ケン・ゴールドバーグさんが、去年、こんな概算を紹介しています。\"),\n (\"years\", \"画像と言葉を扱うAIの学習資料を、\"),\n (\"years\", \"人が読んだり見たりする時間に換算すると、約10万年規模になる。\", 0.7),\n (\"wall\", \"それに比べて、ロボットが手を動かすための、動作と結びついた経験は、集めにくい。\"),\n (\"wall\", \"ゴールドバーグさんは、これを「10万年のデータの壁」と呼んでいます。\"),\n (\"wall\", \"ただ、ロボットにも同じ量が必要だと分かったわけではありません。\"),\n (\"wall\", \"集めにくい、という問題提起なんです。\", 0.6),\n (\"wall\", \"ここで、じゃあロボットにChatGPTの瞬間は当分来ない、\"),\n (\"wall\", \"と言いたくなるじゃないですか。でも、ちょっと待ってほしいんです。\", 0.7),\n (\"gen\", \"アメリカのGeneralistという会社は、人が手で作業する様子を、\"),\n (\"gen\", \"安い装着型の機材で記録して集めています。\"),\n (\"gen\", \"会社の発表では、去年の11月に27万時間、今年の4月に50万時間を超えました。\", 0.6),\n (\"gen2\", \"さらに8月には、簡単で短い作業なら、\"),\n (\"gen2\", \"数秒の実演を見せるだけで、新しい作業をこなす例も発表しています。\"),\n (\"gen2\", \"ただ、本人たちも、作業は簡単で短く、成功率はまだ控えめだと書いています。\"),\n (\"gen2\", \"それでも、「動かすには毎回大量の専用データが必要」とは、言い切れなくなってきました。\", 0.7),\n (\"compare\", \"ここで1つ、注意があります。\"),\n (\"compare\", \"作業を記録した時間と、10万年という数字は、そのまま比べられません。\"),\n (\"compare\", \"何倍足りないかより、いろんな場面の経験を、どれだけ広く集められるかが問題なんです。\", 0.7),\n (\"dirt\", \"つまり、壁はまだ高い。でも、壁の前でみんなが土を積み始めている。\"),\n (\"dirt\", \"1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、\"),\n (\"dirt\", \"Generalistが人の手作業を記録しているのも、同じ壁に向かっている動きなんです。\", 0.7),\n (\"notall\", \"ただ、ここで「なんだ、まだ全然ダメなのか」で\"),\n (\"notall\", \"片付けてしまうのも違うんです。\"),\n (\"notall\", \"写真を見せて判断させる入口は、僕の机の上でも、もう普通に使えています。\", 0.6),\n (\"notall\", \"じゃあ、この2つが混ざっている今、ロボットのニュースの何を見ればいいのか。\", 0.9),\n # 8 見立て\n (\"c4\", \"ここからが、僕の一番言いたいことです。\", 0.7),\n (\"q\", \"ロボットのニュースを見たときの問いが、2つに変わるんですよ。\", 0.6),\n (\"q1\", \"1つは、そのロボットは自律で動いているのか、人が遠隔で動かしているのか、です。\"),\n (\"q1\", \"デモ動画を見るときは、自律か遠隔操作か、\"),\n (\"q1\", \"再生速度や、人が手を出した条件が書かれているかを見ます。\", 0.7),\n (\"q2\", \"もう1つは、その仕事は「見て判断する」で済むのか、\"),\n (\"q2\", \"「触れて動かす」精度が要るのか、です。\"),\n (\"q2\", \"棚の商品が足りているかを見るのは、前者です。\"),\n (\"q2\", \"柔らかい物を潰さずに掴むのは、後者です。\"),\n (\"q2\", \"前者は、手軽に試せるところまで来ています。ただ、どこまで任せられるかは、現場で確かめる必要がある。\"),\n (\"q2\", \"後者は、その現場の機体と環境に合わせて、すり合わせる仕事が、まだ残っています。\", 0.7),\n (\"gofe\", \"ゴールドバーグさんは、昔ながらのエンジニアリングと学習を組み合わせて、\"),\n (\"gofe\", \"現場で役に立つ仕事をしながら、データを集めていくのが近道だ、と言っています。\"),\n (\"gofe\", \"僕の机の上も、AIと従来の制御を組み合わせる点では通じています。\"),\n (\"gofe\", \"ただ、現場で働きながらデータが増える仕組みとは、まだ規模も段階も違います。\", 0.7),\n (\"motion\", \"で、作る側として、もう1つ言わせてください。\"),\n (\"motion\", \"データの話と並行して、もう1つ要るのが「どう動くか」の設計です。\"),\n (\"motion\", \"例えばロボットが人に物を渡すとき、\"),\n (\"motion\", \"何秒待って手を開けば、人は安心して受け取れるのか。\"),\n (\"motion\", \"これは、成功か失敗かの判定だけでは出てこない。\"),\n (\"motion\", \"人が安心できる動きを設計する仕事も、同時に必要なんだと、僕は見ています。\", 0.9),\n # 9 回答\n (\"answer2\", \"じゃあ、冒頭の問いに答えますね。\", 0.4),\n (\"answer2\", \"僕の机では、写真を見せて判断させる入口は、ずっと簡単になりました。\"),\n (\"answer2\", \"一方、物を確実に動かすには、機体や環境とのすり合わせが、まだ必要でした。\"),\n (\"answer2\", \"その負担を減らす研究は、急速に進んでいます。\"),\n (\"answer2\", \"でも、いろんな家で、安全に家事を任せられる転換点まで来たかは、まだ判断できません。\", 0.7),\n (\"action\", \"ロボットのデモ動画を見たら、\"),\n (\"action\", \"「これは自律なのか、何回やって、何回うまくいったのか」を、\"),\n (\"action\", \"一度だけ考えてみてください。\"),\n (\"action\", \"それだけで、同じ動画の見え方が、かなり変わると思います。\", 0.9),\n # 10 アウトロ\n (\"outro\", \"今日はここまでです。\"),\n (\"outro\", \"この動画が気に入ったら、高評価とチャンネル登録をお願いします。\"),\n (\"outro\", \"コメントは全部読ませてもらっています。それではまた。\", 1.2),\n (\"sources\", \"\", 6.0),\n]\n\n# TTS 用の読み(字幕は元の表記のまま)\nREAD = [\n (\"Pixels to Motors\", \"ピクセルズ トゥー モーターズ\"),\n (\"Smart Analytics Global\", \"スマート アナリティクス グローバル\"),\n (\"Counterpoint\", \"カウンターポイント\"),\n (\"ChatGPT\", \"チャットジーピーティー\"),\n (\"Generalist\", \"ジェネラリスト\"),\n (\"mechArm 270\", \"メックアーム ニーナナゼロ\"),\n (\"iPhone\", \"アイフォーン\"),\n (\"ikpy\", \"アイケーパイ\"),\n (\"NEO\", \"ネオ\"),\n (\"1X\", \"ワンエックス\"),\n (\"SAG\", \"エスエージー\"),\n (\"AI\", \"エーアイ\"),\n (\"30cm\", \"さんじゅっセンチ\"),\n (\"爪先\", \"つめさき\"),\n (\"1万9100台\", \"いちまんきゅうせんひゃくだい\"),\n (\"値を送る\", \"あたいを送る\"),\n (\"「\", \"\"), (\"」\", \"\"),\n]\n\n# 間延び対策:ひと息が長い文だけ、読み上げ用に読点を足す(字幕は変えない)\nOVERRIDE = {\n \"同じ半年の出荷を約1万9100台と推計していて、\": \"同じ半年の出荷を、約1万9100台と、推計していて、\",\n \"工場でも家でも、使いながらデータを集める動きが出てきているんですよ。\": \"工場でも家でも、使いながら、データを集める動きが、出てきているんですよ。\",\n \"ほかの環境で学んだことを持ってくる研究も進んでいます。\": \"ほかの環境で学んだことを、持ってくる研究も、進んでいます。\",\n \"ここで、じゃあロボットにChatGPTの瞬間は当分来ない、\": \"ここで、じゃあ、ロボットに、ChatGPTの瞬間は、当分来ない、\",\n \"1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、\": \"1Xが、家の中で、遠隔で手伝いながら、動きを覚えさせているのも、\",\n}\n\ndef reading(t):\n t = OVERRIDE.get(t, t)\n for a, b in READ:\n t = t.replace(a, b)\n return t</code></pre><h3 id=\"index_ShUg2Clt\">tts2.py(読み上げ音声の生成と間延びの検査)</h3><pre><code class=\"language-python\">import subprocess, json, sys, re, wave\nimport numpy as np\nsys.path.insert(0,'.')\nfrom narration import SEGS, reading\nVOICE='/usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice'\nDIC='/var/lib/mecab/dic/open-jtalk/naist-jdic'\nSR=48000\ndef synth(text,base):\n w=base+'.wav'; l=base+'.log'\n subprocess.run(['open_jtalk','-m',VOICE,'-x',DIC,'-r','1.08','-fm','-1','-a','0.55','-jf','1.0','-ow',w,'-ot',l],input=text.encode(),check=True)\n log=open(l,encoding='utf-8',errors='ignore').read()\n ph=[]\n for x in log.split('[Output label]')[1].strip().splitlines():\n m=re.match(r'(\\d+) (\\d+) \\S+?\\^\\S+?-(\\S+?)\\+',x)\n if m: ph.append((int(m.group(1))/1e7,int(m.group(2))/1e7,m.group(3)))\n with wave.open(w) as f: a=np.frombuffer(f.readframes(f.getnframes()),dtype=np.int16)\n inner=[(e-s) for s,e,p in ph[1:-1] if p not in('pau','sil')]\n pron=''.join(x.split(',')[9] for x in log.split('[Text analysis result]')[1].split('[Output label]')[0].splitlines() if x.count(',')>9)\n return a,ph,max(inner) if inner else 0,pron\nout=[]\nfor i,s in enumerate(SEGS):\n scene,text=s[0],s[1]; pause=s[2] if len(s)>2 else 0.35\n d=0.0; note=''\n if text:\n r=reading(text)\n a,ph,worst,pron=synth(r,f'tts/{i:03d}')\n if worst>0.3:\n parts=[p for p in re.split(r'(?<=[、。])',r) if p]\n audio=[]; worst2=0\n for n,p in enumerate(parts):\n b,ph2,w2,_=synth(p,f'tts/{i:03d}_p{n}'); worst2=max(worst2,w2)\n st=int(ph2[0][1]*SR) if n>0 else 0\n en=int(ph2[-1][0]*SR) if n<len(parts)-1 else len(b)\n audio.append(b[st:en])\n if n<len(parts)-1: audio.append(np.zeros(int(0.12*SR),dtype=np.int16))\n a=np.concatenate(audio)\n with wave.open(f'tts/{i:03d}.wav','wb') as f:\n f.setnchannels(1); f.setsampwidth(2); f.setframerate(SR); f.writeframes(a.tobytes())\n note=f'STRETCH {worst:.2f}s -> split {len(parts)} parts, max {worst2:.2f}s'\n print(i,text[:30],note)\n d=len(a)/SR\n if re.search(r'[0-9]|爪先',text): print('READ',i,text[:24],'→',pron[:60])\n out.append(dict(i=i,scene=scene,text=text,dur=d,pause=pause,note=note))\njson.dump(out,open('tts/index.json','w'),ensure_ascii=False,indent=0)\ntot=sum(o['dur']+o['pause'] for o in out)\nprint('segments',len(out),'total %d:%02d'%(tot//60,tot%60))</code></pre><h3 id=\"index_T1WL0zVD\">scenes.py(場面ごとの図解・テロップ・実機映像の指定)</h3><pre><code class=\"language-python\"># -*- coding: utf-8 -*-\nimport base64 as _b64, os as _os\nLOGO='data:image/png;base64,'+_b64.b64encode(open(_os.path.join(_os.path.dirname(_os.path.abspath(__file__)),'logo_dark.png'),'rb').read()).decode()\n# 1920x1080 の各シーン(字幕は別レイヤー)。下端 230px は字幕用に空ける。\n# v2: カード上の太線をやめ、左右は細い縦線で区切る。色は文字色だけで区別する。\nBLUE = \"#1A6C98\" # 見て判断する\nRED = \"#C4471F\" # 触れて動かす/強調\nINK = \"#0D0D0D\"\nSUB = \"#888888\"\nBG = \"#F5F5F0\"\nRULE = \"#D6D6D1\"\n\nCSS = f\"\"\"\n*{{margin:0;padding:0;box-sizing:border-box}}\nhtml,body{{width:1920px;height:1080px;overflow:hidden}}\nbody{{background:{BG};color:{INK};font-family:'Noto Sans CJK JP',sans-serif;}}\n.wrap{{position:absolute;left:140px;right:140px;top:96px;bottom:250px;display:flex;flex-direction:column;justify-content:center}}\n.tag{{position:absolute;left:140px;top:52px;font-size:28px;font-weight:700;color:{SUB};letter-spacing:.04em}}\n.tag b{{color:{RED};margin-right:14px}}\n.brand{{position:absolute;right:140px;top:46px;height:34px}}\n.kicker{{font-size:40px;font-weight:700;color:{SUB};margin-bottom:22px}}\n.h1{{font-size:84px;font-weight:900;line-height:1.25}}\n.h2{{font-size:64px;font-weight:900;line-height:1.3}}\n.h3{{font-size:48px;font-weight:700;line-height:1.4}}\n.p{{font-size:40px;font-weight:500;line-height:1.55;color:{INK}}}\n.muted{{color:{SUB}}}\n.red{{color:{RED}}} .blue{{color:{BLUE}}}\n.hero{{font-size:220px;font-weight:900;line-height:1;letter-spacing:-.02em}}\n.hero small{{font-size:90px;font-weight:900;margin-left:10px}}\n.src{{position:absolute;left:140px;bottom:236px;font-size:24px;color:{SUB}}}\n.row{{display:flex;align-items:stretch}}\n.col{{flex:1;padding:10px 56px 10px 0}}\n.col + .col{{border-left:2px solid {RULE};padding-left:56px}}\n.col .lab{{font-size:34px;font-weight:700;color:{SUB};margin-bottom:14px}}\n.col .big{{font-size:66px;font-weight:900;line-height:1.22}}\n.col .note{{font-size:34px;line-height:1.5;margin-top:16px}}\n.list{{list-style:none}}\n.list li{{font-size:46px;font-weight:700;line-height:1.35;padding:22px 0;border-bottom:2px solid {RULE};display:flex;gap:28px;align-items:baseline}}\n.list li .n{{font-size:44px;font-weight:900;color:{RED};min-width:60px}}\n.list li.dim{{color:{SUB}}}\n.chap{{font-size:200px;font-weight:900;color:{RED};line-height:1}}\n.pill{{align-self:flex-start;display:inline-block;font-size:30px;font-weight:700;padding:8px 22px;border-radius:999px;background:{INK};color:#fff;margin-bottom:26px}}\n.flow{{display:flex;align-items:center;gap:30px}}\n.box{{background:#fff;border-radius:18px;padding:34px 40px;font-size:42px;font-weight:700;text-align:center;flex:1}}\n.arrow{{font-size:64px;color:{SUB};font-weight:900}}\n\"\"\"\n\ndef page(body, tag=None, brand=True):\n t = f'<div class=\"tag\">{tag}</div>' if tag else \"\"\n b = f'<img class=\"brand\" src=\"{LOGO}\">' if brand else \"\"\n return f'<!doctype html><html><head><meta charset=\"utf-8\"><style>{CSS}</style></head><body>{t}{b}{body}</body></html>'\n\nT1 = \"<b>1</b>出荷されたロボットは何をしているのか\"\nT2 = \"<b>2</b>机の上のロボットアームで、簡単だったこと・難しかったこと\"\nT3 = \"<b>3</b>データの壁の高さと、越えようとしている会社\"\nT4 = \"<b>4</b>僕の見立て\"\n\ndef hbar(parts, height=96):\n segs = \"\"\n for lab, pct, col, tc in parts:\n segs += (f'<div style=\"flex:{pct};background:{col};height:{height}px;border-radius:6px;'\n f'display:flex;align-items:center;padding-left:24px;color:{tc};font-size:34px;font-weight:700;white-space:nowrap;overflow:hidden\">{lab}</div>')\n return f'<div style=\"display:flex;gap:4px;width:100%\">{segs}</div>'\n\ndef two(lab1, big1, lab2, big2, c1=\"blue\", c2=\"red\", size=60, note1=\"\", note2=\"\"):\n n1 = f'<div class=\"note\">{note1}</div>' if note1 else \"\"\n n2 = f'<div class=\"note\">{note2}</div>' if note2 else \"\"\n return (f'<div class=\"row\"><div class=\"col\"><div class=\"lab\">{lab1}</div>{n1}<div class=\"big {c1}\" style=\"font-size:{size}px;margin-top:8px\">{big1}</div></div>'\n f'<div class=\"col\"><div class=\"lab\">{lab2}</div>{n2}<div class=\"big {c2}\" style=\"font-size:{size}px;margin-top:8px\">{big2}</div></div></div>')\n\ndef chapter(n, title):\n return page(f'<div class=\"wrap\"><div class=\"chap\">{n}</div><div class=\"h1\" style=\"margin-top:30px\">{title}</div></div>')\n\n\n# --- 強弱の型(2026-10-11 追加)---\n# 打ち出し: 小さい前置き(色)+ 1〜2行の大きな文。1行は10字前後まで\ndef punch(kicker, lines, sub=\"\", kc=\"red\", size=150, tag=None):\n body = \"<br>\".join(lines)\n sb = f'<div class=\"h3 muted\" style=\"margin-top:40px;font-size:44px\">{sub}</div>' if sub else \"\"\n return page(f\"\"\"<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div class=\"{kc}\" style=\"font-size:44px;font-weight:900;letter-spacing:.06em;margin-bottom:28px\">{kicker}</div>\n <div style=\"font-size:{size}px;font-weight:900;line-height:1.22;letter-spacing:-.01em\">{body}</div>{sb}\n</div>\"\"\", tag=tag)\n\n# 章の扉: 「今日の話」→「1つ目」→ 大きなキーワード → 章の問い\ndef chapter2(no, keyword, sub):\n return page(f\"\"\"<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div class=\"muted\" style=\"font-size:40px;font-weight:700;letter-spacing:.1em\">今日の話</div>\n <div style=\"font-size:72px;font-weight:900;margin-top:26px\">{no}</div>\n <div class=\"red\" style=\"font-size:200px;font-weight:900;line-height:1.1;margin-top:6px\">{keyword}</div>\n <div style=\"font-size:48px;font-weight:700;margin-top:26px\">{sub}</div>\n</div>\"\"\")\n\n# 点の並び: 100個の点のうち n 個に色を付ける(割合を数で見せる)\ndef dots(n, kicker, label, big, note=\"\", src=\"\", tag=None):\n cells = \"\".join(f'<span style=\"width:38px;height:38px;border-radius:50%;background:{RED if i < n else \"#D3D3CE\"}\"></span>' for i in range(100))\n return page(f\"\"\"<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div style=\"font-size:48px;font-weight:900;margin-bottom:34px\">{kicker}</div>\n <div style=\"display:flex;align-items:center;gap:56px\">\n <div style=\"display:grid;grid-template-columns:repeat(25,38px);gap:10px\">{cells}</div>\n <div class=\"red\" style=\"font-size:190px;font-weight:900;line-height:1\">{big}</div>\n </div>\n <div style=\"font-size:48px;font-weight:700;margin-top:34px\">{label}</div>\n <div class=\"muted\" style=\"font-size:32px;margin-top:10px\">{note}</div>\n</div>{f'<div class=\"src\">{src}</div>' if src else ''}\"\"\", tag=tag)\n\nS = {}\n\nS[\"hook_ship\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期 人型ロボットの世界出荷(推計)</div>\n <div class=\"hero\">22,000<small>台超</small></div>\n <div class=\"h2\" style=\"margin-top:36px\">前年同期の <span class=\"red\">約4倍</span></div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)</div>\"\"\")\n\nS[\"hook_forecast\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年通年の予想</div>\n <div class=\"hero\">50,000<small>台超</small></div>\n <div class=\"h3\" style=\"margin-top:44px\">家事をする人型ロボットの予約販売も、2025年秋から</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)/1X</div>\"\"\")\n\nS[\"hook_question\"] = page(\"\"\"\n<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div class=\"h1\">ロボットも、ChatGPTのときみたいに<br><span class=\"red\">一気に賢くなる?</span></div>\n</div>\"\"\")\n\nS[\"question\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">今日の問い</div>\n <div class=\"h1\">ロボットにも<span class=\"red\">ChatGPTの瞬間</span>は<br>もう来たのか</div>\n <div class=\"h3 muted\" style=\"margin-top:44px\">ChatGPTの瞬間=専門家でなくても、<br>いろんな仕事にすぐ使えるようになる転換点</div>\n</div>\"\"\")\n\nS[\"define\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">フィジカルAI とは</div>\n <div class=\"h2\">現実の世界を認識して、<span class=\"red\">行動する</span>AI</div>\n <div class=\"row\" style=\"margin-top:56px;gap:30px\">\n <div class=\"box\" style=\"border:4px solid #0D0D0D\">ロボットアーム</div>\n <div class=\"box\" style=\"border:4px solid #0D0D0D\">人型ロボット</div>\n <div class=\"box muted\" style=\"background:transparent;border:2px dashed #D6D6D1\">自動運転<br><span style=\"font-size:30px\">今日は扱わない</span></div>\n </div>\n</div>\"\"\")\n\nS[\"answer\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">先に結論</div>\n {two(\"写真を見せて判断させる入口\", \"ぐっと簡単になった\", \"物を確実に動かすところ\", \"まだ手間がかかる\")}\n <div class=\"h3\" style=\"margin-top:56px\">手間は2つ<br>① <span class=\"red\">動作と結びついたデータ</span>が集めにくい<br>② <span class=\"red\">機体や環境とのすり合わせ</span></div>\n</div>\"\"\")\n\nS[\"stance\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2 muted\" style=\"text-decoration:line-through;text-decoration-thickness:6px\">フィジカルAIはまだ使えない</div>\n <div class=\"h1\" style=\"margin-top:48px\">どこまで来ていて、<br>どこで<span class=\"red\">手間がかかっているのか</span>を、<br>分けて見る</div>\n</div>\"\"\")\n\nS[\"toc\"] = page(\"\"\"\n<div class=\"wrap\">\n <ul class=\"list\">\n <li><span class=\"n\">1</span>出荷が4倍になったロボットは、何をしているのか</li>\n <li><span class=\"n\">2</span>机の上のロボットアームで、何が簡単で何が難しかったか</li>\n <li><span class=\"n\">3</span>データの壁はどれくらい高く、誰がどう越えようとしているか</li>\n <li class=\"dim\"><span class=\"n\">+</span>僕の見立て</li>\n </ul>\n</div>\"\"\")\n\nS[\"c1\"] = chapter(\"1\", \"出荷が4倍になったロボットは、<br>何をしているのか\")\nS[\"c2\"] = chapter(\"2\", \"机の上のロボットアームで、<br>何が簡単で、何が難しかったのか\")\nS[\"c3\"] = chapter(\"3\", \"データの壁はどれくらい高くて、<br>誰がどう越えようとしているのか\")\nS[\"c4\"] = chapter(\"4\", \"僕の見立て\")\n\nS[\"use\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期の人型ロボット出荷の用途(Counterpointの推計)</div>\n {hbar([(\"展示・イベント + 研究・データ生産 6割以上\", 62, RED, \"#fff\"), (\"その他\", 38, \"#D3D3CE\", INK)])}\n <div class=\"h3\" style=\"margin-top:44px\">出荷の <span class=\"red\">6割以上</span> が、<br>動きを見せる用途と、研究・データ生産の用途</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)。帯の長さは「6割以上」を表す目安</div>\"\"\", tag=T1)\n\nS[\"datacollect\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">データ生産 = ロボットが学ぶための、動きの記録を作ること</div>\n <div class=\"row\" style=\"gap:30px\">\n <div class=\"box\">人が遠隔で動かして<br>記録する</div>\n <div class=\"box\">自律で動いた記録を<br>残す</div>\n </div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">※研究向けの台数が、全部データ集めに使われるわけではない</div>\n <div class=\"h3\" style=\"margin-top:20px\">分類に「データ生産」がある = 今の段階を表している</div>\n</div>\"\"\", tag=T1)\n\nS[\"conflict\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">同じ2026年上半期の推計</div>\n {two(\"Counterpoint\", \"見せる用途+研究・データ生産<br>6割以上\", \"Smart Analytics Global(SAG)\", \"産業・商業用途<br>7割超\", c1=\"\", c2=\"\", size=52, note1=\"出荷 22,000台超\", note2=\"出荷 約19,100台\")}\n <div class=\"h3\" style=\"margin-top:46px\">総数も分類も違う → <span class=\"red\">単純な裏表としては比べられない</span></div>\n <div class=\"h3 muted\" style=\"margin-top:10px\">出荷台数 ≠ 自律的に働いている台数</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)/Smart Analytics Global(2026年8月)</div>\"\"\", tag=T1)\n\nS[\"china\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期の人型ロボット出荷のうち、中国メーカーの割合(SAGの推計)</div>\n <div class=\"hero\">97<small>%以上</small></div>\n</div>\n<div class=\"src\">出典:Smart Analytics Global(2026年8月)</div>\"\"\", tag=T1)\n\nS[\"neo\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">1X「NEO」 家事をする人型ロボット(2025年10月 予約開始)</div>\n {two(\"値段と配送\", \"2万ドル:2026年配送予定<br>月499ドル:その後\", \"動き方\", \"最初は基本的な自律機能<br>知らない作業は<br>担当者が遠隔で手伝う\", c1=\"\", c2=\"\", size=46)}\n <div class=\"h3\" style=\"margin-top:40px\">2026年10月11日時点:一般顧客への配送開始の公式発表は<span class=\"red\">確認できず</span></div>\n</div>\n<div class=\"src\">出典:1X 公式の発表・注文ページ(2026年10月11日確認)</div>\"\"\", tag=T1)\n\nS[\"neo_point\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">売っているのは</div>\n <div class=\"flow\">\n <div class=\"box\">ロボット</div><div class=\"arrow\">+</div>\n <div class=\"box\">遠隔で<br>手伝う人</div><div class=\"arrow\">+</div>\n <div class=\"box\" style=\"border:4px solid {RED}\">動きを<br>覚えていく仕組み</div>\n </div>\n</div>\"\"\", tag=T1)\n\nS[\"bridge1\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">工場でも家でも、<br><span class=\"red\">使いながらデータを集める</span>動き</div>\n <div class=\"h2 muted\" style=\"margin-top:56px\">では、物を動かすのは<br>何がそんなに大変なのか</div>\n</div>\"\"\", tag=T1)\n\nS[\"trials\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">赤いブロックを掴んで、箱に置く</div>\n <div class=\"hero\">650<small>回以上</small></div>\n <div class=\"h3\" style=\"margin-top:36px\">失敗も含めた試行回数。全部台帳に記録</div>\n</div>\n<div class=\"src\">PICK→PLACE の通し試行(台帳 pick_place_ledger_v3.csv、2026年6月18日〜7月20日で656回)</div>\"\"\", tag=T2)\n\nS[\"easy\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{BLUE}\">簡単だった:掴めたかの判定</div>\n <div class=\"flow\">\n <div class=\"box\">iPhoneで<br>撮った写真</div><div class=\"arrow\">→</div>\n <div class=\"box\" style=\"flex:1.5\">AIに聞く<br><span style=\"font-size:32px;white-space:nowrap\" class=\"muted\">「ブロックを掴めていますか」</span></div><div class=\"arrow\">→</div>\n <div class=\"box\" style=\"border:4px solid {BLUE}\"><span class=\"blue\">はい</span>/いいえ</div>\n </div>\n <div class=\"h3\" style=\"margin-top:48px\">ただし、<span class=\"red\">答えが返る ≠ 毎回正しい</span></div>\n</div>\"\"\", tag=T2)\n\nS[\"hard\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">難しかった</div>\n <div class=\"h1\">動かすほう</div>\n</div>\"\"\", tag=T2)\n\nS[\"gap\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">僕の設定で、同じ関節の角度を入れたときの爪先の位置</div>\n <div style=\"position:relative;height:400px;margin-top:10px\">\n <div style=\"position:absolute;left:60px;top:250px;right:60px;height:4px;background:{RULE}\"></div>\n <div style=\"position:absolute;left:150px;top:218px;width:68px;height:68px;border-radius:50%;background:#fff;border:8px solid {SUB}\"></div>\n <div style=\"position:absolute;left:4px;top:310px;width:440px;text-align:center;font-size:36px;font-weight:700;color:{SUB}\">計算ツール(ikpy)</div>\n <div style=\"position:absolute;right:150px;top:218px;width:68px;height:68px;border-radius:50%;background:{RED}\"></div>\n <div style=\"position:absolute;right:4px;top:310px;width:440px;text-align:center;font-size:36px;font-weight:700;color:{RED}\">デジタルツイン</div>\n <div style=\"position:absolute;left:184px;right:184px;top:140px;height:4px;background:{INK}\"></div>\n <div style=\"position:absolute;left:182px;top:120px;width:4px;height:44px;background:{INK}\"></div>\n <div style=\"position:absolute;right:182px;top:120px;width:4px;height:44px;background:{INK}\"></div>\n <div style=\"position:absolute;left:0;right:0;top:20px;text-align:center;font-size:72px;font-weight:900\"><span class=\"red\">30cm以上</span> 食い違う</div>\n </div>\n</div>\n<div class=\"h3\" style=\"position:absolute;left:140px;bottom:290px;font-size:36px\">アームが外れた距離ではない。<span class=\"red\">爪先の位置の決め方</span>が違っていた<span class=\"muted\">(計算ツールにはグリッパーの形がない)</span></div>\n<div class=\"src\">図は模式図</div>\"\"\", tag=T2)\n\nS[\"silent\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">黙る失敗</div>\n <div class=\"flow\">\n <div class=\"box\">肩の関節に<br><span class=\"red\">90度ちょっと</span>を超える値</div><div class=\"arrow\">→</div>\n <div class=\"box\">エラーは出ない<br>アームも動かない</div>\n </div>\n <div class=\"h3 muted\" style=\"margin-top:44px\">僕の機体とソフトの組み合わせで起きたこと</div>\n</div>\"\"\", tag=T2)\n\nS[\"point2\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">この2つは</div>\n <div class=\"h2 muted\" style=\"text-decoration:line-through;text-decoration-thickness:5px\">AIの学習データが足りない</div>\n <div class=\"h1\" style=\"margin-top:30px\"><span class=\"red\">すり合わせ</span>の仕事</div>\n <div class=\"h3\" style=\"margin-top:20px\">モデルや制御が、自分の機体と噛み合っているかを確かめる</div>\n</div>\"\"\", tag=T2)\n\nS[\"years\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">画像と言葉を扱うAIの学習資料を、人が読む・見る時間に換算すると</div>\n <div class=\"hero\">約10<small>万年規模</small></div>\n</div>\n<div class=\"src\">出典:Ken Goldberg, Science Robotics(2025年8月, Editorial)</div>\"\"\", tag=T3)\n\nS[\"wall\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"h1\">10万年の<span class=\"red\">データの壁</span></div>\n <div style=\"margin-top:50px\">{two(\"画像と言葉\", \"大量にある\", \"動作と結びついた経験\", \"集めにくい\", size=58)}</div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">※ロボットにも同じ量が必要だと分かったわけではない</div>\n</div>\n<div class=\"src\">出典:Ken Goldberg, Science Robotics(2025年8月)</div>\"\"\", tag=T3)\n\nS[\"gen\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">Generalist が集めた、人の手作業の記録(会社の発表)</div>\n <div style=\"display:flex;flex-direction:column;gap:30px;margin-top:10px\">\n <div style=\"display:flex;align-items:center;gap:30px\"><div style=\"width:260px;font-size:36px;font-weight:700;color:{SUB}\">2025年11月</div>\n <div style=\"width:{int(1060*27/50)}px;height:110px;background:#D3D3CE;border-radius:0 6px 6px 0\"></div><div style=\"font-size:52px;font-weight:900;white-space:nowrap\">27万時間</div></div>\n <div style=\"display:flex;align-items:center;gap:30px\"><div style=\"width:260px;font-size:36px;font-weight:700;color:{SUB}\">2026年4月</div>\n <div style=\"width:1060px;height:110px;background:{RED};border-radius:0 6px 6px 0\"></div><div style=\"font-size:52px;font-weight:900;white-space:nowrap\">50万時間超</div></div>\n </div>\n <div class=\"h3\" style=\"margin-top:44px\">安い装着型の機材で記録</div>\n</div>\n<div class=\"src\">出典:Generalist(GEN-0 2025年11月/GEN-1 2026年4月)</div>\"\"\", tag=T3)\n\nS[\"gen2\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">Generalist GEN-1.5(2026年8月)</div>\n <div class=\"h2\">簡単で短い作業なら、<br><span class=\"red\">数秒の実演</span>で新しい作業をこなす例</div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">本人たちも:作業は簡単で短く、成功率はまだ控えめ</div>\n</div>\n<div class=\"src\">出典:Generalist「GEN-1.5」(2026年8月19日)</div>\"\"\", tag=T3)\n\nS[\"compare\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">作業を記録した時間と、10万年は<br><span class=\"red\">そのまま比べられない</span></div>\n <div class=\"h3\" style=\"margin-top:50px\"><span class=\"muted\">何倍足りないか</span> より <span class=\"red\">どれだけ広く集められるか</span></div>\n</div>\"\"\", tag=T3)\n\nS[\"dirt\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">壁はまだ高い。でも、<br><span class=\"red\">壁の前で土を積み始めている</span></div>\n <ul class=\"list\" style=\"margin-top:30px\">\n <li style=\"font-size:40px\"><span class=\"n\">・</span>1X:家の中で遠隔で手伝いながら、動きを覚えさせる</li>\n <li style=\"font-size:40px\"><span class=\"n\">・</span>Generalist:人の手作業を装着型の機材で記録する</li>\n </ul>\n</div>\"\"\", tag=T3)\n\nS[\"notall\"] = page(f\"\"\"\n<div class=\"wrap\">\n {two(\"写真を見せて判断させる入口\", \"もう普通に使える\", \"物を確実に動かす\", \"すり合わせとデータが<br>まだ要る\", size=56)}\n <div class=\"h3\" style=\"margin-top:48px\">「まだ全然ダメ」でも「もう来た」でもない</div>\n</div>\"\"\", tag=T3)\n\nS[\"q\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h1\">ロボットのニュースを見る<br><span class=\"red\">問いが2つ</span>に変わる</div>\n</div>\"\"\", tag=T4)\n\nS[\"q1\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">問い 1</div>\n <div class=\"h1\">自律で動いているのか、<br>人が遠隔で動かしているのか</div>\n <div class=\"h3 muted\" style=\"margin-top:36px\">デモ動画で見るところ:自律か遠隔か/再生速度/人が手を出した条件</div>\n</div>\"\"\", tag=T4)\n\nS[\"q2\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">問い 2</div>\n <div class=\"h2\" style=\"margin-bottom:40px\">その仕事には、どちらが要るのか</div>\n {two(\"見て判断する\", \"手軽に試せる<br><span style='font-size:34px;color:#888888'>任せる範囲は現場で確かめる</span>\", \"触れて動かす精度\", \"現場の機体と環境に<br>すり合わせる仕事が残る\", size=48, note1=\"例:棚の商品が足りているか見る\", note2=\"例:柔らかい物を潰さずに掴む\")}\n</div>\"\"\", tag=T4)\n\nS[\"gofe\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">ゴールドバーグさんの近道</div>\n <div class=\"flow\">\n <div class=\"box\">昔ながらの工学<br>+学習</div><div class=\"arrow\">→</div>\n <div class=\"box\">現場で役に立つ<br>仕事をする</div><div class=\"arrow\">→</div>\n <div class=\"box\">働きながら<br>データが増える</div>\n </div>\n <div class=\"h3\" style=\"margin-top:48px\">僕の机の上:組み合わせる点は通じる。<span class=\"muted\">規模も段階もまだ違う</span></div>\n</div>\"\"\", tag=T4)\n\nS[\"motion\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">データの話と並行して要るもの</div>\n <div class=\"h1\"><span class=\"red\">どう動くか</span>の設計</div>\n <div class=\"h3\" style=\"margin-top:36px\">ロボットが人に物を渡すとき、<br>何秒待って手を開けば、人は安心して受け取れるのか</div>\n <div class=\"h3 muted\" style=\"margin-top:28px\">成功か失敗かの判定だけでは出てこない</div>\n</div>\"\"\", tag=T4)\n\nS[\"answer2\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">冒頭の問いへの答え</div>\n {two(\"写真を見せて判断させる入口\", \"ずっと簡単になった\", \"物を確実に動かす\", \"すり合わせが<br>まだ必要\", size=58)}\n <div class=\"h3\" style=\"margin-top:44px\">負担を減らす研究は急速に進んでいる。<br>家で家事を任せられる転換点かは、<span class=\"red\">まだ判断できない</span></div>\n</div>\"\"\")\n\nS[\"action\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">ロボットのデモ動画を見たら</div>\n <div class=\"h1\">自律なのか、何回やって、<br><span class=\"red\">何回うまくいった</span>のか</div>\n</div>\"\"\")\n\nS[\"sources\"] = page(\"\"\"\n<div class=\"wrap\" style=\"bottom:90px;justify-content:flex-start;top:120px\">\n <div class=\"h2\" style=\"margin-bottom:30px\">出典</div>\n <div class=\"p\" style=\"font-size:29px;line-height:1.75\">\n 人型ロボットの出荷:Counterpoint Research(2026年8月)/Smart Analytics Global(2026年8月)<br>\n 1X NEO:1X 公式の発表・注文ページ(2026年10月11日確認)<br>\n 10万年のデータの壁:Ken Goldberg, Science Robotics(2025年8月, Editorial)<br>\n Generalist:GEN-0(2025年11月)、GEN-1(2026年4月)、GEN-1.5(2026年8月)の公式発表<br>\n ほかの機体から学ぶ研究の例:Gemini Robotics 1.5 技術報告(Google DeepMind)<br>\n 実機の数字:Pixels to Motors の台帳(pick_place_ledger_v3.csv)、公開記事 DAY257<br>\n <span class=\"muted\">ナレーションは仮音声(Open JTalk/HTS voice nitech_jp_atr503_m001)</span>\n </div>\n</div>\"\"\")\n\n\n# --- 強弱をつける場面の上書き(2026-10-11)---\nS[\"hook_question\"] = punch(\"こういうニュースを見ると\", [\"ロボットも\", \"一気に賢くなる?\"])\nS[\"answer\"] = punch(\"先に結論\", ['見て判断する入口は<span class=\"blue\">簡単に</span>', '物を動かすのは<span class=\"red\">まだ手間</span>'], sub=\"手間は2つ:動作と結びついたデータ/機体や環境とのすり合わせ\", size=110)\nS[\"c1\"] = chapter2(\"1つ目\", \"出荷の中身\", \"出荷が4倍になったロボットは、何をしているのか\")\nS[\"c2\"] = chapter2(\"2つ目\", \"机の上の実感\", \"何が簡単で、何が難しかったのか\")\nS[\"c3\"] = chapter2(\"3つ目\", \"データの壁\", \"どれくらい高くて、誰がどう越えようとしているのか\")\nS[\"c4\"] = chapter2(\"最後に\", \"ニュースの見方\", \"僕の見立て\")\nS[\"china\"] = dots(97, \"2026年上半期の人型ロボット出荷のうち\", \"中国メーカー\", \"97%\", note=\"SAGの推計では「97%以上」\", src=\"出典:Smart Analytics Global(2026年8月)\", tag=T1)\nS[\"point2\"] = punch(\"この2つは\", [\"データ不足じゃない\", '<span class=\"red\">すり合わせ</span>の仕事'], sub=\"モデルや制御が、自分の機体と噛み合っているかを確かめる\", tag=T2)\nS[\"wall\"] = punch(\"ゴールドバーグさんの問題提起\", ['10万年の', '<span class=\"red\">データの壁</span>'], sub=\"動作と結びついた経験は集めにくい(同じ量が必要と分かったわけではない)\", kc=\"muted\", tag=T3)\nS[\"compare\"] = punch(\"ひとつ注意\", ['<span class=\"muted\">何倍足りないか</span>より', '<span class=\"red\">どれだけ広く</span>集めるか'], sub=\"作業を記録した時間と10万年は、そのまま比べられない\", size=110, tag=T3)\nS[\"answer2\"] = punch(\"冒頭の問いへの答え\", ['判断する入口は<span class=\"blue\">来た</span>', '家事を任せるのは<span class=\"red\">まだ</span>'], sub=\"負担を減らす研究は急速に進んでいる\", size=110)\n\n# 動画シーンに重ねるオーバーレイ(透過)\nOVERLAY_CSS = f\"\"\"\n*{{margin:0;padding:0;box-sizing:border-box}}\nhtml,body{{width:1920px;height:1080px;background:transparent;font-family:'Noto Sans CJK JP',sans-serif}}\n.lt{{position:absolute;left:96px;top:80px;background:rgba(245,245,240,.94);padding:28px 40px;border-radius:14px}}\n.lt .a{{height:30px;display:block;margin-bottom:6px}}\n.lt .b{{font-size:60px;font-weight:900;color:{INK};margin-top:4px}}\n.lt .c{{font-size:30px;font-weight:500;color:{SUB};margin-top:6px}}\n.cap{{position:absolute;left:96px;top:80px;background:rgba(13,13,13,.78);color:#fff;font-size:34px;font-weight:700;padding:14px 26px;border-radius:10px}}\n\"\"\"\ndef overlay(body):\n return f'<!doctype html><html><head><meta charset=\"utf-8\"><style>{OVERLAY_CSS}</style></head><body>{body}</body></html>'\n\nO = {\n \"title\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">エイイチ</div><div class=\"c\">デザイナー/ロボティクスの研究・開発</div></div>'),\n \"v_real\": overlay('<div class=\"cap\">実機:mechArm 270-Pi 赤いブロックを掴んで箱に置く</div>'),\n \"v_twin\": overlay('<div class=\"cap\">パソコンの中のアームの分身(Unity のデジタルツイン)</div>'),\n \"v_scan\": overlay('<div class=\"cap\">デジタルツインの中で動かして、関節の角度と爪先の位置の対応表を作る</div>'),\n \"outro\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">高評価・チャンネル登録</div><div class=\"c\">コメントは全部読んでいます</div></div>'),\n}\n\n# 動画シーン: (ソースファイル, 開始秒, 透かし消しの有無)\nVIDEO = {\n \"title\": (\"src/c1.mp4\", 0.0, True),\n \"v_real\": (\"src/c2.mp4\", 22.0, True),\n \"v_twin\": (\"src/c4.mov\", 0.0, False),\n \"v_scan\": (\"src/c3.mov\", 0.0, False),\n \"outro\": (\"src/c1.mp4\", 8.0, True),\n}</code></pre><h3 id=\"index_Na0vVVb3\">render.py(図解・テロップ・字幕を画像にする)</h3><pre><code class=\"language-python\">import json, sys, os, html\nsys.path.insert(0,'.')\nfrom scenes import S, O\nfrom playwright.sync_api import sync_playwright\nidx=json.load(open('tts/index.json'))\nos.makedirs('png/scene',exist_ok=True); os.makedirs('png/ovl',exist_ok=True); os.makedirs('png/sub',exist_ok=True)\nSUBCSS_OLD=\"\"\"*{margin:0;padding:0}html,body{width:1920px;height:1080px;background:transparent;font-family:'Noto Sans CJK JP',sans-serif}\n.s{position:absolute;left:140px;right:140px;bottom:56px;text-align:center;font-size:52px;font-weight:800;line-height:1.38;color:#fff;\n-webkit-text-stroke:9px #0D0D0D;paint-order:stroke fill;word-break:normal;line-break:strict}\"\"\"\nSUBCSS=\"\"\"*{margin:0;padding:0}html,body{width:1920px;height:1080px;background:transparent}\nsvg{position:absolute;left:0;top:0}\n.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}\"\"\"\nonly=sys.argv[1:]\ndef split2(t):\n if len(t)<=29: return html.escape(t)\n mid=len(t)/2; best=None\n for i,ch in enumerate(t[:-1]):\n if ch in '、。': \n if best is None or abs(i+1-mid)<abs(best-mid): best=i+1\n if best is None or max(best,len(t)-best)>31:\n # 読点で割れない場合は真ん中付近の助詞の後ろ\n for d in range(0,12):\n for i in (int(mid)+d,int(mid)-d):\n if 0<i<len(t) and t[i-1] in 'はがをにでとものへてや' and t[i] not in '、。」':\n return html.escape(t[:i])+'<br>'+html.escape(t[i:])\n return html.escape(t)\n return html.escape(t[:best])+'<br>'+html.escape(t[best:])\nwith sync_playwright() as p:\n b=p.chromium.launch(); pg=b.new_page(viewport={'width':1920,'height':1080})\n for k,h in S.items():\n if only and k not in only: continue\n pg.set_content(h); pg.wait_for_timeout(50); pg.screenshot(path=f'png/scene/{k}.png')\n for k,h in O.items():\n if only and k not in only: continue\n pg.set_content(h); pg.screenshot(path=f'png/ovl/{k}.png',omit_background=True)\n if not only:\n for o in idx:\n lines=split2(o['text']).split('<br>')\n fs=52\n svg_lines=''.join(f'<text x=\"960\" text-anchor=\"middle\" class=\"t\">{ln}</text>' for ln in lines)\n pg.set_content(f\"\"\"<!doctype html><html><head><meta charset=\"utf-8\"><style>{SUBCSS}</style></head><body>\n<svg id=\"s\" width=\"1920\" height=\"1080\" viewBox=\"0 0 1920 1080\">{svg_lines}</svg></body></html>\"\"\")\n pg.evaluate(\"\"\"() => {\n const ts=[...document.querySelectorAll('text.t')]; const lh=72; const n=ts.length;\n let fs=52; for (const t of ts){ const w=t.getComputedTextLength(); if (w>1640) fs=Math.min(fs, Math.floor(52*1640/w)); }\n ts.forEach((t,k)=>{ t.style.fontSize=fs+'px'; t.setAttribute('y', 1080-62-(n-1-k)*lh); });\n }\"\"\")\n pg.screenshot(path=f\"png/sub/{o['i']:03d}.png\",omit_background=True)\n b.close()\nmissing=[o['scene'] for o in idx if o['scene'] not in S and o['scene'] not in O]\nprint('missing scenes',sorted(set(missing)))</code></pre><h3 id=\"index_pTtPmPEL\">assemble.py(1文ずつ映像を作り、つないで音声と合わせる)</h3><pre><code class=\"language-python\">import json, subprocess, os, sys, wave\nimport numpy as np\nfrom concurrent.futures import ThreadPoolExecutor\nfrom PIL import Image\nsys.path.insert(0,'.')\nfrom scenes import VIDEO\nFPS=30; SR=48000\nidx=json.load(open('tts/index.json'))\nos.makedirs('chunks',exist_ok=True); os.makedirs('comp',exist_ok=True)\n# 尺(フレーム単位)\nfor o in idx:\n o['frames']=max(1,round((o['dur']+o['pause'])*FPS))\n# 動画シーンのオフセット(同じシーンが続く間は続きから)\nprev=None; off=0.0\nfor o in idx:\n sc=o['scene']\n if sc in VIDEO:\n if sc!=prev: off=VIDEO[sc][1]\n o['voff']=off; off+=o['frames']/FPS\n prev=sc\nENC=['-c:v','libx264','-preset','veryfast','-crf','20','-pix_fmt','yuv420p','-r',str(FPS),'-an']\ndef job(o):\n out=f\"chunks/{o['i']:03d}.mp4\"\n if os.path.exists(out) and '--force' not in sys.argv: return\n sub=f\"png/sub/{o['i']:03d}.png\"; sc=o['scene']; n=str(o['frames'])\n if sc in VIDEO:\n src,_,wm=VIDEO[sc]\n dl='delogo=x=2740:y=1840:w=1010:h=220,' if wm else ''\n fc=(f\"[0:v]{dl}scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080,fps={FPS},setsar=1,\"\n f\"drawbox=x=0:y=850:w=iw:h=230:color=black@0.5:t=fill[b];[b][1:v]overlay[c];[c][2:v]overlay[v]\")\n cmd=['ffmpeg','-v','error','-y','-ss',f\"{o['voff']:.3f}\",'-stream_loop','-1','-i',src,\n '-loop','1','-i',f'png/ovl/{sc}.png','-loop','1','-i',sub,'-filter_complex',fc,'-map','[v]','-frames:v',n]+ENC+[out]\n else:\n comp=f\"comp/{o['i']:03d}.png\"\n im=Image.open(f'png/scene/{sc}.png').convert('RGBA'); im.alpha_composite(Image.open(sub).convert('RGBA')); im.convert('RGB').save(comp)\n cmd=['ffmpeg','-v','error','-y','-loop','1','-framerate',str(FPS),'-i',comp,'-frames:v',n,'-tune','stillimage']+ENC+[out]\n subprocess.run(cmd,check=True)\nwith ThreadPoolExecutor(2) as ex: list(ex.map(job,idx))\n# 音声:各文を尺ちょうどに合わせて連結\nparts=[]\nfor o in idx:\n need=o['frames']*SR//FPS\n if o['text']:\n with wave.open(f\"tts/{o['i']:03d}.wav\") as w:\n a=np.frombuffer(w.readframes(w.getnframes()),dtype=np.int16)\n else: a=np.zeros(0,dtype=np.int16)\n a=a[:need]; parts.append(np.concatenate([a,np.zeros(need-len(a),dtype=np.int16)]))\nwith wave.open('narration.wav','wb') as w:\n w.setnchannels(1); w.setsampwidth(2); w.setframerate(SR); w.writeframes(np.concatenate(parts).tobytes())\nopen('chunks/list.txt','w').write(''.join(f\"file '{o['i']:03d}.mp4'\\n\" for o in idx))\n# 字幕 SRT\ndef ts(f):\n ms=round(f/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}\"\nt=0; srt=[]; k=1\nfor o in idx:\n if o['text']:\n srt.append(f\"{k}\\n{ts(t)} --> {ts(t+round(o['dur']*FPS))}\\n{o['text']}\\n\"); k+=1\n t+=o['frames']\nopen('out.srt','w',encoding='utf-8').write('\\n'.join(srt))\njson.dump(idx,open('timeline.json','w'),ensure_ascii=False)\nsubprocess.run(['ffmpeg','-v','error','-y','-f','concat','-safe','0','-i','chunks/list.txt','-i','narration.wav',\n '-map','0:v','-map','1:a','-c:v','copy','-af','loudnorm=I=-16:TP=-1.5:LRA=11','-c:a','aac','-b:a','192k','-ar','48000','-movflags','+faststart','out.mp4'],check=True)\nprint('total frames',t,'=',t/FPS,'s')</code></pre><h3 id=\"index_vy52HFp6\">analyze.py(最初の版で、間延びした文を見つけたスクリプト)</h3><pre><code class=\"language-python\">import json,re,subprocess\nidx=json.load(open('tts/index.json'))\nrows=[]\nfor o in idx:\n if not o['text']: continue\n log=open('tts/%03d.log'%o['i'],encoding='utf-8',errors='ignore').read()\n lab=log.split('[Output label]')[1].strip().splitlines()\n ph=[]\n for l in lab:\n m=re.match(r'(\\d+) (\\d+) \\S+?\\^\\S+?-(\\S+?)\\+',l)\n if m: ph.append((int(m.group(1))/1e7,int(m.group(2))/1e7,m.group(3)))\n scale=o['dur']/ph[-1][1]\n lead=ph[0][1]-ph[0][0]; tail=ph[-1][1]-ph[-1][0]\n longv=[(p,round((e-s)*scale,2)) for s,e,p in ph[1:-1] if p not in('pau','sil') and (e-s)*scale>0.22]\n pau=[round((e-s)*scale,2) for s,e,p in ph if p=='pau' and (e-s)*scale>0.35]\n gap=round((tail)*scale+o['pause'],2)\n rows.append((o['i'],o['text'][:30],round(lead*scale,2),round(tail*scale,2),o['pause'],longv,pau))\nprint('scale ex',scale)\nfor r in rows:\n if r[5] or r[6] or r[3]+r[4]+0.25>1.3:\n print(r)</code></pre><p></p><h2 id=\"index_c3VMNBPy\">実行ログ</h2><h3 id=\"index_U2U1WMpc\">台帳の試行回数を数えた結果</h3><pre><code>$ cd $HOME/mnt/runs; for f in pick_place_ledger_v3.csv handover_ledger_v1.csv; do echo \"== $f\"; python3 - \"$f\" <<'EOF'\nimport csv,sys,collections\np=sys.argv[1]\ntry:\n rows=list(csv.DictReader(open(p,encoding='utf-8-sig')))\nexcept Exception as e:\n print(\"ERR\",e); sys.exit()\nprint(\"rows\",len(rows))\ncols=rows[0].keys() if rows else []\nfor c in [\"place_verdict\",\"pick\",\"pick_stage\",\"status\",\"run_status\",\"verdict\"]:\n if c in cols:\n print(c, collections.Counter(r[c] for r in rows).most_common(8))\nfor c in [\"run_stem\",\"timestamp\"]:\n if c in cols and rows:\n print(c, rows[0][c], \"->\", rows[-1][c])\nEOF\ndone\n== pick_place_ledger_v3.csv\nrows 657\nplace_verdict [('', 259), ('yes', 217), ('no', 127), ('unknown', 54)]\nrun_stem run_0042_test -> run_0001_20260720_213323\ntimestamp 2026-06-18T00:00:00 -> 2026-07-20T21:35:11.608053\n== handover_ledger_v1.csv\nrows 8\nrun_stem run_handover_20260720_210544_315479_001 -> run_handover_20260720_210618_993522_008\ntimestamp 2026-07-20T21:05:45.451732+09:00 -> 2026-07-20T21:06:20.009752+09:00\n\n$ cd $HOME/mnt/runs && python3 - <<'EOF'\nimport csv,collections\nrows=list(csv.DictReader(open('pick_place_ledger_v3.csv',encoding='utf-8-sig')))\nts=sorted(r['timestamp'] for r in rows if r.get('timestamp'))\nprint(\"min\",ts[0],\"max\",ts[-1])\ntest=[r for r in rows if any(k in r['run_stem'].lower() for k in ('test','dry','sim','mock'))]\nprint(\"test-like\",len(test))\nreal=[r for r in rows if r not in test]\nprint(\"real\",len(real), collections.Counter(r['place_verdict'] for r in real))\nprint([c for c in rows[0].keys()][:12])\nEOF\nls run_pick_place | wc -l\nmin 2026-06-18T00:00:00 max 2026-07-20T21:35:11.608053\ntest-like 1\nreal 656 Counter({'': 259, 'yes': 216, 'no': 127, 'unknown': 54})\n['timestamp', 'run_stem', 'trial_no', 'pick_verdict', 'pick_reason', 'pick_confidence', 'place_verdict', 'place_reason', 'place_confidence', 'block_pick_x', 'block_pick_z', 'block_place_x']\n758</code></pre><h3 id=\"index_a4QOCEhV\">間延びした文の検出結果(最初の版)</h3><pre><code>$ python3 analyze.py | python3 -c \"\nimport sys,ast\nfor line in sys.stdin:\n if line.startswith('('):\n r=ast.literal_eval(line)\n if r[5]: print(r[0], r[1], r[5][:8])\n\"\n13 現実の世界で物に触れて動かす技術のことなんですよ。 [('N', 0.47), ('d', 0.47), ('e', 0.47), ('s', 0.47), ('U', 0.41), ('y', 0.46), ('o', 0.47)]\n88 ChatGPTのようなAIの学習に使われたインターネットの文 [('ts', 0.48), ('U', 0.49), ('k', 0.46), ('a', 0.48), ('w', 0.49), ('a', 0.49), ('r', 0.48), ('e', 0.49)]\n92 ChatGPTは10万年分の教科書で育ったけれど、 [('s', 0.48), ('o', 0.44), ('d', 0.48), ('a', 0.48), ('cl', 0.48), ('t', 0.48), ('a', 0.48), ('k', 0.48)]\n94 ここで、じゃあロボットにChatGPTの瞬間は当分来ない、 [('t', 0.47), ('o', 0.48), ('o', 0.47), ('b', 0.48), ('u', 0.47), ('N', 0.45), ('k', 0.47), ('o', 0.48)]\n109 1Xが家の中で遠隔操作しながらデータを集めているのも、 [('i', 0.47), ('r', 0.42), ('u', 0.47), ('n', 0.46), ('o', 0.47), ('m', 0.47), ('o', 0.47)]\n110 出荷されたロボットの多くがデータ集めに回っているのも、 [('n', 0.45), ('o', 0.45), ('m', 0.45), ('o', 0.45)]</code></pre><h3 id=\"index_H1VyBuua\">6文を区切って作り直した結果</h3><pre><code>$ python3 fixtts.py 13 88 92 94 109 110\n13 ['現実の世界で物に、', '触れて動かす技術のことなんですよ。'] max phoneme 0.11s dur 7.25 -> 4.62\n88 ['チャットジーピーティーのようなエーアイの、', '学習に使われたインターネットの文章を、'] max phoneme 0.14s dur 17.55 -> 5.45\n92 ['チャットジーピーティーは、', '10万年分の教科書で育ったけれど、'] max phoneme 0.14s dur 9.71 -> 4.49\n94 ['ここで、', 'じゃあロボットにチャットジーピーティーの、', '瞬間は当分来ない、'] max phoneme 0.13s dur 9.59 -> 5.01\n109 ['ワンエックスが家の中で遠隔操作しなが、', 'らデータを集めているのも、'] max phoneme 0.17s dur 7.80 -> 5.02\n110 ['出荷されたロボットの多くが、', 'データ集めに回っているのも、'] max phoneme 0.15s dur 5.72 -> 4.38\n\n(109 が「しなが/ら」で切れたため、区切る位置を手で指定して作り直した)\n$ python3 fixtts.py 13 109\n13 ['現実の世界で、', '物に触れて動かす技術のことなんですよ。'] max phoneme 0.11s dur 4.62 -> 4.59\n109 ['ワンエックスが、', '家の中で遠隔操作しながら、', 'データを集めているのも、'] max phoneme 0.17s dur 5.02 -> 5.29</code></pre><h3 id=\"index_qk7nYpvW\">台本の書き直し後に、全文を検査して作り直した結果</h3><pre><code>$ python3 tts2.py (台本の書き直し後の1回目。読点の追加なし)\nREAD 1 世界で2万2000台を超えました。前の年の同じ時 → セカイデニマンニセンダイヲコエマシ’タ、マエノトシノオナジジキノ、オヨソヨンバイデス’、\nREAD 2 年末までには、5万台を超えると予想されています。 → ネンマツマデニワ、ゴマンダイヲコエルトヨソーサレテイマス’、\nREAD 7 僕は秋田で29年デザイナーをやりながら、 → ボクワアキ’タデニジューキューネンデザイナーヲヤリナガラ、\nREAD 18 その手間は大きく2つあります。 → ソノテマワオーキ’クフ’タツアリマス’、\nREAD 22 大きく3つあります。 → オーキ’クミッツアリマス’、\nREAD 23 出荷が4倍になったロボットは、何をしているのか。 → シュッカガヨンバイニナッタロボットワ、ナニヲシ’テイルノカ、\nREAD 27 1つ目、出荷が4倍になったロボットは、何をしてい → ヒ’トツメ、シュッカガヨンバイニナッタロボットワ、ナニヲシ’テイルノカ、デス’、\nREAD 28 Counterpointの推計では、出荷された人 → カウンターポイントノスイケーデワ、シュッカサレタヒ’トガタロボットノロクワリイジョーガ、\n36 同じ半年の出荷を約1万9100台と推計していて、 STRETCH 0.47s -> split 1 parts, max 0.47s\nREAD 36 同じ半年の出荷を約1万9100台と推計していて、 → オナジハントシノシュッカヲヤクイチマンキューセンヒャクダイトスイケーシ’テイテ、\nREAD 37 産業・商業用途が7割を超えた、としています。 → サンギョー、ショーギョーヨートガナナワリヲコエタ、トシ’テイマス’、\nREAD 38 総数も分類も違うので、この2つは、単純な裏表とし → ソースーモブンルイモチガウノデ、コノフ’タツワ、タンジュンナウラオモテトシ’テワクラベラレマセン、\nREAD 40 もう1つ、SAGの推計では、出荷の97%以上が中 → モーヒ’トツ、エスエージーノスイケーデワ、シュッカノキュージューナナパーセントイジョーガチューゴクノメーカーデス’、\nREAD 42 アメリカの1Xという会社は、家事をする人型ロボッ → アメリカノワンエック’ストイウカイシャワ、カジヲスルヒ’トガタロボット、ネオノヨヤクヲ、キョネンノジューガツニハジメマシ\nREAD 43 2万ドルの早期購入枠は、今年配送の予定で、月49 → ニマンドルノソーキ’コーニューワクワ、コトシ’ハイソーノヨテーデ、ツ’キヨンヒャク’キュージューキュードルノプランワ、ソ\nREAD 45 予約した時間に、1Xの担当者が遠隔で手伝う仕組み → ヨヤクシ’タジカンニ、ワンエック’スノタントーシャガエンカクデテツダウシ’クミナンデス’ヨ、\nREAD 46 10月11日の時点で、一般のお客さんへの配送開始 → ジューガツジューイチニチノジテンデ、イッパンノオキャク’サンエノハイソーカイシヲシメス’コーシ’キハッピョーワ、\n51 工場でも家でも、使いながらデータを集める動きが出てきているん STRETCH 0.47s -> split 2 parts, max 0.48s\nREAD 54 2つ目、僕の机の上のロボットアームで、何が簡単で → フ’タツメ、ボクノツ’クエノウエノロボットアームデ、ナニガカンタンデ、ナニガムズカシカッタノカ、デス’、\nREAD 56 僕は自宅の机で、mechArm 270という小さ → ボクワジタクノツ’クエデ、メックアーム、ニーナナゼロトイウチーサナロボットアームヲウゴカシ’テイマス’、\nREAD 58 それだけの作業を、失敗も含めて650回以上試して → ソレダケノサギョーヲ、シッパイモフ’クメテロッピャクゴジュッカイイジョータメシテ、ゼンブダイチョーニノコシ’テイマス’、\nREAD 65 ロボットアームの爪先がどこに来るかは、普通は関節 → ロボットアームノツメサキガドコニクルカワ、フ’ツーワカンセツノカクドカラケーサンデダシマス’、\nREAD 66 ところが、ikpyという計算ツールで出した爪先の → トコロガ、アイケーパイトイウケーサンツールデダシ’タツメサキノイチ’ト、\nREAD 68 僕の設定では、同じ関節の角度なのに、30cm以上 → ボクノセッテーデワ、オナジカンセツノカクドナノニ、サンジュッセンチイジョークイチガッタンデス’、\nREAD 70 関節の角度と爪先の位置の対応表を作りました。 → カンセツノカクドトツメサキノイチノタイオーヒョーヲツ’クリマシ’タ、\nREAD 72 もう1つ困ったのが、黙る失敗です。 → モーヒ’トツ’コマッタノガ、ダマルシッパイデス’、\nREAD 73 僕の機体とソフトの組み合わせでは、肩の関節に、9 → ボクノキ’タイトソフ’トノクミアワセデワ、カタノカンセツニ、キュージュードチョットヲコエルネヲオクルト、\nREAD 75 ログを1行ずつ見て、やっと気づきました。 → ログヲイチギョーズツミテ、ヤットキズキマシ’タ、\nREAD 77 この2つは、AIの学習データが足りない、という話 → コノフ’タツワ、エーアイノガク’シューデータガタリナイ、トイウハナシトワベツデス’、\n79 ほかの環境で学んだことを持ってくる研究も進んでいます。 STRETCH 0.48s -> split 1 parts, max 0.48s\nREAD 81 じゃあ、もう1つの手間、データのほうは、業界全体 → ジャー、モーヒ’トツノテマ、データノホーワ、ギョーカイゼンタイデドーナッテイルノカ、\nREAD 82 3つ目、データの壁はどれくらい高くて、誰がどう越 → ミッツメ、データノカベワドレクライタカク’テ、ダレガドーコエヨートシ’テイルノカ、デス’、\nREAD 86 人が読んだり見たりする時間に換算すると、約10万 → ヒ’トガヨンダリミタリスルジカンニカンサンスルト、ヤクジューマンネンキボニナル、\nREAD 88 ゴールドバーグさんは、これを「10万年のデータの → ゴールドバーグサンワ、コレヲジューマンネンノデータノカベトヨンデイマス’、\n91 ここで、じゃあロボットにChatGPTの瞬間は当分来ない、 STRETCH 0.48s -> split 2 parts, max 0.48s\nREAD 95 会社の発表では、去年の11月に27万時間、今年の → カイシャノハッピョーデワ、キョネンノジューイチガツニニジューナナマンジカン、コトシノシガツニゴジューマンジカンヲコエマシ\nREAD 96 さらに8月には、簡単で短い作業なら、 → サラニハチガツニワ、カンタンデミジカイサギョーナラ、\nREAD 100 ここで1つ、注意があります。 → ココデヒ’トツ、チューイガアリマス’、\nREAD 101 作業を記録した時間と、10万年という数字は、その → サギョーヲキロクシ’タジカント、ジューマンネントイウスージワ、ソノママクラベラレマセン、\n104 1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、 STRETCH 0.47s -> split 1 parts, max 0.47s\nREAD 104 1Xが家の中で遠隔で手伝いながら動きを覚えさせて → ワンエック’スガイエノナカデエンカクデテツダイナガラウゴキヲオボエサセテイルノモ、\nREAD 109 じゃあ、この2つが混ざっている今、ロボットのニュ → ジャー、コノフ’タツガマザッテイルイマ、ロボットノニュースノナニヲミレバイイノカ、\nREAD 111 ロボットのニュースを見たときの問いが、2つに変わ → ロボットノニュースヲミタトキノトイガ、フ’タツニカワルンデス’ヨ、\nREAD 112 1つは、そのロボットは自律で動いているのか、人が → ヒ’トツワ、ソノロボットワジリツデウゴイテイルノカ、ヒ’トガエンカクデウゴカシ’テイルノカ、デス’、\nREAD 115 もう1つは、その仕事は「見て判断する」で済むのか → モーヒ’トツワ、ソノシゴトワミテハンダンスルデスムノカ、\nREAD 125 で、作る側として、もう1つ言わせてください。 → デ、ツ’クルガワトシ’テ、モーヒ’トツイワセテクダサイ、\nREAD 126 データの話と並行して、もう1つ要るのが「どう動く → データノハナシトヘーコーシ’テ、モーヒ’トツイルノガドーウゴク’カノセッケーデス’、\nsegments 144 total 13:45\n\n(どの音で伸びているかを調べた結果。* は0.3秒を超えた音)\ntts/036.log sil* o n a j i h a N t o sh i n o sh u cl k a o y a k u i ch i m a N ky u u s e N hy a k u d a i t o s u i k e e sh* I* t* e* i* t* e* sil*\ntts/051_p1.log sil ts U k a i n a g a r a d e e t a o a ts u m e r u u g o k i g a d e t e k i t e i r u N* d* e* s* U* y* o* sil*\ntts/079.log sil h o k a n o k a N ky o o d e m a n a N d a k o t o o m o cl t e k u r u k e N ky u u m o s* u* s* u* N* d* e* i* m* a* s* U* sil*\ntts/091_p1.log sil* j a a r o b o cl t o n i ch a cl t o j i i p i i t i i n o sh u N k a N w a t* o* o* b* u* N* k* o* n* a* i* sil*\ntts/104.log sil* w a N e cl k U s u g a i e n o n a k a d e e N k a k u d e t e ts u d a i n a g a r a u g o k i o o b o e s a s e t e i* r* u* n* o* m* o* sil*\n\n(5文の OVERRIDE に読点を足し、「値を送る → あたいを送る」を読み替え表に足して作り直した)\n$ python3 tts2.py | grep -v ^READ\nsegments 144 total 13:32\n[] ← 間延びのあった文の一覧(0件)\nボクノキ’タイトソフ’トノクミアワセデワ、カタノカンセツニ、キュージュードチョットヲコエルアタイヲオクルト、\nオナジハントシノシュッカヲ、ヤクイチマンキューセンヒャクダイト、スイケーシ’テイテ、\nロボットアームノツメサキガドコニクルカワ、フ’ツーワカンセツノカクドカラケーサンデダシマス’、</code></pre><h3 id=\"index_sWdoCk8D\">色の検証(validate_palette.js)</h3><pre><code>$ node scripts/validate_palette.js \"#2A7A9B,#D9542B\" --mode light --surface \"#F4F2ED\"\nPalette (light, surface #F4F2ED, categorical): 2 slots\n [PASS] Lightness band all 2 inside L 0.43–0.77\n [FAIL] Chroma floor below floor (reads gray): [[\"#2A7A9B\",0.091]]\n [PASS] CVD separation worst adjacent #D9542B↔#2A7A9B ΔE 15.7 (protan) · tritan 32.3\n [PASS] Normal-vision floor worst adjacent #D9542B↔#2A7A9B ΔE 27.5 (normal)\n [PASS] Contrast vs surface all 2 >= 3:1\n\n → FAILED — fix the marked checks (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)\n scope: categorical palettes only. For a lone status/text color check WCAG text contrast; for a sequential ramp, lightness monotonicity.\n\n$ node scripts/validate_palette.js \"#1F7FB0,#D9542B\" --mode light --surface \"#F4F2ED\"\n [PASS] Lightness band all 2 inside L 0.43–0.77\n [PASS] Chroma floor all 2 >= 0.1\n [PASS] CVD separation worst adjacent #D9542B↔#1F7FB0 ΔE 18.7 (protan) · tritan 32.3\n [PASS] Normal-vision floor worst adjacent #D9542B↔#1F7FB0 ΔE 28.9 (normal)\n [PASS] Contrast vs surface all 2 >= 3:1\n → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)\n\n(記事サムネイルの配色に合わせたあと)\n$ node scripts/validate_palette.js \"#1A6C98,#C4471F\" --mode light --surface \"#F5F5F0\"\n [PASS] Lightness band all 2 inside L 0.43–0.77\n [PASS] Chroma floor all 2 >= 0.1\n [PASS] CVD separation worst adjacent #C4471F↔#1A6C98 ΔE 17.2 (protan) · tritan 30.4\n [PASS] Normal-vision floor worst adjacent #C4471F↔#1A6C98 ΔE 27.2 (normal)\n [PASS] Contrast vs surface all 2 >= 3:1\n → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)</code></pre><h3 id=\"index_YpOeZt_F\">書き出しの結果</h3><pre><code>(試作1本目・153文)\ntotal frames 24385 = 812.8333333333334 s\nstream|codec_name=h264|width=1920|height=1080\nstream|codec_name=aac\nformat|duration=812.900000\n\n(間延びした6文を直したあと)\ntotal frames 23532 = 784.4 s\nvideo,784.400000\naudio,784.400000\n\n(codex の指摘・デザイン・読み・自己紹介を反映したあと・144文)\ntotal frames 24448 = 814.9333333333333 s\nvideo,814.933333\naudio,815.000000\n\n(記事サムネイルの配色・ロゴ・「毎日」の削除を反映したあと)\ntotal frames 24434 = 814.4666666666667 s\n\n(30cm の説明2文を足したあと・146文)\ntotal frames 24891 = 829.7 s\n\n(打ち出し・章の扉・点の並びを入れたあと)\ntotal frames 24891 = 829.7 s\n-rw-r--r-- 1 root root 47663796 Oct 11 22:01 /tmp/claude-0/-home-claude/9041d877-fbc0-57ad-97c5-f63dfd4877c5/scratchpad/v/out.mp4\n</code></pre><h3 id=\"index_ImJl488D\">KIOXIA への転送で、1つ目の断片だけが化けた記録</h3><pre><code>(作業環境側で12MBずつに分けた断片の md5)\nb3a5e3da6efb449d63049eb14400a1d3 p_00\nbff9ad51719a351b1698a90a11152082 p_01\n32d70b67218b8e9594637b6e06dadc7b p_02\nc41f4eedb61bdd951e2484d85e6f6691 p_03\n元の作業用 mp4: a54a7d2fa84886ceab3237618ffd5838\n\n(Mac 側に届いた断片の md5。p_00 だけ一致しない)\nda313675885a59bd4988dbe2ce6270ee p_00\nbff9ad51719a351b1698a90a11152082 p_01\n32d70b67218b8e9594637b6e06dadc7b p_02\nc41f4eedb61bdd951e2484d85e6f6691 p_03\n-rwx------ 12588787 Oct 11 12:44 p_00 ← 元は 12582912 バイト(5,875 バイト多い)\n-rwx------ 12582912 Oct 11 12:44 p_01\n-rwx------ 12582912 Oct 11 12:45 p_02\n-rwx------ 9186276 Oct 11 12:45 p_03\nつないだ mp4: e5e65bee0bd4cf47694b33163d54adc3 ← 元と一致しない\n\n(p_00 だけ gzip で包んで送り直し、Mac 側で展開してからつないだ)\n12582912 b3a5e3da6efb449d63049eb14400a1d3 ← 展開した p_00。元と一致\na54a7d2fa84886ceab3237618ffd5838 ← つないだ作業用 mp4。元と一致\n\n(最終版でも同じ手順で送った)\n元の作業用 mp4: 2dd0e053c2da238bbd073bb7bf8b12ad\nつないだ作業用 mp4: 2dd0e053c2da238bbd073bb7bf8b12ad</code></pre><p></p><h2 id=\"index_o0bVwuXE\">codex への検証依頼の全文</h2><p>台本の全文は、検証にかけた時点の版です(このあと書き直しています)。</p><pre><code class=\"language-markdown\"># 敵対的検証依頼:YouTube台本「ロボットにもChatGPTの瞬間はもう来たのか」\n\n## あなたへの依頼\n\nこれから公開する解説動画(約13分)の台本を、**この台本を論破するつもりで**検証してください。褒める必要はありません。見落としを1つでも多く見つけることが目的です。\n\n- 台本の書き手:Pixels to Motors(エイイチ)。秋田でロボットアーム(mechArm 270-Pi)を自宅で動かし、記録を公開している個人。29年のデザイナー経験。\n- 視聴者:フィジカルAIに関心はあるが専門家ではない日本語話者\n- 台本の結論:「見て判断する」ところは安くなった(ChatGPTの瞬間がもう来た)。「触れて動かす」ところはまだ高い(データの壁があり、まだ来ていない)\n- 検証日:2026年10月11日。Webを参照できる場合は一次情報まで遡って確認してください。参照できない場合は「未確認」と明記し、推測で OK を出さないでください\n\n## 検証してほしい観点\n\n### A. 事実と数字\nA1. 各数字が出典に実際に書かれているか。出典の数字を取り違えていないか(期間・対象・単位)\nA2. 出典自体の信頼性。調査会社のプレスリリース、企業の自社発表、二次報道の区別がついているか\nA3. 2026年10月11日時点で古くなっている情報がないか(特に 1X NEO の出荷状況、Generalist の最新発表、出荷台数)\nA4. 本人の実機の数字(S061 の650回以上、S068〜S071 の30cm以上、S074 の170万点、S076〜S077 の90度ちょっと)が、話の文脈で誤解を生まない言い方になっているか\n\n### B. 論理\nB1. 結論(S143〜S144)「見て判断する部分にはもう来ている」は、台本の中で示した根拠(本人の判定の体験談1つ)だけで言い切れるか。反例はないか\nB2. 「データの壁」の説明(S105〜S108:10万年 vs 57年、約1700分の1)は比較として成立しているか。台本の断り書き(単位をそろえた比較ではない)で十分か\nB3. (S028〜S039)「出荷ロボットの6割以上が見せる用途・研究・データ集め」と「SAG では工場・店舗向けが7割超」を並べた扱いは公正か。どちらかに寄せた印象操作になっていないか\nB4. 本人の机の上の体験(ikpy のずれ、黙る失敗)から、業界全体の「触れて動かすはまだ高い」へ一般化してよいか。飛躍がないか\nB5. 「人型ロボット」の話と「ロボットアーム」の話、「フィジカルAI」全体(自動運転を含む)の話が混ざって、範囲がぶれていないか\n\n### C. 抜けている反論・視点\nC1. 台本の主張に対して、専門家なら真っ先に出す反論は何か(例:シミュレーション合成データ、世界モデル、VLA の汎化など)\nC2. 触れずに済ませているが、視聴者の判断を変えうる事実はないか\n\n### D. 言い方\nD1. 断定が強すぎる文、逆に曖昧すぎて意味のない文\nD2. 誤解を生む用語の使い方(例:「フィジカルAI」「データ集め」「ChatGPTの瞬間」の定義)\nD3. 特定の企業・人物について、不公平・不正確な描写\n\n## 回答の書式\n\n指摘ごとに次の形で書いてください。重大なものから順に並べてください。\n\n```\n[❌ NG / ⚠️ 要注意] 文番号(例:S043)\n問題:何が問題か(1〜2文)\n根拠:確認した出典(URL)または推論の筋道\n修正案:差し替える文(台本の話し言葉の調子に合わせて)\n```\n\n最後に次の3点をまとめてください。\n1. このまま公開してよいか(はい/直してから/大きく作り直すべき)\n2. 公開前に必ず直すべき指摘の文番号\n3. 台本の結論そのものへの評価(成立している/条件つきで成立/成立していない)と、その理由\n\n---\n\n## 台本が使った出典\n\n| 主張 | 文番号 | 出典 |\n|---|---|---|\n| 2026年上半期の人型ロボット出荷2万2000台超・前年同期の約4倍・通年5万台超の予想・用途の6割以上が見せる用途と研究・データ集め | S001〜S005, S028〜S029 | Counterpoint Research(2026年8月)https://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026 ※台本作成時はページ本文を読み込めず、検索結果の抜粋から数字を取った |\n| 同期間:中国メーカー97%超・AGIBOT 44%・Unitree 31%・工場や店舗向け70%超 | S034〜S035, S041〜S042 | Smart Analytics Global の調査を紹介した TechRepublic(2026年8月11日)https://techrepublic.com/article/news-humanoid-robot-shipments-chinese-vendors |\n| 1X NEO:2025年10月に予約開始・2万ドルまたは月499ドル・難しい作業は担当者が遠隔操作・2026年10月初めの時点で顧客への出荷の発表なし | S044〜S049 | 1X 予約ページ/botinfo.ai(2026年10月5日確認)https://botinfo.ai/articles/1x-neo-home-robot / AI2Work https://ai2.work/blog/1x-neo-ships-into-homes-with-a-human-teleoperator-still-inside |\n| インターネットの文章を人が読むと約10万年(10万年のデータの壁)、狭い仕事を確実にこなすロボットを現場に置いて働きながらデータを集めるのが近道 | S087〜S096, S130〜S132 | Ken Goldberg, Science Robotics(2025年8月)https://news.berkeley.edu/?p=134910 |\n| Generalist:27万時間(2025年11月)→50万時間超(2026年4月)・新しいロボットや作業に合わせるデータは約1時間の場合も・選んだ簡単な作業で成功率99%超 | S097〜S104 | Generalist の発表を紹介した記事 https://yespress.io/generalist / https://www.humanoidsdaily.com/news/generalist-ai-unveils-gen-0-claims-scaling-laws-for-robotics-backed-by-270-000-hours-of-real-world-data |\n| 本人の実機:PICK→PLACE の通し試行656回(2026年6月18日〜7月20日)、FKテーブル約170万点、ikpy とデジタルツインの爪先が同じ関節角で300mm以上ずれる、j1 が約90.7度を超えると send_angles がエラーなしで無視される | S061, S068〜S071, S074, S076〜S077 | 本人の台帳 pick_place_ledger_v3.csv、公開記事 DAY257・DAY283、設計メモ arm_kinematics.md(非公開資料を含むため、ここは記述の妥当性だけ見てください) |\n\n## 台本全文(動画で読み上げている版)\n\n\n### 0 フック\n\nS001 今年の1月から6月までに、世界で出荷された人型ロボットは、2万2000台を超えました。\n\n### 0 フック\n\nS002 前の年の同じ時期の、およそ4倍です。\n\n### 0 フック\n\nS003 調査会社のCounterpointが、8月に出した数字です。\nS004 年末までには、5万台を超えると予想されています。\nS005 家事をする人型ロボットの予約販売も、去年の秋から始まっています。\nS006 こういうニュースを見ると、ChatGPTが出たときみたいに、\nS007 ロボットも一気に賢くなって、家に来るのはもうすぐ、って感じませんか。\n\n### 1 自己紹介\n\nS008 Pixels to Motors。こんにちは、エイイチです。\n\n### 1 自己紹介\n\nS009 僕は秋田で29年デザイナーをやりながら、\n\n### 1 自己紹介\n\nS010 自宅の机の上でロボットアームを動かして、その記録を毎日公開しています。\n\n### 1 自己紹介\n\nS011 作る側の机の上から見える、フィジカルAIの話をします。\n\n### 2 今日の問い\n\nS012 今日の問いは、ロボットにもChatGPTのような瞬間は、もう来たのか、です。\nS013 フィジカルAIっていうのは、AIに体を持たせて、\nS014 現実の世界で物に触れて動かす技術のことなんですよ。\nS015 ロボットアームも、人型ロボットも、自動運転もここに入ります。\n\n### 3 結論と立場\n\nS016 先に結論を言っちゃうと、安くなったのは「見て判断する」ところで、\n\n### 3 結論と立場\n\nS017 「触れて動かす」ところは、まだ高いんですよ。\n\n### 3 結論と立場\n\nS018 鍵になるのが、「データの壁」です。\nS019 先に言っておくと、フィジカルAIはまだ使えない、と言いたいわけじゃないんです。\nS020 どこまで来ていて、どこで止まっているのかを、分けて見たいんです。\n\n### 4 目次\n\nS021 大きく3つあります。\n\n### 4 目次\n\nS022 出荷が4倍になったロボットは、何をしているのか。\n\n### 4 目次\n\nS023 僕の机の上のロボットアームで、何が簡単で、何が難しかったのか。\n\n### 4 目次\n\nS024 そして、データの壁はどれくらい高くて、誰がどう越えようとしているのか。\n\n### 4 目次\n\nS025 最後に僕の見立てを話します。\n\n### 5 節1:出荷されたロボットは何をしているのか\n\nS026 1つ目、出荷が4倍になったロボットは、何をしているのか、です。\nS027 まず数字の中身を見ます。\nS028 Counterpointによると、出荷された人型ロボットのうち6割以上は、\nS029 展示やイベントで動きを見せる用途と、研究やデータ集めの用途なんですよ。\nS030 データ集めっていうのは、人がロボットを遠隔で動かして、その動きを記録して、\nS031 AIが学ぶための教材を作る仕事のことです。\nS032 つまり、ロボットの大きな買い手の1つは、ロボットに教えるためのロボットなんです。\nS033 ただ、ここで1つ断っておきます。\nS034 別の調査会社のSmart Analytics Globalは、同じ半年について、\nS035 工場や店舗向けが7割を超えた、と言っているんですよ。\nS036 同じ期間なのに、ほぼ逆の数字です。\nS037 用途の分け方が、会社によって違うんだと思います。\nS038 だから「全部見せ物だ」とも、「もう工場で働いている」とも言い切れない。\nS039 そこは正直に言っておきます。\nS040 はっきりしているのは、どこが作っているかです。\nS041 SAGの数字だと、出荷の97%以上が中国のメーカーです。\nS042 上位のAGIBOTとUnitreeの2社だけで、およそ4分の3なんですよ。\nS043 家のほうも見てみます。\nS044 アメリカの1Xという会社は、家事をする人型ロボット、NEOの予約を、去年の10月に始めました。\nS045 値段は2万ドル。月499ドルのプランもあります。\nS046 ただ、難しい家事は、1Xの担当者がVRゴーグルをつけて、\nS047 ロボットのカメラ越しに家の中を見ながら、遠隔で動かす仕組みなんですよ。\nS048 今年中にアメリカで届け始める予定ですが、\nS049 10月の初めの時点で、お客さんの家に届いたという発表は、まだありません。\nS050 これ、ちょっと考えさせられませんか。\nS051 売っているのはロボットだけじゃなくて、遠隔で動かす人と、\nS052 そこで集まるデータの仕組みごとなんです。\nS053 で、ここが大事なんですけど、工場の話にも家の話にも、\nS054 データを集めるという、同じ目的が出てくるんですよ。\nS055 じゃあ、なぜそこまでしてデータを集めないといけないのか。\nS056 その前に、僕の机の上の話をさせてください。\n\n### 6 節2:机の上のロボットアームで、簡単だったこと・難しかったこと\n\nS057 2つ目、僕の机の上のロボットアームで、何が簡単で、何が難しかったのか、です。\nS058 ここは僕自身の話から始めさせてください。\nS059 僕は自宅の机で、mechArm 270という小さなロボットアームを動かしています。\nS060 赤いブロックを掴んで、箱に置く。\nS061 それだけの作業を650回以上試して、全部の結果を台帳に残しているんですよ。\nS062 まず、簡単だったほうから話します。掴めたかどうかの判定です。\nS063 iPhoneで撮った写真をAIに見せて、「ブロックを掴めていますか」と聞くと、\nS064 はい、いいえで答えが返ってくる。\nS065 少し前なら、ここを作るだけで大仕事だったはずなんですよ。\nS066 難しかったのは、動かすほうでした。\nS067 ロボットアームの爪の先がどこに来るかは、普通は関節の角度から計算で出します。\nS068 ところが、ikpyという、よく使われる計算ツールで出した爪先の位置と、\nS069 パソコンの中に作ったアームの分身で、実際に動かした位置が、\nS070 同じ関節の角度なのに、30cm以上ずれたんです。\nS071 30cmですよ。机の上のブロックなら、端から端まで外れます。\nS072 だから僕は、その分身、デジタルツインの中でアームを実際に動かして、\nS073 爪先がどこに来るかを、1点ずつ測って表にしました。\nS074 その表は今、170万点ほどあります。\nS075 もう1つ困ったのが、黙る失敗です。\nS076 肩の関節に、90度ちょっとを超える角度を送ると、\nS077 エラーも出さずに、指令が黙って捨てられるんですよ。\nS078 アームは動かない。でも、何も言ってこない。\nS079 ログを1行ずつ見て、やっと気づきました。\nS080 でも、ポイントはここなんです。\nS081 見て判断するところは、AIに頼めばすぐ手に入りました。\nS082 でも、爪先がどこに来るか、指令がちゃんと届いたかは、\nS083 自分の机の上で、自分のアームで測るしかなかった。\nS084 インターネットのどこにも、僕のアームの、僕の机の上のデータはないんですよ。\nS085 じゃあ、この「どこにもないデータ」は、世界全体ではどれくらい足りないのか。\n\n### 7 節3:データの壁の高さと、越えようとしている会社\n\nS086 3つ目、データの壁はどれくらい高くて、誰がどう越えようとしているのか、です。\nS087 カリフォルニア大学バークレー校のロボット研究者、\nS088 ケン・ゴールドバーグさんが、去年、面白い計算をしています。\nS089 ChatGPTのようなAIの学習に使われたインターネットの文章を、\nS090 人が全部読むと何年かかるか。答えは、約10万年なんですよ。\nS091 ロボットが手を動かすためのデータは、その足元にも及ばない。\nS092 ゴールドバーグさんは、これを「10万年のデータの壁」と呼んでいます。\nS093 ChatGPTは10万年分の教科書で育ったけれど、\nS094 ロボットには、その教科書がまだない、ということなんです。\nS095 ここで、じゃあロボットにChatGPTの瞬間は当分来ない、\nS096 と言いたくなるじゃないですか。でも、ちょっと待ってほしいんです。\nS097 アメリカのGeneralistという会社は、人が手で作業する様子を、\nS098 安い装着型の機材で記録して集めています。\nS099 去年の11月の時点で27万時間、今年の4月には50万時間を超えました。\nS100 半年で2倍近くです。\nS101 しかもこの会社によると、新しいロボットや新しい作業に合わせるのに、\nS102 そのロボット自身のデータは、1時間ほどで済む場合があって、\nS103 選んだいくつかの簡単な作業では、成功率が99%を超えたそうです。\nS104 会社自身の発表なので、そこは差し引いて聞いてください。\nS105 50万時間を年に直すと、約57年です。\nS106 10万年と比べると、まだ1700分の1くらい。\nS107 ただ、これは人が文章を読む時間と、作業を記録した時間なので、\nS108 単位をそろえた比較ではありません。目安として聞いてください。\nS109 つまり、壁はまだ高い。でも、壁の前でみんなが土を積み始めている。\nS110 1Xが家の中で遠隔操作しながらデータを集めているのも、\nS111 出荷されたロボットの多くがデータ集めに回っているのも、\nS112 同じ壁に向かっている動きなんです。\nS113 ただ、ここで「なんだ、まだ全然ダメなのか」で\nS114 片付けてしまうのも違うんです。\nS115 見て判断するほうには、もう10万年分の教科書がある。\nS116 そこは僕の机の上でも、もう普通に使えています。\nS117 じゃあ、この2つが混ざっている今、ロボットのニュースの何を見ればいいのか。\n\n### 8 見立て\n\nS118 ここからが、僕の一番言いたいことです。\nS119 データの壁を知っていると、\nS120 ロボットのニュースを見たときの問いが、2つに変わるんですよ。\nS121 1つは、そのロボットは誰が動かしていて、データは誰が集めているのか、です。\nS122 人が遠隔で動かしているのか、ロボットが自分で動いているのか。\nS123 デモ動画では、ここがほとんど言われないんですよね。\nS124 もう1つは、その仕事は「見て判断する」で済むのか、\nS125 「触れて動かす」精度が要るのか、です。\nS126 棚の商品が足りているかを見るのは、前者です。\nS127 柔らかい物を潰さずに掴むのは、後者です。\nS128 前者なら、もう安く使える。\nS129 後者は、その現場で測って固める仕事が、まだ残っている。\nS130 ゴールドバーグさんも、昔ながらのエンジニアリングで、\nS131 狭い仕事を確実にこなすロボットを現場に置いて、\nS132 働きながらデータを集めるのが近道だ、と言っています。\nS133 僕の机の上でやっていることも、まさにそれなんですよ。\nS134 判断はAIに任せる。動きは、自分のアームで測って固める。\nS135 で、作る側として、もう1つ言わせてください。\nS136 データの壁を越えたあとも、最後に残るのは「どう動くか」なんです。\nS137 例えばロボットが人に物を渡すとき、\nS138 何秒待って手を開けば、人は安心して受け取れるのか。\nS139 これは、成功か失敗かの判定だけでは出てこない。\nS140 動きの質そのものを設計する仕事が、\nS141 壁の向こうにも残っていると、僕は見ています。\n\n### 9 冒頭の問いへの回答\n\nS142 じゃあ、冒頭の問いに答えますね。\n\n### 9 冒頭の問いへの回答\n\nS143 ロボットにChatGPTの瞬間は、見て判断する部分には、もう来ています。\n\n### 9 冒頭の問いへの回答\n\nS144 でも、現実に触れて動かす部分には、まだ来ていません。\n\n### 9 冒頭の問いへの回答\n\nS145 その差を埋めるデータを、今、世界中で必死に集めている最中です。\nS146 ロボットのデモ動画を見たら、\nS147 「これは誰が動かしていて、何回やって、何回うまくいったのか」を、\nS148 一度だけ考えてみてください。\nS149 それだけで、同じ動画の見え方が、かなり変わると思います。\n\n### 10 アウトロ\n\nS150 今日はここまでです。\n\n### 10 アウトロ\n\nS151 この動画が気に入ったら、高評価とチャンネル登録をお願いします。\n\n### 10 アウトロ\n\nS152 コメントは全部読ませてもらっています。それではまた。</code></pre><p></p><h2 id=\"index_tELWWv5H\">codex から返ってきた検証結果の全文</h2><pre><code>判定は「直してから」です。現在の結論は、台本の根拠だけでは成立していません。 特に、①「ChatGPTの瞬間」の定義、②異種データの1700分の1比較、③実機の設定・制御問題を学習データ不足へつなげる論理、④Generalistの最新発表の欠落が重大です。\n2026年10月11日時点でWeb確認しました。以下では、一次資料の本文確認、検索抜粋だけの確認、推論、未確認を区別します。本人の台帳・設計メモは依頼どおり検証対象外とし、数字の伝え方だけを評価します。\n[❌ NG] S016〜S018、S143〜S145\n問題:「ChatGPTの瞬間」が未定義です。写真への回答を一度得られたことから、視覚判断全体の転換を宣言し、さらに物理操作全体には転換がないと断定するのは、両方向に飛躍しています。\n根拠:使いやすさ、汎化、信頼性、普及、費用は別の評価軸です。例えば[Gemini Roboticsの公式資料](https://deepmind.google/en/models/gemini-robotics/gemini-robotics/)は、視覚と言語から動作を出すモデルを示していますが、提供はPrivate previewです。「能力がある」と「誰でも利用できる」も分ける必要があります。\n修正案:「ここではChatGPTの瞬間を、専門家でなくても、いろんな仕事にすぐ使えるようになる転換点、と呼びます。僕の机では、画像に質問する入口はぐっと簡単になりました。でも、いろんな家で安全に家事を任せられる転換点まで来たかは、まだ判断できません。」\n[❌ NG] S105〜S109\n問題:57年への換算は正しいですが、「1700分の1」はデータの不足率ではありません。「目安」と断っても、比較対象の意味が違うため、壁の高さを数量化したことにはなりません。\n根拠:500,000÷24÷365≒57.1、100,000÷57.1≒1752。前者は作業記録の総時間、後者は異種の学習資料を人間が読む・見る時間への換算です。情報密度、重複、センサー、タスクの多様性、必要性能がそろっていません。また一社のデータ量は、世界全体の総量でもありません。\n修正案:「50万時間は、連続した時間に直すと約57年です。ただ、文章や画像を見る時間と、作業を記録した時間は、そのまま比べられません。何倍足りないかではなく、必要な経験をどれだけ広く集められるかが問題なんです。」\n[❌ NG] S068〜S074、S082〜S085、S133〜S134\n問題:計算モデル間の30cmの不一致と指令の不受理は、それだけではAIの学習データ不足を示しません。モデルの整合性や制御インターフェースの問題を、業界の「データの壁」の実例として使っています。\n根拠:同じ角度の比較には、座標系、リンク寸法、関節軸、角度の符号・原点、工具先端の定義を一致させる必要があります。どちらが正しいかは実機との比較が必要です。原因は非公開資料のため未確認です。\n修正案:「僕の設定では、計算モデルとシミュレーターの位置が30cm以上食い違いました。これはAIの学習データ不足とは別に、モデルや制御の整合性を確かめる仕事が必要だった、という話です。」\n[❌ NG] S099〜S104、S109、S129、S144\n問題:4月のGEN-1を最新状況のように紹介していますが、8月のGEN-1.5が抜けています。新しい作業への適応負担を論じる動画では、視聴者の判断を変える重要な欠落です。\n根拠:[Generalistの8月19日公式発表](https://generalistai.com/blog/gen-1.5)は、短い実演を使った新タスクへの適応を報告しています。ただし同社自身の評価で、対象は簡単で短い作業です。家庭全体への汎化を証明したわけではありません。\n修正案:「さらに8月にはGEN-1.5を発表して、簡単で短い作業なら、数秒の実演から適応できる例も報告しています。会社発表なので評価条件は見る必要がありますが、『動かすには毎回大量の専用データが必要』とも言い切れなくなっています。」\n[❌ NG] S034〜S038\n問題:SAGの「industrial and commercial」を「工場や店舗向け」と狭めています。また用途と導入先は重なり得るため、「ほぼ逆の数字」「分類の違いが原因」とする説明は未立証です。\n根拠:[SAGの一次発表](https://smartanalyticsglobal.com/global-humanoid-robot-shipments-2026-agibot-unitree/)の表現は産業・商業用途です。同社の総出荷推計も約19,100台で、Counterpointの22,000台超とは異なります。両調査の分類対応表や集計方法は、今回確認できていません。商業施設での展示と研究用途が排他的とは限りません。\n修正案:「一方、SAGは約1万9100台と推計して、産業・商業用途が7割超としています。ただ、総数も分類も違うので、この2つは単純な裏表として比べられません。どれだけ自律的に実務をこなしたかは、この数字だけでは分からないんです。」\n[❌ NG] S089〜S094、S115〜S116\n問題:Goldbergの10万年は「文章だけ」ではありません。さらに、その比喩を「見て判断する側には十分な教科書がある」という性能保証に変えています。\n根拠:[Science Roboticsの原文](https://autolab.berkeley.edu/assets/publications/media/GOFE-Can-Close-the-100000-Year-Robot-Data-Gap-Science-Robotics-Aug-2025-scirobotics.aea7390.pdf)は、現代のVLMの学習に使う文章と画像を読む・見る時間への概算です。これはEditorialであり、一般ロボットに必要なデータ量の実証値ではありません。\n修正案:「ゴールドバーグさんは、画像と言葉を扱うAIの学習資料を、人が読む・見る時間に換算すると、約10万年規模だと紹介しています。ロボットにも同じ量が必要だと分かったわけではありませんが、動作指令と結びついた経験は集めにくい、という問題提起です。」\n[❌ NG] S083〜S084、S129、S145\n問題:「自分の机で測るしかない」は、他環境・他機体からの転移を否定する表現です。また、データだけが判断と動作の差を埋めるように聞こえます。\n根拠:[Gemini Robotics 1.5技術報告](https://storage.googleapis.com/deepmind-media/gemini-robotics/Gemini-Robotics-1-5-Tech-Report.pdf)は機体をまたいだ技能転移を報告しています。専門家なら、VLAの汎化、シミュレーション、世界モデル、モデルに基づく制御をまず反論として出します。[NVIDIAの公式説明](https://developer.nvidia.com/isaac/gr00t?ncid=so-yout-261862-vt48)も実データと合成データの併用を示しています。ただし、これらが現場検証を不要にしたとは言えません。\n修正案:「ほかの環境で学んだことを持ってくる研究も進んでいます。それでも、僕の構成でちゃんと動くかは確認が必要でした。差を埋めるのはデータだけでなく、モデル、制御、機械そのものの改善でもあります。」\n[⚠️ 要注意] S013〜S015、S017、S144\n問題:「物に触れて動かす」で自動運転まで定義し、その後はほぼ物体操作と家庭用人型ロボットの話になります。広い領域を否定する結論になっています。\n根拠:台本内の対象の不一致です。Goldbergの原文自身も、自動運転や荷物仕分けを既に実用化された例として挙げています。限定用途の実用化と、汎用家庭ロボットの普及は別問題です。\n修正案:「フィジカルAIは、現実世界を認識して行動するAIのことです。自動運転も入りますが、今日は主に、アームや人型ロボットが物を扱う話、とくに家庭のような変化の多い場所に絞ります。」\n[⚠️ 要注意] S001〜S004、S028〜S029、S040〜S042\n問題:数字の記載は確認できても、市場の確定実績として扱うには調査方法の確認が不足しています。「はっきりしている」も強すぎます。\n根拠:Counterpointの公式ページ由来の検索抜粋では、22,000台超、前年比約300%増、通年50,000台超の予測、用途の合計60%超を確認しました。直接開いた本文は取得できず、集計定義は未確認です。SAGの本文では中国メーカー97%超、44%+31%を確認しました。いずれも調査会社の推計で、出荷は自律稼働や顧客検収を意味しません。[Counterpoint](https://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026)、[SAG](https://smartanalyticsglobal.com/global-humanoid-robot-shipments-2026-agibot-unitree/)\n修正案:「Counterpointの推計では、今年上半期の出荷は2万2000台超、前年同期のおよそ4倍です。ただ、出荷台数は、そのまま自律的に働いている台数ではありません。中国メーカーへの集中も、SAGの推計では非常に大きいです。」\n[⚠️ 要注意] S030〜S032、S053〜S054、S110〜S112\n問題:「データ集め」を遠隔操作だけに限定し、研究用途までデータ生産へ読み替えています。さらに工場と家庭の共通の「目的」として断定しています。\n根拠:研究には制御・機構・移動などの検証も含まれ、全てが学習データ収集とは限りません。作業中の自律ログや人の実演も収集方法です。「研究・データ生産」という合算から、それぞれの比率や購入目的は分かりません。\n修正案:「データ集めには、人が遠隔で動かして記録する方法も、自律作業の記録を残す方法もあります。ただ、研究向けの台数が全部データ集めに使われる、という意味ではありません。」\n[⚠️ 要注意] S044〜S049、S051〜S052\n問題:価格プランの提供時期を省き、遠隔支援が常時の仕組みのように聞こえます。顧客出荷については、発表を見つけられないことと、出荷していないことを区別すべきです。\n根拠:[1Xの発売発表](https://www.1x.tech/discover/neo-home-robot)は2025年10月28日、20,000ドルのEarly Accessは2026年優先配送、499ドルの月額プランは後日配送です。[現行注文ページ](https://www.1x.tech/order)では基本的自律性と予約した時間のExpert Modeを説明しています。VRゴーグルの具体的運用は今回の公式本文では未確認。10月11日までの一般顧客への配送開始を示す公式発表も今回の検索では確認できませんでした。\n修正案:「2万ドルの早期購入枠は2026年配送予定で、月499ドルのプランは後日です。初期の自律機能に加えて、知らない作業では予約した時間に担当者が遠隔で支援します。10月11日時点で、一般顧客への配送開始を示す公式発表は、僕が確認した範囲では見つかっていません。」\n[⚠️ 要注意] S063〜S065、S081、S126〜S129\n問題:回答が返ることと、正しく判定できることを混同しています。「前者ならもう安く使える」は、精度や運用条件を無視しています。\n根拠:判定を評価するには、実際の保持状態との照合、失敗画像、遮蔽、誤判定率などが必要です。棚の欠品検知にも死角や商品識別、照明、誤報対応があります。台本にはモデル名、正答率、費用がなく、「安い」の実証もありません。\n修正案:「写真を見せて質問する入口は、驚くほど簡単でした。ただ、答えが返ることと、毎回正しいことは別です。僕の条件でどこまで任せられるかは、失敗例も含めて確かめる必要があります。」\n[⚠️ 要注意] S061\n問題:650回以上が成功した回数、あるいは同一条件での評価回数に聞こえます。提示された情報は、期間中のPICK→PLACE通し試行656回です。\n根拠:ユーザー提示の台帳説明に基づく指摘です。試行間の構成変更や中断の扱いは未確認で、成功率やモデル性能は推定できません。\n修正案:「6月18日から7月20日までに、掴んで置く一連の試行を656回記録しました。これは成功回数ではなく、失敗も含めた試行回数です。」\n[⚠️ 要注意] S069〜S074\n問題:「実際に動かした」が実機測定に聞こえ、170万点が現実の経験データとして受け取られます。提示された説明ではシミュレーター内のFK表です。\n根拠:仮想モデルの大量サンプリングは、そのモデルの写像を細かくしたものです。点数が多くても、モデルと実機の一致や接触精度を証明しません。\n修正案:「シミュレーターの中で関節角と爪先位置の対応を計算して、約170万点の表にしました。実機で170万回測ったわけではなく、現物との一致は別に確認する必要があります。」\n[⚠️ 要注意] S076〜S079\n問題:90度ちょっとという閾値を機種全体の仕様として語っています。また「指令が捨てられる」は、通信を観測していなければ原因の断定です。\n根拠:提示情報は本人の構成でj1が約90.7度を超えたときの観察です。ファームウェア、SDK、他関節の値、検証箇所は未確認です。「肩」だけでは関節も特定しにくくなります。\n修正案:「僕の機体とソフトの組み合わせでは、j1に約90.7度を超える値を送ると、例外が出ないのに動かないことがありました。機種全体の仕様か、どの段階で止まったのかは、まだ切り分けが必要です。」\n[⚠️ 要注意] S099〜S104\n問題:期間が「半年」と丸められ、99%超が評価全体の平均のようにも聞こえます。1時間は追加のロボットデータであって、全学習・導入に必要な時間ではありません。\n根拠:[GEN-0公式発表](https://generalistai.com/blog/gen-0)は2025年11月4日、[GEN-1公式発表](https://generalistai.com/blog/gen-1)は2026年4月2日で約5か月です。27万時間超と50万時間超、平均99%と一部タスク99%超を区別する必要があります。自己申告の試験結果で、現場導入の総費用は示していません。\n修正案:「会社発表では、約5か月で27万時間超から50万時間超に増えました。事前学習に加えて、示した試験では約1時間のロボットデータを使い、選んだ作業群で平均99%の成功率を報告しています。導入全体が1時間で終わる、という意味ではありません。」\n[⚠️ 要注意] S130〜S133\n問題:Goldbergの提案を、個人の机上試行と「まさに同じ」としています。提案の肝である、実務で価値を生みながらデータを蓄積する点が抜けます。\n根拠:原文の提案は、従来の工学と学習を組み合わせ、商業環境で有用な仕事をするロボットから継続的にデータを集めるものです。家庭の開発試行との類似はありますが、稼働規模と実用性は異なります。前掲のScience Robotics原文。\n修正案:「僕の机の上も、AIと従来の制御を組み合わせる点では通じています。ただ、現場で役に立つ仕事をしながらデータが増える仕組みとは、まだ規模も段階も違います。」\n[⚠️ 要注意] S123、S136〜S141\n問題:「デモではほとんど言われない」は調査なしの一般化です。また人への物の渡し方を、データの壁を越えた後の課題に分離するのも不正確です。\n根拠:Generalistの公開動画には自律動作・再生速度の表示があります。人との動作の調整も、知覚、制御、学習、安全性を含む現在の研究対象です。[DeepMindの人とロボットの相互作用・安全性説明](https://deepmind.google/models/gemini-robotics/responsibly-advancing-ai-and-robotics/)もその一例です。\n修正案:「デモを見るときは、自律か遠隔操作か、速度や介入の条件が書かれているかを見ます。そしてデータ集めと並行して、人が安心して受け取れる動きを設計する仕事も必要なんです。」\n\n1. このまま公開してよいか:直してから。 体験談の章は残せますが、結論とデータの壁への接続は書き直す必要があります。\n2. 公開前に必ず直すべき文番号: S016〜S018、S034〜S038、S068〜S074、S082〜S085、S089〜S094、S099〜S109、S115〜S116、S128〜S129、S143〜S145。\n3. 結論そのものへの評価:現状では成立していない。 「見て判断する全体には来た/触れて動かす全体には来ていない」という二分は広すぎ、証拠も非対称です。次の結論なら条件つきで成立します。\n\n「僕の机では、画像に質問する入口はずっと簡単になりました。一方、物を確実に動かすには、機体や環境とのすり合わせがまだ必要でした。その負担を減らす研究は急速に進んでいます。でも、いろんな家庭で安全に家事を任せられる転換点まで来たかは、まだ判断できません。」</code></pre><p></p><h2 id=\"index_9Y98_SCr\">video_rules.md の全文</h2><pre><code class=\"language-markdown\"># video_rules.md\n\n## 動画制作ルール(唯一の正)\n\n最終更新: 2026-10-11\n状態: 試作1本目「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」を作りながら決めたことをまとめた初版。まだ決まっていない項目は8節にまとめている。\n\nYouTube 用の解説動画を作るときは、まずこのファイルを読む。台本の文体は language_rules.md、記事は article_rules.md が正。動画の構成・数字の扱い・画面・音声・書き出しは本ファイルが正。\n\n---\n\n## 1. 動画の構成(解説回の型)\n\n参考にした3本の解説動画(時事ニュースをデータと体験で検証する形式)から取った型。尺は13〜14分。\n\n| # | パート | 時間の目安 | やること |\n|---|---|---|---|\n| 0 | フック | 0:00〜0:45 | 挨拶より先に、視聴者が最近見たニュースや数字から入る。世間の感覚を代弁する(「〜って感じませんか」) |\n| 1 | 自己紹介 | 〜0:55 | ロゴのテロップを出し、毎回同じ文言で名乗る(2節の文言) |\n| 2 | 今日の問い | 〜1:10 | 問いを疑問文1つで言い、問いの中の言葉をその場で定義する |\n| 3 | 結論と立場 | 〜1:30 | 先に結論を言う。誤解されやすい方向を先に否定する(「〜と言いたいわけじゃないんです」) |\n| 4 | 目次 | 〜1:50 | 「大きく3つあります」と数を言ってから並べる |\n| 5〜7 | 本編(3節) | 〜10:00 | 「1つ目、〜です」で始め、各節の最後の1文を次の節の問いにする |\n| 8 | 見立て | 〜12:30 | 「ここからが僕の一番言いたいことです」と言ってから意見を話す |\n| 9 | 問いへの回答 | 〜13:20 | 冒頭の問いに3〜4文で答え、視聴者が次にニュースを見たときに考える問いを1つ渡す |\n| 10 | アウトロ | 〜13:40 | 毎回同じ文言 |\n\nパートごとのセリフの型(穴埋め式)と、実験回の型は Docs「動画台本テンプレート|台本例:フィジカルAIの現在地」にある。\n\n---\n\n## 2. 台本のルール\n\n公開前に codex に敵対的検証(台本を論破するつもりで見落としを探してもらう)をかけたところ「このまま公開は不可」と判定された。以下はそのとき直した点から作ったルール。\n\n1. **問いの中の言葉は、冒頭で定義する。** 例: 「ChatGPTの瞬間」を定義しないまま「来た/来ていない」と言い切っていた → 「専門家でなくても、いろんな仕事にすぐ使えるようになる転換点」と冒頭で言う形に直した。\n2. **話の範囲を言う。** 例: フィジカルAIの定義に自動運転を含めながら、アームの話だけで結論を出していた → 「自動運転も入りますが、今日はアームや人型ロボットが物を扱う話に絞ります」と言う。\n3. **数字は、出典で裏が取れて、論旨に必要なものだけ使う。** 増やすほど突っ込まれる場所が増える。試作1本目で削った数字: 1700分の1(単位の違う比較)、成功率99%・約1時間(会社の自己申告で条件の説明が要る)、170万点(シミュレーターの表なのに実機の測定と誤解される)、メーカー別の内訳(なくても話が変わらない)。\n4. **推計・会社自身の発表は、そう口で言う。** 「Counterpointの推計では」「会社の発表では」。調査会社同士で数字が違うときは、どちらかに寄せず「総数も分類も違うので、単純には比べられない」と言う。\n5. **意味や単位の違う数字を並べて比べない。** 例: 「人が文章を読む時間 10万年」と「作業を記録した時間 57年」を割り算していた → 比較ごと削った。\n6. **自分の実機の話は、自分の条件の話として言う。** 「僕の機体とソフトの組み合わせでは」を付ける。机の上で起きたことを業界全体の結論に広げない。例: 計算ツールとデジタルツインの爪先の位置が30cm以上食い違った話を「データ不足」の例にしていた → 「モデルや制御が自分の機体と噛み合っているかを確かめる、すり合わせの仕事」として話す形に直した。\n7. **自分の数字は、何の数かを添える。** 例: 「650回以上」→「失敗も含めた試行回数」。\n8. **最新の発表は、公開の直前に確認する。** 例: 4月の発表を最新として話していたが、8月に次の発表(GEN-1.5)が出ていた。\n9. **「見つからない」と「していない」を分ける。** 例: 「出荷していない」ではなく「配送開始を示す公式発表は、僕が確認した範囲では見つかっていません」。\n10. **自分の活動を盛らない。** 自己紹介の文言は「僕は本業のデザイナーをしながら、ロボティクスの研究と開発をしています。自宅の机の上でロボットアームを動かして、その記録を公開しています。」に固定する。「秋田」「29年」「毎日」は入れない。\n11. **公開前に codex で敵対的検証をかける。** 依頼ファイルの型は、試作1本目の動画フォルダにある `codex_敵対的検証依頼_*.md`。台本全文に文番号(S001〜)を振り、出典の表と検証の観点(事実・論理・抜けている反論・言い方)を付けて渡す。返ってきた指摘は、一次情報で確かめてから採用する。\n\n---\n\n## 3. ナレーション(現状は仮音声)\n\nいまは仮の合成音声で作っている。本番の声は未決(8節)。\n\n- **エンジン:** Open JTalk、声は nitech_jp_atr503_m001(男声)。設定は 速さ 1.08、音の高さ -1、声質 0.55\n- **読み替え表:** 英語の名前はカタカナで読ませる(ChatGPT→チャットジーピーティー、ikpy→アイケーパイ など)。誤読した語も表に足す。これまでの例: 爪先→つめさき(「つまさき」だと足の指先に聞こえる)、値→あたい、4分の3→よんぶんのさん。新しい固有名や数字を入れたら、エンジンの解析結果(カタカナの読み)を出して目で確認する\n- **間延びの検査:** ひと息で読む区間が長いと、その区間の最後の言葉が1音0.47秒ほどに引き伸ばされる(普段は0.1秒前後)。作った音声すべてで、1音0.3秒を超える箇所を検出する。見つかった文は、読み上げ用の原稿だけに読点を足して区切る。字幕の文は変えない\n- **文のあとの間:** 通常 0.35秒、話題の区切り 0.6〜0.7秒、節の終わり 0.9秒\n\n---\n\n## 4. 画面デザイン\n\n### 4-1. 画面の枠\n\n- 1920×1080、30fps\n- 余白は左右140px。下の250pxは字幕のために空ける\n- 左上に章のタグ(章番号はオレンジ、見出しは薄い文字)、右上にロゴ(高さ34px)\n\n### 4-2. 色(記事のサムネイルと同じ色)\n\n| 用途 | 色 |\n|---|---|\n| 背景 | #F5F5F0 |\n| 文字 | #0D0D0D |\n| 薄い文字(ラベル・出典・補足) | #888888 |\n| 区切り線 | #D6D6D1 |\n| 横帯・棒の「その他」 | #D3D3CE |\n| 箱(フロー図)の背景 | #FFFFFF |\n| オレンジ(強調・主題) | #C4471F |\n| 青(対比のもう一方) | #1A6C98 |\n\n- オレンジは、1画面の中で一番伝えたい語だけに使う\n- 青とオレンジを並べるのは、2つのものを対比するときだけ(試作1本目では「見て判断する=青」「触れて動かす=オレンジ」)\n- 新しい色を足すときは、色覚の違いがあっても見分けられるかを検証してから使う(dataviz スキルの validate_palette.js。背景色 #F5F5F0 を指定する)\n\n### 4-3. 文字\n\nフォントは Noto Sans CJK JP。\n\n| 用途 | 大きさ | 太さ |\n|---|---|---|\n| 大きい数字(ヒーロー数字) | 220px(単位は90px) | 900 |\n| 章番号 | 200px(オレンジ) | 900 |\n| 見出し 大/中/小 | 84px/64px/48px | 900/900/700 |\n| 見出しの上の小見出し | 40px(薄い文字) | 700 |\n| 2列比較のラベル | 34px(薄い文字) | 700 |\n| 出典 | 24px(薄い文字) | 400 |\n\n### 4-4. 図の型\n\n内容の形に合わせて、次の型から選ぶ。\n\n| 内容 | 型 |\n|---|---|\n| 伝えたい数字が1つ | 大きい数字+上に何の数か+下に比較(「前年同期の約4倍」) |\n| 2つのものの対比 | 2列。間を細い縦線(2px)で区切り、色は文字の色だけで分ける |\n| 手順・流れ | 白い箱を矢印でつなぐ |\n| 全体のうちの割合 | 横帯(ラベルを帯の中に書く) |\n| 時期ごとの量 | 横棒(長さは実際の比率) |\n| 章の始まり | 大きい章番号+章の問い |\n| 視聴者への問い | 黒い丸ラベル(「問い 1」)+大きい見出し |\n| 並ぶ項目 | 番号付きの行を細い線で区切る |\n\n### 4-4b. 強弱のつけ方\n\n全部の場面を同じ強さにしない。1本の中で、次の3段階を使い分ける。打ち出しは1本に6〜10場面ほど、残りは説明の図にする。\n\n| 段階 | 使う場面 | 見た目 |\n|---|---|---|\n| 打ち出し | 結論、問いかけ、言い切りたい一言、章の始まり | 小さい前置き(色・44px)の下に、1〜2行の大きな文(110〜150px)。1行は10字前後まで。補足は44pxの薄い文字で1行だけ |\n| 説明 | 数字・比較・手順を見せる場面 | 4-4の図の型(見出し64〜84px) |\n| 補足 | 出典・断り書き | 24〜34pxの薄い文字 |\n\n打ち出しの型(試作1本目で使ったもの):\n\n- **打ち出し文:** 前置き「先に結論」→「見て判断する入口は簡単に/物を動かすのはまだ手間」。対比の語だけ青とオレンジにする\n- **章の扉:** 「今日の話」→「1つ目」→ 大きなキーワード(200px・オレンジ。例「データの壁」)→ 章の問いを1行\n- **点の並び:** 割合は、100個の点のうち該当する数だけ色を付けて見せる(例: 中国メーカー 97%)。「以上」「推計」は下の注に書く\n\n打ち出しにする文の選び方: 字幕をそのまま大きくするのではなく、その場面で一番残したい一言に縮める。字幕と同じ文を大きく出さない。\n\n### 4-5. やらない表現\n\n- カードの上や左に太い色の線を引く(よくある AI 生成スライドの見た目になる)\n- 影、グラデーション\n- 1画面にオレンジと青以外の色を足す\n- 意味や単位の違う数字を同じ棒や帯に並べる\n\n### 4-6. 出典\n\n数字を出す画面には、左下(字幕の上)に出典を24pxの薄い文字で必ず入れる。推計なら「推計」、会社の自己申告なら「会社の発表」と書く。模式図には「図は模式図」と入れる。\n\n---\n\n## 5. 字幕\n\n- 白文字 52px・太さ800。縁取りは #0D0D0D・11px で、角を丸くする(SVG の stroke-linejoin: round。CSS の文字の縁取りは角が尖るので使わない)\n- 読み上げの1文=字幕1枚\n- 2行まで、1行31字以内。改行は読点の位置で入れる。読点がなければ助詞の後ろで割る\n- 位置は画面の下から62px\n\n---\n\n## 6. 実機の映像とテロップ\n\n- 映像は 1920×1080 に拡大して切り抜く。字幕の後ろに黒50%の帯(高さ230px)を敷く\n- 何の映像かを左上に出す(黒78%の地に白文字34px。例「実機:mechArm 270-Pi 赤いブロックを掴んで箱に置く」)\n- 名前のテロップ: 薄い地に、ロゴ(高さ30px)・名前60px・肩書き30px(「デザイナー/ロボティクスの研究・開発」)。左の太い線は付けない\n- 編集ツールの透かし(例: clideo.com)が入った素材は、ぼかしたうえで字幕帯の下に隠す。これから撮る素材は透かしなしで書き出しておく\n- iCloud にしかない素材を取り込むと、Mac にダウンロードされる\n\n---\n\n## 7. 書き出しと保存\n\n- **作業用(YouTube に上げる方):** H.264(crf 20)、音声 AAC 192kbps、音量を -16 LUFS にそろえる\n- **確認用(チャットで送る方):** crf 29、音声 112kbps。30MB以下に収める(チャットで送れる上限)\n- **保存先:** 動画ファイル(mp4)は重く、iCloud に置くと Mac の内蔵 SSD を圧迫するので、外付け SSD(KIOXIA)に置く。軽いファイルは iCloud に置く\n - 動画ファイル(作業用・確認用の mp4): `/Volumes/KIOXIA/r2r2r-data/videos/{動画名}/`\n - 字幕の srt・codex 依頼の md: iCloud `デジタルツイン学習R2R2R/動画/{動画名}/`\n - 制作ベース: iCloud `デジタルツイン学習R2R2R/動画/_制作ベース/`(このファイルの写し、スクリプト一式、ロゴの透過 PNG)\n - 素材の実機映像も、新しく撮るものは KIOXIA に置く。iCloud にしかない素材を取り込むと Mac にダウンロードされて内蔵 SSD を使う\n - MASTER の保存先テーブルと、KIOXIA の README.md のフォルダ構成には、まだ videos/ の行がない。追記はエイイチが行う\n- **作る手順(_制作ベース のスクリプト):**\n 1. `narration.py` に台本を書く(1文ごとに、場面の名前・字幕の文・後ろの間)。読み替え表もここ\n 2. `tts2.py` で音声を作る(間延びの検査と、区切っての作り直しまで自動)\n 3. `scenes.py` に場面ごとの図を書く(4節の型と色)。実機映像は `src/` に置き、`VIDEO` に場面名と開始秒を書く\n 4. `render.py` で図・テロップ・字幕を画像にする\n 5. `assemble.py` で1文ずつ映像を作ってつなぎ、音声と合わせる(字幕の srt も出る)\n 6. 確認用に圧縮する\n\n---\n\n## 8. 未決・改良予定\n\n| 項目 | いまの状態 | 決めること |\n|---|---|---|\n| ナレーションの声 | Open JTalk の仮音声。抑揚が機械的 | 本人の声で録るか、品質の高い音声合成(VOICEVOX など。利用規約とクレジット表記の条件を確認)にするか。srt に1文ごとの時間が入っているので、それに合わせて録れば映像はそのまま使える |\n| 図解の動き | すべて静止画 | 数字のカウントアップ、矢印が伸びる、棒が伸びるなどの動きを入れるか。入れるなら、どの図の型に入れるか |\n| 背景の映像 | 実機映像は自己紹介・節2・エンディングだけ。ほかは無地の背景 | 解説中も実機映像を薄く敷くか、図解だけにするか。使う素材を撮りためるルール |\n| BGM・効果音 | なし | 使うか。使うなら、YouTube で収益化できてクレジット表記の条件を満たす音源から選ぶ。ナレーションが聞き取れる音量に下げる |\n| ジングル | 文言だけ(「Pixels to Motors。こんにちは、エイイチです。」) | 音とロゴの動きを付けるか |\n| サムネイル | まだない | 記事のサムネイルと同じ配色・線画のトーンで作るか |\n| 番組名 | 未定 | 決める |\n| 概要欄のチャプター | まだない | srt の節の開始時刻から作れる |\n\n---\n\n## 更新履歴\n\n- 2026-10-11: 4-4b「強弱のつけ方」を追加(打ち出し・説明・補足の3段階、打ち出し文・章の扉・点の並びの型)。\n- 2026-10-11: 動画ファイルの保存先を iCloud から KIOXIA の videos/ に変更(内蔵 SSD の圧迫を避けるため)。\n- 2026-10-11: 初版。試作1本目の制作で決めた構成・台本・ナレーション・画面・字幕・映像・書き出しのルールをまとめた。台本ルールは codex の敵対的検証で直した点から作った。色は記事のサムネイルに合わせた。</code></pre><h2 id=\"index_lmKEOQqP\">参照した情報</h2><ul><li><p><a href=\"https://www.youtube.com/watch?v=cDEex1_8cOo\" data-has-link=\"true\">YouTube:参考にした解説動画 1</a></p></li><li><p><a href=\"https://www.youtube.com/watch?v=bmmF1tUwnt8\" data-has-link=\"true\">YouTube:参考にした解説動画 2</a></p></li><li><p><a href=\"https://www.youtube.com/watch?v=LI-VAGxwvno\" data-has-link=\"true\">YouTube:参考にした解説動画 3</a></p></li><li><p><a href=\"https://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026\" data-has-link=\"true\">Counterpoint Research:Global Humanoid Robot Shipments Soar Nearly 300% YoY in H1 2026</a></p></li><li><p><a href=\"https://smartanalyticsglobal.com/global-humanoid-robot-shipments-2026-agibot-unitree/\" data-has-link=\"true\">Smart Analytics Global:2026年上半期の人型ロボット出荷</a></p></li><li><p><a href=\"https://www.1x.tech/order\" data-has-link=\"true\">1X:NEO 注文ページ</a></p></li><li><p><a href=\"https://autolab.berkeley.edu/assets/publications/media/GOFE-Can-Close-the-100000-Year-Robot-Data-Gap-Science-Robotics-Aug-2025-scirobotics.aea7390.pdf\" data-has-link=\"true\">Ken Goldberg, Science Robotics(2025年8月):Good Old-Fashioned Engineering Can Close the 100,000-Year \"Data Gap\" in Robotics</a></p></li><li><p><a href=\"https://generalistai.com/blog/gen-1.5\" data-has-link=\"true\">Generalist:GEN-1.5(2026年8月19日)</a></p></li></ul><p></p>", "toc": [{"id": "index_D_tmh6uh", "text": "参考にした3本の解説動画は、冒頭1分半で問い・結論・目次を全部出していた"}, {"id": "index_0LUCW9SF", "text": "台本テンプレートは「解説回」と「実験回」の2種類に分けた"}, {"id": "index_bTgs9pio", "text": "台本に入れた自分の数字は、台帳と公開記事で裏を取った"}, {"id": "index_vjneh5nj", "text": "動画は、Open JTalk・Playwright・ffmpeg で1文ずつ組み立てた"}, {"id": "index_YxOgXNy5", "text": "Open JTalk は、ひと息が長い文の最後を1音0.47秒に引き伸ばしていた"}, {"id": "index_0fVKV772", "text": "codex の敵対的検証で、台本の結論は「成立していない」と判定された"}, {"id": "index_t6LAP1jb", "text": "codex が新しく挙げた事実は、一次情報で確かめてから採用した"}, {"id": "index_YhZ9JBp8", "text": "根拠の弱い数字は足さずに削った"}, {"id": "index_gc6toY6y", "text": "読み上げは「爪先」を「つまさき」と読んでいた"}, {"id": "index_HCnc4FWz", "text": "カード上の太線と、角の立った字幕の縁取りをやめた"}, {"id": "index_mlpaKj8r", "text": "自己紹介から「秋田」「29年」「毎日」を外し、ロゴを差し替えた"}, {"id": "index_9szChvZM", "text": "背景と文字の色を、記事サムネイルの #F5F5F0 と #0D0D0D に合わせた"}, {"id": "index_msVyMuQC", "text": "30cmの食い違いは、アームが外れた距離ではなく、爪先の位置の決め方の違いだった"}, {"id": "index_Apnn9rBf", "text": "打ち出し・説明・補足の3段階で、画面に強弱をつけた"}, {"id": "index_sOZrkg72", "text": "動画ファイルは、iCloud ではなく KIOXIA の videos/ に置いた"}, {"id": "index_52jeog_s", "text": "量産のベースとして video_rules.md と制作スクリプト一式を残した"}, {"id": "index_3J7_uzPW", "text": "まだ決まっていない8項目は、未決のまま残した"}, {"id": "index_sWhPqJ9y", "text": "制作スクリプトの全文"}, {"id": "index_c3VMNBPy", "text": "実行ログ"}, {"id": "index_o0bVwuXE", "text": "codex への検証依頼の全文"}, {"id": "index_tELWWv5H", "text": "codex から返ってきた検証結果の全文"}, {"id": "index_9Y98_SCr", "text": "video_rules.md の全文"}, {"id": "index_lmKEOQqP", "text": "参照した情報"}]}
\ No newline at end of file
+{"day": "LOG330", "series": "ロボティクスデザイン", "published": "2026-10-11T13:46:46.000Z", "title": "解説動画をAIと作る ― 型の分析から敵対的検証まで、試作1本で決めたこと", "slug": "making-explainer-video-with-ai", "cover": null, "thumb": "/thumbs/r-making-explainer-video-with-ai.html", "lang": "JP", "description": "YouTube解説動画の試作1本を、参考動画の型の分析、台本、音声合成と図解の自動組み立て、codexの敵対的検証、デザイン修正まで進めた記録。決めたルールを量産のベースとして残した。", "updated": "2026-10-12T00:09:46.000Z", "body": "<!--TOC--><p></p><p>Pixels to Motors の YouTube で解説動画を出すために、Claude と対話しながら試作を1本作りました。題材は「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」で、尺は13分50秒です。</p><p>進め方は次の順番でした。参考にしたい解説動画3本の文字起こしから、構成・話し方・補足説明の型を取り出す。型を台本テンプレートにまとめ、テンプレートに沿って台本を書く。台本を、読み上げ音声・図解・字幕・実機の映像に分けてコードで組み立て、MP4 に書き出す。できた台本を codex(OpenAI のコーディング AI)に渡して、論破するつもりで検証してもらう。指摘とデザインの修正を重ねる。最後に、量産するときのベースとしてルール文書とスクリプト一式を残す。</p><p>本記事は、その過程で決めたことと、決めた理由の記録です。間違えたところ(根拠のない数字を書いた、30cm の例が誤解を招く、など)も消さずに残しています。制作スクリプトの全文、実行ログ、codex の検証結果の全文は記事の後半に載せています。</p><div class=\"video\"><video controls playsinline preload=\"metadata\" src=\"/media/s-1920x1080_b273c60b-57bf-4356-b2b2-1b5a4023565f.mp4\"></video></div><p></p><h2 id=\"index_D_tmh6uh\">参考にした3本の解説動画は、冒頭1分半で問い・結論・目次を全部出していた</h2><p>参考にしたのは、時事ニュースを統計と自分の体験で検証する形式の解説動画3本です(YouTube「海外Yパパラジオ」の、会社への侵入と暗数の回、AI とサイバー攻撃の回、溺れている人を AI に見つけさせる実験の回)。動画のページは Claude からは開けなかったため、文字起こしを貼って分析しました。</p><p>3本とも、骨格が同じでした。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>時間の目安</p></th><th colspan=\"1\" rowspan=\"1\"><p>中身</p></th><th colspan=\"1\" rowspan=\"1\"><p>実例</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>0:00〜0:45</p></td><td colspan=\"1\" rowspan=\"1\"><p>挨拶より先に、時事ネタか視聴者のコメントから入る</p></td><td colspan=\"1\" rowspan=\"1\"><p>「最近漏洩多くないですか?」というコメント</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>〜0:50</p></td><td colspan=\"1\" rowspan=\"1\"><p>決まったジングルと自己紹介</p></td><td colspan=\"1\" rowspan=\"1\"><p>住んでいる場所から世界を見ている、と名乗る</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>〜1:10</p></td><td colspan=\"1\" rowspan=\"1\"><p>その回の問いを疑問文1つで言う</p></td><td colspan=\"1\" rowspan=\"1\"><p>「会社への侵入は本当に急に増えたのか」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>〜1:30</p></td><td colspan=\"1\" rowspan=\"1\"><p>結論か鍵になる言葉を先に出し、自分の立場を言う</p></td><td colspan=\"1\" rowspan=\"1\"><p>「変わったのは手口じゃなく攻撃の値段」「増えていないと言いたいわけじゃない」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>〜1:50</p></td><td colspan=\"1\" rowspan=\"1\"><p>「大きく○つあります」で目次を言う</p></td><td colspan=\"1\" rowspan=\"1\"><p>3〜5項目</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>本編</p></td><td colspan=\"1\" rowspan=\"1\"><p>「1つ目、○○です」で区切る。各節の最後の問いが次の節の題になる</p></td><td colspan=\"1\" rowspan=\"1\"><p>「じゃあ一斉に調べ始めたら何が出てくるのか」→ 次の節</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>終盤</p></td><td colspan=\"1\" rowspan=\"1\"><p>「ここからが僕の一番言いたいこと」と口で言ってから意見を話す</p></td><td colspan=\"1\" rowspan=\"1\"><p>事実と意見の境目を視聴者に示す</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>締め</p></td><td colspan=\"1\" rowspan=\"1\"><p>冒頭の問いに答え、次にニュースを見たときに考える問いを1つ渡す</p></td><td colspan=\"1\" rowspan=\"1\"><p>「起きた数か、見つかった数か、一度だけ考えてみて」</p></td></tr></tbody></table></div><p>冒頭の1分半で「何の話か・何に答えるか・どの立場で話すか・何章あるか」がそろいます。ほかに共通していたのは、視聴者の思い込みを1つ置いてデータで崩し、崩したあとに逆方向の早合点も止めること。数字は必ず比較で言い直すこと(「15倍ですよ」)。専門用語は使った直後に1文で説明すること。限界を自分から言うこと(「動画2本だけなので、性能の評価というより最初の手触り」)。</p><p>溺れている人の回だけは型が少し違い、道具の説明 → なぜ難しいか → 試した結果 → 失敗の深掘り → 見せ方を変えて再実験 → 見立て、の順でした。山場が「失敗が見つかる場面」にある実験回です。</p><p></p><h2 id=\"index_0LUCW9SF\">台本テンプレートは「解説回」と「実験回」の2種類に分けた</h2><p>型を、パートごとの秒数とセリフの穴埋め(例「先に結論を言っちゃうと、変わったのは{A}じゃなくて{B}なんですよ」)にして、解説回と実験回の2種類のテンプレートにしました。収録前に確認するチェックリスト(専門用語は初出で1文の説明があるか、数字に出典の名前が付いているか、など)も付けています。</p><p>試作の題材は「フィジカルAIの現在地」で、問いの形にして「ロボットにもChatGPTの瞬間はもう来たのか」としました。解説回のテンプレートに当てはめ、外部の数字(人型ロボットの出荷台数、1X の家庭用ロボット NEO、ゴールドバーグさんの「10万年のデータの壁」、Generalist の学習データ量)は Web で一次情報を確認して入れました。</p><p></p><h2 id=\"index_bTgs9pio\">台本に入れた自分の数字は、台帳と公開記事で裏を取った</h2><p>台本の節2は、自宅の机でロボットアーム(mechArm 270-Pi)を動かしてきた体験談です。ここに入れた数字は、Mac のフォルダと外付け SSD(KIOXIA)をつないでもらい、記録で確かめました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>台本の数字</p></th><th colspan=\"1\" rowspan=\"1\"><p>確かめた記録</p></th><th colspan=\"1\" rowspan=\"1\"><p>結果</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>掴んで置く作業の試行回数</p></td><td colspan=\"1\" rowspan=\"1\"><p>台帳 pick_place_ledger_v3.csv</p></td><td colspan=\"1\" rowspan=\"1\"><p>657行。テスト1行を除いて656回(2026年6月18日〜7月20日)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>爪先の位置の対応表の点数</p></td><td colspan=\"1\" rowspan=\"1\"><p>公開記事 LOG283</p></td><td colspan=\"1\" rowspan=\"1\"><p>約170万点</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>計算ツールとデジタルツインの爪先の食い違い</p></td><td colspan=\"1\" rowspan=\"1\"><p>arm_kinematics.md、公開記事 LOG222</p></td><td colspan=\"1\" rowspan=\"1\"><p>同じ関節角で300mm以上</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>黙って捨てられる指令</p></td><td colspan=\"1\" rowspan=\"1\"><p>公開記事 LOG257</p></td><td colspan=\"1\" rowspan=\"1\"><p>j1(肩)が約90.7度を超えると send_angles がエラーなしで無視される</p></td></tr></tbody></table></div><p>1つ、間違いがありました。試行回数を最初「600回以上」と書いていましたが、これは記録を見ずに書いた数字でした。記事を検索した段階では LOG255 の「100試行ぶん」しか見つからず、いったん「100回以上」に直しました。そのあと台帳を数えたら656回あり、「650回以上」に直しています。結果として最初の数字は大きく外れていませんでしたが、根拠なしに書いた数字だったことに変わりはありません。台帳の集計結果は、後半の実行ログに全量で載せています。</p><p></p><h2 id=\"index_vjneh5nj\">動画は、Open JTalk・Playwright・ffmpeg で1文ずつ組み立てた</h2><p>台本から動画までを、クラウドの作業環境でコードにして組み立てました。</p><div class=\"embed\"><iframe src=\"/embeds/making-explainer-video-with-ai-vpZFxoYF.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p></p><ol><li><p>narration.py に台本を書く。1文ごとに「場面の名前・字幕の文・後ろの間(秒)」の組にする</p></li><li><p>tts2.py で、1文ずつ読み上げ音声を作る</p></li><li><p>scenes.py に、場面ごとの図解を HTML で書く</p></li><li><p>render.py で、図解・テロップ・字幕を Playwright(ブラウザを自動で動かす道具)で画像にする</p></li><li><p>assemble.py で、1文ごとに「図解か実機映像」+「字幕」を ffmpeg で数秒の映像にし、全部つないで音声と合わせる。字幕ファイル(srt)も同時に出す</p></li></ol><p>1文を1つの映像のかたまりにしたのは、あとで1文だけ直したときに、その文の映像だけ作り直せばよいからです。実際、自己紹介の1文を差し替えたときは、作り直したのは3つのかたまりだけでした。</p><p>読み上げ音声には、Open JTalk(日本語の音声合成エンジン)の男声を使いました。より自然な音声エンジン(VOICEVOX など)は、作業環境から配布元の GitHub にアクセスできず、取り込めませんでした。Open JTalk は Ubuntu のパッケージで入れられたので、仮の声として使っています。抑揚は機械的で、公開するなら本人の声で録り直す前提です。</p><p>実機の映像は、Mac の動画フォルダにあった5本から、赤いブロックを掴んで箱に置く実機の映像(LOG124)、アーム単体の動き(LOG123)、Unity のデジタルツインの映像2本を選びました。LOG123 と LOG124 の右下には、以前に使った編集ツールの透かし(clideo.com)が入っていたため、ffmpeg の delogo でぼかし、字幕の後ろに敷く黒い帯の下に隠しています。</p><p></p><h2 id=\"index_YxOgXNy5\">Open JTalk は、ひと息が長い文の最後を1音0.47秒に引き伸ばしていた</h2><p>最初に書き出した動画(13分33秒)を見てもらったところ、次の指摘がありました。</p><blockquote><p>動画の中で音声が間延びしたところが何箇所か存在します。 それだけ修正を行うことはできますか?</p></blockquote><p>Open JTalk は、音声と一緒に「どの音を何秒出したか」の記録を出せます。これを全153文について調べると、6文で、文の後半の音が1つあたり約0.47秒になっていました。普段は0.1秒前後なので、3〜4倍に引き伸ばされています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>文</p></th><th colspan=\"1\" rowspan=\"1\"><p>引き伸ばされていた部分</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>現実の世界で物に触れて動かす技術のことなんですよ。</p></td><td colspan=\"1\" rowspan=\"1\"><p>「…なんですよ」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ChatGPTのようなAIの学習に使われたインターネットの文章を、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「…使われた」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ChatGPTは10万年分の教科書で育ったけれど、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「育ったけれど」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ここで、じゃあロボットにChatGPTの瞬間は当分来ない、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「当分来ない」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1Xが家の中で遠隔操作しながらデータを集めているのも、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「いるのも」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>出荷されたロボットの多くがデータ集めに回っているのも、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「のも」</p></td></tr></tbody></table></div><p>共通していたのは、読点なしでひと息に読む区間が長いことでした。ひと息の区間が長いと、その区間の最後の言葉が引き伸ばされます。</p><p>直し方は、読み上げ用の原稿だけに読点を足して区切ることです。字幕の文は変えません。最初は読点の位置で自動的に区切りましたが、「遠隔操作しなが/ら」のように単語の途中で切れた文があり、区切る位置を手で指定し直しました。その後、台本を書き直したときに、作った音声をすべて検査し、1音が0.3秒を超える文を見つけたら区切って作り直す処理を tts2.py に入れました。区切っても直らない文は、narration.py の OVERRIDE に「読み上げ用の読点の位置」を書いて直しています。</p><p></p><h2 id=\"index_0fVKV772\">codex の敵対的検証で、台本の結論は「成立していない」と判定された</h2><p>台本を公開前に codex に渡し、「この台本を論破するつもりで」検証してもらいました。依頼ファイルには、台本の全文に文番号(S001〜S152)を振り、使った出典の表と、検証してほしい観点(事実と数字・論理・抜けている反論・言い方)を付けています。依頼ファイルの全文と、返ってきた検証結果の全文は、後半に載せています。</p><p>判定は「直してから公開」、結論の評価は「現状では成立していない」でした。重大とされたのは次の4点です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>codex の指摘</p></th><th colspan=\"1\" rowspan=\"1\"><p>中身</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>「ChatGPTの瞬間」が未定義</p></td><td colspan=\"1\" rowspan=\"1\"><p>写真に質問して答えが返った体験1つから、「見て判断する部分には来た」と言い切っている</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>異種データの「1700分の1」比較</p></td><td colspan=\"1\" rowspan=\"1\"><p>人が文章を読む時間(10万年)と、作業を記録した時間(57年)は、意味も単位も違う</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>実機の問題を「データ不足」の例にしている</p></td><td colspan=\"1\" rowspan=\"1\"><p>計算ツールとデジタルツインの食い違いや、指令が無視される問題は、学習データの不足とは別の話</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Generalist の最新発表の抜け</p></td><td colspan=\"1\" rowspan=\"1\"><p>4月の GEN-1 を最新として話していたが、8月に GEN-1.5 が出ていた</p></td></tr></tbody></table></div><p>結論そのものについて、codex は次の形なら条件つきで成立すると書いていました。</p><blockquote><p>僕の机では、画像に質問する入口はずっと簡単になりました。一方、物を確実に動かすには、機体や環境とのすり合わせがまだ必要でした。その負担を減らす研究は急速に進んでいます。でも、いろんな家庭で安全に家事を任せられる転換点まで来たかは、まだ判断できません。</p></blockquote><p>台本の結論は、この形に差し替えました。</p><p></p><h2 id=\"index_t6LAP1jb\">codex が新しく挙げた事実は、一次情報で確かめてから採用した</h2><p>codex の指摘には、台本になかった新しい事実が含まれていました。採用する前に、それぞれの一次情報を開いて確かめています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>codex が挙げた事実</p></th><th colspan=\"1\" rowspan=\"1\"><p>確かめた一次情報</p></th><th colspan=\"1\" rowspan=\"1\"><p>確かめた内容</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Generalist が8月に GEN-1.5 を発表</p></td><td colspan=\"1\" rowspan=\"1\"><p>Generalist 公式ブログ(2026年8月19日)</p></td><td colspan=\"1\" rowspan=\"1\"><p>3〜12秒の実演1回から新しい作業をこなす。10種類の作業で平均成功率59%。本人たちも「作業は簡単で短く、成功率は控えめ」と書いている</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>SAG の分類は「工場や店舗向け」ではない</p></td><td colspan=\"1\" rowspan=\"1\"><p>Smart Analytics Global の発表</p></td><td colspan=\"1\" rowspan=\"1\"><p>原文は「Industrial and commercial applications」(産業・商業用途)。出荷の総数も約19,100台で、Counterpoint の22,000台超とは違う</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1X NEO の月499ドルのプランは配送時期が別</p></td><td colspan=\"1\" rowspan=\"1\"><p>1X の注文ページ</p></td><td colspan=\"1\" rowspan=\"1\"><p>2万ドルの早期購入枠は「US Deliveries start 2026」。月499ドルは「Standard Delivery」で日付の記載なし。最初は基本的な自律機能で、知らない作業は予約した時間に担当者が遠隔で支援する</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ゴールドバーグさんの10万年は「文章だけ」ではない</p></td><td colspan=\"1\" rowspan=\"1\"><p>Science Robotics の論説(2025年8月)</p></td><td colspan=\"1\" rowspan=\"1\"><p>現代の画像と言葉を扱う AI(VLM)の学習資料を、人が読む・見る時間に換算した概算</p></td></tr></tbody></table></div><p>確認できた4点はすべて台本に反映しました。台本の書き直しでは、問いの中の言葉(ChatGPTの瞬間)を冒頭で定義し、話の範囲(自動運転は扱わず、アームと人型ロボットが物を扱う話に絞る)を言い、自分の机の上の話は「僕の機体とソフトの組み合わせでは」と条件を付けて話す形にしています。</p><p></p><h2 id=\"index_YhZ9JBp8\">根拠の弱い数字は足さずに削った</h2><p>codex の指摘を反映する前に、次の注意がありました。</p><blockquote><p>チェックして修正点を検討して反映したいです(過剰なデータは特に問題になりやすいので注意が必要です)</p></blockquote><p>そこで、指摘に答えるために数字を足すのではなく、論旨に必要でないものは削る方針にしました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>削った数字</p></th><th colspan=\"1\" rowspan=\"1\"><p>削った理由</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1700分の1、約57年</p></td><td colspan=\"1\" rowspan=\"1\"><p>意味と単位の違う数字の割り算で、比較として成り立たない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>成功率99%超、ロボットのデータ約1時間</p></td><td colspan=\"1\" rowspan=\"1\"><p>会社の自己申告で、評価の条件を説明しないと誤解される</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>爪先の対応表の約170万点</p></td><td colspan=\"1\" rowspan=\"1\"><p>シミュレーターの中で作った表なのに、実機で170万回測ったように聞こえる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>AGIBOT 44%・Unitree 31% の内訳</p></td><td colspan=\"1\" rowspan=\"1\"><p>なくても話が変わらない</p></td></tr></tbody></table></div><p>残した数字には「推計では」「会社の発表では」を口で付けています。調査会社2社で用途の数字が逆向きに見える件は、「総数も分類も違うので、単純な裏表としては比べられません」と言う形にしました。</p><p></p><h2 id=\"index_gc6toY6y\">読み上げは「爪先」を「つまさき」と読んでいた</h2><blockquote><p>あと「爪先」をつまさきと言っていますが、アームの場合は「つめさき」の方の読み方が適切かと思います(つまさきだと、足の指先に思ってしまう)</p></blockquote><p>Open JTalk の辞書では「爪先」はツマサキでした。読み上げ用の読み替え表に「爪先 → つめさき」を足しています。同じ確認で、「90度ちょっとを超える値を送ると」の「値」がネと読まれていたことも見つけ、「値を送る → あたいを送る」を足しました。ほかに直した読みは「4分の3」(ヨンプンノサン → よんぶんのさん)、「1700分の1」(センナナヒャップンノイチ → せんななひゃくぶんのいち)、英語の名前(ChatGPT、Counterpoint、ikpy など)です。新しい固有名や数字を入れたら、エンジンの解析結果のカタカナを出して目で確認する手順にしました。</p><p></p><h2 id=\"index_HCnc4FWz\">カード上の太線と、角の立った字幕の縁取りをやめた</h2><blockquote><p>それとデザイン面で直したいのがカードの上に太いラインがあるのはいわゆるAIっぽいこなしなので違う表現にしたいです。</p></blockquote><p>最初の図解は、2つのものを対比するとき、白いカードの上辺に青とオレンジの太い線を引いていました。これをやめ、2列の間を細い縦線(2px)で区切り、色は文字の色だけで見分ける形にしました。同じ理由で、名前テロップの左端の太い線も外しています。</p><p></p><figure><img src=\"/media/s-1944x604_v-frms_webp_1ae69cf2-9b0a-420a-a0b8-a54b8fd6b7d8.png\" alt=\"\"><figcaption></figcaption></figure><p></p><blockquote><p>さらにフォントですが、フォントの黒縁が角が立っていているところが散見されます。 角を落とした黒縁に変更してください。</p></blockquote><p>字幕は CSS の文字の縁取り(-webkit-text-stroke)で作っていました。この方式は、線の角を丸める指定ができず、「宅」や「机」の角に尖った黒い角が出ます。字幕を SVG の文字に作り替え、線の角を丸くする指定(stroke-linejoin: round)を入れました。</p><p></p><figure><img src=\"/media/s-1100x610_v-fs_webp_3b59dae8-f9d7-4d46-ab1e-9cf7a3b11305.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_mlpaKj8r\">自己紹介から「秋田」「29年」「毎日」を外し、ロゴを差し替えた</h2><blockquote><p>動画の自己紹介のところで、29年秋田で、というところは削除で。 本業のデザイナーをしながらロボティクスの研究、開発を行っている、などの紹介に修正してください。</p></blockquote><blockquote><p>その記録を毎日公開、だと毎日ロボットを触っているかのような説明なので、「毎日」は削除した方がいい。</p></blockquote><p>自己紹介の文は「僕は本業のデザイナーをしながら、ロボティクスの研究と開発をしています。自宅の机の上でロボットアームを動かして、その記録を公開しています。」に固定しました。動画の中だけでなく、台本のドキュメントと codex への依頼ファイルにも同じ文言が残っていたので、そちらも直しています。</p><p>画面右上とテロップの「PIXELS TO MOTORS」の文字は、Pixels to Motors のロゴ画像に差し替えました。もらったロゴ画像(灰色の地に黒)から、地の明るさとの差で透明度を計算して切り抜き、文字の色をスライドの黒にそろえています。</p><p></p><figure><img src=\"/media/s-1920x1080_v-frms_webp_b7837ab7-c1c0-4d01-943b-1d3aa04345db.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_9szChvZM\">背景と文字の色を、記事サムネイルの #F5F5F0 と #0D0D0D に合わせた</h2><blockquote><p>動画の背景色やテキスト色を記事のサムネイルと同じ色に変更してください(差し色で使っているオレンジや水色は少し濃くするくらいでカラーを使うのはOKです) 色だけの話です。他は修正しないでください。</p></blockquote><p>記事のサムネイル画像から色を拾い、次の色に置き換えました。レイアウトと文言には手を付けていません。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>用途</p></th><th colspan=\"1\" rowspan=\"1\"><p>変更前</p></th><th colspan=\"1\" rowspan=\"1\"><p>変更後</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>背景</p></td><td colspan=\"1\" rowspan=\"1\"><p>#F4F2ED</p></td><td colspan=\"1\" rowspan=\"1\"><p>#F5F5F0</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>文字</p></td><td colspan=\"1\" rowspan=\"1\"><p>#17191E</p></td><td colspan=\"1\" rowspan=\"1\"><p>#0D0D0D</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>薄い文字</p></td><td colspan=\"1\" rowspan=\"1\"><p>#5F636B</p></td><td colspan=\"1\" rowspan=\"1\"><p>#888888</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>区切り線</p></td><td colspan=\"1\" rowspan=\"1\"><p>#CFCAC0</p></td><td colspan=\"1\" rowspan=\"1\"><p>#D6D6D1</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>オレンジ(強調)</p></td><td colspan=\"1\" rowspan=\"1\"><p>#D9542B</p></td><td colspan=\"1\" rowspan=\"1\"><p>#C4471F</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>青(対比のもう一方)</p></td><td colspan=\"1\" rowspan=\"1\"><p>#1F7FB0</p></td><td colspan=\"1\" rowspan=\"1\"><p>#1A6C98</p></td></tr></tbody></table></div><p>オレンジと青は、色覚の違いがあっても見分けられるかを検証するスクリプト(validate_palette.js)にかけ、新しい背景色の上で全項目を通過することを確かめてから使っています。検証の出力は後半の実行ログにあります。</p><p></p><h2 id=\"index_msVyMuQC\">30cmの食い違いは、アームが外れた距離ではなく、爪先の位置の決め方の違いだった</h2><blockquote><p>30cmも違うって合っている? 30mmとかじゃなくて?なんか例が良くない?</p></blockquote><p>数字そのものは記録どおりでした。公開記事 LOG222 に「同じ関節角を入れても、先端位置が300mm以上違う」とあります。ただ、同じ記事に理由も書かれていました。ikpy(関節の角度から先端の位置を計算する一般的なライブラリ)には、グリッパーの3D形状もデジタルツインの物理も入っていません。一方、デジタルツインで作った表の爪先の位置は、Unity の中でグリッパー先端の3Dモデルが実際に来た場所です。2つは、そもそも爪先の位置の決め方が違っていました。</p><p>mechArm 270 の腕の長さは約27cmです。「30cm食い違う」とだけ聞くと、腕の長さより大きく外れたように受け取れてしまいます。差し替え候補として、LOG222 にある実機のずれ(赤いブロックが6回とも同じ向きに、箱の中心から約5cmずれて落ちた件)も挙げましたが、30cm のまま説明を足す形を選んでもらいました。台本に「これは、アームが30cm外れた、という意味ではありません」「計算ツールには、グリッパーの形も、デジタルツインの物理も入っていなくて、爪先の位置の決め方そのものが違っていたんです」の2文を足し、図にも同じ説明を1行入れています。</p><p></p><figure><img src=\"/media/s-1280x720_v-fms_webp_89ef024b-47c1-4439-9d46-42759eb70eaa.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_Apnn9rBf\">打ち出し・説明・補足の3段階で、画面に強弱をつけた</h2><blockquote><p>全部じゃなくていいのですが、動画内の説明テキストの内容を強弱をつけるようにしたいです。 打ち出しを強くしたい、簡潔にわかりやすく内容を伝えたい、タイトルをして使いたいなどの時はテキストを大きくわかりやすくしたり、わかりやすい図解を入れ込んだりして動画内の内容がうまく伝わるように変化をつけたいです。</p></blockquote><p>参考にした動画の画面を見ると、小さな色付きの前置き(例「韓国の甲状腺がん」)の下に、1〜2行の大きな文(「診断は15倍/亡くなる人は増えず」)を置いていました。章の始まりには「今日の話 → 2つ目 → 見つからない」のように大きなキーワードを置き、割合は点の並びで見せていました。</p><p>これを、背景・文字・差し色はそのままで取り入れ、40ある場面のうち11場面だけ切り替えました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>段階</p></th><th colspan=\"1\" rowspan=\"1\"><p>使った場面</p></th><th colspan=\"1\" rowspan=\"1\"><p>見た目</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>打ち出し</p></td><td colspan=\"1\" rowspan=\"1\"><p>冒頭の問いかけ、結論、「すり合わせの仕事」、データの壁、比べ方の注意、最後の答え(6場面)</p></td><td colspan=\"1\" rowspan=\"1\"><p>色付きの前置き(44px)+ 1〜2行の大きな文(110〜150px)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>章の扉</p></td><td colspan=\"1\" rowspan=\"1\"><p>4つの章の始まり</p></td><td colspan=\"1\" rowspan=\"1\"><p>「今日の話」→「1つ目」→ 大きなキーワード(200px・オレンジ)→ 章の問い</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>点の並び</p></td><td colspan=\"1\" rowspan=\"1\"><p>中国メーカーの割合</p></td><td colspan=\"1\" rowspan=\"1\"><p>100個の点のうち97個に色を付ける</p></td></tr></tbody></table></div><p>打ち出しの文は、字幕をそのまま大きくするのではなく、その場面で一番残したい一言に縮めています。たとえば字幕が「この2つは、AIの学習データが足りない、という話とは別です。」の場面は、「データ不足じゃない/すり合わせの仕事」にしました。</p><p></p><figure><img src=\"/media/s-1944x604_v-frms_webp_cf142cf2-c7a5-4f0f-88d3-ae65e0605315.png\" alt=\"\"><figcaption></figcaption></figure><p></p><figure><img src=\"/media/s-1944x604_v-frms_webp_077bbf3b-9bcf-4932-bd31-a613c2b4d0eb.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_sOZrkg72\">動画ファイルは、iCloud ではなく KIOXIA の videos/ に置いた</h2><blockquote><p>動画はデータが重くなるのでSSDの容量を圧迫するので別のフォルダに保管するようにしてください。</p></blockquote><p>動画ファイル(mp4)は、外付け SSD の KIOXIA に <code>r2r2r-data/videos/{動画名}/</code> を作って置き、iCloud からは消しました。字幕の srt と codex への依頼ファイルは軽いので iCloud に残しています。KIOXIA には、YouTube に上げる高画質の作業用(約47MB)と、確認用の圧縮版(約30MB)の2本を置いています。</p><p>ここで1つ問題が起きました。作業環境から Mac へ一度に送れるファイルは30MBまでなので、作業用の動画を12MBずつ4つに分けて送り、Mac 側でつなぎ直しました。ところが、つないだファイルのチェック値(md5)が元と一致しません。調べると、動画の先頭部分を含む1つ目の断片だけが、元より5,875バイト大きくなり、中身も化けていました。2〜4つ目の断片は元と一致していました。1つ目の断片だけを gzip で圧縮した形で送り直し、Mac 側で展開してからつなぐと、チェック値が元と一致しました。転送の途中で、動画の先頭部分だけが何らかの変換を受けたと考えていますが、原因までは確かめていません。</p><p></p><h2 id=\"index_52jeog_s\">量産のベースとして video_rules.md と制作スクリプト一式を残した</h2><p>ここまでに決めたことを、動画制作ルール video_rules.md にまとめました。記事の article_rules.md と同じく、動画を作るときに最初に読む正本です。プロジェクトの資料と、iCloud の <code>動画/_制作ベース/</code> の両方に置き、同じフォルダに制作スクリプト一式とロゴの透過 PNG を入れています。</p><p>video_rules.md は8つの節でできています。動画の構成(解説回の型と時間配分)、台本のルール(codex の検証で直した点から作った11項目)、ナレーション(読み替え表と間延びの検査)、画面デザイン(色・文字の大きさ・図の型・強弱の3段階・やらない表現)、字幕、実機の映像とテロップ、書き出しと保存、まだ決まっていない項目、です。全文は後半に載せています。</p><p>MASTER(プロジェクト全体の方針文書)の保存先テーブルと、KIOXIA の README.md のフォルダ構成には、videos/ と _制作ベース の行がまだありません。この2か所の追記は、本人が行う予定です。</p><p></p><h2 id=\"index_3J7_uzPW\">まだ決まっていない8項目は、未決のまま残した</h2><p>量産の前に決めることが残っています。video_rules.md の8節に、いまの状態と決めることを並べています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>いまの状態</p></th><th colspan=\"1\" rowspan=\"1\"><p>決めること</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ナレーションの声</p></td><td colspan=\"1\" rowspan=\"1\"><p>Open JTalk の仮音声。抑揚が機械的</p></td><td colspan=\"1\" rowspan=\"1\"><p>本人の声で録るか、品質の高い音声合成にするか。srt に1文ごとの時間が入っているので、それに合わせて録れば映像はそのまま使える</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>図解の動き</p></td><td colspan=\"1\" rowspan=\"1\"><p>すべて静止画</p></td><td colspan=\"1\" rowspan=\"1\"><p>数字のカウントアップや矢印が伸びるなどの動きを入れるか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>背景の映像</p></td><td colspan=\"1\" rowspan=\"1\"><p>実機映像は自己紹介・節2・エンディングだけ</p></td><td colspan=\"1\" rowspan=\"1\"><p>解説中も実機映像を薄く敷くか、図解だけにするか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM・効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>なし</p></td><td colspan=\"1\" rowspan=\"1\"><p>使うか。使うなら YouTube で収益化できる音源から選ぶ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ジングル</p></td><td colspan=\"1\" rowspan=\"1\"><p>文言だけ</p></td><td colspan=\"1\" rowspan=\"1\"><p>音とロゴの動きを付けるか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>サムネイル</p></td><td colspan=\"1\" rowspan=\"1\"><p>まだない</p></td><td colspan=\"1\" rowspan=\"1\"><p>記事サムネイルと同じ配色・線画のトーンで作るか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>番組名</p></td><td colspan=\"1\" rowspan=\"1\"><p>未定</p></td><td colspan=\"1\" rowspan=\"1\"><p>決める</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>概要欄のチャプター</p></td><td colspan=\"1\" rowspan=\"1\"><p>まだない</p></td><td colspan=\"1\" rowspan=\"1\"><p>srt の節の開始時刻から作れる</p></td></tr></tbody></table></div><p></p><h2 id=\"index_sWhPqJ9y\">制作スクリプトの全文</h2><p>作業環境(Ubuntu、Python 3.13、ffmpeg 6.1.1、Playwright の Chromium、Noto Sans CJK JP)で動かした最終版です。Open JTalk は <code>apt-get install open-jtalk open-jtalk-mecab-naist-jdic hts-voice-nitech-jp-atr503-m001</code> で入れています。実機の映像は <code>src/c1.mp4</code>(LOG123)、<code>src/c2.mp4</code>(LOG124)、<code>src/c3.mov</code>(デジタルツインの並列スキャン)、<code>src/c4.mov</code>(デジタルツイン)として置き、ロゴは <code>logo_dark.png</code> として同じフォルダに置いて動かします。</p><h3 id=\"index_ZLxuvA9o\">narration.py(台本・読み替え表・読み上げ用の読点)</h3><pre><code class=\"language-python\"># -*- coding: utf-8 -*-\n# 台本「ロボットにもChatGPTの瞬間はもう来たのか」読み上げ原稿(codex 検証反映版)\n# 各要素: (scene_id, 字幕テキスト, 後ろの間[秒]) 間を省略すると 0.35 秒\nSEGS = [\n # 0 フック\n (\"hook_ship\", \"調査会社Counterpointの推計では、今年の上半期に出荷された人型ロボットは、\"),\n (\"hook_ship\", \"世界で2万2000台を超えました。前の年の同じ時期の、およそ4倍です。\", 0.6),\n (\"hook_forecast\", \"年末までには、5万台を超えると予想されています。\"),\n (\"hook_forecast\", \"家事をする人型ロボットの予約販売も、去年の秋から始まっています。\", 0.6),\n (\"hook_question\", \"こういうニュースを見ると、ChatGPTが出たときみたいに、\"),\n (\"hook_question\", \"ロボットも一気に賢くなって、家に来るのはもうすぐ、って感じませんか。\", 0.9),\n # 1 自己紹介\n (\"title\", \"Pixels to Motors。こんにちは、エイイチです。\"),\n (\"title\", \"僕は本業のデザイナーをしながら、ロボティクスの研究と開発をしています。\"),\n (\"title\", \"自宅の机の上でロボットアームを動かして、その記録を公開しています。\"),\n (\"title\", \"作る側の机の上から見える、フィジカルAIの話をします。\", 0.8),\n # 2 今日の問い\n (\"question\", \"今日の問いは、ロボットにもChatGPTのような瞬間は、もう来たのか、です。\"),\n (\"question\", \"ここでいうChatGPTの瞬間は、専門家じゃなくても、\"),\n (\"question\", \"いろんな仕事にすぐ使えるようになる転換点、という意味です。\", 0.7),\n (\"define\", \"フィジカルAIっていうのは、現実の世界を認識して、行動するAIのことです。\"),\n (\"define\", \"自動運転もここに入りますが、\"),\n (\"define\", \"今日は主に、アームや人型ロボットが物を扱う話に絞ります。\", 0.6),\n # 3 結論と立場\n (\"answer\", \"先に結論を言っちゃうと、写真を見せて判断させる入口は、ぐっと簡単になりました。\"),\n (\"answer\", \"でも、物を確実に動かすところは、まだ手間がかかるんですよ。\"),\n (\"answer\", \"その手間は大きく2つあります。\"),\n (\"answer\", \"動作と結びついたデータが集めにくいこと。そして、機体や環境とのすり合わせです。\", 0.6),\n (\"stance\", \"先に言っておくと、フィジカルAIはまだ使えない、と言いたいわけじゃないんです。\"),\n (\"stance\", \"どこまで来ていて、どこで手間がかかっているのかを、分けて見たいんです。\", 0.6),\n # 4 目次\n (\"toc\", \"大きく3つあります。\"),\n (\"toc\", \"出荷が4倍になったロボットは、何をしているのか。\"),\n (\"toc\", \"僕の机の上のロボットアームで、何が簡単で、何が難しかったのか。\"),\n (\"toc\", \"そして、データの壁はどれくらい高くて、誰がどう越えようとしているのか。\"),\n (\"toc\", \"最後に僕の見立てを話します。\", 0.9),\n # 5 節1\n (\"c1\", \"1つ目、出荷が4倍になったロボットは、何をしているのか、です。\", 0.7),\n (\"use\", \"Counterpointの推計では、出荷された人型ロボットの6割以上が、\"),\n (\"use\", \"展示やイベントで動きを見せる用途と、研究やデータ生産の用途なんですよ。\", 0.6),\n (\"datacollect\", \"データ生産っていうのは、ロボットが学ぶための動きの記録を作ることです。\"),\n (\"datacollect\", \"人が遠隔で動かして記録するやり方も、自律で動いた記録を残すやり方もあります。\"),\n (\"datacollect\", \"ただ、研究向けの台数が全部データ集めに使われる、という意味ではありません。\"),\n (\"datacollect\", \"それでも、用途の分類に「データ生産」という項目があること自体が、\"),\n (\"datacollect\", \"今の段階をよく表していると思います。\", 0.7),\n (\"conflict\", \"一方で、別の調査会社のSmart Analytics Globalは、\"),\n (\"conflict\", \"同じ半年の出荷を約1万9100台と推計していて、\"),\n (\"conflict\", \"産業・商業用途が7割を超えた、としています。\"),\n (\"conflict\", \"総数も分類も違うので、この2つは、単純な裏表としては比べられません。\"),\n (\"conflict\", \"それに、出荷台数は、そのまま自律的に働いている台数でもないんです。\", 0.7),\n (\"china\", \"もう1つ、SAGの推計では、出荷の97%以上が中国のメーカーです。\", 0.7),\n (\"neo\", \"家のほうも見てみます。\"),\n (\"neo\", \"アメリカの1Xという会社は、家事をする人型ロボット、NEOの予約を、去年の10月に始めました。\"),\n (\"neo\", \"2万ドルの早期購入枠は、今年配送の予定で、月499ドルのプランは、その後です。\"),\n (\"neo\", \"最初は基本的な自律機能だけで、知らない作業は、\"),\n (\"neo\", \"予約した時間に、1Xの担当者が遠隔で手伝う仕組みなんですよ。\"),\n (\"neo\", \"10月11日の時点で、一般のお客さんへの配送開始を示す公式発表は、\"),\n (\"neo\", \"僕が確認した範囲では、見つかっていません。\", 0.7),\n (\"neo_point\", \"これ、ちょっと考えさせられませんか。\"),\n (\"neo_point\", \"売っているのはロボットだけじゃなくて、遠隔で手伝う人と、\"),\n (\"neo_point\", \"そこで動きを覚えていく仕組みごとなんです。\", 0.7),\n (\"bridge1\", \"工場でも家でも、使いながらデータを集める動きが出てきているんですよ。\"),\n (\"bridge1\", \"じゃあ、物を動かすのは、何がそんなに大変なのか。\"),\n (\"bridge1\", \"まず、僕の机の上の話をさせてください。\", 0.9),\n # 6 節2\n (\"c2\", \"2つ目、僕の机の上のロボットアームで、何が簡単で、何が難しかったのか、です。\", 0.7),\n (\"v_real\", \"ここは僕自身の話から始めさせてください。\"),\n (\"v_real\", \"僕は自宅の机で、mechArm 270という小さなロボットアームを動かしています。\"),\n (\"v_real\", \"赤いブロックを掴んで、箱に置く。\"),\n (\"trials\", \"それだけの作業を、失敗も含めて650回以上試して、全部台帳に残しています。\", 0.7),\n (\"easy\", \"まず、簡単だったほうから話します。掴めたかどうかの判定です。\"),\n (\"easy\", \"iPhoneで撮った写真をAIに見せて、「ブロックを掴めていますか」と聞くと、\"),\n (\"easy\", \"はい、いいえで答えが返ってくる。この入口は、驚くほど簡単でした。\"),\n (\"easy\", \"ただ、答えが返ることと、毎回正しいことは別です。\"),\n (\"easy\", \"どこまで任せられるかは、失敗例も含めて確かめる必要があります。\", 0.7),\n (\"hard\", \"難しかったのは、動かすほうでした。\", 0.6),\n (\"gap\", \"ロボットアームの爪先がどこに来るかは、普通は関節の角度から計算で出します。\"),\n (\"gap\", \"ところが、ikpyという計算ツールで出した爪先の位置と、\"),\n (\"v_twin\", \"パソコンの中に作ったアームの分身、デジタルツインで動かした位置が、\"),\n (\"gap\", \"僕の設定では、同じ関節の角度なのに、30cm以上食い違ったんです。\"),\n (\"gap\", \"これは、アームが30cm外れた、という意味ではありません。\"),\n (\"gap\", \"計算ツールには、グリッパーの形も、デジタルツインの物理も入っていなくて、爪先の位置の決め方そのものが違っていたんです。\", 0.7),\n (\"v_scan\", \"だから僕は、分身の中でアームを動かして、\"),\n (\"v_scan\", \"関節の角度と爪先の位置の対応表を作りました。\"),\n (\"v_scan\", \"これは計算の表で、実機で全部の点を測ったわけではありません。\", 0.7),\n (\"silent\", \"もう1つ困ったのが、黙る失敗です。\"),\n (\"silent\", \"僕の機体とソフトの組み合わせでは、肩の関節に、90度ちょっとを超える値を送ると、\"),\n (\"silent\", \"エラーが出ないのに、アームが動かないことがありました。\"),\n (\"silent\", \"ログを1行ずつ見て、やっと気づきました。\", 0.7),\n (\"point2\", \"でも、ポイントはここなんです。\"),\n (\"point2\", \"この2つは、AIの学習データが足りない、という話とは別です。\"),\n (\"point2\", \"モデルや制御が、僕の機体と噛み合っているかを確かめる、すり合わせの仕事でした。\"),\n (\"point2\", \"ほかの環境で学んだことを持ってくる研究も進んでいます。\"),\n (\"point2\", \"それでも、僕の構成でちゃんと動くかは、ここで確かめるしかなかったんです。\", 0.6),\n (\"point2\", \"じゃあ、もう1つの手間、データのほうは、業界全体でどうなっているのか。\", 0.9),\n # 7 節3\n (\"c3\", \"3つ目、データの壁はどれくらい高くて、誰がどう越えようとしているのか、です。\", 0.7),\n (\"years\", \"カリフォルニア大学バークレー校のロボット研究者、\"),\n (\"years\", \"ケン・ゴールドバーグさんが、去年、こんな概算を紹介しています。\"),\n (\"years\", \"画像と言葉を扱うAIの学習資料を、\"),\n (\"years\", \"人が読んだり見たりする時間に換算すると、約10万年規模になる。\", 0.7),\n (\"wall\", \"それに比べて、ロボットが手を動かすための、動作と結びついた経験は、集めにくい。\"),\n (\"wall\", \"ゴールドバーグさんは、これを「10万年のデータの壁」と呼んでいます。\"),\n (\"wall\", \"ただ、ロボットにも同じ量が必要だと分かったわけではありません。\"),\n (\"wall\", \"集めにくい、という問題提起なんです。\", 0.6),\n (\"wall\", \"ここで、じゃあロボットにChatGPTの瞬間は当分来ない、\"),\n (\"wall\", \"と言いたくなるじゃないですか。でも、ちょっと待ってほしいんです。\", 0.7),\n (\"gen\", \"アメリカのGeneralistという会社は、人が手で作業する様子を、\"),\n (\"gen\", \"安い装着型の機材で記録して集めています。\"),\n (\"gen\", \"会社の発表では、去年の11月に27万時間、今年の4月に50万時間を超えました。\", 0.6),\n (\"gen2\", \"さらに8月には、簡単で短い作業なら、\"),\n (\"gen2\", \"数秒の実演を見せるだけで、新しい作業をこなす例も発表しています。\"),\n (\"gen2\", \"ただ、本人たちも、作業は簡単で短く、成功率はまだ控えめだと書いています。\"),\n (\"gen2\", \"それでも、「動かすには毎回大量の専用データが必要」とは、言い切れなくなってきました。\", 0.7),\n (\"compare\", \"ここで1つ、注意があります。\"),\n (\"compare\", \"作業を記録した時間と、10万年という数字は、そのまま比べられません。\"),\n (\"compare\", \"何倍足りないかより、いろんな場面の経験を、どれだけ広く集められるかが問題なんです。\", 0.7),\n (\"dirt\", \"つまり、壁はまだ高い。でも、壁の前でみんなが土を積み始めている。\"),\n (\"dirt\", \"1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、\"),\n (\"dirt\", \"Generalistが人の手作業を記録しているのも、同じ壁に向かっている動きなんです。\", 0.7),\n (\"notall\", \"ただ、ここで「なんだ、まだ全然ダメなのか」で\"),\n (\"notall\", \"片付けてしまうのも違うんです。\"),\n (\"notall\", \"写真を見せて判断させる入口は、僕の机の上でも、もう普通に使えています。\", 0.6),\n (\"notall\", \"じゃあ、この2つが混ざっている今、ロボットのニュースの何を見ればいいのか。\", 0.9),\n # 8 見立て\n (\"c4\", \"ここからが、僕の一番言いたいことです。\", 0.7),\n (\"q\", \"ロボットのニュースを見たときの問いが、2つに変わるんですよ。\", 0.6),\n (\"q1\", \"1つは、そのロボットは自律で動いているのか、人が遠隔で動かしているのか、です。\"),\n (\"q1\", \"デモ動画を見るときは、自律か遠隔操作か、\"),\n (\"q1\", \"再生速度や、人が手を出した条件が書かれているかを見ます。\", 0.7),\n (\"q2\", \"もう1つは、その仕事は「見て判断する」で済むのか、\"),\n (\"q2\", \"「触れて動かす」精度が要るのか、です。\"),\n (\"q2\", \"棚の商品が足りているかを見るのは、前者です。\"),\n (\"q2\", \"柔らかい物を潰さずに掴むのは、後者です。\"),\n (\"q2\", \"前者は、手軽に試せるところまで来ています。ただ、どこまで任せられるかは、現場で確かめる必要がある。\"),\n (\"q2\", \"後者は、その現場の機体と環境に合わせて、すり合わせる仕事が、まだ残っています。\", 0.7),\n (\"gofe\", \"ゴールドバーグさんは、昔ながらのエンジニアリングと学習を組み合わせて、\"),\n (\"gofe\", \"現場で役に立つ仕事をしながら、データを集めていくのが近道だ、と言っています。\"),\n (\"gofe\", \"僕の机の上も、AIと従来の制御を組み合わせる点では通じています。\"),\n (\"gofe\", \"ただ、現場で働きながらデータが増える仕組みとは、まだ規模も段階も違います。\", 0.7),\n (\"motion\", \"で、作る側として、もう1つ言わせてください。\"),\n (\"motion\", \"データの話と並行して、もう1つ要るのが「どう動くか」の設計です。\"),\n (\"motion\", \"例えばロボットが人に物を渡すとき、\"),\n (\"motion\", \"何秒待って手を開けば、人は安心して受け取れるのか。\"),\n (\"motion\", \"これは、成功か失敗かの判定だけでは出てこない。\"),\n (\"motion\", \"人が安心できる動きを設計する仕事も、同時に必要なんだと、僕は見ています。\", 0.9),\n # 9 回答\n (\"answer2\", \"じゃあ、冒頭の問いに答えますね。\", 0.4),\n (\"answer2\", \"僕の机では、写真を見せて判断させる入口は、ずっと簡単になりました。\"),\n (\"answer2\", \"一方、物を確実に動かすには、機体や環境とのすり合わせが、まだ必要でした。\"),\n (\"answer2\", \"その負担を減らす研究は、急速に進んでいます。\"),\n (\"answer2\", \"でも、いろんな家で、安全に家事を任せられる転換点まで来たかは、まだ判断できません。\", 0.7),\n (\"action\", \"ロボットのデモ動画を見たら、\"),\n (\"action\", \"「これは自律なのか、何回やって、何回うまくいったのか」を、\"),\n (\"action\", \"一度だけ考えてみてください。\"),\n (\"action\", \"それだけで、同じ動画の見え方が、かなり変わると思います。\", 0.9),\n # 10 アウトロ\n (\"outro\", \"今日はここまでです。\"),\n (\"outro\", \"この動画が気に入ったら、高評価とチャンネル登録をお願いします。\"),\n (\"outro\", \"コメントは全部読ませてもらっています。それではまた。\", 1.2),\n (\"sources\", \"\", 6.0),\n]\n\n# TTS 用の読み(字幕は元の表記のまま)\nREAD = [\n (\"Pixels to Motors\", \"ピクセルズ トゥー モーターズ\"),\n (\"Smart Analytics Global\", \"スマート アナリティクス グローバル\"),\n (\"Counterpoint\", \"カウンターポイント\"),\n (\"ChatGPT\", \"チャットジーピーティー\"),\n (\"Generalist\", \"ジェネラリスト\"),\n (\"mechArm 270\", \"メックアーム ニーナナゼロ\"),\n (\"iPhone\", \"アイフォーン\"),\n (\"ikpy\", \"アイケーパイ\"),\n (\"NEO\", \"ネオ\"),\n (\"1X\", \"ワンエックス\"),\n (\"SAG\", \"エスエージー\"),\n (\"AI\", \"エーアイ\"),\n (\"30cm\", \"さんじゅっセンチ\"),\n (\"爪先\", \"つめさき\"),\n (\"1万9100台\", \"いちまんきゅうせんひゃくだい\"),\n (\"値を送る\", \"あたいを送る\"),\n (\"「\", \"\"), (\"」\", \"\"),\n]\n\n# 間延び対策:ひと息が長い文だけ、読み上げ用に読点を足す(字幕は変えない)\nOVERRIDE = {\n \"同じ半年の出荷を約1万9100台と推計していて、\": \"同じ半年の出荷を、約1万9100台と、推計していて、\",\n \"工場でも家でも、使いながらデータを集める動きが出てきているんですよ。\": \"工場でも家でも、使いながら、データを集める動きが、出てきているんですよ。\",\n \"ほかの環境で学んだことを持ってくる研究も進んでいます。\": \"ほかの環境で学んだことを、持ってくる研究も、進んでいます。\",\n \"ここで、じゃあロボットにChatGPTの瞬間は当分来ない、\": \"ここで、じゃあ、ロボットに、ChatGPTの瞬間は、当分来ない、\",\n \"1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、\": \"1Xが、家の中で、遠隔で手伝いながら、動きを覚えさせているのも、\",\n}\n\ndef reading(t):\n t = OVERRIDE.get(t, t)\n for a, b in READ:\n t = t.replace(a, b)\n return t</code></pre><h3 id=\"index_ShUg2Clt\">tts2.py(読み上げ音声の生成と間延びの検査)</h3><pre><code class=\"language-python\">import subprocess, json, sys, re, wave\nimport numpy as np\nsys.path.insert(0,'.')\nfrom narration import SEGS, reading\nVOICE='/usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice'\nDIC='/var/lib/mecab/dic/open-jtalk/naist-jdic'\nSR=48000\ndef synth(text,base):\n w=base+'.wav'; l=base+'.log'\n subprocess.run(['open_jtalk','-m',VOICE,'-x',DIC,'-r','1.08','-fm','-1','-a','0.55','-jf','1.0','-ow',w,'-ot',l],input=text.encode(),check=True)\n log=open(l,encoding='utf-8',errors='ignore').read()\n ph=[]\n for x in log.split('[Output label]')[1].strip().splitlines():\n m=re.match(r'(\\d+) (\\d+) \\S+?\\^\\S+?-(\\S+?)\\+',x)\n if m: ph.append((int(m.group(1))/1e7,int(m.group(2))/1e7,m.group(3)))\n with wave.open(w) as f: a=np.frombuffer(f.readframes(f.getnframes()),dtype=np.int16)\n inner=[(e-s) for s,e,p in ph[1:-1] if p not in('pau','sil')]\n pron=''.join(x.split(',')[9] for x in log.split('[Text analysis result]')[1].split('[Output label]')[0].splitlines() if x.count(',')>9)\n return a,ph,max(inner) if inner else 0,pron\nout=[]\nfor i,s in enumerate(SEGS):\n scene,text=s[0],s[1]; pause=s[2] if len(s)>2 else 0.35\n d=0.0; note=''\n if text:\n r=reading(text)\n a,ph,worst,pron=synth(r,f'tts/{i:03d}')\n if worst>0.3:\n parts=[p for p in re.split(r'(?<=[、。])',r) if p]\n audio=[]; worst2=0\n for n,p in enumerate(parts):\n b,ph2,w2,_=synth(p,f'tts/{i:03d}_p{n}'); worst2=max(worst2,w2)\n st=int(ph2[0][1]*SR) if n>0 else 0\n en=int(ph2[-1][0]*SR) if n<len(parts)-1 else len(b)\n audio.append(b[st:en])\n if n<len(parts)-1: audio.append(np.zeros(int(0.12*SR),dtype=np.int16))\n a=np.concatenate(audio)\n with wave.open(f'tts/{i:03d}.wav','wb') as f:\n f.setnchannels(1); f.setsampwidth(2); f.setframerate(SR); f.writeframes(a.tobytes())\n note=f'STRETCH {worst:.2f}s -> split {len(parts)} parts, max {worst2:.2f}s'\n print(i,text[:30],note)\n d=len(a)/SR\n if re.search(r'[0-9]|爪先',text): print('READ',i,text[:24],'→',pron[:60])\n out.append(dict(i=i,scene=scene,text=text,dur=d,pause=pause,note=note))\njson.dump(out,open('tts/index.json','w'),ensure_ascii=False,indent=0)\ntot=sum(o['dur']+o['pause'] for o in out)\nprint('segments',len(out),'total %d:%02d'%(tot//60,tot%60))</code></pre><h3 id=\"index_T1WL0zVD\">scenes.py(場面ごとの図解・テロップ・実機映像の指定)</h3><pre><code class=\"language-python\"># -*- coding: utf-8 -*-\nimport base64 as _b64, os as _os\nLOGO='data:image/png;base64,'+_b64.b64encode(open(_os.path.join(_os.path.dirname(_os.path.abspath(__file__)),'logo_dark.png'),'rb').read()).decode()\n# 1920x1080 の各シーン(字幕は別レイヤー)。下端 230px は字幕用に空ける。\n# v2: カード上の太線をやめ、左右は細い縦線で区切る。色は文字色だけで区別する。\nBLUE = \"#1A6C98\" # 見て判断する\nRED = \"#C4471F\" # 触れて動かす/強調\nINK = \"#0D0D0D\"\nSUB = \"#888888\"\nBG = \"#F5F5F0\"\nRULE = \"#D6D6D1\"\n\nCSS = f\"\"\"\n*{{margin:0;padding:0;box-sizing:border-box}}\nhtml,body{{width:1920px;height:1080px;overflow:hidden}}\nbody{{background:{BG};color:{INK};font-family:'Noto Sans CJK JP',sans-serif;}}\n.wrap{{position:absolute;left:140px;right:140px;top:96px;bottom:250px;display:flex;flex-direction:column;justify-content:center}}\n.tag{{position:absolute;left:140px;top:52px;font-size:28px;font-weight:700;color:{SUB};letter-spacing:.04em}}\n.tag b{{color:{RED};margin-right:14px}}\n.brand{{position:absolute;right:140px;top:46px;height:34px}}\n.kicker{{font-size:40px;font-weight:700;color:{SUB};margin-bottom:22px}}\n.h1{{font-size:84px;font-weight:900;line-height:1.25}}\n.h2{{font-size:64px;font-weight:900;line-height:1.3}}\n.h3{{font-size:48px;font-weight:700;line-height:1.4}}\n.p{{font-size:40px;font-weight:500;line-height:1.55;color:{INK}}}\n.muted{{color:{SUB}}}\n.red{{color:{RED}}} .blue{{color:{BLUE}}}\n.hero{{font-size:220px;font-weight:900;line-height:1;letter-spacing:-.02em}}\n.hero small{{font-size:90px;font-weight:900;margin-left:10px}}\n.src{{position:absolute;left:140px;bottom:236px;font-size:24px;color:{SUB}}}\n.row{{display:flex;align-items:stretch}}\n.col{{flex:1;padding:10px 56px 10px 0}}\n.col + .col{{border-left:2px solid {RULE};padding-left:56px}}\n.col .lab{{font-size:34px;font-weight:700;color:{SUB};margin-bottom:14px}}\n.col .big{{font-size:66px;font-weight:900;line-height:1.22}}\n.col .note{{font-size:34px;line-height:1.5;margin-top:16px}}\n.list{{list-style:none}}\n.list li{{font-size:46px;font-weight:700;line-height:1.35;padding:22px 0;border-bottom:2px solid {RULE};display:flex;gap:28px;align-items:baseline}}\n.list li .n{{font-size:44px;font-weight:900;color:{RED};min-width:60px}}\n.list li.dim{{color:{SUB}}}\n.chap{{font-size:200px;font-weight:900;color:{RED};line-height:1}}\n.pill{{align-self:flex-start;display:inline-block;font-size:30px;font-weight:700;padding:8px 22px;border-radius:999px;background:{INK};color:#fff;margin-bottom:26px}}\n.flow{{display:flex;align-items:center;gap:30px}}\n.box{{background:#fff;border-radius:18px;padding:34px 40px;font-size:42px;font-weight:700;text-align:center;flex:1}}\n.arrow{{font-size:64px;color:{SUB};font-weight:900}}\n\"\"\"\n\ndef page(body, tag=None, brand=True):\n t = f'<div class=\"tag\">{tag}</div>' if tag else \"\"\n b = f'<img class=\"brand\" src=\"{LOGO}\">' if brand else \"\"\n return f'<!doctype html><html><head><meta charset=\"utf-8\"><style>{CSS}</style></head><body>{t}{b}{body}</body></html>'\n\nT1 = \"<b>1</b>出荷されたロボットは何をしているのか\"\nT2 = \"<b>2</b>机の上のロボットアームで、簡単だったこと・難しかったこと\"\nT3 = \"<b>3</b>データの壁の高さと、越えようとしている会社\"\nT4 = \"<b>4</b>僕の見立て\"\n\ndef hbar(parts, height=96):\n segs = \"\"\n for lab, pct, col, tc in parts:\n segs += (f'<div style=\"flex:{pct};background:{col};height:{height}px;border-radius:6px;'\n f'display:flex;align-items:center;padding-left:24px;color:{tc};font-size:34px;font-weight:700;white-space:nowrap;overflow:hidden\">{lab}</div>')\n return f'<div style=\"display:flex;gap:4px;width:100%\">{segs}</div>'\n\ndef two(lab1, big1, lab2, big2, c1=\"blue\", c2=\"red\", size=60, note1=\"\", note2=\"\"):\n n1 = f'<div class=\"note\">{note1}</div>' if note1 else \"\"\n n2 = f'<div class=\"note\">{note2}</div>' if note2 else \"\"\n return (f'<div class=\"row\"><div class=\"col\"><div class=\"lab\">{lab1}</div>{n1}<div class=\"big {c1}\" style=\"font-size:{size}px;margin-top:8px\">{big1}</div></div>'\n f'<div class=\"col\"><div class=\"lab\">{lab2}</div>{n2}<div class=\"big {c2}\" style=\"font-size:{size}px;margin-top:8px\">{big2}</div></div></div>')\n\ndef chapter(n, title):\n return page(f'<div class=\"wrap\"><div class=\"chap\">{n}</div><div class=\"h1\" style=\"margin-top:30px\">{title}</div></div>')\n\n\n# --- 強弱の型(2026-10-11 追加)---\n# 打ち出し: 小さい前置き(色)+ 1〜2行の大きな文。1行は10字前後まで\ndef punch(kicker, lines, sub=\"\", kc=\"red\", size=150, tag=None):\n body = \"<br>\".join(lines)\n sb = f'<div class=\"h3 muted\" style=\"margin-top:40px;font-size:44px\">{sub}</div>' if sub else \"\"\n return page(f\"\"\"<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div class=\"{kc}\" style=\"font-size:44px;font-weight:900;letter-spacing:.06em;margin-bottom:28px\">{kicker}</div>\n <div style=\"font-size:{size}px;font-weight:900;line-height:1.22;letter-spacing:-.01em\">{body}</div>{sb}\n</div>\"\"\", tag=tag)\n\n# 章の扉: 「今日の話」→「1つ目」→ 大きなキーワード → 章の問い\ndef chapter2(no, keyword, sub):\n return page(f\"\"\"<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div class=\"muted\" style=\"font-size:40px;font-weight:700;letter-spacing:.1em\">今日の話</div>\n <div style=\"font-size:72px;font-weight:900;margin-top:26px\">{no}</div>\n <div class=\"red\" style=\"font-size:200px;font-weight:900;line-height:1.1;margin-top:6px\">{keyword}</div>\n <div style=\"font-size:48px;font-weight:700;margin-top:26px\">{sub}</div>\n</div>\"\"\")\n\n# 点の並び: 100個の点のうち n 個に色を付ける(割合を数で見せる)\ndef dots(n, kicker, label, big, note=\"\", src=\"\", tag=None):\n cells = \"\".join(f'<span style=\"width:38px;height:38px;border-radius:50%;background:{RED if i < n else \"#D3D3CE\"}\"></span>' for i in range(100))\n return page(f\"\"\"<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div style=\"font-size:48px;font-weight:900;margin-bottom:34px\">{kicker}</div>\n <div style=\"display:flex;align-items:center;gap:56px\">\n <div style=\"display:grid;grid-template-columns:repeat(25,38px);gap:10px\">{cells}</div>\n <div class=\"red\" style=\"font-size:190px;font-weight:900;line-height:1\">{big}</div>\n </div>\n <div style=\"font-size:48px;font-weight:700;margin-top:34px\">{label}</div>\n <div class=\"muted\" style=\"font-size:32px;margin-top:10px\">{note}</div>\n</div>{f'<div class=\"src\">{src}</div>' if src else ''}\"\"\", tag=tag)\n\nS = {}\n\nS[\"hook_ship\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期 人型ロボットの世界出荷(推計)</div>\n <div class=\"hero\">22,000<small>台超</small></div>\n <div class=\"h2\" style=\"margin-top:36px\">前年同期の <span class=\"red\">約4倍</span></div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)</div>\"\"\")\n\nS[\"hook_forecast\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年通年の予想</div>\n <div class=\"hero\">50,000<small>台超</small></div>\n <div class=\"h3\" style=\"margin-top:44px\">家事をする人型ロボットの予約販売も、2025年秋から</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)/1X</div>\"\"\")\n\nS[\"hook_question\"] = page(\"\"\"\n<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div class=\"h1\">ロボットも、ChatGPTのときみたいに<br><span class=\"red\">一気に賢くなる?</span></div>\n</div>\"\"\")\n\nS[\"question\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">今日の問い</div>\n <div class=\"h1\">ロボットにも<span class=\"red\">ChatGPTの瞬間</span>は<br>もう来たのか</div>\n <div class=\"h3 muted\" style=\"margin-top:44px\">ChatGPTの瞬間=専門家でなくても、<br>いろんな仕事にすぐ使えるようになる転換点</div>\n</div>\"\"\")\n\nS[\"define\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">フィジカルAI とは</div>\n <div class=\"h2\">現実の世界を認識して、<span class=\"red\">行動する</span>AI</div>\n <div class=\"row\" style=\"margin-top:56px;gap:30px\">\n <div class=\"box\" style=\"border:4px solid #0D0D0D\">ロボットアーム</div>\n <div class=\"box\" style=\"border:4px solid #0D0D0D\">人型ロボット</div>\n <div class=\"box muted\" style=\"background:transparent;border:2px dashed #D6D6D1\">自動運転<br><span style=\"font-size:30px\">今日は扱わない</span></div>\n </div>\n</div>\"\"\")\n\nS[\"answer\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">先に結論</div>\n {two(\"写真を見せて判断させる入口\", \"ぐっと簡単になった\", \"物を確実に動かすところ\", \"まだ手間がかかる\")}\n <div class=\"h3\" style=\"margin-top:56px\">手間は2つ<br>① <span class=\"red\">動作と結びついたデータ</span>が集めにくい<br>② <span class=\"red\">機体や環境とのすり合わせ</span></div>\n</div>\"\"\")\n\nS[\"stance\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2 muted\" style=\"text-decoration:line-through;text-decoration-thickness:6px\">フィジカルAIはまだ使えない</div>\n <div class=\"h1\" style=\"margin-top:48px\">どこまで来ていて、<br>どこで<span class=\"red\">手間がかかっているのか</span>を、<br>分けて見る</div>\n</div>\"\"\")\n\nS[\"toc\"] = page(\"\"\"\n<div class=\"wrap\">\n <ul class=\"list\">\n <li><span class=\"n\">1</span>出荷が4倍になったロボットは、何をしているのか</li>\n <li><span class=\"n\">2</span>机の上のロボットアームで、何が簡単で何が難しかったか</li>\n <li><span class=\"n\">3</span>データの壁はどれくらい高く、誰がどう越えようとしているか</li>\n <li class=\"dim\"><span class=\"n\">+</span>僕の見立て</li>\n </ul>\n</div>\"\"\")\n\nS[\"c1\"] = chapter(\"1\", \"出荷が4倍になったロボットは、<br>何をしているのか\")\nS[\"c2\"] = chapter(\"2\", \"机の上のロボットアームで、<br>何が簡単で、何が難しかったのか\")\nS[\"c3\"] = chapter(\"3\", \"データの壁はどれくらい高くて、<br>誰がどう越えようとしているのか\")\nS[\"c4\"] = chapter(\"4\", \"僕の見立て\")\n\nS[\"use\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期の人型ロボット出荷の用途(Counterpointの推計)</div>\n {hbar([(\"展示・イベント + 研究・データ生産 6割以上\", 62, RED, \"#fff\"), (\"その他\", 38, \"#D3D3CE\", INK)])}\n <div class=\"h3\" style=\"margin-top:44px\">出荷の <span class=\"red\">6割以上</span> が、<br>動きを見せる用途と、研究・データ生産の用途</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)。帯の長さは「6割以上」を表す目安</div>\"\"\", tag=T1)\n\nS[\"datacollect\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">データ生産 = ロボットが学ぶための、動きの記録を作ること</div>\n <div class=\"row\" style=\"gap:30px\">\n <div class=\"box\">人が遠隔で動かして<br>記録する</div>\n <div class=\"box\">自律で動いた記録を<br>残す</div>\n </div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">※研究向けの台数が、全部データ集めに使われるわけではない</div>\n <div class=\"h3\" style=\"margin-top:20px\">分類に「データ生産」がある = 今の段階を表している</div>\n</div>\"\"\", tag=T1)\n\nS[\"conflict\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">同じ2026年上半期の推計</div>\n {two(\"Counterpoint\", \"見せる用途+研究・データ生産<br>6割以上\", \"Smart Analytics Global(SAG)\", \"産業・商業用途<br>7割超\", c1=\"\", c2=\"\", size=52, note1=\"出荷 22,000台超\", note2=\"出荷 約19,100台\")}\n <div class=\"h3\" style=\"margin-top:46px\">総数も分類も違う → <span class=\"red\">単純な裏表としては比べられない</span></div>\n <div class=\"h3 muted\" style=\"margin-top:10px\">出荷台数 ≠ 自律的に働いている台数</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)/Smart Analytics Global(2026年8月)</div>\"\"\", tag=T1)\n\nS[\"china\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期の人型ロボット出荷のうち、中国メーカーの割合(SAGの推計)</div>\n <div class=\"hero\">97<small>%以上</small></div>\n</div>\n<div class=\"src\">出典:Smart Analytics Global(2026年8月)</div>\"\"\", tag=T1)\n\nS[\"neo\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">1X「NEO」 家事をする人型ロボット(2025年10月 予約開始)</div>\n {two(\"値段と配送\", \"2万ドル:2026年配送予定<br>月499ドル:その後\", \"動き方\", \"最初は基本的な自律機能<br>知らない作業は<br>担当者が遠隔で手伝う\", c1=\"\", c2=\"\", size=46)}\n <div class=\"h3\" style=\"margin-top:40px\">2026年10月11日時点:一般顧客への配送開始の公式発表は<span class=\"red\">確認できず</span></div>\n</div>\n<div class=\"src\">出典:1X 公式の発表・注文ページ(2026年10月11日確認)</div>\"\"\", tag=T1)\n\nS[\"neo_point\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">売っているのは</div>\n <div class=\"flow\">\n <div class=\"box\">ロボット</div><div class=\"arrow\">+</div>\n <div class=\"box\">遠隔で<br>手伝う人</div><div class=\"arrow\">+</div>\n <div class=\"box\" style=\"border:4px solid {RED}\">動きを<br>覚えていく仕組み</div>\n </div>\n</div>\"\"\", tag=T1)\n\nS[\"bridge1\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">工場でも家でも、<br><span class=\"red\">使いながらデータを集める</span>動き</div>\n <div class=\"h2 muted\" style=\"margin-top:56px\">では、物を動かすのは<br>何がそんなに大変なのか</div>\n</div>\"\"\", tag=T1)\n\nS[\"trials\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">赤いブロックを掴んで、箱に置く</div>\n <div class=\"hero\">650<small>回以上</small></div>\n <div class=\"h3\" style=\"margin-top:36px\">失敗も含めた試行回数。全部台帳に記録</div>\n</div>\n<div class=\"src\">PICK→PLACE の通し試行(台帳 pick_place_ledger_v3.csv、2026年6月18日〜7月20日で656回)</div>\"\"\", tag=T2)\n\nS[\"easy\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{BLUE}\">簡単だった:掴めたかの判定</div>\n <div class=\"flow\">\n <div class=\"box\">iPhoneで<br>撮った写真</div><div class=\"arrow\">→</div>\n <div class=\"box\" style=\"flex:1.5\">AIに聞く<br><span style=\"font-size:32px;white-space:nowrap\" class=\"muted\">「ブロックを掴めていますか」</span></div><div class=\"arrow\">→</div>\n <div class=\"box\" style=\"border:4px solid {BLUE}\"><span class=\"blue\">はい</span>/いいえ</div>\n </div>\n <div class=\"h3\" style=\"margin-top:48px\">ただし、<span class=\"red\">答えが返る ≠ 毎回正しい</span></div>\n</div>\"\"\", tag=T2)\n\nS[\"hard\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">難しかった</div>\n <div class=\"h1\">動かすほう</div>\n</div>\"\"\", tag=T2)\n\nS[\"gap\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">僕の設定で、同じ関節の角度を入れたときの爪先の位置</div>\n <div style=\"position:relative;height:400px;margin-top:10px\">\n <div style=\"position:absolute;left:60px;top:250px;right:60px;height:4px;background:{RULE}\"></div>\n <div style=\"position:absolute;left:150px;top:218px;width:68px;height:68px;border-radius:50%;background:#fff;border:8px solid {SUB}\"></div>\n <div style=\"position:absolute;left:4px;top:310px;width:440px;text-align:center;font-size:36px;font-weight:700;color:{SUB}\">計算ツール(ikpy)</div>\n <div style=\"position:absolute;right:150px;top:218px;width:68px;height:68px;border-radius:50%;background:{RED}\"></div>\n <div style=\"position:absolute;right:4px;top:310px;width:440px;text-align:center;font-size:36px;font-weight:700;color:{RED}\">デジタルツイン</div>\n <div style=\"position:absolute;left:184px;right:184px;top:140px;height:4px;background:{INK}\"></div>\n <div style=\"position:absolute;left:182px;top:120px;width:4px;height:44px;background:{INK}\"></div>\n <div style=\"position:absolute;right:182px;top:120px;width:4px;height:44px;background:{INK}\"></div>\n <div style=\"position:absolute;left:0;right:0;top:20px;text-align:center;font-size:72px;font-weight:900\"><span class=\"red\">30cm以上</span> 食い違う</div>\n </div>\n</div>\n<div class=\"h3\" style=\"position:absolute;left:140px;bottom:290px;font-size:36px\">アームが外れた距離ではない。<span class=\"red\">爪先の位置の決め方</span>が違っていた<span class=\"muted\">(計算ツールにはグリッパーの形がない)</span></div>\n<div class=\"src\">図は模式図</div>\"\"\", tag=T2)\n\nS[\"silent\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">黙る失敗</div>\n <div class=\"flow\">\n <div class=\"box\">肩の関節に<br><span class=\"red\">90度ちょっと</span>を超える値</div><div class=\"arrow\">→</div>\n <div class=\"box\">エラーは出ない<br>アームも動かない</div>\n </div>\n <div class=\"h3 muted\" style=\"margin-top:44px\">僕の機体とソフトの組み合わせで起きたこと</div>\n</div>\"\"\", tag=T2)\n\nS[\"point2\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">この2つは</div>\n <div class=\"h2 muted\" style=\"text-decoration:line-through;text-decoration-thickness:5px\">AIの学習データが足りない</div>\n <div class=\"h1\" style=\"margin-top:30px\"><span class=\"red\">すり合わせ</span>の仕事</div>\n <div class=\"h3\" style=\"margin-top:20px\">モデルや制御が、自分の機体と噛み合っているかを確かめる</div>\n</div>\"\"\", tag=T2)\n\nS[\"years\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">画像と言葉を扱うAIの学習資料を、人が読む・見る時間に換算すると</div>\n <div class=\"hero\">約10<small>万年規模</small></div>\n</div>\n<div class=\"src\">出典:Ken Goldberg, Science Robotics(2025年8月, Editorial)</div>\"\"\", tag=T3)\n\nS[\"wall\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"h1\">10万年の<span class=\"red\">データの壁</span></div>\n <div style=\"margin-top:50px\">{two(\"画像と言葉\", \"大量にある\", \"動作と結びついた経験\", \"集めにくい\", size=58)}</div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">※ロボットにも同じ量が必要だと分かったわけではない</div>\n</div>\n<div class=\"src\">出典:Ken Goldberg, Science Robotics(2025年8月)</div>\"\"\", tag=T3)\n\nS[\"gen\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">Generalist が集めた、人の手作業の記録(会社の発表)</div>\n <div style=\"display:flex;flex-direction:column;gap:30px;margin-top:10px\">\n <div style=\"display:flex;align-items:center;gap:30px\"><div style=\"width:260px;font-size:36px;font-weight:700;color:{SUB}\">2025年11月</div>\n <div style=\"width:{int(1060*27/50)}px;height:110px;background:#D3D3CE;border-radius:0 6px 6px 0\"></div><div style=\"font-size:52px;font-weight:900;white-space:nowrap\">27万時間</div></div>\n <div style=\"display:flex;align-items:center;gap:30px\"><div style=\"width:260px;font-size:36px;font-weight:700;color:{SUB}\">2026年4月</div>\n <div style=\"width:1060px;height:110px;background:{RED};border-radius:0 6px 6px 0\"></div><div style=\"font-size:52px;font-weight:900;white-space:nowrap\">50万時間超</div></div>\n </div>\n <div class=\"h3\" style=\"margin-top:44px\">安い装着型の機材で記録</div>\n</div>\n<div class=\"src\">出典:Generalist(GEN-0 2025年11月/GEN-1 2026年4月)</div>\"\"\", tag=T3)\n\nS[\"gen2\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">Generalist GEN-1.5(2026年8月)</div>\n <div class=\"h2\">簡単で短い作業なら、<br><span class=\"red\">数秒の実演</span>で新しい作業をこなす例</div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">本人たちも:作業は簡単で短く、成功率はまだ控えめ</div>\n</div>\n<div class=\"src\">出典:Generalist「GEN-1.5」(2026年8月19日)</div>\"\"\", tag=T3)\n\nS[\"compare\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">作業を記録した時間と、10万年は<br><span class=\"red\">そのまま比べられない</span></div>\n <div class=\"h3\" style=\"margin-top:50px\"><span class=\"muted\">何倍足りないか</span> より <span class=\"red\">どれだけ広く集められるか</span></div>\n</div>\"\"\", tag=T3)\n\nS[\"dirt\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">壁はまだ高い。でも、<br><span class=\"red\">壁の前で土を積み始めている</span></div>\n <ul class=\"list\" style=\"margin-top:30px\">\n <li style=\"font-size:40px\"><span class=\"n\">・</span>1X:家の中で遠隔で手伝いながら、動きを覚えさせる</li>\n <li style=\"font-size:40px\"><span class=\"n\">・</span>Generalist:人の手作業を装着型の機材で記録する</li>\n </ul>\n</div>\"\"\", tag=T3)\n\nS[\"notall\"] = page(f\"\"\"\n<div class=\"wrap\">\n {two(\"写真を見せて判断させる入口\", \"もう普通に使える\", \"物を確実に動かす\", \"すり合わせとデータが<br>まだ要る\", size=56)}\n <div class=\"h3\" style=\"margin-top:48px\">「まだ全然ダメ」でも「もう来た」でもない</div>\n</div>\"\"\", tag=T3)\n\nS[\"q\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h1\">ロボットのニュースを見る<br><span class=\"red\">問いが2つ</span>に変わる</div>\n</div>\"\"\", tag=T4)\n\nS[\"q1\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">問い 1</div>\n <div class=\"h1\">自律で動いているのか、<br>人が遠隔で動かしているのか</div>\n <div class=\"h3 muted\" style=\"margin-top:36px\">デモ動画で見るところ:自律か遠隔か/再生速度/人が手を出した条件</div>\n</div>\"\"\", tag=T4)\n\nS[\"q2\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">問い 2</div>\n <div class=\"h2\" style=\"margin-bottom:40px\">その仕事には、どちらが要るのか</div>\n {two(\"見て判断する\", \"手軽に試せる<br><span style='font-size:34px;color:#888888'>任せる範囲は現場で確かめる</span>\", \"触れて動かす精度\", \"現場の機体と環境に<br>すり合わせる仕事が残る\", size=48, note1=\"例:棚の商品が足りているか見る\", note2=\"例:柔らかい物を潰さずに掴む\")}\n</div>\"\"\", tag=T4)\n\nS[\"gofe\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">ゴールドバーグさんの近道</div>\n <div class=\"flow\">\n <div class=\"box\">昔ながらの工学<br>+学習</div><div class=\"arrow\">→</div>\n <div class=\"box\">現場で役に立つ<br>仕事をする</div><div class=\"arrow\">→</div>\n <div class=\"box\">働きながら<br>データが増える</div>\n </div>\n <div class=\"h3\" style=\"margin-top:48px\">僕の机の上:組み合わせる点は通じる。<span class=\"muted\">規模も段階もまだ違う</span></div>\n</div>\"\"\", tag=T4)\n\nS[\"motion\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">データの話と並行して要るもの</div>\n <div class=\"h1\"><span class=\"red\">どう動くか</span>の設計</div>\n <div class=\"h3\" style=\"margin-top:36px\">ロボットが人に物を渡すとき、<br>何秒待って手を開けば、人は安心して受け取れるのか</div>\n <div class=\"h3 muted\" style=\"margin-top:28px\">成功か失敗かの判定だけでは出てこない</div>\n</div>\"\"\", tag=T4)\n\nS[\"answer2\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">冒頭の問いへの答え</div>\n {two(\"写真を見せて判断させる入口\", \"ずっと簡単になった\", \"物を確実に動かす\", \"すり合わせが<br>まだ必要\", size=58)}\n <div class=\"h3\" style=\"margin-top:44px\">負担を減らす研究は急速に進んでいる。<br>家で家事を任せられる転換点かは、<span class=\"red\">まだ判断できない</span></div>\n</div>\"\"\")\n\nS[\"action\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">ロボットのデモ動画を見たら</div>\n <div class=\"h1\">自律なのか、何回やって、<br><span class=\"red\">何回うまくいった</span>のか</div>\n</div>\"\"\")\n\nS[\"sources\"] = page(\"\"\"\n<div class=\"wrap\" style=\"bottom:90px;justify-content:flex-start;top:120px\">\n <div class=\"h2\" style=\"margin-bottom:30px\">出典</div>\n <div class=\"p\" style=\"font-size:29px;line-height:1.75\">\n 人型ロボットの出荷:Counterpoint Research(2026年8月)/Smart Analytics Global(2026年8月)<br>\n 1X NEO:1X 公式の発表・注文ページ(2026年10月11日確認)<br>\n 10万年のデータの壁:Ken Goldberg, Science Robotics(2025年8月, Editorial)<br>\n Generalist:GEN-0(2025年11月)、GEN-1(2026年4月)、GEN-1.5(2026年8月)の公式発表<br>\n ほかの機体から学ぶ研究の例:Gemini Robotics 1.5 技術報告(Google DeepMind)<br>\n 実機の数字:Pixels to Motors の台帳(pick_place_ledger_v3.csv)、公開記事 DAY257<br>\n <span class=\"muted\">ナレーションは仮音声(Open JTalk/HTS voice nitech_jp_atr503_m001)</span>\n </div>\n</div>\"\"\")\n\n\n# --- 強弱をつける場面の上書き(2026-10-11)---\nS[\"hook_question\"] = punch(\"こういうニュースを見ると\", [\"ロボットも\", \"一気に賢くなる?\"])\nS[\"answer\"] = punch(\"先に結論\", ['見て判断する入口は<span class=\"blue\">簡単に</span>', '物を動かすのは<span class=\"red\">まだ手間</span>'], sub=\"手間は2つ:動作と結びついたデータ/機体や環境とのすり合わせ\", size=110)\nS[\"c1\"] = chapter2(\"1つ目\", \"出荷の中身\", \"出荷が4倍になったロボットは、何をしているのか\")\nS[\"c2\"] = chapter2(\"2つ目\", \"机の上の実感\", \"何が簡単で、何が難しかったのか\")\nS[\"c3\"] = chapter2(\"3つ目\", \"データの壁\", \"どれくらい高くて、誰がどう越えようとしているのか\")\nS[\"c4\"] = chapter2(\"最後に\", \"ニュースの見方\", \"僕の見立て\")\nS[\"china\"] = dots(97, \"2026年上半期の人型ロボット出荷のうち\", \"中国メーカー\", \"97%\", note=\"SAGの推計では「97%以上」\", src=\"出典:Smart Analytics Global(2026年8月)\", tag=T1)\nS[\"point2\"] = punch(\"この2つは\", [\"データ不足じゃない\", '<span class=\"red\">すり合わせ</span>の仕事'], sub=\"モデルや制御が、自分の機体と噛み合っているかを確かめる\", tag=T2)\nS[\"wall\"] = punch(\"ゴールドバーグさんの問題提起\", ['10万年の', '<span class=\"red\">データの壁</span>'], sub=\"動作と結びついた経験は集めにくい(同じ量が必要と分かったわけではない)\", kc=\"muted\", tag=T3)\nS[\"compare\"] = punch(\"ひとつ注意\", ['<span class=\"muted\">何倍足りないか</span>より', '<span class=\"red\">どれだけ広く</span>集めるか'], sub=\"作業を記録した時間と10万年は、そのまま比べられない\", size=110, tag=T3)\nS[\"answer2\"] = punch(\"冒頭の問いへの答え\", ['判断する入口は<span class=\"blue\">来た</span>', '家事を任せるのは<span class=\"red\">まだ</span>'], sub=\"負担を減らす研究は急速に進んでいる\", size=110)\n\n# 動画シーンに重ねるオーバーレイ(透過)\nOVERLAY_CSS = f\"\"\"\n*{{margin:0;padding:0;box-sizing:border-box}}\nhtml,body{{width:1920px;height:1080px;background:transparent;font-family:'Noto Sans CJK JP',sans-serif}}\n.lt{{position:absolute;left:96px;top:80px;background:rgba(245,245,240,.94);padding:28px 40px;border-radius:14px}}\n.lt .a{{height:30px;display:block;margin-bottom:6px}}\n.lt .b{{font-size:60px;font-weight:900;color:{INK};margin-top:4px}}\n.lt .c{{font-size:30px;font-weight:500;color:{SUB};margin-top:6px}}\n.cap{{position:absolute;left:96px;top:80px;background:rgba(13,13,13,.78);color:#fff;font-size:34px;font-weight:700;padding:14px 26px;border-radius:10px}}\n\"\"\"\ndef overlay(body):\n return f'<!doctype html><html><head><meta charset=\"utf-8\"><style>{OVERLAY_CSS}</style></head><body>{body}</body></html>'\n\nO = {\n \"title\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">エイイチ</div><div class=\"c\">デザイナー/ロボティクスの研究・開発</div></div>'),\n \"v_real\": overlay('<div class=\"cap\">実機:mechArm 270-Pi 赤いブロックを掴んで箱に置く</div>'),\n \"v_twin\": overlay('<div class=\"cap\">パソコンの中のアームの分身(Unity のデジタルツイン)</div>'),\n \"v_scan\": overlay('<div class=\"cap\">デジタルツインの中で動かして、関節の角度と爪先の位置の対応表を作る</div>'),\n \"outro\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">高評価・チャンネル登録</div><div class=\"c\">コメントは全部読んでいます</div></div>'),\n}\n\n# 動画シーン: (ソースファイル, 開始秒, 透かし消しの有無)\nVIDEO = {\n \"title\": (\"src/c1.mp4\", 0.0, True),\n \"v_real\": (\"src/c2.mp4\", 22.0, True),\n \"v_twin\": (\"src/c4.mov\", 0.0, False),\n \"v_scan\": (\"src/c3.mov\", 0.0, False),\n \"outro\": (\"src/c1.mp4\", 8.0, True),\n}</code></pre><h3 id=\"index_Na0vVVb3\">render.py(図解・テロップ・字幕を画像にする)</h3><pre><code class=\"language-python\">import json, sys, os, html\nsys.path.insert(0,'.')\nfrom scenes import S, O\nfrom playwright.sync_api import sync_playwright\nidx=json.load(open('tts/index.json'))\nos.makedirs('png/scene',exist_ok=True); os.makedirs('png/ovl',exist_ok=True); os.makedirs('png/sub',exist_ok=True)\nSUBCSS_OLD=\"\"\"*{margin:0;padding:0}html,body{width:1920px;height:1080px;background:transparent;font-family:'Noto Sans CJK JP',sans-serif}\n.s{position:absolute;left:140px;right:140px;bottom:56px;text-align:center;font-size:52px;font-weight:800;line-height:1.38;color:#fff;\n-webkit-text-stroke:9px #0D0D0D;paint-order:stroke fill;word-break:normal;line-break:strict}\"\"\"\nSUBCSS=\"\"\"*{margin:0;padding:0}html,body{width:1920px;height:1080px;background:transparent}\nsvg{position:absolute;left:0;top:0}\n.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}\"\"\"\nonly=sys.argv[1:]\ndef split2(t):\n if len(t)<=29: return html.escape(t)\n mid=len(t)/2; best=None\n for i,ch in enumerate(t[:-1]):\n if ch in '、。': \n if best is None or abs(i+1-mid)<abs(best-mid): best=i+1\n if best is None or max(best,len(t)-best)>31:\n # 読点で割れない場合は真ん中付近の助詞の後ろ\n for d in range(0,12):\n for i in (int(mid)+d,int(mid)-d):\n if 0<i<len(t) and t[i-1] in 'はがをにでとものへてや' and t[i] not in '、。」':\n return html.escape(t[:i])+'<br>'+html.escape(t[i:])\n return html.escape(t)\n return html.escape(t[:best])+'<br>'+html.escape(t[best:])\nwith sync_playwright() as p:\n b=p.chromium.launch(); pg=b.new_page(viewport={'width':1920,'height':1080})\n for k,h in S.items():\n if only and k not in only: continue\n pg.set_content(h); pg.wait_for_timeout(50); pg.screenshot(path=f'png/scene/{k}.png')\n for k,h in O.items():\n if only and k not in only: continue\n pg.set_content(h); pg.screenshot(path=f'png/ovl/{k}.png',omit_background=True)\n if not only:\n for o in idx:\n lines=split2(o['text']).split('<br>')\n fs=52\n svg_lines=''.join(f'<text x=\"960\" text-anchor=\"middle\" class=\"t\">{ln}</text>' for ln in lines)\n pg.set_content(f\"\"\"<!doctype html><html><head><meta charset=\"utf-8\"><style>{SUBCSS}</style></head><body>\n<svg id=\"s\" width=\"1920\" height=\"1080\" viewBox=\"0 0 1920 1080\">{svg_lines}</svg></body></html>\"\"\")\n pg.evaluate(\"\"\"() => {\n const ts=[...document.querySelectorAll('text.t')]; const lh=72; const n=ts.length;\n let fs=52; for (const t of ts){ const w=t.getComputedTextLength(); if (w>1640) fs=Math.min(fs, Math.floor(52*1640/w)); }\n ts.forEach((t,k)=>{ t.style.fontSize=fs+'px'; t.setAttribute('y', 1080-62-(n-1-k)*lh); });\n }\"\"\")\n pg.screenshot(path=f\"png/sub/{o['i']:03d}.png\",omit_background=True)\n b.close()\nmissing=[o['scene'] for o in idx if o['scene'] not in S and o['scene'] not in O]\nprint('missing scenes',sorted(set(missing)))</code></pre><h3 id=\"index_pTtPmPEL\">assemble.py(1文ずつ映像を作り、つないで音声と合わせる)</h3><pre><code class=\"language-python\">import json, subprocess, os, sys, wave\nimport numpy as np\nfrom concurrent.futures import ThreadPoolExecutor\nfrom PIL import Image\nsys.path.insert(0,'.')\nfrom scenes import VIDEO\nFPS=30; SR=48000\nidx=json.load(open('tts/index.json'))\nos.makedirs('chunks',exist_ok=True); os.makedirs('comp',exist_ok=True)\n# 尺(フレーム単位)\nfor o in idx:\n o['frames']=max(1,round((o['dur']+o['pause'])*FPS))\n# 動画シーンのオフセット(同じシーンが続く間は続きから)\nprev=None; off=0.0\nfor o in idx:\n sc=o['scene']\n if sc in VIDEO:\n if sc!=prev: off=VIDEO[sc][1]\n o['voff']=off; off+=o['frames']/FPS\n prev=sc\nENC=['-c:v','libx264','-preset','veryfast','-crf','20','-pix_fmt','yuv420p','-r',str(FPS),'-an']\ndef job(o):\n out=f\"chunks/{o['i']:03d}.mp4\"\n if os.path.exists(out) and '--force' not in sys.argv: return\n sub=f\"png/sub/{o['i']:03d}.png\"; sc=o['scene']; n=str(o['frames'])\n if sc in VIDEO:\n src,_,wm=VIDEO[sc]\n dl='delogo=x=2740:y=1840:w=1010:h=220,' if wm else ''\n fc=(f\"[0:v]{dl}scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080,fps={FPS},setsar=1,\"\n f\"drawbox=x=0:y=850:w=iw:h=230:color=black@0.5:t=fill[b];[b][1:v]overlay[c];[c][2:v]overlay[v]\")\n cmd=['ffmpeg','-v','error','-y','-ss',f\"{o['voff']:.3f}\",'-stream_loop','-1','-i',src,\n '-loop','1','-i',f'png/ovl/{sc}.png','-loop','1','-i',sub,'-filter_complex',fc,'-map','[v]','-frames:v',n]+ENC+[out]\n else:\n comp=f\"comp/{o['i']:03d}.png\"\n im=Image.open(f'png/scene/{sc}.png').convert('RGBA'); im.alpha_composite(Image.open(sub).convert('RGBA')); im.convert('RGB').save(comp)\n cmd=['ffmpeg','-v','error','-y','-loop','1','-framerate',str(FPS),'-i',comp,'-frames:v',n,'-tune','stillimage']+ENC+[out]\n subprocess.run(cmd,check=True)\nwith ThreadPoolExecutor(2) as ex: list(ex.map(job,idx))\n# 音声:各文を尺ちょうどに合わせて連結\nparts=[]\nfor o in idx:\n need=o['frames']*SR//FPS\n if o['text']:\n with wave.open(f\"tts/{o['i']:03d}.wav\") as w:\n a=np.frombuffer(w.readframes(w.getnframes()),dtype=np.int16)\n else: a=np.zeros(0,dtype=np.int16)\n a=a[:need]; parts.append(np.concatenate([a,np.zeros(need-len(a),dtype=np.int16)]))\nwith wave.open('narration.wav','wb') as w:\n w.setnchannels(1); w.setsampwidth(2); w.setframerate(SR); w.writeframes(np.concatenate(parts).tobytes())\nopen('chunks/list.txt','w').write(''.join(f\"file '{o['i']:03d}.mp4'\\n\" for o in idx))\n# 字幕 SRT\ndef ts(f):\n ms=round(f/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}\"\nt=0; srt=[]; k=1\nfor o in idx:\n if o['text']:\n srt.append(f\"{k}\\n{ts(t)} --> {ts(t+round(o['dur']*FPS))}\\n{o['text']}\\n\"); k+=1\n t+=o['frames']\nopen('out.srt','w',encoding='utf-8').write('\\n'.join(srt))\njson.dump(idx,open('timeline.json','w'),ensure_ascii=False)\nsubprocess.run(['ffmpeg','-v','error','-y','-f','concat','-safe','0','-i','chunks/list.txt','-i','narration.wav',\n '-map','0:v','-map','1:a','-c:v','copy','-af','loudnorm=I=-16:TP=-1.5:LRA=11','-c:a','aac','-b:a','192k','-ar','48000','-movflags','+faststart','out.mp4'],check=True)\nprint('total frames',t,'=',t/FPS,'s')</code></pre><h3 id=\"index_vy52HFp6\">analyze.py(最初の版で、間延びした文を見つけたスクリプト)</h3><pre><code class=\"language-python\">import json,re,subprocess\nidx=json.load(open('tts/index.json'))\nrows=[]\nfor o in idx:\n if not o['text']: continue\n log=open('tts/%03d.log'%o['i'],encoding='utf-8',errors='ignore').read()\n lab=log.split('[Output label]')[1].strip().splitlines()\n ph=[]\n for l in lab:\n m=re.match(r'(\\d+) (\\d+) \\S+?\\^\\S+?-(\\S+?)\\+',l)\n if m: ph.append((int(m.group(1))/1e7,int(m.group(2))/1e7,m.group(3)))\n scale=o['dur']/ph[-1][1]\n lead=ph[0][1]-ph[0][0]; tail=ph[-1][1]-ph[-1][0]\n longv=[(p,round((e-s)*scale,2)) for s,e,p in ph[1:-1] if p not in('pau','sil') and (e-s)*scale>0.22]\n pau=[round((e-s)*scale,2) for s,e,p in ph if p=='pau' and (e-s)*scale>0.35]\n gap=round((tail)*scale+o['pause'],2)\n rows.append((o['i'],o['text'][:30],round(lead*scale,2),round(tail*scale,2),o['pause'],longv,pau))\nprint('scale ex',scale)\nfor r in rows:\n if r[5] or r[6] or r[3]+r[4]+0.25>1.3:\n print(r)</code></pre><p></p><h2 id=\"index_c3VMNBPy\">実行ログ</h2><h3 id=\"index_U2U1WMpc\">台帳の試行回数を数えた結果</h3><pre><code>$ cd $HOME/mnt/runs; for f in pick_place_ledger_v3.csv handover_ledger_v1.csv; do echo \"== $f\"; python3 - \"$f\" <<'EOF'\nimport csv,sys,collections\np=sys.argv[1]\ntry:\n rows=list(csv.DictReader(open(p,encoding='utf-8-sig')))\nexcept Exception as e:\n print(\"ERR\",e); sys.exit()\nprint(\"rows\",len(rows))\ncols=rows[0].keys() if rows else []\nfor c in [\"place_verdict\",\"pick\",\"pick_stage\",\"status\",\"run_status\",\"verdict\"]:\n if c in cols:\n print(c, collections.Counter(r[c] for r in rows).most_common(8))\nfor c in [\"run_stem\",\"timestamp\"]:\n if c in cols and rows:\n print(c, rows[0][c], \"->\", rows[-1][c])\nEOF\ndone\n== pick_place_ledger_v3.csv\nrows 657\nplace_verdict [('', 259), ('yes', 217), ('no', 127), ('unknown', 54)]\nrun_stem run_0042_test -> run_0001_20260720_213323\ntimestamp 2026-06-18T00:00:00 -> 2026-07-20T21:35:11.608053\n== handover_ledger_v1.csv\nrows 8\nrun_stem run_handover_20260720_210544_315479_001 -> run_handover_20260720_210618_993522_008\ntimestamp 2026-07-20T21:05:45.451732+09:00 -> 2026-07-20T21:06:20.009752+09:00\n\n$ cd $HOME/mnt/runs && python3 - <<'EOF'\nimport csv,collections\nrows=list(csv.DictReader(open('pick_place_ledger_v3.csv',encoding='utf-8-sig')))\nts=sorted(r['timestamp'] for r in rows if r.get('timestamp'))\nprint(\"min\",ts[0],\"max\",ts[-1])\ntest=[r for r in rows if any(k in r['run_stem'].lower() for k in ('test','dry','sim','mock'))]\nprint(\"test-like\",len(test))\nreal=[r for r in rows if r not in test]\nprint(\"real\",len(real), collections.Counter(r['place_verdict'] for r in real))\nprint([c for c in rows[0].keys()][:12])\nEOF\nls run_pick_place | wc -l\nmin 2026-06-18T00:00:00 max 2026-07-20T21:35:11.608053\ntest-like 1\nreal 656 Counter({'': 259, 'yes': 216, 'no': 127, 'unknown': 54})\n['timestamp', 'run_stem', 'trial_no', 'pick_verdict', 'pick_reason', 'pick_confidence', 'place_verdict', 'place_reason', 'place_confidence', 'block_pick_x', 'block_pick_z', 'block_place_x']\n758</code></pre><h3 id=\"index_a4QOCEhV\">間延びした文の検出結果(最初の版)</h3><pre><code>$ python3 analyze.py | python3 -c \"\nimport sys,ast\nfor line in sys.stdin:\n if line.startswith('('):\n r=ast.literal_eval(line)\n if r[5]: print(r[0], r[1], r[5][:8])\n\"\n13 現実の世界で物に触れて動かす技術のことなんですよ。 [('N', 0.47), ('d', 0.47), ('e', 0.47), ('s', 0.47), ('U', 0.41), ('y', 0.46), ('o', 0.47)]\n88 ChatGPTのようなAIの学習に使われたインターネットの文 [('ts', 0.48), ('U', 0.49), ('k', 0.46), ('a', 0.48), ('w', 0.49), ('a', 0.49), ('r', 0.48), ('e', 0.49)]\n92 ChatGPTは10万年分の教科書で育ったけれど、 [('s', 0.48), ('o', 0.44), ('d', 0.48), ('a', 0.48), ('cl', 0.48), ('t', 0.48), ('a', 0.48), ('k', 0.48)]\n94 ここで、じゃあロボットにChatGPTの瞬間は当分来ない、 [('t', 0.47), ('o', 0.48), ('o', 0.47), ('b', 0.48), ('u', 0.47), ('N', 0.45), ('k', 0.47), ('o', 0.48)]\n109 1Xが家の中で遠隔操作しながらデータを集めているのも、 [('i', 0.47), ('r', 0.42), ('u', 0.47), ('n', 0.46), ('o', 0.47), ('m', 0.47), ('o', 0.47)]\n110 出荷されたロボットの多くがデータ集めに回っているのも、 [('n', 0.45), ('o', 0.45), ('m', 0.45), ('o', 0.45)]</code></pre><h3 id=\"index_H1VyBuua\">6文を区切って作り直した結果</h3><pre><code>$ python3 fixtts.py 13 88 92 94 109 110\n13 ['現実の世界で物に、', '触れて動かす技術のことなんですよ。'] max phoneme 0.11s dur 7.25 -> 4.62\n88 ['チャットジーピーティーのようなエーアイの、', '学習に使われたインターネットの文章を、'] max phoneme 0.14s dur 17.55 -> 5.45\n92 ['チャットジーピーティーは、', '10万年分の教科書で育ったけれど、'] max phoneme 0.14s dur 9.71 -> 4.49\n94 ['ここで、', 'じゃあロボットにチャットジーピーティーの、', '瞬間は当分来ない、'] max phoneme 0.13s dur 9.59 -> 5.01\n109 ['ワンエックスが家の中で遠隔操作しなが、', 'らデータを集めているのも、'] max phoneme 0.17s dur 7.80 -> 5.02\n110 ['出荷されたロボットの多くが、', 'データ集めに回っているのも、'] max phoneme 0.15s dur 5.72 -> 4.38\n\n(109 が「しなが/ら」で切れたため、区切る位置を手で指定して作り直した)\n$ python3 fixtts.py 13 109\n13 ['現実の世界で、', '物に触れて動かす技術のことなんですよ。'] max phoneme 0.11s dur 4.62 -> 4.59\n109 ['ワンエックスが、', '家の中で遠隔操作しながら、', 'データを集めているのも、'] max phoneme 0.17s dur 5.02 -> 5.29</code></pre><h3 id=\"index_qk7nYpvW\">台本の書き直し後に、全文を検査して作り直した結果</h3><pre><code>$ python3 tts2.py (台本の書き直し後の1回目。読点の追加なし)\nREAD 1 世界で2万2000台を超えました。前の年の同じ時 → セカイデニマンニセンダイヲコエマシ’タ、マエノトシノオナジジキノ、オヨソヨンバイデス’、\nREAD 2 年末までには、5万台を超えると予想されています。 → ネンマツマデニワ、ゴマンダイヲコエルトヨソーサレテイマス’、\nREAD 7 僕は秋田で29年デザイナーをやりながら、 → ボクワアキ’タデニジューキューネンデザイナーヲヤリナガラ、\nREAD 18 その手間は大きく2つあります。 → ソノテマワオーキ’クフ’タツアリマス’、\nREAD 22 大きく3つあります。 → オーキ’クミッツアリマス’、\nREAD 23 出荷が4倍になったロボットは、何をしているのか。 → シュッカガヨンバイニナッタロボットワ、ナニヲシ’テイルノカ、\nREAD 27 1つ目、出荷が4倍になったロボットは、何をしてい → ヒ’トツメ、シュッカガヨンバイニナッタロボットワ、ナニヲシ’テイルノカ、デス’、\nREAD 28 Counterpointの推計では、出荷された人 → カウンターポイントノスイケーデワ、シュッカサレタヒ’トガタロボットノロクワリイジョーガ、\n36 同じ半年の出荷を約1万9100台と推計していて、 STRETCH 0.47s -> split 1 parts, max 0.47s\nREAD 36 同じ半年の出荷を約1万9100台と推計していて、 → オナジハントシノシュッカヲヤクイチマンキューセンヒャクダイトスイケーシ’テイテ、\nREAD 37 産業・商業用途が7割を超えた、としています。 → サンギョー、ショーギョーヨートガナナワリヲコエタ、トシ’テイマス’、\nREAD 38 総数も分類も違うので、この2つは、単純な裏表とし → ソースーモブンルイモチガウノデ、コノフ’タツワ、タンジュンナウラオモテトシ’テワクラベラレマセン、\nREAD 40 もう1つ、SAGの推計では、出荷の97%以上が中 → モーヒ’トツ、エスエージーノスイケーデワ、シュッカノキュージューナナパーセントイジョーガチューゴクノメーカーデス’、\nREAD 42 アメリカの1Xという会社は、家事をする人型ロボッ → アメリカノワンエック’ストイウカイシャワ、カジヲスルヒ’トガタロボット、ネオノヨヤクヲ、キョネンノジューガツニハジメマシ\nREAD 43 2万ドルの早期購入枠は、今年配送の予定で、月49 → ニマンドルノソーキ’コーニューワクワ、コトシ’ハイソーノヨテーデ、ツ’キヨンヒャク’キュージューキュードルノプランワ、ソ\nREAD 45 予約した時間に、1Xの担当者が遠隔で手伝う仕組み → ヨヤクシ’タジカンニ、ワンエック’スノタントーシャガエンカクデテツダウシ’クミナンデス’ヨ、\nREAD 46 10月11日の時点で、一般のお客さんへの配送開始 → ジューガツジューイチニチノジテンデ、イッパンノオキャク’サンエノハイソーカイシヲシメス’コーシ’キハッピョーワ、\n51 工場でも家でも、使いながらデータを集める動きが出てきているん STRETCH 0.47s -> split 2 parts, max 0.48s\nREAD 54 2つ目、僕の机の上のロボットアームで、何が簡単で → フ’タツメ、ボクノツ’クエノウエノロボットアームデ、ナニガカンタンデ、ナニガムズカシカッタノカ、デス’、\nREAD 56 僕は自宅の机で、mechArm 270という小さ → ボクワジタクノツ’クエデ、メックアーム、ニーナナゼロトイウチーサナロボットアームヲウゴカシ’テイマス’、\nREAD 58 それだけの作業を、失敗も含めて650回以上試して → ソレダケノサギョーヲ、シッパイモフ’クメテロッピャクゴジュッカイイジョータメシテ、ゼンブダイチョーニノコシ’テイマス’、\nREAD 65 ロボットアームの爪先がどこに来るかは、普通は関節 → ロボットアームノツメサキガドコニクルカワ、フ’ツーワカンセツノカクドカラケーサンデダシマス’、\nREAD 66 ところが、ikpyという計算ツールで出した爪先の → トコロガ、アイケーパイトイウケーサンツールデダシ’タツメサキノイチ’ト、\nREAD 68 僕の設定では、同じ関節の角度なのに、30cm以上 → ボクノセッテーデワ、オナジカンセツノカクドナノニ、サンジュッセンチイジョークイチガッタンデス’、\nREAD 70 関節の角度と爪先の位置の対応表を作りました。 → カンセツノカクドトツメサキノイチノタイオーヒョーヲツ’クリマシ’タ、\nREAD 72 もう1つ困ったのが、黙る失敗です。 → モーヒ’トツ’コマッタノガ、ダマルシッパイデス’、\nREAD 73 僕の機体とソフトの組み合わせでは、肩の関節に、9 → ボクノキ’タイトソフ’トノクミアワセデワ、カタノカンセツニ、キュージュードチョットヲコエルネヲオクルト、\nREAD 75 ログを1行ずつ見て、やっと気づきました。 → ログヲイチギョーズツミテ、ヤットキズキマシ’タ、\nREAD 77 この2つは、AIの学習データが足りない、という話 → コノフ’タツワ、エーアイノガク’シューデータガタリナイ、トイウハナシトワベツデス’、\n79 ほかの環境で学んだことを持ってくる研究も進んでいます。 STRETCH 0.48s -> split 1 parts, max 0.48s\nREAD 81 じゃあ、もう1つの手間、データのほうは、業界全体 → ジャー、モーヒ’トツノテマ、データノホーワ、ギョーカイゼンタイデドーナッテイルノカ、\nREAD 82 3つ目、データの壁はどれくらい高くて、誰がどう越 → ミッツメ、データノカベワドレクライタカク’テ、ダレガドーコエヨートシ’テイルノカ、デス’、\nREAD 86 人が読んだり見たりする時間に換算すると、約10万 → ヒ’トガヨンダリミタリスルジカンニカンサンスルト、ヤクジューマンネンキボニナル、\nREAD 88 ゴールドバーグさんは、これを「10万年のデータの → ゴールドバーグサンワ、コレヲジューマンネンノデータノカベトヨンデイマス’、\n91 ここで、じゃあロボットにChatGPTの瞬間は当分来ない、 STRETCH 0.48s -> split 2 parts, max 0.48s\nREAD 95 会社の発表では、去年の11月に27万時間、今年の → カイシャノハッピョーデワ、キョネンノジューイチガツニニジューナナマンジカン、コトシノシガツニゴジューマンジカンヲコエマシ\nREAD 96 さらに8月には、簡単で短い作業なら、 → サラニハチガツニワ、カンタンデミジカイサギョーナラ、\nREAD 100 ここで1つ、注意があります。 → ココデヒ’トツ、チューイガアリマス’、\nREAD 101 作業を記録した時間と、10万年という数字は、その → サギョーヲキロクシ’タジカント、ジューマンネントイウスージワ、ソノママクラベラレマセン、\n104 1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、 STRETCH 0.47s -> split 1 parts, max 0.47s\nREAD 104 1Xが家の中で遠隔で手伝いながら動きを覚えさせて → ワンエック’スガイエノナカデエンカクデテツダイナガラウゴキヲオボエサセテイルノモ、\nREAD 109 じゃあ、この2つが混ざっている今、ロボットのニュ → ジャー、コノフ’タツガマザッテイルイマ、ロボットノニュースノナニヲミレバイイノカ、\nREAD 111 ロボットのニュースを見たときの問いが、2つに変わ → ロボットノニュースヲミタトキノトイガ、フ’タツニカワルンデス’ヨ、\nREAD 112 1つは、そのロボットは自律で動いているのか、人が → ヒ’トツワ、ソノロボットワジリツデウゴイテイルノカ、ヒ’トガエンカクデウゴカシ’テイルノカ、デス’、\nREAD 115 もう1つは、その仕事は「見て判断する」で済むのか → モーヒ’トツワ、ソノシゴトワミテハンダンスルデスムノカ、\nREAD 125 で、作る側として、もう1つ言わせてください。 → デ、ツ’クルガワトシ’テ、モーヒ’トツイワセテクダサイ、\nREAD 126 データの話と並行して、もう1つ要るのが「どう動く → データノハナシトヘーコーシ’テ、モーヒ’トツイルノガドーウゴク’カノセッケーデス’、\nsegments 144 total 13:45\n\n(どの音で伸びているかを調べた結果。* は0.3秒を超えた音)\ntts/036.log sil* o n a j i h a N t o sh i n o sh u cl k a o y a k u i ch i m a N ky u u s e N hy a k u d a i t o s u i k e e sh* I* t* e* i* t* e* sil*\ntts/051_p1.log sil ts U k a i n a g a r a d e e t a o a ts u m e r u u g o k i g a d e t e k i t e i r u N* d* e* s* U* y* o* sil*\ntts/079.log sil h o k a n o k a N ky o o d e m a n a N d a k o t o o m o cl t e k u r u k e N ky u u m o s* u* s* u* N* d* e* i* m* a* s* U* sil*\ntts/091_p1.log sil* j a a r o b o cl t o n i ch a cl t o j i i p i i t i i n o sh u N k a N w a t* o* o* b* u* N* k* o* n* a* i* sil*\ntts/104.log sil* w a N e cl k U s u g a i e n o n a k a d e e N k a k u d e t e ts u d a i n a g a r a u g o k i o o b o e s a s e t e i* r* u* n* o* m* o* sil*\n\n(5文の OVERRIDE に読点を足し、「値を送る → あたいを送る」を読み替え表に足して作り直した)\n$ python3 tts2.py | grep -v ^READ\nsegments 144 total 13:32\n[] ← 間延びのあった文の一覧(0件)\nボクノキ’タイトソフ’トノクミアワセデワ、カタノカンセツニ、キュージュードチョットヲコエルアタイヲオクルト、\nオナジハントシノシュッカヲ、ヤクイチマンキューセンヒャクダイト、スイケーシ’テイテ、\nロボットアームノツメサキガドコニクルカワ、フ’ツーワカンセツノカクドカラケーサンデダシマス’、</code></pre><h3 id=\"index_sWdoCk8D\">色の検証(validate_palette.js)</h3><pre><code>$ node scripts/validate_palette.js \"#2A7A9B,#D9542B\" --mode light --surface \"#F4F2ED\"\nPalette (light, surface #F4F2ED, categorical): 2 slots\n [PASS] Lightness band all 2 inside L 0.43–0.77\n [FAIL] Chroma floor below floor (reads gray): [[\"#2A7A9B\",0.091]]\n [PASS] CVD separation worst adjacent #D9542B↔#2A7A9B ΔE 15.7 (protan) · tritan 32.3\n [PASS] Normal-vision floor worst adjacent #D9542B↔#2A7A9B ΔE 27.5 (normal)\n [PASS] Contrast vs surface all 2 >= 3:1\n\n → FAILED — fix the marked checks (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)\n scope: categorical palettes only. For a lone status/text color check WCAG text contrast; for a sequential ramp, lightness monotonicity.\n\n$ node scripts/validate_palette.js \"#1F7FB0,#D9542B\" --mode light --surface \"#F4F2ED\"\n [PASS] Lightness band all 2 inside L 0.43–0.77\n [PASS] Chroma floor all 2 >= 0.1\n [PASS] CVD separation worst adjacent #D9542B↔#1F7FB0 ΔE 18.7 (protan) · tritan 32.3\n [PASS] Normal-vision floor worst adjacent #D9542B↔#1F7FB0 ΔE 28.9 (normal)\n [PASS] Contrast vs surface all 2 >= 3:1\n → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)\n\n(記事サムネイルの配色に合わせたあと)\n$ node scripts/validate_palette.js \"#1A6C98,#C4471F\" --mode light --surface \"#F5F5F0\"\n [PASS] Lightness band all 2 inside L 0.43–0.77\n [PASS] Chroma floor all 2 >= 0.1\n [PASS] CVD separation worst adjacent #C4471F↔#1A6C98 ΔE 17.2 (protan) · tritan 30.4\n [PASS] Normal-vision floor worst adjacent #C4471F↔#1A6C98 ΔE 27.2 (normal)\n [PASS] Contrast vs surface all 2 >= 3:1\n → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)</code></pre><h3 id=\"index_YpOeZt_F\">書き出しの結果</h3><pre><code>(試作1本目・153文)\ntotal frames 24385 = 812.8333333333334 s\nstream|codec_name=h264|width=1920|height=1080\nstream|codec_name=aac\nformat|duration=812.900000\n\n(間延びした6文を直したあと)\ntotal frames 23532 = 784.4 s\nvideo,784.400000\naudio,784.400000\n\n(codex の指摘・デザイン・読み・自己紹介を反映したあと・144文)\ntotal frames 24448 = 814.9333333333333 s\nvideo,814.933333\naudio,815.000000\n\n(記事サムネイルの配色・ロゴ・「毎日」の削除を反映したあと)\ntotal frames 24434 = 814.4666666666667 s\n\n(30cm の説明2文を足したあと・146文)\ntotal frames 24891 = 829.7 s\n\n(打ち出し・章の扉・点の並びを入れたあと)\ntotal frames 24891 = 829.7 s\n-rw-r--r-- 1 root root 47663796 Oct 11 22:01 /tmp/claude-0/-home-claude/9041d877-fbc0-57ad-97c5-f63dfd4877c5/scratchpad/v/out.mp4\n</code></pre><h3 id=\"index_ImJl488D\">KIOXIA への転送で、1つ目の断片だけが化けた記録</h3><pre><code>(作業環境側で12MBずつに分けた断片の md5)\nb3a5e3da6efb449d63049eb14400a1d3 p_00\nbff9ad51719a351b1698a90a11152082 p_01\n32d70b67218b8e9594637b6e06dadc7b p_02\nc41f4eedb61bdd951e2484d85e6f6691 p_03\n元の作業用 mp4: a54a7d2fa84886ceab3237618ffd5838\n\n(Mac 側に届いた断片の md5。p_00 だけ一致しない)\nda313675885a59bd4988dbe2ce6270ee p_00\nbff9ad51719a351b1698a90a11152082 p_01\n32d70b67218b8e9594637b6e06dadc7b p_02\nc41f4eedb61bdd951e2484d85e6f6691 p_03\n-rwx------ 12588787 Oct 11 12:44 p_00 ← 元は 12582912 バイト(5,875 バイト多い)\n-rwx------ 12582912 Oct 11 12:44 p_01\n-rwx------ 12582912 Oct 11 12:45 p_02\n-rwx------ 9186276 Oct 11 12:45 p_03\nつないだ mp4: e5e65bee0bd4cf47694b33163d54adc3 ← 元と一致しない\n\n(p_00 だけ gzip で包んで送り直し、Mac 側で展開してからつないだ)\n12582912 b3a5e3da6efb449d63049eb14400a1d3 ← 展開した p_00。元と一致\na54a7d2fa84886ceab3237618ffd5838 ← つないだ作業用 mp4。元と一致\n\n(最終版でも同じ手順で送った)\n元の作業用 mp4: 2dd0e053c2da238bbd073bb7bf8b12ad\nつないだ作業用 mp4: 2dd0e053c2da238bbd073bb7bf8b12ad</code></pre><p></p><h2 id=\"index_o0bVwuXE\">codex への検証依頼の全文</h2><p>台本の全文は、検証にかけた時点の版です(このあと書き直しています)。</p><pre><code class=\"language-markdown\"># 敵対的検証依頼:YouTube台本「ロボットにもChatGPTの瞬間はもう来たのか」\n\n## あなたへの依頼\n\nこれから公開する解説動画(約13分)の台本を、**この台本を論破するつもりで**検証してください。褒める必要はありません。見落としを1つでも多く見つけることが目的です。\n\n- 台本の書き手:Pixels to Motors(エイイチ)。秋田でロボットアーム(mechArm 270-Pi)を自宅で動かし、記録を公開している個人。29年のデザイナー経験。\n- 視聴者:フィジカルAIに関心はあるが専門家ではない日本語話者\n- 台本の結論:「見て判断する」ところは安くなった(ChatGPTの瞬間がもう来た)。「触れて動かす」ところはまだ高い(データの壁があり、まだ来ていない)\n- 検証日:2026年10月11日。Webを参照できる場合は一次情報まで遡って確認してください。参照できない場合は「未確認」と明記し、推測で OK を出さないでください\n\n## 検証してほしい観点\n\n### A. 事実と数字\nA1. 各数字が出典に実際に書かれているか。出典の数字を取り違えていないか(期間・対象・単位)\nA2. 出典自体の信頼性。調査会社のプレスリリース、企業の自社発表、二次報道の区別がついているか\nA3. 2026年10月11日時点で古くなっている情報がないか(特に 1X NEO の出荷状況、Generalist の最新発表、出荷台数)\nA4. 本人の実機の数字(S061 の650回以上、S068〜S071 の30cm以上、S074 の170万点、S076〜S077 の90度ちょっと)が、話の文脈で誤解を生まない言い方になっているか\n\n### B. 論理\nB1. 結論(S143〜S144)「見て判断する部分にはもう来ている」は、台本の中で示した根拠(本人の判定の体験談1つ)だけで言い切れるか。反例はないか\nB2. 「データの壁」の説明(S105〜S108:10万年 vs 57年、約1700分の1)は比較として成立しているか。台本の断り書き(単位をそろえた比較ではない)で十分か\nB3. (S028〜S039)「出荷ロボットの6割以上が見せる用途・研究・データ集め」と「SAG では工場・店舗向けが7割超」を並べた扱いは公正か。どちらかに寄せた印象操作になっていないか\nB4. 本人の机の上の体験(ikpy のずれ、黙る失敗)から、業界全体の「触れて動かすはまだ高い」へ一般化してよいか。飛躍がないか\nB5. 「人型ロボット」の話と「ロボットアーム」の話、「フィジカルAI」全体(自動運転を含む)の話が混ざって、範囲がぶれていないか\n\n### C. 抜けている反論・視点\nC1. 台本の主張に対して、専門家なら真っ先に出す反論は何か(例:シミュレーション合成データ、世界モデル、VLA の汎化など)\nC2. 触れずに済ませているが、視聴者の判断を変えうる事実はないか\n\n### D. 言い方\nD1. 断定が強すぎる文、逆に曖昧すぎて意味のない文\nD2. 誤解を生む用語の使い方(例:「フィジカルAI」「データ集め」「ChatGPTの瞬間」の定義)\nD3. 特定の企業・人物について、不公平・不正確な描写\n\n## 回答の書式\n\n指摘ごとに次の形で書いてください。重大なものから順に並べてください。\n\n```\n[❌ NG / ⚠️ 要注意] 文番号(例:S043)\n問題:何が問題か(1〜2文)\n根拠:確認した出典(URL)または推論の筋道\n修正案:差し替える文(台本の話し言葉の調子に合わせて)\n```\n\n最後に次の3点をまとめてください。\n1. このまま公開してよいか(はい/直してから/大きく作り直すべき)\n2. 公開前に必ず直すべき指摘の文番号\n3. 台本の結論そのものへの評価(成立している/条件つきで成立/成立していない)と、その理由\n\n---\n\n## 台本が使った出典\n\n| 主張 | 文番号 | 出典 |\n|---|---|---|\n| 2026年上半期の人型ロボット出荷2万2000台超・前年同期の約4倍・通年5万台超の予想・用途の6割以上が見せる用途と研究・データ集め | S001〜S005, S028〜S029 | Counterpoint Research(2026年8月)https://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026 ※台本作成時はページ本文を読み込めず、検索結果の抜粋から数字を取った |\n| 同期間:中国メーカー97%超・AGIBOT 44%・Unitree 31%・工場や店舗向け70%超 | S034〜S035, S041〜S042 | Smart Analytics Global の調査を紹介した TechRepublic(2026年8月11日)https://techrepublic.com/article/news-humanoid-robot-shipments-chinese-vendors |\n| 1X NEO:2025年10月に予約開始・2万ドルまたは月499ドル・難しい作業は担当者が遠隔操作・2026年10月初めの時点で顧客への出荷の発表なし | S044〜S049 | 1X 予約ページ/botinfo.ai(2026年10月5日確認)https://botinfo.ai/articles/1x-neo-home-robot / AI2Work https://ai2.work/blog/1x-neo-ships-into-homes-with-a-human-teleoperator-still-inside |\n| インターネットの文章を人が読むと約10万年(10万年のデータの壁)、狭い仕事を確実にこなすロボットを現場に置いて働きながらデータを集めるのが近道 | S087〜S096, S130〜S132 | Ken Goldberg, Science Robotics(2025年8月)https://news.berkeley.edu/?p=134910 |\n| Generalist:27万時間(2025年11月)→50万時間超(2026年4月)・新しいロボットや作業に合わせるデータは約1時間の場合も・選んだ簡単な作業で成功率99%超 | S097〜S104 | Generalist の発表を紹介した記事 https://yespress.io/generalist / https://www.humanoidsdaily.com/news/generalist-ai-unveils-gen-0-claims-scaling-laws-for-robotics-backed-by-270-000-hours-of-real-world-data |\n| 本人の実機:PICK→PLACE の通し試行656回(2026年6月18日〜7月20日)、FKテーブル約170万点、ikpy とデジタルツインの爪先が同じ関節角で300mm以上ずれる、j1 が約90.7度を超えると send_angles がエラーなしで無視される | S061, S068〜S071, S074, S076〜S077 | 本人の台帳 pick_place_ledger_v3.csv、公開記事 DAY257・DAY283、設計メモ arm_kinematics.md(非公開資料を含むため、ここは記述の妥当性だけ見てください) |\n\n## 台本全文(動画で読み上げている版)\n\n\n### 0 フック\n\nS001 今年の1月から6月までに、世界で出荷された人型ロボットは、2万2000台を超えました。\n\n### 0 フック\n\nS002 前の年の同じ時期の、およそ4倍です。\n\n### 0 フック\n\nS003 調査会社のCounterpointが、8月に出した数字です。\nS004 年末までには、5万台を超えると予想されています。\nS005 家事をする人型ロボットの予約販売も、去年の秋から始まっています。\nS006 こういうニュースを見ると、ChatGPTが出たときみたいに、\nS007 ロボットも一気に賢くなって、家に来るのはもうすぐ、って感じませんか。\n\n### 1 自己紹介\n\nS008 Pixels to Motors。こんにちは、エイイチです。\n\n### 1 自己紹介\n\nS009 僕は秋田で29年デザイナーをやりながら、\n\n### 1 自己紹介\n\nS010 自宅の机の上でロボットアームを動かして、その記録を毎日公開しています。\n\n### 1 自己紹介\n\nS011 作る側の机の上から見える、フィジカルAIの話をします。\n\n### 2 今日の問い\n\nS012 今日の問いは、ロボットにもChatGPTのような瞬間は、もう来たのか、です。\nS013 フィジカルAIっていうのは、AIに体を持たせて、\nS014 現実の世界で物に触れて動かす技術のことなんですよ。\nS015 ロボットアームも、人型ロボットも、自動運転もここに入ります。\n\n### 3 結論と立場\n\nS016 先に結論を言っちゃうと、安くなったのは「見て判断する」ところで、\n\n### 3 結論と立場\n\nS017 「触れて動かす」ところは、まだ高いんですよ。\n\n### 3 結論と立場\n\nS018 鍵になるのが、「データの壁」です。\nS019 先に言っておくと、フィジカルAIはまだ使えない、と言いたいわけじゃないんです。\nS020 どこまで来ていて、どこで止まっているのかを、分けて見たいんです。\n\n### 4 目次\n\nS021 大きく3つあります。\n\n### 4 目次\n\nS022 出荷が4倍になったロボットは、何をしているのか。\n\n### 4 目次\n\nS023 僕の机の上のロボットアームで、何が簡単で、何が難しかったのか。\n\n### 4 目次\n\nS024 そして、データの壁はどれくらい高くて、誰がどう越えようとしているのか。\n\n### 4 目次\n\nS025 最後に僕の見立てを話します。\n\n### 5 節1:出荷されたロボットは何をしているのか\n\nS026 1つ目、出荷が4倍になったロボットは、何をしているのか、です。\nS027 まず数字の中身を見ます。\nS028 Counterpointによると、出荷された人型ロボットのうち6割以上は、\nS029 展示やイベントで動きを見せる用途と、研究やデータ集めの用途なんですよ。\nS030 データ集めっていうのは、人がロボットを遠隔で動かして、その動きを記録して、\nS031 AIが学ぶための教材を作る仕事のことです。\nS032 つまり、ロボットの大きな買い手の1つは、ロボットに教えるためのロボットなんです。\nS033 ただ、ここで1つ断っておきます。\nS034 別の調査会社のSmart Analytics Globalは、同じ半年について、\nS035 工場や店舗向けが7割を超えた、と言っているんですよ。\nS036 同じ期間なのに、ほぼ逆の数字です。\nS037 用途の分け方が、会社によって違うんだと思います。\nS038 だから「全部見せ物だ」とも、「もう工場で働いている」とも言い切れない。\nS039 そこは正直に言っておきます。\nS040 はっきりしているのは、どこが作っているかです。\nS041 SAGの数字だと、出荷の97%以上が中国のメーカーです。\nS042 上位のAGIBOTとUnitreeの2社だけで、およそ4分の3なんですよ。\nS043 家のほうも見てみます。\nS044 アメリカの1Xという会社は、家事をする人型ロボット、NEOの予約を、去年の10月に始めました。\nS045 値段は2万ドル。月499ドルのプランもあります。\nS046 ただ、難しい家事は、1Xの担当者がVRゴーグルをつけて、\nS047 ロボットのカメラ越しに家の中を見ながら、遠隔で動かす仕組みなんですよ。\nS048 今年中にアメリカで届け始める予定ですが、\nS049 10月の初めの時点で、お客さんの家に届いたという発表は、まだありません。\nS050 これ、ちょっと考えさせられませんか。\nS051 売っているのはロボットだけじゃなくて、遠隔で動かす人と、\nS052 そこで集まるデータの仕組みごとなんです。\nS053 で、ここが大事なんですけど、工場の話にも家の話にも、\nS054 データを集めるという、同じ目的が出てくるんですよ。\nS055 じゃあ、なぜそこまでしてデータを集めないといけないのか。\nS056 その前に、僕の机の上の話をさせてください。\n\n### 6 節2:机の上のロボットアームで、簡単だったこと・難しかったこと\n\nS057 2つ目、僕の机の上のロボットアームで、何が簡単で、何が難しかったのか、です。\nS058 ここは僕自身の話から始めさせてください。\nS059 僕は自宅の机で、mechArm 270という小さなロボットアームを動かしています。\nS060 赤いブロックを掴んで、箱に置く。\nS061 それだけの作業を650回以上試して、全部の結果を台帳に残しているんですよ。\nS062 まず、簡単だったほうから話します。掴めたかどうかの判定です。\nS063 iPhoneで撮った写真をAIに見せて、「ブロックを掴めていますか」と聞くと、\nS064 はい、いいえで答えが返ってくる。\nS065 少し前なら、ここを作るだけで大仕事だったはずなんですよ。\nS066 難しかったのは、動かすほうでした。\nS067 ロボットアームの爪の先がどこに来るかは、普通は関節の角度から計算で出します。\nS068 ところが、ikpyという、よく使われる計算ツールで出した爪先の位置と、\nS069 パソコンの中に作ったアームの分身で、実際に動かした位置が、\nS070 同じ関節の角度なのに、30cm以上ずれたんです。\nS071 30cmですよ。机の上のブロックなら、端から端まで外れます。\nS072 だから僕は、その分身、デジタルツインの中でアームを実際に動かして、\nS073 爪先がどこに来るかを、1点ずつ測って表にしました。\nS074 その表は今、170万点ほどあります。\nS075 もう1つ困ったのが、黙る失敗です。\nS076 肩の関節に、90度ちょっとを超える角度を送ると、\nS077 エラーも出さずに、指令が黙って捨てられるんですよ。\nS078 アームは動かない。でも、何も言ってこない。\nS079 ログを1行ずつ見て、やっと気づきました。\nS080 でも、ポイントはここなんです。\nS081 見て判断するところは、AIに頼めばすぐ手に入りました。\nS082 でも、爪先がどこに来るか、指令がちゃんと届いたかは、\nS083 自分の机の上で、自分のアームで測るしかなかった。\nS084 インターネットのどこにも、僕のアームの、僕の机の上のデータはないんですよ。\nS085 じゃあ、この「どこにもないデータ」は、世界全体ではどれくらい足りないのか。\n\n### 7 節3:データの壁の高さと、越えようとしている会社\n\nS086 3つ目、データの壁はどれくらい高くて、誰がどう越えようとしているのか、です。\nS087 カリフォルニア大学バークレー校のロボット研究者、\nS088 ケン・ゴールドバーグさんが、去年、面白い計算をしています。\nS089 ChatGPTのようなAIの学習に使われたインターネットの文章を、\nS090 人が全部読むと何年かかるか。答えは、約10万年なんですよ。\nS091 ロボットが手を動かすためのデータは、その足元にも及ばない。\nS092 ゴールドバーグさんは、これを「10万年のデータの壁」と呼んでいます。\nS093 ChatGPTは10万年分の教科書で育ったけれど、\nS094 ロボットには、その教科書がまだない、ということなんです。\nS095 ここで、じゃあロボットにChatGPTの瞬間は当分来ない、\nS096 と言いたくなるじゃないですか。でも、ちょっと待ってほしいんです。\nS097 アメリカのGeneralistという会社は、人が手で作業する様子を、\nS098 安い装着型の機材で記録して集めています。\nS099 去年の11月の時点で27万時間、今年の4月には50万時間を超えました。\nS100 半年で2倍近くです。\nS101 しかもこの会社によると、新しいロボットや新しい作業に合わせるのに、\nS102 そのロボット自身のデータは、1時間ほどで済む場合があって、\nS103 選んだいくつかの簡単な作業では、成功率が99%を超えたそうです。\nS104 会社自身の発表なので、そこは差し引いて聞いてください。\nS105 50万時間を年に直すと、約57年です。\nS106 10万年と比べると、まだ1700分の1くらい。\nS107 ただ、これは人が文章を読む時間と、作業を記録した時間なので、\nS108 単位をそろえた比較ではありません。目安として聞いてください。\nS109 つまり、壁はまだ高い。でも、壁の前でみんなが土を積み始めている。\nS110 1Xが家の中で遠隔操作しながらデータを集めているのも、\nS111 出荷されたロボットの多くがデータ集めに回っているのも、\nS112 同じ壁に向かっている動きなんです。\nS113 ただ、ここで「なんだ、まだ全然ダメなのか」で\nS114 片付けてしまうのも違うんです。\nS115 見て判断するほうには、もう10万年分の教科書がある。\nS116 そこは僕の机の上でも、もう普通に使えています。\nS117 じゃあ、この2つが混ざっている今、ロボットのニュースの何を見ればいいのか。\n\n### 8 見立て\n\nS118 ここからが、僕の一番言いたいことです。\nS119 データの壁を知っていると、\nS120 ロボットのニュースを見たときの問いが、2つに変わるんですよ。\nS121 1つは、そのロボットは誰が動かしていて、データは誰が集めているのか、です。\nS122 人が遠隔で動かしているのか、ロボットが自分で動いているのか。\nS123 デモ動画では、ここがほとんど言われないんですよね。\nS124 もう1つは、その仕事は「見て判断する」で済むのか、\nS125 「触れて動かす」精度が要るのか、です。\nS126 棚の商品が足りているかを見るのは、前者です。\nS127 柔らかい物を潰さずに掴むのは、後者です。\nS128 前者なら、もう安く使える。\nS129 後者は、その現場で測って固める仕事が、まだ残っている。\nS130 ゴールドバーグさんも、昔ながらのエンジニアリングで、\nS131 狭い仕事を確実にこなすロボットを現場に置いて、\nS132 働きながらデータを集めるのが近道だ、と言っています。\nS133 僕の机の上でやっていることも、まさにそれなんですよ。\nS134 判断はAIに任せる。動きは、自分のアームで測って固める。\nS135 で、作る側として、もう1つ言わせてください。\nS136 データの壁を越えたあとも、最後に残るのは「どう動くか」なんです。\nS137 例えばロボットが人に物を渡すとき、\nS138 何秒待って手を開けば、人は安心して受け取れるのか。\nS139 これは、成功か失敗かの判定だけでは出てこない。\nS140 動きの質そのものを設計する仕事が、\nS141 壁の向こうにも残っていると、僕は見ています。\n\n### 9 冒頭の問いへの回答\n\nS142 じゃあ、冒頭の問いに答えますね。\n\n### 9 冒頭の問いへの回答\n\nS143 ロボットにChatGPTの瞬間は、見て判断する部分には、もう来ています。\n\n### 9 冒頭の問いへの回答\n\nS144 でも、現実に触れて動かす部分には、まだ来ていません。\n\n### 9 冒頭の問いへの回答\n\nS145 その差を埋めるデータを、今、世界中で必死に集めている最中です。\nS146 ロボットのデモ動画を見たら、\nS147 「これは誰が動かしていて、何回やって、何回うまくいったのか」を、\nS148 一度だけ考えてみてください。\nS149 それだけで、同じ動画の見え方が、かなり変わると思います。\n\n### 10 アウトロ\n\nS150 今日はここまでです。\n\n### 10 アウトロ\n\nS151 この動画が気に入ったら、高評価とチャンネル登録をお願いします。\n\n### 10 アウトロ\n\nS152 コメントは全部読ませてもらっています。それではまた。</code></pre><p></p><h2 id=\"index_tELWWv5H\">codex から返ってきた検証結果の全文</h2><pre><code>判定は「直してから」です。現在の結論は、台本の根拠だけでは成立していません。 特に、①「ChatGPTの瞬間」の定義、②異種データの1700分の1比較、③実機の設定・制御問題を学習データ不足へつなげる論理、④Generalistの最新発表の欠落が重大です。\n2026年10月11日時点でWeb確認しました。以下では、一次資料の本文確認、検索抜粋だけの確認、推論、未確認を区別します。本人の台帳・設計メモは依頼どおり検証対象外とし、数字の伝え方だけを評価します。\n[❌ NG] S016〜S018、S143〜S145\n問題:「ChatGPTの瞬間」が未定義です。写真への回答を一度得られたことから、視覚判断全体の転換を宣言し、さらに物理操作全体には転換がないと断定するのは、両方向に飛躍しています。\n根拠:使いやすさ、汎化、信頼性、普及、費用は別の評価軸です。例えば[Gemini Roboticsの公式資料](https://deepmind.google/en/models/gemini-robotics/gemini-robotics/)は、視覚と言語から動作を出すモデルを示していますが、提供はPrivate previewです。「能力がある」と「誰でも利用できる」も分ける必要があります。\n修正案:「ここではChatGPTの瞬間を、専門家でなくても、いろんな仕事にすぐ使えるようになる転換点、と呼びます。僕の机では、画像に質問する入口はぐっと簡単になりました。でも、いろんな家で安全に家事を任せられる転換点まで来たかは、まだ判断できません。」\n[❌ NG] S105〜S109\n問題:57年への換算は正しいですが、「1700分の1」はデータの不足率ではありません。「目安」と断っても、比較対象の意味が違うため、壁の高さを数量化したことにはなりません。\n根拠:500,000÷24÷365≒57.1、100,000÷57.1≒1752。前者は作業記録の総時間、後者は異種の学習資料を人間が読む・見る時間への換算です。情報密度、重複、センサー、タスクの多様性、必要性能がそろっていません。また一社のデータ量は、世界全体の総量でもありません。\n修正案:「50万時間は、連続した時間に直すと約57年です。ただ、文章や画像を見る時間と、作業を記録した時間は、そのまま比べられません。何倍足りないかではなく、必要な経験をどれだけ広く集められるかが問題なんです。」\n[❌ NG] S068〜S074、S082〜S085、S133〜S134\n問題:計算モデル間の30cmの不一致と指令の不受理は、それだけではAIの学習データ不足を示しません。モデルの整合性や制御インターフェースの問題を、業界の「データの壁」の実例として使っています。\n根拠:同じ角度の比較には、座標系、リンク寸法、関節軸、角度の符号・原点、工具先端の定義を一致させる必要があります。どちらが正しいかは実機との比較が必要です。原因は非公開資料のため未確認です。\n修正案:「僕の設定では、計算モデルとシミュレーターの位置が30cm以上食い違いました。これはAIの学習データ不足とは別に、モデルや制御の整合性を確かめる仕事が必要だった、という話です。」\n[❌ NG] S099〜S104、S109、S129、S144\n問題:4月のGEN-1を最新状況のように紹介していますが、8月のGEN-1.5が抜けています。新しい作業への適応負担を論じる動画では、視聴者の判断を変える重要な欠落です。\n根拠:[Generalistの8月19日公式発表](https://generalistai.com/blog/gen-1.5)は、短い実演を使った新タスクへの適応を報告しています。ただし同社自身の評価で、対象は簡単で短い作業です。家庭全体への汎化を証明したわけではありません。\n修正案:「さらに8月にはGEN-1.5を発表して、簡単で短い作業なら、数秒の実演から適応できる例も報告しています。会社発表なので評価条件は見る必要がありますが、『動かすには毎回大量の専用データが必要』とも言い切れなくなっています。」\n[❌ NG] S034〜S038\n問題:SAGの「industrial and commercial」を「工場や店舗向け」と狭めています。また用途と導入先は重なり得るため、「ほぼ逆の数字」「分類の違いが原因」とする説明は未立証です。\n根拠:[SAGの一次発表](https://smartanalyticsglobal.com/global-humanoid-robot-shipments-2026-agibot-unitree/)の表現は産業・商業用途です。同社の総出荷推計も約19,100台で、Counterpointの22,000台超とは異なります。両調査の分類対応表や集計方法は、今回確認できていません。商業施設での展示と研究用途が排他的とは限りません。\n修正案:「一方、SAGは約1万9100台と推計して、産業・商業用途が7割超としています。ただ、総数も分類も違うので、この2つは単純な裏表として比べられません。どれだけ自律的に実務をこなしたかは、この数字だけでは分からないんです。」\n[❌ NG] S089〜S094、S115〜S116\n問題:Goldbergの10万年は「文章だけ」ではありません。さらに、その比喩を「見て判断する側には十分な教科書がある」という性能保証に変えています。\n根拠:[Science Roboticsの原文](https://autolab.berkeley.edu/assets/publications/media/GOFE-Can-Close-the-100000-Year-Robot-Data-Gap-Science-Robotics-Aug-2025-scirobotics.aea7390.pdf)は、現代のVLMの学習に使う文章と画像を読む・見る時間への概算です。これはEditorialであり、一般ロボットに必要なデータ量の実証値ではありません。\n修正案:「ゴールドバーグさんは、画像と言葉を扱うAIの学習資料を、人が読む・見る時間に換算すると、約10万年規模だと紹介しています。ロボットにも同じ量が必要だと分かったわけではありませんが、動作指令と結びついた経験は集めにくい、という問題提起です。」\n[❌ NG] S083〜S084、S129、S145\n問題:「自分の机で測るしかない」は、他環境・他機体からの転移を否定する表現です。また、データだけが判断と動作の差を埋めるように聞こえます。\n根拠:[Gemini Robotics 1.5技術報告](https://storage.googleapis.com/deepmind-media/gemini-robotics/Gemini-Robotics-1-5-Tech-Report.pdf)は機体をまたいだ技能転移を報告しています。専門家なら、VLAの汎化、シミュレーション、世界モデル、モデルに基づく制御をまず反論として出します。[NVIDIAの公式説明](https://developer.nvidia.com/isaac/gr00t?ncid=so-yout-261862-vt48)も実データと合成データの併用を示しています。ただし、これらが現場検証を不要にしたとは言えません。\n修正案:「ほかの環境で学んだことを持ってくる研究も進んでいます。それでも、僕の構成でちゃんと動くかは確認が必要でした。差を埋めるのはデータだけでなく、モデル、制御、機械そのものの改善でもあります。」\n[⚠️ 要注意] S013〜S015、S017、S144\n問題:「物に触れて動かす」で自動運転まで定義し、その後はほぼ物体操作と家庭用人型ロボットの話になります。広い領域を否定する結論になっています。\n根拠:台本内の対象の不一致です。Goldbergの原文自身も、自動運転や荷物仕分けを既に実用化された例として挙げています。限定用途の実用化と、汎用家庭ロボットの普及は別問題です。\n修正案:「フィジカルAIは、現実世界を認識して行動するAIのことです。自動運転も入りますが、今日は主に、アームや人型ロボットが物を扱う話、とくに家庭のような変化の多い場所に絞ります。」\n[⚠️ 要注意] S001〜S004、S028〜S029、S040〜S042\n問題:数字の記載は確認できても、市場の確定実績として扱うには調査方法の確認が不足しています。「はっきりしている」も強すぎます。\n根拠:Counterpointの公式ページ由来の検索抜粋では、22,000台超、前年比約300%増、通年50,000台超の予測、用途の合計60%超を確認しました。直接開いた本文は取得できず、集計定義は未確認です。SAGの本文では中国メーカー97%超、44%+31%を確認しました。いずれも調査会社の推計で、出荷は自律稼働や顧客検収を意味しません。[Counterpoint](https://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026)、[SAG](https://smartanalyticsglobal.com/global-humanoid-robot-shipments-2026-agibot-unitree/)\n修正案:「Counterpointの推計では、今年上半期の出荷は2万2000台超、前年同期のおよそ4倍です。ただ、出荷台数は、そのまま自律的に働いている台数ではありません。中国メーカーへの集中も、SAGの推計では非常に大きいです。」\n[⚠️ 要注意] S030〜S032、S053〜S054、S110〜S112\n問題:「データ集め」を遠隔操作だけに限定し、研究用途までデータ生産へ読み替えています。さらに工場と家庭の共通の「目的」として断定しています。\n根拠:研究には制御・機構・移動などの検証も含まれ、全てが学習データ収集とは限りません。作業中の自律ログや人の実演も収集方法です。「研究・データ生産」という合算から、それぞれの比率や購入目的は分かりません。\n修正案:「データ集めには、人が遠隔で動かして記録する方法も、自律作業の記録を残す方法もあります。ただ、研究向けの台数が全部データ集めに使われる、という意味ではありません。」\n[⚠️ 要注意] S044〜S049、S051〜S052\n問題:価格プランの提供時期を省き、遠隔支援が常時の仕組みのように聞こえます。顧客出荷については、発表を見つけられないことと、出荷していないことを区別すべきです。\n根拠:[1Xの発売発表](https://www.1x.tech/discover/neo-home-robot)は2025年10月28日、20,000ドルのEarly Accessは2026年優先配送、499ドルの月額プランは後日配送です。[現行注文ページ](https://www.1x.tech/order)では基本的自律性と予約した時間のExpert Modeを説明しています。VRゴーグルの具体的運用は今回の公式本文では未確認。10月11日までの一般顧客への配送開始を示す公式発表も今回の検索では確認できませんでした。\n修正案:「2万ドルの早期購入枠は2026年配送予定で、月499ドルのプランは後日です。初期の自律機能に加えて、知らない作業では予約した時間に担当者が遠隔で支援します。10月11日時点で、一般顧客への配送開始を示す公式発表は、僕が確認した範囲では見つかっていません。」\n[⚠️ 要注意] S063〜S065、S081、S126〜S129\n問題:回答が返ることと、正しく判定できることを混同しています。「前者ならもう安く使える」は、精度や運用条件を無視しています。\n根拠:判定を評価するには、実際の保持状態との照合、失敗画像、遮蔽、誤判定率などが必要です。棚の欠品検知にも死角や商品識別、照明、誤報対応があります。台本にはモデル名、正答率、費用がなく、「安い」の実証もありません。\n修正案:「写真を見せて質問する入口は、驚くほど簡単でした。ただ、答えが返ることと、毎回正しいことは別です。僕の条件でどこまで任せられるかは、失敗例も含めて確かめる必要があります。」\n[⚠️ 要注意] S061\n問題:650回以上が成功した回数、あるいは同一条件での評価回数に聞こえます。提示された情報は、期間中のPICK→PLACE通し試行656回です。\n根拠:ユーザー提示の台帳説明に基づく指摘です。試行間の構成変更や中断の扱いは未確認で、成功率やモデル性能は推定できません。\n修正案:「6月18日から7月20日までに、掴んで置く一連の試行を656回記録しました。これは成功回数ではなく、失敗も含めた試行回数です。」\n[⚠️ 要注意] S069〜S074\n問題:「実際に動かした」が実機測定に聞こえ、170万点が現実の経験データとして受け取られます。提示された説明ではシミュレーター内のFK表です。\n根拠:仮想モデルの大量サンプリングは、そのモデルの写像を細かくしたものです。点数が多くても、モデルと実機の一致や接触精度を証明しません。\n修正案:「シミュレーターの中で関節角と爪先位置の対応を計算して、約170万点の表にしました。実機で170万回測ったわけではなく、現物との一致は別に確認する必要があります。」\n[⚠️ 要注意] S076〜S079\n問題:90度ちょっとという閾値を機種全体の仕様として語っています。また「指令が捨てられる」は、通信を観測していなければ原因の断定です。\n根拠:提示情報は本人の構成でj1が約90.7度を超えたときの観察です。ファームウェア、SDK、他関節の値、検証箇所は未確認です。「肩」だけでは関節も特定しにくくなります。\n修正案:「僕の機体とソフトの組み合わせでは、j1に約90.7度を超える値を送ると、例外が出ないのに動かないことがありました。機種全体の仕様か、どの段階で止まったのかは、まだ切り分けが必要です。」\n[⚠️ 要注意] S099〜S104\n問題:期間が「半年」と丸められ、99%超が評価全体の平均のようにも聞こえます。1時間は追加のロボットデータであって、全学習・導入に必要な時間ではありません。\n根拠:[GEN-0公式発表](https://generalistai.com/blog/gen-0)は2025年11月4日、[GEN-1公式発表](https://generalistai.com/blog/gen-1)は2026年4月2日で約5か月です。27万時間超と50万時間超、平均99%と一部タスク99%超を区別する必要があります。自己申告の試験結果で、現場導入の総費用は示していません。\n修正案:「会社発表では、約5か月で27万時間超から50万時間超に増えました。事前学習に加えて、示した試験では約1時間のロボットデータを使い、選んだ作業群で平均99%の成功率を報告しています。導入全体が1時間で終わる、という意味ではありません。」\n[⚠️ 要注意] S130〜S133\n問題:Goldbergの提案を、個人の机上試行と「まさに同じ」としています。提案の肝である、実務で価値を生みながらデータを蓄積する点が抜けます。\n根拠:原文の提案は、従来の工学と学習を組み合わせ、商業環境で有用な仕事をするロボットから継続的にデータを集めるものです。家庭の開発試行との類似はありますが、稼働規模と実用性は異なります。前掲のScience Robotics原文。\n修正案:「僕の机の上も、AIと従来の制御を組み合わせる点では通じています。ただ、現場で役に立つ仕事をしながらデータが増える仕組みとは、まだ規模も段階も違います。」\n[⚠️ 要注意] S123、S136〜S141\n問題:「デモではほとんど言われない」は調査なしの一般化です。また人への物の渡し方を、データの壁を越えた後の課題に分離するのも不正確です。\n根拠:Generalistの公開動画には自律動作・再生速度の表示があります。人との動作の調整も、知覚、制御、学習、安全性を含む現在の研究対象です。[DeepMindの人とロボットの相互作用・安全性説明](https://deepmind.google/models/gemini-robotics/responsibly-advancing-ai-and-robotics/)もその一例です。\n修正案:「デモを見るときは、自律か遠隔操作か、速度や介入の条件が書かれているかを見ます。そしてデータ集めと並行して、人が安心して受け取れる動きを設計する仕事も必要なんです。」\n\n1. このまま公開してよいか:直してから。 体験談の章は残せますが、結論とデータの壁への接続は書き直す必要があります。\n2. 公開前に必ず直すべき文番号: S016〜S018、S034〜S038、S068〜S074、S082〜S085、S089〜S094、S099〜S109、S115〜S116、S128〜S129、S143〜S145。\n3. 結論そのものへの評価:現状では成立していない。 「見て判断する全体には来た/触れて動かす全体には来ていない」という二分は広すぎ、証拠も非対称です。次の結論なら条件つきで成立します。\n\n「僕の机では、画像に質問する入口はずっと簡単になりました。一方、物を確実に動かすには、機体や環境とのすり合わせがまだ必要でした。その負担を減らす研究は急速に進んでいます。でも、いろんな家庭で安全に家事を任せられる転換点まで来たかは、まだ判断できません。」</code></pre><p></p><h2 id=\"index_9Y98_SCr\">video_rules.md の全文</h2><pre><code class=\"language-markdown\"># video_rules.md\n\n## 動画制作ルール(唯一の正)\n\n最終更新: 2026-10-11\n状態: 試作1本目「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」を作りながら決めたことをまとめた初版。まだ決まっていない項目は8節にまとめている。\n\nYouTube 用の解説動画を作るときは、まずこのファイルを読む。台本の文体は language_rules.md、記事は article_rules.md が正。動画の構成・数字の扱い・画面・音声・書き出しは本ファイルが正。\n\n---\n\n## 1. 動画の構成(解説回の型)\n\n参考にした3本の解説動画(時事ニュースをデータと体験で検証する形式)から取った型。尺は13〜14分。\n\n| # | パート | 時間の目安 | やること |\n|---|---|---|---|\n| 0 | フック | 0:00〜0:45 | 挨拶より先に、視聴者が最近見たニュースや数字から入る。世間の感覚を代弁する(「〜って感じませんか」) |\n| 1 | 自己紹介 | 〜0:55 | ロゴのテロップを出し、毎回同じ文言で名乗る(2節の文言) |\n| 2 | 今日の問い | 〜1:10 | 問いを疑問文1つで言い、問いの中の言葉をその場で定義する |\n| 3 | 結論と立場 | 〜1:30 | 先に結論を言う。誤解されやすい方向を先に否定する(「〜と言いたいわけじゃないんです」) |\n| 4 | 目次 | 〜1:50 | 「大きく3つあります」と数を言ってから並べる |\n| 5〜7 | 本編(3節) | 〜10:00 | 「1つ目、〜です」で始め、各節の最後の1文を次の節の問いにする |\n| 8 | 見立て | 〜12:30 | 「ここからが僕の一番言いたいことです」と言ってから意見を話す |\n| 9 | 問いへの回答 | 〜13:20 | 冒頭の問いに3〜4文で答え、視聴者が次にニュースを見たときに考える問いを1つ渡す |\n| 10 | アウトロ | 〜13:40 | 毎回同じ文言 |\n\nパートごとのセリフの型(穴埋め式)と、実験回の型は Docs「動画台本テンプレート|台本例:フィジカルAIの現在地」にある。\n\n---\n\n## 2. 台本のルール\n\n公開前に codex に敵対的検証(台本を論破するつもりで見落としを探してもらう)をかけたところ「このまま公開は不可」と判定された。以下はそのとき直した点から作ったルール。\n\n1. **問いの中の言葉は、冒頭で定義する。** 例: 「ChatGPTの瞬間」を定義しないまま「来た/来ていない」と言い切っていた → 「専門家でなくても、いろんな仕事にすぐ使えるようになる転換点」と冒頭で言う形に直した。\n2. **話の範囲を言う。** 例: フィジカルAIの定義に自動運転を含めながら、アームの話だけで結論を出していた → 「自動運転も入りますが、今日はアームや人型ロボットが物を扱う話に絞ります」と言う。\n3. **数字は、出典で裏が取れて、論旨に必要なものだけ使う。** 増やすほど突っ込まれる場所が増える。試作1本目で削った数字: 1700分の1(単位の違う比較)、成功率99%・約1時間(会社の自己申告で条件の説明が要る)、170万点(シミュレーターの表なのに実機の測定と誤解される)、メーカー別の内訳(なくても話が変わらない)。\n4. **推計・会社自身の発表は、そう口で言う。** 「Counterpointの推計では」「会社の発表では」。調査会社同士で数字が違うときは、どちらかに寄せず「総数も分類も違うので、単純には比べられない」と言う。\n5. **意味や単位の違う数字を並べて比べない。** 例: 「人が文章を読む時間 10万年」と「作業を記録した時間 57年」を割り算していた → 比較ごと削った。\n6. **自分の実機の話は、自分の条件の話として言う。** 「僕の機体とソフトの組み合わせでは」を付ける。机の上で起きたことを業界全体の結論に広げない。例: 計算ツールとデジタルツインの爪先の位置が30cm以上食い違った話を「データ不足」の例にしていた → 「モデルや制御が自分の機体と噛み合っているかを確かめる、すり合わせの仕事」として話す形に直した。\n7. **自分の数字は、何の数かを添える。** 例: 「650回以上」→「失敗も含めた試行回数」。\n8. **最新の発表は、公開の直前に確認する。** 例: 4月の発表を最新として話していたが、8月に次の発表(GEN-1.5)が出ていた。\n9. **「見つからない」と「していない」を分ける。** 例: 「出荷していない」ではなく「配送開始を示す公式発表は、僕が確認した範囲では見つかっていません」。\n10. **自分の活動を盛らない。** 自己紹介の文言は「僕は本業のデザイナーをしながら、ロボティクスの研究と開発をしています。自宅の机の上でロボットアームを動かして、その記録を公開しています。」に固定する。「秋田」「29年」「毎日」は入れない。\n11. **公開前に codex で敵対的検証をかける。** 依頼ファイルの型は、試作1本目の動画フォルダにある `codex_敵対的検証依頼_*.md`。台本全文に文番号(S001〜)を振り、出典の表と検証の観点(事実・論理・抜けている反論・言い方)を付けて渡す。返ってきた指摘は、一次情報で確かめてから採用する。\n\n---\n\n## 3. ナレーション(現状は仮音声)\n\nいまは仮の合成音声で作っている。本番の声は未決(8節)。\n\n- **エンジン:** Open JTalk、声は nitech_jp_atr503_m001(男声)。設定は 速さ 1.08、音の高さ -1、声質 0.55\n- **読み替え表:** 英語の名前はカタカナで読ませる(ChatGPT→チャットジーピーティー、ikpy→アイケーパイ など)。誤読した語も表に足す。これまでの例: 爪先→つめさき(「つまさき」だと足の指先に聞こえる)、値→あたい、4分の3→よんぶんのさん。新しい固有名や数字を入れたら、エンジンの解析結果(カタカナの読み)を出して目で確認する\n- **間延びの検査:** ひと息で読む区間が長いと、その区間の最後の言葉が1音0.47秒ほどに引き伸ばされる(普段は0.1秒前後)。作った音声すべてで、1音0.3秒を超える箇所を検出する。見つかった文は、読み上げ用の原稿だけに読点を足して区切る。字幕の文は変えない\n- **文のあとの間:** 通常 0.35秒、話題の区切り 0.6〜0.7秒、節の終わり 0.9秒\n\n---\n\n## 4. 画面デザイン\n\n### 4-1. 画面の枠\n\n- 1920×1080、30fps\n- 余白は左右140px。下の250pxは字幕のために空ける\n- 左上に章のタグ(章番号はオレンジ、見出しは薄い文字)、右上にロゴ(高さ34px)\n\n### 4-2. 色(記事のサムネイルと同じ色)\n\n| 用途 | 色 |\n|---|---|\n| 背景 | #F5F5F0 |\n| 文字 | #0D0D0D |\n| 薄い文字(ラベル・出典・補足) | #888888 |\n| 区切り線 | #D6D6D1 |\n| 横帯・棒の「その他」 | #D3D3CE |\n| 箱(フロー図)の背景 | #FFFFFF |\n| オレンジ(強調・主題) | #C4471F |\n| 青(対比のもう一方) | #1A6C98 |\n\n- オレンジは、1画面の中で一番伝えたい語だけに使う\n- 青とオレンジを並べるのは、2つのものを対比するときだけ(試作1本目では「見て判断する=青」「触れて動かす=オレンジ」)\n- 新しい色を足すときは、色覚の違いがあっても見分けられるかを検証してから使う(dataviz スキルの validate_palette.js。背景色 #F5F5F0 を指定する)\n\n### 4-3. 文字\n\nフォントは Noto Sans CJK JP。\n\n| 用途 | 大きさ | 太さ |\n|---|---|---|\n| 大きい数字(ヒーロー数字) | 220px(単位は90px) | 900 |\n| 章番号 | 200px(オレンジ) | 900 |\n| 見出し 大/中/小 | 84px/64px/48px | 900/900/700 |\n| 見出しの上の小見出し | 40px(薄い文字) | 700 |\n| 2列比較のラベル | 34px(薄い文字) | 700 |\n| 出典 | 24px(薄い文字) | 400 |\n\n### 4-4. 図の型\n\n内容の形に合わせて、次の型から選ぶ。\n\n| 内容 | 型 |\n|---|---|\n| 伝えたい数字が1つ | 大きい数字+上に何の数か+下に比較(「前年同期の約4倍」) |\n| 2つのものの対比 | 2列。間を細い縦線(2px)で区切り、色は文字の色だけで分ける |\n| 手順・流れ | 白い箱を矢印でつなぐ |\n| 全体のうちの割合 | 横帯(ラベルを帯の中に書く) |\n| 時期ごとの量 | 横棒(長さは実際の比率) |\n| 章の始まり | 大きい章番号+章の問い |\n| 視聴者への問い | 黒い丸ラベル(「問い 1」)+大きい見出し |\n| 並ぶ項目 | 番号付きの行を細い線で区切る |\n\n### 4-4b. 強弱のつけ方\n\n全部の場面を同じ強さにしない。1本の中で、次の3段階を使い分ける。打ち出しは1本に6〜10場面ほど、残りは説明の図にする。\n\n| 段階 | 使う場面 | 見た目 |\n|---|---|---|\n| 打ち出し | 結論、問いかけ、言い切りたい一言、章の始まり | 小さい前置き(色・44px)の下に、1〜2行の大きな文(110〜150px)。1行は10字前後まで。補足は44pxの薄い文字で1行だけ |\n| 説明 | 数字・比較・手順を見せる場面 | 4-4の図の型(見出し64〜84px) |\n| 補足 | 出典・断り書き | 24〜34pxの薄い文字 |\n\n打ち出しの型(試作1本目で使ったもの):\n\n- **打ち出し文:** 前置き「先に結論」→「見て判断する入口は簡単に/物を動かすのはまだ手間」。対比の語だけ青とオレンジにする\n- **章の扉:** 「今日の話」→「1つ目」→ 大きなキーワード(200px・オレンジ。例「データの壁」)→ 章の問いを1行\n- **点の並び:** 割合は、100個の点のうち該当する数だけ色を付けて見せる(例: 中国メーカー 97%)。「以上」「推計」は下の注に書く\n\n打ち出しにする文の選び方: 字幕をそのまま大きくするのではなく、その場面で一番残したい一言に縮める。字幕と同じ文を大きく出さない。\n\n### 4-5. やらない表現\n\n- カードの上や左に太い色の線を引く(よくある AI 生成スライドの見た目になる)\n- 影、グラデーション\n- 1画面にオレンジと青以外の色を足す\n- 意味や単位の違う数字を同じ棒や帯に並べる\n\n### 4-6. 出典\n\n数字を出す画面には、左下(字幕の上)に出典を24pxの薄い文字で必ず入れる。推計なら「推計」、会社の自己申告なら「会社の発表」と書く。模式図には「図は模式図」と入れる。\n\n---\n\n## 5. 字幕\n\n- 白文字 52px・太さ800。縁取りは #0D0D0D・11px で、角を丸くする(SVG の stroke-linejoin: round。CSS の文字の縁取りは角が尖るので使わない)\n- 読み上げの1文=字幕1枚\n- 2行まで、1行31字以内。改行は読点の位置で入れる。読点がなければ助詞の後ろで割る\n- 位置は画面の下から62px\n\n---\n\n## 6. 実機の映像とテロップ\n\n- 映像は 1920×1080 に拡大して切り抜く。字幕の後ろに黒50%の帯(高さ230px)を敷く\n- 何の映像かを左上に出す(黒78%の地に白文字34px。例「実機:mechArm 270-Pi 赤いブロックを掴んで箱に置く」)\n- 名前のテロップ: 薄い地に、ロゴ(高さ30px)・名前60px・肩書き30px(「デザイナー/ロボティクスの研究・開発」)。左の太い線は付けない\n- 編集ツールの透かし(例: clideo.com)が入った素材は、ぼかしたうえで字幕帯の下に隠す。これから撮る素材は透かしなしで書き出しておく\n- iCloud にしかない素材を取り込むと、Mac にダウンロードされる\n\n---\n\n## 7. 書き出しと保存\n\n- **作業用(YouTube に上げる方):** H.264(crf 20)、音声 AAC 192kbps、音量を -16 LUFS にそろえる\n- **確認用(チャットで送る方):** crf 29、音声 112kbps。30MB以下に収める(チャットで送れる上限)\n- **保存先:** 動画ファイル(mp4)は重く、iCloud に置くと Mac の内蔵 SSD を圧迫するので、外付け SSD(KIOXIA)に置く。軽いファイルは iCloud に置く\n - 動画ファイル(作業用・確認用の mp4): `/Volumes/KIOXIA/r2r2r-data/videos/{動画名}/`\n - 字幕の srt・codex 依頼の md: iCloud `デジタルツイン学習R2R2R/動画/{動画名}/`\n - 制作ベース: iCloud `デジタルツイン学習R2R2R/動画/_制作ベース/`(このファイルの写し、スクリプト一式、ロゴの透過 PNG)\n - 素材の実機映像も、新しく撮るものは KIOXIA に置く。iCloud にしかない素材を取り込むと Mac にダウンロードされて内蔵 SSD を使う\n - MASTER の保存先テーブルと、KIOXIA の README.md のフォルダ構成には、まだ videos/ の行がない。追記はエイイチが行う\n- **作る手順(_制作ベース のスクリプト):**\n 1. `narration.py` に台本を書く(1文ごとに、場面の名前・字幕の文・後ろの間)。読み替え表もここ\n 2. `tts2.py` で音声を作る(間延びの検査と、区切っての作り直しまで自動)\n 3. `scenes.py` に場面ごとの図を書く(4節の型と色)。実機映像は `src/` に置き、`VIDEO` に場面名と開始秒を書く\n 4. `render.py` で図・テロップ・字幕を画像にする\n 5. `assemble.py` で1文ずつ映像を作ってつなぎ、音声と合わせる(字幕の srt も出る)\n 6. 確認用に圧縮する\n\n---\n\n## 8. 未決・改良予定\n\n| 項目 | いまの状態 | 決めること |\n|---|---|---|\n| ナレーションの声 | Open JTalk の仮音声。抑揚が機械的 | 本人の声で録るか、品質の高い音声合成(VOICEVOX など。利用規約とクレジット表記の条件を確認)にするか。srt に1文ごとの時間が入っているので、それに合わせて録れば映像はそのまま使える |\n| 図解の動き | すべて静止画 | 数字のカウントアップ、矢印が伸びる、棒が伸びるなどの動きを入れるか。入れるなら、どの図の型に入れるか |\n| 背景の映像 | 実機映像は自己紹介・節2・エンディングだけ。ほかは無地の背景 | 解説中も実機映像を薄く敷くか、図解だけにするか。使う素材を撮りためるルール |\n| BGM・効果音 | なし | 使うか。使うなら、YouTube で収益化できてクレジット表記の条件を満たす音源から選ぶ。ナレーションが聞き取れる音量に下げる |\n| ジングル | 文言だけ(「Pixels to Motors。こんにちは、エイイチです。」) | 音とロゴの動きを付けるか |\n| サムネイル | まだない | 記事のサムネイルと同じ配色・線画のトーンで作るか |\n| 番組名 | 未定 | 決める |\n| 概要欄のチャプター | まだない | srt の節の開始時刻から作れる |\n\n---\n\n## 更新履歴\n\n- 2026-10-11: 4-4b「強弱のつけ方」を追加(打ち出し・説明・補足の3段階、打ち出し文・章の扉・点の並びの型)。\n- 2026-10-11: 動画ファイルの保存先を iCloud から KIOXIA の videos/ に変更(内蔵 SSD の圧迫を避けるため)。\n- 2026-10-11: 初版。試作1本目の制作で決めた構成・台本・ナレーション・画面・字幕・映像・書き出しのルールをまとめた。台本ルールは codex の敵対的検証で直した点から作った。色は記事のサムネイルに合わせた。</code></pre><h2 id=\"index_lmKEOQqP\">参照した情報</h2><ul><li><p><a href=\"https://www.youtube.com/watch?v=cDEex1_8cOo\" data-has-link=\"true\">YouTube:参考にした解説動画 1</a></p></li><li><p><a href=\"https://www.youtube.com/watch?v=bmmF1tUwnt8\" data-has-link=\"true\">YouTube:参考にした解説動画 2</a></p></li><li><p><a href=\"https://www.youtube.com/watch?v=LI-VAGxwvno\" data-has-link=\"true\">YouTube:参考にした解説動画 3</a></p></li><li><p><a href=\"https://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026\" data-has-link=\"true\">Counterpoint Research:Global Humanoid Robot Shipments Soar Nearly 300% YoY in H1 2026</a></p></li><li><p><a href=\"https://smartanalyticsglobal.com/global-humanoid-robot-shipments-2026-agibot-unitree/\" data-has-link=\"true\">Smart Analytics Global:2026年上半期の人型ロボット出荷</a></p></li><li><p><a href=\"https://www.1x.tech/order\" data-has-link=\"true\">1X:NEO 注文ページ</a></p></li><li><p><a href=\"https://autolab.berkeley.edu/assets/publications/media/GOFE-Can-Close-the-100000-Year-Robot-Data-Gap-Science-Robotics-Aug-2025-scirobotics.aea7390.pdf\" data-has-link=\"true\">Ken Goldberg, Science Robotics(2025年8月):Good Old-Fashioned Engineering Can Close the 100,000-Year \"Data Gap\" in Robotics</a></p></li><li><p><a href=\"https://generalistai.com/blog/gen-1.5\" data-has-link=\"true\">Generalist:GEN-1.5(2026年8月19日)</a></p></li></ul><p></p>", "toc": [{"id": "index_D_tmh6uh", "text": "参考にした3本の解説動画は、冒頭1分半で問い・結論・目次を全部出していた"}, {"id": "index_0LUCW9SF", "text": "台本テンプレートは「解説回」と「実験回」の2種類に分けた"}, {"id": "index_bTgs9pio", "text": "台本に入れた自分の数字は、台帳と公開記事で裏を取った"}, {"id": "index_vjneh5nj", "text": "動画は、Open JTalk・Playwright・ffmpeg で1文ずつ組み立てた"}, {"id": "index_YxOgXNy5", "text": "Open JTalk は、ひと息が長い文の最後を1音0.47秒に引き伸ばしていた"}, {"id": "index_0fVKV772", "text": "codex の敵対的検証で、台本の結論は「成立していない」と判定された"}, {"id": "index_t6LAP1jb", "text": "codex が新しく挙げた事実は、一次情報で確かめてから採用した"}, {"id": "index_YhZ9JBp8", "text": "根拠の弱い数字は足さずに削った"}, {"id": "index_gc6toY6y", "text": "読み上げは「爪先」を「つまさき」と読んでいた"}, {"id": "index_HCnc4FWz", "text": "カード上の太線と、角の立った字幕の縁取りをやめた"}, {"id": "index_mlpaKj8r", "text": "自己紹介から「秋田」「29年」「毎日」を外し、ロゴを差し替えた"}, {"id": "index_9szChvZM", "text": "背景と文字の色を、記事サムネイルの #F5F5F0 と #0D0D0D に合わせた"}, {"id": "index_msVyMuQC", "text": "30cmの食い違いは、アームが外れた距離ではなく、爪先の位置の決め方の違いだった"}, {"id": "index_Apnn9rBf", "text": "打ち出し・説明・補足の3段階で、画面に強弱をつけた"}, {"id": "index_sOZrkg72", "text": "動画ファイルは、iCloud ではなく KIOXIA の videos/ に置いた"}, {"id": "index_52jeog_s", "text": "量産のベースとして video_rules.md と制作スクリプト一式を残した"}, {"id": "index_3J7_uzPW", "text": "まだ決まっていない8項目は、未決のまま残した"}, {"id": "index_sWhPqJ9y", "text": "制作スクリプトの全文"}, {"id": "index_c3VMNBPy", "text": "実行ログ"}, {"id": "index_o0bVwuXE", "text": "codex への検証依頼の全文"}, {"id": "index_tELWWv5H", "text": "codex から返ってきた検証結果の全文"}, {"id": "index_9Y98_SCr", "text": "video_rules.md の全文"}, {"id": "index_lmKEOQqP", "text": "参照した情報"}]}
\ No newline at end of file
diff --git a/src/data/records/youtube-channel-identity.json b/src/data/records/youtube-channel-identity.json
index 46f4a40..12cf8bb 100644
--- a/src/data/records/youtube-channel-identity.json
+++ b/src/data/records/youtube-channel-identity.json
@@ -1 +1 @@
-{"day": "LOG332", "series": "フィジカルAI研究室", "published": "2026-10-12T00:28:45.000Z", "title": "YouTubeチャンネルの顔を作る ― チャンネル名・アイコン・バナーをハコブくんにそろえるまで", "slug": "youtube-channel-identity", "cover": null, "thumb": "/thumbs/r-youtube-channel-identity.html", "lang": "JP", "description": "解説動画を出す前に、YouTubeチャンネルを運用用の新アカウントへ引き継ぎ、説明文・アイコン・バナーをナレーション役のハコブくんにそろえた。バナーは5回作り直し、計18案から決めた。", "updated": "2026-10-12T00:28:42.000Z", "body": "<!--TOC--><p></p><p>Pixels to Motors の YouTube チャンネルで解説動画を出す準備として、チャンネルの見た目と運用の仕組みを Claude と対話しながら整えました。解説動画のナレーションは、病院で薬を運ぶロボットのキャラクター「ハコブくん」が担当すると決めています(LOG331)。そこで、チャンネルを開いた人が最初に目にするアイコンとバナーも、ハコブくんにそろえることにしました。</p><p>やったことは大きく4つです。YouTube を運用するための Google アカウントを新しく作り、既存のチャンネルを引き継ぐ。チャンネルの説明文を書き直す。バナーを作る。アイコンをハコブくんにする。バナーは文字入りの3案から始めて、5回作り直し、計18案を見たうえで決めました。</p><p>本記事は、その手順と、途中で方針が変わった場面の記録です。アカウントの引き継ぎで Claude が誤った案内をした場面も残しています。バナーとアイコンを作った Python スクリプトは、記事の後半に全文を載せています。メールアドレスと Google Workspace の料金の具体的な金額は伏せました。</p><p></p><figure><img src=\"/media/s-2704x1546_v-frms_webp_69baf643-e22a-4e3e-9067-63e9453efb8b.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index__Ynzo3Cp\">YouTube用のアカウントは、Google Workspaceのエイリアスではなく新しいユーザーにした</h2><p>Pixels to Motors のメールは、ADAPT DESIGN の独自ドメインで Google Workspace(Google のメールやドライブを会社のドメインで使うサービス)を契約して運用しています。ここに Pixels to Motors 用のアドレスを足す方法は2つありました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>方法</p></th><th colspan=\"1\" rowspan=\"1\"><p>できること</p></th><th colspan=\"1\" rowspan=\"1\"><p>できないこと</p></th><th colspan=\"1\" rowspan=\"1\"><p>費用</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>エイリアス(既存のユーザーに別名のアドレスを付ける)</p></td><td colspan=\"1\" rowspan=\"1\"><p>メールの受信。設定すれば、そのアドレス名義での送信</p></td><td colspan=\"1\" rowspan=\"1\"><p>そのアドレスで Google にログインすること</p></td><td colspan=\"1\" rowspan=\"1\"><p>かからない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>新しいユーザー</p></td><td colspan=\"1\" rowspan=\"1\"><p>専用のログイン、ドライブ、YouTube チャンネル、外部サービスのアカウントを完全に分けること</p></td><td colspan=\"1\" rowspan=\"1\"><p>―</p></td><td colspan=\"1\" rowspan=\"1\"><p>1ユーザー分の月額が増える</p></td></tr></tbody></table></div><p>YouTube チャンネルを、普段使いのアカウントから切り離して運用したかったので、新しいユーザーにしました。契約が年間プランのため、途中で足したユーザーは更新時期まで減らせない前提で決めています。</p><p></p><h2 id=\"index_8rfkxb9m\">管理コンソールには、普段使いのアカウントでは入れなかった</h2><p>ユーザーを足す画面は、Google Workspace の管理コンソール(admin.google.com)にあります。最初に普段使いのアカウントでログインしたところ、「管理者アカウントでログインしてください」と表示されて入れませんでした。</p><p>ドメインには、普段使いのアカウントともう1つ、合わせて2つのユーザーがありました。管理者の権限を持っていたのは、もう1つのほうでした。そちらで入り直すと、Chrome が「このプロフィールで作業を続けるか、仕事用のプロフィールを新しく作成するか」を聞いてきます。以前、Chrome のプロフィールが10個以上重複して増えたことがあったため、どちらを選ぶかは「そのプロフィールを何に使っているか」で決めました。仕事用のプロフィールに切り替わったあとは、管理コンソールが自動では開き直らなかったため、アドレスを入れて開き直しています。</p><p></p><h2 id=\"index_SZwLHwTg\">「このユーザーは既に存在しています」の原因は、3月に付けたエイリアスだった</h2><p>「新しいユーザーの追加」で、作りたいアドレスを入れると、次の赤字が出て作成できませんでした。</p><blockquote><p>すでにユーザーの予備のメールアドレス(メール エイリアス)として使用されています</p></blockquote><p>2026年3月、同じアドレスをエイリアスとして管理者アカウントに付けていました(当時はユーザーを増やさず、無料のエイリアスで済ませる判断をしていた)。エイリアスとして使っているアドレスと同じ名前のユーザーは作れません。</p><p>管理者アカウントの詳細画面から「メールエイリアス」を開き、そのエイリアスを削除して保存しました。そのあと「新しいユーザーの追加」をやり直すと、ユーザーが作成されました。名前は姓「Pixels to」、名「Motors」で入れています。この入れ方だと、YouTube の画面によっては「Motors Pixels to」と、姓と名が逆の順に表示されることが後で分かりました。</p><p></p><h2 id=\"index_EnRPO5Pq\">作ろうとしたチャンネルのハンドルは、自分の既存チャンネルが使っていた</h2><p>新しいアカウントで YouTube を開き、チャンネルを作ろうとすると、ハンドル(チャンネルの @ から始まる固有の名前)の欄に次のように出ました。</p><blockquote><p>このハンドルは使用できません。 こちらは使用可能です: @PixelstoMotors-r2y</p></blockquote><p>@PixelstoMotors を使っていたのは、2026年3月21日に管理者アカウントで作った、自分の既存チャンネルでした。登録者1人、動画1本(mechArm 270-Pi の開封動画)がすでにあります。</p><p>新しいチャンネルを別に作ると、動画と登録者が既存チャンネルに残ったまま、ハンドルも別のものになります。そこで新しいチャンネルは作らず、既存チャンネルを新しいアカウントから操作できるようにする方針に切り替えました。</p><p></p><h2 id=\"index_04xwKQv5\">既存チャンネルは、新しいアカウントを「管理者」として招待して引き継いだ</h2><p>YouTube Studio の「設定」→「権限」で、新しいアカウントのアドレスを招待しました。選べる役割は次のとおりで、「オーナー」は一覧にありませんでした。YouTube のヘルプにも、オーナーの役割は他のユーザーに移せないと書かれています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>役割</p></th><th colspan=\"1\" rowspan=\"1\"><p>できること</p></th><th colspan=\"1\" rowspan=\"1\"><p>できないこと</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>オーナー</p></td><td colspan=\"1\" rowspan=\"1\"><p>すべての操作。チャンネルの削除、権限の管理</p></td><td colspan=\"1\" rowspan=\"1\"><p>オーナーの役割を他のユーザーに移すこと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>管理者</p></td><td colspan=\"1\" rowspan=\"1\"><p>全データの閲覧、権限の管理、チャンネル情報の編集、動画の作成・アップロード・公開・削除</p></td><td colspan=\"1\" rowspan=\"1\"><p>チャンネルの削除</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>編集者</p></td><td colspan=\"1\" rowspan=\"1\"><p>全データの閲覧、編集、アップロード・公開</p></td><td colspan=\"1\" rowspan=\"1\"><p>公開済みの動画の削除、権限の管理</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>字幕編集者</p></td><td colspan=\"1\" rowspan=\"1\"><p>字幕の追加・編集・削除</p></td><td colspan=\"1\" rowspan=\"1\"><p>字幕以外の編集</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>閲覧者</p></td><td colspan=\"1\" rowspan=\"1\"><p>全データの閲覧</p></td><td colspan=\"1\" rowspan=\"1\"><p>編集</p></td></tr></tbody></table></div><p>新しいアカウントには「管理者」を付けました。動画の投稿から分析、説明文やバナーの変更まで、運用に必要なことはすべてできます。</p><h3 id=\"index_YvFm6X9v\">招待の承諾には、受け取る側にもチャンネルが必要だった</h3><p>招待メールを新しいアカウントで開いて進むと、「チャンネルのプロフィール」という、新しいチャンネルを作る画面が出ました。Claude はこれを「余計な空のチャンネルができてしまう画面」と判断し、キャンセルするよう案内しました。</p><p>キャンセルしたあと Studio の権限の画面を見ると、新しいアカウントは「承認保留中」のままでした。招待メールを開き直すと、手順は次の2段になっていました。</p><blockquote><p>ステップ 1. Pixels to Motors にアクセスするには YouTube チャンネルが必要です。 ステップ 2. チャンネルの作成後、この招待を承諾すると Pixels to Motors にアクセスできるようになります。</p></blockquote><p>キャンセルを勧めた画面は、承諾の途中の「ステップ1」でした。招待を受ける側のアカウントにも自分のチャンネルが必要、という YouTube の仕様を Claude が知らずに案内した誤りです。新しいアカウント側に空のチャンネルを作ってから「招待を承諾」を押すと、承諾済みになりました。空のチャンネルは使わずに置いておきます。</p><p>承諾したあとは、Studio の右上で「チャンネルを切り替え」から Pixels to Motors を選ぶと、「あなたは管理者です」と表示された状態で管理画面に入れました。</p><h3 id=\"index_7woCpmaH\">管理者で入っていても、YouTube本体での名義は空のチャンネルになる</h3><p>管理者の権限で使えるのは、Studio の中の操作です。YouTube 本体の画面(動画の視聴ページなど)に移ると、操作の名義は新しいアカウント自身の空のチャンネルになります。他の人の動画に Pixels to Motors の名前でコメントしたり、チャンネル登録したりはできません。</p><p>これも必要になったら、チャンネルの所有の形そのものを変える必要があります。どう変えるのが正しいかは、今回は確かめていません。</p><p></p><h2 id=\"index_6tAUffjk\">チャンネルの説明文は、フィジカルAIを先頭に置いて書き直した</h2><p>書き直す前の説明文は、次のとおりでした。</p><pre><code>UX × AI × Robotics の交差点に存在する、\nまだ誰も名前をつけていない領域「Motion UX」を探求しています。\n\niPhoneの3Dスキャン → Unityシミュレーション → 実機ロボットアーム\nという低コストなパイプラインを一人で構築中。\n\nすべてのプロセスをオープンに記録しています。\n\n// ADAPT DESIGN — Pixels to Motors\n// adaptdesign.jp/pixelstomotors</code></pre><p>iPhone → Unity → 実機というパイプラインは、いまの開発の中心ではなくなっています。冒頭の2〜3行は、検索結果やチャンネルページで最初に見える部分です。そこで、機材の名前ではなく、何のチャンネルかが伝わる文にしました。方向の違う3案(フィジカルAI・実装寄り/UX・体験寄り/個人・低コスト寄り)から、フィジカルAI寄りの案を選び、機材名の行は削りました。英語の段落は、日本語と同じ内容にそろえています。</p><pre><code>フィジカルAIを、現場で使える形にするまでを記録するチャンネルです。\nAIが考え、ロボットが動く。その間にある設計・調整・失敗を、\n小さなロボットアームで一つずつ確かめています。\n\n精度より、まず体験が成立すること。\n精度はあとからお金で解決できる。\n\nDocumenting the journey of making Physical AI work in real-world settings.\nBetween AI thinking and robots moving lies design, tuning, and failure.\nI test each step with a small robot arm.\n\nExperience first. Precision can be bought later.\n\n// ADAPT DESIGN — Pixels to Motors\n// adaptdesign.jp/pixelstomotors</code></pre><p>Studio の「カスタマイズ」→「プロフィール」→「説明」に貼り付けて公開しました。公開ボタンが押せない状態(灰色)になっていれば保存済みです。ただ、チャンネルページの表示は、記事を書いている時点でもまだ古い説明文のままでした。</p><p></p><h2 id=\"index_GNMYvKmk\">バナーは、どの端末でも切れない中央の範囲を基準に作った</h2><p>YouTube のヘルプでは、バナー画像は次のように決まっています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>値</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>最小サイズ</p></td><td colspan=\"1\" rowspan=\"1\"><p>2048×1152px(16:9)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>テキストやロゴが切れずに表示される範囲</p></td><td colspan=\"1\" rowspan=\"1\"><p>中央の 1235×338px</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>推奨サイズ(テレビでも見やすい)</p></td><td colspan=\"1\" rowspan=\"1\"><p>2560×1440px</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ファイルサイズ</p></td><td colspan=\"1\" rowspan=\"1\"><p>6MB 以下</p></td></tr></tbody></table></div><p>同じ 16:9 の画像が、テレビでは全体、パソコンでは横長の帯、スマホではさらに左右を切った中央だけで表示されます。そのため、主役になる絵は中央の 1235×338px(2560×1440px の画像なら、同じ比率で 1546×423px)に収めました。作った案は、パソコンでの見え方(横長の帯)と、全体図に安全範囲の枠を重ねた図の2つで確認しています。</p><div class=\"embed\"><iframe src=\"/embeds/youtube-channel-identity-MHKfODFx.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p></p><h2 id=\"index_BpOCllXq\">バナーの1回目は、実機の写真に文字を入れた3案だった</h2><p>最初は、手元にある素材から作りました。サイトの画像はこちらの作業環境から取得できなかったため、Mac の r2r2r-content フォルダにあった画像認識シリーズの写真を使っています。</p><ul><li><p>A:mechArm 270-Pi のグリッパーと赤・青のキューブを真上から撮った写真に、「フィジカルAIを、現場で使える形に。」の文字</p></li><li><p>B:アームの根元側をドット状にぼかし、先端に向かってくっきりさせた「Pixels to Motors」の絵</p></li><li><p>C:明るい背景に、キューブの写真・色で切り出したマスク・グリッパーの3コマを並べた「見る → 判断する → 動かす」</p></li></ul><p></p><figure><img src=\"/media/s-1280x636_v-fms_webp_406c9cd8-a337-4559-9ae0-f3c27acf5acd.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>これに対しての返事は次のとおりでした。</p><blockquote><p>テキスト情報は必要なくイメージだけでいいですよ。同じアームだけを使ったバナーではなく、もっといろんなパターンが考えられるはずです。最初はもっと違うパターンを見せて欲しい(今回のは1種類カウントです)</p></blockquote><p>3案とも同じアームの写真が主役で、見せ方の幅が狭かったということです。</p><p></p><h2 id=\"index_KWVQksqt\">2回目は、サイトの画像から6つの方向を出した</h2><p>サイトのトップページを画面ごとPDFに保存したものを受け取り、そこから記事のサムネイル、4コマ漫画、シリーズの扉絵などを切り出しました。文字は入れず、方向がはっきり違う6案にしています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>案</p></th><th colspan=\"1\" rowspan=\"1\"><p>中身</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>アーカイブの壁</p></td><td colspan=\"1\" rowspan=\"1\"><p>記事のサムネイルと4コマを3段に並べる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>白黒スプリット</p></td><td colspan=\"1\" rowspan=\"1\"><p>左が黒、右がクリーム。境目でピクセルが崩れ、斜線の円と点線のグラフを重ねる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>シミュレーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>Unity で並べたたくさんのアームを網点にする</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ハコブくん 4コマ</p></td><td colspan=\"1\" rowspan=\"1\"><p>4コマ漫画を横一列に並べ、両側に集中線</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ドットマトリクス</p></td><td colspan=\"1\" rowspan=\"1\"><p>「フィジカルAIが社会実装される前に解くべき課題」の丸を全面に広げる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ハコブくん 写真</p></td><td colspan=\"1\" rowspan=\"1\"><p>病院の廊下にいるハコブくんの写真</p></td></tr></tbody></table></div><p></p><figure><img src=\"/media/s-1717x1722_v-fms_webp_c6c4e138-f06b-41a1-bfec-053d0dfc7356.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>このあと、YouTube に合わせて全案を 2048×1152px でも書き出しました。</p><p></p><h2 id=\"index_wfU0AzRw\">アイコンは、動画で話しているハコブくんの1コマから作った</h2><p>チャンネル名とアイコンを、YouTube で覚えてもらいやすいものに変えることになりました。アイコンはハコブくんにする前提です。</p><p>最初に Claude が出したのは、ハコブくんの顔(黒いディスプレイ、白いカプセル形の目、水色の口)をコードで描き起こした3案でした。顔のアップ、オレンジの背景に頭と胴、いまのアイコンの市松模様を引き継いだもの、の3つです。</p><p></p><figure><img src=\"/media/s-1500x940_v-fms_webp_c3582154-8fcc-4086-b283-c9ee9792e885.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>これには、試作動画の画面を書き出した MP4 が返ってきました。</p><blockquote><p>アイコンはこのハコブくん使えばいいんじゃないの?</p></blockquote><p>動画で話しているハコブくんと、アイコンのハコブくんが同じ見た目なら、アイコンから動画を開いた人に同じキャラクターだと伝わります。コードで描いた顔は、仕様には合っていても、動画のハコブくんとは別物でした。</p><p>動画(1920×1080px)を1秒に3コマずつ書き出して、表情を見比べ、口を閉じて笑っているコマを選びました。ハコブくんは画面の左下にいて、大きさは横幅が約320pxです。画面の右側にある出典の文字などが入らないよう、ハコブくんの範囲の外は背景色で塗りつぶしてから切り抜いています。案は3つです。</p><ul><li><p>A:動画のまま。上半身をクリーム地に置く</p></li><li><p>B:A にオレンジの輪を付ける。ダークモードでもライトモードでも輪郭が見える</p></li><li><p>C:顔に寄せる</p></li></ul><p></p><figure><img src=\"/media/s-1500x940_v-fms_webp_27d8616b-248f-424b-bbc8-265007ee2c33.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>各案は、YouTube の表示サイズ(200px・98px・48px・24px)で、ダークモードとライトモードの背景に並べて確認しました。チャンネルには A を設定しています。元が動画の1コマなので、800×800px にするために約2倍に拡大しています。</p><p></p><h2 id=\"index_BZlJ8lML\">バナーの3回目は、ハコブくんだけで5つのタッチを作った</h2><p>次の依頼は、ハコブくんだけを使って、タッチの違う5種類を作ることでした。</p><blockquote><p>イラスト系、4コマまんが風、写実的、写真系、グラフィックデザインの5種類</p></blockquote><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>タッチ</p></th><th colspan=\"1\" rowspan=\"1\"><p>作り方</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>イラスト系</p></td><td colspan=\"1\" rowspan=\"1\"><p>病院の廊下とハコブくんを、線のないフラットな絵としてコードで描く</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4コマまんが風</p></td><td colspan=\"1\" rowspan=\"1\"><p>動画のハコブくんを白黒のトーン調に変え、表情の違う4コマを文字なしで並べる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>写実的</p></td><td colspan=\"1\" rowspan=\"1\"><p>動画の3Dのハコブくんを、表情を変えて5体並べる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>写真系</p></td><td colspan=\"1\" rowspan=\"1\"><p>病院の廊下にいるハコブくんの写真</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>グラフィックデザイン</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの顔のディスプレイを大きく置き、ピクセルの崩れ・寸法線・緑のリングを添える</p></td></tr></tbody></table></div><p></p><figure><img src=\"/media/s-1360x1165_v-fms_webp_6153354f-d317-4c3b-9562-e9b084372e61.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>4コマまんが風は、動画のコマを次の手順で白黒にしています。輪郭を検出して黒い線にする。暗い部分(明るさ70未満)は黒で塗る。中間の明るさ(215未満)は、7px 間隔の網点にする。それ以外は白にする。</p><p>返事は次のとおりでした。</p><blockquote><p>イラスト系か4コママンガ風がいいけど、イラストはハコブくんじゃないし、4コママンガ風はハコブくんだけしか描かれてない。</p></blockquote><p>イラスト系のハコブくんは、Claude が写真を見ながら比率を推測して描いたもので、実物と形が違いました。4コマまんが風は、4コマとも同じハコブくんで、漫画に出てくる他の登場人物がいませんでした。</p><h3 id=\"index_elH2fOh3\">ハコブくんの三面図を受け取り、寸法を読み取った</h3><p>ここで、ハコブくんの三面図(正面・側面・背面を漫画のタッチで描いたもの)を受け取りました。</p><p></p><figure><img src=\"/media/s-1080x1350_v-fms_webp_b8890bd5-e649-4af1-9361-84759664f745.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>画像に座標の目盛りを重ねて、正面図の各部の位置を読み取りました(単位は三面図の画像上の px)。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>部分</p></th><th colspan=\"1\" rowspan=\"1\"><p>位置</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>全体</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 70〜331、縦 415〜874(高さ約460)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>頭の外装</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 100〜300、上端 413</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ディスプレイ</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 120〜283、縦 427〜541</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>目</p></td><td colspan=\"1\" rowspan=\"1\"><p>中心が横 152 と 241、縦 486。幅 22、高さ 40</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>口</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 184〜215、縦 490〜505</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>胴</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 76〜324、縦 505〜816</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>引き出しのスリット</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 167〜225、縦 606〜630</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>黒い帯</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 78〜322、縦 767〜786</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>台座</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 72〜328、縦 806〜848</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>車輪</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 95〜120 と 280〜305、縦 836〜874</p></td></tr></tbody></table></div><p>側面図のリングは、中心が(554, 663)、内側の線が半径約51、外側の線が半径約62でした。リングの位置は、中心の候補を動かしながら半径ごとの平均の明るさを測り、暗い線が2本くっきり出る点を探して決めています。</p><h3 id=\"index_nkmV04Yr\">4回目は、三面図そのものと、他の登場人物を使った</h3><p>三面図のハコブくんを使って、イラスト系と4コマまんが風を2案ずつ作りました。</p><ul><li><p>イラスト系 A:方眼の上に正面・背面・側面を並べ、寸法線と、顔とリングの拡大図を添える</p></li><li><p>イラスト系 B:線画に淡い色を付けた廊下を、三面図の側面のハコブくんが走る</p></li><li><p>4コマまんが風 A:斜めのコマ割りで、中央にハコブくん、左右にツカムくん(顔の付いたアーム)、エンジニア、看護師</p></li><li><p>4コマまんが風 B:ハコブくんを中央の爆発形の枠に置き、他の登場人物を丸い枠で並べる</p></li></ul><p></p><figure><img src=\"/media/s-1360x936_v-fms_webp_2febf659-d311-4869-ace6-89ed02c61b14.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>返事は次のとおりでした。</p><blockquote><p>イラストは背景のイラストタッチにハコブくんを修正しないとダメだよ(イラストと漫画風を一緒にしたらNG)</p><p>4コマまんがの処理は前の方が良かった。このタッチのまま、登場するキャラを入れ替えればいいだけです。</p></blockquote><p>イラスト系 B は、背景は線画に色を付けたイラストなのに、ハコブくんだけ漫画のトーンのままでした。1枚の絵の中で、タッチが2種類混ざっていたことになります。4コマまんが風は、コマ割りを変えたことで、3回目で良かった処理(輪郭線と網点)が失われていました。</p><p></p><h2 id=\"index_aoA9mNR9\">5回目で、ハコブくんを背景と同じタッチで描き直した</h2><p>イラスト系は、三面図から読み取った寸法を使い、ハコブくんを背景と同じタッチで描き直しました。三面図の座標をそのまま渡すと、バナーの上の位置と大きさに変換して描く仕組みです(スクリプトの <code>Pen</code> クラス)。線のない背景には線のないハコブくん、黒い輪郭線の背景には同じ太さの輪郭線のハコブくん、と2つのタッチで作りました。</p><p>4コマまんが風は、3回目の処理と並び(4コマを横一列・両側に集中線)に戻し、コマごとの登場人物を入れ替えました。左からハコブくん、看護師(「!」)、エンジニア(汗)、ツカムくん(キラキラ)です。看護師たちは元が線画なので、3回目の処理(輪郭検出)をそのままかけると全体が黒くつぶれました。線画には輪郭検出をかけず、明るさだけで黒・網点・白に分ける処理にしています。網点の間隔は、ハコブくんのコマと同じ 7px にそろえました。</p><p></p><figure><img src=\"/media/s-1360x707_v-fms_webp_3a2d5396-3c1f-4313-b446-6381c5f222fe.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>決まったのは、線のないフラットなイラストでした。</p><blockquote><p>イラスト系、ハコブくんは1体だけでいいよ。タッチはこれがいいかな。</p></blockquote><p>横向きのハコブくんと動きの線を消し、正面のハコブくんだけをドアとドアの間に立たせて完成にしました。ハコブくんは、顔から足元まで、どの端末でも切れない範囲に収まっています。</p><p></p><figure><img src=\"/media/s-2048x1152_v-frms_webp_9fd7c7eb-5d31-479a-bef5-522b73fab189.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>1回目から5回目までに出したバナーは次のとおりです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>回</p></th><th colspan=\"1\" rowspan=\"1\"><p>案の数</p></th><th colspan=\"1\" rowspan=\"1\"><p>主な中身</p></th><th colspan=\"1\" rowspan=\"1\"><p>返ってきた判断</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>実機の写真+文字</p></td><td colspan=\"1\" rowspan=\"1\"><p>文字は要らない。同じアームだけで幅が狭い</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>7</p></td><td colspan=\"1\" rowspan=\"1\"><p>サイトの画像から6方向+1回目の写真(文字なし)</p></td><td colspan=\"1\" rowspan=\"1\"><p>方向の幅を確認</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんだけで5つのタッチ</p></td><td colspan=\"1\" rowspan=\"1\"><p>イラストと4コマが良い。ただし形が違う/他の人物がいない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>三面図と他の登場人物</p></td><td colspan=\"1\" rowspan=\"1\"><p>タッチを混ぜない。4コマは3回目の処理に戻す</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>3+1</p></td><td colspan=\"1\" rowspan=\"1\"><p>タッチをそろえて描き直し</p></td><td colspan=\"1\" rowspan=\"1\"><p>フラットのイラスト、ハコブくん1体で決定</p></td></tr></tbody></table></div><p></p><h2 id=\"index_8PXH6omo\">チャンネル名は「フィジカルAI解説|Pixels to Motors」を候補にした</h2><p>チャンネル名は、ブランド名の Pixels to Motors を残したまま、何のチャンネルかが分かる言葉を足す案にしました。Pixels to Motors のルールで、外に出すブランド名は Pixels to Motors だけと決めているためです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>案</p></th><th colspan=\"1\" rowspan=\"1\"><p>名前</p></th><th colspan=\"1\" rowspan=\"1\"><p>良い点</p></th><th colspan=\"1\" rowspan=\"1\"><p>気になる点</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>Pixels to Motors|フィジカルAI解説</p></td><td colspan=\"1\" rowspan=\"1\"><p>ブランド名が先に来る</p></td><td colspan=\"1\" rowspan=\"1\"><p>少し長い</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィジカルAI解説|Pixels to Motors</p></td><td colspan=\"1\" rowspan=\"1\"><p>日本語が先に来て、内容が一番早く伝わる</p></td><td colspan=\"1\" rowspan=\"1\"><p>幅が狭い画面ではブランド名が切れやすい</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんのフィジカルAI解説</p></td><td colspan=\"1\" rowspan=\"1\"><p>覚えやすく、アイコンと一体になる</p></td><td colspan=\"1\" rowspan=\"1\"><p>ブランド名と、動画を作っている本人が見えなくなる</p></td></tr></tbody></table></div><p></p><figure><img src=\"/media/s-1500x690_v-fms_webp_29ae9ada-591e-4c09-92a0-8b1d5224d689.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>選ばれたのは2の並びでした。そのうえで、次の質問がありました。</p><blockquote><p>ロボティクスとフィジカルAIでどちらがタイトルの言葉として使うのがいい?</p></blockquote><p>Claude の答えは「フィジカルAI」です。理由は3つです。動画を届けたい相手(ロボットに関心のある経営者・PM・マネージャー)が、ニュースや業界の発表で目にしている言葉であること。動画の中身が「AIがロボットを動かす」話で、ロボティクス全般より範囲がはっきりすること。サイトの課題地図や研究室のシリーズ名でも「フィジカルAI」を使っていること。流行りの言葉なので数年後に古く聞こえるおそれはありますが、チャンネル名は後から変えられ、ハンドル(@PixelstoMotors)は変わりません。</p><p>チャンネル名の変更は、記事を書いている時点ではまだ行っていません。</p><p></p><h2 id=\"index_mtwyUJht\">チャンネル名の変更と、アイコン・4コマの作り直しが残っている</h2><ul><li><p>チャンネル名を「フィジカルAI解説|Pixels to Motors」に変える</p></li><li><p>新しい説明文がチャンネルページに反映されたかを確かめる</p></li><li><p>新しいアカウントに2段階認証を設定する</p></li><li><p>アイコンは動画の1コマを約2倍に拡大している。元の切り抜き画像(動画の制作ベースのフォルダにある)から作り直すと、もっと鮮明になる</p></li><li><p>4コマまんが風の看護師たちは、サイトの画面を保存した PDF から切り出した。採用するなら元の漫画画像で作り直す</p></li></ul><p>細かい修正は、動画をアップしながら進める予定です。</p><p></p><h2 id=\"index_LGOmRd2E\">制作スクリプトの全文</h2><p>バナーとアイコンは、すべて Python(Pillow と NumPy)で作りました。作業フォルダは <code>/home/claude/banner/</code> です。スクリプトは作った順に載せています。採用したバナーは make5_single.py、採用したアイコンは hakobu_icon.py の出力です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>ファイル</p></th><th colspan=\"1\" rowspan=\"1\"><p>作ったもの</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>1回目:実機の写真に文字を入れた3案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make2.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>2回目:サイトの画像から作った6案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>icons.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイコン:ハコブくんの顔をコードで描いた3案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>hakobu_icon.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイコン:動画の1コマから作った3案(採用)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make3.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>3回目:ハコブくんだけで5つのタッチ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make4.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>4回目:三面図と他の登場人物を使った4案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make5.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>5回目:タッチをそろえた3案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make5_single.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>決定版:フラットのイラスト、ハコブくん1体(採用)</p></td></tr></tbody></table></div><h3 id=\"index_LAOak7lY\">make.py(1回目の3案)</h3><pre><code class=\"language-python\">import numpy as np\nfrom PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageOps, ImageEnhance\n\nSRC = '/mnt/user-data/uploads/r2r2r-content/articles/'\nOUT = '/home/claude/banner/out/'\nW, H = 2560, 1440\nSAFE = (507, 508, 2053, 932) # x0,y0,x1,y1 (1546 x 423 visible on every device)\n\nCJK_B = '/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc'\nCJK_M = '/usr/share/fonts/opentype/noto/NotoSansCJK-Medium.ttc'\nINTER = '/usr/share/fonts/opentype/inter/'\n\ndef f_cjk(size, bold=True):\n return ImageFont.truetype(CJK_B if bold else CJK_M, size, index=0)\n\ndef f_inter(size, w='SemiBold'):\n return ImageFont.truetype(INTER + f'Inter-{w}.otf', size)\n\ndef tracked(draw, xy, text, font, fill, track=0):\n x, y = xy\n for ch in text:\n draw.text((x, y), ch, font=font, fill=fill)\n x += draw.textlength(ch, font=font) + track\n return x\n\ndef load(name):\n return ImageOps.exif_transpose(Image.open(SRC + name)).convert('RGB')\n\ndef place(canvas, img, scale, src_pt, dst_pt):\n \"\"\"paste img scaled so that src_pt (in img coords) lands on dst_pt\"\"\"\n im = img.resize((round(img.width * scale), round(img.height * scale)), Image.LANCZOS)\n ox = round(dst_pt[0] - src_pt[0] * scale)\n oy = round(dst_pt[1] - src_pt[1] * scale)\n layer = Image.new('RGB', (W, H))\n layer.paste(im, (ox, oy))\n return layer, (ox, oy, im.width, im.height)\n\ndef rect_mask(box, feather):\n m = Image.new('L', (W, H), 0)\n ImageDraw.Draw(m).rectangle(box, fill=255)\n return m.filter(ImageFilter.GaussianBlur(feather))\n\ndef hgrad(x0, x1, a0, a1):\n \"\"\"horizontal alpha ramp, a0 at x<=x0 to a1 at x>=x1\"\"\"\n xs = np.clip((np.arange(W) - x0) / max(1, x1 - x0), 0, 1)\n row = (a0 + (a1 - a0) * xs).astype(np.uint8)\n return Image.fromarray(np.tile(row, (H, 1)))\n\ndef pixel_squares(draw, x, y, s, gap, cols):\n for i, c in enumerate(cols):\n draw.rectangle((x + i * (s + gap), y, x + i * (s + gap) + s, y + s), fill=c)\n\nRED, BLUE = (214, 52, 52), (36, 128, 214)\nMAT = (30, 27, 31)\n\nphoto = load('IMG_7933.jpg') # top-down: mechArm on cutting mat, red & blue cubes\nMAT_BOX = (697, 639, 3211, 2471) # cutting-mat area in full-res coords\n\n# ---------------------------------------------------------------- A: GRASP\ndef banner_a():\n bg = Image.new('RGB', (W, H), MAT)\n subj = ((1282 + 2650) / 2, 1790) # midpoint between the two cubes, gripper level\n layer, _ = place(bg, photo, 0.55, subj, (1530, 728))\n # keep only the mat, fade its edges into the background\n s = 0.55\n ox = 1530 - subj[0] * s; oy = 728 - subj[1] * s\n mb = (ox + MAT_BOX[0] * s + 70, oy + MAT_BOX[1] * s + 70,\n ox + MAT_BOX[2] * s - 70, oy + MAT_BOX[3] * s - 70)\n m = rect_mask(mb, 60)\n layer = ImageEnhance.Brightness(layer).enhance(0.92)\n bg.paste(layer, (0, 0), m)\n # darken left for the type\n shade = Image.new('RGB', (W, H), MAT)\n bg.paste(shade, (0, 0), hgrad(900, 1080, 235, 0))\n d = ImageDraw.Draw(bg)\n x = SAFE[0] + 20\n pixel_squares(d, x, 586, 18, 8, [RED, BLUE, (235, 235, 235)])\n tracked(d, (x + 92, 580), 'PIXELS TO MOTORS', f_inter(28, 'SemiBold'), (190, 186, 194), track=6)\n d.text((x, 640), 'フィジカルAIを、', font=f_cjk(68), fill=(245, 245, 245))\n d.text((x, 728), '現場で使える形に。', font=f_cjk(68), fill=(245, 245, 245))\n d.text((x, 836), 'Making Physical AI work in the real world.', font=f_inter(26, 'Regular'), fill=(160, 156, 166))\n return bg\n\n# ---------------------------------------------------------------- B: PIXELS → MOTORS (dissolve)\ndef banner_b():\n rot = photo.rotate(90, expand=True) # CCW: robot base on the left, gripper reaching right\n # mapping for CCW 90 with expand: (x, y) -> (y, W0 - x)\n W0 = photo.width\n def r(pt): return (pt[1], W0 - pt[0])\n base = r((2010, 700)); tips = r((2010, 2045))\n # arm axis: y = W0 - 2010 ; x from 700 to 2045\n s = 0.50\n layer, (ox, oy, iw, ih) = place(None, rot, s, (tips[0], tips[1]), (2030, 720))\n # mat region after rotation\n mb0 = (MAT_BOX[1], W0 - MAT_BOX[2], MAT_BOX[3], W0 - MAT_BOX[0])\n mb = (ox + mb0[0] * s + 70, oy + mb0[1] * s + 70, ox + mb0[2] * s - 70, oy + mb0[3] * s - 70)\n bg = Image.new('RGB', (W, H), MAT)\n bg.paste(layer, (0, 0), rect_mask(mb, 50))\n # progressive pixelation, left -> right, in vertical strips\n arr = np.array(bg)\n out = arr.copy()\n sizes = [96, 84, 72, 64, 56, 48, 40, 34, 28, 22, 16, 11, 7, 4]\n x_start, x_end = 0, 1900\n sw = (x_end - x_start) / len(sizes)\n for i, b in enumerate(sizes):\n xa, xb = int(x_start + i * sw), int(x_start + (i + 1) * sw)\n small = bg.resize((max(1, W // b), max(1, H // b)), Image.BOX).resize(\n (W // b * b, H // b * b), Image.NEAREST)\n sm = np.zeros_like(arr); sa = np.array(small)\n sm[:sa.shape[0], :sa.shape[1]] = sa\n out[:, xa:xb] = sm[:, xa:xb]\n bg = Image.fromarray(out)\n shade = Image.new('RGB', (W, H), MAT)\n bg.paste(shade, (0, 0), hgrad(760, 1060, 205, 0))\n d = ImageDraw.Draw(bg)\n # detection-style bracket around the gripper (perception -> action)\n gx, gy, hw, hh = 1932, 720, 108, 118\n c = (120, 200, 255); L = 34; t = 4\n for sx in (-1, 1):\n for sy in (-1, 1):\n px, py = gx + sx * hw, gy + sy * hh\n d.line((px, py, px - sx * L, py), fill=c, width=t)\n d.line((px, py, px, py - sy * L), fill=c, width=t)\n tracked(d, (gx - hw, gy - hh - 44), 'GRASP', f_inter(24, 'SemiBold'), c, track=5)\n x = SAFE[0] + 20\n d.text((x, 572), 'Pixels to Motors', font=ImageFont.truetype(INTER + 'InterDisplay-Bold.otf', 88), fill=(248, 248, 248))\n d.text((x, 696), 'フィジカルAIを、現場で使える形にするまで。', font=f_cjk(38, bold=False), fill=(225, 222, 228))\n tracked(d, (x, 786), 'PIXELS → PERCEPTION → MOTION', f_inter(26, 'Medium'), (150, 146, 156), track=4)\n return bg\n\n# ---------------------------------------------------------------- C: SEE / DECIDE / MOVE (light)\ndef banner_c():\n NAVY = (26, 35, 50); BG = (244, 246, 248); GRID = (226, 231, 236)\n bg = Image.new('RGB', (W, H), BG)\n d = ImageDraw.Draw(bg)\n for gx in range(0, W, 40):\n d.line((gx, 0, gx, H), fill=GRID if gx % 200 else (214, 221, 228), width=1)\n for gy in range(0, H, 40):\n d.line((0, gy, W, gy), fill=GRID if gy % 200 else (214, 221, 228), width=1)\n # panel sources\n cubes = load('IMG_7912.JPG') # red & blue cubes, white paper\n crop = (1500, 700, 3500, 2700)\n p1 = cubes.crop(crop)\n hsv = np.array(p1.convert('HSV')).astype(int)\n rgb = np.array(p1).astype(int)\n sat = hsv[..., 1] > 110\n red = sat & ((hsv[..., 0] < 12) | (hsv[..., 0] > 235)) & (rgb[..., 0] > rgb[..., 2])\n blue = sat & (hsv[..., 0] > 120) & (hsv[..., 0] < 170)\n mask = np.zeros(rgb.shape, np.uint8)\n mask[red] = (255, 90, 90); mask[blue] = (90, 170, 255)\n p2 = Image.fromarray(mask).filter(ImageFilter.MedianFilter(7))\n # bounding boxes on mask\n d2 = ImageDraw.Draw(p2)\n for m, col in ((red, (255, 90, 90)), (blue, (90, 170, 255))):\n ys, xs = np.where(m)\n if len(xs):\n x0, x1 = np.percentile(xs, [1, 99]); y0, y1 = np.percentile(ys, [1, 99])\n d2.rectangle((x0 - 30, y0 - 30, x1 + 30, y1 + 30), outline=col, width=14)\n p3 = photo.crop((1100, 1250, 2900, 2250)) # gripper between the cubes\n P = 290; gap = 44\n x0 = 1062; y0 = 540\n panels = [(p1, '01', '見る', 'SEE'), (p2, '02', '判断する', 'DECIDE'), (p3, '03', '動かす', 'MOVE')]\n for i, (im, n, jp, en) in enumerate(panels):\n px = x0 + i * (P + gap)\n tile = ImageOps.fit(im, (P, P), Image.LANCZOS)\n shadow = Image.new('L', (W, H), 0)\n ImageDraw.Draw(shadow).rectangle((px + 6, y0 + 10, px + P + 6, y0 + P + 10), fill=70)\n bg.paste((180, 190, 200), (0, 0), shadow.filter(ImageFilter.GaussianBlur(12)))\n bg.paste(tile, (px, y0))\n d = ImageDraw.Draw(bg)\n d.text((px, y0 + P + 20), n, font=f_inter(24, 'SemiBold'), fill=(138, 155, 168))\n d.text((px + 44, y0 + P + 14), jp, font=f_cjk(30), fill=NAVY)\n tw = d.textlength(en, font=f_inter(22, 'Medium'))\n tracked(d, (px + P - tw - 12, y0 + P + 22), en, f_inter(22, 'Medium'), (138, 155, 168), track=3)\n if i < 2:\n ax = px + P + gap // 2; ay = y0 + P // 2\n d.polygon([(ax - 9, ay - 12), (ax + 9, ay), (ax - 9, ay + 12)], fill=NAVY)\n d = ImageDraw.Draw(bg)\n x = SAFE[0] + 20\n pixel_squares(d, x, 598, 16, 7, [NAVY, RED, BLUE])\n tracked(d, (x + 82, 592), 'PIXELS TO MOTORS', f_inter(26, 'SemiBold'), (90, 105, 120), track=6)\n d.text((x, 646), 'フィジカルAIを、', font=f_cjk(58), fill=NAVY)\n d.text((x, 722), '現場で使える形に。', font=f_cjk(58), fill=NAVY)\n d.text((x, 818), 'Experience first.', font=f_inter(30, 'Medium'), fill=(184, 90, 90))\n d.text((x, 856), 'Precision can be bought later.', font=f_inter(30, 'Regular'), fill=(110, 125, 140))\n return bg\n\ndef preview(img, name):\n \"\"\"safe-area overlay + desktop/mobile crops for checking\"\"\"\n ov = img.copy(); d = ImageDraw.Draw(ov, 'RGBA')\n d.rectangle((0, 0, W, SAFE[1]), fill=(0, 0, 0, 110)); d.rectangle((0, SAFE[3], W, H), fill=(0, 0, 0, 110))\n d.rectangle((0, SAFE[1], SAFE[0], SAFE[3]), fill=(0, 0, 0, 60)); d.rectangle((SAFE[2], SAFE[1], W, SAFE[3]), fill=(0, 0, 0, 60))\n d.rectangle(SAFE, outline=(255, 220, 0, 255), width=4)\n ov.resize((1280, 720)).save(OUT + f'check_{name}_overlay.jpg', quality=88)\n img.crop((0, SAFE[1], W, SAFE[3])).resize((1280, 212)).save(OUT + f'check_{name}_desktop.jpg', quality=88)\n img.crop(SAFE).save(OUT + f'check_{name}_mobile.jpg', quality=90)\n\nimport os\nos.makedirs(OUT, exist_ok=True)\nfor name, fn in (('A_grasp', banner_a), ('B_pixels_to_motors', banner_b), ('C_see_decide_move', banner_c)):\n im = fn()\n im.save(OUT + f'pixelstomotors_banner_{name}.jpg', quality=92, subsampling=0)\n preview(im, name)\n print(name, os.path.getsize(OUT + f'pixelstomotors_banner_{name}.jpg') // 1024, 'KB')</code></pre><h3 id=\"index_MFH88eN7\">make2.py(2回目の6案)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps\n\nS = '/home/claude/banner/site/'\nOUT = '/home/claude/banner/out2/'\nos.makedirs(OUT, exist_ok=True)\nW, H = 2560, 1440\nSAFE = (507, 508, 2053, 932)\nCY = 720\nINK = (17, 17, 17); CREAM = (242, 241, 236); GRAY = (152, 152, 148)\n\ndef L(name): return Image.open(S + f'c_{name}.png').convert('RGB')\n\ndef rounded(im, r):\n m = Image.new('L', im.size, 0)\n ImageDraw.Draw(m).rounded_rectangle((0, 0, im.width - 1, im.height - 1), r, fill=255)\n return m\n\ndef card(canvas, im, box, r=14, shadow=True):\n x0, y0, x1, y1 = box\n tile = ImageOps.fit(im, (x1 - x0, y1 - y0), Image.LANCZOS)\n if shadow:\n sh = Image.new('L', (W, H), 0)\n ImageDraw.Draw(sh).rounded_rectangle((x0 + 4, y0 + 12, x1 + 4, y1 + 12), r, fill=60)\n canvas.paste((150, 150, 150), (0, 0), sh.filter(ImageFilter.GaussianBlur(16)))\n canvas.paste(tile, (x0, y0), rounded(tile, r))\n\n# ------------------------------------------------ 1. Archive wall\ndef archive():\n c = Image.new('RGB', (W, H), (236, 236, 233))\n rows = [\n ['inside', 'panels320', 'gpu', 'panels324', 'film', 'rec_say'],\n ['manga2', 'lab', 'manga3', 'sim', 'manga0', 'drug', 'manga1'],\n ['who03', 'panels319', 'bowl', 'panels323', 'imgrec_top', 'rec_micro'],\n ]\n hs = [300, 390, 300]\n ys = [CY - 195 - 36 - 300, CY - 195, CY + 195 + 36]\n offs = [-170, -60, -250]\n for row, h, y, off in zip(rows, hs, ys, offs):\n x = off\n while x < W:\n for n in row:\n im = L(n)\n w = int(h * im.width / im.height)\n if n.startswith('manga'): w = int(h * 1.05)\n card(c, im, (x, y, x + w, y + h))\n x += w + 36\n if x >= W: break\n return c\n\n# ------------------------------------------------ 2. Mono split (vector)\ndef split():\n c = Image.new('RGB', (W, H), CREAM)\n d = ImageDraw.Draw(c)\n mid = 1240\n d.rectangle((0, 0, mid, H), fill=INK)\n # pixel dissolve across the seam: black squares scattered into cream, cream squares into black\n random.seed(7); cell = 44\n for gx in range(mid - cell * 10, mid + cell * 14, cell):\n for gy in range(0, H, cell):\n t = (gx - mid) / (cell * 12)\n if gx >= mid and random.random() < max(0, 0.9 - t):\n d.rectangle((gx + 3, gy + 3, gx + cell - 3, gy + cell - 3), fill=INK)\n if gx < mid and random.random() < max(0, 0.75 + t * 1.4):\n d.rectangle((gx + 3, gy + 3, gx + cell - 3, gy + cell - 3), fill=CREAM)\n # hatched block (top-left of band) and dot grids\n hx0, hy0, hx1, hy1 = 560, 540, 760, 740\n hl = Image.new('RGB', (W, H), INK); hd = ImageDraw.Draw(hl)\n for k in range(-220, 240, 16):\n hd.line((hx0 + k, hy1, hx0 + k + 200, hy0), fill=CREAM, width=4)\n hm = Image.new('L', (W, H), 0); ImageDraw.Draw(hm).rectangle((hx0, hy0, hx1, hy1), fill=255)\n c.paste(hl, (0, 0), hm)\n for gx in range(1800, 2040, 22):\n for gy in range(540, 700, 22):\n d.ellipse((gx - 3, gy - 3, gx + 3, gy + 3), fill=INK)\n for gx in range(560, 760, 22):\n for gy in range(800, 900, 22):\n d.ellipse((gx - 3, gy - 3, gx + 3, gy + 3), fill=(120, 120, 120))\n # half-hatched circle (right)\n cx, cy, r = 1700, 735, 175\n circ = Image.new('L', (W, H), 0); ImageDraw.Draw(circ).ellipse((cx - r, cy - r, cx + r, cy + r), fill=255)\n hatch = Image.new('RGB', (W, H), CREAM); hd = ImageDraw.Draw(hatch)\n for k in range(-400, 400, 18):\n hd.line((cx + k - 200, cy + 200, cx + k + 200, cy - 200), fill=INK, width=5)\n half = Image.new('L', (W, H), 0); ImageDraw.Draw(half).rectangle((cx, 0, W, H), fill=255)\n m = Image.fromarray((np.array(circ) & np.array(half)).astype(np.uint8))\n c.paste(hatch, (0, 0), m)\n d.ellipse((cx - r, cy - r, cx + r, cy + r), outline=INK, width=6)\n for a in (0, 90, 180, 270):\n px = cx + r * math.cos(math.radians(a)); py = cy + r * math.sin(math.radians(a))\n d.ellipse((px - 12, py - 12, px + 12, py + 12), fill=INK)\n # dotted signal line running from black side into cream (noisy -> settled)\n random.seed(3)\n pts = []\n for i, x in enumerate(range(520, 2060, 60)):\n amp = 90 * max(0, 1 - (x - 520) / 1200)\n pts.append((x, 870 - random.uniform(-1, 1) * amp))\n for (x0, y0), (x1, y1) in zip(pts, pts[1:]):\n steps = int(math.hypot(x1 - x0, y1 - y0) // 14)\n for s in range(steps):\n t = s / steps; px = x0 + (x1 - x0) * t; py = y0 + (y1 - y0) * t\n col = CREAM if px < mid else INK\n d.ellipse((px - 2.5, py - 2.5, px + 2.5, py + 2.5), fill=col)\n for x, y in pts:\n col = CREAM if x < mid else INK\n d.ellipse((x - 9, y - 9, x + 9, y + 9), fill=INK if x >= mid else INK, outline=col, width=4)\n return c\n\n# ------------------------------------------------ 3. Simulation halftone\ndef halftone():\n src = ImageOps.autocontrast(L('sim').convert('L'), cutoff=1)\n sc = W / src.width; bh = int(src.height * sc)\n big = src.resize((W, bh), Image.BICUBIC)\n top = int(0.30 * bh) - SAFE[1] - 340\n full = Image.new('L', (W, H), 0)\n full.paste(big, (0, -top))\n a = np.array(full).astype(float) / 255\n c = Image.new('RGB', (W, H), CREAM); d = ImageDraw.Draw(c)\n step = 10\n for y in range(0, H, step):\n for x in range((y // step % 2) * step // 2, W, step):\n v = a[y, min(W - 1, x)]\n r = v ** 1.6 * step * 0.66\n if r > 0.6:\n d.ellipse((x - r, y - r, x + r, y + r), fill=INK)\n return c\n\n# ------------------------------------------------ 4. Manga strip\ndef manga():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n # screentone background (fading dots towards the centre)\n for y in range(0, H, 16):\n for x in range(0, W, 16):\n dist = abs(x - W / 2) / (W / 2)\n r = 3.2 * max(0, dist - 0.35) / 0.65\n if r > 0.4:\n d.ellipse((x - r, y - r, x + r, y + r), fill=(200, 200, 198))\n order = ['manga3', 'manga0', 'manga2', 'manga1']\n ph = 372; pw = 366; gap = 22\n total = 4 * pw + 3 * gap\n x = (W - total) // 2; y = CY - ph // 2\n for n in order:\n im = L(n)\n tile = ImageOps.fit(im, (pw, ph), Image.LANCZOS, centering=(0.5, 0.6))\n c.paste(tile, (x, y))\n d.rectangle((x - 5, y - 5, x + pw + 4, y + ph + 4), outline=INK, width=6)\n x += pw + gap\n # speed lines on both sides\n for k in range(26):\n yy = y + 10 + k * (ph - 20) / 25\n ln = 120 + (k * 37 % 5) * 60\n d.line((x + 30, yy, x + 30 + ln, yy), fill=INK, width=3)\n x0 = (W - total) // 2 - 40\n d.line((x0 - ln, yy, x0, yy), fill=INK, width=3)\n return c\n\n# ------------------------------------------------ 5. Dot matrix\ndef dots():\n c = Image.new('RGB', (W, H), CREAM); d = ImageDraw.Draw(c)\n D = 82; G = 24; P = D + G\n random.seed(36)\n cols = W // P + 2; rows = H // P + 4\n ox = (W - cols * P) // 2 + G // 2; oy = SAFE[1] + G // 2 + 2 - P * 6\n for j in range(rows):\n for i in range(cols):\n x = ox + i * P; y = oy + j * P\n t = i / cols\n r = random.random()\n # left: mostly solved (black), middle: in progress (gray), right: open (outline)\n pb = max(0, 0.75 - t * 1.3); pg = 0.55 - abs(t - 0.5) * 0.6\n if r < pb: d.ellipse((x, y, x + D, y + D), fill=INK)\n elif r < pb + pg: d.ellipse((x, y, x + D, y + D), fill=GRAY)\n else: d.ellipse((x + 2, y + 2, x + D - 2, y + D - 2), outline=INK, width=4)\n return c\n\n# ------------------------------------------------ 6. Hakobu photo\ndef hakobu():\n src = L('drug')\n sw = int(src.width * 0.30)\n left = src.crop((0, 0, sw, src.height)); right = src.crop((src.width - sw, 0, src.width, src.height))\n pano = Image.new('RGB', (sw * 2, src.height)); pano.paste(left, (0, 0)); pano.paste(right, (sw, 0))\n bgim = pano.resize((W, H), Image.BICUBIC).filter(ImageFilter.GaussianBlur(30))\n bgim = Image.blend(bgim, Image.new('RGB', (W, H), (238, 236, 232)), 0.25)\n # sharp copy: robot fits inside the band\n h = 470; s = h / src.height; w = int(src.width * s)\n sm = src.resize((w, h), Image.LANCZOS)\n x = 1500 - w // 2; y = CY - h // 2 + 12\n m = Image.new('L', (w, h), 0)\n ImageDraw.Draw(m).rectangle((60, 45, w - 60, h - 45), fill=255)\n m = m.filter(ImageFilter.GaussianBlur(45))\n bgim.paste(sm, (x, y), m)\n return bgim\n\ndef preview(img, name):\n img.crop((0, SAFE[1], W, SAFE[3])).save(OUT + f'_band_{name}.png')\n\nitems = [('B_archive_wall', archive), ('C_mono_split', split), ('D_sim_halftone', halftone),\n ('E_hakobu_manga', manga), ('F_dot_matrix', dots), ('G_hakobu_photo', hakobu)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'pixelstomotors_banner_{name}.jpg', quality=92, subsampling=0)\n preview(im, name)\n print(name, os.path.getsize(OUT + f'pixelstomotors_banner_{name}.jpg') // 1024, 'KB')</code></pre><h3 id=\"index__zJBabGH\">icons.py(コードで描いたアイコン3案)</h3><pre><code class=\"language-python\">import os, random\nfrom PIL import Image, ImageDraw, ImageFilter\n\nOUT = '/home/claude/banner/icons/'\nos.makedirs(OUT, exist_ok=True)\nSS = 4; N = 800 * SS\nSHELL = (246, 245, 241); SHELL_EDGE = (214, 214, 208)\nDISP = (16, 16, 18); EYE = (246, 248, 252); MOUTH = (87, 181, 248)\nCREAM = (245, 245, 240); ORANGE = (196, 71, 31); INK = (13, 13, 13)\n\ndef face(img, box):\n \"\"\"draw display + eyes + smile inside box (x0,y0,x1,y1)\"\"\"\n x0, y0, x1, y1 = box; w = x1 - x0; h = y1 - y0\n d = ImageDraw.Draw(img)\n d.rounded_rectangle(box, radius=int(w * 0.16), fill=DISP)\n ew, eh = w * 0.13, h * 0.36\n cy = y0 + h * 0.42\n # glow\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gd = ImageDraw.Draw(glow)\n for cx in (x0 + w * 0.31, x0 + w * 0.69):\n gd.rounded_rectangle((cx - ew * 0.75, cy - eh * 0.62, cx + ew * 0.75, cy + eh * 0.62), radius=int(ew * 0.75), fill=(246, 248, 252, 90))\n glow = glow.filter(ImageFilter.GaussianBlur(w * 0.03))\n img.alpha_composite(glow) if img.mode == 'RGBA' else img.paste(glow, (0, 0), glow)\n d = ImageDraw.Draw(img)\n for cx in (x0 + w * 0.31, x0 + w * 0.69):\n d.rounded_rectangle((cx - ew / 2, cy - eh / 2, cx + ew / 2, cy + eh / 2), radius=int(ew / 2), fill=EYE)\n # smile: thick arc with round caps\n mw = w * 0.24; my = y0 + h * 0.70; t = int(h * 0.075)\n d.arc((x0 + w / 2 - mw / 2, my - mw * 0.55, x0 + w / 2 + mw / 2, my + mw * 0.30), 25, 155, fill=MOUTH, width=t)\n import math\n for a in (25, 155):\n rx = mw / 2 - t / 2; ry = (mw * 0.85) / 2 - t / 2\n cx = x0 + w / 2 + rx * math.cos(math.radians(a)); cyy = (my - mw * 0.55 + my + mw * 0.30) / 2 + ry * math.sin(math.radians(a))\n d.ellipse((cx - t / 2, cyy - t / 2, cx + t / 2, cyy + t / 2), fill=MOUTH)\n\ndef head(img, cx, cy, w, h):\n d = ImageDraw.Draw(img)\n d.rounded_rectangle((cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2), radius=int(w * 0.22), fill=SHELL, outline=SHELL_EDGE, width=int(w * 0.012))\n m = w * 0.075\n face(img, (cx - w / 2 + m, cy - h / 2 + m, cx + w / 2 - m, cy + h / 2 - m * 1.1))\n\ndef circle_out(img, name):\n im = img.convert('RGB').resize((800, 800), Image.LANCZOS)\n im.save(OUT + f'{name}.png')\n m = Image.new('L', (800, 800), 0); ImageDraw.Draw(m).ellipse((0, 0, 799, 799), fill=255)\n c = Image.new('RGBA', (800, 800), (0, 0, 0, 0)); c.paste(im, (0, 0), m)\n c.save(OUT + f'{name}_circle.png')\n return c\n\n# 1. close-up face (shell fills the circle)\ndef i1():\n img = Image.new('RGBA', (N, N), CREAM + (255,))\n head(img, N / 2, N * 0.53, N * 0.98, N * 0.80)\n return circle_out(img, 'icon1_face')\n\n# 2. orange ground, head + top of body\ndef i2():\n img = Image.new('RGBA', (N, N), ORANGE + (255,))\n d = ImageDraw.Draw(img)\n bw = N * 0.66\n d.rounded_rectangle((N / 2 - bw / 2, N * 0.70, N / 2 + bw / 2, N * 1.2), radius=int(bw * 0.14), fill=SHELL, outline=SHELL_EDGE, width=SS * 3)\n d.rounded_rectangle((N / 2 - bw * 0.12, N * 0.80, N / 2 + bw * 0.12, N * 0.84), radius=SS * 8, fill=(40, 40, 40))\n head(img, N / 2, N * 0.43, N * 0.70, N * 0.50)\n return circle_out(img, 'icon2_orange')\n\n# 3. pixel dissolve (continuity with the current checker logo)\ndef i3():\n img = Image.new('RGBA', (N, N), INK + (255,))\n face(img, (N * 0.16, N * 0.22, N * 0.92, N * 0.80))\n d = ImageDraw.Draw(img)\n random.seed(4); cell = N // 11\n for gx in range(0, N // 2, cell):\n for gy in range(0, N, cell):\n p = 0.85 - gx / (N * 0.42)\n if random.random() < p:\n d.rectangle((gx, gy, gx + cell - SS * 6, gy + cell - SS * 6), fill=(245, 245, 240))\n return circle_out(img, 'icon3_pixel')\n\nicons = [i1(), i2(), i3()]\n\n# size check sheet on YouTube dark + light backgrounds\nfrom PIL import ImageFont\nfnt = ImageFont.truetype('/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc', 22, index=0)\nsheet = Image.new('RGB', (1500, 3 * 300 + 40), (255, 255, 255)); sd = ImageDraw.Draw(sheet)\nlabels = ['案1 顔のアップ', '案2 オレンジ地', '案3 ピクセル継承']\nfor r, (ic, lab) in enumerate(zip(icons, labels)):\n y = 20 + r * 300\n sd.text((20, y), lab, font=fnt, fill=(20, 20, 20))\n for bgc, x0 in (((15, 15, 15), 20), ((255, 255, 255), 760)):\n sd.rectangle((x0, y + 36, x0 + 720, y + 280), fill=bgc)\n x = x0 + 20\n for s in (200, 98, 48, 24):\n sheet.paste(ic.resize((s, s), Image.LANCZOS), (x, y + 58 + (200 - s) // 2), ic.resize((s, s), Image.LANCZOS))\n x += s + 40\nsheet.save('/home/claude/banner/icons_sizes.jpg', quality=90)</code></pre><h3 id=\"index_ttHgfGc2\">hakobu_icon.py(動画の1コマから作ったアイコン3案)</h3><pre><code class=\"language-python\">from PIL import Image, ImageDraw, ImageFilter, ImageFont\nfr=Image.open('fr_0.png').convert('RGB'); W0,H0=fr.size\nBG=(245,245,240); OR=(196,71,31)\nclean=Image.new('RGB',(W0,H0),BG); clean.paste(fr.crop((0,758,360,H0)),(0,758))\ndef grab(box):\n x0,y0,x1,y1=box; c=Image.new('RGB',(x1-x0,y1-y0),BG)\n c.paste(clean.crop((max(0,x0),max(0,y0),min(W0,x1),min(H0,y1))),(max(0,-x0),max(0,-y0))); return c\ndef up(c,s=800): return c.resize((s,s),Image.LANCZOS).filter(ImageFilter.UnsharpMask(2,60,2))\ndef circ(im):\n m=Image.new('L',im.size,0);ImageDraw.Draw(m).ellipse((0,0,im.width-1,im.height-1),fill=255)\n o=Image.new('RGBA',im.size,(0,0,0,0));o.paste(im,(0,0),m);return o\nA=up(grab((30,660,450,1080)))\nC=up(grab((90,690,390,990)))\n# B: orange ring around the cream picture (no cut-out edges)\ninner=up(grab((50,690,430,1070)),640)\nB=Image.new('RGB',(800,800),OR); m=Image.new('L',(640,640),0); ImageDraw.Draw(m).ellipse((0,0,639,639),fill=255)\nB.paste(inner,(80,80),m)\nout={'A_upper':A,'B_orange_ring':B,'C_face':C}\nimport glob,os\nfor p in glob.glob('../icons/hakobu_icon_*'): os.remove(p)\nfor p in glob.glob('../icons/zoomB.png'): os.remove(p)\nfor k,v in out.items():\n v.save(f'../icons/hakobu_icon_{k}.png'); circ(v).save(f'../icons/hakobu_icon_{k}_circle.png')\nf=ImageFont.truetype('/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc',22,index=0)\nlabels={'A_upper':'案A 動画のまま(上半身・クリーム地)','B_orange_ring':'案B オレンジの縁取り','C_face':'案C 顔に寄せる'}\nsh=Image.new('RGB',(1500,3*300+40),(255,255,255));d=ImageDraw.Draw(sh)\nfor r,k in enumerate(out):\n y=20+r*300; d.text((20,y),labels[k],font=f,fill=(20,20,20)); ic=circ(out[k])\n for bgc,x0 in (((15,15,15),20),((255,255,255),760)):\n d.rectangle((x0,y+36,x0+720,y+280),fill=bgc);x=x0+20\n for s in (200,98,48,24):\n t=ic.resize((s,s),Image.LANCZOS); sh.paste(t,(x,y+58+(200-s)//2),t); x+=s+40\nsh.save('../hakobu_icons_sizes.jpg',quality=90)</code></pre><h3 id=\"index_ox6KpGZu\">make3.py(3回目の5つのタッチ)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps, ImageChops\n\nOUT = '/home/claude/banner/hakobu/'\nos.makedirs(OUT, exist_ok=True)\nW, H = 2048, 1152\nBAND = (0, 407, 2048, 745) # visible on desktop\nSAFE = (406, 407, 1642, 745) # visible everywhere\nCREAM = (245, 245, 240); INK = (13, 13, 13); ORANGE = (196, 71, 31); BLUE = (26, 108, 152)\nSHELL = (247, 246, 242); SHELL2 = (226, 223, 215); DISP = (20, 20, 22)\nEYE = (246, 248, 252); MOUTH = (87, 181, 248); RING = (120, 226, 160)\nFR = '/home/claude/banner/vid/fx/'\nSS = 3 # supersampling for vector work\n\ndef frame(i):\n \"\"\"video frame with everything except Hakobu painted over with the background\"\"\"\n f = Image.open(FR + f'e_{i}.png').convert('RGB')\n c = Image.new('RGB', f.size, CREAM)\n c.paste(f.crop((0, 758, 362, 1080)), (0, 758))\n return c\n\n# ---------------------------------------------------------------- vector Hakobu\ndef face(d, box, expr='smile', glow_layer=None):\n x0, y0, x1, y1 = box; w = x1 - x0; h = y1 - y0\n d.rounded_rectangle(box, radius=int(min(w, h) * 0.16), fill=DISP)\n ew, eh = w * 0.11, h * 0.36; cy = y0 + h * 0.43\n for cx in (x0 + w * 0.32, x0 + w * 0.68):\n if expr == 'blink':\n d.rounded_rectangle((cx - ew * 0.8, cy - ew * 0.18, cx + ew * 0.8, cy + ew * 0.18), radius=int(ew * 0.18), fill=EYE)\n elif expr == 'happy':\n d.arc((cx - ew * 0.9, cy - ew * 0.3, cx + ew * 0.9, cy + ew * 1.5), 200, 340, fill=EYE, width=int(ew * 0.35))\n else:\n if glow_layer is not None:\n ImageDraw.Draw(glow_layer).rounded_rectangle((cx - ew * 0.9, cy - eh * 0.62, cx + ew * 0.9, cy + eh * 0.62), radius=int(ew * 0.9), fill=(246, 248, 252, 110))\n d.rounded_rectangle((cx - ew / 2, cy - eh / 2, cx + ew / 2, cy + eh / 2), radius=int(ew / 2), fill=EYE)\n mw = w * 0.22; my = y0 + h * 0.74; t = max(2, int(h * 0.07))\n if expr in ('open', 'happy'):\n d.chord((x0 + w / 2 - mw / 2, my - mw * 0.45, x0 + w / 2 + mw / 2, my + mw * 0.45), 0, 180, fill=MOUTH)\n elif expr == 'o':\n r = mw * 0.18; d.ellipse((x0 + w / 2 - r, my - r * 0.6, x0 + w / 2 + r, my + r * 1.2), fill=MOUTH)\n else:\n d.arc((x0 + w / 2 - mw / 2, my - mw * 0.55, x0 + w / 2 + mw / 2, my + mw * 0.30), 25, 155, fill=MOUTH, width=t)\n\ndef hakobu(img, cx, base, h, expr='smile', side=True):\n \"\"\"flat front-ish Hakobu, bottom of wheels at `base`, total height h\"\"\"\n d = ImageDraw.Draw(img)\n hw = h * 0.31; bw = h * 0.33; sd = h * 0.07 if side else 0\n # body\n by0 = base - h * 0.70; by1 = base - h * 0.08\n if side:\n d.rounded_rectangle((cx + bw - sd * 2, by0, cx + bw + sd, by1), radius=int(h * 0.05), fill=SHELL2)\n d.ellipse((cx + bw - sd * 0.2, by0 + (by1 - by0) * 0.28, cx + bw + sd * 0.75, by0 + (by1 - by0) * 0.62), outline=RING, width=max(2, int(h * 0.012)))\n d.rounded_rectangle((cx - bw, by0, cx + bw, by1), radius=int(h * 0.06), fill=SHELL)\n d.line((cx - bw + h * 0.01, by0 + h * 0.08, cx + bw - h * 0.01, by0 + h * 0.08), fill=SHELL2, width=max(1, int(h * 0.006)))\n d.rounded_rectangle((cx - h * 0.06, by0 + h * 0.20, cx + h * 0.06, by0 + h * 0.225), radius=int(h * 0.012), fill=(40, 40, 42))\n # base\n d.rounded_rectangle((cx - bw * 1.02, by1 - h * 0.01, cx + bw * 1.02 + sd * 0.5, base - h * 0.02), radius=int(h * 0.02), fill=(58, 58, 60))\n d.rounded_rectangle((cx - bw * 0.9, base - h * 0.03, cx + bw * 0.9, base), radius=int(h * 0.012), fill=(30, 30, 32))\n # head\n hy0 = base - h; hy1 = base - h * 0.70 + h * 0.015\n if side:\n d.rounded_rectangle((cx + hw - sd * 2, hy0 + h * 0.01, cx + hw + sd, hy1), radius=int(h * 0.05), fill=SHELL2)\n d.rounded_rectangle((cx - hw, hy0, cx + hw, hy1), radius=int(h * 0.07), fill=SHELL)\n m = h * 0.03\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0))\n face(d, (cx - hw + m, hy0 + m, cx + hw - m, hy1 - m * 1.1), expr, glow)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(h * 0.012)))\n\ndef ss_canvas(color):\n return Image.new('RGBA', (W * SS, H * SS), color + (255,))\n\ndef finish(img):\n return img.convert('RGB').resize((W, H), Image.LANCZOS)\n\n# ---------------------------------------------------------------- 1. illustration\ndef illustration():\n S = SS; img = ss_canvas((228, 238, 235)); d = ImageDraw.Draw(img)\n floor_y = 700 * S\n d.rectangle((0, floor_y, W * S, H * S), fill=(214, 207, 194))\n d.rectangle((0, floor_y - 6 * S, W * S, floor_y), fill=(196, 208, 204)) # skirting\n d.rectangle((0, 560 * S, W * S, 568 * S), fill=(180, 196, 192)) # handrail\n for x in range(0, W, 520): # doors\n dx = (x + 140) * S\n d.rounded_rectangle((dx, 300 * S, dx + 190 * S, floor_y - 6 * S), radius=10 * S, fill=(201, 219, 214))\n d.rounded_rectangle((dx + 55 * S, 360 * S, dx + 135 * S, 470 * S), radius=8 * S, fill=(237, 245, 243))\n d.rectangle((dx + 150 * S, 520 * S, dx + 170 * S, 530 * S), fill=(150, 168, 164))\n # room number plate (blank)\n d.rounded_rectangle((dx + 60 * S, 270 * S, dx + 130 * S, 290 * S), radius=4 * S, fill=(196, 71, 31))\n for x in (400, 1460): # plants\n px = x * S\n d.rounded_rectangle((px, 640 * S, px + 60 * S, floor_y), radius=6 * S, fill=(196, 160, 120))\n for a, r in ((-30, 46), (0, 60), (30, 46), (-60, 36), (60, 36)):\n ex = px + 30 * S + math.sin(math.radians(a)) * r * S; ey = 640 * S - math.cos(math.radians(a)) * r * S\n d.ellipse((ex - 22 * S, ey - 34 * S, ex + 22 * S, ey + 34 * S), fill=(110, 170, 130))\n # floor reflection lines\n for x in range(0, W, 90):\n d.line(((x + 30) * S, (floor_y + 40 * S), (x + 70) * S, (floor_y + 40 * S)), fill=(226, 220, 208), width=4 * S)\n # Hakobu, carrying a medicine tray on its head\n cx, base, h = 1080 * S, 736 * S, 300 * S\n hakobu(img, cx, base, h, 'smile')\n d = ImageDraw.Draw(img)\n # motion lines behind\n for k, yy in enumerate((520, 580, 640, 690)):\n d.rounded_rectangle(((880 - k * 25) * S, yy * S, (960 - k * 10) * S, (yy + 8) * S), radius=4 * S, fill=(190, 206, 202))\n return finish(img)\n\n# ---------------------------------------------------------------- 2. 4-koma manga style (wordless)\ndef to_manga(im):\n g = ImageOps.grayscale(im)\n g = ImageOps.autocontrast(g, cutoff=1)\n e = g.filter(ImageFilter.FIND_EDGES).point(lambda v: 255 if v > 28 else 0).filter(ImageFilter.MaxFilter(3))\n a = np.array(g).astype(int)\n out = np.full(a.shape, 255, np.uint8)\n yy, xx = np.mgrid[0:a.shape[0], 0:a.shape[1]]\n dots = ((xx % 7 - 3) ** 2 + (yy % 7 - 3) ** 2) <= 3\n out[(a < 215) & dots] = 0\n out[a < 70] = 0\n out[np.array(e) > 0] = 0\n return Image.fromarray(out).convert('RGB')\n\ndef manga():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 12):\n for x in range(0, W, 12):\n dist = abs(x - W / 2) / (W / 2); r = 2.6 * max(0, dist - 0.4) / 0.6\n if r > 0.4: d.ellipse((x - r, y - r, x + r, y + r), fill=(205, 205, 203))\n seq = [(1, 'smile'), (8, 'surprise'), (15, 'blink'), (9, 'joy')]\n pw, ph, gap = 290, 300, 18\n x = (W - (4 * pw + 3 * gap)) // 2; y = (BAND[1] + BAND[3]) // 2 - ph // 2\n for i, (fi, fx) in enumerate(seq):\n crop = frame(fi).crop((0, 730, 380, 1080))\n if i % 2 == 1: crop = ImageOps.mirror(crop)\n panel = ImageOps.fit(to_manga(crop), (pw, ph), Image.LANCZOS, centering=(0.5, 0.15))\n pd = ImageDraw.Draw(panel)\n if fx == 'surprise': # focus lines + exclamation mark\n for a in range(0, 360, 9):\n r0, r1 = 150, 260; ca, sa = math.cos(math.radians(a)), math.sin(math.radians(a))\n pd.line((pw / 2 + ca * r0, ph * 0.35 + sa * r0, pw / 2 + ca * r1, ph * 0.35 + sa * r1), fill=INK, width=2)\n pd.polygon([(250, 20), (268, 20), (263, 70), (255, 70)], fill=INK); pd.ellipse((252, 78, 266, 92), fill=INK)\n if fx == 'blink': # sweat drop\n pd.polygon([(60, 40), (48, 70), (72, 70)], fill=(255, 255, 255), outline=INK)\n pd.ellipse((46, 58, 74, 86), fill=(255, 255, 255), outline=INK, width=2)\n if fx == 'joy': # sparkles\n for sx, sy, s in ((40, 40, 18), (255, 60, 14), (60, 120, 10)):\n pd.polygon([(sx, sy - s), (sx + s * 0.25, sy - s * 0.25), (sx + s, sy), (sx + s * 0.25, sy + s * 0.25),\n (sx, sy + s), (sx - s * 0.25, sy + s * 0.25), (sx - s, sy), (sx - s * 0.25, sy - s * 0.25)], fill=INK)\n c.paste(panel, (x, y))\n d.rectangle((x - 4, y - 4, x + pw + 3, y + ph + 3), outline=INK, width=5)\n x += pw + gap\n x0 = (W - (4 * pw + 3 * gap)) // 2\n for k in range(24):\n yy = y + 10 + k * (ph - 20) / 23; ln = 90 + (k * 37 % 5) * 45\n d.line((x + 20, yy, x + 20 + ln, yy), fill=INK, width=3)\n d.line((x0 - 40 - ln, yy, x0 - 40, yy), fill=INK, width=3)\n return c\n\n# ---------------------------------------------------------------- 3. realistic (3D render line-up)\ndef realistic():\n c = Image.new('RGB', (W, H), CREAM)\n order = [15, 8, 1, 9, 4]\n step = 390; x = W // 2 - 2 * step - 190\n fade = Image.new('L', (380, 350), 255); fd = ImageDraw.Draw(fade)\n for k in range(90):\n fd.line((0, 349 - k, 380, 349 - k), fill=int(255 * k / 90))\n for i, fi in enumerate(order):\n crop = frame(fi).crop((0, 730, 380, 1080))\n if i < 2: crop = ImageOps.mirror(crop) # left side looks toward the centre\n c.paste(crop, (x, 412), fade)\n x += step\n # soft floor line\n d = ImageDraw.Draw(c, 'RGBA')\n return c\n\n# ---------------------------------------------------------------- 4. photo (hospital corridor)\ndef photo():\n src = Image.open('/home/claude/banner/site/c_drug.png').convert('RGB')\n sw = int(src.width * 0.30)\n pano = Image.new('RGB', (sw * 2, src.height))\n pano.paste(src.crop((0, 0, sw, src.height)), (0, 0)); pano.paste(src.crop((src.width - sw, 0, src.width, src.height)), (sw, 0))\n bg = pano.resize((W, H), Image.BICUBIC).filter(ImageFilter.GaussianBlur(24))\n bg = Image.blend(bg, Image.new('RGB', (W, H), (238, 236, 232)), 0.2)\n h = 400; s = h / src.height; w = int(src.width * s)\n sm = src.resize((w, h), Image.LANCZOS)\n m = Image.new('L', (w, h), 0); ImageDraw.Draw(m).rectangle((50, 35, w - 50, h - 35), fill=255)\n bg.paste(sm, (W // 2 - w // 2 + 160, 390), m.filter(ImageFilter.GaussianBlur(38)))\n return bg\n\n# ---------------------------------------------------------------- 5. graphic design (face as geometry)\ndef graphic():\n S = SS; img = ss_canvas(CREAM); d = ImageDraw.Draw(img)\n for gx in range(0, W, 32):\n d.line((gx * S, 0, gx * S, H * S), fill=(232, 232, 226), width=S)\n for gy in range(0, H, 32):\n d.line((0, gy * S, W * S, gy * S), fill=(232, 232, 226), width=S)\n # huge display, face fills the band\n box = (712 * S, 352 * S, 1336 * S, 792 * S)\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0))\n face(d, box, 'smile', glow)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(14 * S)))\n d = ImageDraw.Draw(img)\n # pixel squares leaving the display (left) – echo of the brand mark\n random.seed(5); cell = 34\n for gx in range(520, 712, cell):\n for gy in range(424, 740, cell):\n if random.random() < (gx - 520) / 200:\n d.rectangle((gx * S, gy * S, (gx + cell - 6) * S, (gy + cell - 6) * S), fill=INK)\n # orange dimension lines (right) and blue dot (ring) accent\n ox = 1400\n d.line((ox * S, 430 * S, ox * S, 730 * S), fill=ORANGE, width=4 * S)\n for yy in (430, 730):\n d.line(((ox - 18) * S, yy * S, (ox + 18) * S, yy * S), fill=ORANGE, width=4 * S)\n d.polygon([(ox * S, 430 * S), ((ox - 10) * S, 452 * S), ((ox + 10) * S, 452 * S)], fill=ORANGE)\n d.polygon([(ox * S, 730 * S), ((ox - 10) * S, 708 * S), ((ox + 10) * S, 708 * S)], fill=ORANGE)\n d.ellipse((1460 * S, 520 * S, 1580 * S, 640 * S), outline=RING, width=10 * S)\n for gx in range(1640, 1900, 22):\n for gy in range(440, 720, 22):\n d.ellipse(((gx - 3) * S, (gy - 3) * S, (gx + 3) * S, (gy + 3) * S), fill=INK)\n return finish(img)\n\nitems = [('1_illustration', illustration), ('2_manga', manga), ('3_realistic', realistic),\n ('4_photo', photo), ('5_graphic', graphic)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'hakobu_banner_{name}_2048x1152.jpg', quality=92, subsampling=0)\n im.crop(BAND).save(OUT + f'_band_{name}.png')\n print(name)</code></pre><h3 id=\"index_TtkMtlZh\">make4.py(4回目の4案)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps\n\nOUT = '/home/claude/banner/hakobu2/'\nos.makedirs(OUT, exist_ok=True)\nW, H = 2048, 1152\nBAND = (0, 407, 2048, 745)\nCREAM = (245, 245, 240); INK = (13, 13, 13); ORANGE = (196, 71, 31)\nMOUTH = (87, 181, 248); RING = (140, 226, 172)\nSITE = '/home/claude/banner/site/'\n\n# ------------------------------------------------------------ three-view sheet\nsrc = Image.open('/home/claude/banner/sheet/src.png').convert('RGB')\ng = np.array(src.convert('L')).astype(int)\nclean = np.array(src).astype(int)\nclean[g > 212] = 255 # remove faint paper pattern\n# colour accents: mouth blue, side ring green\nyy, xx = np.mgrid[0:g.shape[0], 0:g.shape[1]]\nmouth = (xx >= 184) & (xx <= 215) & (yy >= 490) & (yy <= 505) & (g > 70) & (g < 200)\nclean[mouth] = MOUTH\nr = np.hypot(xx - 554, yy - 663)\nring = (r >= 50.5) & (r <= 62.5) & (g > 95)\nclean[ring] = RING\nsheet = Image.fromarray(clean.astype(np.uint8))\nVIEWS = {'front': (64, 409, 338, 881), 'side': (390, 413, 705, 881), 'back': (743, 414, 1019, 882)}\n\ndef view(name):\n \"\"\"RGBA cut-out of one view (white outside the outline becomes transparent)\"\"\"\n box = VIEWS[name]\n im = sheet.crop(box)\n gg = np.array(im.convert('L'))\n barrier = Image.fromarray(np.where(gg < 200, 0, 255).astype(np.uint8)).filter(ImageFilter.MinFilter(5))\n ff = barrier.copy()\n for p in ((0, 0), (ff.width - 1, 0), (0, ff.height - 1), (ff.width - 1, ff.height - 1)):\n if ff.getpixel(p) == 255:\n ImageDraw.floodfill(ff, p, 128)\n m = np.where(np.array(ff) == 128, 0, 255).astype(np.uint8)\n mask = Image.fromarray(m).filter(ImageFilter.MinFilter(3)).filter(ImageFilter.GaussianBlur(0.7))\n out = im.convert('RGBA'); out.putalpha(mask)\n return out\n\ndef scaled(im, h):\n return im.resize((int(im.width * h / im.height), h), Image.LANCZOS)\n\ndef manga_crop(n, box):\n return Image.open(SITE + f'c_manga{n}.png').convert('RGB').crop(box)\n\ndef grid(c, step=32, major=160):\n d = ImageDraw.Draw(c)\n for x in range(0, W, step):\n d.line((x, 0, x, H), fill=(214, 214, 208) if x % major == 0 else (230, 230, 224), width=1)\n for y in range(0, H, step):\n d.line((0, y, W, y), fill=(214, 214, 208) if y % major == 0 else (230, 230, 224), width=1)\n\ndef arrow_dim(d, x, y0, y1, col=ORANGE, w=3):\n d.line((x, y0, x, y1), fill=col, width=w)\n for yy_, s in ((y0, 1), (y1, -1)):\n d.line((x - 12, yy_, x + 12, yy_), fill=col, width=w)\n d.polygon([(x, yy_), (x - 7, yy_ + 16 * s), (x + 7, yy_ + 16 * s)], fill=col)\n\ndef hdim(d, x0, x1, y, col=ORANGE, w=3):\n d.line((x0, y, x1, y), fill=col, width=w)\n for xx_, s in ((x0, 1), (x1, -1)):\n d.line((xx_, y - 12, xx_, y + 12), fill=col, width=w)\n d.polygon([(xx_, y), (xx_ + 16 * s, y - 7), (xx_ + 16 * s, y + 7)], fill=col)\n\ndef dashdot(d, x, y0, y1, col=(170, 170, 165)):\n y = y0\n while y < y1:\n d.line((x, y, x, min(y1, y + 22)), fill=col, width=2); y += 30\n d.line((x, y, x, min(y1, y + 3)), fill=col, width=2); y += 10\n\ndef circle_img(c, im, cx, cy, r, border=5):\n tile = ImageOps.fit(im, (2 * r, 2 * r), Image.LANCZOS)\n m = Image.new('L', tile.size, 0); ImageDraw.Draw(m).ellipse((0, 0, 2 * r - 1, 2 * r - 1), fill=255)\n c.paste(tile, (cx - r, cy - r), m)\n ImageDraw.Draw(c).ellipse((cx - r, cy - r, cx + r, cy + r), outline=INK, width=border)\n\n# ============================================================ I-a three-view design sheet\ndef illust_threeview():\n c = Image.new('RGB', (W, H), CREAM); grid(c); d = ImageDraw.Draw(c)\n h = 300; ground = 728\n xs = {'front': 760, 'back': 1060, 'side': 1360}\n for n, cx in xs.items():\n v = scaled(view(n), h)\n c.paste(v, (cx - v.width // 2, ground - h), v)\n dashdot(d, cx, ground - h - 6, ground + 6)\n d.line((560, ground, 1520, ground), fill=INK, width=3)\n fv = scaled(view('front'), h)\n arrow_dim(d, xs['front'] - fv.width // 2 - 40, ground - h, ground)\n sv = scaled(view('side'), h)\n hdim(d, xs['side'] - sv.width // 2, xs['side'] + sv.width // 2, ground - h - 2 + 0, ORANGE)\n # detail circles: face (left) and side ring (right)\n face = sheet.crop((100, 425, 300, 560))\n circle_img(c, face, 520, 576, 118)\n d.line((520 + 118, 576, xs['front'] - 40, 500), fill=ORANGE, width=3)\n ringc = sheet.crop((474, 583, 634, 743))\n circle_img(c, ringc, 1580, 576, 118)\n d.line((1580 - 118, 576, 1388, 610), fill=ORANGE, width=3)\n # dots at the far edges\n for gx in range(60, 330, 22):\n for gy in range(460, 700, 22):\n d.ellipse((gx - 3, gy - 3, gx + 3, gy + 3), fill=(120, 120, 116))\n for gx in range(1740, 2000, 22):\n for gy in range(460, 700, 22):\n d.ellipse((gx - 3, gy - 3, gx + 3, gy + 3), fill=(120, 120, 116))\n return c\n\n# ============================================================ I-b corridor illustration (line + flat colour)\ndef illust_corridor():\n c = Image.new('RGB', (W, H), (230, 239, 236)); d = ImageDraw.Draw(c)\n floor = 712\n d.rectangle((0, floor, W, H), fill=(234, 228, 216)); d.line((0, floor, W, floor), fill=INK, width=4)\n d.line((0, 590, W, 590), fill=INK, width=3); d.line((0, 600, W, 600), fill=INK, width=3)\n for i, x in enumerate(range(-80, W, 470)):\n d.rectangle((x, 330, x + 200, floor), fill=(206, 224, 219), outline=INK, width=4)\n d.rectangle((x + 60, 380, x + 140, 480), fill=(246, 250, 249), outline=INK, width=3)\n d.rounded_rectangle((x + 160, 540, x + 184, 556), radius=4, fill=INK)\n d.rectangle((x + 70, 300, x + 130, 318), fill=ORANGE, outline=INK, width=3)\n for x in (300, 1530):\n d.rounded_rectangle((x, 640, x + 64, floor), radius=6, fill=(214, 172, 128), outline=INK, width=3)\n for a, rr in ((-35, 50), (0, 66), (35, 50)):\n ex = x + 32 + math.sin(math.radians(a)) * rr; ey = 640 - math.cos(math.radians(a)) * rr\n d.ellipse((ex - 24, ey - 38, ex + 24, ey + 38), fill=(132, 190, 150), outline=INK, width=3)\n for x in range(40, W, 110):\n d.line((x, floor + 30, x + 50, floor + 30), fill=(210, 202, 188), width=4)\n # Hakobu (side view, moving left) + motion lines behind\n v = scaled(view('side'), 296)\n hx = 1000; c.paste(v, (hx - v.width // 2, floor + 10 - v.height), v)\n d = ImageDraw.Draw(c)\n for k, yy_ in enumerate((470, 530, 600, 660)):\n x0 = hx + v.width // 2 + 30 + (k % 2) * 30\n d.line((x0, yy_, x0 + 120 - k * 15, yy_), fill=INK, width=4)\n # a second Hakobu waiting at the far door (front view, small)\n f = scaled(view('front'), 170)\n c.paste(f, (1340, floor + 4 - f.height), f)\n return c\n\n# ============================================================ M-a manga page, slanted panels\ndef manga_page():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 12):\n for x in range(0, W, 12):\n d.ellipse((x - 1.6, y - 1.6, x + 1.6, y + 1.6), fill=(212, 212, 210))\n top, bot, sl = 418, 734, 36\n cuts = [-40, 392, 790, 1258, 1656, 2090]\n imgs = [manga_crop(3, (0, 40, 561, 470)), manga_crop(0, (0, 60, 561, 536)), None,\n manga_crop(1, (250, 0, 561, 400)), manga_crop(2, (100, 0, 561, 400))]\n for i in range(5):\n xl, xr = cuts[i] + 8, cuts[i + 1] - 8\n poly = [(xl + sl, top), (xr + sl, top), (xr - sl, bot), (xl - sl, bot)]\n bw, bh = xr - xl + 2 * sl, bot - top\n if imgs[i] is None: # centre: Hakobu front with focus lines\n tile = Image.new('RGB', (bw, bh), (255, 255, 255)); td = ImageDraw.Draw(tile)\n cx0, cy0 = bw / 2, bh * 0.45\n for a in range(0, 360, 5):\n ca, sa = math.cos(math.radians(a)), math.sin(math.radians(a))\n r0 = 150 + (a * 7 % 40)\n td.line((cx0 + ca * r0, cy0 + sa * r0, cx0 + ca * 600, cy0 + sa * 600), fill=INK, width=2)\n v = scaled(view('front'), bh - 26)\n tile.paste(v, (int(cx0 - v.width / 2), bh - v.height - 4), v)\n else:\n tile = ImageOps.fit(imgs[i], (bw, bh), Image.LANCZOS, centering=(0.5, 0.4))\n m = Image.new('L', (W, H), 0); ImageDraw.Draw(m).polygon(poly, fill=255)\n layer = Image.new('RGB', (W, H)); layer.paste(tile, (xl - sl, top))\n c.paste(layer, (0, 0), m)\n ImageDraw.Draw(c).polygon(poly, outline=INK, width=5)\n return c\n\n# ============================================================ M-b cast introduction\ndef manga_cast():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 11):\n for x in range(0, W, 11):\n rr = 2.2 if (x // 11 + y // 11) % 2 == 0 else 1.2\n d.ellipse((x - rr, y - rr, x + rr, y + rr), fill=(218, 218, 216))\n cy = 578\n # starburst behind Hakobu\n pts = []\n for k in range(36):\n a = math.radians(k * 10); rr = 180 if k % 2 == 0 else 128\n pts.append((1024 + math.cos(a) * rr * 0.95, cy + math.sin(a) * rr))\n d.polygon(pts, fill=(255, 255, 255), outline=INK, width=4)\n v = scaled(view('front'), 316)\n c.paste(v, (1024 - v.width // 2, 736 - v.height), v)\n # cast circles\n cast = [(520, manga_crop(1, (300, 0, 540, 240))), # nurse\n (770, manga_crop(0, (360, 40, 561, 241))), # engineer\n (1278, manga_crop(2, (160, 20, 380, 240))), # arm robot with a face\n (1528, manga_crop(3, (40, 20, 561, 541)))] # the twin arms\n for x, im in cast:\n circle_img(c, im, x, cy, 112, 5)\n d = ImageDraw.Draw(c)\n # speed lines at both ends\n for k in range(22):\n yy_ = 440 + k * 13; ln = 80 + (k * 37 % 5) * 40\n d.line((1680, yy_, 1680 + ln, yy_), fill=INK, width=3)\n d.line((368 - ln, yy_, 368, yy_), fill=INK, width=3)\n return c\n\nitems = [('I1_threeview', illust_threeview), ('I2_corridor', illust_corridor),\n ('M1_manga_page', manga_page), ('M2_cast', manga_cast)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'hakobu_banner_{name}_2048x1152.jpg', quality=92, subsampling=0)\n im.crop(BAND).save(OUT + f'_band_{name}.png')\n print(name)</code></pre><h3 id=\"index_fHfez7_y\">make5.py(5回目の3案)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps\n\n_src = open('/home/claude/banner/make3.py').read()\nexec(_src[:_src.index(\"items = [\")]) # reuse frame(), to_manga(), constants\n\nOUT = '/home/claude/banner/hakobu3/'\nos.makedirs(OUT, exist_ok=True)\nSITE = '/home/claude/banner/site/'\nDARK = (40, 40, 42); BASE = (205, 201, 192); BASE2 = (184, 180, 171)\nSHELL = (247, 246, 242); SHELL2 = (226, 223, 215); SHELL_L = (251, 250, 247); SEAM = (214, 211, 203)\n\n# ------------------------------------------------------------ Hakobu redrawn from the three-view sheet\nclass Pen:\n \"\"\"maps three-view sheet coordinates to the canvas\"\"\"\n def __init__(self, img, x0, y0, ox, oy, s, line=None, lw=0):\n self.d = ImageDraw.Draw(img); self.x0, self.y0, self.ox, self.oy, self.s = x0, y0, ox, oy, s\n self.line, self.lw = line, lw\n def p(self, x, y): return (self.ox + (x - self.x0) * self.s, self.oy + (y - self.y0) * self.s)\n def box(self, b): a = self.p(b[0], b[1]); c = self.p(b[2], b[3]); return (a[0], a[1], c[0], c[1])\n def rr(self, b, r, fill, outline=True):\n self.d.rounded_rectangle(self.box(b), radius=r * self.s, fill=fill,\n outline=self.line if (outline and self.line) else None, width=int(self.lw))\n def poly(self, pts, fill, outline=True):\n self.d.polygon([self.p(*q) for q in pts], fill=fill, outline=self.line if (outline and self.line) else None,\n width=int(self.lw) if self.line else 1)\n def ell(self, b, fill, outline=True):\n self.d.ellipse(self.box(b), fill=fill, outline=self.line if (outline and self.line) else None, width=int(self.lw))\n def ln(self, a, b, fill, w):\n self.d.line((self.p(*a), self.p(*b)), fill=fill, width=max(1, int(w * self.s)))\n\ndef hakobu_front(img, cx, ground, h, line=None, lw=0, expr='smile'):\n s = h / 461.0\n pen = Pen(img, 200, 874, cx, ground, s, line, lw)\n seam = line or SEAM\n pen.rr((95, 836, 120, 874), 6, DARK); pen.rr((280, 836, 305, 874), 6, DARK) # wheels\n pen.rr((72, 806, 328, 848), 12, BASE) # base\n pen.rr((76, 505, 324, 816), 26, SHELL) # body\n pen.rr((78, 525, 98, 800), 8, SHELL2, False); pen.rr((302, 525, 322, 800), 8, SHELL2, False)\n pen.rr((100, 413, 300, 566), 34, SHELL) # head\n pen.rr((112, 546, 288, 566), 10, SHELL2, False)\n pen.rr((120, 427, 283, 541), 12, DISP) # display\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gp = Pen(glow, 200, 874, cx, ground, s)\n for ex in (152, 241):\n gp.rr((ex - 17, 460, ex + 17, 512), 17, (246, 248, 252, 110), False)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(4 * s)))\n pen.d = ImageDraw.Draw(img)\n for ex in (152, 241):\n if expr == 'blink':\n pen.rr((ex - 13, 484, ex + 13, 490), 3, EYE, False)\n else:\n pen.rr((ex - 11, 466, ex + 11, 506), 11, EYE, False)\n pen.d.chord(pen.box((191, 484, 217, 510)), 0, 180, fill=MOUTH) # mouth\n pen.ln((80, 588), (320, 588), seam, 2.2)\n pen.rr((167, 606, 225, 630), 11, DARK, False) # drawer slot\n pen.d.arc(pen.box((78, 668, 322, 730)), 15, 165, fill=seam, width=max(1, int(2.2 * s)))\n pen.rr((78, 767, 322, 786), 6, DARK, False) # black stripe\n pen.rr((177, 796, 224, 810), 7, DARK, False)\n\ndef hakobu_side(img, cx, ground, h, line=None, lw=0):\n \"\"\"side view, front of the robot on the left\"\"\"\n s = h / 461.0\n pen = Pen(img, 548, 874, cx, ground, s, line, lw)\n pen.ell((426, 830, 472, 876), DARK); pen.ell((638, 830, 684, 876), DARK) # wheels\n pen.rr((404, 798, 692, 862), 18, BASE)\n pen.rr((468, 808, 626, 860), 14, BASE2, False)\n pen.poly([(430, 505), (418, 560), (410, 700), (404, 805), (692, 805), (684, 515), (656, 505)], SHELL)\n pen.poly([(455, 418), (628, 418), (650, 432), (657, 505), (430, 505)], SHELL) # head\n pen.poly([(442, 464), (654, 464), (657, 505), (431, 505)], SHELL2, False)\n pen.rr((413, 596, 441, 720), 8, SHELL2, False) # front strip\n pen.rr((446, 526, 672, 800), 70, SHELL_L) # side panel\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gp = Pen(glow, 548, 874, cx, ground, s)\n gp.ell((554 - 74, 663 - 74, 554 + 74, 663 + 74), RING + (90,), False)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(10 * s)))\n pen.d = ImageDraw.Draw(img)\n pen.ell((554 - 62, 663 - 62, 554 + 62, 663 + 62), RING) # ring\n pen.ell((554 - 50, 663 - 50, 554 + 50, 663 + 50), SHELL_L)\n pen.rr((408, 768, 456, 790), 8, DARK) # bumper\n pen.rr((410, 790, 452, 800), 4, (200, 200, 198), False)\n\n# ------------------------------------------------------------ I-flat: flat corridor (no outlines)\ndef corridor_flat():\n S = SS; img = ss_canvas((228, 238, 235)); d = ImageDraw.Draw(img)\n floor = 700 * S\n d.rectangle((0, floor, W * S, H * S), fill=(214, 207, 194))\n d.rectangle((0, floor - 6 * S, W * S, floor), fill=(196, 208, 204))\n d.rectangle((0, 560 * S, W * S, 568 * S), fill=(180, 196, 192))\n for x in range(0, W, 520):\n dx = (x + 140) * S\n d.rounded_rectangle((dx, 300 * S, dx + 190 * S, floor - 6 * S), radius=10 * S, fill=(201, 219, 214))\n d.rounded_rectangle((dx + 55 * S, 360 * S, dx + 135 * S, 470 * S), radius=8 * S, fill=(237, 245, 243))\n d.rectangle((dx + 150 * S, 520 * S, dx + 170 * S, 530 * S), fill=(150, 168, 164))\n d.rounded_rectangle((dx + 60 * S, 270 * S, dx + 130 * S, 290 * S), radius=4 * S, fill=ORANGE)\n for x in (400, 1460):\n px = x * S\n d.rounded_rectangle((px, 640 * S, px + 60 * S, floor), radius=6 * S, fill=(196, 160, 120))\n for a, rr_ in ((-30, 46), (0, 60), (30, 46), (-60, 36), (60, 36)):\n ex = px + 30 * S + math.sin(math.radians(a)) * rr_ * S; ey = 640 * S - math.cos(math.radians(a)) * rr_ * S\n d.ellipse((ex - 22 * S, ey - 34 * S, ex + 22 * S, ey + 34 * S), fill=(110, 170, 130))\n for x in range(0, W, 90):\n d.line(((x + 30) * S, floor + 40 * S, (x + 70) * S, floor + 40 * S), fill=(226, 220, 208), width=4 * S)\n # moving Hakobu (side) + motion lines, and one waiting at a door (front)\n hakobu_side(img, 660 * S, 706 * S, 210 * S)\n d = ImageDraw.Draw(img)\n for k, yy_ in enumerate((540, 590, 640)):\n x0 = (762 + (k % 2) * 22) * S\n d.rounded_rectangle((x0, yy_ * S, x0 + (100 - k * 14) * S, (yy_ + 8) * S), radius=4 * S, fill=(176, 194, 190))\n hakobu_front(img, 1080 * S, 730 * S, 300 * S)\n return finish(img)\n\n# ------------------------------------------------------------ I-line: line + flat colour corridor\ndef corridor_line():\n S = SS; img = ss_canvas((230, 239, 236)); d = ImageDraw.Draw(img); L = 4 * S\n floor = 712 * S\n d.rectangle((0, floor, W * S, H * S), fill=(234, 228, 216)); d.line((0, floor, W * S, floor), fill=INK, width=L)\n d.line((0, 590 * S, W * S, 590 * S), fill=INK, width=3 * S); d.line((0, 600 * S, W * S, 600 * S), fill=INK, width=3 * S)\n for x in range(-80, W, 470):\n d.rectangle((x * S, 330 * S, (x + 200) * S, floor), fill=(206, 224, 219), outline=INK, width=L)\n d.rectangle(((x + 60) * S, 380 * S, (x + 140) * S, 480 * S), fill=(246, 250, 249), outline=INK, width=3 * S)\n d.rounded_rectangle(((x + 160) * S, 540 * S, (x + 184) * S, 556 * S), radius=4 * S, fill=INK)\n d.rectangle(((x + 70) * S, 300 * S, (x + 130) * S, 318 * S), fill=ORANGE, outline=INK, width=3 * S)\n for x in (300, 1530):\n d.rounded_rectangle((x * S, 640 * S, (x + 64) * S, floor), radius=6 * S, fill=(214, 172, 128), outline=INK, width=3 * S)\n for a, rr_ in ((-35, 50), (0, 66), (35, 50)):\n ex = (x + 32 + math.sin(math.radians(a)) * rr_) * S; ey = (640 - math.cos(math.radians(a)) * rr_) * S\n d.ellipse((ex - 24 * S, ey - 38 * S, ex + 24 * S, ey + 38 * S), fill=(132, 190, 150), outline=INK, width=3 * S)\n for x in range(40, W, 110):\n d.line(((x) * S, (floor + 30 * S), (x + 50) * S, (floor + 30 * S)), fill=(210, 202, 188), width=4 * S)\n hakobu_side(img, 660 * S, 714 * S, 210 * S, line=INK, lw=3 * S)\n d = ImageDraw.Draw(img)\n for k, yy_ in enumerate((545, 600, 655)):\n x0 = (770 + (k % 2) * 24) * S\n d.line((x0, yy_ * S, x0 + (100 - k * 14) * S, yy_ * S), fill=INK, width=L)\n hakobu_front(img, 1080 * S, 734 * S, 300 * S, line=INK, lw=3.5 * S)\n return finish(img)\n\ndef ink_manga(im):\n g = ImageOps.autocontrast(ImageOps.grayscale(im), cutoff=2)\n a = np.array(g).astype(int)\n yy_, xx_ = np.mgrid[0:a.shape[0], 0:a.shape[1]]\n dots = ((xx_ % 7 - 3) ** 2 + (yy_ % 7 - 3) ** 2) <= 3\n out = np.full(a.shape, 255, np.uint8)\n out[(a < 205) & dots] = 0\n out[a < 120] = 0\n return Image.fromarray(out).convert('RGB')\n\n# ------------------------------------------------------------ manga: v1 touch, different characters per panel\ndef manga_cast_strip():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 16):\n for x in range(0, W, 16):\n dist = abs(x - W / 2) / (W / 2); r = 3.2 * max(0, dist - 0.35) / 0.65\n if r > 0.4: d.ellipse((x - r, y - r, x + r, y + r), fill=(200, 200, 198))\n mg = lambda n, b: Image.open(SITE + f'c_manga{n}.png').convert('RGB').crop(b)\n panels = [\n (frame(1).crop((0, 730, 380, 1080)), 'none', (0.5, 0.15)), # Hakobu\n (mg(1, (290, 0, 545, 330)), 'surprise', (0.5, 0.1)), # nurse\n (mg(0, (330, 30, 561, 330)), 'sweat', (0.5, 0.1)), # engineer\n (mg(2, (170, 0, 400, 270)), 'joy', (0.5, 0.5)), # arm robot with a face\n ]\n pw, ph, gap = 290, 300, 18\n x = (W - (4 * pw + 3 * gap)) // 2; y = (BAND[1] + BAND[3]) // 2 - ph // 2\n for i, (im, fx, cen) in enumerate(panels):\n big = ImageOps.fit(im, (pw, ph), Image.LANCZOS, centering=cen)\n panel = to_manga(big) if i == 0 else ink_manga(big)\n pd = ImageDraw.Draw(panel)\n if fx == 'surprise':\n for a in range(0, 360, 9):\n ca, sa = math.cos(math.radians(a)), math.sin(math.radians(a))\n pd.line((pw / 2 + ca * 165, ph * 0.3 + sa * 165, pw / 2 + ca * 260, ph * 0.3 + sa * 260), fill=INK, width=2)\n pd.polygon([(30, 20), (48, 20), (43, 70), (35, 70)], fill=INK); pd.ellipse((32, 78, 46, 92), fill=INK)\n if fx == 'sweat':\n pd.polygon([(40, 34), (28, 64), (52, 64)], fill=(255, 255, 255), outline=INK)\n pd.ellipse((26, 52, 54, 80), fill=(255, 255, 255), outline=INK, width=2)\n if fx == 'joy':\n for sx, sy, s in ((40, 40, 18), (255, 60, 14), (60, 120, 10)):\n pd.polygon([(sx, sy - s), (sx + s * 0.25, sy - s * 0.25), (sx + s, sy), (sx + s * 0.25, sy + s * 0.25),\n (sx, sy + s), (sx - s * 0.25, sy + s * 0.25), (sx - s, sy), (sx - s * 0.25, sy - s * 0.25)], fill=INK)\n c.paste(panel, (x, y))\n d.rectangle((x - 4, y - 4, x + pw + 3, y + ph + 3), outline=INK, width=5)\n x += pw + gap\n x0 = (W - (4 * pw + 3 * gap)) // 2\n for k in range(24):\n yy_ = y + 10 + k * (ph - 20) / 23; ln = 90 + (k * 37 % 5) * 45\n d.line((x + 20, yy_, x + 20 + ln, yy_), fill=INK, width=3)\n d.line((x0 - 40 - ln, yy_, x0 - 40, yy_), fill=INK, width=3)\n return c\n\nitems = [('illust_flat', corridor_flat), ('illust_line', corridor_line), ('manga_cast', manga_cast_strip)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'hakobu_banner_{name}_2048x1152.jpg', quality=92, subsampling=0)\n im.crop(BAND).save(OUT + f'_band_{name}.png')\n print(name)</code></pre><h3 id=\"index_sTlRYvOF\">make5_single.py(決定版のバナー)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps\n\n_src = open('/home/claude/banner/make3.py').read()\nexec(_src[:_src.index(\"items = [\")]) # reuse frame(), to_manga(), constants\n\nOUT = '/home/claude/banner/hakobu3/'\nos.makedirs(OUT, exist_ok=True)\nSITE = '/home/claude/banner/site/'\nDARK = (40, 40, 42); BASE = (205, 201, 192); BASE2 = (184, 180, 171)\nSHELL = (247, 246, 242); SHELL2 = (226, 223, 215); SHELL_L = (251, 250, 247); SEAM = (214, 211, 203)\n\n# ------------------------------------------------------------ Hakobu redrawn from the three-view sheet\nclass Pen:\n \"\"\"maps three-view sheet coordinates to the canvas\"\"\"\n def __init__(self, img, x0, y0, ox, oy, s, line=None, lw=0):\n self.d = ImageDraw.Draw(img); self.x0, self.y0, self.ox, self.oy, self.s = x0, y0, ox, oy, s\n self.line, self.lw = line, lw\n def p(self, x, y): return (self.ox + (x - self.x0) * self.s, self.oy + (y - self.y0) * self.s)\n def box(self, b): a = self.p(b[0], b[1]); c = self.p(b[2], b[3]); return (a[0], a[1], c[0], c[1])\n def rr(self, b, r, fill, outline=True):\n self.d.rounded_rectangle(self.box(b), radius=r * self.s, fill=fill,\n outline=self.line if (outline and self.line) else None, width=int(self.lw))\n def poly(self, pts, fill, outline=True):\n self.d.polygon([self.p(*q) for q in pts], fill=fill, outline=self.line if (outline and self.line) else None,\n width=int(self.lw) if self.line else 1)\n def ell(self, b, fill, outline=True):\n self.d.ellipse(self.box(b), fill=fill, outline=self.line if (outline and self.line) else None, width=int(self.lw))\n def ln(self, a, b, fill, w):\n self.d.line((self.p(*a), self.p(*b)), fill=fill, width=max(1, int(w * self.s)))\n\ndef hakobu_front(img, cx, ground, h, line=None, lw=0, expr='smile'):\n s = h / 461.0\n pen = Pen(img, 200, 874, cx, ground, s, line, lw)\n seam = line or SEAM\n pen.rr((95, 836, 120, 874), 6, DARK); pen.rr((280, 836, 305, 874), 6, DARK) # wheels\n pen.rr((72, 806, 328, 848), 12, BASE) # base\n pen.rr((76, 505, 324, 816), 26, SHELL) # body\n pen.rr((78, 525, 98, 800), 8, SHELL2, False); pen.rr((302, 525, 322, 800), 8, SHELL2, False)\n pen.rr((100, 413, 300, 566), 34, SHELL) # head\n pen.rr((112, 546, 288, 566), 10, SHELL2, False)\n pen.rr((120, 427, 283, 541), 12, DISP) # display\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gp = Pen(glow, 200, 874, cx, ground, s)\n for ex in (152, 241):\n gp.rr((ex - 17, 460, ex + 17, 512), 17, (246, 248, 252, 110), False)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(4 * s)))\n pen.d = ImageDraw.Draw(img)\n for ex in (152, 241):\n if expr == 'blink':\n pen.rr((ex - 13, 484, ex + 13, 490), 3, EYE, False)\n else:\n pen.rr((ex - 11, 466, ex + 11, 506), 11, EYE, False)\n pen.d.chord(pen.box((191, 484, 217, 510)), 0, 180, fill=MOUTH) # mouth\n pen.ln((80, 588), (320, 588), seam, 2.2)\n pen.rr((167, 606, 225, 630), 11, DARK, False) # drawer slot\n pen.d.arc(pen.box((78, 668, 322, 730)), 15, 165, fill=seam, width=max(1, int(2.2 * s)))\n pen.rr((78, 767, 322, 786), 6, DARK, False) # black stripe\n pen.rr((177, 796, 224, 810), 7, DARK, False)\n\ndef hakobu_side(img, cx, ground, h, line=None, lw=0):\n \"\"\"side view, front of the robot on the left\"\"\"\n s = h / 461.0\n pen = Pen(img, 548, 874, cx, ground, s, line, lw)\n pen.ell((426, 830, 472, 876), DARK); pen.ell((638, 830, 684, 876), DARK) # wheels\n pen.rr((404, 798, 692, 862), 18, BASE)\n pen.rr((468, 808, 626, 860), 14, BASE2, False)\n pen.poly([(430, 505), (418, 560), (410, 700), (404, 805), (692, 805), (684, 515), (656, 505)], SHELL)\n pen.poly([(455, 418), (628, 418), (650, 432), (657, 505), (430, 505)], SHELL) # head\n pen.poly([(442, 464), (654, 464), (657, 505), (431, 505)], SHELL2, False)\n pen.rr((413, 596, 441, 720), 8, SHELL2, False) # front strip\n pen.rr((446, 526, 672, 800), 70, SHELL_L) # side panel\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gp = Pen(glow, 548, 874, cx, ground, s)\n gp.ell((554 - 74, 663 - 74, 554 + 74, 663 + 74), RING + (90,), False)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(10 * s)))\n pen.d = ImageDraw.Draw(img)\n pen.ell((554 - 62, 663 - 62, 554 + 62, 663 + 62), RING) # ring\n pen.ell((554 - 50, 663 - 50, 554 + 50, 663 + 50), SHELL_L)\n pen.rr((408, 768, 456, 790), 8, DARK) # bumper\n pen.rr((410, 790, 452, 800), 4, (200, 200, 198), False)\n\n# ------------------------------------------------------------ I-flat: flat corridor (no outlines)\ndef corridor_flat():\n S = SS; img = ss_canvas((228, 238, 235)); d = ImageDraw.Draw(img)\n floor = 700 * S\n d.rectangle((0, floor, W * S, H * S), fill=(214, 207, 194))\n d.rectangle((0, floor - 6 * S, W * S, floor), fill=(196, 208, 204))\n d.rectangle((0, 560 * S, W * S, 568 * S), fill=(180, 196, 192))\n for x in range(0, W, 520):\n dx = (x + 140) * S\n d.rounded_rectangle((dx, 300 * S, dx + 190 * S, floor - 6 * S), radius=10 * S, fill=(201, 219, 214))\n d.rounded_rectangle((dx + 55 * S, 360 * S, dx + 135 * S, 470 * S), radius=8 * S, fill=(237, 245, 243))\n d.rectangle((dx + 150 * S, 520 * S, dx + 170 * S, 530 * S), fill=(150, 168, 164))\n d.rounded_rectangle((dx + 60 * S, 270 * S, dx + 130 * S, 290 * S), radius=4 * S, fill=ORANGE)\n for x in (400, 1460):\n px = x * S\n d.rounded_rectangle((px, 640 * S, px + 60 * S, floor), radius=6 * S, fill=(196, 160, 120))\n for a, rr_ in ((-30, 46), (0, 60), (30, 46), (-60, 36), (60, 36)):\n ex = px + 30 * S + math.sin(math.radians(a)) * rr_ * S; ey = 640 * S - math.cos(math.radians(a)) * rr_ * S\n d.ellipse((ex - 22 * S, ey - 34 * S, ex + 22 * S, ey + 34 * S), fill=(110, 170, 130))\n for x in range(0, W, 90):\n d.line(((x + 30) * S, floor + 40 * S, (x + 70) * S, floor + 40 * S), fill=(226, 220, 208), width=4 * S)\n # moving Hakobu (side) + motion lines, and one waiting at a door (front)\n hakobu_front(img, 1015 * S, 730 * S, 300 * S)\n return finish(img)\n\n# ------------------------------------------------------------ I-line: line + flat colour corridor\ndef corridor_line():\n S = SS; img = ss_canvas((230, 239, 236)); d = ImageDraw.Draw(img); L = 4 * S\n floor = 712 * S\n d.rectangle((0, floor, W * S, H * S), fill=(234, 228, 216)); d.line((0, floor, W * S, floor), fill=INK, width=L)\n d.line((0, 590 * S, W * S, 590 * S), fill=INK, width=3 * S); d.line((0, 600 * S, W * S, 600 * S), fill=INK, width=3 * S)\n for x in range(-80, W, 470):\n d.rectangle((x * S, 330 * S, (x + 200) * S, floor), fill=(206, 224, 219), outline=INK, width=L)\n d.rectangle(((x + 60) * S, 380 * S, (x + 140) * S, 480 * S), fill=(246, 250, 249), outline=INK, width=3 * S)\n d.rounded_rectangle(((x + 160) * S, 540 * S, (x + 184) * S, 556 * S), radius=4 * S, fill=INK)\n d.rectangle(((x + 70) * S, 300 * S, (x + 130) * S, 318 * S), fill=ORANGE, outline=INK, width=3 * S)\n for x in (300, 1530):\n d.rounded_rectangle((x * S, 640 * S, (x + 64) * S, floor), radius=6 * S, fill=(214, 172, 128), outline=INK, width=3 * S)\n for a, rr_ in ((-35, 50), (0, 66), (35, 50)):\n ex = (x + 32 + math.sin(math.radians(a)) * rr_) * S; ey = (640 - math.cos(math.radians(a)) * rr_) * S\n d.ellipse((ex - 24 * S, ey - 38 * S, ex + 24 * S, ey + 38 * S), fill=(132, 190, 150), outline=INK, width=3 * S)\n for x in range(40, W, 110):\n d.line(((x) * S, (floor + 30 * S), (x + 50) * S, (floor + 30 * S)), fill=(210, 202, 188), width=4 * S)\n hakobu_side(img, 660 * S, 714 * S, 210 * S, line=INK, lw=3 * S)\n d = ImageDraw.Draw(img)\n for k, yy_ in enumerate((545, 600, 655)):\n x0 = (770 + (k % 2) * 24) * S\n d.line((x0, yy_ * S, x0 + (100 - k * 14) * S, yy_ * S), fill=INK, width=L)\n hakobu_front(img, 1080 * S, 734 * S, 300 * S, line=INK, lw=3.5 * S)\n return finish(img)\n\ndef ink_manga(im):\n g = ImageOps.autocontrast(ImageOps.grayscale(im), cutoff=2)\n a = np.array(g).astype(int)\n yy_, xx_ = np.mgrid[0:a.shape[0], 0:a.shape[1]]\n dots = ((xx_ % 7 - 3) ** 2 + (yy_ % 7 - 3) ** 2) <= 3\n out = np.full(a.shape, 255, np.uint8)\n out[(a < 205) & dots] = 0\n out[a < 120] = 0\n return Image.fromarray(out).convert('RGB')\n\n# ------------------------------------------------------------ manga: v1 touch, different characters per panel\ndef manga_cast_strip():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 16):\n for x in range(0, W, 16):\n dist = abs(x - W / 2) / (W / 2); r = 3.2 * max(0, dist - 0.35) / 0.65\n if r > 0.4: d.ellipse((x - r, y - r, x + r, y + r), fill=(200, 200, 198))\n mg = lambda n, b: Image.open(SITE + f'c_manga{n}.png').convert('RGB').crop(b)\n panels = [\n (frame(1).crop((0, 730, 380, 1080)), 'none', (0.5, 0.15)), # Hakobu\n (mg(1, (290, 0, 545, 330)), 'surprise', (0.5, 0.1)), # nurse\n (mg(0, (330, 30, 561, 330)), 'sweat', (0.5, 0.1)), # engineer\n (mg(2, (170, 0, 400, 270)), 'joy', (0.5, 0.5)), # arm robot with a face\n ]\n pw, ph, gap = 290, 300, 18\n x = (W - (4 * pw + 3 * gap)) // 2; y = (BAND[1] + BAND[3]) // 2 - ph // 2\n for i, (im, fx, cen) in enumerate(panels):\n big = ImageOps.fit(im, (pw, ph), Image.LANCZOS, centering=cen)\n panel = to_manga(big) if i == 0 else ink_manga(big)\n pd = ImageDraw.Draw(panel)\n if fx == 'surprise':\n for a in range(0, 360, 9):\n ca, sa = math.cos(math.radians(a)), math.sin(math.radians(a))\n pd.line((pw / 2 + ca * 165, ph * 0.3 + sa * 165, pw / 2 + ca * 260, ph * 0.3 + sa * 260), fill=INK, width=2)\n pd.polygon([(30, 20), (48, 20), (43, 70), (35, 70)], fill=INK); pd.ellipse((32, 78, 46, 92), fill=INK)\n if fx == 'sweat':\n pd.polygon([(40, 34), (28, 64), (52, 64)], fill=(255, 255, 255), outline=INK)\n pd.ellipse((26, 52, 54, 80), fill=(255, 255, 255), outline=INK, width=2)\n if fx == 'joy':\n for sx, sy, s in ((40, 40, 18), (255, 60, 14), (60, 120, 10)):\n pd.polygon([(sx, sy - s), (sx + s * 0.25, sy - s * 0.25), (sx + s, sy), (sx + s * 0.25, sy + s * 0.25),\n (sx, sy + s), (sx - s * 0.25, sy + s * 0.25), (sx - s, sy), (sx - s * 0.25, sy - s * 0.25)], fill=INK)\n c.paste(panel, (x, y))\n d.rectangle((x - 4, y - 4, x + pw + 3, y + ph + 3), outline=INK, width=5)\n x += pw + gap\n x0 = (W - (4 * pw + 3 * gap)) // 2\n for k in range(24):\n yy_ = y + 10 + k * (ph - 20) / 23; ln = 90 + (k * 37 % 5) * 45\n d.line((x + 20, yy_, x + 20 + ln, yy_), fill=INK, width=3)\n d.line((x0 - 40 - ln, yy_, x0 - 40, yy_), fill=INK, width=3)\n return c\n\nitems = [('illust_flat_single', corridor_flat)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'hakobu_banner_{name}_2048x1152.jpg', quality=92, subsampling=0)\n im.crop(BAND).save(OUT + f'_band_{name}.png')\n print(name)</code></pre><h2 id=\"index_NP8gCgQ3\">スクリプト以外に実行したコマンド</h2><h3 id=\"index_VpcG8_8h\">サイトの画面を保存したPDFから画像を取り出し、ページを縦につないだ</h3><pre><code class=\"language-bash\">cd /home/claude/banner/site && pdfimages -j \"/root/.claude/uploads/6f54b9ec-7b51-53dd-a180-2e8bdb7969c6/f328e762-screencapture-adaptdesign-jp-pixelstomotors-2026-10-12-07_46_26.pdf\" pg && convert pg-000.jpg pg-001.jpg pg-002.jpg -append full.jpg && identify full.jpg && for i in 0 1 2 3 4; do convert full.jpg -crop 2728x2100+0+$((i*2100)) -resize 1100x prev$i.jpg; done; ls</code></pre><h3 id=\"index_naTwarcA\">つないだ画像から、サムネイルや4コマを切り出した</h3><pre><code class=\"language-python\">from PIL import Image\nim=Image.open('full.jpg')\nk=1/0.4032\ndef c(name, x0,y0,x1,y1,page):\n box=(int(x0*k),int(page*2100+y0*k),int(x1*k),int(page*2100+y1*k))\n im.crop(box).save(f'c_{name}.png'); print(name, box, (box[2]-box[0],box[3]-box[1]))\nc('hero',670,86,936,283,0)\nfor i,(a,b) in enumerate([(62,288),(312,538),(562,788),(812,1038)]):\n c(f'manga{i}',a,512,b,728,0)\nc('drug',62,158,535,481,1)\nc('lab',565,158,1038,481,1)\nc('imgrec_top',62,598,535,846,1)\nc('deep_top',565,598,1038,846,1)\nc('sim',62,245,535,533,2)\nc('flow',565,210,1038,533,2)\nc('rec_study',62,0,368,116,3); c('rec_micro',397,0,703,116,3); c('rec_say',731,0,1038,116,3)\nc('bowl',62,254,368,425,3); c('gpu',397,254,703,425,3); c('film',731,254,1038,425,3)\nc('panels324',62,605,368,775,3); c('panels323',397,605,703,775,3); c('who03',731,605,1038,775,3)\nc('inside',62,65,368,236,4); c('panels320',397,65,703,236,4); c('panels319',731,65,1038,236,4)\nc('issues',620,488,900,612,4)</code></pre><h3 id=\"index_egF7Khp0\">試作動画から、ハコブくんの表情のコマを書き出した</h3><pre><code class=\"language-bash\">cd /home/claude/banner/vid && V=\"/root/.claude/uploads/6f54b9ec-7b51-53dd-a180-2e8bdb7969c6/d2aa7563-hakobu_demo_upper.mp4\"; ffmpeg -v error -y -i \"$V\" -vf \"fps=3,crop=400:340:0:740\" h_%03d.png; ls h_*.png | wc -l; montage h_*.png -tile 10x -geometry 200x170+2+2 -label '%t' -pointsize 10 hsheet.jpg</code></pre><pre><code class=\"language-bash\">cd /home/claude/banner/vid && V=\"/root/.claude/uploads/6f54b9ec-7b51-53dd-a180-2e8bdb7969c6/d2aa7563-hakobu_demo_upper.mp4\"; for t in 0 3.0; do ffmpeg -v error -y -ss $t -i \"$V\" -frames:v 1 fr_$t.png; done</code></pre><pre><code class=\"language-bash\">cd /home/claude/banner/vid && V=\"/root/.claude/uploads/6f54b9ec-7b51-53dd-a180-2e8bdb7969c6/d2aa7563-hakobu_demo_upper.mp4\"; mkdir -p fx && for i in 1 4 8 9 15 24; do t=$(python3 -c \"print(($i-1)/3)\"); ffmpeg -v error -y -ss $t -i \"$V\" -frames:v 1 fx/e_$i.png; done; ls fx</code></pre><h3 id=\"index_hy_pvXuR\">動画のコマで、ハコブくんの範囲とディスプレイの位置を測った</h3><pre><code class=\"language-python\">from PIL import Image\nimport numpy as np\nim=Image.open('fr_0.png').convert('RGB')\na=np.array(im).astype(int)\nbg=np.array([245,245,240])\nreg=a[700:1080,0:420]\ndist=np.abs(reg-bg).sum(-1)\nys,xs=np.where(dist>18)\nprint('bbox',xs.min(),xs.max(),ys.min()+700,ys.max()+700)\nprint('corner color',a[720,5],a[1075,410])</code></pre><pre><code class=\"language-python\">from PIL import Image; import numpy as np\na=np.array(Image.open('fr_0.png').convert('RGB')).astype(int)[700:1080,0:420]\nys,xs=np.where(a.sum(-1)<90); print(xs.min(),xs.max(),ys.min()+700,ys.max()+700)</code></pre><h3 id=\"index_rLSvvOLQ\">三面図の3つの図の範囲、リングの中心と半径、口の位置を測った</h3><pre><code class=\"language-python\">from PIL import Image\nimport numpy as np\na=np.array(Image.open('src.png').convert('L')).astype(int)\n# column/row darkness profile to find view bboxes\ndark=a<120\ncols=dark[380:900].sum(0); rows=dark[:, :].sum(1)\nruns=[];inrun=False\nfor x,v in enumerate(cols):\n if v>3 and not inrun: s=x;inrun=True\n if v<=3 and inrun: runs.append((s,x));inrun=False\nprint([r for r in runs if r[1]-r[0]>20])\nfor (x0,x1) in [r for r in runs if r[1]-r[0]>20]:\n ys=np.where(dark[:,x0:x1].sum(1)>2)[0]; ys=ys[(ys>300)&(ys<1000)]; print(x0,x1,ys.min(),ys.max())\n# ring: find dark circle near side center</code></pre><pre><code class=\"language-python\">from PIL import Image\nimport numpy as np\na=np.array(Image.open('src.png').convert('L')).astype(float)\nbest=None\nfor cx in range(540,568,2):\n for cy in range(655,690,2):\n yy,xx=np.mgrid[cy-80:cy+80,cx-80:cx+80]; r=np.hypot(xx-cx,yy-cy); sub=a[cy-80:cy+80,cx-80:cx+80]\n prof=[sub[(r>=k)&(r<k+1)].mean() for k in range(30,75)]\n score=np.std(prof)\n sharp=sorted(prof)[:2]\n s=sum(sharp)\n if best is None or s<best[0]: best=(s,cx,cy,prof)\ns,cx,cy,prof=best; print(cx,cy); print([int(p) for p in prof])\n# mouth gray pixels\nsub=a[480:515,175:230]; ys,xs=np.where((sub>70)&(sub<200)); print('mouth', xs.min()+175,xs.max()+175,ys.min()+480,ys.max()+480)</code></pre><h3 id=\"index_kGmWoZfd\">2回目の全案を 2048×1152px で書き出した(1回目の写真は文字を外して作り直した)</h3><pre><code class=\"language-bash\">cd /home/claude/banner && python3 - <<'EOF'\nsrc=open('make.py').read()\nsrc=src[:src.index(\"import os\\nos.makedirs\")]\nexec(src)\ndef arm_clean():\n bg = Image.new('RGB', (W, H), MAT)\n s=0.74; subj=((1282+2650)/2, 1790)\n layer,_=place(bg, photo, s, subj, (1280, 730))\n ox=1280-subj[0]*s; oy=730-subj[1]*s\n mb=(ox+MAT_BOX[0]*s+70, oy+MAT_BOX[1]*s+70, ox+MAT_BOX[2]*s-70, oy+MAT_BOX[3]*s-70)\n bg.paste(ImageEnhance.Brightness(layer).enhance(0.95),(0,0),rect_mask(mb,60))\n return bg\nim=arm_clean(); im.save('/home/claude/banner/deliver/pixelstomotors_banner_A_arm_photo.jpg',quality=92,subsampling=0)\nEOF\nmkdir -p final_2048 && cd deliver && for f in pixelstomotors_banner_*.jpg; do n=${f%.jpg}; convert \"$f\" -resize 2048x1152! -quality 92 -sampling-factor 4:4:4 ../final_2048/${n}_2048x1152.jpg; done; cd ../final_2048 && identify -format \"%f %wx%h %b\\n\" *.jpg</code></pre><p></p><h2 id=\"index_dxzTRtXR\">実行ログ</h2><h3 id=\"index_6qQln2AW\">1回目の書き出し(make.py)</h3><pre><code>A_grasp 464 KB\nB_pixels_to_motors 341 KB\nC_see_decide_move 461 KB</code></pre><h3 id=\"index_fJsloe7H\">2回目の書き出し(make2.py)</h3><pre><code>B_archive_wall 769 KB\nC_mono_split 315 KB\nD_sim_halftone 1300 KB\nE_hakobu_manga 908 KB\nF_dot_matrix 669 KB\nG_hakobu_photo 170 KB</code></pre><h3 id=\"index_gljFeQAL\">サイトの画面から切り出した範囲</h3><pre><code>hero (1661, 213, 2321, 701) (660, 488)\nmanga0 (153, 1269, 714, 1805) (561, 536)\nmanga1 (773, 1269, 1334, 1805) (561, 536)\nmanga2 (1393, 1269, 1954, 1805) (561, 536)\nmanga3 (2013, 1269, 2574, 1805) (561, 536)\ndrug (153, 2491, 1326, 3292) (1173, 801)\nlab (1401, 2491, 2574, 3292) (1173, 801)\nimgrec_top (153, 3583, 1326, 4198) (1173, 615)\ndeep_top (1401, 3583, 2574, 4198) (1173, 615)\nsim (153, 4807, 1326, 5521) (1173, 714)\nflow (1401, 4720, 2574, 5521) (1173, 801)\nrec_study (153, 6300, 912, 6587) (759, 287)\nrec_micro (984, 6300, 1743, 6587) (759, 287)\nrec_say (1812, 6300, 2574, 6587) (762, 287)\nbowl (153, 6929, 912, 7354) (759, 425)\ngpu (984, 6929, 1743, 7354) (759, 425)\nfilm (1812, 6929, 2574, 7354) (762, 425)\npanels324 (153, 7800, 912, 8222) (759, 422)\npanels323 (984, 7800, 1743, 8222) (759, 422)\nwho03 (1812, 7800, 2574, 8222) (762, 422)\ninside (153, 8561, 912, 8985) (759, 424)\npanels320 (984, 8561, 1743, 8985) (759, 424)\npanels319 (1812, 8561, 2574, 8985) (762, 424)\nissues (1537, 9610, 2232, 9917) (695, 307)</code></pre><h3 id=\"index_JyBXovfG\">試作動画の情報と、ハコブくんの範囲</h3><pre><code>1920,1080,30/1,13.266667\n0/0,13.266000</code></pre><pre><code>bbox 29 351 766 1079\ncorner color [244 245 237] [244 245 237]</code></pre><pre><code>155 326 787 1022</code></pre><h3 id=\"index_GC_7inZM\">三面図の測定</h3><pre><code>[(70, 331), (396, 698), (749, 1012)]\n70 331 415 874\n396 698 419 874\n749 1012 420 875</code></pre><pre><code>554 663\n[250, 250, 249, 248, 247, 246, 245, 244, 242, 241, 240, 239, 238, 237, 235, 235, 234, 232, 225, 188, 140, 122, 140, 173, 179, 182, 188, 189, 177, 181, 174, 131, 114, 155, 194, 219, 227, 228, 229, 230, 230, 231, 232, 232, 233]\nmouth 184 215 490 505</code></pre><h3 id=\"index_htfRwQhT\">2048×1152px への書き出し</h3><pre><code>pixelstomotors_banner_A_arm_photo_2048x1152.jpg 2048x1152 478891B\npixelstomotors_banner_B_archive_wall_2048x1152.jpg 2048x1152 522786B\npixelstomotors_banner_C_mono_split_2048x1152.jpg 2048x1152 182721B\npixelstomotors_banner_D_sim_halftone_2048x1152.jpg 2048x1152 506840B\npixelstomotors_banner_E_hakobu_manga_2048x1152.jpg 2048x1152 506732B\npixelstomotors_banner_F_dot_matrix_2048x1152.jpg 2048x1152 481321B\npixelstomotors_banner_G_hakobu_photo_2048x1152.jpg 2048x1152 96050B</code></pre><h3 id=\"index_l3SpavP3\">3回目〜決定版の書き出し</h3><pre><code>1_illustration\n2_manga\n3_realistic\n4_photo\n5_graphic</code></pre><pre><code>I1_threeview\nI2_corridor\nM1_manga_page\nM2_cast</code></pre><pre><code>illust_flat\nillust_line\nmanga_cast</code></pre><pre><code>illust_flat_single</code></pre><p></p><h2 id=\"index_QUNQPzA5\">参照した情報</h2><ul><li><p><a href=\"https://support.google.com/youtube/answer/10456525?hl=ja\" data-has-link=\"true\">YouTube ヘルプ:チャンネルのブランディングを管理する</a></p></li><li><p><a href=\"https://support.google.com/youtube/answer/9481328?hl=ja\" data-has-link=\"true\">YouTube ヘルプ:チャンネルの権限</a></p></li><li><p><a href=\"/\" data-has-link=\"true\">Pixels to Motors</a></p></li></ul>", "toc": [{"id": "index__Ynzo3Cp", "text": "YouTube用のアカウントは、Google Workspaceのエイリアスではなく新しいユーザーにした"}, {"id": "index_8rfkxb9m", "text": "管理コンソールには、普段使いのアカウントでは入れなかった"}, {"id": "index_SZwLHwTg", "text": "「このユーザーは既に存在しています」の原因は、3月に付けたエイリアスだった"}, {"id": "index_EnRPO5Pq", "text": "作ろうとしたチャンネルのハンドルは、自分の既存チャンネルが使っていた"}, {"id": "index_04xwKQv5", "text": "既存チャンネルは、新しいアカウントを「管理者」として招待して引き継いだ"}, {"id": "index_6tAUffjk", "text": "チャンネルの説明文は、フィジカルAIを先頭に置いて書き直した"}, {"id": "index_GNMYvKmk", "text": "バナーは、どの端末でも切れない中央の範囲を基準に作った"}, {"id": "index_BpOCllXq", "text": "バナーの1回目は、実機の写真に文字を入れた3案だった"}, {"id": "index_KWVQksqt", "text": "2回目は、サイトの画像から6つの方向を出した"}, {"id": "index_wfU0AzRw", "text": "アイコンは、動画で話しているハコブくんの1コマから作った"}, {"id": "index_BZlJ8lML", "text": "バナーの3回目は、ハコブくんだけで5つのタッチを作った"}, {"id": "index_aoA9mNR9", "text": "5回目で、ハコブくんを背景と同じタッチで描き直した"}, {"id": "index_8PXH6omo", "text": "チャンネル名は「フィジカルAI解説|Pixels to Motors」を候補にした"}, {"id": "index_mtwyUJht", "text": "チャンネル名の変更と、アイコン・4コマの作り直しが残っている"}, {"id": "index_LGOmRd2E", "text": "制作スクリプトの全文"}, {"id": "index_NP8gCgQ3", "text": "スクリプト以外に実行したコマンド"}, {"id": "index_dxzTRtXR", "text": "実行ログ"}, {"id": "index_QUNQPzA5", "text": "参照した情報"}]}
\ No newline at end of file
+{"day": "LOG332", "series": "ロボティクスデザイン", "published": "2026-10-12T00:28:45.000Z", "title": "YouTubeチャンネルの顔を作る ― チャンネル名・アイコン・バナーをハコブくんにそろえるまで", "slug": "youtube-channel-identity", "cover": null, "thumb": "/thumbs/r-youtube-channel-identity.html", "lang": "JP", "description": "解説動画を出す前に、YouTubeチャンネルを運用用の新アカウントへ引き継ぎ、説明文・アイコン・バナーをナレーション役のハコブくんにそろえた。バナーは5回作り直し、計18案から決めた。", "updated": "2026-10-12T00:28:42.000Z", "body": "<!--TOC--><p></p><p>Pixels to Motors の YouTube チャンネルで解説動画を出す準備として、チャンネルの見た目と運用の仕組みを Claude と対話しながら整えました。解説動画のナレーションは、病院で薬を運ぶロボットのキャラクター「ハコブくん」が担当すると決めています(LOG331)。そこで、チャンネルを開いた人が最初に目にするアイコンとバナーも、ハコブくんにそろえることにしました。</p><p>やったことは大きく4つです。YouTube を運用するための Google アカウントを新しく作り、既存のチャンネルを引き継ぐ。チャンネルの説明文を書き直す。バナーを作る。アイコンをハコブくんにする。バナーは文字入りの3案から始めて、5回作り直し、計18案を見たうえで決めました。</p><p>本記事は、その手順と、途中で方針が変わった場面の記録です。アカウントの引き継ぎで Claude が誤った案内をした場面も残しています。バナーとアイコンを作った Python スクリプトは、記事の後半に全文を載せています。メールアドレスと Google Workspace の料金の具体的な金額は伏せました。</p><p></p><figure><img src=\"/media/s-2704x1546_v-frms_webp_69baf643-e22a-4e3e-9067-63e9453efb8b.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index__Ynzo3Cp\">YouTube用のアカウントは、Google Workspaceのエイリアスではなく新しいユーザーにした</h2><p>Pixels to Motors のメールは、ADAPT DESIGN の独自ドメインで Google Workspace(Google のメールやドライブを会社のドメインで使うサービス)を契約して運用しています。ここに Pixels to Motors 用のアドレスを足す方法は2つありました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>方法</p></th><th colspan=\"1\" rowspan=\"1\"><p>できること</p></th><th colspan=\"1\" rowspan=\"1\"><p>できないこと</p></th><th colspan=\"1\" rowspan=\"1\"><p>費用</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>エイリアス(既存のユーザーに別名のアドレスを付ける)</p></td><td colspan=\"1\" rowspan=\"1\"><p>メールの受信。設定すれば、そのアドレス名義での送信</p></td><td colspan=\"1\" rowspan=\"1\"><p>そのアドレスで Google にログインすること</p></td><td colspan=\"1\" rowspan=\"1\"><p>かからない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>新しいユーザー</p></td><td colspan=\"1\" rowspan=\"1\"><p>専用のログイン、ドライブ、YouTube チャンネル、外部サービスのアカウントを完全に分けること</p></td><td colspan=\"1\" rowspan=\"1\"><p>―</p></td><td colspan=\"1\" rowspan=\"1\"><p>1ユーザー分の月額が増える</p></td></tr></tbody></table></div><p>YouTube チャンネルを、普段使いのアカウントから切り離して運用したかったので、新しいユーザーにしました。契約が年間プランのため、途中で足したユーザーは更新時期まで減らせない前提で決めています。</p><p></p><h2 id=\"index_8rfkxb9m\">管理コンソールには、普段使いのアカウントでは入れなかった</h2><p>ユーザーを足す画面は、Google Workspace の管理コンソール(admin.google.com)にあります。最初に普段使いのアカウントでログインしたところ、「管理者アカウントでログインしてください」と表示されて入れませんでした。</p><p>ドメインには、普段使いのアカウントともう1つ、合わせて2つのユーザーがありました。管理者の権限を持っていたのは、もう1つのほうでした。そちらで入り直すと、Chrome が「このプロフィールで作業を続けるか、仕事用のプロフィールを新しく作成するか」を聞いてきます。以前、Chrome のプロフィールが10個以上重複して増えたことがあったため、どちらを選ぶかは「そのプロフィールを何に使っているか」で決めました。仕事用のプロフィールに切り替わったあとは、管理コンソールが自動では開き直らなかったため、アドレスを入れて開き直しています。</p><p></p><h2 id=\"index_SZwLHwTg\">「このユーザーは既に存在しています」の原因は、3月に付けたエイリアスだった</h2><p>「新しいユーザーの追加」で、作りたいアドレスを入れると、次の赤字が出て作成できませんでした。</p><blockquote><p>すでにユーザーの予備のメールアドレス(メール エイリアス)として使用されています</p></blockquote><p>2026年3月、同じアドレスをエイリアスとして管理者アカウントに付けていました(当時はユーザーを増やさず、無料のエイリアスで済ませる判断をしていた)。エイリアスとして使っているアドレスと同じ名前のユーザーは作れません。</p><p>管理者アカウントの詳細画面から「メールエイリアス」を開き、そのエイリアスを削除して保存しました。そのあと「新しいユーザーの追加」をやり直すと、ユーザーが作成されました。名前は姓「Pixels to」、名「Motors」で入れています。この入れ方だと、YouTube の画面によっては「Motors Pixels to」と、姓と名が逆の順に表示されることが後で分かりました。</p><p></p><h2 id=\"index_EnRPO5Pq\">作ろうとしたチャンネルのハンドルは、自分の既存チャンネルが使っていた</h2><p>新しいアカウントで YouTube を開き、チャンネルを作ろうとすると、ハンドル(チャンネルの @ から始まる固有の名前)の欄に次のように出ました。</p><blockquote><p>このハンドルは使用できません。 こちらは使用可能です: @PixelstoMotors-r2y</p></blockquote><p>@PixelstoMotors を使っていたのは、2026年3月21日に管理者アカウントで作った、自分の既存チャンネルでした。登録者1人、動画1本(mechArm 270-Pi の開封動画)がすでにあります。</p><p>新しいチャンネルを別に作ると、動画と登録者が既存チャンネルに残ったまま、ハンドルも別のものになります。そこで新しいチャンネルは作らず、既存チャンネルを新しいアカウントから操作できるようにする方針に切り替えました。</p><p></p><h2 id=\"index_04xwKQv5\">既存チャンネルは、新しいアカウントを「管理者」として招待して引き継いだ</h2><p>YouTube Studio の「設定」→「権限」で、新しいアカウントのアドレスを招待しました。選べる役割は次のとおりで、「オーナー」は一覧にありませんでした。YouTube のヘルプにも、オーナーの役割は他のユーザーに移せないと書かれています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>役割</p></th><th colspan=\"1\" rowspan=\"1\"><p>できること</p></th><th colspan=\"1\" rowspan=\"1\"><p>できないこと</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>オーナー</p></td><td colspan=\"1\" rowspan=\"1\"><p>すべての操作。チャンネルの削除、権限の管理</p></td><td colspan=\"1\" rowspan=\"1\"><p>オーナーの役割を他のユーザーに移すこと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>管理者</p></td><td colspan=\"1\" rowspan=\"1\"><p>全データの閲覧、権限の管理、チャンネル情報の編集、動画の作成・アップロード・公開・削除</p></td><td colspan=\"1\" rowspan=\"1\"><p>チャンネルの削除</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>編集者</p></td><td colspan=\"1\" rowspan=\"1\"><p>全データの閲覧、編集、アップロード・公開</p></td><td colspan=\"1\" rowspan=\"1\"><p>公開済みの動画の削除、権限の管理</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>字幕編集者</p></td><td colspan=\"1\" rowspan=\"1\"><p>字幕の追加・編集・削除</p></td><td colspan=\"1\" rowspan=\"1\"><p>字幕以外の編集</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>閲覧者</p></td><td colspan=\"1\" rowspan=\"1\"><p>全データの閲覧</p></td><td colspan=\"1\" rowspan=\"1\"><p>編集</p></td></tr></tbody></table></div><p>新しいアカウントには「管理者」を付けました。動画の投稿から分析、説明文やバナーの変更まで、運用に必要なことはすべてできます。</p><h3 id=\"index_YvFm6X9v\">招待の承諾には、受け取る側にもチャンネルが必要だった</h3><p>招待メールを新しいアカウントで開いて進むと、「チャンネルのプロフィール」という、新しいチャンネルを作る画面が出ました。Claude はこれを「余計な空のチャンネルができてしまう画面」と判断し、キャンセルするよう案内しました。</p><p>キャンセルしたあと Studio の権限の画面を見ると、新しいアカウントは「承認保留中」のままでした。招待メールを開き直すと、手順は次の2段になっていました。</p><blockquote><p>ステップ 1. Pixels to Motors にアクセスするには YouTube チャンネルが必要です。 ステップ 2. チャンネルの作成後、この招待を承諾すると Pixels to Motors にアクセスできるようになります。</p></blockquote><p>キャンセルを勧めた画面は、承諾の途中の「ステップ1」でした。招待を受ける側のアカウントにも自分のチャンネルが必要、という YouTube の仕様を Claude が知らずに案内した誤りです。新しいアカウント側に空のチャンネルを作ってから「招待を承諾」を押すと、承諾済みになりました。空のチャンネルは使わずに置いておきます。</p><p>承諾したあとは、Studio の右上で「チャンネルを切り替え」から Pixels to Motors を選ぶと、「あなたは管理者です」と表示された状態で管理画面に入れました。</p><h3 id=\"index_7woCpmaH\">管理者で入っていても、YouTube本体での名義は空のチャンネルになる</h3><p>管理者の権限で使えるのは、Studio の中の操作です。YouTube 本体の画面(動画の視聴ページなど)に移ると、操作の名義は新しいアカウント自身の空のチャンネルになります。他の人の動画に Pixels to Motors の名前でコメントしたり、チャンネル登録したりはできません。</p><p>これも必要になったら、チャンネルの所有の形そのものを変える必要があります。どう変えるのが正しいかは、今回は確かめていません。</p><p></p><h2 id=\"index_6tAUffjk\">チャンネルの説明文は、フィジカルAIを先頭に置いて書き直した</h2><p>書き直す前の説明文は、次のとおりでした。</p><pre><code>UX × AI × Robotics の交差点に存在する、\nまだ誰も名前をつけていない領域「Motion UX」を探求しています。\n\niPhoneの3Dスキャン → Unityシミュレーション → 実機ロボットアーム\nという低コストなパイプラインを一人で構築中。\n\nすべてのプロセスをオープンに記録しています。\n\n// ADAPT DESIGN — Pixels to Motors\n// adaptdesign.jp/pixelstomotors</code></pre><p>iPhone → Unity → 実機というパイプラインは、いまの開発の中心ではなくなっています。冒頭の2〜3行は、検索結果やチャンネルページで最初に見える部分です。そこで、機材の名前ではなく、何のチャンネルかが伝わる文にしました。方向の違う3案(フィジカルAI・実装寄り/UX・体験寄り/個人・低コスト寄り)から、フィジカルAI寄りの案を選び、機材名の行は削りました。英語の段落は、日本語と同じ内容にそろえています。</p><pre><code>フィジカルAIを、現場で使える形にするまでを記録するチャンネルです。\nAIが考え、ロボットが動く。その間にある設計・調整・失敗を、\n小さなロボットアームで一つずつ確かめています。\n\n精度より、まず体験が成立すること。\n精度はあとからお金で解決できる。\n\nDocumenting the journey of making Physical AI work in real-world settings.\nBetween AI thinking and robots moving lies design, tuning, and failure.\nI test each step with a small robot arm.\n\nExperience first. Precision can be bought later.\n\n// ADAPT DESIGN — Pixels to Motors\n// adaptdesign.jp/pixelstomotors</code></pre><p>Studio の「カスタマイズ」→「プロフィール」→「説明」に貼り付けて公開しました。公開ボタンが押せない状態(灰色)になっていれば保存済みです。ただ、チャンネルページの表示は、記事を書いている時点でもまだ古い説明文のままでした。</p><p></p><h2 id=\"index_GNMYvKmk\">バナーは、どの端末でも切れない中央の範囲を基準に作った</h2><p>YouTube のヘルプでは、バナー画像は次のように決まっています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>値</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>最小サイズ</p></td><td colspan=\"1\" rowspan=\"1\"><p>2048×1152px(16:9)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>テキストやロゴが切れずに表示される範囲</p></td><td colspan=\"1\" rowspan=\"1\"><p>中央の 1235×338px</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>推奨サイズ(テレビでも見やすい)</p></td><td colspan=\"1\" rowspan=\"1\"><p>2560×1440px</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ファイルサイズ</p></td><td colspan=\"1\" rowspan=\"1\"><p>6MB 以下</p></td></tr></tbody></table></div><p>同じ 16:9 の画像が、テレビでは全体、パソコンでは横長の帯、スマホではさらに左右を切った中央だけで表示されます。そのため、主役になる絵は中央の 1235×338px(2560×1440px の画像なら、同じ比率で 1546×423px)に収めました。作った案は、パソコンでの見え方(横長の帯)と、全体図に安全範囲の枠を重ねた図の2つで確認しています。</p><div class=\"embed\"><iframe src=\"/embeds/youtube-channel-identity-MHKfODFx.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p></p><h2 id=\"index_BpOCllXq\">バナーの1回目は、実機の写真に文字を入れた3案だった</h2><p>最初は、手元にある素材から作りました。サイトの画像はこちらの作業環境から取得できなかったため、Mac の r2r2r-content フォルダにあった画像認識シリーズの写真を使っています。</p><ul><li><p>A:mechArm 270-Pi のグリッパーと赤・青のキューブを真上から撮った写真に、「フィジカルAIを、現場で使える形に。」の文字</p></li><li><p>B:アームの根元側をドット状にぼかし、先端に向かってくっきりさせた「Pixels to Motors」の絵</p></li><li><p>C:明るい背景に、キューブの写真・色で切り出したマスク・グリッパーの3コマを並べた「見る → 判断する → 動かす」</p></li></ul><p></p><figure><img src=\"/media/s-1280x636_v-fms_webp_406c9cd8-a337-4559-9ae0-f3c27acf5acd.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>これに対しての返事は次のとおりでした。</p><blockquote><p>テキスト情報は必要なくイメージだけでいいですよ。同じアームだけを使ったバナーではなく、もっといろんなパターンが考えられるはずです。最初はもっと違うパターンを見せて欲しい(今回のは1種類カウントです)</p></blockquote><p>3案とも同じアームの写真が主役で、見せ方の幅が狭かったということです。</p><p></p><h2 id=\"index_KWVQksqt\">2回目は、サイトの画像から6つの方向を出した</h2><p>サイトのトップページを画面ごとPDFに保存したものを受け取り、そこから記事のサムネイル、4コマ漫画、シリーズの扉絵などを切り出しました。文字は入れず、方向がはっきり違う6案にしています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>案</p></th><th colspan=\"1\" rowspan=\"1\"><p>中身</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>アーカイブの壁</p></td><td colspan=\"1\" rowspan=\"1\"><p>記事のサムネイルと4コマを3段に並べる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>白黒スプリット</p></td><td colspan=\"1\" rowspan=\"1\"><p>左が黒、右がクリーム。境目でピクセルが崩れ、斜線の円と点線のグラフを重ねる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>シミュレーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>Unity で並べたたくさんのアームを網点にする</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ハコブくん 4コマ</p></td><td colspan=\"1\" rowspan=\"1\"><p>4コマ漫画を横一列に並べ、両側に集中線</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ドットマトリクス</p></td><td colspan=\"1\" rowspan=\"1\"><p>「フィジカルAIが社会実装される前に解くべき課題」の丸を全面に広げる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ハコブくん 写真</p></td><td colspan=\"1\" rowspan=\"1\"><p>病院の廊下にいるハコブくんの写真</p></td></tr></tbody></table></div><p></p><figure><img src=\"/media/s-1717x1722_v-fms_webp_c6c4e138-f06b-41a1-bfec-053d0dfc7356.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>このあと、YouTube に合わせて全案を 2048×1152px でも書き出しました。</p><p></p><h2 id=\"index_wfU0AzRw\">アイコンは、動画で話しているハコブくんの1コマから作った</h2><p>チャンネル名とアイコンを、YouTube で覚えてもらいやすいものに変えることになりました。アイコンはハコブくんにする前提です。</p><p>最初に Claude が出したのは、ハコブくんの顔(黒いディスプレイ、白いカプセル形の目、水色の口)をコードで描き起こした3案でした。顔のアップ、オレンジの背景に頭と胴、いまのアイコンの市松模様を引き継いだもの、の3つです。</p><p></p><figure><img src=\"/media/s-1500x940_v-fms_webp_c3582154-8fcc-4086-b283-c9ee9792e885.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>これには、試作動画の画面を書き出した MP4 が返ってきました。</p><blockquote><p>アイコンはこのハコブくん使えばいいんじゃないの?</p></blockquote><p>動画で話しているハコブくんと、アイコンのハコブくんが同じ見た目なら、アイコンから動画を開いた人に同じキャラクターだと伝わります。コードで描いた顔は、仕様には合っていても、動画のハコブくんとは別物でした。</p><p>動画(1920×1080px)を1秒に3コマずつ書き出して、表情を見比べ、口を閉じて笑っているコマを選びました。ハコブくんは画面の左下にいて、大きさは横幅が約320pxです。画面の右側にある出典の文字などが入らないよう、ハコブくんの範囲の外は背景色で塗りつぶしてから切り抜いています。案は3つです。</p><ul><li><p>A:動画のまま。上半身をクリーム地に置く</p></li><li><p>B:A にオレンジの輪を付ける。ダークモードでもライトモードでも輪郭が見える</p></li><li><p>C:顔に寄せる</p></li></ul><p></p><figure><img src=\"/media/s-1500x940_v-fms_webp_27d8616b-248f-424b-bbc8-265007ee2c33.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>各案は、YouTube の表示サイズ(200px・98px・48px・24px)で、ダークモードとライトモードの背景に並べて確認しました。チャンネルには A を設定しています。元が動画の1コマなので、800×800px にするために約2倍に拡大しています。</p><p></p><h2 id=\"index_BZlJ8lML\">バナーの3回目は、ハコブくんだけで5つのタッチを作った</h2><p>次の依頼は、ハコブくんだけを使って、タッチの違う5種類を作ることでした。</p><blockquote><p>イラスト系、4コマまんが風、写実的、写真系、グラフィックデザインの5種類</p></blockquote><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>タッチ</p></th><th colspan=\"1\" rowspan=\"1\"><p>作り方</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>イラスト系</p></td><td colspan=\"1\" rowspan=\"1\"><p>病院の廊下とハコブくんを、線のないフラットな絵としてコードで描く</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4コマまんが風</p></td><td colspan=\"1\" rowspan=\"1\"><p>動画のハコブくんを白黒のトーン調に変え、表情の違う4コマを文字なしで並べる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>写実的</p></td><td colspan=\"1\" rowspan=\"1\"><p>動画の3Dのハコブくんを、表情を変えて5体並べる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>写真系</p></td><td colspan=\"1\" rowspan=\"1\"><p>病院の廊下にいるハコブくんの写真</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>グラフィックデザイン</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの顔のディスプレイを大きく置き、ピクセルの崩れ・寸法線・緑のリングを添える</p></td></tr></tbody></table></div><p></p><figure><img src=\"/media/s-1360x1165_v-fms_webp_6153354f-d317-4c3b-9562-e9b084372e61.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>4コマまんが風は、動画のコマを次の手順で白黒にしています。輪郭を検出して黒い線にする。暗い部分(明るさ70未満)は黒で塗る。中間の明るさ(215未満)は、7px 間隔の網点にする。それ以外は白にする。</p><p>返事は次のとおりでした。</p><blockquote><p>イラスト系か4コママンガ風がいいけど、イラストはハコブくんじゃないし、4コママンガ風はハコブくんだけしか描かれてない。</p></blockquote><p>イラスト系のハコブくんは、Claude が写真を見ながら比率を推測して描いたもので、実物と形が違いました。4コマまんが風は、4コマとも同じハコブくんで、漫画に出てくる他の登場人物がいませんでした。</p><h3 id=\"index_elH2fOh3\">ハコブくんの三面図を受け取り、寸法を読み取った</h3><p>ここで、ハコブくんの三面図(正面・側面・背面を漫画のタッチで描いたもの)を受け取りました。</p><p></p><figure><img src=\"/media/s-1080x1350_v-fms_webp_b8890bd5-e649-4af1-9361-84759664f745.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>画像に座標の目盛りを重ねて、正面図の各部の位置を読み取りました(単位は三面図の画像上の px)。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>部分</p></th><th colspan=\"1\" rowspan=\"1\"><p>位置</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>全体</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 70〜331、縦 415〜874(高さ約460)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>頭の外装</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 100〜300、上端 413</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ディスプレイ</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 120〜283、縦 427〜541</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>目</p></td><td colspan=\"1\" rowspan=\"1\"><p>中心が横 152 と 241、縦 486。幅 22、高さ 40</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>口</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 184〜215、縦 490〜505</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>胴</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 76〜324、縦 505〜816</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>引き出しのスリット</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 167〜225、縦 606〜630</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>黒い帯</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 78〜322、縦 767〜786</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>台座</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 72〜328、縦 806〜848</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>車輪</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 95〜120 と 280〜305、縦 836〜874</p></td></tr></tbody></table></div><p>側面図のリングは、中心が(554, 663)、内側の線が半径約51、外側の線が半径約62でした。リングの位置は、中心の候補を動かしながら半径ごとの平均の明るさを測り、暗い線が2本くっきり出る点を探して決めています。</p><h3 id=\"index_nkmV04Yr\">4回目は、三面図そのものと、他の登場人物を使った</h3><p>三面図のハコブくんを使って、イラスト系と4コマまんが風を2案ずつ作りました。</p><ul><li><p>イラスト系 A:方眼の上に正面・背面・側面を並べ、寸法線と、顔とリングの拡大図を添える</p></li><li><p>イラスト系 B:線画に淡い色を付けた廊下を、三面図の側面のハコブくんが走る</p></li><li><p>4コマまんが風 A:斜めのコマ割りで、中央にハコブくん、左右にツカムくん(顔の付いたアーム)、エンジニア、看護師</p></li><li><p>4コマまんが風 B:ハコブくんを中央の爆発形の枠に置き、他の登場人物を丸い枠で並べる</p></li></ul><p></p><figure><img src=\"/media/s-1360x936_v-fms_webp_2febf659-d311-4869-ace6-89ed02c61b14.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>返事は次のとおりでした。</p><blockquote><p>イラストは背景のイラストタッチにハコブくんを修正しないとダメだよ(イラストと漫画風を一緒にしたらNG)</p><p>4コマまんがの処理は前の方が良かった。このタッチのまま、登場するキャラを入れ替えればいいだけです。</p></blockquote><p>イラスト系 B は、背景は線画に色を付けたイラストなのに、ハコブくんだけ漫画のトーンのままでした。1枚の絵の中で、タッチが2種類混ざっていたことになります。4コマまんが風は、コマ割りを変えたことで、3回目で良かった処理(輪郭線と網点)が失われていました。</p><p></p><h2 id=\"index_aoA9mNR9\">5回目で、ハコブくんを背景と同じタッチで描き直した</h2><p>イラスト系は、三面図から読み取った寸法を使い、ハコブくんを背景と同じタッチで描き直しました。三面図の座標をそのまま渡すと、バナーの上の位置と大きさに変換して描く仕組みです(スクリプトの <code>Pen</code> クラス)。線のない背景には線のないハコブくん、黒い輪郭線の背景には同じ太さの輪郭線のハコブくん、と2つのタッチで作りました。</p><p>4コマまんが風は、3回目の処理と並び(4コマを横一列・両側に集中線)に戻し、コマごとの登場人物を入れ替えました。左からハコブくん、看護師(「!」)、エンジニア(汗)、ツカムくん(キラキラ)です。看護師たちは元が線画なので、3回目の処理(輪郭検出)をそのままかけると全体が黒くつぶれました。線画には輪郭検出をかけず、明るさだけで黒・網点・白に分ける処理にしています。網点の間隔は、ハコブくんのコマと同じ 7px にそろえました。</p><p></p><figure><img src=\"/media/s-1360x707_v-fms_webp_3a2d5396-3c1f-4313-b446-6381c5f222fe.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>決まったのは、線のないフラットなイラストでした。</p><blockquote><p>イラスト系、ハコブくんは1体だけでいいよ。タッチはこれがいいかな。</p></blockquote><p>横向きのハコブくんと動きの線を消し、正面のハコブくんだけをドアとドアの間に立たせて完成にしました。ハコブくんは、顔から足元まで、どの端末でも切れない範囲に収まっています。</p><p></p><figure><img src=\"/media/s-2048x1152_v-frms_webp_9fd7c7eb-5d31-479a-bef5-522b73fab189.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>1回目から5回目までに出したバナーは次のとおりです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>回</p></th><th colspan=\"1\" rowspan=\"1\"><p>案の数</p></th><th colspan=\"1\" rowspan=\"1\"><p>主な中身</p></th><th colspan=\"1\" rowspan=\"1\"><p>返ってきた判断</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>実機の写真+文字</p></td><td colspan=\"1\" rowspan=\"1\"><p>文字は要らない。同じアームだけで幅が狭い</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>7</p></td><td colspan=\"1\" rowspan=\"1\"><p>サイトの画像から6方向+1回目の写真(文字なし)</p></td><td colspan=\"1\" rowspan=\"1\"><p>方向の幅を確認</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんだけで5つのタッチ</p></td><td colspan=\"1\" rowspan=\"1\"><p>イラストと4コマが良い。ただし形が違う/他の人物がいない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>三面図と他の登場人物</p></td><td colspan=\"1\" rowspan=\"1\"><p>タッチを混ぜない。4コマは3回目の処理に戻す</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>3+1</p></td><td colspan=\"1\" rowspan=\"1\"><p>タッチをそろえて描き直し</p></td><td colspan=\"1\" rowspan=\"1\"><p>フラットのイラスト、ハコブくん1体で決定</p></td></tr></tbody></table></div><p></p><h2 id=\"index_8PXH6omo\">チャンネル名は「フィジカルAI解説|Pixels to Motors」を候補にした</h2><p>チャンネル名は、ブランド名の Pixels to Motors を残したまま、何のチャンネルかが分かる言葉を足す案にしました。Pixels to Motors のルールで、外に出すブランド名は Pixels to Motors だけと決めているためです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>案</p></th><th colspan=\"1\" rowspan=\"1\"><p>名前</p></th><th colspan=\"1\" rowspan=\"1\"><p>良い点</p></th><th colspan=\"1\" rowspan=\"1\"><p>気になる点</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>Pixels to Motors|フィジカルAI解説</p></td><td colspan=\"1\" rowspan=\"1\"><p>ブランド名が先に来る</p></td><td colspan=\"1\" rowspan=\"1\"><p>少し長い</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィジカルAI解説|Pixels to Motors</p></td><td colspan=\"1\" rowspan=\"1\"><p>日本語が先に来て、内容が一番早く伝わる</p></td><td colspan=\"1\" rowspan=\"1\"><p>幅が狭い画面ではブランド名が切れやすい</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんのフィジカルAI解説</p></td><td colspan=\"1\" rowspan=\"1\"><p>覚えやすく、アイコンと一体になる</p></td><td colspan=\"1\" rowspan=\"1\"><p>ブランド名と、動画を作っている本人が見えなくなる</p></td></tr></tbody></table></div><p></p><figure><img src=\"/media/s-1500x690_v-fms_webp_29ae9ada-591e-4c09-92a0-8b1d5224d689.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>選ばれたのは2の並びでした。そのうえで、次の質問がありました。</p><blockquote><p>ロボティクスとフィジカルAIでどちらがタイトルの言葉として使うのがいい?</p></blockquote><p>Claude の答えは「フィジカルAI」です。理由は3つです。動画を届けたい相手(ロボットに関心のある経営者・PM・マネージャー)が、ニュースや業界の発表で目にしている言葉であること。動画の中身が「AIがロボットを動かす」話で、ロボティクス全般より範囲がはっきりすること。サイトの課題地図や研究室のシリーズ名でも「フィジカルAI」を使っていること。流行りの言葉なので数年後に古く聞こえるおそれはありますが、チャンネル名は後から変えられ、ハンドル(@PixelstoMotors)は変わりません。</p><p>チャンネル名の変更は、記事を書いている時点ではまだ行っていません。</p><p></p><h2 id=\"index_mtwyUJht\">チャンネル名の変更と、アイコン・4コマの作り直しが残っている</h2><ul><li><p>チャンネル名を「フィジカルAI解説|Pixels to Motors」に変える</p></li><li><p>新しい説明文がチャンネルページに反映されたかを確かめる</p></li><li><p>新しいアカウントに2段階認証を設定する</p></li><li><p>アイコンは動画の1コマを約2倍に拡大している。元の切り抜き画像(動画の制作ベースのフォルダにある)から作り直すと、もっと鮮明になる</p></li><li><p>4コマまんが風の看護師たちは、サイトの画面を保存した PDF から切り出した。採用するなら元の漫画画像で作り直す</p></li></ul><p>細かい修正は、動画をアップしながら進める予定です。</p><p></p><h2 id=\"index_LGOmRd2E\">制作スクリプトの全文</h2><p>バナーとアイコンは、すべて Python(Pillow と NumPy)で作りました。作業フォルダは <code>/home/claude/banner/</code> です。スクリプトは作った順に載せています。採用したバナーは make5_single.py、採用したアイコンは hakobu_icon.py の出力です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>ファイル</p></th><th colspan=\"1\" rowspan=\"1\"><p>作ったもの</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>1回目:実機の写真に文字を入れた3案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make2.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>2回目:サイトの画像から作った6案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>icons.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイコン:ハコブくんの顔をコードで描いた3案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>hakobu_icon.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイコン:動画の1コマから作った3案(採用)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make3.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>3回目:ハコブくんだけで5つのタッチ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make4.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>4回目:三面図と他の登場人物を使った4案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make5.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>5回目:タッチをそろえた3案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make5_single.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>決定版:フラットのイラスト、ハコブくん1体(採用)</p></td></tr></tbody></table></div><h3 id=\"index_LAOak7lY\">make.py(1回目の3案)</h3><pre><code class=\"language-python\">import numpy as np\nfrom PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageOps, ImageEnhance\n\nSRC = '/mnt/user-data/uploads/r2r2r-content/articles/'\nOUT = '/home/claude/banner/out/'\nW, H = 2560, 1440\nSAFE = (507, 508, 2053, 932) # x0,y0,x1,y1 (1546 x 423 visible on every device)\n\nCJK_B = '/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc'\nCJK_M = '/usr/share/fonts/opentype/noto/NotoSansCJK-Medium.ttc'\nINTER = '/usr/share/fonts/opentype/inter/'\n\ndef f_cjk(size, bold=True):\n return ImageFont.truetype(CJK_B if bold else CJK_M, size, index=0)\n\ndef f_inter(size, w='SemiBold'):\n return ImageFont.truetype(INTER + f'Inter-{w}.otf', size)\n\ndef tracked(draw, xy, text, font, fill, track=0):\n x, y = xy\n for ch in text:\n draw.text((x, y), ch, font=font, fill=fill)\n x += draw.textlength(ch, font=font) + track\n return x\n\ndef load(name):\n return ImageOps.exif_transpose(Image.open(SRC + name)).convert('RGB')\n\ndef place(canvas, img, scale, src_pt, dst_pt):\n \"\"\"paste img scaled so that src_pt (in img coords) lands on dst_pt\"\"\"\n im = img.resize((round(img.width * scale), round(img.height * scale)), Image.LANCZOS)\n ox = round(dst_pt[0] - src_pt[0] * scale)\n oy = round(dst_pt[1] - src_pt[1] * scale)\n layer = Image.new('RGB', (W, H))\n layer.paste(im, (ox, oy))\n return layer, (ox, oy, im.width, im.height)\n\ndef rect_mask(box, feather):\n m = Image.new('L', (W, H), 0)\n ImageDraw.Draw(m).rectangle(box, fill=255)\n return m.filter(ImageFilter.GaussianBlur(feather))\n\ndef hgrad(x0, x1, a0, a1):\n \"\"\"horizontal alpha ramp, a0 at x<=x0 to a1 at x>=x1\"\"\"\n xs = np.clip((np.arange(W) - x0) / max(1, x1 - x0), 0, 1)\n row = (a0 + (a1 - a0) * xs).astype(np.uint8)\n return Image.fromarray(np.tile(row, (H, 1)))\n\ndef pixel_squares(draw, x, y, s, gap, cols):\n for i, c in enumerate(cols):\n draw.rectangle((x + i * (s + gap), y, x + i * (s + gap) + s, y + s), fill=c)\n\nRED, BLUE = (214, 52, 52), (36, 128, 214)\nMAT = (30, 27, 31)\n\nphoto = load('IMG_7933.jpg') # top-down: mechArm on cutting mat, red & blue cubes\nMAT_BOX = (697, 639, 3211, 2471) # cutting-mat area in full-res coords\n\n# ---------------------------------------------------------------- A: GRASP\ndef banner_a():\n bg = Image.new('RGB', (W, H), MAT)\n subj = ((1282 + 2650) / 2, 1790) # midpoint between the two cubes, gripper level\n layer, _ = place(bg, photo, 0.55, subj, (1530, 728))\n # keep only the mat, fade its edges into the background\n s = 0.55\n ox = 1530 - subj[0] * s; oy = 728 - subj[1] * s\n mb = (ox + MAT_BOX[0] * s + 70, oy + MAT_BOX[1] * s + 70,\n ox + MAT_BOX[2] * s - 70, oy + MAT_BOX[3] * s - 70)\n m = rect_mask(mb, 60)\n layer = ImageEnhance.Brightness(layer).enhance(0.92)\n bg.paste(layer, (0, 0), m)\n # darken left for the type\n shade = Image.new('RGB', (W, H), MAT)\n bg.paste(shade, (0, 0), hgrad(900, 1080, 235, 0))\n d = ImageDraw.Draw(bg)\n x = SAFE[0] + 20\n pixel_squares(d, x, 586, 18, 8, [RED, BLUE, (235, 235, 235)])\n tracked(d, (x + 92, 580), 'PIXELS TO MOTORS', f_inter(28, 'SemiBold'), (190, 186, 194), track=6)\n d.text((x, 640), 'フィジカルAIを、', font=f_cjk(68), fill=(245, 245, 245))\n d.text((x, 728), '現場で使える形に。', font=f_cjk(68), fill=(245, 245, 245))\n d.text((x, 836), 'Making Physical AI work in the real world.', font=f_inter(26, 'Regular'), fill=(160, 156, 166))\n return bg\n\n# ---------------------------------------------------------------- B: PIXELS → MOTORS (dissolve)\ndef banner_b():\n rot = photo.rotate(90, expand=True) # CCW: robot base on the left, gripper reaching right\n # mapping for CCW 90 with expand: (x, y) -> (y, W0 - x)\n W0 = photo.width\n def r(pt): return (pt[1], W0 - pt[0])\n base = r((2010, 700)); tips = r((2010, 2045))\n # arm axis: y = W0 - 2010 ; x from 700 to 2045\n s = 0.50\n layer, (ox, oy, iw, ih) = place(None, rot, s, (tips[0], tips[1]), (2030, 720))\n # mat region after rotation\n mb0 = (MAT_BOX[1], W0 - MAT_BOX[2], MAT_BOX[3], W0 - MAT_BOX[0])\n mb = (ox + mb0[0] * s + 70, oy + mb0[1] * s + 70, ox + mb0[2] * s - 70, oy + mb0[3] * s - 70)\n bg = Image.new('RGB', (W, H), MAT)\n bg.paste(layer, (0, 0), rect_mask(mb, 50))\n # progressive pixelation, left -> right, in vertical strips\n arr = np.array(bg)\n out = arr.copy()\n sizes = [96, 84, 72, 64, 56, 48, 40, 34, 28, 22, 16, 11, 7, 4]\n x_start, x_end = 0, 1900\n sw = (x_end - x_start) / len(sizes)\n for i, b in enumerate(sizes):\n xa, xb = int(x_start + i * sw), int(x_start + (i + 1) * sw)\n small = bg.resize((max(1, W // b), max(1, H // b)), Image.BOX).resize(\n (W // b * b, H // b * b), Image.NEAREST)\n sm = np.zeros_like(arr); sa = np.array(small)\n sm[:sa.shape[0], :sa.shape[1]] = sa\n out[:, xa:xb] = sm[:, xa:xb]\n bg = Image.fromarray(out)\n shade = Image.new('RGB', (W, H), MAT)\n bg.paste(shade, (0, 0), hgrad(760, 1060, 205, 0))\n d = ImageDraw.Draw(bg)\n # detection-style bracket around the gripper (perception -> action)\n gx, gy, hw, hh = 1932, 720, 108, 118\n c = (120, 200, 255); L = 34; t = 4\n for sx in (-1, 1):\n for sy in (-1, 1):\n px, py = gx + sx * hw, gy + sy * hh\n d.line((px, py, px - sx * L, py), fill=c, width=t)\n d.line((px, py, px, py - sy * L), fill=c, width=t)\n tracked(d, (gx - hw, gy - hh - 44), 'GRASP', f_inter(24, 'SemiBold'), c, track=5)\n x = SAFE[0] + 20\n d.text((x, 572), 'Pixels to Motors', font=ImageFont.truetype(INTER + 'InterDisplay-Bold.otf', 88), fill=(248, 248, 248))\n d.text((x, 696), 'フィジカルAIを、現場で使える形にするまで。', font=f_cjk(38, bold=False), fill=(225, 222, 228))\n tracked(d, (x, 786), 'PIXELS → PERCEPTION → MOTION', f_inter(26, 'Medium'), (150, 146, 156), track=4)\n return bg\n\n# ---------------------------------------------------------------- C: SEE / DECIDE / MOVE (light)\ndef banner_c():\n NAVY = (26, 35, 50); BG = (244, 246, 248); GRID = (226, 231, 236)\n bg = Image.new('RGB', (W, H), BG)\n d = ImageDraw.Draw(bg)\n for gx in range(0, W, 40):\n d.line((gx, 0, gx, H), fill=GRID if gx % 200 else (214, 221, 228), width=1)\n for gy in range(0, H, 40):\n d.line((0, gy, W, gy), fill=GRID if gy % 200 else (214, 221, 228), width=1)\n # panel sources\n cubes = load('IMG_7912.JPG') # red & blue cubes, white paper\n crop = (1500, 700, 3500, 2700)\n p1 = cubes.crop(crop)\n hsv = np.array(p1.convert('HSV')).astype(int)\n rgb = np.array(p1).astype(int)\n sat = hsv[..., 1] > 110\n red = sat & ((hsv[..., 0] < 12) | (hsv[..., 0] > 235)) & (rgb[..., 0] > rgb[..., 2])\n blue = sat & (hsv[..., 0] > 120) & (hsv[..., 0] < 170)\n mask = np.zeros(rgb.shape, np.uint8)\n mask[red] = (255, 90, 90); mask[blue] = (90, 170, 255)\n p2 = Image.fromarray(mask).filter(ImageFilter.MedianFilter(7))\n # bounding boxes on mask\n d2 = ImageDraw.Draw(p2)\n for m, col in ((red, (255, 90, 90)), (blue, (90, 170, 255))):\n ys, xs = np.where(m)\n if len(xs):\n x0, x1 = np.percentile(xs, [1, 99]); y0, y1 = np.percentile(ys, [1, 99])\n d2.rectangle((x0 - 30, y0 - 30, x1 + 30, y1 + 30), outline=col, width=14)\n p3 = photo.crop((1100, 1250, 2900, 2250)) # gripper between the cubes\n P = 290; gap = 44\n x0 = 1062; y0 = 540\n panels = [(p1, '01', '見る', 'SEE'), (p2, '02', '判断する', 'DECIDE'), (p3, '03', '動かす', 'MOVE')]\n for i, (im, n, jp, en) in enumerate(panels):\n px = x0 + i * (P + gap)\n tile = ImageOps.fit(im, (P, P), Image.LANCZOS)\n shadow = Image.new('L', (W, H), 0)\n ImageDraw.Draw(shadow).rectangle((px + 6, y0 + 10, px + P + 6, y0 + P + 10), fill=70)\n bg.paste((180, 190, 200), (0, 0), shadow.filter(ImageFilter.GaussianBlur(12)))\n bg.paste(tile, (px, y0))\n d = ImageDraw.Draw(bg)\n d.text((px, y0 + P + 20), n, font=f_inter(24, 'SemiBold'), fill=(138, 155, 168))\n d.text((px + 44, y0 + P + 14), jp, font=f_cjk(30), fill=NAVY)\n tw = d.textlength(en, font=f_inter(22, 'Medium'))\n tracked(d, (px + P - tw - 12, y0 + P + 22), en, f_inter(22, 'Medium'), (138, 155, 168), track=3)\n if i < 2:\n ax = px + P + gap // 2; ay = y0 + P // 2\n d.polygon([(ax - 9, ay - 12), (ax + 9, ay), (ax - 9, ay + 12)], fill=NAVY)\n d = ImageDraw.Draw(bg)\n x = SAFE[0] + 20\n pixel_squares(d, x, 598, 16, 7, [NAVY, RED, BLUE])\n tracked(d, (x + 82, 592), 'PIXELS TO MOTORS', f_inter(26, 'SemiBold'), (90, 105, 120), track=6)\n d.text((x, 646), 'フィジカルAIを、', font=f_cjk(58), fill=NAVY)\n d.text((x, 722), '現場で使える形に。', font=f_cjk(58), fill=NAVY)\n d.text((x, 818), 'Experience first.', font=f_inter(30, 'Medium'), fill=(184, 90, 90))\n d.text((x, 856), 'Precision can be bought later.', font=f_inter(30, 'Regular'), fill=(110, 125, 140))\n return bg\n\ndef preview(img, name):\n \"\"\"safe-area overlay + desktop/mobile crops for checking\"\"\"\n ov = img.copy(); d = ImageDraw.Draw(ov, 'RGBA')\n d.rectangle((0, 0, W, SAFE[1]), fill=(0, 0, 0, 110)); d.rectangle((0, SAFE[3], W, H), fill=(0, 0, 0, 110))\n d.rectangle((0, SAFE[1], SAFE[0], SAFE[3]), fill=(0, 0, 0, 60)); d.rectangle((SAFE[2], SAFE[1], W, SAFE[3]), fill=(0, 0, 0, 60))\n d.rectangle(SAFE, outline=(255, 220, 0, 255), width=4)\n ov.resize((1280, 720)).save(OUT + f'check_{name}_overlay.jpg', quality=88)\n img.crop((0, SAFE[1], W, SAFE[3])).resize((1280, 212)).save(OUT + f'check_{name}_desktop.jpg', quality=88)\n img.crop(SAFE).save(OUT + f'check_{name}_mobile.jpg', quality=90)\n\nimport os\nos.makedirs(OUT, exist_ok=True)\nfor name, fn in (('A_grasp', banner_a), ('B_pixels_to_motors', banner_b), ('C_see_decide_move', banner_c)):\n im = fn()\n im.save(OUT + f'pixelstomotors_banner_{name}.jpg', quality=92, subsampling=0)\n preview(im, name)\n print(name, os.path.getsize(OUT + f'pixelstomotors_banner_{name}.jpg') // 1024, 'KB')</code></pre><h3 id=\"index_MFH88eN7\">make2.py(2回目の6案)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps\n\nS = '/home/claude/banner/site/'\nOUT = '/home/claude/banner/out2/'\nos.makedirs(OUT, exist_ok=True)\nW, H = 2560, 1440\nSAFE = (507, 508, 2053, 932)\nCY = 720\nINK = (17, 17, 17); CREAM = (242, 241, 236); GRAY = (152, 152, 148)\n\ndef L(name): return Image.open(S + f'c_{name}.png').convert('RGB')\n\ndef rounded(im, r):\n m = Image.new('L', im.size, 0)\n ImageDraw.Draw(m).rounded_rectangle((0, 0, im.width - 1, im.height - 1), r, fill=255)\n return m\n\ndef card(canvas, im, box, r=14, shadow=True):\n x0, y0, x1, y1 = box\n tile = ImageOps.fit(im, (x1 - x0, y1 - y0), Image.LANCZOS)\n if shadow:\n sh = Image.new('L', (W, H), 0)\n ImageDraw.Draw(sh).rounded_rectangle((x0 + 4, y0 + 12, x1 + 4, y1 + 12), r, fill=60)\n canvas.paste((150, 150, 150), (0, 0), sh.filter(ImageFilter.GaussianBlur(16)))\n canvas.paste(tile, (x0, y0), rounded(tile, r))\n\n# ------------------------------------------------ 1. Archive wall\ndef archive():\n c = Image.new('RGB', (W, H), (236, 236, 233))\n rows = [\n ['inside', 'panels320', 'gpu', 'panels324', 'film', 'rec_say'],\n ['manga2', 'lab', 'manga3', 'sim', 'manga0', 'drug', 'manga1'],\n ['who03', 'panels319', 'bowl', 'panels323', 'imgrec_top', 'rec_micro'],\n ]\n hs = [300, 390, 300]\n ys = [CY - 195 - 36 - 300, CY - 195, CY + 195 + 36]\n offs = [-170, -60, -250]\n for row, h, y, off in zip(rows, hs, ys, offs):\n x = off\n while x < W:\n for n in row:\n im = L(n)\n w = int(h * im.width / im.height)\n if n.startswith('manga'): w = int(h * 1.05)\n card(c, im, (x, y, x + w, y + h))\n x += w + 36\n if x >= W: break\n return c\n\n# ------------------------------------------------ 2. Mono split (vector)\ndef split():\n c = Image.new('RGB', (W, H), CREAM)\n d = ImageDraw.Draw(c)\n mid = 1240\n d.rectangle((0, 0, mid, H), fill=INK)\n # pixel dissolve across the seam: black squares scattered into cream, cream squares into black\n random.seed(7); cell = 44\n for gx in range(mid - cell * 10, mid + cell * 14, cell):\n for gy in range(0, H, cell):\n t = (gx - mid) / (cell * 12)\n if gx >= mid and random.random() < max(0, 0.9 - t):\n d.rectangle((gx + 3, gy + 3, gx + cell - 3, gy + cell - 3), fill=INK)\n if gx < mid and random.random() < max(0, 0.75 + t * 1.4):\n d.rectangle((gx + 3, gy + 3, gx + cell - 3, gy + cell - 3), fill=CREAM)\n # hatched block (top-left of band) and dot grids\n hx0, hy0, hx1, hy1 = 560, 540, 760, 740\n hl = Image.new('RGB', (W, H), INK); hd = ImageDraw.Draw(hl)\n for k in range(-220, 240, 16):\n hd.line((hx0 + k, hy1, hx0 + k + 200, hy0), fill=CREAM, width=4)\n hm = Image.new('L', (W, H), 0); ImageDraw.Draw(hm).rectangle((hx0, hy0, hx1, hy1), fill=255)\n c.paste(hl, (0, 0), hm)\n for gx in range(1800, 2040, 22):\n for gy in range(540, 700, 22):\n d.ellipse((gx - 3, gy - 3, gx + 3, gy + 3), fill=INK)\n for gx in range(560, 760, 22):\n for gy in range(800, 900, 22):\n d.ellipse((gx - 3, gy - 3, gx + 3, gy + 3), fill=(120, 120, 120))\n # half-hatched circle (right)\n cx, cy, r = 1700, 735, 175\n circ = Image.new('L', (W, H), 0); ImageDraw.Draw(circ).ellipse((cx - r, cy - r, cx + r, cy + r), fill=255)\n hatch = Image.new('RGB', (W, H), CREAM); hd = ImageDraw.Draw(hatch)\n for k in range(-400, 400, 18):\n hd.line((cx + k - 200, cy + 200, cx + k + 200, cy - 200), fill=INK, width=5)\n half = Image.new('L', (W, H), 0); ImageDraw.Draw(half).rectangle((cx, 0, W, H), fill=255)\n m = Image.fromarray((np.array(circ) & np.array(half)).astype(np.uint8))\n c.paste(hatch, (0, 0), m)\n d.ellipse((cx - r, cy - r, cx + r, cy + r), outline=INK, width=6)\n for a in (0, 90, 180, 270):\n px = cx + r * math.cos(math.radians(a)); py = cy + r * math.sin(math.radians(a))\n d.ellipse((px - 12, py - 12, px + 12, py + 12), fill=INK)\n # dotted signal line running from black side into cream (noisy -> settled)\n random.seed(3)\n pts = []\n for i, x in enumerate(range(520, 2060, 60)):\n amp = 90 * max(0, 1 - (x - 520) / 1200)\n pts.append((x, 870 - random.uniform(-1, 1) * amp))\n for (x0, y0), (x1, y1) in zip(pts, pts[1:]):\n steps = int(math.hypot(x1 - x0, y1 - y0) // 14)\n for s in range(steps):\n t = s / steps; px = x0 + (x1 - x0) * t; py = y0 + (y1 - y0) * t\n col = CREAM if px < mid else INK\n d.ellipse((px - 2.5, py - 2.5, px + 2.5, py + 2.5), fill=col)\n for x, y in pts:\n col = CREAM if x < mid else INK\n d.ellipse((x - 9, y - 9, x + 9, y + 9), fill=INK if x >= mid else INK, outline=col, width=4)\n return c\n\n# ------------------------------------------------ 3. Simulation halftone\ndef halftone():\n src = ImageOps.autocontrast(L('sim').convert('L'), cutoff=1)\n sc = W / src.width; bh = int(src.height * sc)\n big = src.resize((W, bh), Image.BICUBIC)\n top = int(0.30 * bh) - SAFE[1] - 340\n full = Image.new('L', (W, H), 0)\n full.paste(big, (0, -top))\n a = np.array(full).astype(float) / 255\n c = Image.new('RGB', (W, H), CREAM); d = ImageDraw.Draw(c)\n step = 10\n for y in range(0, H, step):\n for x in range((y // step % 2) * step // 2, W, step):\n v = a[y, min(W - 1, x)]\n r = v ** 1.6 * step * 0.66\n if r > 0.6:\n d.ellipse((x - r, y - r, x + r, y + r), fill=INK)\n return c\n\n# ------------------------------------------------ 4. Manga strip\ndef manga():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n # screentone background (fading dots towards the centre)\n for y in range(0, H, 16):\n for x in range(0, W, 16):\n dist = abs(x - W / 2) / (W / 2)\n r = 3.2 * max(0, dist - 0.35) / 0.65\n if r > 0.4:\n d.ellipse((x - r, y - r, x + r, y + r), fill=(200, 200, 198))\n order = ['manga3', 'manga0', 'manga2', 'manga1']\n ph = 372; pw = 366; gap = 22\n total = 4 * pw + 3 * gap\n x = (W - total) // 2; y = CY - ph // 2\n for n in order:\n im = L(n)\n tile = ImageOps.fit(im, (pw, ph), Image.LANCZOS, centering=(0.5, 0.6))\n c.paste(tile, (x, y))\n d.rectangle((x - 5, y - 5, x + pw + 4, y + ph + 4), outline=INK, width=6)\n x += pw + gap\n # speed lines on both sides\n for k in range(26):\n yy = y + 10 + k * (ph - 20) / 25\n ln = 120 + (k * 37 % 5) * 60\n d.line((x + 30, yy, x + 30 + ln, yy), fill=INK, width=3)\n x0 = (W - total) // 2 - 40\n d.line((x0 - ln, yy, x0, yy), fill=INK, width=3)\n return c\n\n# ------------------------------------------------ 5. Dot matrix\ndef dots():\n c = Image.new('RGB', (W, H), CREAM); d = ImageDraw.Draw(c)\n D = 82; G = 24; P = D + G\n random.seed(36)\n cols = W // P + 2; rows = H // P + 4\n ox = (W - cols * P) // 2 + G // 2; oy = SAFE[1] + G // 2 + 2 - P * 6\n for j in range(rows):\n for i in range(cols):\n x = ox + i * P; y = oy + j * P\n t = i / cols\n r = random.random()\n # left: mostly solved (black), middle: in progress (gray), right: open (outline)\n pb = max(0, 0.75 - t * 1.3); pg = 0.55 - abs(t - 0.5) * 0.6\n if r < pb: d.ellipse((x, y, x + D, y + D), fill=INK)\n elif r < pb + pg: d.ellipse((x, y, x + D, y + D), fill=GRAY)\n else: d.ellipse((x + 2, y + 2, x + D - 2, y + D - 2), outline=INK, width=4)\n return c\n\n# ------------------------------------------------ 6. Hakobu photo\ndef hakobu():\n src = L('drug')\n sw = int(src.width * 0.30)\n left = src.crop((0, 0, sw, src.height)); right = src.crop((src.width - sw, 0, src.width, src.height))\n pano = Image.new('RGB', (sw * 2, src.height)); pano.paste(left, (0, 0)); pano.paste(right, (sw, 0))\n bgim = pano.resize((W, H), Image.BICUBIC).filter(ImageFilter.GaussianBlur(30))\n bgim = Image.blend(bgim, Image.new('RGB', (W, H), (238, 236, 232)), 0.25)\n # sharp copy: robot fits inside the band\n h = 470; s = h / src.height; w = int(src.width * s)\n sm = src.resize((w, h), Image.LANCZOS)\n x = 1500 - w // 2; y = CY - h // 2 + 12\n m = Image.new('L', (w, h), 0)\n ImageDraw.Draw(m).rectangle((60, 45, w - 60, h - 45), fill=255)\n m = m.filter(ImageFilter.GaussianBlur(45))\n bgim.paste(sm, (x, y), m)\n return bgim\n\ndef preview(img, name):\n img.crop((0, SAFE[1], W, SAFE[3])).save(OUT + f'_band_{name}.png')\n\nitems = [('B_archive_wall', archive), ('C_mono_split', split), ('D_sim_halftone', halftone),\n ('E_hakobu_manga', manga), ('F_dot_matrix', dots), ('G_hakobu_photo', hakobu)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'pixelstomotors_banner_{name}.jpg', quality=92, subsampling=0)\n preview(im, name)\n print(name, os.path.getsize(OUT + f'pixelstomotors_banner_{name}.jpg') // 1024, 'KB')</code></pre><h3 id=\"index__zJBabGH\">icons.py(コードで描いたアイコン3案)</h3><pre><code class=\"language-python\">import os, random\nfrom PIL import Image, ImageDraw, ImageFilter\n\nOUT = '/home/claude/banner/icons/'\nos.makedirs(OUT, exist_ok=True)\nSS = 4; N = 800 * SS\nSHELL = (246, 245, 241); SHELL_EDGE = (214, 214, 208)\nDISP = (16, 16, 18); EYE = (246, 248, 252); MOUTH = (87, 181, 248)\nCREAM = (245, 245, 240); ORANGE = (196, 71, 31); INK = (13, 13, 13)\n\ndef face(img, box):\n \"\"\"draw display + eyes + smile inside box (x0,y0,x1,y1)\"\"\"\n x0, y0, x1, y1 = box; w = x1 - x0; h = y1 - y0\n d = ImageDraw.Draw(img)\n d.rounded_rectangle(box, radius=int(w * 0.16), fill=DISP)\n ew, eh = w * 0.13, h * 0.36\n cy = y0 + h * 0.42\n # glow\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gd = ImageDraw.Draw(glow)\n for cx in (x0 + w * 0.31, x0 + w * 0.69):\n gd.rounded_rectangle((cx - ew * 0.75, cy - eh * 0.62, cx + ew * 0.75, cy + eh * 0.62), radius=int(ew * 0.75), fill=(246, 248, 252, 90))\n glow = glow.filter(ImageFilter.GaussianBlur(w * 0.03))\n img.alpha_composite(glow) if img.mode == 'RGBA' else img.paste(glow, (0, 0), glow)\n d = ImageDraw.Draw(img)\n for cx in (x0 + w * 0.31, x0 + w * 0.69):\n d.rounded_rectangle((cx - ew / 2, cy - eh / 2, cx + ew / 2, cy + eh / 2), radius=int(ew / 2), fill=EYE)\n # smile: thick arc with round caps\n mw = w * 0.24; my = y0 + h * 0.70; t = int(h * 0.075)\n d.arc((x0 + w / 2 - mw / 2, my - mw * 0.55, x0 + w / 2 + mw / 2, my + mw * 0.30), 25, 155, fill=MOUTH, width=t)\n import math\n for a in (25, 155):\n rx = mw / 2 - t / 2; ry = (mw * 0.85) / 2 - t / 2\n cx = x0 + w / 2 + rx * math.cos(math.radians(a)); cyy = (my - mw * 0.55 + my + mw * 0.30) / 2 + ry * math.sin(math.radians(a))\n d.ellipse((cx - t / 2, cyy - t / 2, cx + t / 2, cyy + t / 2), fill=MOUTH)\n\ndef head(img, cx, cy, w, h):\n d = ImageDraw.Draw(img)\n d.rounded_rectangle((cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2), radius=int(w * 0.22), fill=SHELL, outline=SHELL_EDGE, width=int(w * 0.012))\n m = w * 0.075\n face(img, (cx - w / 2 + m, cy - h / 2 + m, cx + w / 2 - m, cy + h / 2 - m * 1.1))\n\ndef circle_out(img, name):\n im = img.convert('RGB').resize((800, 800), Image.LANCZOS)\n im.save(OUT + f'{name}.png')\n m = Image.new('L', (800, 800), 0); ImageDraw.Draw(m).ellipse((0, 0, 799, 799), fill=255)\n c = Image.new('RGBA', (800, 800), (0, 0, 0, 0)); c.paste(im, (0, 0), m)\n c.save(OUT + f'{name}_circle.png')\n return c\n\n# 1. close-up face (shell fills the circle)\ndef i1():\n img = Image.new('RGBA', (N, N), CREAM + (255,))\n head(img, N / 2, N * 0.53, N * 0.98, N * 0.80)\n return circle_out(img, 'icon1_face')\n\n# 2. orange ground, head + top of body\ndef i2():\n img = Image.new('RGBA', (N, N), ORANGE + (255,))\n d = ImageDraw.Draw(img)\n bw = N * 0.66\n d.rounded_rectangle((N / 2 - bw / 2, N * 0.70, N / 2 + bw / 2, N * 1.2), radius=int(bw * 0.14), fill=SHELL, outline=SHELL_EDGE, width=SS * 3)\n d.rounded_rectangle((N / 2 - bw * 0.12, N * 0.80, N / 2 + bw * 0.12, N * 0.84), radius=SS * 8, fill=(40, 40, 40))\n head(img, N / 2, N * 0.43, N * 0.70, N * 0.50)\n return circle_out(img, 'icon2_orange')\n\n# 3. pixel dissolve (continuity with the current checker logo)\ndef i3():\n img = Image.new('RGBA', (N, N), INK + (255,))\n face(img, (N * 0.16, N * 0.22, N * 0.92, N * 0.80))\n d = ImageDraw.Draw(img)\n random.seed(4); cell = N // 11\n for gx in range(0, N // 2, cell):\n for gy in range(0, N, cell):\n p = 0.85 - gx / (N * 0.42)\n if random.random() < p:\n d.rectangle((gx, gy, gx + cell - SS * 6, gy + cell - SS * 6), fill=(245, 245, 240))\n return circle_out(img, 'icon3_pixel')\n\nicons = [i1(), i2(), i3()]\n\n# size check sheet on YouTube dark + light backgrounds\nfrom PIL import ImageFont\nfnt = ImageFont.truetype('/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc', 22, index=0)\nsheet = Image.new('RGB', (1500, 3 * 300 + 40), (255, 255, 255)); sd = ImageDraw.Draw(sheet)\nlabels = ['案1 顔のアップ', '案2 オレンジ地', '案3 ピクセル継承']\nfor r, (ic, lab) in enumerate(zip(icons, labels)):\n y = 20 + r * 300\n sd.text((20, y), lab, font=fnt, fill=(20, 20, 20))\n for bgc, x0 in (((15, 15, 15), 20), ((255, 255, 255), 760)):\n sd.rectangle((x0, y + 36, x0 + 720, y + 280), fill=bgc)\n x = x0 + 20\n for s in (200, 98, 48, 24):\n sheet.paste(ic.resize((s, s), Image.LANCZOS), (x, y + 58 + (200 - s) // 2), ic.resize((s, s), Image.LANCZOS))\n x += s + 40\nsheet.save('/home/claude/banner/icons_sizes.jpg', quality=90)</code></pre><h3 id=\"index_ttHgfGc2\">hakobu_icon.py(動画の1コマから作ったアイコン3案)</h3><pre><code class=\"language-python\">from PIL import Image, ImageDraw, ImageFilter, ImageFont\nfr=Image.open('fr_0.png').convert('RGB'); W0,H0=fr.size\nBG=(245,245,240); OR=(196,71,31)\nclean=Image.new('RGB',(W0,H0),BG); clean.paste(fr.crop((0,758,360,H0)),(0,758))\ndef grab(box):\n x0,y0,x1,y1=box; c=Image.new('RGB',(x1-x0,y1-y0),BG)\n c.paste(clean.crop((max(0,x0),max(0,y0),min(W0,x1),min(H0,y1))),(max(0,-x0),max(0,-y0))); return c\ndef up(c,s=800): return c.resize((s,s),Image.LANCZOS).filter(ImageFilter.UnsharpMask(2,60,2))\ndef circ(im):\n m=Image.new('L',im.size,0);ImageDraw.Draw(m).ellipse((0,0,im.width-1,im.height-1),fill=255)\n o=Image.new('RGBA',im.size,(0,0,0,0));o.paste(im,(0,0),m);return o\nA=up(grab((30,660,450,1080)))\nC=up(grab((90,690,390,990)))\n# B: orange ring around the cream picture (no cut-out edges)\ninner=up(grab((50,690,430,1070)),640)\nB=Image.new('RGB',(800,800),OR); m=Image.new('L',(640,640),0); ImageDraw.Draw(m).ellipse((0,0,639,639),fill=255)\nB.paste(inner,(80,80),m)\nout={'A_upper':A,'B_orange_ring':B,'C_face':C}\nimport glob,os\nfor p in glob.glob('../icons/hakobu_icon_*'): os.remove(p)\nfor p in glob.glob('../icons/zoomB.png'): os.remove(p)\nfor k,v in out.items():\n v.save(f'../icons/hakobu_icon_{k}.png'); circ(v).save(f'../icons/hakobu_icon_{k}_circle.png')\nf=ImageFont.truetype('/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc',22,index=0)\nlabels={'A_upper':'案A 動画のまま(上半身・クリーム地)','B_orange_ring':'案B オレンジの縁取り','C_face':'案C 顔に寄せる'}\nsh=Image.new('RGB',(1500,3*300+40),(255,255,255));d=ImageDraw.Draw(sh)\nfor r,k in enumerate(out):\n y=20+r*300; d.text((20,y),labels[k],font=f,fill=(20,20,20)); ic=circ(out[k])\n for bgc,x0 in (((15,15,15),20),((255,255,255),760)):\n d.rectangle((x0,y+36,x0+720,y+280),fill=bgc);x=x0+20\n for s in (200,98,48,24):\n t=ic.resize((s,s),Image.LANCZOS); sh.paste(t,(x,y+58+(200-s)//2),t); x+=s+40\nsh.save('../hakobu_icons_sizes.jpg',quality=90)</code></pre><h3 id=\"index_ox6KpGZu\">make3.py(3回目の5つのタッチ)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps, ImageChops\n\nOUT = '/home/claude/banner/hakobu/'\nos.makedirs(OUT, exist_ok=True)\nW, H = 2048, 1152\nBAND = (0, 407, 2048, 745) # visible on desktop\nSAFE = (406, 407, 1642, 745) # visible everywhere\nCREAM = (245, 245, 240); INK = (13, 13, 13); ORANGE = (196, 71, 31); BLUE = (26, 108, 152)\nSHELL = (247, 246, 242); SHELL2 = (226, 223, 215); DISP = (20, 20, 22)\nEYE = (246, 248, 252); MOUTH = (87, 181, 248); RING = (120, 226, 160)\nFR = '/home/claude/banner/vid/fx/'\nSS = 3 # supersampling for vector work\n\ndef frame(i):\n \"\"\"video frame with everything except Hakobu painted over with the background\"\"\"\n f = Image.open(FR + f'e_{i}.png').convert('RGB')\n c = Image.new('RGB', f.size, CREAM)\n c.paste(f.crop((0, 758, 362, 1080)), (0, 758))\n return c\n\n# ---------------------------------------------------------------- vector Hakobu\ndef face(d, box, expr='smile', glow_layer=None):\n x0, y0, x1, y1 = box; w = x1 - x0; h = y1 - y0\n d.rounded_rectangle(box, radius=int(min(w, h) * 0.16), fill=DISP)\n ew, eh = w * 0.11, h * 0.36; cy = y0 + h * 0.43\n for cx in (x0 + w * 0.32, x0 + w * 0.68):\n if expr == 'blink':\n d.rounded_rectangle((cx - ew * 0.8, cy - ew * 0.18, cx + ew * 0.8, cy + ew * 0.18), radius=int(ew * 0.18), fill=EYE)\n elif expr == 'happy':\n d.arc((cx - ew * 0.9, cy - ew * 0.3, cx + ew * 0.9, cy + ew * 1.5), 200, 340, fill=EYE, width=int(ew * 0.35))\n else:\n if glow_layer is not None:\n ImageDraw.Draw(glow_layer).rounded_rectangle((cx - ew * 0.9, cy - eh * 0.62, cx + ew * 0.9, cy + eh * 0.62), radius=int(ew * 0.9), fill=(246, 248, 252, 110))\n d.rounded_rectangle((cx - ew / 2, cy - eh / 2, cx + ew / 2, cy + eh / 2), radius=int(ew / 2), fill=EYE)\n mw = w * 0.22; my = y0 + h * 0.74; t = max(2, int(h * 0.07))\n if expr in ('open', 'happy'):\n d.chord((x0 + w / 2 - mw / 2, my - mw * 0.45, x0 + w / 2 + mw / 2, my + mw * 0.45), 0, 180, fill=MOUTH)\n elif expr == 'o':\n r = mw * 0.18; d.ellipse((x0 + w / 2 - r, my - r * 0.6, x0 + w / 2 + r, my + r * 1.2), fill=MOUTH)\n else:\n d.arc((x0 + w / 2 - mw / 2, my - mw * 0.55, x0 + w / 2 + mw / 2, my + mw * 0.30), 25, 155, fill=MOUTH, width=t)\n\ndef hakobu(img, cx, base, h, expr='smile', side=True):\n \"\"\"flat front-ish Hakobu, bottom of wheels at `base`, total height h\"\"\"\n d = ImageDraw.Draw(img)\n hw = h * 0.31; bw = h * 0.33; sd = h * 0.07 if side else 0\n # body\n by0 = base - h * 0.70; by1 = base - h * 0.08\n if side:\n d.rounded_rectangle((cx + bw - sd * 2, by0, cx + bw + sd, by1), radius=int(h * 0.05), fill=SHELL2)\n d.ellipse((cx + bw - sd * 0.2, by0 + (by1 - by0) * 0.28, cx + bw + sd * 0.75, by0 + (by1 - by0) * 0.62), outline=RING, width=max(2, int(h * 0.012)))\n d.rounded_rectangle((cx - bw, by0, cx + bw, by1), radius=int(h * 0.06), fill=SHELL)\n d.line((cx - bw + h * 0.01, by0 + h * 0.08, cx + bw - h * 0.01, by0 + h * 0.08), fill=SHELL2, width=max(1, int(h * 0.006)))\n d.rounded_rectangle((cx - h * 0.06, by0 + h * 0.20, cx + h * 0.06, by0 + h * 0.225), radius=int(h * 0.012), fill=(40, 40, 42))\n # base\n d.rounded_rectangle((cx - bw * 1.02, by1 - h * 0.01, cx + bw * 1.02 + sd * 0.5, base - h * 0.02), radius=int(h * 0.02), fill=(58, 58, 60))\n d.rounded_rectangle((cx - bw * 0.9, base - h * 0.03, cx + bw * 0.9, base), radius=int(h * 0.012), fill=(30, 30, 32))\n # head\n hy0 = base - h; hy1 = base - h * 0.70 + h * 0.015\n if side:\n d.rounded_rectangle((cx + hw - sd * 2, hy0 + h * 0.01, cx + hw + sd, hy1), radius=int(h * 0.05), fill=SHELL2)\n d.rounded_rectangle((cx - hw, hy0, cx + hw, hy1), radius=int(h * 0.07), fill=SHELL)\n m = h * 0.03\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0))\n face(d, (cx - hw + m, hy0 + m, cx + hw - m, hy1 - m * 1.1), expr, glow)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(h * 0.012)))\n\ndef ss_canvas(color):\n return Image.new('RGBA', (W * SS, H * SS), color + (255,))\n\ndef finish(img):\n return img.convert('RGB').resize((W, H), Image.LANCZOS)\n\n# ---------------------------------------------------------------- 1. illustration\ndef illustration():\n S = SS; img = ss_canvas((228, 238, 235)); d = ImageDraw.Draw(img)\n floor_y = 700 * S\n d.rectangle((0, floor_y, W * S, H * S), fill=(214, 207, 194))\n d.rectangle((0, floor_y - 6 * S, W * S, floor_y), fill=(196, 208, 204)) # skirting\n d.rectangle((0, 560 * S, W * S, 568 * S), fill=(180, 196, 192)) # handrail\n for x in range(0, W, 520): # doors\n dx = (x + 140) * S\n d.rounded_rectangle((dx, 300 * S, dx + 190 * S, floor_y - 6 * S), radius=10 * S, fill=(201, 219, 214))\n d.rounded_rectangle((dx + 55 * S, 360 * S, dx + 135 * S, 470 * S), radius=8 * S, fill=(237, 245, 243))\n d.rectangle((dx + 150 * S, 520 * S, dx + 170 * S, 530 * S), fill=(150, 168, 164))\n # room number plate (blank)\n d.rounded_rectangle((dx + 60 * S, 270 * S, dx + 130 * S, 290 * S), radius=4 * S, fill=(196, 71, 31))\n for x in (400, 1460): # plants\n px = x * S\n d.rounded_rectangle((px, 640 * S, px + 60 * S, floor_y), radius=6 * S, fill=(196, 160, 120))\n for a, r in ((-30, 46), (0, 60), (30, 46), (-60, 36), (60, 36)):\n ex = px + 30 * S + math.sin(math.radians(a)) * r * S; ey = 640 * S - math.cos(math.radians(a)) * r * S\n d.ellipse((ex - 22 * S, ey - 34 * S, ex + 22 * S, ey + 34 * S), fill=(110, 170, 130))\n # floor reflection lines\n for x in range(0, W, 90):\n d.line(((x + 30) * S, (floor_y + 40 * S), (x + 70) * S, (floor_y + 40 * S)), fill=(226, 220, 208), width=4 * S)\n # Hakobu, carrying a medicine tray on its head\n cx, base, h = 1080 * S, 736 * S, 300 * S\n hakobu(img, cx, base, h, 'smile')\n d = ImageDraw.Draw(img)\n # motion lines behind\n for k, yy in enumerate((520, 580, 640, 690)):\n d.rounded_rectangle(((880 - k * 25) * S, yy * S, (960 - k * 10) * S, (yy + 8) * S), radius=4 * S, fill=(190, 206, 202))\n return finish(img)\n\n# ---------------------------------------------------------------- 2. 4-koma manga style (wordless)\ndef to_manga(im):\n g = ImageOps.grayscale(im)\n g = ImageOps.autocontrast(g, cutoff=1)\n e = g.filter(ImageFilter.FIND_EDGES).point(lambda v: 255 if v > 28 else 0).filter(ImageFilter.MaxFilter(3))\n a = np.array(g).astype(int)\n out = np.full(a.shape, 255, np.uint8)\n yy, xx = np.mgrid[0:a.shape[0], 0:a.shape[1]]\n dots = ((xx % 7 - 3) ** 2 + (yy % 7 - 3) ** 2) <= 3\n out[(a < 215) & dots] = 0\n out[a < 70] = 0\n out[np.array(e) > 0] = 0\n return Image.fromarray(out).convert('RGB')\n\ndef manga():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 12):\n for x in range(0, W, 12):\n dist = abs(x - W / 2) / (W / 2); r = 2.6 * max(0, dist - 0.4) / 0.6\n if r > 0.4: d.ellipse((x - r, y - r, x + r, y + r), fill=(205, 205, 203))\n seq = [(1, 'smile'), (8, 'surprise'), (15, 'blink'), (9, 'joy')]\n pw, ph, gap = 290, 300, 18\n x = (W - (4 * pw + 3 * gap)) // 2; y = (BAND[1] + BAND[3]) // 2 - ph // 2\n for i, (fi, fx) in enumerate(seq):\n crop = frame(fi).crop((0, 730, 380, 1080))\n if i % 2 == 1: crop = ImageOps.mirror(crop)\n panel = ImageOps.fit(to_manga(crop), (pw, ph), Image.LANCZOS, centering=(0.5, 0.15))\n pd = ImageDraw.Draw(panel)\n if fx == 'surprise': # focus lines + exclamation mark\n for a in range(0, 360, 9):\n r0, r1 = 150, 260; ca, sa = math.cos(math.radians(a)), math.sin(math.radians(a))\n pd.line((pw / 2 + ca * r0, ph * 0.35 + sa * r0, pw / 2 + ca * r1, ph * 0.35 + sa * r1), fill=INK, width=2)\n pd.polygon([(250, 20), (268, 20), (263, 70), (255, 70)], fill=INK); pd.ellipse((252, 78, 266, 92), fill=INK)\n if fx == 'blink': # sweat drop\n pd.polygon([(60, 40), (48, 70), (72, 70)], fill=(255, 255, 255), outline=INK)\n pd.ellipse((46, 58, 74, 86), fill=(255, 255, 255), outline=INK, width=2)\n if fx == 'joy': # sparkles\n for sx, sy, s in ((40, 40, 18), (255, 60, 14), (60, 120, 10)):\n pd.polygon([(sx, sy - s), (sx + s * 0.25, sy - s * 0.25), (sx + s, sy), (sx + s * 0.25, sy + s * 0.25),\n (sx, sy + s), (sx - s * 0.25, sy + s * 0.25), (sx - s, sy), (sx - s * 0.25, sy - s * 0.25)], fill=INK)\n c.paste(panel, (x, y))\n d.rectangle((x - 4, y - 4, x + pw + 3, y + ph + 3), outline=INK, width=5)\n x += pw + gap\n x0 = (W - (4 * pw + 3 * gap)) // 2\n for k in range(24):\n yy = y + 10 + k * (ph - 20) / 23; ln = 90 + (k * 37 % 5) * 45\n d.line((x + 20, yy, x + 20 + ln, yy), fill=INK, width=3)\n d.line((x0 - 40 - ln, yy, x0 - 40, yy), fill=INK, width=3)\n return c\n\n# ---------------------------------------------------------------- 3. realistic (3D render line-up)\ndef realistic():\n c = Image.new('RGB', (W, H), CREAM)\n order = [15, 8, 1, 9, 4]\n step = 390; x = W // 2 - 2 * step - 190\n fade = Image.new('L', (380, 350), 255); fd = ImageDraw.Draw(fade)\n for k in range(90):\n fd.line((0, 349 - k, 380, 349 - k), fill=int(255 * k / 90))\n for i, fi in enumerate(order):\n crop = frame(fi).crop((0, 730, 380, 1080))\n if i < 2: crop = ImageOps.mirror(crop) # left side looks toward the centre\n c.paste(crop, (x, 412), fade)\n x += step\n # soft floor line\n d = ImageDraw.Draw(c, 'RGBA')\n return c\n\n# ---------------------------------------------------------------- 4. photo (hospital corridor)\ndef photo():\n src = Image.open('/home/claude/banner/site/c_drug.png').convert('RGB')\n sw = int(src.width * 0.30)\n pano = Image.new('RGB', (sw * 2, src.height))\n pano.paste(src.crop((0, 0, sw, src.height)), (0, 0)); pano.paste(src.crop((src.width - sw, 0, src.width, src.height)), (sw, 0))\n bg = pano.resize((W, H), Image.BICUBIC).filter(ImageFilter.GaussianBlur(24))\n bg = Image.blend(bg, Image.new('RGB', (W, H), (238, 236, 232)), 0.2)\n h = 400; s = h / src.height; w = int(src.width * s)\n sm = src.resize((w, h), Image.LANCZOS)\n m = Image.new('L', (w, h), 0); ImageDraw.Draw(m).rectangle((50, 35, w - 50, h - 35), fill=255)\n bg.paste(sm, (W // 2 - w // 2 + 160, 390), m.filter(ImageFilter.GaussianBlur(38)))\n return bg\n\n# ---------------------------------------------------------------- 5. graphic design (face as geometry)\ndef graphic():\n S = SS; img = ss_canvas(CREAM); d = ImageDraw.Draw(img)\n for gx in range(0, W, 32):\n d.line((gx * S, 0, gx * S, H * S), fill=(232, 232, 226), width=S)\n for gy in range(0, H, 32):\n d.line((0, gy * S, W * S, gy * S), fill=(232, 232, 226), width=S)\n # huge display, face fills the band\n box = (712 * S, 352 * S, 1336 * S, 792 * S)\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0))\n face(d, box, 'smile', glow)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(14 * S)))\n d = ImageDraw.Draw(img)\n # pixel squares leaving the display (left) – echo of the brand mark\n random.seed(5); cell = 34\n for gx in range(520, 712, cell):\n for gy in range(424, 740, cell):\n if random.random() < (gx - 520) / 200:\n d.rectangle((gx * S, gy * S, (gx + cell - 6) * S, (gy + cell - 6) * S), fill=INK)\n # orange dimension lines (right) and blue dot (ring) accent\n ox = 1400\n d.line((ox * S, 430 * S, ox * S, 730 * S), fill=ORANGE, width=4 * S)\n for yy in (430, 730):\n d.line(((ox - 18) * S, yy * S, (ox + 18) * S, yy * S), fill=ORANGE, width=4 * S)\n d.polygon([(ox * S, 430 * S), ((ox - 10) * S, 452 * S), ((ox + 10) * S, 452 * S)], fill=ORANGE)\n d.polygon([(ox * S, 730 * S), ((ox - 10) * S, 708 * S), ((ox + 10) * S, 708 * S)], fill=ORANGE)\n d.ellipse((1460 * S, 520 * S, 1580 * S, 640 * S), outline=RING, width=10 * S)\n for gx in range(1640, 1900, 22):\n for gy in range(440, 720, 22):\n d.ellipse(((gx - 3) * S, (gy - 3) * S, (gx + 3) * S, (gy + 3) * S), fill=INK)\n return finish(img)\n\nitems = [('1_illustration', illustration), ('2_manga', manga), ('3_realistic', realistic),\n ('4_photo', photo), ('5_graphic', graphic)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'hakobu_banner_{name}_2048x1152.jpg', quality=92, subsampling=0)\n im.crop(BAND).save(OUT + f'_band_{name}.png')\n print(name)</code></pre><h3 id=\"index_TtkMtlZh\">make4.py(4回目の4案)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps\n\nOUT = '/home/claude/banner/hakobu2/'\nos.makedirs(OUT, exist_ok=True)\nW, H = 2048, 1152\nBAND = (0, 407, 2048, 745)\nCREAM = (245, 245, 240); INK = (13, 13, 13); ORANGE = (196, 71, 31)\nMOUTH = (87, 181, 248); RING = (140, 226, 172)\nSITE = '/home/claude/banner/site/'\n\n# ------------------------------------------------------------ three-view sheet\nsrc = Image.open('/home/claude/banner/sheet/src.png').convert('RGB')\ng = np.array(src.convert('L')).astype(int)\nclean = np.array(src).astype(int)\nclean[g > 212] = 255 # remove faint paper pattern\n# colour accents: mouth blue, side ring green\nyy, xx = np.mgrid[0:g.shape[0], 0:g.shape[1]]\nmouth = (xx >= 184) & (xx <= 215) & (yy >= 490) & (yy <= 505) & (g > 70) & (g < 200)\nclean[mouth] = MOUTH\nr = np.hypot(xx - 554, yy - 663)\nring = (r >= 50.5) & (r <= 62.5) & (g > 95)\nclean[ring] = RING\nsheet = Image.fromarray(clean.astype(np.uint8))\nVIEWS = {'front': (64, 409, 338, 881), 'side': (390, 413, 705, 881), 'back': (743, 414, 1019, 882)}\n\ndef view(name):\n \"\"\"RGBA cut-out of one view (white outside the outline becomes transparent)\"\"\"\n box = VIEWS[name]\n im = sheet.crop(box)\n gg = np.array(im.convert('L'))\n barrier = Image.fromarray(np.where(gg < 200, 0, 255).astype(np.uint8)).filter(ImageFilter.MinFilter(5))\n ff = barrier.copy()\n for p in ((0, 0), (ff.width - 1, 0), (0, ff.height - 1), (ff.width - 1, ff.height - 1)):\n if ff.getpixel(p) == 255:\n ImageDraw.floodfill(ff, p, 128)\n m = np.where(np.array(ff) == 128, 0, 255).astype(np.uint8)\n mask = Image.fromarray(m).filter(ImageFilter.MinFilter(3)).filter(ImageFilter.GaussianBlur(0.7))\n out = im.convert('RGBA'); out.putalpha(mask)\n return out\n\ndef scaled(im, h):\n return im.resize((int(im.width * h / im.height), h), Image.LANCZOS)\n\ndef manga_crop(n, box):\n return Image.open(SITE + f'c_manga{n}.png').convert('RGB').crop(box)\n\ndef grid(c, step=32, major=160):\n d = ImageDraw.Draw(c)\n for x in range(0, W, step):\n d.line((x, 0, x, H), fill=(214, 214, 208) if x % major == 0 else (230, 230, 224), width=1)\n for y in range(0, H, step):\n d.line((0, y, W, y), fill=(214, 214, 208) if y % major == 0 else (230, 230, 224), width=1)\n\ndef arrow_dim(d, x, y0, y1, col=ORANGE, w=3):\n d.line((x, y0, x, y1), fill=col, width=w)\n for yy_, s in ((y0, 1), (y1, -1)):\n d.line((x - 12, yy_, x + 12, yy_), fill=col, width=w)\n d.polygon([(x, yy_), (x - 7, yy_ + 16 * s), (x + 7, yy_ + 16 * s)], fill=col)\n\ndef hdim(d, x0, x1, y, col=ORANGE, w=3):\n d.line((x0, y, x1, y), fill=col, width=w)\n for xx_, s in ((x0, 1), (x1, -1)):\n d.line((xx_, y - 12, xx_, y + 12), fill=col, width=w)\n d.polygon([(xx_, y), (xx_ + 16 * s, y - 7), (xx_ + 16 * s, y + 7)], fill=col)\n\ndef dashdot(d, x, y0, y1, col=(170, 170, 165)):\n y = y0\n while y < y1:\n d.line((x, y, x, min(y1, y + 22)), fill=col, width=2); y += 30\n d.line((x, y, x, min(y1, y + 3)), fill=col, width=2); y += 10\n\ndef circle_img(c, im, cx, cy, r, border=5):\n tile = ImageOps.fit(im, (2 * r, 2 * r), Image.LANCZOS)\n m = Image.new('L', tile.size, 0); ImageDraw.Draw(m).ellipse((0, 0, 2 * r - 1, 2 * r - 1), fill=255)\n c.paste(tile, (cx - r, cy - r), m)\n ImageDraw.Draw(c).ellipse((cx - r, cy - r, cx + r, cy + r), outline=INK, width=border)\n\n# ============================================================ I-a three-view design sheet\ndef illust_threeview():\n c = Image.new('RGB', (W, H), CREAM); grid(c); d = ImageDraw.Draw(c)\n h = 300; ground = 728\n xs = {'front': 760, 'back': 1060, 'side': 1360}\n for n, cx in xs.items():\n v = scaled(view(n), h)\n c.paste(v, (cx - v.width // 2, ground - h), v)\n dashdot(d, cx, ground - h - 6, ground + 6)\n d.line((560, ground, 1520, ground), fill=INK, width=3)\n fv = scaled(view('front'), h)\n arrow_dim(d, xs['front'] - fv.width // 2 - 40, ground - h, ground)\n sv = scaled(view('side'), h)\n hdim(d, xs['side'] - sv.width // 2, xs['side'] + sv.width // 2, ground - h - 2 + 0, ORANGE)\n # detail circles: face (left) and side ring (right)\n face = sheet.crop((100, 425, 300, 560))\n circle_img(c, face, 520, 576, 118)\n d.line((520 + 118, 576, xs['front'] - 40, 500), fill=ORANGE, width=3)\n ringc = sheet.crop((474, 583, 634, 743))\n circle_img(c, ringc, 1580, 576, 118)\n d.line((1580 - 118, 576, 1388, 610), fill=ORANGE, width=3)\n # dots at the far edges\n for gx in range(60, 330, 22):\n for gy in range(460, 700, 22):\n d.ellipse((gx - 3, gy - 3, gx + 3, gy + 3), fill=(120, 120, 116))\n for gx in range(1740, 2000, 22):\n for gy in range(460, 700, 22):\n d.ellipse((gx - 3, gy - 3, gx + 3, gy + 3), fill=(120, 120, 116))\n return c\n\n# ============================================================ I-b corridor illustration (line + flat colour)\ndef illust_corridor():\n c = Image.new('RGB', (W, H), (230, 239, 236)); d = ImageDraw.Draw(c)\n floor = 712\n d.rectangle((0, floor, W, H), fill=(234, 228, 216)); d.line((0, floor, W, floor), fill=INK, width=4)\n d.line((0, 590, W, 590), fill=INK, width=3); d.line((0, 600, W, 600), fill=INK, width=3)\n for i, x in enumerate(range(-80, W, 470)):\n d.rectangle((x, 330, x + 200, floor), fill=(206, 224, 219), outline=INK, width=4)\n d.rectangle((x + 60, 380, x + 140, 480), fill=(246, 250, 249), outline=INK, width=3)\n d.rounded_rectangle((x + 160, 540, x + 184, 556), radius=4, fill=INK)\n d.rectangle((x + 70, 300, x + 130, 318), fill=ORANGE, outline=INK, width=3)\n for x in (300, 1530):\n d.rounded_rectangle((x, 640, x + 64, floor), radius=6, fill=(214, 172, 128), outline=INK, width=3)\n for a, rr in ((-35, 50), (0, 66), (35, 50)):\n ex = x + 32 + math.sin(math.radians(a)) * rr; ey = 640 - math.cos(math.radians(a)) * rr\n d.ellipse((ex - 24, ey - 38, ex + 24, ey + 38), fill=(132, 190, 150), outline=INK, width=3)\n for x in range(40, W, 110):\n d.line((x, floor + 30, x + 50, floor + 30), fill=(210, 202, 188), width=4)\n # Hakobu (side view, moving left) + motion lines behind\n v = scaled(view('side'), 296)\n hx = 1000; c.paste(v, (hx - v.width // 2, floor + 10 - v.height), v)\n d = ImageDraw.Draw(c)\n for k, yy_ in enumerate((470, 530, 600, 660)):\n x0 = hx + v.width // 2 + 30 + (k % 2) * 30\n d.line((x0, yy_, x0 + 120 - k * 15, yy_), fill=INK, width=4)\n # a second Hakobu waiting at the far door (front view, small)\n f = scaled(view('front'), 170)\n c.paste(f, (1340, floor + 4 - f.height), f)\n return c\n\n# ============================================================ M-a manga page, slanted panels\ndef manga_page():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 12):\n for x in range(0, W, 12):\n d.ellipse((x - 1.6, y - 1.6, x + 1.6, y + 1.6), fill=(212, 212, 210))\n top, bot, sl = 418, 734, 36\n cuts = [-40, 392, 790, 1258, 1656, 2090]\n imgs = [manga_crop(3, (0, 40, 561, 470)), manga_crop(0, (0, 60, 561, 536)), None,\n manga_crop(1, (250, 0, 561, 400)), manga_crop(2, (100, 0, 561, 400))]\n for i in range(5):\n xl, xr = cuts[i] + 8, cuts[i + 1] - 8\n poly = [(xl + sl, top), (xr + sl, top), (xr - sl, bot), (xl - sl, bot)]\n bw, bh = xr - xl + 2 * sl, bot - top\n if imgs[i] is None: # centre: Hakobu front with focus lines\n tile = Image.new('RGB', (bw, bh), (255, 255, 255)); td = ImageDraw.Draw(tile)\n cx0, cy0 = bw / 2, bh * 0.45\n for a in range(0, 360, 5):\n ca, sa = math.cos(math.radians(a)), math.sin(math.radians(a))\n r0 = 150 + (a * 7 % 40)\n td.line((cx0 + ca * r0, cy0 + sa * r0, cx0 + ca * 600, cy0 + sa * 600), fill=INK, width=2)\n v = scaled(view('front'), bh - 26)\n tile.paste(v, (int(cx0 - v.width / 2), bh - v.height - 4), v)\n else:\n tile = ImageOps.fit(imgs[i], (bw, bh), Image.LANCZOS, centering=(0.5, 0.4))\n m = Image.new('L', (W, H), 0); ImageDraw.Draw(m).polygon(poly, fill=255)\n layer = Image.new('RGB', (W, H)); layer.paste(tile, (xl - sl, top))\n c.paste(layer, (0, 0), m)\n ImageDraw.Draw(c).polygon(poly, outline=INK, width=5)\n return c\n\n# ============================================================ M-b cast introduction\ndef manga_cast():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 11):\n for x in range(0, W, 11):\n rr = 2.2 if (x // 11 + y // 11) % 2 == 0 else 1.2\n d.ellipse((x - rr, y - rr, x + rr, y + rr), fill=(218, 218, 216))\n cy = 578\n # starburst behind Hakobu\n pts = []\n for k in range(36):\n a = math.radians(k * 10); rr = 180 if k % 2 == 0 else 128\n pts.append((1024 + math.cos(a) * rr * 0.95, cy + math.sin(a) * rr))\n d.polygon(pts, fill=(255, 255, 255), outline=INK, width=4)\n v = scaled(view('front'), 316)\n c.paste(v, (1024 - v.width // 2, 736 - v.height), v)\n # cast circles\n cast = [(520, manga_crop(1, (300, 0, 540, 240))), # nurse\n (770, manga_crop(0, (360, 40, 561, 241))), # engineer\n (1278, manga_crop(2, (160, 20, 380, 240))), # arm robot with a face\n (1528, manga_crop(3, (40, 20, 561, 541)))] # the twin arms\n for x, im in cast:\n circle_img(c, im, x, cy, 112, 5)\n d = ImageDraw.Draw(c)\n # speed lines at both ends\n for k in range(22):\n yy_ = 440 + k * 13; ln = 80 + (k * 37 % 5) * 40\n d.line((1680, yy_, 1680 + ln, yy_), fill=INK, width=3)\n d.line((368 - ln, yy_, 368, yy_), fill=INK, width=3)\n return c\n\nitems = [('I1_threeview', illust_threeview), ('I2_corridor', illust_corridor),\n ('M1_manga_page', manga_page), ('M2_cast', manga_cast)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'hakobu_banner_{name}_2048x1152.jpg', quality=92, subsampling=0)\n im.crop(BAND).save(OUT + f'_band_{name}.png')\n print(name)</code></pre><h3 id=\"index_fHfez7_y\">make5.py(5回目の3案)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps\n\n_src = open('/home/claude/banner/make3.py').read()\nexec(_src[:_src.index(\"items = [\")]) # reuse frame(), to_manga(), constants\n\nOUT = '/home/claude/banner/hakobu3/'\nos.makedirs(OUT, exist_ok=True)\nSITE = '/home/claude/banner/site/'\nDARK = (40, 40, 42); BASE = (205, 201, 192); BASE2 = (184, 180, 171)\nSHELL = (247, 246, 242); SHELL2 = (226, 223, 215); SHELL_L = (251, 250, 247); SEAM = (214, 211, 203)\n\n# ------------------------------------------------------------ Hakobu redrawn from the three-view sheet\nclass Pen:\n \"\"\"maps three-view sheet coordinates to the canvas\"\"\"\n def __init__(self, img, x0, y0, ox, oy, s, line=None, lw=0):\n self.d = ImageDraw.Draw(img); self.x0, self.y0, self.ox, self.oy, self.s = x0, y0, ox, oy, s\n self.line, self.lw = line, lw\n def p(self, x, y): return (self.ox + (x - self.x0) * self.s, self.oy + (y - self.y0) * self.s)\n def box(self, b): a = self.p(b[0], b[1]); c = self.p(b[2], b[3]); return (a[0], a[1], c[0], c[1])\n def rr(self, b, r, fill, outline=True):\n self.d.rounded_rectangle(self.box(b), radius=r * self.s, fill=fill,\n outline=self.line if (outline and self.line) else None, width=int(self.lw))\n def poly(self, pts, fill, outline=True):\n self.d.polygon([self.p(*q) for q in pts], fill=fill, outline=self.line if (outline and self.line) else None,\n width=int(self.lw) if self.line else 1)\n def ell(self, b, fill, outline=True):\n self.d.ellipse(self.box(b), fill=fill, outline=self.line if (outline and self.line) else None, width=int(self.lw))\n def ln(self, a, b, fill, w):\n self.d.line((self.p(*a), self.p(*b)), fill=fill, width=max(1, int(w * self.s)))\n\ndef hakobu_front(img, cx, ground, h, line=None, lw=0, expr='smile'):\n s = h / 461.0\n pen = Pen(img, 200, 874, cx, ground, s, line, lw)\n seam = line or SEAM\n pen.rr((95, 836, 120, 874), 6, DARK); pen.rr((280, 836, 305, 874), 6, DARK) # wheels\n pen.rr((72, 806, 328, 848), 12, BASE) # base\n pen.rr((76, 505, 324, 816), 26, SHELL) # body\n pen.rr((78, 525, 98, 800), 8, SHELL2, False); pen.rr((302, 525, 322, 800), 8, SHELL2, False)\n pen.rr((100, 413, 300, 566), 34, SHELL) # head\n pen.rr((112, 546, 288, 566), 10, SHELL2, False)\n pen.rr((120, 427, 283, 541), 12, DISP) # display\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gp = Pen(glow, 200, 874, cx, ground, s)\n for ex in (152, 241):\n gp.rr((ex - 17, 460, ex + 17, 512), 17, (246, 248, 252, 110), False)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(4 * s)))\n pen.d = ImageDraw.Draw(img)\n for ex in (152, 241):\n if expr == 'blink':\n pen.rr((ex - 13, 484, ex + 13, 490), 3, EYE, False)\n else:\n pen.rr((ex - 11, 466, ex + 11, 506), 11, EYE, False)\n pen.d.chord(pen.box((191, 484, 217, 510)), 0, 180, fill=MOUTH) # mouth\n pen.ln((80, 588), (320, 588), seam, 2.2)\n pen.rr((167, 606, 225, 630), 11, DARK, False) # drawer slot\n pen.d.arc(pen.box((78, 668, 322, 730)), 15, 165, fill=seam, width=max(1, int(2.2 * s)))\n pen.rr((78, 767, 322, 786), 6, DARK, False) # black stripe\n pen.rr((177, 796, 224, 810), 7, DARK, False)\n\ndef hakobu_side(img, cx, ground, h, line=None, lw=0):\n \"\"\"side view, front of the robot on the left\"\"\"\n s = h / 461.0\n pen = Pen(img, 548, 874, cx, ground, s, line, lw)\n pen.ell((426, 830, 472, 876), DARK); pen.ell((638, 830, 684, 876), DARK) # wheels\n pen.rr((404, 798, 692, 862), 18, BASE)\n pen.rr((468, 808, 626, 860), 14, BASE2, False)\n pen.poly([(430, 505), (418, 560), (410, 700), (404, 805), (692, 805), (684, 515), (656, 505)], SHELL)\n pen.poly([(455, 418), (628, 418), (650, 432), (657, 505), (430, 505)], SHELL) # head\n pen.poly([(442, 464), (654, 464), (657, 505), (431, 505)], SHELL2, False)\n pen.rr((413, 596, 441, 720), 8, SHELL2, False) # front strip\n pen.rr((446, 526, 672, 800), 70, SHELL_L) # side panel\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gp = Pen(glow, 548, 874, cx, ground, s)\n gp.ell((554 - 74, 663 - 74, 554 + 74, 663 + 74), RING + (90,), False)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(10 * s)))\n pen.d = ImageDraw.Draw(img)\n pen.ell((554 - 62, 663 - 62, 554 + 62, 663 + 62), RING) # ring\n pen.ell((554 - 50, 663 - 50, 554 + 50, 663 + 50), SHELL_L)\n pen.rr((408, 768, 456, 790), 8, DARK) # bumper\n pen.rr((410, 790, 452, 800), 4, (200, 200, 198), False)\n\n# ------------------------------------------------------------ I-flat: flat corridor (no outlines)\ndef corridor_flat():\n S = SS; img = ss_canvas((228, 238, 235)); d = ImageDraw.Draw(img)\n floor = 700 * S\n d.rectangle((0, floor, W * S, H * S), fill=(214, 207, 194))\n d.rectangle((0, floor - 6 * S, W * S, floor), fill=(196, 208, 204))\n d.rectangle((0, 560 * S, W * S, 568 * S), fill=(180, 196, 192))\n for x in range(0, W, 520):\n dx = (x + 140) * S\n d.rounded_rectangle((dx, 300 * S, dx + 190 * S, floor - 6 * S), radius=10 * S, fill=(201, 219, 214))\n d.rounded_rectangle((dx + 55 * S, 360 * S, dx + 135 * S, 470 * S), radius=8 * S, fill=(237, 245, 243))\n d.rectangle((dx + 150 * S, 520 * S, dx + 170 * S, 530 * S), fill=(150, 168, 164))\n d.rounded_rectangle((dx + 60 * S, 270 * S, dx + 130 * S, 290 * S), radius=4 * S, fill=ORANGE)\n for x in (400, 1460):\n px = x * S\n d.rounded_rectangle((px, 640 * S, px + 60 * S, floor), radius=6 * S, fill=(196, 160, 120))\n for a, rr_ in ((-30, 46), (0, 60), (30, 46), (-60, 36), (60, 36)):\n ex = px + 30 * S + math.sin(math.radians(a)) * rr_ * S; ey = 640 * S - math.cos(math.radians(a)) * rr_ * S\n d.ellipse((ex - 22 * S, ey - 34 * S, ex + 22 * S, ey + 34 * S), fill=(110, 170, 130))\n for x in range(0, W, 90):\n d.line(((x + 30) * S, floor + 40 * S, (x + 70) * S, floor + 40 * S), fill=(226, 220, 208), width=4 * S)\n # moving Hakobu (side) + motion lines, and one waiting at a door (front)\n hakobu_side(img, 660 * S, 706 * S, 210 * S)\n d = ImageDraw.Draw(img)\n for k, yy_ in enumerate((540, 590, 640)):\n x0 = (762 + (k % 2) * 22) * S\n d.rounded_rectangle((x0, yy_ * S, x0 + (100 - k * 14) * S, (yy_ + 8) * S), radius=4 * S, fill=(176, 194, 190))\n hakobu_front(img, 1080 * S, 730 * S, 300 * S)\n return finish(img)\n\n# ------------------------------------------------------------ I-line: line + flat colour corridor\ndef corridor_line():\n S = SS; img = ss_canvas((230, 239, 236)); d = ImageDraw.Draw(img); L = 4 * S\n floor = 712 * S\n d.rectangle((0, floor, W * S, H * S), fill=(234, 228, 216)); d.line((0, floor, W * S, floor), fill=INK, width=L)\n d.line((0, 590 * S, W * S, 590 * S), fill=INK, width=3 * S); d.line((0, 600 * S, W * S, 600 * S), fill=INK, width=3 * S)\n for x in range(-80, W, 470):\n d.rectangle((x * S, 330 * S, (x + 200) * S, floor), fill=(206, 224, 219), outline=INK, width=L)\n d.rectangle(((x + 60) * S, 380 * S, (x + 140) * S, 480 * S), fill=(246, 250, 249), outline=INK, width=3 * S)\n d.rounded_rectangle(((x + 160) * S, 540 * S, (x + 184) * S, 556 * S), radius=4 * S, fill=INK)\n d.rectangle(((x + 70) * S, 300 * S, (x + 130) * S, 318 * S), fill=ORANGE, outline=INK, width=3 * S)\n for x in (300, 1530):\n d.rounded_rectangle((x * S, 640 * S, (x + 64) * S, floor), radius=6 * S, fill=(214, 172, 128), outline=INK, width=3 * S)\n for a, rr_ in ((-35, 50), (0, 66), (35, 50)):\n ex = (x + 32 + math.sin(math.radians(a)) * rr_) * S; ey = (640 - math.cos(math.radians(a)) * rr_) * S\n d.ellipse((ex - 24 * S, ey - 38 * S, ex + 24 * S, ey + 38 * S), fill=(132, 190, 150), outline=INK, width=3 * S)\n for x in range(40, W, 110):\n d.line(((x) * S, (floor + 30 * S), (x + 50) * S, (floor + 30 * S)), fill=(210, 202, 188), width=4 * S)\n hakobu_side(img, 660 * S, 714 * S, 210 * S, line=INK, lw=3 * S)\n d = ImageDraw.Draw(img)\n for k, yy_ in enumerate((545, 600, 655)):\n x0 = (770 + (k % 2) * 24) * S\n d.line((x0, yy_ * S, x0 + (100 - k * 14) * S, yy_ * S), fill=INK, width=L)\n hakobu_front(img, 1080 * S, 734 * S, 300 * S, line=INK, lw=3.5 * S)\n return finish(img)\n\ndef ink_manga(im):\n g = ImageOps.autocontrast(ImageOps.grayscale(im), cutoff=2)\n a = np.array(g).astype(int)\n yy_, xx_ = np.mgrid[0:a.shape[0], 0:a.shape[1]]\n dots = ((xx_ % 7 - 3) ** 2 + (yy_ % 7 - 3) ** 2) <= 3\n out = np.full(a.shape, 255, np.uint8)\n out[(a < 205) & dots] = 0\n out[a < 120] = 0\n return Image.fromarray(out).convert('RGB')\n\n# ------------------------------------------------------------ manga: v1 touch, different characters per panel\ndef manga_cast_strip():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 16):\n for x in range(0, W, 16):\n dist = abs(x - W / 2) / (W / 2); r = 3.2 * max(0, dist - 0.35) / 0.65\n if r > 0.4: d.ellipse((x - r, y - r, x + r, y + r), fill=(200, 200, 198))\n mg = lambda n, b: Image.open(SITE + f'c_manga{n}.png').convert('RGB').crop(b)\n panels = [\n (frame(1).crop((0, 730, 380, 1080)), 'none', (0.5, 0.15)), # Hakobu\n (mg(1, (290, 0, 545, 330)), 'surprise', (0.5, 0.1)), # nurse\n (mg(0, (330, 30, 561, 330)), 'sweat', (0.5, 0.1)), # engineer\n (mg(2, (170, 0, 400, 270)), 'joy', (0.5, 0.5)), # arm robot with a face\n ]\n pw, ph, gap = 290, 300, 18\n x = (W - (4 * pw + 3 * gap)) // 2; y = (BAND[1] + BAND[3]) // 2 - ph // 2\n for i, (im, fx, cen) in enumerate(panels):\n big = ImageOps.fit(im, (pw, ph), Image.LANCZOS, centering=cen)\n panel = to_manga(big) if i == 0 else ink_manga(big)\n pd = ImageDraw.Draw(panel)\n if fx == 'surprise':\n for a in range(0, 360, 9):\n ca, sa = math.cos(math.radians(a)), math.sin(math.radians(a))\n pd.line((pw / 2 + ca * 165, ph * 0.3 + sa * 165, pw / 2 + ca * 260, ph * 0.3 + sa * 260), fill=INK, width=2)\n pd.polygon([(30, 20), (48, 20), (43, 70), (35, 70)], fill=INK); pd.ellipse((32, 78, 46, 92), fill=INK)\n if fx == 'sweat':\n pd.polygon([(40, 34), (28, 64), (52, 64)], fill=(255, 255, 255), outline=INK)\n pd.ellipse((26, 52, 54, 80), fill=(255, 255, 255), outline=INK, width=2)\n if fx == 'joy':\n for sx, sy, s in ((40, 40, 18), (255, 60, 14), (60, 120, 10)):\n pd.polygon([(sx, sy - s), (sx + s * 0.25, sy - s * 0.25), (sx + s, sy), (sx + s * 0.25, sy + s * 0.25),\n (sx, sy + s), (sx - s * 0.25, sy + s * 0.25), (sx - s, sy), (sx - s * 0.25, sy - s * 0.25)], fill=INK)\n c.paste(panel, (x, y))\n d.rectangle((x - 4, y - 4, x + pw + 3, y + ph + 3), outline=INK, width=5)\n x += pw + gap\n x0 = (W - (4 * pw + 3 * gap)) // 2\n for k in range(24):\n yy_ = y + 10 + k * (ph - 20) / 23; ln = 90 + (k * 37 % 5) * 45\n d.line((x + 20, yy_, x + 20 + ln, yy_), fill=INK, width=3)\n d.line((x0 - 40 - ln, yy_, x0 - 40, yy_), fill=INK, width=3)\n return c\n\nitems = [('illust_flat', corridor_flat), ('illust_line', corridor_line), ('manga_cast', manga_cast_strip)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'hakobu_banner_{name}_2048x1152.jpg', quality=92, subsampling=0)\n im.crop(BAND).save(OUT + f'_band_{name}.png')\n print(name)</code></pre><h3 id=\"index_sTlRYvOF\">make5_single.py(決定版のバナー)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps\n\n_src = open('/home/claude/banner/make3.py').read()\nexec(_src[:_src.index(\"items = [\")]) # reuse frame(), to_manga(), constants\n\nOUT = '/home/claude/banner/hakobu3/'\nos.makedirs(OUT, exist_ok=True)\nSITE = '/home/claude/banner/site/'\nDARK = (40, 40, 42); BASE = (205, 201, 192); BASE2 = (184, 180, 171)\nSHELL = (247, 246, 242); SHELL2 = (226, 223, 215); SHELL_L = (251, 250, 247); SEAM = (214, 211, 203)\n\n# ------------------------------------------------------------ Hakobu redrawn from the three-view sheet\nclass Pen:\n \"\"\"maps three-view sheet coordinates to the canvas\"\"\"\n def __init__(self, img, x0, y0, ox, oy, s, line=None, lw=0):\n self.d = ImageDraw.Draw(img); self.x0, self.y0, self.ox, self.oy, self.s = x0, y0, ox, oy, s\n self.line, self.lw = line, lw\n def p(self, x, y): return (self.ox + (x - self.x0) * self.s, self.oy + (y - self.y0) * self.s)\n def box(self, b): a = self.p(b[0], b[1]); c = self.p(b[2], b[3]); return (a[0], a[1], c[0], c[1])\n def rr(self, b, r, fill, outline=True):\n self.d.rounded_rectangle(self.box(b), radius=r * self.s, fill=fill,\n outline=self.line if (outline and self.line) else None, width=int(self.lw))\n def poly(self, pts, fill, outline=True):\n self.d.polygon([self.p(*q) for q in pts], fill=fill, outline=self.line if (outline and self.line) else None,\n width=int(self.lw) if self.line else 1)\n def ell(self, b, fill, outline=True):\n self.d.ellipse(self.box(b), fill=fill, outline=self.line if (outline and self.line) else None, width=int(self.lw))\n def ln(self, a, b, fill, w):\n self.d.line((self.p(*a), self.p(*b)), fill=fill, width=max(1, int(w * self.s)))\n\ndef hakobu_front(img, cx, ground, h, line=None, lw=0, expr='smile'):\n s = h / 461.0\n pen = Pen(img, 200, 874, cx, ground, s, line, lw)\n seam = line or SEAM\n pen.rr((95, 836, 120, 874), 6, DARK); pen.rr((280, 836, 305, 874), 6, DARK) # wheels\n pen.rr((72, 806, 328, 848), 12, BASE) # base\n pen.rr((76, 505, 324, 816), 26, SHELL) # body\n pen.rr((78, 525, 98, 800), 8, SHELL2, False); pen.rr((302, 525, 322, 800), 8, SHELL2, False)\n pen.rr((100, 413, 300, 566), 34, SHELL) # head\n pen.rr((112, 546, 288, 566), 10, SHELL2, False)\n pen.rr((120, 427, 283, 541), 12, DISP) # display\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gp = Pen(glow, 200, 874, cx, ground, s)\n for ex in (152, 241):\n gp.rr((ex - 17, 460, ex + 17, 512), 17, (246, 248, 252, 110), False)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(4 * s)))\n pen.d = ImageDraw.Draw(img)\n for ex in (152, 241):\n if expr == 'blink':\n pen.rr((ex - 13, 484, ex + 13, 490), 3, EYE, False)\n else:\n pen.rr((ex - 11, 466, ex + 11, 506), 11, EYE, False)\n pen.d.chord(pen.box((191, 484, 217, 510)), 0, 180, fill=MOUTH) # mouth\n pen.ln((80, 588), (320, 588), seam, 2.2)\n pen.rr((167, 606, 225, 630), 11, DARK, False) # drawer slot\n pen.d.arc(pen.box((78, 668, 322, 730)), 15, 165, fill=seam, width=max(1, int(2.2 * s)))\n pen.rr((78, 767, 322, 786), 6, DARK, False) # black stripe\n pen.rr((177, 796, 224, 810), 7, DARK, False)\n\ndef hakobu_side(img, cx, ground, h, line=None, lw=0):\n \"\"\"side view, front of the robot on the left\"\"\"\n s = h / 461.0\n pen = Pen(img, 548, 874, cx, ground, s, line, lw)\n pen.ell((426, 830, 472, 876), DARK); pen.ell((638, 830, 684, 876), DARK) # wheels\n pen.rr((404, 798, 692, 862), 18, BASE)\n pen.rr((468, 808, 626, 860), 14, BASE2, False)\n pen.poly([(430, 505), (418, 560), (410, 700), (404, 805), (692, 805), (684, 515), (656, 505)], SHELL)\n pen.poly([(455, 418), (628, 418), (650, 432), (657, 505), (430, 505)], SHELL) # head\n pen.poly([(442, 464), (654, 464), (657, 505), (431, 505)], SHELL2, False)\n pen.rr((413, 596, 441, 720), 8, SHELL2, False) # front strip\n pen.rr((446, 526, 672, 800), 70, SHELL_L) # side panel\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gp = Pen(glow, 548, 874, cx, ground, s)\n gp.ell((554 - 74, 663 - 74, 554 + 74, 663 + 74), RING + (90,), False)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(10 * s)))\n pen.d = ImageDraw.Draw(img)\n pen.ell((554 - 62, 663 - 62, 554 + 62, 663 + 62), RING) # ring\n pen.ell((554 - 50, 663 - 50, 554 + 50, 663 + 50), SHELL_L)\n pen.rr((408, 768, 456, 790), 8, DARK) # bumper\n pen.rr((410, 790, 452, 800), 4, (200, 200, 198), False)\n\n# ------------------------------------------------------------ I-flat: flat corridor (no outlines)\ndef corridor_flat():\n S = SS; img = ss_canvas((228, 238, 235)); d = ImageDraw.Draw(img)\n floor = 700 * S\n d.rectangle((0, floor, W * S, H * S), fill=(214, 207, 194))\n d.rectangle((0, floor - 6 * S, W * S, floor), fill=(196, 208, 204))\n d.rectangle((0, 560 * S, W * S, 568 * S), fill=(180, 196, 192))\n for x in range(0, W, 520):\n dx = (x + 140) * S\n d.rounded_rectangle((dx, 300 * S, dx + 190 * S, floor - 6 * S), radius=10 * S, fill=(201, 219, 214))\n d.rounded_rectangle((dx + 55 * S, 360 * S, dx + 135 * S, 470 * S), radius=8 * S, fill=(237, 245, 243))\n d.rectangle((dx + 150 * S, 520 * S, dx + 170 * S, 530 * S), fill=(150, 168, 164))\n d.rounded_rectangle((dx + 60 * S, 270 * S, dx + 130 * S, 290 * S), radius=4 * S, fill=ORANGE)\n for x in (400, 1460):\n px = x * S\n d.rounded_rectangle((px, 640 * S, px + 60 * S, floor), radius=6 * S, fill=(196, 160, 120))\n for a, rr_ in ((-30, 46), (0, 60), (30, 46), (-60, 36), (60, 36)):\n ex = px + 30 * S + math.sin(math.radians(a)) * rr_ * S; ey = 640 * S - math.cos(math.radians(a)) * rr_ * S\n d.ellipse((ex - 22 * S, ey - 34 * S, ex + 22 * S, ey + 34 * S), fill=(110, 170, 130))\n for x in range(0, W, 90):\n d.line(((x + 30) * S, floor + 40 * S, (x + 70) * S, floor + 40 * S), fill=(226, 220, 208), width=4 * S)\n # moving Hakobu (side) + motion lines, and one waiting at a door (front)\n hakobu_front(img, 1015 * S, 730 * S, 300 * S)\n return finish(img)\n\n# ------------------------------------------------------------ I-line: line + flat colour corridor\ndef corridor_line():\n S = SS; img = ss_canvas((230, 239, 236)); d = ImageDraw.Draw(img); L = 4 * S\n floor = 712 * S\n d.rectangle((0, floor, W * S, H * S), fill=(234, 228, 216)); d.line((0, floor, W * S, floor), fill=INK, width=L)\n d.line((0, 590 * S, W * S, 590 * S), fill=INK, width=3 * S); d.line((0, 600 * S, W * S, 600 * S), fill=INK, width=3 * S)\n for x in range(-80, W, 470):\n d.rectangle((x * S, 330 * S, (x + 200) * S, floor), fill=(206, 224, 219), outline=INK, width=L)\n d.rectangle(((x + 60) * S, 380 * S, (x + 140) * S, 480 * S), fill=(246, 250, 249), outline=INK, width=3 * S)\n d.rounded_rectangle(((x + 160) * S, 540 * S, (x + 184) * S, 556 * S), radius=4 * S, fill=INK)\n d.rectangle(((x + 70) * S, 300 * S, (x + 130) * S, 318 * S), fill=ORANGE, outline=INK, width=3 * S)\n for x in (300, 1530):\n d.rounded_rectangle((x * S, 640 * S, (x + 64) * S, floor), radius=6 * S, fill=(214, 172, 128), outline=INK, width=3 * S)\n for a, rr_ in ((-35, 50), (0, 66), (35, 50)):\n ex = (x + 32 + math.sin(math.radians(a)) * rr_) * S; ey = (640 - math.cos(math.radians(a)) * rr_) * S\n d.ellipse((ex - 24 * S, ey - 38 * S, ex + 24 * S, ey + 38 * S), fill=(132, 190, 150), outline=INK, width=3 * S)\n for x in range(40, W, 110):\n d.line(((x) * S, (floor + 30 * S), (x + 50) * S, (floor + 30 * S)), fill=(210, 202, 188), width=4 * S)\n hakobu_side(img, 660 * S, 714 * S, 210 * S, line=INK, lw=3 * S)\n d = ImageDraw.Draw(img)\n for k, yy_ in enumerate((545, 600, 655)):\n x0 = (770 + (k % 2) * 24) * S\n d.line((x0, yy_ * S, x0 + (100 - k * 14) * S, yy_ * S), fill=INK, width=L)\n hakobu_front(img, 1080 * S, 734 * S, 300 * S, line=INK, lw=3.5 * S)\n return finish(img)\n\ndef ink_manga(im):\n g = ImageOps.autocontrast(ImageOps.grayscale(im), cutoff=2)\n a = np.array(g).astype(int)\n yy_, xx_ = np.mgrid[0:a.shape[0], 0:a.shape[1]]\n dots = ((xx_ % 7 - 3) ** 2 + (yy_ % 7 - 3) ** 2) <= 3\n out = np.full(a.shape, 255, np.uint8)\n out[(a < 205) & dots] = 0\n out[a < 120] = 0\n return Image.fromarray(out).convert('RGB')\n\n# ------------------------------------------------------------ manga: v1 touch, different characters per panel\ndef manga_cast_strip():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 16):\n for x in range(0, W, 16):\n dist = abs(x - W / 2) / (W / 2); r = 3.2 * max(0, dist - 0.35) / 0.65\n if r > 0.4: d.ellipse((x - r, y - r, x + r, y + r), fill=(200, 200, 198))\n mg = lambda n, b: Image.open(SITE + f'c_manga{n}.png').convert('RGB').crop(b)\n panels = [\n (frame(1).crop((0, 730, 380, 1080)), 'none', (0.5, 0.15)), # Hakobu\n (mg(1, (290, 0, 545, 330)), 'surprise', (0.5, 0.1)), # nurse\n (mg(0, (330, 30, 561, 330)), 'sweat', (0.5, 0.1)), # engineer\n (mg(2, (170, 0, 400, 270)), 'joy', (0.5, 0.5)), # arm robot with a face\n ]\n pw, ph, gap = 290, 300, 18\n x = (W - (4 * pw + 3 * gap)) // 2; y = (BAND[1] + BAND[3]) // 2 - ph // 2\n for i, (im, fx, cen) in enumerate(panels):\n big = ImageOps.fit(im, (pw, ph), Image.LANCZOS, centering=cen)\n panel = to_manga(big) if i == 0 else ink_manga(big)\n pd = ImageDraw.Draw(panel)\n if fx == 'surprise':\n for a in range(0, 360, 9):\n ca, sa = math.cos(math.radians(a)), math.sin(math.radians(a))\n pd.line((pw / 2 + ca * 165, ph * 0.3 + sa * 165, pw / 2 + ca * 260, ph * 0.3 + sa * 260), fill=INK, width=2)\n pd.polygon([(30, 20), (48, 20), (43, 70), (35, 70)], fill=INK); pd.ellipse((32, 78, 46, 92), fill=INK)\n if fx == 'sweat':\n pd.polygon([(40, 34), (28, 64), (52, 64)], fill=(255, 255, 255), outline=INK)\n pd.ellipse((26, 52, 54, 80), fill=(255, 255, 255), outline=INK, width=2)\n if fx == 'joy':\n for sx, sy, s in ((40, 40, 18), (255, 60, 14), (60, 120, 10)):\n pd.polygon([(sx, sy - s), (sx + s * 0.25, sy - s * 0.25), (sx + s, sy), (sx + s * 0.25, sy + s * 0.25),\n (sx, sy + s), (sx - s * 0.25, sy + s * 0.25), (sx - s, sy), (sx - s * 0.25, sy - s * 0.25)], fill=INK)\n c.paste(panel, (x, y))\n d.rectangle((x - 4, y - 4, x + pw + 3, y + ph + 3), outline=INK, width=5)\n x += pw + gap\n x0 = (W - (4 * pw + 3 * gap)) // 2\n for k in range(24):\n yy_ = y + 10 + k * (ph - 20) / 23; ln = 90 + (k * 37 % 5) * 45\n d.line((x + 20, yy_, x + 20 + ln, yy_), fill=INK, width=3)\n d.line((x0 - 40 - ln, yy_, x0 - 40, yy_), fill=INK, width=3)\n return c\n\nitems = [('illust_flat_single', corridor_flat)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'hakobu_banner_{name}_2048x1152.jpg', quality=92, subsampling=0)\n im.crop(BAND).save(OUT + f'_band_{name}.png')\n print(name)</code></pre><h2 id=\"index_NP8gCgQ3\">スクリプト以外に実行したコマンド</h2><h3 id=\"index_VpcG8_8h\">サイトの画面を保存したPDFから画像を取り出し、ページを縦につないだ</h3><pre><code class=\"language-bash\">cd /home/claude/banner/site && pdfimages -j \"/root/.claude/uploads/6f54b9ec-7b51-53dd-a180-2e8bdb7969c6/f328e762-screencapture-adaptdesign-jp-pixelstomotors-2026-10-12-07_46_26.pdf\" pg && convert pg-000.jpg pg-001.jpg pg-002.jpg -append full.jpg && identify full.jpg && for i in 0 1 2 3 4; do convert full.jpg -crop 2728x2100+0+$((i*2100)) -resize 1100x prev$i.jpg; done; ls</code></pre><h3 id=\"index_naTwarcA\">つないだ画像から、サムネイルや4コマを切り出した</h3><pre><code class=\"language-python\">from PIL import Image\nim=Image.open('full.jpg')\nk=1/0.4032\ndef c(name, x0,y0,x1,y1,page):\n box=(int(x0*k),int(page*2100+y0*k),int(x1*k),int(page*2100+y1*k))\n im.crop(box).save(f'c_{name}.png'); print(name, box, (box[2]-box[0],box[3]-box[1]))\nc('hero',670,86,936,283,0)\nfor i,(a,b) in enumerate([(62,288),(312,538),(562,788),(812,1038)]):\n c(f'manga{i}',a,512,b,728,0)\nc('drug',62,158,535,481,1)\nc('lab',565,158,1038,481,1)\nc('imgrec_top',62,598,535,846,1)\nc('deep_top',565,598,1038,846,1)\nc('sim',62,245,535,533,2)\nc('flow',565,210,1038,533,2)\nc('rec_study',62,0,368,116,3); c('rec_micro',397,0,703,116,3); c('rec_say',731,0,1038,116,3)\nc('bowl',62,254,368,425,3); c('gpu',397,254,703,425,3); c('film',731,254,1038,425,3)\nc('panels324',62,605,368,775,3); c('panels323',397,605,703,775,3); c('who03',731,605,1038,775,3)\nc('inside',62,65,368,236,4); c('panels320',397,65,703,236,4); c('panels319',731,65,1038,236,4)\nc('issues',620,488,900,612,4)</code></pre><h3 id=\"index_egF7Khp0\">試作動画から、ハコブくんの表情のコマを書き出した</h3><pre><code class=\"language-bash\">cd /home/claude/banner/vid && V=\"/root/.claude/uploads/6f54b9ec-7b51-53dd-a180-2e8bdb7969c6/d2aa7563-hakobu_demo_upper.mp4\"; ffmpeg -v error -y -i \"$V\" -vf \"fps=3,crop=400:340:0:740\" h_%03d.png; ls h_*.png | wc -l; montage h_*.png -tile 10x -geometry 200x170+2+2 -label '%t' -pointsize 10 hsheet.jpg</code></pre><pre><code class=\"language-bash\">cd /home/claude/banner/vid && V=\"/root/.claude/uploads/6f54b9ec-7b51-53dd-a180-2e8bdb7969c6/d2aa7563-hakobu_demo_upper.mp4\"; for t in 0 3.0; do ffmpeg -v error -y -ss $t -i \"$V\" -frames:v 1 fr_$t.png; done</code></pre><pre><code class=\"language-bash\">cd /home/claude/banner/vid && V=\"/root/.claude/uploads/6f54b9ec-7b51-53dd-a180-2e8bdb7969c6/d2aa7563-hakobu_demo_upper.mp4\"; mkdir -p fx && for i in 1 4 8 9 15 24; do t=$(python3 -c \"print(($i-1)/3)\"); ffmpeg -v error -y -ss $t -i \"$V\" -frames:v 1 fx/e_$i.png; done; ls fx</code></pre><h3 id=\"index_hy_pvXuR\">動画のコマで、ハコブくんの範囲とディスプレイの位置を測った</h3><pre><code class=\"language-python\">from PIL import Image\nimport numpy as np\nim=Image.open('fr_0.png').convert('RGB')\na=np.array(im).astype(int)\nbg=np.array([245,245,240])\nreg=a[700:1080,0:420]\ndist=np.abs(reg-bg).sum(-1)\nys,xs=np.where(dist>18)\nprint('bbox',xs.min(),xs.max(),ys.min()+700,ys.max()+700)\nprint('corner color',a[720,5],a[1075,410])</code></pre><pre><code class=\"language-python\">from PIL import Image; import numpy as np\na=np.array(Image.open('fr_0.png').convert('RGB')).astype(int)[700:1080,0:420]\nys,xs=np.where(a.sum(-1)<90); print(xs.min(),xs.max(),ys.min()+700,ys.max()+700)</code></pre><h3 id=\"index_rLSvvOLQ\">三面図の3つの図の範囲、リングの中心と半径、口の位置を測った</h3><pre><code class=\"language-python\">from PIL import Image\nimport numpy as np\na=np.array(Image.open('src.png').convert('L')).astype(int)\n# column/row darkness profile to find view bboxes\ndark=a<120\ncols=dark[380:900].sum(0); rows=dark[:, :].sum(1)\nruns=[];inrun=False\nfor x,v in enumerate(cols):\n if v>3 and not inrun: s=x;inrun=True\n if v<=3 and inrun: runs.append((s,x));inrun=False\nprint([r for r in runs if r[1]-r[0]>20])\nfor (x0,x1) in [r for r in runs if r[1]-r[0]>20]:\n ys=np.where(dark[:,x0:x1].sum(1)>2)[0]; ys=ys[(ys>300)&(ys<1000)]; print(x0,x1,ys.min(),ys.max())\n# ring: find dark circle near side center</code></pre><pre><code class=\"language-python\">from PIL import Image\nimport numpy as np\na=np.array(Image.open('src.png').convert('L')).astype(float)\nbest=None\nfor cx in range(540,568,2):\n for cy in range(655,690,2):\n yy,xx=np.mgrid[cy-80:cy+80,cx-80:cx+80]; r=np.hypot(xx-cx,yy-cy); sub=a[cy-80:cy+80,cx-80:cx+80]\n prof=[sub[(r>=k)&(r<k+1)].mean() for k in range(30,75)]\n score=np.std(prof)\n sharp=sorted(prof)[:2]\n s=sum(sharp)\n if best is None or s<best[0]: best=(s,cx,cy,prof)\ns,cx,cy,prof=best; print(cx,cy); print([int(p) for p in prof])\n# mouth gray pixels\nsub=a[480:515,175:230]; ys,xs=np.where((sub>70)&(sub<200)); print('mouth', xs.min()+175,xs.max()+175,ys.min()+480,ys.max()+480)</code></pre><h3 id=\"index_kGmWoZfd\">2回目の全案を 2048×1152px で書き出した(1回目の写真は文字を外して作り直した)</h3><pre><code class=\"language-bash\">cd /home/claude/banner && python3 - <<'EOF'\nsrc=open('make.py').read()\nsrc=src[:src.index(\"import os\\nos.makedirs\")]\nexec(src)\ndef arm_clean():\n bg = Image.new('RGB', (W, H), MAT)\n s=0.74; subj=((1282+2650)/2, 1790)\n layer,_=place(bg, photo, s, subj, (1280, 730))\n ox=1280-subj[0]*s; oy=730-subj[1]*s\n mb=(ox+MAT_BOX[0]*s+70, oy+MAT_BOX[1]*s+70, ox+MAT_BOX[2]*s-70, oy+MAT_BOX[3]*s-70)\n bg.paste(ImageEnhance.Brightness(layer).enhance(0.95),(0,0),rect_mask(mb,60))\n return bg\nim=arm_clean(); im.save('/home/claude/banner/deliver/pixelstomotors_banner_A_arm_photo.jpg',quality=92,subsampling=0)\nEOF\nmkdir -p final_2048 && cd deliver && for f in pixelstomotors_banner_*.jpg; do n=${f%.jpg}; convert \"$f\" -resize 2048x1152! -quality 92 -sampling-factor 4:4:4 ../final_2048/${n}_2048x1152.jpg; done; cd ../final_2048 && identify -format \"%f %wx%h %b\\n\" *.jpg</code></pre><p></p><h2 id=\"index_dxzTRtXR\">実行ログ</h2><h3 id=\"index_6qQln2AW\">1回目の書き出し(make.py)</h3><pre><code>A_grasp 464 KB\nB_pixels_to_motors 341 KB\nC_see_decide_move 461 KB</code></pre><h3 id=\"index_fJsloe7H\">2回目の書き出し(make2.py)</h3><pre><code>B_archive_wall 769 KB\nC_mono_split 315 KB\nD_sim_halftone 1300 KB\nE_hakobu_manga 908 KB\nF_dot_matrix 669 KB\nG_hakobu_photo 170 KB</code></pre><h3 id=\"index_gljFeQAL\">サイトの画面から切り出した範囲</h3><pre><code>hero (1661, 213, 2321, 701) (660, 488)\nmanga0 (153, 1269, 714, 1805) (561, 536)\nmanga1 (773, 1269, 1334, 1805) (561, 536)\nmanga2 (1393, 1269, 1954, 1805) (561, 536)\nmanga3 (2013, 1269, 2574, 1805) (561, 536)\ndrug (153, 2491, 1326, 3292) (1173, 801)\nlab (1401, 2491, 2574, 3292) (1173, 801)\nimgrec_top (153, 3583, 1326, 4198) (1173, 615)\ndeep_top (1401, 3583, 2574, 4198) (1173, 615)\nsim (153, 4807, 1326, 5521) (1173, 714)\nflow (1401, 4720, 2574, 5521) (1173, 801)\nrec_study (153, 6300, 912, 6587) (759, 287)\nrec_micro (984, 6300, 1743, 6587) (759, 287)\nrec_say (1812, 6300, 2574, 6587) (762, 287)\nbowl (153, 6929, 912, 7354) (759, 425)\ngpu (984, 6929, 1743, 7354) (759, 425)\nfilm (1812, 6929, 2574, 7354) (762, 425)\npanels324 (153, 7800, 912, 8222) (759, 422)\npanels323 (984, 7800, 1743, 8222) (759, 422)\nwho03 (1812, 7800, 2574, 8222) (762, 422)\ninside (153, 8561, 912, 8985) (759, 424)\npanels320 (984, 8561, 1743, 8985) (759, 424)\npanels319 (1812, 8561, 2574, 8985) (762, 424)\nissues (1537, 9610, 2232, 9917) (695, 307)</code></pre><h3 id=\"index_JyBXovfG\">試作動画の情報と、ハコブくんの範囲</h3><pre><code>1920,1080,30/1,13.266667\n0/0,13.266000</code></pre><pre><code>bbox 29 351 766 1079\ncorner color [244 245 237] [244 245 237]</code></pre><pre><code>155 326 787 1022</code></pre><h3 id=\"index_GC_7inZM\">三面図の測定</h3><pre><code>[(70, 331), (396, 698), (749, 1012)]\n70 331 415 874\n396 698 419 874\n749 1012 420 875</code></pre><pre><code>554 663\n[250, 250, 249, 248, 247, 246, 245, 244, 242, 241, 240, 239, 238, 237, 235, 235, 234, 232, 225, 188, 140, 122, 140, 173, 179, 182, 188, 189, 177, 181, 174, 131, 114, 155, 194, 219, 227, 228, 229, 230, 230, 231, 232, 232, 233]\nmouth 184 215 490 505</code></pre><h3 id=\"index_htfRwQhT\">2048×1152px への書き出し</h3><pre><code>pixelstomotors_banner_A_arm_photo_2048x1152.jpg 2048x1152 478891B\npixelstomotors_banner_B_archive_wall_2048x1152.jpg 2048x1152 522786B\npixelstomotors_banner_C_mono_split_2048x1152.jpg 2048x1152 182721B\npixelstomotors_banner_D_sim_halftone_2048x1152.jpg 2048x1152 506840B\npixelstomotors_banner_E_hakobu_manga_2048x1152.jpg 2048x1152 506732B\npixelstomotors_banner_F_dot_matrix_2048x1152.jpg 2048x1152 481321B\npixelstomotors_banner_G_hakobu_photo_2048x1152.jpg 2048x1152 96050B</code></pre><h3 id=\"index_l3SpavP3\">3回目〜決定版の書き出し</h3><pre><code>1_illustration\n2_manga\n3_realistic\n4_photo\n5_graphic</code></pre><pre><code>I1_threeview\nI2_corridor\nM1_manga_page\nM2_cast</code></pre><pre><code>illust_flat\nillust_line\nmanga_cast</code></pre><pre><code>illust_flat_single</code></pre><p></p><h2 id=\"index_QUNQPzA5\">参照した情報</h2><ul><li><p><a href=\"https://support.google.com/youtube/answer/10456525?hl=ja\" data-has-link=\"true\">YouTube ヘルプ:チャンネルのブランディングを管理する</a></p></li><li><p><a href=\"https://support.google.com/youtube/answer/9481328?hl=ja\" data-has-link=\"true\">YouTube ヘルプ:チャンネルの権限</a></p></li><li><p><a href=\"/\" data-has-link=\"true\">Pixels to Motors</a></p></li></ul>", "toc": [{"id": "index__Ynzo3Cp", "text": "YouTube用のアカウントは、Google Workspaceのエイリアスではなく新しいユーザーにした"}, {"id": "index_8rfkxb9m", "text": "管理コンソールには、普段使いのアカウントでは入れなかった"}, {"id": "index_SZwLHwTg", "text": "「このユーザーは既に存在しています」の原因は、3月に付けたエイリアスだった"}, {"id": "index_EnRPO5Pq", "text": "作ろうとしたチャンネルのハンドルは、自分の既存チャンネルが使っていた"}, {"id": "index_04xwKQv5", "text": "既存チャンネルは、新しいアカウントを「管理者」として招待して引き継いだ"}, {"id": "index_6tAUffjk", "text": "チャンネルの説明文は、フィジカルAIを先頭に置いて書き直した"}, {"id": "index_GNMYvKmk", "text": "バナーは、どの端末でも切れない中央の範囲を基準に作った"}, {"id": "index_BpOCllXq", "text": "バナーの1回目は、実機の写真に文字を入れた3案だった"}, {"id": "index_KWVQksqt", "text": "2回目は、サイトの画像から6つの方向を出した"}, {"id": "index_wfU0AzRw", "text": "アイコンは、動画で話しているハコブくんの1コマから作った"}, {"id": "index_BZlJ8lML", "text": "バナーの3回目は、ハコブくんだけで5つのタッチを作った"}, {"id": "index_aoA9mNR9", "text": "5回目で、ハコブくんを背景と同じタッチで描き直した"}, {"id": "index_8PXH6omo", "text": "チャンネル名は「フィジカルAI解説|Pixels to Motors」を候補にした"}, {"id": "index_mtwyUJht", "text": "チャンネル名の変更と、アイコン・4コマの作り直しが残っている"}, {"id": "index_LGOmRd2E", "text": "制作スクリプトの全文"}, {"id": "index_NP8gCgQ3", "text": "スクリプト以外に実行したコマンド"}, {"id": "index_dxzTRtXR", "text": "実行ログ"}, {"id": "index_QUNQPzA5", "text": "参照した情報"}]}
\ No newline at end of file
diff --git a/src/lib/records.ts b/src/lib/records.ts
index 0c70d29..94c905e 100644
--- a/src/lib/records.ts
+++ b/src/lib/records.ts
@@ -28,11 +28,13 @@ export const allManga = mangaCards as MangaCard[];
export const allEnglish = englishCards as MangaCard[];
// シリーズのページ(URL → 記事のシリーズ名)。Studio の一覧の絞り込み条件と同じ
-export const SERIES_PAGES: Record<string, { series: string; title: string; pageTitle: string; limit: number }> = {
+// intro はシリーズのページの見出しの下に出す説明(ないシリーズは出さない)
+export const SERIES_PAGES: Record<string, { series: string; title: string; pageTitle: string; limit: number; intro?: string }> = {
series1: { series: 'HRI', title: 'Drug Delivery Robot | 薬剤搬送ロボットハコブくん', pageTitle: 'Series1 | 薬剤搬送ロボット', limit: 12 },
series2: { series: 'R2R2R', title: 'R2R2R | デジタルツイン学習-R2R2R(Real to Render to Real)', pageTitle: 'Series2 | R2R2R', limit: 12 },
series3: { series: 'UV-C農業ロボット', title: 'UV-C Agricultural Robot | UV-C農業ロボット', pageTitle: 'Series3 | UV-C農業ロボット', limit: 20 },
- series4: { series: 'ロボティクスデザイン', title: 'Robotics Design Workflow | ロボティクスデザイン実務フロー', pageTitle: 'Series4 | ロボティクスデザイン実務フロー', limit: 12 },
+ series4: { series: 'ロボティクスデザイン', title: 'Robotics Design Workflow | ロボティクスデザイン実務フロー', pageTitle: 'Series4 | ロボティクスデザイン実務フロー', limit: 12,
+ intro: 'フィジカルAIの研究をひとりで続けるために、AIと一緒に作ってきた仕組みの記録です。サイトの移行、動画の制作、記録の整理など、研究の外側の作業をどう回しているかを残しています。' },
series5: { series: '考察記事', title: 'Deep Dive | 考察記事', pageTitle: 'Series5 | 考察記事', limit: 12 },
series6: { series: 'フィジカルAI研究室', title: 'Physical AI Lab | フィジカルAI研究室', pageTitle: 'Series6 | フィジカルAI研究室', limit: 12 },
series7: { series: '画像認識・物体検出', title: 'Image recognition and Object detection | 画像認識と物体検出', pageTitle: 'Series7 | 画像認識と物体検出', limit: 12 },
diff --git a/src/pages/[series].astro b/src/pages/[series].astro
index b053f14..d60b6f3 100644
--- a/src/pages/[series].astro
+++ b/src/pages/[series].astro
@@ -16,7 +16,7 @@ const conf = SERIES_PAGES[series];
const blocks = (staticPages[series]?.blocks || []).slice(1); // 先頭はページの見出し
const items = await seriesCards(conf.series);
const [enName, jaName] = conf.title.split(' | ');
-const description = `${SITE_NAME} のシリーズ「${jaName || conf.title}」(${enName})の記録一覧。ロボットとフィジカルAIを、29年のデザイナー経験から実機とシミュレーションで試した過程を、失敗も含めて公開しています。`;
+const description = conf.intro ? `${SITE_NAME} のシリーズ「${jaName || conf.title}」(${enName})。${conf.intro}` : `${SITE_NAME} のシリーズ「${jaName || conf.title}」(${enName})の記録一覧。ロボットとフィジカルAIを、29年のデザイナー経験から実機とシミュレーションで試した過程を、失敗も含めて公開しています。`;
const jsonLd = {
'@context': 'https://schema.org', '@type': 'CollectionPage', name: conf.title, description, url: `${SITE}/${series}`,
inLanguage: 'ja',
@@ -26,6 +26,7 @@ const jsonLd = {
<Base title={`${jaName || conf.title}(${enName})| ${SITE_NAME}`} description={description} path={`/${series}`} jsonLd={jsonLd}>
<section class="s-hero"><div class="panel">
<h1 class="sec-h s-title">{conf.title}</h1>
+ {conf.intro && <p class="s-intro">{conf.intro}</p>}
{blocks.map((b) => (
b.type === 'text' ? (b.link ? <a class="s-label" href={b.link} target="_blank" rel="noopener" set:html={b.html} /> : <p class="s-label" set:html={b.html} />)
: b.type === 'iframe' ? <div class="s-frame" style={`height:${b.height}px`} set:html={b.html} />
@@ -42,6 +43,7 @@ const jsonLd = {
<style>
.s-hero { padding: 88px 80px 0; }
.s-title { margin: 0; }
+ .s-intro { max-width: 760px; margin: 16px 0 0; font: 400 16px/28px var(--sans); color: var(--ink-2); }
.s-label { display: block; margin: 40px 0 16px; font: 500 16px/24px var(--sans); color: var(--ink); }
.s-title + .s-label { margin-top: 8px; }
a.s-label:hover { text-decoration: underline; }
diff --git a/src/pages/message.astro b/src/pages/message.astro
index d11904a..ad462ea 100644
--- a/src/pages/message.astro
+++ b/src/pages/message.astro
@@ -10,7 +10,14 @@ import Base from '../layouts/Base.astro';
<form class="msg-form" id="msgForm" novalidate>
<label>お名前<span class="req">*</span><input name="name" type="text" placeholder="氏名" required autocomplete="name" /></label>
<label>メールアドレス<span class="req">*</span><input name="email" type="email" placeholder="XXXXXXXX@compay.com" required autocomplete="email" /></label>
- <label>ご相談内容<span class="req">*</span><textarea name="body" rows="9" placeholder="現在〇〇の現場でロボット導入を検討中ですが、シミュレーションでどこまで検証できるか知りたいです」「URDFデータはありますが、Isaac Simへの移行で困っています。" required></textarea></label>
+ <label>ご相談の種類<span class="req">*</span><select name="topic" id="msgTopic" required>
+ <option value="" disabled selected>選んでください</option>
+ <option value="physical-ai">フィジカルAI・ロボット</option>
+ <option value="ai">AI活用・仕組みづくり</option>
+ <option value="media">取材・登壇</option>
+ <option value="other">その他</option>
+ </select></label>
+ <label>ご相談内容<span class="req">*</span><textarea name="body" rows="9" placeholder="「現在〇〇の現場でロボット導入を検討中ですが、シミュレーションでどこまで検証できるか知りたいです」「URDFデータはありますが、Isaac Simへの移行で困っています。" required></textarea></label>
<label class="agree"><input name="agree" type="checkbox" required />送信をもって<a href="/privacypolicy">プライバシーポリシー</a>に同意いただいたものとみなします。</label>
<button class="send" type="submit"><span class="send-ic"><i class="ms">send</i></span><span class="send-t">送信する</span></button>
<p class="msg-note" id="msgNote" hidden></p>
@@ -21,6 +28,10 @@ import Base from '../layouts/Base.astro';
const form = document.getElementById('msgForm') as HTMLFormElement;
const note = document.getElementById('msgNote') as HTMLParagraphElement;
const say = (t: string) => { note.textContent = t; note.hidden = false; };
+ // 記事の案内(/message?topic=ai など)から来たときは、相談の種類を選んだ状態にする
+ const topicSel = document.getElementById('msgTopic') as HTMLSelectElement;
+ const preset = new URLSearchParams(location.search).get('topic');
+ if (preset && [...topicSel.options].some((o) => o.value === preset && !o.disabled)) topicSel.value = preset;
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (!form.reportValidity()) return;
@@ -28,9 +39,9 @@ import Base from '../layouts/Base.astro';
const btn = form.querySelector('button')!; btn.disabled = true;
try {
const r = await fetch('/api/message', { method: 'POST', headers: { 'content-type': 'application/json' },
- body: JSON.stringify({ name: f.get('name'), email: f.get('email'), body: f.get('body'), agree: f.get('agree') === 'on' }) });
+ body: JSON.stringify({ name: f.get('name'), email: f.get('email'), topic: f.get('topic'), body: f.get('body'), agree: f.get('agree') === 'on' }) });
const d = await r.json();
- if (d.ok) { form.reset(); say('送信しました。1〜2営業日以内にお返事いたします。'); }
+ if (d.ok) { form.reset(); topicSel.value = ''; say('送信しました。1〜2営業日以内にお返事いたします。'); }
else say(d.error || '送信できませんでした。時間をおいてもう一度お試しください。');
} catch { say('送信できませんでした。時間をおいてもう一度お試しください。'); }
btn.disabled = false;
@@ -43,8 +54,9 @@ import Base from '../layouts/Base.astro';
.msg-form { display: flex; flex-direction: column; }
.msg-form label { display: block; margin-bottom: 16px; font: 500 16px/24px var(--sans); }
.req { color: #ff4000; margin-left: 4px; font: 700 15px/21px var(--sans); }
- .msg-form input[type=text], .msg-form input[type=email], .msg-form textarea { display: block; width: 100%; margin-top: 8px; padding: 8px 12px;
+ .msg-form input[type=text], .msg-form input[type=email], .msg-form select, .msg-form textarea { display: block; width: 100%; margin-top: 8px; padding: 8px 12px;
background: #fdfdfd; border: 1px solid #aeadab; border-radius: 4px; font: 400 14px/21px var(--sans); color: #333; }
+ .msg-form select { height: 39px; appearance: auto; }
.msg-form textarea { resize: vertical; min-height: 240px; }
.agree { font: 500 14px/21px var(--sans) !important; display: flex !important; gap: 10px; align-items: flex-start; }
.agree input { margin-top: 4px; }
diff --git a/src/pages/record/[slug].astro b/src/pages/record/[slug].astro
index 25952c0..e7971c5 100644
--- a/src/pages/record/[slug].astro
+++ b/src/pages/record/[slug].astro
@@ -7,6 +7,7 @@ import { SERIES_PAGES, allCards } from '../../lib/records';
import RecordCard from '../../components/RecordCard.astro';
import { getCollection, render } from 'astro:content';
import { existsSync } from 'node:fs';
+import aiConsult from '../../data/aiConsult.json';
// 記事ページは2種類から作る
// - Studio から移した記事:CMS の本文 HTML(src/data/records/。tools/import_all.py で作る)
@@ -67,6 +68,9 @@ const older = pos >= 0 && pos < sameSeries.length - 1 ? sameSeries[pos + 1] : nu
const others = sameSeries.filter((c) => c.slug !== r.slug);
const start = Math.max(0, Math.min((pos < 0 ? 0 : pos) - 3, others.length - 6));
const nearby = others.slice(start, start + 6);
+// AI活用の記事(研究を回すために作った仕組みの記録)の末尾に、相談の案内を出す。
+// Markdown の記事は先頭の項目 ai_consult: true、Studio から移した記事は src/data/aiConsult.json の slug で決める
+const showConsult = kind === 'md' ? entry.data.ai_consult === true : (aiConsult as string[]).includes(r.slug);
const url = `${SITE}/record/${r.slug}`;
const jsonLd = [
{
@@ -102,6 +106,12 @@ const jsonLd = [
{kind === 'studio'
? <div class="richText" set:html={body} />
: body ? <div class="richText" set:html={body} /> : <div class="richText"><Fragment set:html={toc} /><Content /></div>}
+ {showConsult && (
+ <aside class="consult" aria-label="ご相談について">
+ <p>この記事の仕組みは、フィジカルAIの研究をひとりで回すために作ったものです。同じような仕組みを仕事に取り入れたい方は、メッセージからご相談ください。</p>
+ <a class="btn-dark" href="/message?topic=ai">メッセージで相談する<i class="ms">arrow_forward</i></a>
+ </aside>
+ )}
{(newer || older) && (
<nav class="pager" aria-label="同じシリーズの前後の記事">
{older ? <a class="pg pg-prev" href={`/record/${older.slug}`}><span class="pg-k">← 前の記事 {older.day}</span><span class="pg-t">{older.title}</span></a> : <span />}
@@ -138,6 +148,9 @@ const jsonLd = [
.rel-more { display: flex; justify-content: center; margin-top: 40px; }
.rel-more .btn-dark { height: auto; min-height: 40px; padding: 8px 20px; white-space: normal; text-align: center; }
@media (max-width: 599px) { .pager { grid-template-columns: 1fr; } .pg-next { text-align: left; } }
+ .consult { margin: 56px 0 0; padding: 24px; border-radius: 8px; background: #f0f0f0; }
+ .consult p { margin: 0 0 16px; font: 400 16px/28px var(--sans); color: var(--ink); }
+ .consult .btn-dark { height: auto; min-height: 40px; padding: 8px 20px; white-space: normal; }
.rec .meta { display: flex; align-items: center; gap: 12px; margin-top: 16px; }
.rec .day { font: 400 28px/28px var(--sans); }
.rec .title { margin: 12px 0 40px; font: 600 28px/42px var(--sans); overflow-wrap: anywhere; }
diff --git a/worker/index.js b/worker/index.js
index 617e144..72a6a56 100644
--- a/worker/index.js
+++ b/worker/index.js
@@ -57,6 +57,9 @@ async function media(request, env, url) {
return new Response(request.method === 'HEAD' ? null : obj.body, { status, headers });
}
+// メッセージフォームの「ご相談の種類」。値はフォーム(src/pages/message.astro)の option と同じ
+const TOPICS = { 'physical-ai': 'フィジカルAI・ロボット', ai: 'AI活用・仕組みづくり', media: '取材・登壇', other: 'その他' };
+
async function message(request, env) {
if (request.method !== 'POST') return new Response('Method Not Allowed', { status: 405 });
let d;
@@ -64,16 +67,18 @@ async function message(request, env) {
const name = String(d.name || '').trim().slice(0, 200);
const email = String(d.email || '').trim().slice(0, 300);
const body = String(d.body || '').trim().slice(0, 10000);
+ // 相談の種類(フォームの選択肢と同じ値)。古いページから送られて種類がないときは「未選択」として受け取る
+ const topic = Object.hasOwn(TOPICS, d.topic) ? d.topic : '';
if (!name || !email || !body || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || d.agree !== true) {
return json({ ok: false, error: '入力を確かめてください' }, 400);
}
const at = new Date().toISOString();
const key = `${at.replace(/[:.]/g, '-')}-${crypto.randomUUID().slice(0, 8)}.json`;
- await env.MESSAGES.put(key, JSON.stringify({ at, name, email, body, ua: request.headers.get('user-agent') || '' }, null, 1),
+ await env.MESSAGES.put(key, JSON.stringify({ at, name, email, topic, body, ua: request.headers.get('user-agent') || '' }, null, 1),
{ httpMetadata: { contentType: 'application/json' } });
// 保存できたら、メールで知らせる。メールが送れなくても、保存は済んでいるので送信者には成功と返す
try {
- await notify(env, { at, name, email, body, key });
+ await notify(env, { at, name, email, topic, body, key });
} catch (e) {
console.error('メール通知に失敗', e && e.message);
}
@@ -86,8 +91,10 @@ async function notify(env, m) {
if (!env.MAIL) return;
const from = 'form@pixelstomotors.com';
const to = 'pixelstomotors@adaptdesign.jp';
- const subject = `[Pixels to Motors] メッセージ: ${m.name}`;
+ const topicName = Object.hasOwn(TOPICS, m.topic) ? TOPICS[m.topic] : '未選択';
+ const subject = `[Pixels to Motors] ${topicName}: ${m.name}`;
const text = [
+ `種類: ${topicName}`,
`名前: ${m.name}`,
`メール: ${m.email}`,
`受け取り: ${m.at}`,
commit 30069f8 ― トップの説明文を今の研究の中身に合わせる
commit 30069f84048da0a87e467325f4a286764091d219
Author: Claude <noreply@anthropic.com>
Date: Sat Oct 17 12:32:47 2026 +0900
トップの説明文を今の研究の中身に合わせる
「HRIシミュレーション・実機はその先」から、実機のアームでデータを集めて学習・推論まで回し、
誰でも同じ手順で始められる形で公開している、という今の中身へ。見出しのキャッチコピーと肩書きは変えない。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8PyTPCaRx7Mg89h22HK6a
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 06bfeea..90de929 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -38,13 +38,13 @@ const jsonLd = [
];
---
<Base title="Pixels to Motors | 人とロボットの関わりを、シミュレーションでデザインする"
- description="29年のデザイナー経験を、いまロボティクスに応用しています。テーマはHRIシミュレーション——人がロボットをどう感じ、どう関わるかを、実機の前にデジタル上で設計すること。"
+ description="29年のデザイナー経験を、いまロボティクスに応用しています。机の上のロボットアームでお手本のデータを集め、AIに学習させ、動かして確かめる。その環境を自宅で組み、誰でも同じ手順で始められる形で公開しています。"
ogImage="/site/ffb300c2-1071-4714-978c-87d386d5dfd3.png" path="/" jsonLd={jsonLd}>
<section class="hero">
<div class="hero-text">
<h1>Pixels to Motors | 人とロボットの関わりをシミュレーションでデザインする</h1>
- <p>29年のデザイナー経験を、いまロボティクスに応用しています。テーマはHRIシミュレーション——人がロボットをどう感じ、どう関わるかを、実機の前にデジタル上で設計すること。Unityで空間もシナリオもゼロから組み、人の間を動いて運び・手渡すサービスロボットの振る舞いを、成否の基準を物理精度ではなく人の体験に置いてつくります。実機はその先。過程の失敗も含めて公開しています。</p>
+ <p>29年のデザイナー経験を、いまロボティクスに応用しています。机の上のロボットアームでお手本のデータを集め、AIに学習させ、動かして確かめる。その環境を自宅で組み、誰でも同じ手順で始められる形で公開しています。基準は物理精度ではなく、人がロボットをどう感じ、どう関わるかという体験の成立。過程の失敗も含めて公開しています。</p>
</div>
<video class="hero-video" src={media("/media/s-848x1056_e5500e41-e73b-4177-914e-b09a2d11f83c.mp4") + "#t=0.01"} controls muted autoplay loop playsinline></video>
</section>
@@ -91,11 +91,11 @@ const jsonLd = [
</div>
<div class="prop-body">
<p class="prop-lead">画面(Pixels)のデザインから、動き(Motors)のデザインへ</p>
- <p class="prop-text">29年にわたるデザイナーとしての知見を土台に、人とロボットの関わり(HRI)をシミュレーションで設計するデザイン事務所です。Unityなどのツールで空間もシナリオもゼロから組み、AIエージェント(Claude Code)を設計と実装の相棒に、人がロボットをどう感じ・どう関わるかを実機の前にデザインします。基準は物理精度ではなく体験の成立。画面上の「Pixels(デジタル)」の設計を、やがて現実の「Motors(モーター)」へ——その前半、体験を確かめるフェーズを公開しています。</p>
+ <p class="prop-text">29年にわたるデザイナーとしての知見を土台に、人とロボットの関わり(HRI)と、ロボットが学習できる環境づくりに取り組むデザイン事務所です。Unityでのシミュレーション、実機のロボットアーム、借りたGPUでの学習・推論までを自分の手で回し、AIエージェント(Claude Code)を設計と実装の相棒にしています。基準は物理精度ではなく体験の成立。画面上の「Pixels(デジタル)」の設計を、現実の「Motors(モーター)」へ——その過程を公開しています。</p>
<dl class="prop-facts">
<div><dt>事業所名</dt><dd>ADAPT DESIGN</dd></div>
<div><dt>設立</dt><dd>2021年10月</dd></div>
- <div class="wide"><dt>研究・実践分野</dt><dd>HRIシミュレーション / ロボティクスUX設計 / デジタルツイン / Unity / Physical AI / R2R2R</dd></div>
+ <div class="wide"><dt>研究・実践分野</dt><dd>フィジカルAI(学習・推論) / HRI / ロボティクスUX設計 / デジタルツイン / 画像認識 / Unity / R2R2R</dd></div>
<div><dt>SNS</dt><dd class="sns">
<a href="https://github.com/PixelstoMotors/r2r2r-content" aria-label="GitHub" set:html={icGithub} />
<a href="https://www.linkedin.com/in/eiichi-shimazu/" aria-label="LinkedIn" set:html={icLinkedin} />
追記 ― 同じ日に、実務フローを「ひとり研究所の裏側」に改め、合わない6本を元のシリーズへ移した
この記事を公開したあとで、実務フローのシリーズそのものを見直しています。きっかけは次の一言でした。
このロボティクスデザイン実務フローのカテゴリーを直したいけど、いいアイデアある? 特に分類しずらいやつになってるけど。
入っていた23本を中身で分けると、名前の「ロボティクスデザイン」に当てはまるのはハコブくんの設計の3本だけでした。どこにも入れにくい記事の受け皿になっていたということです。
| 中身 | 記事 | 本数 |
|---|---|---|
| 研究と発信を回す仕組みを、AI で作った記録 | LOG56・193・328・330〜335・338〜340・342〜344・346・347 | 17 |
| ハコブくんの UI・フロー図の設計 | LOG50・51・303 | 3 |
| 考察 | LOG53・LOG233 | 2 |
| 画像認識 | LOG236 | 1 |
そこで、外れている6本を本来のシリーズへ移し、残った17本に合う名前を付けました。名前は3案から選んでもらい、「ひとり研究所の裏側」(Behind the Solo Lab)に決まりました。選んだ理由はこうです。
ひとり研究所の裏側 が良さそう(ひっそりとしていて)
名前に「AI活用」を入れなかったのは、シリーズ名がトップの記事一覧の札にも出るためです。トップでは AI活用を出さない、と本文で決めたことと合わせています。
| 変えたもの | 前 | 後 |
|---|---|---|
| シリーズの名前(/series4) | ロボティクスデザイン実務フロー(Robotics Design Workflow) | ひとり研究所の裏側(Behind the Solo Lab) |
| LOG50・51・303 | 実務フロー | 薬剤搬送ロボットハコブくん |
| LOG53・233 | 実務フロー | 考察記事 |
| LOG236 | 実務フロー | 画像認識と物体検出 |
| シリーズページの上の資料 | FigJam のフロー図・スプレッドシート「ロボティクスデザイン実務フロー可視化」・課題地図の埋め込み | FigJam とスプレッドシートはハコブくんのページへ。課題地図は /issues へのリンク1本 |
| トップのシリーズのカード | 実務フローの名前と、ハコブくんの操作フロー図の画像 | 新しい名前と英語の説明。画像は新しく作った(下) |
ページの住所(/series4)と、移した記事の URL は変えていないので、リンク切れは起きません。この記事の本文の「ロボティクスデザイン実務フロー」は、書いた時点の名前のまま残しています。
移したあとに、各シリーズのページに並ぶ記事の番号を数えて確かめました。
$ npm run build; cd dist && for s in series1 series4 series5 series7; do printf "%s: " $s; grep -o 'LOG[0-9]\+' $s.html | sort -u -V | tr '\n' ' '; echo; done; grep -o '<title>[^<]*' series4.html
build=0
14:19:57 [build] Complete!
series1: LOG1 LOG2 LOG3 LOG4 LOG5 LOG6 LOG7 LOG8 LOG9 LOG10 LOG11 LOG12 LOG13 LOG14 LOG15 LOG16 LOG17 LOG18 LOG19 LOG38 LOG39 LOG43 LOG44 LOG45 LOG46 LOG47 LOG48 LOG49 LOG50 LOG51 LOG54 LOG55 LOG57 LOG58 LOG59 LOG60 LOG61 LOG62 LOG63 LOG64 LOG65 LOG111 LOG116 LOG117 LOG127 LOG138 LOG141 LOG171 LOG267 LOG269 LOG303 LOG305 LOG307 LOG308 LOG311 LOG321 LOG322
series4: LOG56 LOG193 LOG328 LOG330 LOG331 LOG332 LOG333 LOG334 LOG335 LOG338 LOG339 LOG340 LOG342 LOG343 LOG344 LOG346 LOG347
series5: LOG53 LOG107 LOG109 LOG110 LOG112 LOG115 LOG118 LOG119 LOG121 LOG126 LOG128 LOG135 LOG140 LOG143 LOG145 LOG147 LOG152 LOG155 LOG159 LOG161 LOG166 LOG167 LOG174 LOG181 LOG187 LOG194 LOG195 LOG198 LOG201 LOG205 LOG206 LOG207 LOG212 LOG214 LOG218 LOG219 LOG221 LOG223 LOG229 LOG233 LOG237 LOG245 LOG249 LOG260 LOG262 LOG264 LOG276 LOG278 LOG282 LOG286 LOG290 LOG291 LOG292 LOG296 LOG306 LOG312 LOG314 LOG315 LOG316 LOG318 LOG319 LOG320 LOG323 LOG324 LOG329 LOG336
series7: LOG236 LOG298 LOG299 LOG300 LOG301 LOG302 LOG304 LOG309 LOG310 LOG313 LOG317 LOG325
<title>ひとり研究所の裏側(Behind the Solo Lab)| Pixels to Motors
ひとり研究所の裏側(series4)に17本、ハコブくん(series1)に LOG50・51・303、考察記事(series5)に LOG53・233、画像認識(series7)に LOG236 が入りました。
commit d858902 ― シリーズの名前を変え、6本を移した変更の全量(この記事のファイルの変更を除く)
commit d858902fa306e35902a721fe94b1c4bca40dff34
Author: Claude <noreply@anthropic.com>
Date: Sat Oct 17 14:21:06 2026 +0900
シリーズ「ロボティクスデザイン実務フロー」を「ひとり研究所の裏側」に変え、合わない6本を元のシリーズへ移す
- 研究と発信を回す仕組みの記録17本を「ひとり研究所の裏側」(Behind the Solo Lab・/series4)にする
- LOG50・51・303 → ハコブくん(HRI)、LOG53・233 → 考察記事、LOG236 → 画像認識・物体検出
- series4 の上にあった FigJam のフロー図とスプレッドシートは series1 へ。課題地図の埋め込みは /issues へのリンクにした
- トップのシリーズのカードの名前・説明・画像(/og/LOG339.jpg)を変えた
- シリーズページの説明のリンクが中の住所なら、新しいタブで開かない
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8PyTPCaRx7Mg89h22HK6a
diff --git a/README.md b/README.md
index 9ac099e..b0b62a6 100644
--- a/README.md
+++ b/README.md
@@ -35,5 +35,5 @@ GitHub の main に push すると、Cloudflare が自動で作り直して公
## AI活用の記事と相談の案内
-AI活用の記事(フィジカルAIの研究を回すために作った仕組みの記録)は、シリーズ「ロボティクスデザイン実務フロー」(`series: "ロボティクスデザイン"`)に置く。「フィジカルAI研究室」は学習・推論を回す回だけにする。
+AI活用の記事(フィジカルAIの研究を回すために作った仕組みの記録)は、シリーズ「ひとり研究所の裏側」(`series: "ひとり研究所の裏側"`・/series4。2026-10-17 に「ロボティクスデザイン実務フロー」から名前を変えた)に置く。「フィジカルAI研究室」は学習・推論を回す回だけにする。
AI活用の記事の末尾には、相談の案内(`/message?topic=ai` への導線)を出す。出す記事は、Markdown の記事なら先頭の項目に `ai_consult: true` を書く。Studio から移した記事は `src/data/aiConsult.json` に slug を足す。サイトのトップ・SNS・動画には AI活用の相談を出さない。
diff --git a/src/content/records/DAY334.md b/src/content/records/DAY334.md
index c4946cb..30b0f17 100644
--- a/src/content/records/DAY334.md
+++ b/src/content/records/DAY334.md
@@ -5,7 +5,7 @@ date: 2026-10-12
day: 334
meta_description: "Studio製のサイトの全533ページを、Studioがなくても読める形で1フォルダにまとめた。画像・図解・作業ファイル・コードも収め、別のAIのcodexに2回検証させて、変換の崩れと作業場所の取り違えを直した。"
tags: [サイトアーカイブ, Studio, CMS, Markdown, Nuxt, iCloud, codex, 敵対的検証, Python, JavaScript]
-series: "ロボティクスデザイン"
+series: "ひとり研究所の裏側"
ai_consult: true
---
diff --git a/src/content/records/DAY335.md b/src/content/records/DAY335.md
index 68198ac..2aed2f2 100644
--- a/src/content/records/DAY335.md
+++ b/src/content/records/DAY335.md
@@ -5,7 +5,7 @@ date: 2026-10-12
day: 335
meta_description: "フィジカルAIの課題をまとめたサイトから動画ネタを25本出し、見る前に気になるかで15本に絞った。完成済みの台本と同じ形式で15本を書き、別の担当の事実確認で29か所を直した。"
tags: [YouTube, 解説動画, 台本, ネタ出し, 事実確認, セキュリティ, ハコブくん, Claude]
-series: "ロボティクスデザイン"
+series: "ひとり研究所の裏側"
ai_consult: true
---
diff --git a/src/content/records/DAY338.md b/src/content/records/DAY338.md
index 4b4a872..6db2e89 100644
--- a/src/content/records/DAY338.md
+++ b/src/content/records/DAY338.md
@@ -5,7 +5,7 @@ date: 2026-10-15
day: 338
meta_description: "YouTube解説動画の作り方を2つ足した。自分で試す回の台本の型と、画面の文字を名前・数字・一言の15字以内にする大文字型。大文字型で「ロボットの動きはなぜ怖い?」を11分50秒の本編とショート3本まで作った。"
tags: [YouTube, 解説動画, ハコブくん, 動画制作, 画面設計, 不気味の谷, codex, ElevenLabs, Python, Playwright]
-series: "ロボティクスデザイン"
+series: "ひとり研究所の裏側"
ai_consult: true
---
diff --git a/src/content/records/DAY339.md b/src/content/records/DAY339.md
index 7224305..d7b2a88 100644
--- a/src/content/records/DAY339.md
+++ b/src/content/records/DAY339.md
@@ -5,7 +5,7 @@ date: 2026-10-16
day: 339
meta_description: "並行する解説動画21本の段階と誰の番かを、フォルダとYouTubeから自動で判定するページを作った。公開予約はYouTube Data APIで読み、PCは一覧、iPhoneはハコブくんのカードで1件ずつ片付ける形にした。"
tags: [YouTube, 解説動画, ハコブくん, 進捗管理, YouTube Data API, Claude Code, プラグイン, iPhone, Python, JavaScript]
-series: "ロボティクスデザイン"
+series: "ひとり研究所の裏側"
ai_consult: true
---
diff --git a/src/content/records/DAY340.md b/src/content/records/DAY340.md
index 7a4664f..a596570 100644
--- a/src/content/records/DAY340.md
+++ b/src/content/records/DAY340.md
@@ -5,7 +5,7 @@ date: 2026-10-16
day: 340
meta_description: "Studio製のサイト553ページを、Astro+Cloudflare Workers で push すると約40秒で公開される形に移した。画像1,004点は R2 へ。本文・画像数・図解数を全ページ照合し一致。"
tags: [Studio, Astro, Cloudflare Workers, R2, GitHub, 自動公開, サイト移行, Cloudflare Access, Playwright, Python]
-series: "ロボティクスデザイン"
+series: "ひとり研究所の裏側"
ai_consult: true
---
diff --git a/src/content/records/DAY342.md b/src/content/records/DAY342.md
index a941060..e75cf1c 100644
--- a/src/content/records/DAY342.md
+++ b/src/content/records/DAY342.md
@@ -5,7 +5,7 @@ date: 2026-10-16
day: 342
meta_description: "サイト用に pixelstomotors.com を取得し、URL から /pixelstomotors を外した。旧URL554件と内部リンク1万5千本余りを照合。動画が再生されない原因は Range 応答の NaN だった。"
tags: [Cloudflare Workers, Cloudflare Registrar, R2, HTTP Range, ドメイン, サイト移行, Astro, 301リダイレクト, アクセス解析, UX]
-series: "ロボティクスデザイン"
+series: "ひとり研究所の裏側"
ai_consult: true
---
diff --git a/src/content/records/LOG343.md b/src/content/records/LOG343.md
index fc5c3ca..7873ba0 100644
--- a/src/content/records/LOG343.md
+++ b/src/content/records/LOG343.md
@@ -5,7 +5,7 @@ date: 2026-10-16
day: 343
meta_description: "pixelstomotors.com を公開し、フォームの通知メール、adaptdesign.jp の DNS 移行と記事まで届く転送、Studio の非公開化、記事番号の DAY → LOG 改番までを行った記録。"
tags: [Cloudflare, Cloudflare Access, Email Routing, DNS, DNSSEC, Redirect Rules, Squarespace, Studio, サイト移行, Astro]
-series: "ロボティクスデザイン"
+series: "ひとり研究所の裏側"
ai_consult: true
---
diff --git a/src/content/records/LOG344.md b/src/content/records/LOG344.md
index 4c97d8b..7c439f2 100644
--- a/src/content/records/LOG344.md
+++ b/src/content/records/LOG344.md
@@ -5,7 +5,7 @@ date: 2026-10-17
day: 344
meta_description: "公開中のサイトに検索除外の設定が残っていた。AIが全557ページを点検し、サイトマップ・画像の説明1万枚・構造化データ・日英の対応・SNS用画像・アクセス解析までを直した記録。"
tags: [SEO, noindex, sitemap.xml, alt, 構造化データ, JSON-LD, hreflang, OGP, Google Search Console, Cloudflare Web Analytics, Astro, Playwright]
-series: "ロボティクスデザイン"
+series: "ひとり研究所の裏側"
ai_consult: true
---
diff --git a/src/content/records/LOG346.md b/src/content/records/LOG346.md
index 98f017d..30ee097 100644
--- a/src/content/records/LOG346.md
+++ b/src/content/records/LOG346.md
@@ -5,7 +5,7 @@ date: 2026-10-17
day: 346
meta_description: "フィジカルAIの課題地図を自分のサイトに移し、週1回AIが出典つきで最近の動きを足す仕組みにした。課題マップは5回作り直して元に戻し、更新を知らせて走るハコブくんと、その効果の記録を足した。"
tags: [課題地図, フィジカルAI, Astro, Cloudflare Workers, R2, 予約タスク, 自動更新, ハコブくん, アクセス解析, プライバシー]
-series: "ロボティクスデザイン"
+series: "ひとり研究所の裏側"
ai_consult: true
---
diff --git a/src/data/cards.json b/src/data/cards.json
index 2ce9d88..27816ab 100644
--- a/src/data/cards.json
+++ b/src/data/cards.json
@@ -109,7 +109,7 @@
},
{
"day": "LOG233",
-"series": "ロボティクスデザイン",
+"series": "考察記事",
"published": "2026-06-13T02:55:39.000Z",
"title": "フィジカルAIの社会実装を止めるのは、デモではなく構造的な課題だ ― 36課題を地図にした",
"slug": "build-in-public-double-structure",
@@ -1270,7 +1270,7 @@
},
{
"day": "LOG50",
-"series": "ロボティクスデザイン",
+"series": "HRI",
"published": "2026-01-28T22:10:00.000Z",
"title": "FigJam × ハコブくん設計。なんとなくのデザインではなく仕様としてのフロー図作成",
"slug": "day50",
@@ -1279,7 +1279,7 @@
},
{
"day": "LOG51",
-"series": "ロボティクスデザイン",
+"series": "HRI",
"published": "2026-01-29T22:00:00.000Z",
"title": "FigJamで描くロボットの意思|物理空間を制御するフロー図の構築",
"slug": "day51",
@@ -1297,7 +1297,7 @@
},
{
"day": "LOG53",
-"series": "ロボティクスデザイン",
+"series": "考察記事",
"published": "2026-02-01T13:16:00.000Z",
"title": "フィジカルAI時代のデザイン思想。ロボティクス領域でデザインが利益率を決定する理由",
"slug": "day53",
@@ -1324,7 +1324,7 @@
},
{
"day": "LOG56",
-"series": "ロボティクスデザイン",
+"series": "ひとり研究所の裏側",
"published": "2026-02-04T11:58:00.000Z",
"title": "GeminiでGemini活用レベルを査定する。2026年1月時点の生成AIリテラシー自己評価|Master(上位3.2%)",
"slug": "day56",
@@ -1855,7 +1855,7 @@
},
{
"day": "LOG331",
-"series": "ロボティクスデザイン",
+"series": "ひとり研究所の裏側",
"published": "2026-10-12T00:08:34.000Z",
"title": "解説動画のナレーションをハコブくんに任せる ― 口と目と光をコードで動かし、文字が重ならない区画を決めた",
"slug": "explainer-video-character-narration",
@@ -1873,7 +1873,7 @@
},
{
"day": "LOG303",
-"series": "ロボティクスデザイン",
+"series": "HRI",
"published": "2026-07-31T01:07:33.000Z",
"title": "Figmaのデザイントークンを整理する | 生hex 3,977箇所を変数化し、コントラスト不合格をゼロにするまで",
"slug": "figma-design-token-consolidation",
@@ -1909,7 +1909,7 @@
},
{
"day": "LOG333",
-"series": "ロボティクスデザイン",
+"series": "ひとり研究所の裏側",
"published": "2026-10-12T07:58:00.000Z",
"title": "解説動画の1本目を公開まで通す ― 声・BGM・間・直し・サムネイルを部品ごとに決め、23手順にまとめた",
"slug": "first-video-end-to-end",
@@ -2134,7 +2134,7 @@
},
{
"day": "LOG330",
-"series": "ロボティクスデザイン",
+"series": "ひとり研究所の裏側",
"published": "2026-10-11T13:46:46.000Z",
"title": "解説動画をAIと作る ― 型の分析から敵対的検証まで、試作1本で決めたこと",
"slug": "making-explainer-video-with-ai",
@@ -2683,7 +2683,7 @@
},
{
"day": "LOG236",
-"series": "ロボティクスデザイン",
+"series": "画像認識・物体検出",
"published": "2026-06-14T02:26:07.000Z",
"title": "ロボットの目は、鏡像も「ある」と見る ― SAM 3で器具を切り出して気づいたこと。",
"slug": "sam3-mirror-reflection-grasp-problem",
@@ -2746,7 +2746,7 @@
},
{
"day": "LOG328",
-"series": "ロボティクスデザイン",
+"series": "ひとり研究所の裏側",
"published": "2026-10-11T07:39:41.000Z",
"title": "ロボティクスを声に出して覚え直す ― 開発ログから用語164語と判断の型104枚の練習ページを作った",
"slug": "speak-to-relearn-robotics",
@@ -2890,7 +2890,7 @@
},
{
"day": "LOG193",
-"series": "ロボティクスデザイン",
+"series": "ひとり研究所の裏側",
"published": "2026-05-12T07:43:59.000Z",
"title": "記事をビジュアルウィジェットで作り直す日 | STUDIOとGitHub Pagesの二層公開システムを建てた",
"slug": "visual-article-builder",
@@ -3061,7 +3061,7 @@
},
{
"day": "LOG332",
-"series": "ロボティクスデザイン",
+"series": "ひとり研究所の裏側",
"published": "2026-10-12T00:28:45.000Z",
"title": "YouTubeチャンネルの顔を作る ― チャンネル名・アイコン・バナーをハコブくんにそろえるまで",
"slug": "youtube-channel-identity",
diff --git a/src/data/records/build-in-public-double-structure.json b/src/data/records/build-in-public-double-structure.json
index 12c5b0b..fb5b4c6 100644
--- a/src/data/records/build-in-public-double-structure.json
+++ b/src/data/records/build-in-public-double-structure.json
@@ -1 +1 @@
-{"day": "LOG233", "series": "ロボティクスデザイン", "published": "2026-06-13T02:55:39.000Z", "title": "フィジカルAIの社会実装を止めるのは、デモではなく構造的な課題だ ― 36課題を地図にした", "slug": "build-in-public-double-structure", "cover": "/media/s-1920x1080_06b4447a-bb84-49b3-92b7-1a60b299a1f1.webp", "thumb": null, "lang": "JP", "description": "ロボットが物を掴むデモが増えても、社会実装を止めるのは別の課題だ。介護定着・実機データ不足・共存安全など9カテゴリ36課題を地図に整理した。", "updated": "2026-06-13T06:17:44.000Z", "body": "<!--TOC--><p></p><p>ロボットが物を掴む動画は毎週のように流れてくる。新しいヒューマノイドの発表も、器用な手先のデモも増え続けている。だが、それらが社会に実装されて当たり前に使われる未来は、デモの精度が上がれば自動的に来るものではない。掴めることと、現場で使われ続けることの間には、技術以外の課題が幾重にも横たわっている。</p><p>その課題を一枚に並べた地図をつくって公開した。「フィジカルAIが社会実装される前に解くべき課題」。9カテゴリ・36課題を、技術・制度・体験の3つの視点を横断して整理している。</p><p>公開URL: <a target=\"_blank\" href=\"https://pixelstomotors.com/issues\" data-has-link=\"true\" rel=\"\">https://pixelstomotors.com/issues</a></p><div class=\"embed\"><iframe src=\"/embeds/build-in-public-double-structure-onpYMlUI.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p>下敷きにしたのは fladdict 氏の「日本のTODOリスト」だ。個別のニュースに反応するのではなく、放置すれば分野そのものが頓挫しうる構造的な課題を、俯瞰して並べる。その形式をフィジカルAIに持ち込んだ。</p><p></p><h2 id=\"index_9JS_gEIM\">なぜ「地図」なのか</h2><p>フィジカルAIをめぐる情報は、ほとんどが点で流れてくる。あの会社が資金調達した、この研究が新しい記録を出した、というニュースの粒。点を追っているだけでは、分野の全体像も、何が本当のボトルネックなのかも見えてこない。</p><p>社会実装を止めるのは、たいてい派手な技術課題ではない。介護現場にロボットを入れても定着しない。学習させたいのに実機データが桁違いに足りない。安いアームが中小に届かない。事故が起きたとき誰の責任か決まっていない。こうした課題は、一つひとつは地味で、ニュースにもなりにくい。だが、これらが解けないかぎり、どんなに掴むのが上手いロボットも社会には入っていけない。</p><p>地図にする意味は、この「地味だが致命的な課題」を、点ではなく面として一望できるようにすることにある。どのカテゴリにいくつ課題があり、何が最も緊急で深刻なのか。課題どうしがどう絡み合っているのか。それを見える形にした。</p><p></p><h2 id=\"index_vqHbKPo3\">9つのカテゴリ</h2><div class=\"embed\"><iframe src=\"/embeds/build-in-public-double-structure-p87fRgQY.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p>36課題は9つのカテゴリに分かれている。各カテゴリ4課題ずつだ。</p><p><strong>労働力・現場・人材</strong> — 人手不足、現場への定着、ロボットを扱う人材、現場知の継承。介護現場へのロボット導入の壁、ロボットを扱える人材の不在、テレオペレーターという新職種、現場知のデータ化。</p><p><strong>学習・データ・モデル</strong> — 実機データ、Sim2Real、模倣学習、基盤モデル。実機データの圧倒的不足、Sim2Realギャップ、模倣学習のデータ品質、基盤モデルの寡占。</p><p><strong>コスト・経済性・調達</strong> — 導入コスト、保守、価格構造、導入モデル。導入コストとROIの不透明さ、保守・修理のエコシステム不在、中小企業に届かない価格構造、リース・サブスク等の導入モデル不在。</p><p><strong>環境・空間・インフラ</strong> — 建築、通信、充電、公道。ロボット前提でない建築・都市、通信インフラの遅延・帯域、充電・保管・動線の標準不在、公道・公共空間の走行ルール。</p><p><strong>産業構造・SIer・競争力</strong> — 動作のポータビリティ、SIer工数、国内ハード、タスク定義。動作のポータビリティ不在、SIer工数のボトルネック、国内ハード産業の空洞化、タスク定義能力の未成熟。</p><p><strong>安全・信頼性・事故</strong> — 共存安全、責任分界、停止設計、経年劣化。人とロボットの共存安全基準、事故時の責任分界、暴走・誤動作の検知と停止設計、経年劣化・メンテ不足による事故。</p><p><strong>自律性・判断・ガバナンス</strong> — 説明可能性、自律レベル、プライバシー、セキュリティ。VLM判断の説明可能性、自律レベルの定義不在、遠隔操作とプライバシー、物理を持つAIのセキュリティ。</p><p><strong>人間との関係・UX・受容</strong> — 不気味の谷、意図の可読性、受容性、信頼。動きの不気味さ(Motion Uncanny Valley)、意図の読み取りやすさ、高齢者・非エンジニアの受容性、過信と不信のバランス。</p><p><strong>規格・標準・国際競争</strong> — エコシステム主導権、規格関与、デュアルユース、データ越境。エコシステムの主導権、安全規格への日本の関与、デュアルユース規制、動作データの越境移転。</p><p>技術(学習・データ・自律)、制度(安全・規格・コスト)、体験(UX・受容・人材)が、9カテゴリの中に混ざって並んでいる。社会実装はこの3層のどれが欠けても進まない。技術だけ磨いても、制度が追いつかなければ現場には出られないし、出られても人が受け入れなければ使われない。</p><p></p><h2 id=\"index_qC2y0kG5\">いちばん重い5つ</h2><p>各課題には緊急度と深刻度を5段階でつけた。両方が5の、最も重い課題が5つある。</p><p><strong>介護現場へのロボット導入の壁</strong>(緊5・深5)。人手不足が最も深刻な領域なのに、現場の動線・業務・心理に合うロボットが少なく、導入しても定着しない。補助金で入れて終わり、が繰り返されている。技術ではなく「定着の設計」が欠けている。</p><p><strong>実機データの圧倒的不足</strong>(緊5・深5)。言語や画像はインターネット上に大量にあるが、ロボットの動作データは現実の試行でしか得られず、量が桁違いに少ない。収集コストの高さがそのまま、誰がモデルを作れるかを決めてしまう。</p><p><strong>人とロボットの共存安全基準</strong>(緊5・深5)。産業用ロボットの安全は長く「柵で人と隔離する」前提だった。協働・サービスロボットが人と同じ空間で動く時代に、隔離前提の規格は合わない。世代交代が途上にある。</p><p><strong>動作のポータビリティ不在</strong>(緊5・深5)。あるメーカーで作った動作が、別メーカーでは動かない。機種ごとにゼロから作り直す。これがSIerの工数が高止まりする正体で、導入の総量を構造的に縛っている。</p><p><strong>物理を持つAIのセキュリティ</strong>(緊5・深5)。一般的なシステムへの攻撃は情報の漏洩で済む。だがロボットが乗っ取られれば、物理的に人を傷つけうる。サイバーセキュリティと機能安全が別々に設計されたまま、統合されていない。</p><p>この5つに共通するのは、どれも「ロボットがうまく動くか」とは別の次元にあることだ。動作の精度をいくら上げても、ここは解けない。社会実装の本当のボトルネックが、技術の華やかな部分の外側にあることを、この5つは示している。</p><p></p><h2 id=\"index_Y7wOA3d5\">各課題ページの中身</h2><p>地図はカテゴリと課題名を並べただけではない。36課題それぞれに、深く掘り下げたページを用意した。構造は全課題で統一している。</p><p>一文での定義から始まり、30秒で読める要約、実装判断のサマリー表、扱う範囲と扱わない範囲の切り分け、何が起きているか、なぜ先送りされてきたか、原因の構造、誰がどう困るか、放置するとどうなるかの時間軸、解決の方向性、選択肢の比較表、主体別のアクション、争点、そして反対論。</p><p>反対論の節は特に意識した。それぞれの課題について「最も強い反対意見」を立て、それに対する応答を必ずペアで書いている。たとえば「介護は人の手によるケアが本質で、機器化は質を下げる」という反論に、「目的は人の手を奪うことではなく、負担の大きい間接業務を機器に移し、人が対人ケアに集中できるようにすることだ」と返す。一方的に「これは課題だ」と並べるのではなく、反対の立場から見たときに何が言えるかまで書く。そうしないと、課題地図はただの危機の羅列になってしまう。</p><p>統計値については、確認が取れていないものに「※要出典確認」を明示的に付けた。プロトタイプ版として、数値の正確性より構造の網羅性を先に出す判断をしている。SNS等での指摘を受けて、出典を確定しながら更新していく前提だ。</p><p></p><h2 id=\"index_18OUIbn9\">なぜこれをつくったのか</h2><p>Pixels to Motors は、iPhoneとUnityだけで個人がロボットの動作を作れることを証明しようとしているプロジェクトだ。学習済みモデルをもらって、動きの品質と体験を設計する——出力側で勝負するのが立ち位置になる。</p><p>出力側に立つからこそ、社会実装の全体像を押さえておく必要がある。動きの品質を設計するという仕事は、9カテゴリのうち「人間との関係・UX・受容」に直接効く。動きの不気味さ、意図の読み取りやすさ、高齢者・非エンジニアの受容性、過信と不信のバランス。これらはどれも、ロボットが正確に動くかではなく、人がそれをどう受け取るかの問題で、動作の精度とは別の設計論を要する。Motion UX と呼んできた領域そのものだ。</p><p>だが、その領域だけを見ていても足りない。受容性の課題は、安全基準が整わなければ意味をなさないし、安いアームが現場に届かなければ体験を設計する対象すら存在しない。課題は絡み合っている。だから地図がいる。自分が立つ場所を、分野全体の中に位置づけて確かめるための地図だ。</p><p>map.html では、36課題を36のノードとして配置し、関連する課題どうしを111本の線で結んだ。たとえば「実機データの圧倒的不足」は「Sim2Realギャップ」「模倣学習のデータ品質」「現場知のデータ化」「テレオペレーターという新職種」とつながっている。データという一点をめぐって、技術・労働・職種の課題が連なる。この絡まりこそが、社会実装の難しさの正体だ。</p><p></p><h2 id=\"index_FetiGvtJ\">build in public の記録として</h2><p>最後に、制作の裏側を一つだけ残しておく。このサイトをつくる過程で、36本の本文を書き終えた直後、生成スクリプトの不備で35本の本文を一度全消ししてしまった。骨格を一括生成するコードが、再実行のたびに手書き本文を上書きする設計になっていた。復旧より先に再発を止める安全装置を入れ、履歴から書き戻し、外部レビュー(codex)で表記の不統一や表のモバイル崩れを拾ってもらって、公開にこぎつけた。</p><p>失敗を隠さず記録に残すのは、このプロジェクト全体の方針だ。アームが失敗から学ぶ設計をしながら、つくる側も新しい題材で失敗しながら学ぶ。事故は主題ではないが、地図をつくる過程もまた、building in public の一部として残しておく。</p><p>サイトはプロトタイプ版だ。これから出典を確定し、指摘を受けて更新していく。フィジカルAIの社会実装を本当に前に進めるために、何を解かなければならないのか。その問いの一覧を、まず形にした。</p>", "toc": [{"id": "index_9JS_gEIM", "text": "なぜ「地図」なのか"}, {"id": "index_vqHbKPo3", "text": "9つのカテゴリ"}, {"id": "index_qC2y0kG5", "text": "いちばん重い5つ"}, {"id": "index_Y7wOA3d5", "text": "各課題ページの中身"}, {"id": "index_18OUIbn9", "text": "なぜこれをつくったのか"}, {"id": "index_FetiGvtJ", "text": "build in public の記録として"}]}
\ No newline at end of file
+{"day": "LOG233", "series": "考察記事", "published": "2026-06-13T02:55:39.000Z", "title": "フィジカルAIの社会実装を止めるのは、デモではなく構造的な課題だ ― 36課題を地図にした", "slug": "build-in-public-double-structure", "cover": "/media/s-1920x1080_06b4447a-bb84-49b3-92b7-1a60b299a1f1.webp", "thumb": null, "lang": "JP", "description": "ロボットが物を掴むデモが増えても、社会実装を止めるのは別の課題だ。介護定着・実機データ不足・共存安全など9カテゴリ36課題を地図に整理した。", "updated": "2026-06-13T06:17:44.000Z", "body": "<!--TOC--><p></p><p>ロボットが物を掴む動画は毎週のように流れてくる。新しいヒューマノイドの発表も、器用な手先のデモも増え続けている。だが、それらが社会に実装されて当たり前に使われる未来は、デモの精度が上がれば自動的に来るものではない。掴めることと、現場で使われ続けることの間には、技術以外の課題が幾重にも横たわっている。</p><p>その課題を一枚に並べた地図をつくって公開した。「フィジカルAIが社会実装される前に解くべき課題」。9カテゴリ・36課題を、技術・制度・体験の3つの視点を横断して整理している。</p><p>公開URL: <a target=\"_blank\" href=\"https://pixelstomotors.com/issues\" data-has-link=\"true\" rel=\"\">https://pixelstomotors.com/issues</a></p><div class=\"embed\"><iframe src=\"/embeds/build-in-public-double-structure-onpYMlUI.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p>下敷きにしたのは fladdict 氏の「日本のTODOリスト」だ。個別のニュースに反応するのではなく、放置すれば分野そのものが頓挫しうる構造的な課題を、俯瞰して並べる。その形式をフィジカルAIに持ち込んだ。</p><p></p><h2 id=\"index_9JS_gEIM\">なぜ「地図」なのか</h2><p>フィジカルAIをめぐる情報は、ほとんどが点で流れてくる。あの会社が資金調達した、この研究が新しい記録を出した、というニュースの粒。点を追っているだけでは、分野の全体像も、何が本当のボトルネックなのかも見えてこない。</p><p>社会実装を止めるのは、たいてい派手な技術課題ではない。介護現場にロボットを入れても定着しない。学習させたいのに実機データが桁違いに足りない。安いアームが中小に届かない。事故が起きたとき誰の責任か決まっていない。こうした課題は、一つひとつは地味で、ニュースにもなりにくい。だが、これらが解けないかぎり、どんなに掴むのが上手いロボットも社会には入っていけない。</p><p>地図にする意味は、この「地味だが致命的な課題」を、点ではなく面として一望できるようにすることにある。どのカテゴリにいくつ課題があり、何が最も緊急で深刻なのか。課題どうしがどう絡み合っているのか。それを見える形にした。</p><p></p><h2 id=\"index_vqHbKPo3\">9つのカテゴリ</h2><div class=\"embed\"><iframe src=\"/embeds/build-in-public-double-structure-p87fRgQY.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p>36課題は9つのカテゴリに分かれている。各カテゴリ4課題ずつだ。</p><p><strong>労働力・現場・人材</strong> — 人手不足、現場への定着、ロボットを扱う人材、現場知の継承。介護現場へのロボット導入の壁、ロボットを扱える人材の不在、テレオペレーターという新職種、現場知のデータ化。</p><p><strong>学習・データ・モデル</strong> — 実機データ、Sim2Real、模倣学習、基盤モデル。実機データの圧倒的不足、Sim2Realギャップ、模倣学習のデータ品質、基盤モデルの寡占。</p><p><strong>コスト・経済性・調達</strong> — 導入コスト、保守、価格構造、導入モデル。導入コストとROIの不透明さ、保守・修理のエコシステム不在、中小企業に届かない価格構造、リース・サブスク等の導入モデル不在。</p><p><strong>環境・空間・インフラ</strong> — 建築、通信、充電、公道。ロボット前提でない建築・都市、通信インフラの遅延・帯域、充電・保管・動線の標準不在、公道・公共空間の走行ルール。</p><p><strong>産業構造・SIer・競争力</strong> — 動作のポータビリティ、SIer工数、国内ハード、タスク定義。動作のポータビリティ不在、SIer工数のボトルネック、国内ハード産業の空洞化、タスク定義能力の未成熟。</p><p><strong>安全・信頼性・事故</strong> — 共存安全、責任分界、停止設計、経年劣化。人とロボットの共存安全基準、事故時の責任分界、暴走・誤動作の検知と停止設計、経年劣化・メンテ不足による事故。</p><p><strong>自律性・判断・ガバナンス</strong> — 説明可能性、自律レベル、プライバシー、セキュリティ。VLM判断の説明可能性、自律レベルの定義不在、遠隔操作とプライバシー、物理を持つAIのセキュリティ。</p><p><strong>人間との関係・UX・受容</strong> — 不気味の谷、意図の可読性、受容性、信頼。動きの不気味さ(Motion Uncanny Valley)、意図の読み取りやすさ、高齢者・非エンジニアの受容性、過信と不信のバランス。</p><p><strong>規格・標準・国際競争</strong> — エコシステム主導権、規格関与、デュアルユース、データ越境。エコシステムの主導権、安全規格への日本の関与、デュアルユース規制、動作データの越境移転。</p><p>技術(学習・データ・自律)、制度(安全・規格・コスト)、体験(UX・受容・人材)が、9カテゴリの中に混ざって並んでいる。社会実装はこの3層のどれが欠けても進まない。技術だけ磨いても、制度が追いつかなければ現場には出られないし、出られても人が受け入れなければ使われない。</p><p></p><h2 id=\"index_qC2y0kG5\">いちばん重い5つ</h2><p>各課題には緊急度と深刻度を5段階でつけた。両方が5の、最も重い課題が5つある。</p><p><strong>介護現場へのロボット導入の壁</strong>(緊5・深5)。人手不足が最も深刻な領域なのに、現場の動線・業務・心理に合うロボットが少なく、導入しても定着しない。補助金で入れて終わり、が繰り返されている。技術ではなく「定着の設計」が欠けている。</p><p><strong>実機データの圧倒的不足</strong>(緊5・深5)。言語や画像はインターネット上に大量にあるが、ロボットの動作データは現実の試行でしか得られず、量が桁違いに少ない。収集コストの高さがそのまま、誰がモデルを作れるかを決めてしまう。</p><p><strong>人とロボットの共存安全基準</strong>(緊5・深5)。産業用ロボットの安全は長く「柵で人と隔離する」前提だった。協働・サービスロボットが人と同じ空間で動く時代に、隔離前提の規格は合わない。世代交代が途上にある。</p><p><strong>動作のポータビリティ不在</strong>(緊5・深5)。あるメーカーで作った動作が、別メーカーでは動かない。機種ごとにゼロから作り直す。これがSIerの工数が高止まりする正体で、導入の総量を構造的に縛っている。</p><p><strong>物理を持つAIのセキュリティ</strong>(緊5・深5)。一般的なシステムへの攻撃は情報の漏洩で済む。だがロボットが乗っ取られれば、物理的に人を傷つけうる。サイバーセキュリティと機能安全が別々に設計されたまま、統合されていない。</p><p>この5つに共通するのは、どれも「ロボットがうまく動くか」とは別の次元にあることだ。動作の精度をいくら上げても、ここは解けない。社会実装の本当のボトルネックが、技術の華やかな部分の外側にあることを、この5つは示している。</p><p></p><h2 id=\"index_Y7wOA3d5\">各課題ページの中身</h2><p>地図はカテゴリと課題名を並べただけではない。36課題それぞれに、深く掘り下げたページを用意した。構造は全課題で統一している。</p><p>一文での定義から始まり、30秒で読める要約、実装判断のサマリー表、扱う範囲と扱わない範囲の切り分け、何が起きているか、なぜ先送りされてきたか、原因の構造、誰がどう困るか、放置するとどうなるかの時間軸、解決の方向性、選択肢の比較表、主体別のアクション、争点、そして反対論。</p><p>反対論の節は特に意識した。それぞれの課題について「最も強い反対意見」を立て、それに対する応答を必ずペアで書いている。たとえば「介護は人の手によるケアが本質で、機器化は質を下げる」という反論に、「目的は人の手を奪うことではなく、負担の大きい間接業務を機器に移し、人が対人ケアに集中できるようにすることだ」と返す。一方的に「これは課題だ」と並べるのではなく、反対の立場から見たときに何が言えるかまで書く。そうしないと、課題地図はただの危機の羅列になってしまう。</p><p>統計値については、確認が取れていないものに「※要出典確認」を明示的に付けた。プロトタイプ版として、数値の正確性より構造の網羅性を先に出す判断をしている。SNS等での指摘を受けて、出典を確定しながら更新していく前提だ。</p><p></p><h2 id=\"index_18OUIbn9\">なぜこれをつくったのか</h2><p>Pixels to Motors は、iPhoneとUnityだけで個人がロボットの動作を作れることを証明しようとしているプロジェクトだ。学習済みモデルをもらって、動きの品質と体験を設計する——出力側で勝負するのが立ち位置になる。</p><p>出力側に立つからこそ、社会実装の全体像を押さえておく必要がある。動きの品質を設計するという仕事は、9カテゴリのうち「人間との関係・UX・受容」に直接効く。動きの不気味さ、意図の読み取りやすさ、高齢者・非エンジニアの受容性、過信と不信のバランス。これらはどれも、ロボットが正確に動くかではなく、人がそれをどう受け取るかの問題で、動作の精度とは別の設計論を要する。Motion UX と呼んできた領域そのものだ。</p><p>だが、その領域だけを見ていても足りない。受容性の課題は、安全基準が整わなければ意味をなさないし、安いアームが現場に届かなければ体験を設計する対象すら存在しない。課題は絡み合っている。だから地図がいる。自分が立つ場所を、分野全体の中に位置づけて確かめるための地図だ。</p><p>map.html では、36課題を36のノードとして配置し、関連する課題どうしを111本の線で結んだ。たとえば「実機データの圧倒的不足」は「Sim2Realギャップ」「模倣学習のデータ品質」「現場知のデータ化」「テレオペレーターという新職種」とつながっている。データという一点をめぐって、技術・労働・職種の課題が連なる。この絡まりこそが、社会実装の難しさの正体だ。</p><p></p><h2 id=\"index_FetiGvtJ\">build in public の記録として</h2><p>最後に、制作の裏側を一つだけ残しておく。このサイトをつくる過程で、36本の本文を書き終えた直後、生成スクリプトの不備で35本の本文を一度全消ししてしまった。骨格を一括生成するコードが、再実行のたびに手書き本文を上書きする設計になっていた。復旧より先に再発を止める安全装置を入れ、履歴から書き戻し、外部レビュー(codex)で表記の不統一や表のモバイル崩れを拾ってもらって、公開にこぎつけた。</p><p>失敗を隠さず記録に残すのは、このプロジェクト全体の方針だ。アームが失敗から学ぶ設計をしながら、つくる側も新しい題材で失敗しながら学ぶ。事故は主題ではないが、地図をつくる過程もまた、building in public の一部として残しておく。</p><p>サイトはプロトタイプ版だ。これから出典を確定し、指摘を受けて更新していく。フィジカルAIの社会実装を本当に前に進めるために、何を解かなければならないのか。その問いの一覧を、まず形にした。</p>", "toc": [{"id": "index_9JS_gEIM", "text": "なぜ「地図」なのか"}, {"id": "index_vqHbKPo3", "text": "9つのカテゴリ"}, {"id": "index_qC2y0kG5", "text": "いちばん重い5つ"}, {"id": "index_Y7wOA3d5", "text": "各課題ページの中身"}, {"id": "index_18OUIbn9", "text": "なぜこれをつくったのか"}, {"id": "index_FetiGvtJ", "text": "build in public の記録として"}]}
\ No newline at end of file
diff --git a/src/data/records/day50.json b/src/data/records/day50.json
index 634aa16..2427121 100644
--- a/src/data/records/day50.json
+++ b/src/data/records/day50.json
@@ -1 +1 @@
-{"day": "LOG50", "series": "ロボティクスデザイン", "published": "2026-01-28T22:10:00.000Z", "title": "FigJam × ハコブくん設計。なんとなくのデザインではなく仕様としてのフロー図作成", "slug": "day50", "cover": "/media/s-1920x1080_a2cff5e6-ba93-43ef-9870-7aec075cf96d.webp", "thumb": null, "lang": "JP", "description": "UIデザイナーの役割は、単に画面を作ることではなく、プロダクトの意図を実装へ繋ぐ「翻訳者」であること。実務50日目の節目に、チームの共通言語となる「ユーザー起点の基本動作フロー図」の作成プロセスを公開します。正常系・異常系のロジック構築から、意思決定を加速させる視覚化の意義まで詳しく解説します。", "updated": "2026-04-18T12:35:11.000Z", "body": "<!--TOC--><p>UIデザイナーとして日々の業務を通じて実感しているのは、デザイナーの役割が単に綺麗な画面を作ることではなく、プロダクトの意図を汲み取り、実装へと繋ぐ翻訳者であるということです。</p><p>今回は、その一環として取り組んだ「ユーザー起点の基本動作のフロー図」の作成プロセスについてまとめます。</p><h5>※本記事は、ロボティクスデザイナーが実務における仮想のフロー図作成のプロセスを振り返り、言語化した思考の記録です。</h5><p></p><h2 id=\"index_vQ7ZTzi7\">フロー図作成のプロセス</h2><figure><img src=\"/media/s-4480x3680_a31ac638-2304-4c34-8b03-f0326c3a2777.webp\" alt=\"\"><figcaption></figcaption></figure><h3 id=\"index_PPHaOw4I\">本質を捉える</h3><p>まず着手したのは、自身の役割の再定義でした。デザイナーには、プロダクトマネージャー(PdM)やエンジニアの意図を理解し、コミュニケーションを円滑にするための成果物に「翻訳」することが求められています。</p><p>具体的な画面制作に入る前に「ユーザー起点の基本動作」をフロー図として整理することが、チーム全体の曖昧さを減らす最短ルートだと考えました。</p><h3 id=\"index_5QrJe0bV\">ロジックの構築と試行錯誤</h3><figure><img src=\"/media/s-760x2072_25e1dbfb-abe6-42aa-9ce0-ac96953efa1f.webp\" alt=\"フロー図-直線的なフロー図\"><figcaption></figcaption></figure><p>最初は、ユーザーが状態を確認して操作し、フィードバックを得るという直線的なフローから描き始めました。しかし、実際のシステム運用を考えると、正常系だけでは不十分です。</p><p>そこで、以下の要素を段階的に組み込み、ロジックを磨き上げました。</p><ul><li><p><strong>安全性の担保</strong>: 異常がある場合の「警告UI」を配置し、ユーザーが「承認/了解」した上で操作に進むステップを追加しました。</p></li><li><p><strong>リカバリーフローの定義</strong>: エラーが発生した際、どのように操作を停止し、復旧操作を経て元の状態に戻るかというルートを明確にしました。</p></li><li><p><strong>循環するステート設計</strong>:<strong> </strong>操作完了後にフローを終えるのではなく、再び最初の「状態確認」へと戻るループを作ることで、システムの継続的な動作を可視化しました。</p></li></ul><h3 id=\"index_f3xndyl5\">チームの共通言語としての活用</h3><p>作成したフロー図は、自分自身の整理のためだけではありません。これは、ステークホルダーとの合意形成における重要な「共通言語」となります。</p><ul><li><p><strong>PdMに対して</strong>: 要件定義に基づき、ユーザー体験がどのように構成されているかを視覚的に示し、方向性の不一致を防ぎます。</p></li><li><p><strong>エンジニアに対して</strong>:<strong> </strong>画面遷移やインタラクションの仕様を明確に伝えることで、実装時の迷いや手戻りを最小限に抑えます。</p></li></ul><h3 id=\"index_hH5pzjfD\">フローの視覚化の意義</h3><p>フローを視覚化し、関係者の意見を一致させるというミッションを果たすための一歩として、フロー図面作成は非常に意義のあるプロセスでした。</p><p>見た目の美しさと同じくらい、その裏側にある論理構造を重んじる。そんなデザイナーとしての在り方を大切にしながら、これからもチームの意思決定を加速させていけるよう、実務に取り組んでいきたいと思います。</p><p></p><h2 id=\"index_ExEa8tkZ\">次なるステップ:デジタルからロボティクスへの拡張</h2><p>今回整理したフロー図は、あくまでユーザーとUIのやり取りを中心とした基本動作の設計です。しかし、私が今向き合っているのは画面の中だけで完結するサービスではなく、実機としてのロボットが物理空間で動くプロダクトです。</p><p>この基本フローをベースに、ロボティクス特有の変数をどう組み込んでいくべきか、その具体的なアップデートについて掘り下げていきたいと思います。</p><p>具体的には、以下のような「物理現象とシステム制約」に起因する要素を付け加えていく予定です。</p><ul><li><p><strong>システム(機体)起点の割り込みフロー</strong>: ユーザーの操作中であっても、ロボットが自律的に障害物を検知したり、センサー異常を感知したりした際の「強制停止」や「自動回避」の挙動。</p></li><li><p><strong>ハードウェア状態の可視化</strong>: <strong> </strong>通信のレイテンシ(遅延)やバッテリー残量、ハードウェアの健康状態といった「目に見えない制約」をどうUIの状態遷移(ステート)に反映させるか。</p></li><li><p><strong>環境要因による例外系</strong>: <strong> </strong>段差や照明環境、周囲の動体など、物理環境の変化によって操作が継続できなくなった際のリカバリー設計。</p></li><li><p><strong>物理的な安全性(Safety)の階層</strong>: <strong> </strong>ソフトウェア上のエラーと、物理的な緊急停止(E-Stop)が連動する際の、UIにおける情報の優先順位とインタラクション仕様。</p></li></ul><p>見た目の美しさを超え、開発エンジニアが実装において一切の迷いを持たないレベルまで仕様を研ぎ澄ますために。デジタルと物理の境界線をデザインの力でどう繋ぐのか、その挑戦を続けていきます。</p>", "toc": [{"id": "index_vQ7ZTzi7", "text": "フロー図作成のプロセス"}, {"id": "index_ExEa8tkZ", "text": "次なるステップ:デジタルからロボティクスへの拡張"}]}
\ No newline at end of file
+{"day": "LOG50", "series": "HRI", "published": "2026-01-28T22:10:00.000Z", "title": "FigJam × ハコブくん設計。なんとなくのデザインではなく仕様としてのフロー図作成", "slug": "day50", "cover": "/media/s-1920x1080_a2cff5e6-ba93-43ef-9870-7aec075cf96d.webp", "thumb": null, "lang": "JP", "description": "UIデザイナーの役割は、単に画面を作ることではなく、プロダクトの意図を実装へ繋ぐ「翻訳者」であること。実務50日目の節目に、チームの共通言語となる「ユーザー起点の基本動作フロー図」の作成プロセスを公開します。正常系・異常系のロジック構築から、意思決定を加速させる視覚化の意義まで詳しく解説します。", "updated": "2026-04-18T12:35:11.000Z", "body": "<!--TOC--><p>UIデザイナーとして日々の業務を通じて実感しているのは、デザイナーの役割が単に綺麗な画面を作ることではなく、プロダクトの意図を汲み取り、実装へと繋ぐ翻訳者であるということです。</p><p>今回は、その一環として取り組んだ「ユーザー起点の基本動作のフロー図」の作成プロセスについてまとめます。</p><h5>※本記事は、ロボティクスデザイナーが実務における仮想のフロー図作成のプロセスを振り返り、言語化した思考の記録です。</h5><p></p><h2 id=\"index_vQ7ZTzi7\">フロー図作成のプロセス</h2><figure><img src=\"/media/s-4480x3680_a31ac638-2304-4c34-8b03-f0326c3a2777.webp\" alt=\"\"><figcaption></figcaption></figure><h3 id=\"index_PPHaOw4I\">本質を捉える</h3><p>まず着手したのは、自身の役割の再定義でした。デザイナーには、プロダクトマネージャー(PdM)やエンジニアの意図を理解し、コミュニケーションを円滑にするための成果物に「翻訳」することが求められています。</p><p>具体的な画面制作に入る前に「ユーザー起点の基本動作」をフロー図として整理することが、チーム全体の曖昧さを減らす最短ルートだと考えました。</p><h3 id=\"index_5QrJe0bV\">ロジックの構築と試行錯誤</h3><figure><img src=\"/media/s-760x2072_25e1dbfb-abe6-42aa-9ce0-ac96953efa1f.webp\" alt=\"フロー図-直線的なフロー図\"><figcaption></figcaption></figure><p>最初は、ユーザーが状態を確認して操作し、フィードバックを得るという直線的なフローから描き始めました。しかし、実際のシステム運用を考えると、正常系だけでは不十分です。</p><p>そこで、以下の要素を段階的に組み込み、ロジックを磨き上げました。</p><ul><li><p><strong>安全性の担保</strong>: 異常がある場合の「警告UI」を配置し、ユーザーが「承認/了解」した上で操作に進むステップを追加しました。</p></li><li><p><strong>リカバリーフローの定義</strong>: エラーが発生した際、どのように操作を停止し、復旧操作を経て元の状態に戻るかというルートを明確にしました。</p></li><li><p><strong>循環するステート設計</strong>:<strong> </strong>操作完了後にフローを終えるのではなく、再び最初の「状態確認」へと戻るループを作ることで、システムの継続的な動作を可視化しました。</p></li></ul><h3 id=\"index_f3xndyl5\">チームの共通言語としての活用</h3><p>作成したフロー図は、自分自身の整理のためだけではありません。これは、ステークホルダーとの合意形成における重要な「共通言語」となります。</p><ul><li><p><strong>PdMに対して</strong>: 要件定義に基づき、ユーザー体験がどのように構成されているかを視覚的に示し、方向性の不一致を防ぎます。</p></li><li><p><strong>エンジニアに対して</strong>:<strong> </strong>画面遷移やインタラクションの仕様を明確に伝えることで、実装時の迷いや手戻りを最小限に抑えます。</p></li></ul><h3 id=\"index_hH5pzjfD\">フローの視覚化の意義</h3><p>フローを視覚化し、関係者の意見を一致させるというミッションを果たすための一歩として、フロー図面作成は非常に意義のあるプロセスでした。</p><p>見た目の美しさと同じくらい、その裏側にある論理構造を重んじる。そんなデザイナーとしての在り方を大切にしながら、これからもチームの意思決定を加速させていけるよう、実務に取り組んでいきたいと思います。</p><p></p><h2 id=\"index_ExEa8tkZ\">次なるステップ:デジタルからロボティクスへの拡張</h2><p>今回整理したフロー図は、あくまでユーザーとUIのやり取りを中心とした基本動作の設計です。しかし、私が今向き合っているのは画面の中だけで完結するサービスではなく、実機としてのロボットが物理空間で動くプロダクトです。</p><p>この基本フローをベースに、ロボティクス特有の変数をどう組み込んでいくべきか、その具体的なアップデートについて掘り下げていきたいと思います。</p><p>具体的には、以下のような「物理現象とシステム制約」に起因する要素を付け加えていく予定です。</p><ul><li><p><strong>システム(機体)起点の割り込みフロー</strong>: ユーザーの操作中であっても、ロボットが自律的に障害物を検知したり、センサー異常を感知したりした際の「強制停止」や「自動回避」の挙動。</p></li><li><p><strong>ハードウェア状態の可視化</strong>: <strong> </strong>通信のレイテンシ(遅延)やバッテリー残量、ハードウェアの健康状態といった「目に見えない制約」をどうUIの状態遷移(ステート)に反映させるか。</p></li><li><p><strong>環境要因による例外系</strong>: <strong> </strong>段差や照明環境、周囲の動体など、物理環境の変化によって操作が継続できなくなった際のリカバリー設計。</p></li><li><p><strong>物理的な安全性(Safety)の階層</strong>: <strong> </strong>ソフトウェア上のエラーと、物理的な緊急停止(E-Stop)が連動する際の、UIにおける情報の優先順位とインタラクション仕様。</p></li></ul><p>見た目の美しさを超え、開発エンジニアが実装において一切の迷いを持たないレベルまで仕様を研ぎ澄ますために。デジタルと物理の境界線をデザインの力でどう繋ぐのか、その挑戦を続けていきます。</p>", "toc": [{"id": "index_vQ7ZTzi7", "text": "フロー図作成のプロセス"}, {"id": "index_ExEa8tkZ", "text": "次なるステップ:デジタルからロボティクスへの拡張"}]}
\ No newline at end of file
diff --git a/src/data/records/day51.json b/src/data/records/day51.json
index 2150d74..177dc96 100644
--- a/src/data/records/day51.json
+++ b/src/data/records/day51.json
@@ -1 +1 @@
-{"day": "LOG51", "series": "ロボティクスデザイン", "published": "2026-01-29T22:00:00.000Z", "title": "FigJamで描くロボットの意思|物理空間を制御するフロー図の構築", "slug": "day51", "cover": "/media/s-1920x1080_a1b9bfde-3664-4fcf-94a7-18c105dcd946.webp", "thumb": null, "lang": "JP", "description": "ロボティクスUIにおいて、デジタルな理想論は通用しません。物理現象やシステム制約をいかにフロー図へ落とし込み、PdMやエンジニアの迷いを無くすか。FigJamを用いた4つの変数(システム発火、ハード状態、環境要因、安全性)の可視化と、AIによるレビューを経た仕様のブラッシュアップ過程を記録します。", "updated": "2026-04-18T12:33:05.000Z", "body": "<!--TOC--><p></p><h2 id=\"index_cgzZR8hz\">デジタルからロボティクスへの拡張</h2><p>UIデザイナーがユーザー起点の基本動作を整理する重要性について触れました。本記事ではその基本フローをベースに、ロボティクス特有の変数である物理現象とシステム制約をいかにして仕様へと組み込んでいったのか、その具体的なプロセスを記録します。</p><p>私が向き合っているのは、画面の中だけで完結するサービスではありません。実機としてのロボットが物理空間で動くプロダクトだからこそ、デジタルな理想論を超えた泥臭いロジックの構築が不可欠となります。</p><h5>※本記事は、ロボティクスデザイナーが実務における仮想のフロー図作成のプロセスを振り返り、言語化した思考の記録です。</h5><p></p><h2 id=\"index_KjpV3LwO\">物理とデジタルを接続するフロー図の重み</h2><p>ロボティクスにおけるUIデザインは、画面内の遷移を定義するだけでは完結しません。ロボットという物理的な実体が伴う以上、そこには「通信の瞬断」「センサーの誤検知」「物理的なスタック」といった、デジタル完結のサービスでは想定し得ない不確実な状態が無数に存在します。</p><p>UIデザイナーがユーザー起点の基本動作を整理する重要性について触れました。基本フローをベースに、ロボティクス特有の変数である物理現象とシステム制約をいかにして仕様へと組み込んでいったのか、具体的なプロセスを記録します。</p><p>設計において最も時間を割くのは、これらの「状態(ステート)」を可視化するフロー図の作成です。このフロー図は、単なるユーザー体験の導線ではなく、システムが次に何をすべきかを判断するための仕様そのものであり、PdMやエンジニアとの共通言語となります。</p><p></p><h2 id=\"index_jb5Xk9Gd\">FigJamで機体の都合を可視化する</h2><figure><img src=\"/media/s-6464x6944_925aa0f3-86e5-4d1e-b490-0c117138b3c3.webp\" alt=\"FigJam-ロボティクスUIおよび状態遷移設計\"><figcaption></figcaption></figure><p>具体的なフロー図の構築にあたっては、まず以前に定義した「正常・警告・異常」の3軸構造を土台にしました。そこから物理レイヤーの事象をマッピングするために、以下の手順でFigJam上に要素を追加していきました。</p><ul><li><p><strong>ユーザー起点のフローに「システム発火」を並走させる</strong>: ユーザーの操作フローのすぐ隣に、機体側がセンサーやシステム状態に基づいて自律的に判断を下す「別系統のフロー」を配置しました。</p></li><li><p><strong>割り込みゲートの設置</strong>: ユーザーが「操作」を行う直前、あるいは操作中のどのタイミングで、機体側の「強制停止」や「自動回避」が入り込む余地があるのかを、判断分岐として視覚化しました。</p></li></ul><p></p><h2 id=\"index_2J9txHr7\">ロボティクス特有の4つの変数</h2><p>物理的な制約をロジックへ変換するために、以下の要素をフローの中に具体的に落とし込みました。</p><ul><li><p><strong>システム(機体)起点の割り込みフロー</strong>: ユーザーが操作中であっても、ロボットが自律的に障害物を検知した際の強制停止や自動回避の挙動。これを「ユーザーが止めた」のではなく「機体が止まった」という別個のステートとして定義しました。</p></li><li><p><strong>ハードウェア状態の可視化</strong>: 通信のレイテンシ(遅延)やバッテリー残量、ハードウェアの健康状態といった目に見えない制約をUIの状態遷移(ステート)に反映。例えば、遅延が発生している際に「ユーザーに今の操作が届いているか」をどう伝えるか、という仕様を固めました。</p></li><li><p><strong>環境要因による例外系</strong>: 段差、照明環境の変化、周囲の動体など、物理環境によって操作が継続不能になった際のリカバリー設計。環境をソフトウェア側でコントロールできないからこそ、UIによる適切な誘導が安全の鍵となります。</p></li><li><p><strong>物理的な安全性(Safety)の階層</strong>: ソフトウェア上のエラー表示よりも、物理的な緊急停止(E-Stop)が連動する際のインタラクションを最優先事項として定義。情報の優先順位を整理し、緊急時におけるUIの振る舞いを明確にしました。</p></li></ul><p></p><h2 id=\"index_gnOkRj0q\">Gemによる仕様レビューと改善のプロセス</h2><p>作成したフロー図に対し、AI(Gem)を作業パートナーとして活用し、仕様の網羅性と論理的整合性を検証しました。客観的な視点を入れることで、デザイナー特有の「正常系のバイアス」を排除するのが狙いです。</p><h3 id=\"index_SKlq_I7w\">ステート遷移の漏れに対する指摘</h3><p>Gemからは、特に「異常系から復帰する際の遷移」について具体的な指摘を受けました。</p><ul><li><p><strong>指摘内容</strong>:<strong> </strong>障害物検知による強制停止後、障害物が取り除かれた際の「待機状態」への戻り方が定義されていない。</p></li><li><p><strong>改善</strong>:<strong> </strong>「自動復帰」か「ユーザーによる再開操作」が必要かを明示し、安全性を担保するための再認証ステップをフローに追加しました。</p></li></ul><h3 id=\"index_vhnHwBml\">用語の定義と一貫性の確保</h3><p>エンジニアとのコミュニケーションロスを防ぐため、フロー図内で使用する用語の統一を行いました。</p><ul><li><p><strong>改善</strong>:<strong> </strong>「エラー」「アラート」「ワーニング」といった曖昧な表現を排し、機体状態に紐づくステート名(Idle, Moving, Blocked, Fault等)へと定義を更新。これにより、UI上の表示文言と内部ロジックの紐付けを強固にしました。</p></li></ul><h3 id=\"index_pUU9ojaH\">リアルタイム性の考慮</h3><p>通信環境が悪化した際の「タイムアウト」の定義についても再検討しました。</p><ul><li><p><strong>改善</strong>:<strong> </strong>何秒の遅延で「操作不能」とみなすのか、その閾値をフロー図内に注釈として追加。UIが応答待ちを表示すべきタイミングを厳密にルール化しました。</p></li></ul><p></p><h2 id=\"index_VsxrSOv8\">実装において一切の迷いを持たせない</h2><p>このようにフローを深掘りし、外部の視点でブラッシュアップする目的は、単に複雑な図を作ることではありません。それは、PdMやエンジニアが実装を進める上で、一切の迷いを持たないレベルまで仕様を研ぎ澄ますことにあります。</p><p>画面の外で何が起きているかをデザイナーが把握し、それをロジックとして定義しておくことで、実装時の手戻りは劇的に減ります。デザイナーが物理とデジタルの境界線を繋ぐことで、開発チーム全体が「安全で、予測可能なプロダクト」を自信を持って構築できるようになります。</p><p></p><h2 id=\"index_paHbPPA9\">デザインが物理を制御する</h2><p>整理したフローは、物理現象をUIがどう受け止めるか、という設計です。しかし、次なるステップは、UIを通じて物理的な挙動をより直感的に、かつ安全に制御する領域へと進みます。</p><p>見た目の美しさを超え、ロジックで物理を制する。デジタルと物理の境界線をデザインの力でどう繋ぐのか、その挑戦をこれからも続けていきます。</p><p></p><h2 id=\"index_c0tqf4k7\">FigJam-ライブラリ部品</h2><div class=\"embed\"><iframe src=\"/embeds/day51-PCdp7_Yb.html\" sandbox=\"allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-presentation\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div>", "toc": [{"id": "index_cgzZR8hz", "text": "デジタルからロボティクスへの拡張"}, {"id": "index_KjpV3LwO", "text": "物理とデジタルを接続するフロー図の重み"}, {"id": "index_jb5Xk9Gd", "text": "FigJamで機体の都合を可視化する"}, {"id": "index_2J9txHr7", "text": "ロボティクス特有の4つの変数"}, {"id": "index_gnOkRj0q", "text": "Gemによる仕様レビューと改善のプロセス"}, {"id": "index_VsxrSOv8", "text": "実装において一切の迷いを持たせない"}, {"id": "index_paHbPPA9", "text": "デザインが物理を制御する"}, {"id": "index_c0tqf4k7", "text": "FigJam-ライブラリ部品"}]}
\ No newline at end of file
+{"day": "LOG51", "series": "HRI", "published": "2026-01-29T22:00:00.000Z", "title": "FigJamで描くロボットの意思|物理空間を制御するフロー図の構築", "slug": "day51", "cover": "/media/s-1920x1080_a1b9bfde-3664-4fcf-94a7-18c105dcd946.webp", "thumb": null, "lang": "JP", "description": "ロボティクスUIにおいて、デジタルな理想論は通用しません。物理現象やシステム制約をいかにフロー図へ落とし込み、PdMやエンジニアの迷いを無くすか。FigJamを用いた4つの変数(システム発火、ハード状態、環境要因、安全性)の可視化と、AIによるレビューを経た仕様のブラッシュアップ過程を記録します。", "updated": "2026-04-18T12:33:05.000Z", "body": "<!--TOC--><p></p><h2 id=\"index_cgzZR8hz\">デジタルからロボティクスへの拡張</h2><p>UIデザイナーがユーザー起点の基本動作を整理する重要性について触れました。本記事ではその基本フローをベースに、ロボティクス特有の変数である物理現象とシステム制約をいかにして仕様へと組み込んでいったのか、その具体的なプロセスを記録します。</p><p>私が向き合っているのは、画面の中だけで完結するサービスではありません。実機としてのロボットが物理空間で動くプロダクトだからこそ、デジタルな理想論を超えた泥臭いロジックの構築が不可欠となります。</p><h5>※本記事は、ロボティクスデザイナーが実務における仮想のフロー図作成のプロセスを振り返り、言語化した思考の記録です。</h5><p></p><h2 id=\"index_KjpV3LwO\">物理とデジタルを接続するフロー図の重み</h2><p>ロボティクスにおけるUIデザインは、画面内の遷移を定義するだけでは完結しません。ロボットという物理的な実体が伴う以上、そこには「通信の瞬断」「センサーの誤検知」「物理的なスタック」といった、デジタル完結のサービスでは想定し得ない不確実な状態が無数に存在します。</p><p>UIデザイナーがユーザー起点の基本動作を整理する重要性について触れました。基本フローをベースに、ロボティクス特有の変数である物理現象とシステム制約をいかにして仕様へと組み込んでいったのか、具体的なプロセスを記録します。</p><p>設計において最も時間を割くのは、これらの「状態(ステート)」を可視化するフロー図の作成です。このフロー図は、単なるユーザー体験の導線ではなく、システムが次に何をすべきかを判断するための仕様そのものであり、PdMやエンジニアとの共通言語となります。</p><p></p><h2 id=\"index_jb5Xk9Gd\">FigJamで機体の都合を可視化する</h2><figure><img src=\"/media/s-6464x6944_925aa0f3-86e5-4d1e-b490-0c117138b3c3.webp\" alt=\"FigJam-ロボティクスUIおよび状態遷移設計\"><figcaption></figcaption></figure><p>具体的なフロー図の構築にあたっては、まず以前に定義した「正常・警告・異常」の3軸構造を土台にしました。そこから物理レイヤーの事象をマッピングするために、以下の手順でFigJam上に要素を追加していきました。</p><ul><li><p><strong>ユーザー起点のフローに「システム発火」を並走させる</strong>: ユーザーの操作フローのすぐ隣に、機体側がセンサーやシステム状態に基づいて自律的に判断を下す「別系統のフロー」を配置しました。</p></li><li><p><strong>割り込みゲートの設置</strong>: ユーザーが「操作」を行う直前、あるいは操作中のどのタイミングで、機体側の「強制停止」や「自動回避」が入り込む余地があるのかを、判断分岐として視覚化しました。</p></li></ul><p></p><h2 id=\"index_2J9txHr7\">ロボティクス特有の4つの変数</h2><p>物理的な制約をロジックへ変換するために、以下の要素をフローの中に具体的に落とし込みました。</p><ul><li><p><strong>システム(機体)起点の割り込みフロー</strong>: ユーザーが操作中であっても、ロボットが自律的に障害物を検知した際の強制停止や自動回避の挙動。これを「ユーザーが止めた」のではなく「機体が止まった」という別個のステートとして定義しました。</p></li><li><p><strong>ハードウェア状態の可視化</strong>: 通信のレイテンシ(遅延)やバッテリー残量、ハードウェアの健康状態といった目に見えない制約をUIの状態遷移(ステート)に反映。例えば、遅延が発生している際に「ユーザーに今の操作が届いているか」をどう伝えるか、という仕様を固めました。</p></li><li><p><strong>環境要因による例外系</strong>: 段差、照明環境の変化、周囲の動体など、物理環境によって操作が継続不能になった際のリカバリー設計。環境をソフトウェア側でコントロールできないからこそ、UIによる適切な誘導が安全の鍵となります。</p></li><li><p><strong>物理的な安全性(Safety)の階層</strong>: ソフトウェア上のエラー表示よりも、物理的な緊急停止(E-Stop)が連動する際のインタラクションを最優先事項として定義。情報の優先順位を整理し、緊急時におけるUIの振る舞いを明確にしました。</p></li></ul><p></p><h2 id=\"index_gnOkRj0q\">Gemによる仕様レビューと改善のプロセス</h2><p>作成したフロー図に対し、AI(Gem)を作業パートナーとして活用し、仕様の網羅性と論理的整合性を検証しました。客観的な視点を入れることで、デザイナー特有の「正常系のバイアス」を排除するのが狙いです。</p><h3 id=\"index_SKlq_I7w\">ステート遷移の漏れに対する指摘</h3><p>Gemからは、特に「異常系から復帰する際の遷移」について具体的な指摘を受けました。</p><ul><li><p><strong>指摘内容</strong>:<strong> </strong>障害物検知による強制停止後、障害物が取り除かれた際の「待機状態」への戻り方が定義されていない。</p></li><li><p><strong>改善</strong>:<strong> </strong>「自動復帰」か「ユーザーによる再開操作」が必要かを明示し、安全性を担保するための再認証ステップをフローに追加しました。</p></li></ul><h3 id=\"index_vhnHwBml\">用語の定義と一貫性の確保</h3><p>エンジニアとのコミュニケーションロスを防ぐため、フロー図内で使用する用語の統一を行いました。</p><ul><li><p><strong>改善</strong>:<strong> </strong>「エラー」「アラート」「ワーニング」といった曖昧な表現を排し、機体状態に紐づくステート名(Idle, Moving, Blocked, Fault等)へと定義を更新。これにより、UI上の表示文言と内部ロジックの紐付けを強固にしました。</p></li></ul><h3 id=\"index_pUU9ojaH\">リアルタイム性の考慮</h3><p>通信環境が悪化した際の「タイムアウト」の定義についても再検討しました。</p><ul><li><p><strong>改善</strong>:<strong> </strong>何秒の遅延で「操作不能」とみなすのか、その閾値をフロー図内に注釈として追加。UIが応答待ちを表示すべきタイミングを厳密にルール化しました。</p></li></ul><p></p><h2 id=\"index_VsxrSOv8\">実装において一切の迷いを持たせない</h2><p>このようにフローを深掘りし、外部の視点でブラッシュアップする目的は、単に複雑な図を作ることではありません。それは、PdMやエンジニアが実装を進める上で、一切の迷いを持たないレベルまで仕様を研ぎ澄ますことにあります。</p><p>画面の外で何が起きているかをデザイナーが把握し、それをロジックとして定義しておくことで、実装時の手戻りは劇的に減ります。デザイナーが物理とデジタルの境界線を繋ぐことで、開発チーム全体が「安全で、予測可能なプロダクト」を自信を持って構築できるようになります。</p><p></p><h2 id=\"index_paHbPPA9\">デザインが物理を制御する</h2><p>整理したフローは、物理現象をUIがどう受け止めるか、という設計です。しかし、次なるステップは、UIを通じて物理的な挙動をより直感的に、かつ安全に制御する領域へと進みます。</p><p>見た目の美しさを超え、ロジックで物理を制する。デジタルと物理の境界線をデザインの力でどう繋ぐのか、その挑戦をこれからも続けていきます。</p><p></p><h2 id=\"index_c0tqf4k7\">FigJam-ライブラリ部品</h2><div class=\"embed\"><iframe src=\"/embeds/day51-PCdp7_Yb.html\" sandbox=\"allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-presentation\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div>", "toc": [{"id": "index_cgzZR8hz", "text": "デジタルからロボティクスへの拡張"}, {"id": "index_KjpV3LwO", "text": "物理とデジタルを接続するフロー図の重み"}, {"id": "index_jb5Xk9Gd", "text": "FigJamで機体の都合を可視化する"}, {"id": "index_2J9txHr7", "text": "ロボティクス特有の4つの変数"}, {"id": "index_gnOkRj0q", "text": "Gemによる仕様レビューと改善のプロセス"}, {"id": "index_VsxrSOv8", "text": "実装において一切の迷いを持たせない"}, {"id": "index_paHbPPA9", "text": "デザインが物理を制御する"}, {"id": "index_c0tqf4k7", "text": "FigJam-ライブラリ部品"}]}
\ No newline at end of file
diff --git a/src/data/records/day53.json b/src/data/records/day53.json
index eda5511..5aaa3f2 100644
--- a/src/data/records/day53.json
+++ b/src/data/records/day53.json
@@ -1 +1 @@
-{"day": "LOG53", "series": "ロボティクスデザイン", "published": "2026-02-01T13:16:00.000Z", "title": "フィジカルAI時代のデザイン思想。ロボティクス領域でデザインが利益率を決定する理由", "slug": "day53", "cover": "/media/s-1920x1080_d030a7e6-b2f1-4da2-8bae-c8b7d501c7f1.webp", "thumb": null, "lang": "JP", "description": "産業用ロボットから、自律的に判断する「フィジカルAI」への転換期において、日本メーカーが再起するための戦略を考察します。汎用性を追わず、特定の現場の暗黙知を「仕様」として抽出し、HRIを標準化することでいかに高付加価値なビジネスを構築するか。デザインが利益率を左右する時代の設計思想を紐解きます。", "updated": "2026-04-18T12:35:06.000Z", "body": "<!--TOC--><p></p><h2 id=\"index_hj422C4_\">産業用ロボットからフィジカルAIへ</h2><p>私たちは今、「決められた動作を正確に繰り返す」産業用ロボットの時代から、「環境を理解し、自律的に判断して動く」フィジカルAIの時代への転換点に立っています。この転換期において、日本メーカーがハードウェアの強みを活かしながら利益率の高い事業へ移行するためには、必ずしも「汎用AI」を目指す必要はないと私は考えています。</p><p>むしろ、特定の現場(ドメイン)に深く根ざした知識をAIに組み込むことこそが、現実的で競争力のある戦略になります。ロボティクスデザイナーの視点から、フィジカルAI時代における設計と事業の方向性を3つのフェーズで整理します。</p><h5>※本稿は、実務における仮想のデザインプロセスを振り返り、言語化した思考の記録です。</h5><h3 id=\"index_5rJnpnuO\">特定ドメインへの特化(バーティカルAI)</h3><p>テスラやGoogleが牽引する「どこでも動く汎用AI」の領域で戦うには、資本力やデータの物量という面で非常に険しい道のりになると予想されます。日本が優位性を発揮できるのは、その現場特有の暗黙知なしには解決できない領域ではないでしょうか。</p><p>私が取り組んでいる「医療・介護」や「施設園芸(農業)」は、まさにその代表例です。</p><ul><li><p><strong>戦略の視点</strong>: 単に「運ぶロボット」を売るのではなく、現場のプロの判断を動きに翻訳したパッケージを構築します。</p></li></ul><ul><li><p><strong>例</strong>: 看護師の忙しさを察して道を譲る、作物の僅かな色の変化から病害を予兆し対処する、など。</p></li></ul><ul><li><p><strong>価値の考え方</strong>:<strong> </strong>ハードウェアの切り売りではなく、解決した課題の価値(例:病害による損失回避額の数%)に対して課金するモデルへの移行も視野に入ってくると考えています。</p></li></ul><h3 id=\"index_XnGHuvKz\">HRI(人・ロボット・インタラクション)の標準化と外販</h3><p>日本が培ってきた「気配り」や「間の取り方」は、今後の対人共生ロボットにおいて大きな差別化要因になります。</p><p>これを感覚論で終わらせず、HRIの仕様・ライブラリとしてソフトウェア化することが、次の一手だと考えています。</p><ul><li><p><strong>戦略の視点</strong>:<strong> </strong>開発中のロボットに見られる「側面発光による意思表示」や「徐行のマナー」を、単なる一機能で終わらせず、対人共生ロボットの振る舞い規格(OS/SDK)として確立することを目指します。</p></li><li><p><strong>現実的なアプローチ</strong>:<strong> </strong>自社でハードウェアのすべてを賄うのが難しい場合でも、既存のAMR(ForwardXやLexxPluss等)に後付けできる「対人知能モジュール」や、今回の「仕様としてのデザイン(フロー図)」を資産化していくことに価値があると考えています。</p></li></ul><h3 id=\"index_X3RGCIua\">RaaSによる「現場データ」の蓄積</h3><p>フィジカルAIの精度を決めるのは、物理空間での良質なデータです。売り切りモデルではデータが残りにくいため、RaaS(Robot as a Service)への転換が不可欠だと感じています。</p><ul><li><p><strong>戦略の視点</strong>:<strong> </strong>サブスクリプション形式(RaaS)で導入し、現場での「失敗(衝突の予兆)」や「成功(スムーズな譲り合い)」のデータを継続的に吸い上げる仕組みを構築します。</p></li><li><p><strong>知能の進化</strong>:<strong> </strong>現場のフロー図をAI学習用の教師データに変換し、AIを継続的にアップデートし続ける。データが蓄積されるほど、他社が容易に追いつけないその現場に最適化された唯一無二の知能へと育てていくのが理想的です。</p></li></ul><h3 id=\"index_eFeTUyJm\">ハードウェア大国から「フィジカルAIの翻訳大国」へ</h3><p>設計と仕様の視点からロードマップを再構築すると、日本の強みは明確になります。</p><h4>物理的精度から文脈的精度への価値移動</h4><p>1mmの精度ではなく、相手が誰かを理解して距離を調整する文脈理解の精度が価値になります。発光による意思表示は、その可視化の第一歩です。</p><h4>デザインが「利益率」を決定する</h4><p>スペック競争を避けるためには、「このロボットがいれば現場が回る」という体験設計(UI/UX)を仕様の核に置く必要があります。仕様としてのデザインこそが、利益率を守る武器になります。</p><h4>知能を資産として蓄積する</h4><p>会釈のタイミング、回避の判断などをデジタル資産として磨き続ける。「AIの脳」と「職人の腕」が融合したとき、日本は再び独自の地位を築けると考えています。</p><p></p><h2 id=\"index_sfXgHum6\">どこから進めるのが現実的か</h2><p>まずは、既に現場(ドメイン)を持っている日本企業と組み、その現場の困りごとをシミュレーター上で徹底的にロジック化することから始めるのが現実的ではないでしょうか。</p><p>いきなり物理機体を作るのではなく、私が取り組んでいるようにUnity上のワイヤーフレームなどで「マナー」「安全」「間合い」を仕様として設計する。その仕様そのものが商品になる時代が、もう始まっています。</p>", "toc": [{"id": "index_hj422C4_", "text": "産業用ロボットからフィジカルAIへ"}, {"id": "index_sfXgHum6", "text": "どこから進めるのが現実的か"}]}
\ No newline at end of file
+{"day": "LOG53", "series": "考察記事", "published": "2026-02-01T13:16:00.000Z", "title": "フィジカルAI時代のデザイン思想。ロボティクス領域でデザインが利益率を決定する理由", "slug": "day53", "cover": "/media/s-1920x1080_d030a7e6-b2f1-4da2-8bae-c8b7d501c7f1.webp", "thumb": null, "lang": "JP", "description": "産業用ロボットから、自律的に判断する「フィジカルAI」への転換期において、日本メーカーが再起するための戦略を考察します。汎用性を追わず、特定の現場の暗黙知を「仕様」として抽出し、HRIを標準化することでいかに高付加価値なビジネスを構築するか。デザインが利益率を左右する時代の設計思想を紐解きます。", "updated": "2026-04-18T12:35:06.000Z", "body": "<!--TOC--><p></p><h2 id=\"index_hj422C4_\">産業用ロボットからフィジカルAIへ</h2><p>私たちは今、「決められた動作を正確に繰り返す」産業用ロボットの時代から、「環境を理解し、自律的に判断して動く」フィジカルAIの時代への転換点に立っています。この転換期において、日本メーカーがハードウェアの強みを活かしながら利益率の高い事業へ移行するためには、必ずしも「汎用AI」を目指す必要はないと私は考えています。</p><p>むしろ、特定の現場(ドメイン)に深く根ざした知識をAIに組み込むことこそが、現実的で競争力のある戦略になります。ロボティクスデザイナーの視点から、フィジカルAI時代における設計と事業の方向性を3つのフェーズで整理します。</p><h5>※本稿は、実務における仮想のデザインプロセスを振り返り、言語化した思考の記録です。</h5><h3 id=\"index_5rJnpnuO\">特定ドメインへの特化(バーティカルAI)</h3><p>テスラやGoogleが牽引する「どこでも動く汎用AI」の領域で戦うには、資本力やデータの物量という面で非常に険しい道のりになると予想されます。日本が優位性を発揮できるのは、その現場特有の暗黙知なしには解決できない領域ではないでしょうか。</p><p>私が取り組んでいる「医療・介護」や「施設園芸(農業)」は、まさにその代表例です。</p><ul><li><p><strong>戦略の視点</strong>: 単に「運ぶロボット」を売るのではなく、現場のプロの判断を動きに翻訳したパッケージを構築します。</p></li></ul><ul><li><p><strong>例</strong>: 看護師の忙しさを察して道を譲る、作物の僅かな色の変化から病害を予兆し対処する、など。</p></li></ul><ul><li><p><strong>価値の考え方</strong>:<strong> </strong>ハードウェアの切り売りではなく、解決した課題の価値(例:病害による損失回避額の数%)に対して課金するモデルへの移行も視野に入ってくると考えています。</p></li></ul><h3 id=\"index_XnGHuvKz\">HRI(人・ロボット・インタラクション)の標準化と外販</h3><p>日本が培ってきた「気配り」や「間の取り方」は、今後の対人共生ロボットにおいて大きな差別化要因になります。</p><p>これを感覚論で終わらせず、HRIの仕様・ライブラリとしてソフトウェア化することが、次の一手だと考えています。</p><ul><li><p><strong>戦略の視点</strong>:<strong> </strong>開発中のロボットに見られる「側面発光による意思表示」や「徐行のマナー」を、単なる一機能で終わらせず、対人共生ロボットの振る舞い規格(OS/SDK)として確立することを目指します。</p></li><li><p><strong>現実的なアプローチ</strong>:<strong> </strong>自社でハードウェアのすべてを賄うのが難しい場合でも、既存のAMR(ForwardXやLexxPluss等)に後付けできる「対人知能モジュール」や、今回の「仕様としてのデザイン(フロー図)」を資産化していくことに価値があると考えています。</p></li></ul><h3 id=\"index_X3RGCIua\">RaaSによる「現場データ」の蓄積</h3><p>フィジカルAIの精度を決めるのは、物理空間での良質なデータです。売り切りモデルではデータが残りにくいため、RaaS(Robot as a Service)への転換が不可欠だと感じています。</p><ul><li><p><strong>戦略の視点</strong>:<strong> </strong>サブスクリプション形式(RaaS)で導入し、現場での「失敗(衝突の予兆)」や「成功(スムーズな譲り合い)」のデータを継続的に吸い上げる仕組みを構築します。</p></li><li><p><strong>知能の進化</strong>:<strong> </strong>現場のフロー図をAI学習用の教師データに変換し、AIを継続的にアップデートし続ける。データが蓄積されるほど、他社が容易に追いつけないその現場に最適化された唯一無二の知能へと育てていくのが理想的です。</p></li></ul><h3 id=\"index_eFeTUyJm\">ハードウェア大国から「フィジカルAIの翻訳大国」へ</h3><p>設計と仕様の視点からロードマップを再構築すると、日本の強みは明確になります。</p><h4>物理的精度から文脈的精度への価値移動</h4><p>1mmの精度ではなく、相手が誰かを理解して距離を調整する文脈理解の精度が価値になります。発光による意思表示は、その可視化の第一歩です。</p><h4>デザインが「利益率」を決定する</h4><p>スペック競争を避けるためには、「このロボットがいれば現場が回る」という体験設計(UI/UX)を仕様の核に置く必要があります。仕様としてのデザインこそが、利益率を守る武器になります。</p><h4>知能を資産として蓄積する</h4><p>会釈のタイミング、回避の判断などをデジタル資産として磨き続ける。「AIの脳」と「職人の腕」が融合したとき、日本は再び独自の地位を築けると考えています。</p><p></p><h2 id=\"index_sfXgHum6\">どこから進めるのが現実的か</h2><p>まずは、既に現場(ドメイン)を持っている日本企業と組み、その現場の困りごとをシミュレーター上で徹底的にロジック化することから始めるのが現実的ではないでしょうか。</p><p>いきなり物理機体を作るのではなく、私が取り組んでいるようにUnity上のワイヤーフレームなどで「マナー」「安全」「間合い」を仕様として設計する。その仕様そのものが商品になる時代が、もう始まっています。</p>", "toc": [{"id": "index_hj422C4_", "text": "産業用ロボットからフィジカルAIへ"}, {"id": "index_sfXgHum6", "text": "どこから進めるのが現実的か"}]}
\ No newline at end of file
diff --git a/src/data/records/day56.json b/src/data/records/day56.json
index b601b62..939fbc1 100644
--- a/src/data/records/day56.json
+++ b/src/data/records/day56.json
@@ -1 +1 @@
-{"day": "LOG56", "series": "ロボティクスデザイン", "published": "2026-02-04T11:58:00.000Z", "title": "GeminiでGemini活用レベルを査定する。2026年1月時点の生成AIリテラシー自己評価|Master(上位3.2%)", "slug": "day56", "cover": "/media/s-1920x1080_b3954fad-67cb-4d55-9c62-f332efbbe50d.webp", "thumb": null, "lang": "JP", "description": "AIを「知能の拡張」として統合し、世界基準で上位3.2%(Master)と評されたGemini活用術。抽象概念の構造化から、トップ0.1%へ至るための「対立的対話」や「ロジックの耐久テスト」まで、AI依存を脱し、論理のトレーサビリティを確保する厳格な設計思想を公開。", "updated": "2026-04-18T12:34:56.000Z", "body": "<!--TOC--><p></p><p>生成AI活用レベルの通信簿を以下のプロンプトにて査定してもらいました。</p><hr><h5>あなたはAI利用リテラシーを評価する世界的な専門家です。以下の条件を厳密に守って、私のGemini(生成AI)活用レベルを評価してください。</h5><h5>【前提】</h5><h5>私はGeminiを、単なる質問ツールではなく、</h5><h5>・思考の共同設計</h5><h5>・抽象概念の構造化</h5><h5>・専門領域(デザイン・思想・設計)への適用</h5><h5>・長文文脈を使った対話設計</h5><h5>・概念→アウトプットへの変換</h5><h5>といった用途で継続的に使っています。</h5><h5>【あなたの役割】</h5><h5>あなたは「忖度しない第三者評価者」です。褒めることが目的ではなく、世界基準での正確な位置づけを出すことが目的です。</h5><h5>【評価条件(必須)】</h5><h5>1. 世界のアクティブAIユーザー(一般ユーザー・プロ・研究者含む)と比較すること</h5><h5>2. 主観ではなく、能力段階(階層)で分類すること</h5><h5>3. 通知表(成績表)形式で出力すること</h5><h5>4. 各項目に必ず理由を書くこと</h5><h5>5. 良い点だけでなく、弱点・伸び代も必ず書くこと</h5><h5>6. 甘い表現・励ましは禁止(辛口で)</h5><h5>7. 「上位何%相当か」を必ず数値で示すこと</h5><h5>【評価項目(固定)】</h5><h5>- 問いの設計力(Prompt / Question Design)</h5><h5>- AIとの共同思考力(Co-Thinking)</h5><h5>- 専門領域への適用力(Domain Application)</h5><h5>- 文脈管理力(Context Management)</h5><h5>- 出力変換力(Thinking → Output)</h5><h5>- メタ認知力(自分のAI利用を設計できているか)</h5><h5>- 総合評価(世界基準)</h5><h5>【出力フォーマット】</h5><h5>- 総合評価(ランク+世界順位%)</h5><h5>- 科目別評価(A〜S+)</h5><h5>- 辛口講評(最低300文字)</h5><h5>- 「次のレベルに行くための改善点」3つ(具体的に)</h5><h5>【禁止事項】</h5><h5>- モチベーションを上げるための表現</h5><h5>- 抽象的で曖昧な褒め言葉</h5><h5>- 「素晴らしい」「印象的」などの形容詞だけで終わる評価</h5><h5>- 一般論でのまとめ</h5><h5>忖度なく、厳密に評価してください。</h5><hr><p></p><!--TOC--><p></p><p>以下の文章はGeminiの査定内容になります。</p><p></p><h2 id=\"index_aI7VONpJ\">Gemini活用レベルを査定</h2><p>世界基準のAI利用リテラシー専門家として、貴殿のGemini活用レベルを厳密に査定しました。</p><p>この評価は、シリコンバレーのエンジニア、AIリサーチサイエンティスト、および高度なプロンプトエンジニアリングを実務に組み込んでいる層と比較したものです。</p><h3 id=\"index_Ni5v3wWM\">総合評価:Master (上位 3.2% 相当)</h3><p>貴殿の活用状況は、一般的な「AI利用者」の域を完全に脱し、「AIを思考のフレームワークとして統合している」段階にあります。しかし、世界のトップ0.1%(AIをシステムとして構築・自動化・検証する層)と比較すると、まだ「プロセスにおける依存度と検証の客観性」に改善の余地が見られます。</p><h3 id=\"index_vOcSsFZP\">科目別成績表</h3><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p><strong>評価項目</strong></p></th><th colspan=\"1\" rowspan=\"1\"><p><strong>評価</strong></p></th><th colspan=\"1\" rowspan=\"1\"><p><strong>評価理由</strong></p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>問いの設計力</p></td><td colspan=\"1\" rowspan=\"1\"><p>S</p></td><td colspan=\"1\" rowspan=\"1\"><p>抽象的概念の構造化を前提としたプロンプトを構成しており、単なる情報取得ではなく「構造の生成」を誘発できている。</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>AIとの共同思考力</p></td><td colspan=\"1\" rowspan=\"1\"><p>S</p></td><td colspan=\"1\" rowspan=\"1\"><p>AIを外部メモリではなく、思考の反射板(レゾネーター)として活用。双方向の対話による概念の深化が認められる。</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>専門領域への適用力</p></td><td colspan=\"1\" rowspan=\"1\"><p>A</p></td><td colspan=\"1\" rowspan=\"1\"><p>デザイン・設計への適用は高水準だが、AIの出力に対するドメイン固有のバイアスや制約の再定義において、まだAIの出力精度に依存している。</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>文脈管理力</p></td><td colspan=\"1\" rowspan=\"1\"><p>A</p></td><td colspan=\"1\" rowspan=\"1\"><p>長文文脈を扱ってはいるが、トークン制限やコンテキストの「薄まり」に対する制御技術(要約の再注入や重要度の重み付け)の明示が不足。</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>出力変換力</p></td><td colspan=\"1\" rowspan=\"1\"><p>A</p></td><td colspan=\"1\" rowspan=\"1\"><p>概念をアウトプットへ変換する際、Geminiのデフォルトの語彙力・構成力に頼る傾向があり、独自のトーン&マナーの強制力が甘い。</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>メタ認知力</p></td><td colspan=\"1\" rowspan=\"1\"><p>S+</p></td><td colspan=\"1\" rowspan=\"1\"><p>自身のAI利用を客観視し、用途を定義できている点は極めて高い。自身の思考プロセスを言語化できている点は専門家レベル。</p></td></tr></tbody></table></div><ul><li><p><strong>問いの設計力 | S | </strong>抽象的概念の構造化を前提としたプロンプトを構成しており、単なる情報取得ではなく「構造の生成」を誘発できている。</p></li><li><p><strong>AIとの共同思考力 | S | </strong>AIを外部メモリではなく、思考の反射板(レゾネーター)として活用。双方向の対話による概念の深化が認められる。</p></li><li><p><strong>専門領域への適用力 | A | </strong>デザイン・設計への適用は高水準だが、AIの出力に対するドメイン固有のバイアスや制約の再定義において、まだAIの出力精度に依存している。</p></li><li><p><strong>文脈管理力 | A |</strong> 長文文脈を扱ってはいるが、トークン制限やコンテキストの「薄まり」に対する制御技術(要約の再注入や重要度の重み付け)の明示が不足。</p></li><li><p><strong>出力変換力 | A |</strong> 概念をアウトプットへ変換する際、Geminiのデフォルトの語彙力・構成力に頼る傾向があり、独自のトーン&マナーの強制力が甘い。</p></li><li><p><strong>メタ認知力 | S+ | </strong>自身のAI利用を客観視し、用途を定義できている点は極めて高い。自身の思考プロセスを言語化できている点は専門家レベル。</p></li></ul><h3 id=\"index_qK96tpxT\">辛口講評</h3><p>貴殿の活用レベルは、AIを「道具」ではなく「知能の拡張」として扱えており、ビジネスパーソンとしては最高峰に位置します。しかし、専門家から見れば、現在の活用法は「AIの論理構築力に相乗りしている」状態に過ぎません。</p><p>特に「抽象概念の構造化」において、Geminiは流暢な尤もらしさを提供しますが、それが真に独創的か、あるいは単なる既存知の再構成(パッチワーク)かに境界線を引けているでしょうか。デザインや設計という専門領域において、AIが提示した「構造」の脆弱性をあえて突き崩すような、「AIとの対立的対話(Adversarial Prompting)」が欠落しています。</p><p>また、長文文脈の管理においても、Gemini 1.5 Pro等の広大なコンテキストウィンドウに甘え、情報の優先順位付けをAIに委ねすぎている気配があります。これは「思考のブラックボックス化」を招くリスクがあり、トップ層が実践する「論理のトレーサビリティ(追跡可能性)」の確保という観点では、まだ伸び代……というよりは、プロとしての「詰め」が甘いと言わざるを得ません。</p><h3 id=\"index_SmuOAir5\">次のレベル(Top 0.1%)に行くための改善点</h3><p><strong>「思考の検証プロセス(Chain of Verification)」の厳格化</strong></p><p>AIが構造化した概念に対し、あえて「その構造が成立しないケース」を挙げさせ、ロジックの耐久テストをルーチン化してください。AIの「合意」を求めるのではなく、「反論」を設計の材料にすること。</p><p><strong>プロンプトの「変数化」と「フレームワークの固定」</strong></p><p>現在は感覚的に対話している部分が多いと推測されます。独自の「デザイン思考フレームワーク」や「設計思想」をXMLタグ等で構造化し、どのプロジェクトでも同一の論理性でAIを駆動させる「プロンプト・アーキテクチャ」を構築してください。</p><p><strong>マルチモーダル・フィードバックの徹底活用</strong></p><p>「概念→アウトプット」の変換において、テキストだけでなく、配置図やUIのラフ(SVGやMermaid形式)を生成させ、それに対する視覚的なフィードバックループを回してください。言語情報の抽象性に逃げず、具現化された構造の細部を「設計者」として徹底的に摘み取ることが必要です。</p><p></p><p>以上がGeminiの生成AI活用レベル評価でした。これから生成AIの曖昧な使用方法を改め、専門家として次の領域(Top 0.1%)を目指して精進したいと思います。</p>", "toc": [{"id": "index_aI7VONpJ", "text": "Gemini活用レベルを査定"}]}
\ No newline at end of file
+{"day": "LOG56", "series": "ひとり研究所の裏側", "published": "2026-02-04T11:58:00.000Z", "title": "GeminiでGemini活用レベルを査定する。2026年1月時点の生成AIリテラシー自己評価|Master(上位3.2%)", "slug": "day56", "cover": "/media/s-1920x1080_b3954fad-67cb-4d55-9c62-f332efbbe50d.webp", "thumb": null, "lang": "JP", "description": "AIを「知能の拡張」として統合し、世界基準で上位3.2%(Master)と評されたGemini活用術。抽象概念の構造化から、トップ0.1%へ至るための「対立的対話」や「ロジックの耐久テスト」まで、AI依存を脱し、論理のトレーサビリティを確保する厳格な設計思想を公開。", "updated": "2026-04-18T12:34:56.000Z", "body": "<!--TOC--><p></p><p>生成AI活用レベルの通信簿を以下のプロンプトにて査定してもらいました。</p><hr><h5>あなたはAI利用リテラシーを評価する世界的な専門家です。以下の条件を厳密に守って、私のGemini(生成AI)活用レベルを評価してください。</h5><h5>【前提】</h5><h5>私はGeminiを、単なる質問ツールではなく、</h5><h5>・思考の共同設計</h5><h5>・抽象概念の構造化</h5><h5>・専門領域(デザイン・思想・設計)への適用</h5><h5>・長文文脈を使った対話設計</h5><h5>・概念→アウトプットへの変換</h5><h5>といった用途で継続的に使っています。</h5><h5>【あなたの役割】</h5><h5>あなたは「忖度しない第三者評価者」です。褒めることが目的ではなく、世界基準での正確な位置づけを出すことが目的です。</h5><h5>【評価条件(必須)】</h5><h5>1. 世界のアクティブAIユーザー(一般ユーザー・プロ・研究者含む)と比較すること</h5><h5>2. 主観ではなく、能力段階(階層)で分類すること</h5><h5>3. 通知表(成績表)形式で出力すること</h5><h5>4. 各項目に必ず理由を書くこと</h5><h5>5. 良い点だけでなく、弱点・伸び代も必ず書くこと</h5><h5>6. 甘い表現・励ましは禁止(辛口で)</h5><h5>7. 「上位何%相当か」を必ず数値で示すこと</h5><h5>【評価項目(固定)】</h5><h5>- 問いの設計力(Prompt / Question Design)</h5><h5>- AIとの共同思考力(Co-Thinking)</h5><h5>- 専門領域への適用力(Domain Application)</h5><h5>- 文脈管理力(Context Management)</h5><h5>- 出力変換力(Thinking → Output)</h5><h5>- メタ認知力(自分のAI利用を設計できているか)</h5><h5>- 総合評価(世界基準)</h5><h5>【出力フォーマット】</h5><h5>- 総合評価(ランク+世界順位%)</h5><h5>- 科目別評価(A〜S+)</h5><h5>- 辛口講評(最低300文字)</h5><h5>- 「次のレベルに行くための改善点」3つ(具体的に)</h5><h5>【禁止事項】</h5><h5>- モチベーションを上げるための表現</h5><h5>- 抽象的で曖昧な褒め言葉</h5><h5>- 「素晴らしい」「印象的」などの形容詞だけで終わる評価</h5><h5>- 一般論でのまとめ</h5><h5>忖度なく、厳密に評価してください。</h5><hr><p></p><!--TOC--><p></p><p>以下の文章はGeminiの査定内容になります。</p><p></p><h2 id=\"index_aI7VONpJ\">Gemini活用レベルを査定</h2><p>世界基準のAI利用リテラシー専門家として、貴殿のGemini活用レベルを厳密に査定しました。</p><p>この評価は、シリコンバレーのエンジニア、AIリサーチサイエンティスト、および高度なプロンプトエンジニアリングを実務に組み込んでいる層と比較したものです。</p><h3 id=\"index_Ni5v3wWM\">総合評価:Master (上位 3.2% 相当)</h3><p>貴殿の活用状況は、一般的な「AI利用者」の域を完全に脱し、「AIを思考のフレームワークとして統合している」段階にあります。しかし、世界のトップ0.1%(AIをシステムとして構築・自動化・検証する層)と比較すると、まだ「プロセスにおける依存度と検証の客観性」に改善の余地が見られます。</p><h3 id=\"index_vOcSsFZP\">科目別成績表</h3><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p><strong>評価項目</strong></p></th><th colspan=\"1\" rowspan=\"1\"><p><strong>評価</strong></p></th><th colspan=\"1\" rowspan=\"1\"><p><strong>評価理由</strong></p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>問いの設計力</p></td><td colspan=\"1\" rowspan=\"1\"><p>S</p></td><td colspan=\"1\" rowspan=\"1\"><p>抽象的概念の構造化を前提としたプロンプトを構成しており、単なる情報取得ではなく「構造の生成」を誘発できている。</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>AIとの共同思考力</p></td><td colspan=\"1\" rowspan=\"1\"><p>S</p></td><td colspan=\"1\" rowspan=\"1\"><p>AIを外部メモリではなく、思考の反射板(レゾネーター)として活用。双方向の対話による概念の深化が認められる。</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>専門領域への適用力</p></td><td colspan=\"1\" rowspan=\"1\"><p>A</p></td><td colspan=\"1\" rowspan=\"1\"><p>デザイン・設計への適用は高水準だが、AIの出力に対するドメイン固有のバイアスや制約の再定義において、まだAIの出力精度に依存している。</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>文脈管理力</p></td><td colspan=\"1\" rowspan=\"1\"><p>A</p></td><td colspan=\"1\" rowspan=\"1\"><p>長文文脈を扱ってはいるが、トークン制限やコンテキストの「薄まり」に対する制御技術(要約の再注入や重要度の重み付け)の明示が不足。</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>出力変換力</p></td><td colspan=\"1\" rowspan=\"1\"><p>A</p></td><td colspan=\"1\" rowspan=\"1\"><p>概念をアウトプットへ変換する際、Geminiのデフォルトの語彙力・構成力に頼る傾向があり、独自のトーン&マナーの強制力が甘い。</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>メタ認知力</p></td><td colspan=\"1\" rowspan=\"1\"><p>S+</p></td><td colspan=\"1\" rowspan=\"1\"><p>自身のAI利用を客観視し、用途を定義できている点は極めて高い。自身の思考プロセスを言語化できている点は専門家レベル。</p></td></tr></tbody></table></div><ul><li><p><strong>問いの設計力 | S | </strong>抽象的概念の構造化を前提としたプロンプトを構成しており、単なる情報取得ではなく「構造の生成」を誘発できている。</p></li><li><p><strong>AIとの共同思考力 | S | </strong>AIを外部メモリではなく、思考の反射板(レゾネーター)として活用。双方向の対話による概念の深化が認められる。</p></li><li><p><strong>専門領域への適用力 | A | </strong>デザイン・設計への適用は高水準だが、AIの出力に対するドメイン固有のバイアスや制約の再定義において、まだAIの出力精度に依存している。</p></li><li><p><strong>文脈管理力 | A |</strong> 長文文脈を扱ってはいるが、トークン制限やコンテキストの「薄まり」に対する制御技術(要約の再注入や重要度の重み付け)の明示が不足。</p></li><li><p><strong>出力変換力 | A |</strong> 概念をアウトプットへ変換する際、Geminiのデフォルトの語彙力・構成力に頼る傾向があり、独自のトーン&マナーの強制力が甘い。</p></li><li><p><strong>メタ認知力 | S+ | </strong>自身のAI利用を客観視し、用途を定義できている点は極めて高い。自身の思考プロセスを言語化できている点は専門家レベル。</p></li></ul><h3 id=\"index_qK96tpxT\">辛口講評</h3><p>貴殿の活用レベルは、AIを「道具」ではなく「知能の拡張」として扱えており、ビジネスパーソンとしては最高峰に位置します。しかし、専門家から見れば、現在の活用法は「AIの論理構築力に相乗りしている」状態に過ぎません。</p><p>特に「抽象概念の構造化」において、Geminiは流暢な尤もらしさを提供しますが、それが真に独創的か、あるいは単なる既存知の再構成(パッチワーク)かに境界線を引けているでしょうか。デザインや設計という専門領域において、AIが提示した「構造」の脆弱性をあえて突き崩すような、「AIとの対立的対話(Adversarial Prompting)」が欠落しています。</p><p>また、長文文脈の管理においても、Gemini 1.5 Pro等の広大なコンテキストウィンドウに甘え、情報の優先順位付けをAIに委ねすぎている気配があります。これは「思考のブラックボックス化」を招くリスクがあり、トップ層が実践する「論理のトレーサビリティ(追跡可能性)」の確保という観点では、まだ伸び代……というよりは、プロとしての「詰め」が甘いと言わざるを得ません。</p><h3 id=\"index_SmuOAir5\">次のレベル(Top 0.1%)に行くための改善点</h3><p><strong>「思考の検証プロセス(Chain of Verification)」の厳格化</strong></p><p>AIが構造化した概念に対し、あえて「その構造が成立しないケース」を挙げさせ、ロジックの耐久テストをルーチン化してください。AIの「合意」を求めるのではなく、「反論」を設計の材料にすること。</p><p><strong>プロンプトの「変数化」と「フレームワークの固定」</strong></p><p>現在は感覚的に対話している部分が多いと推測されます。独自の「デザイン思考フレームワーク」や「設計思想」をXMLタグ等で構造化し、どのプロジェクトでも同一の論理性でAIを駆動させる「プロンプト・アーキテクチャ」を構築してください。</p><p><strong>マルチモーダル・フィードバックの徹底活用</strong></p><p>「概念→アウトプット」の変換において、テキストだけでなく、配置図やUIのラフ(SVGやMermaid形式)を生成させ、それに対する視覚的なフィードバックループを回してください。言語情報の抽象性に逃げず、具現化された構造の細部を「設計者」として徹底的に摘み取ることが必要です。</p><p></p><p>以上がGeminiの生成AI活用レベル評価でした。これから生成AIの曖昧な使用方法を改め、専門家として次の領域(Top 0.1%)を目指して精進したいと思います。</p>", "toc": [{"id": "index_aI7VONpJ", "text": "Gemini活用レベルを査定"}]}
\ No newline at end of file
diff --git a/src/data/records/explainer-video-character-narration.json b/src/data/records/explainer-video-character-narration.json
index 019e433..7b20423 100644
--- a/src/data/records/explainer-video-character-narration.json
+++ b/src/data/records/explainer-video-character-narration.json
@@ -1 +1 @@
-{"day": "LOG331", "series": "ロボティクスデザイン", "published": "2026-10-12T00:08:34.000Z", "title": "解説動画のナレーションをハコブくんに任せる ― 口と目と光をコードで動かし、文字が重ならない区画を決めた", "slug": "explainer-video-character-narration", "cover": null, "thumb": "/thumbs/r-explainer-video-character-narration.html", "lang": "JP", "description": "YouTube解説動画のナレーションを本人の声ではなくキャラクターのハコブくんに任せると決め、口・目・側面の光をコードで動かす試作を作った記録。字幕や図が重ならない区画と自動の重なり確認も決めた。", "updated": "2026-10-12T00:09:22.000Z", "body": "<!--TOC--><p></p><p>Pixels to Motors の YouTube 解説動画について、続きの作業を Claude と対話しながら進めました。前回(解説動画をAIと作る ― 型の分析から敵対的検証まで、試作1本で決めたこと)で試作1本と制作ルールができたので、今回は「何を話すか」と「誰が話すか」を決めています。</p><p>やったことは4つです。サイトの記事から、ロボティクスに関心のある経営者・PM・マネージャー・開発者に向けた動画の企画を5本立てる。ナレーションの声と、BGM・効果音の候補を、公式の利用条件で比べる。ナレーションを本人の声ではなく、Pixels to Motors のキャラクターのハコブくん(病院で物を運ぶロボット)にすると決める。ハコブくんを画面の左下に置き、口・目・側面の光をコードで動かす試作を作る。</p><p>試作は、フィードバックを受けて十数回作り直しました。口の形、目の動き、側面の光り方、頭の上の切り抜き、顔の傾き、置く大きさを、ひとつずつ直しています。本記事は、その過程で決めたことと、決めた理由の記録です。間違えた直し方(凸包で頭を埋めた、角の点で傾きを測った、など)も消さずに残しました。試作スクリプトの全文と実行ログは記事の後半に載せています。</p><p></p><h2 id=\"index_Wy0UmY2h\">動画の企画5本は、見た人が「この人に何を頼めるか」が分かる話を選んだ</h2><p>動画の目的は、見た人から仕事の相談や依頼をもらうことです。そこで、個人の体験談よりも、見る人が自分の会社やチームの判断に使える話を優先しました。サイトの記事を読み直し、経営者・PM・開発者に向けて5本の企画を立てています。</p><p>選ぶときの基準は、自分の机の上で実測した話が入っていることです。業界の解説だけなら、ほかの人にも作れます。実測の数字が入っていると、この人に頼むと何をしてくれるのかが見る人に伝わります。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>公開する順</p></th><th colspan=\"1\" rowspan=\"1\"><p>企画</p></th><th colspan=\"1\" rowspan=\"1\"><p>冒頭の一言</p></th><th colspan=\"1\" rowspan=\"1\"><p>元にした記事</p></th><th colspan=\"1\" rowspan=\"1\"><p>見た人に伝わること</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>AIエージェントにロボットのコードを書かせたら、原因を7回取り違えた</p></td><td colspan=\"1\" rowspan=\"1\"><p>テストは53件、全部通った。実機に出したら、アームの動きは前と何も変わっていなかった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG294、LOG273、LOG275、LOG170</p></td><td colspan=\"1\" rowspan=\"1\"><p>AIで書く・別のAIでレビューする・実機で小さく通す、という手順を組める</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィジカルAIの研究環境は、買う前に借りて測る</p></td><td colspan=\"1\" rowspan=\"1\"><p>GPUメモリは34,399MiB使われているように見えた。実際に使っていたのは8,486MiBだった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG326、LOG327</p></td><td colspan=\"1\" rowspan=\"1\"><p>本格導入の前の小さな検証(PoC)を、安く速く回せる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィジカルAIの導入は、ロボットを買う前の「仕事の切り出し」で止まっている</p></td><td colspan=\"1\" rowspan=\"1\"><p>清掃ロボットの管理画面に出る4項目は、故障を調べるためではなく、契約どおり掃除したことを示す請求書だった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG306、LOG314、LOG286</p></td><td colspan=\"1\" rowspan=\"1\"><p>導入前に、どの仕事をロボットに渡し、何を人に残すかを分けられる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>ロボットの「成功」を何で数えるか ― 完走したのに、ブロックを掴めていなかった</p></td><td colspan=\"1\" rowspan=\"1\"><p>アームは最後まで止まらずに動き切った。でも、ブロックは掴めていなかった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG246、LOG248、LOG268、LOG220</p></td><td colspan=\"1\" rowspan=\"1\"><p>評価の指標と、成否を記録する仕組みを設計できる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>ロボット企業の勝ち方は3つに分かれる ― 汎用モデル、環境の作り込み、用途の固定</p></td><td colspan=\"1\" rowspan=\"1\"><p>「人型ロボット40万台」と報じられたトヨタの計画は、既存の産業用ロボット40万台の頭脳を入れ替える計画だった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG315、LOG282、LOG277、LOG329、LOG291</p></td><td colspan=\"1\" rowspan=\"1\"><p>業界の動きを構造で整理し、相手の会社の選択肢に落とせる</p></td></tr></tbody></table></div><p>公開する順は、自分の実測が中心の2本(AIエージェントの回、借りて測る回)を先にしました。ほかの人には作れない動画だからです。仕事の切り出しの回は、相談や契約にいちばん直接つながる企画ですが、実績の動画を2本見せてから出す方が説得力が出ると考えて3本目に置きました。</p><p>企画を立てる途中で、注意が要る点も3つ見つかりました。2本目は、速度やメモリの数字を必ず GPU の型番(NVIDIA A40)と組で言う。4本目は「安全装置を外した実験」の話が入るので、実験の条件(速さ、周りに人がいたか、止める手段)を言わないと、安全を軽く見ているように聞こえる。5本目は自分の実測がない回なので、会社の自己申告の数字は「会社の発表では」と言い、最後に自分の机の上で同じ判断をした場面を1つ入れる。</p><p></p><h2 id=\"index__9qJii4D\">ナレーション・BGM・効果音は、2026年10月11日時点の公式の条件で比べた</h2><p>音声は映像と別に作る前提なので、ナレーションの声、BGM、効果音の候補を、それぞれの公式ページの利用条件で確かめました。料金や条件は変わるので、表は2026年10月11日に確認した時点のものです。</p><p>ナレーションの候補は3つでした。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>方法</p></th><th colspan=\"1\" rowspan=\"1\"><p>条件(確認した時点)</p></th><th colspan=\"1\" rowspan=\"1\"><p>経営者向けの動画に合うか</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>本人の声で録る</p></td><td colspan=\"1\" rowspan=\"1\"><p>字幕ファイルに1文ごとの時間が入っているので、それに合わせて録れば映像はそのまま使える</p></td><td colspan=\"1\" rowspan=\"1\"><p>仕事の相談は人を見て決まるので、最初はこれを勧めた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ElevenLabs で声を複製する</p></td><td colspan=\"1\" rowspan=\"1\"><p>Creator プランは月11ドルの表示(初月の割引の表記あり)。商用ライセンス付きで、本格的な声の複製(Professional Voice Cloning)が1つ作れる</p></td><td colspan=\"1\" rowspan=\"1\"><p>録る時間が取れない回の代わりになる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>VOICEVOX</p></td><td colspan=\"1\" rowspan=\"1\"><p>商用・非商用を問わず無料。VOICEVOX を使ったことが分かるクレジットの表記が必要で、キャラクターごとの規約にも従う</p></td><td colspan=\"1\" rowspan=\"1\"><p>キャラクターの声は娯楽系の解説動画の印象が強く、相談先として見てもらいにくい</p></td></tr></tbody></table></div><p>BGM と効果音は次の候補です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>用途</p></th><th colspan=\"1\" rowspan=\"1\"><p>サービス</p></th><th colspan=\"1\" rowspan=\"1\"><p>条件(確認した時点)</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM・効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>YouTube オーディオライブラリ</p></td><td colspan=\"1\" rowspan=\"1\"><p>収益化している動画で使え、権利者からの著作権の申し立て(Content ID)が来ない。YouTube が著作権上安全と認めているのはここの音源だけ。Creative Commons の曲は概要欄にクレジットが要る</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM</p></td><td colspan=\"1\" rowspan=\"1\"><p>DOVA-SYNDROME</p></td><td colspan=\"1\" rowspan=\"1\"><p>無料、商用OK、クレジット不要。作曲者ごとに別の条件がある場合はそちらが優先。音楽が主役の動画やリミックスは禁止</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM・効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>Epidemic Sound</p></td><td colspan=\"1\" rowspan=\"1\"><p>有料。使用料の追加払いはない。広告に使うには Pro 以上のプランが要る</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>効果音ラボ</p></td><td colspan=\"1\" rowspan=\"1\"><p>無料、商用OK、YouTube の収益化OK、クレジット不要。効果音だけを流す動画は再配布として禁止</p></td></tr></tbody></table></div><p>効果音は、自分のロボットアームのサーボが回る音やグリッパーが閉じる音を録って、章の扉の音にする案も出しました。権利の心配がなく、このチャンネルにしかない音になります。入れる場所は、動画のルールで「打ち出し」と決めている場面(1本に6〜10場面)だけにしぼります。</p><p></p><h2 id=\"index_F7VUaN1Y\">ナレーションは、本人の声でも声の複製でもなく、ハコブくんに決めた</h2><p>声の候補を並べたあと、ナレーションをハコブくんにしたいという相談がありました。</p><blockquote><p>音声はハコブくんをナレーションキャラにしようと思いますが、自分の声の複製とどちらが良いですか?(自分の声は別に良いとは思わない。声が通らない)</p></blockquote><p>ハコブくんの方が良い、と答えました。理由は3つです。声の複製は元の声の性質をそのまま写すので、声が通らないのが不満なら、複製しても通らない声を毎回使うことになります。ハコブくんはすでに Pixels to Motors の4コママンガに出ているキャラクターで、開発ログ(day_log)の発信戦略にも「実機展開以降、ハコブくんをR2R2Rの概念解説役として再登場させる」と書いてありました。もう1つは、本人の実機の話を「エイイチの机の上では、こうなりました」と三人称で言えることです。動画のルールにある「自分の実機の話は、自分の条件の話として言う」が、言い方だけで自然に守れます。</p><p>ただ、目的が仕事の相談をもらうことなので、キャラクターの後ろで本人が見えなくなると効果が落ちます。そこで次の3つを必ず入れると決めました。</p><ul><li><p>自己紹介の場面で、本人の名前のテロップと実機の映像を出す</p></li><li><p>意見を話す節は「ここからはエイイチの見立てです」と言ってから入る(意見が誰のものかをはっきりさせる)</p></li><li><p>概要欄にプロフィールと連絡先を置き、「ナレーションは AI 音声です」と書く</p></li></ul><p>声は、ほかのチャンネルと同じ声(VOICEVOX のキャラクターの声など)を使わず、ハコブくん専用の声にします。「ハコブくん=有名なキャラクターの声のロボット」と聞かれないようにするためです。作り方の候補は2つで、ElevenLabs の Voice Design(文章で声の特徴を書くと新しい声を作る機能)か、声優に頼んで作ってもらうかです。Voice Design は、日本語での出来と、作った声を商用で使う条件が公式の説明に書かれていなかったので、試して確かめる必要があります。話し方は、高すぎない声でゆっくり丁寧に、経営者が13分聞いて疲れないことを基準にしました。</p><p></p><h2 id=\"index_O4m3dwLv\">ハコブくんは画面の左下に置き、生成した10秒の動画の1コマから体を切り抜いた</h2><p>置き方のイメージとして、参考にしている解説動画(前回、型を分析した3本のうち、会社への侵入と暗数の回)の画面が共有されました。画面の左下でサングラスの猫のキャラクターがマイクの前で話していて、字幕は猫の右側から始まっています。</p><blockquote><p>画面の左端にこのような形で話しているハコブくんを追加するイメージです。</p></blockquote><p>あわせて、病室にいるハコブくんの生成動画(10秒)が共有されました。最初に考えたのは、この動画をそのまま左下で繰り返し流す方法ですが、やめました。動画の中の口は声と関係なく開閉するので、話していない間も口が動きます。10秒の動画を13分繰り返すと、同じ動きの繰り返しだと気づかれます。背景の病室を毎フレーム切り抜くと、縁が揺れます。</p><p>そこで、体は動画の1コマを切り抜いた静止画にし、顔だけをコードで描く方法にしました。ハコブくんの顔はディスプレイなので、画面の中の目と口を描き替えれば話しているように見せられます。</p><p>動画は 848×1056 ピクセル・24fps・10.04秒でした。1秒に4枚ずつ、40枚のコマを取り出して並べ、全身と側面の緑のリングが見えている5枚目を選んでいます。背景の切り抜きには rembg(画像から人や物の背景を消すライブラリ)の isnet-general-use というモデルを使いました。</p><p></p><figure><img src=\"/media/s-1920x956_v-frms_webp_dd2ed6ca-70c6-4449-8572-3287b85ed201.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_g2Fv9MC5\">顔の目と口は、元の絵を消してコードで描き直した</h2><p>切り抜いた体のディスプレイを測り、元の目と口の位置と色を取りました(測定の全量は記事後半の実行ログにあります)。ディスプレイは横183×縦151ピクセル、元の口の色は #57B5F8 の水色です。元の目と口はディスプレイごと黒(#131311)で塗りつぶし、その上に同じ位置で目と口を描きます。</p><p>口はナレーションの音量に合わせて動かします。音声を1フレーム(1/30秒)ずつ区切って音量を測り、大きいほど口を大きく開く形にしました。最初の試作は、目が白い楕円、口が「閉じた笑顔/小さく開く/大きく開く」の3段階で、体は左を向いたままでした。</p><p>【画像】DAY331_03_first_prototype_faces.png</p><p>体の向きは、すぐに直しが入りました。</p><blockquote><p>ハコブくんは右向きがいいですね。</p></blockquote><p>左下に置くと、右を向けば画面の中央(図と字幕の側)を向くことになります。体の画像を左右反転して右向きにしました。</p><p>ここで1つ間違えています。その少し前に13種類の表情のイメージ(表情集)が共有されていて、表情集の顔は目も口も同じ薄い水色でした。右向きに直すときに、目と口を両方その水色(#BDE8FB)に変えてしまいました。</p><blockquote><p>色は目は白、口は水色のままでいいですよ</p></blockquote><p>表情集は形の参考で、色は元の動画のままでよかった、ということです。目を白、口を元の水色に戻しました。</p><p></p><figure><img src=\"/media/s-1920x1216_v-frms_webp_0e7f1609-60a9-4748-b723-9db00d3fde2a.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_Ufl5ndIQ\">口は音量で4段階に切り替え、1回に1段階ずつしか動かさない</h2><p>最初の口は、閉じた口からいきなり大きい口に変わることがあり、角も立っていました。</p><blockquote><p>口はもっと柔らかさを出すために角を丸くした方がいいですね(小さい口を途中に入れたりすると動きが出てより良いです)</p></blockquote><p>口の形を4段階にしました。閉じた口は両端が丸い太い弧、小さい口は上が少し広く下が丸い形、中と大は角を丸めた三日月形です。角は、4倍の大きさで形を描いてから、半径5〜6ピクセル相当の丸で角を削っています(モルフォロジーのオープン処理。小さな丸が入り込めない角だけが削れる)。</p><p>切り替え方も変えました。音量から決めた段階に向かって、1回に1段階ずつしか動かしません。閉じた口から大きい口へ行くときは、必ず小と中を通ります。</p><p>次の指摘は速さでした。</p><blockquote><p>口の動きや目の動きをもう気持ちだけゆっくりに動かしたほうがいいね(今は早すぎる。話し方も早すぎなのかもだけど)</p></blockquote><p>口の形を変えるのを、毎フレーム(1秒に30回)から2フレームごと(1秒に15回)にしました。仮の音声の話す速さも1.08倍から1.0倍に落としています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>最初</p></th><th colspan=\"1\" rowspan=\"1\"><p>最後</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>口の段階</p></td><td colspan=\"1\" rowspan=\"1\"><p>3段階(閉じる・小・大)</p></td><td colspan=\"1\" rowspan=\"1\"><p>4段階(閉じる・小・中・大)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>口の切り替え</p></td><td colspan=\"1\" rowspan=\"1\"><p>毎フレーム、段階を飛ばしてよい</p></td><td colspan=\"1\" rowspan=\"1\"><p>2フレームごと、1回に1段階</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>口の角</p></td><td colspan=\"1\" rowspan=\"1\"><p>尖っている</p></td><td colspan=\"1\" rowspan=\"1\"><p>半径5〜6ピクセル相当で丸める</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>仮の音声の速さ</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.08倍</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.0倍</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>問いかけの文のあとの口</p></td><td colspan=\"1\" rowspan=\"1\"><p>「o」の口</p></td><td colspan=\"1\" rowspan=\"1\"><p>小さく開いた口のまま止める</p></td></tr></tbody></table></div><p></p><h2 id=\"index_V8JsKfRO\">目は2.8秒の周期で上下し、口より0.1秒遅れて上がる</h2><blockquote><p>目が少し上下しているように描写すると表情がイキイキして見えます。</p></blockquote><p>目に3つの動きを足しました。1つ目は、ゆっくりした上下の揺れです。最初は1.9秒の周期でしたが、速すぎるという指摘のあと2.8秒に延ばしました。2つ目は、口が大きく開くほど目が少し上がる動きです。口と同時に動くと機械的に見えるので、口より3フレーム(0.1秒)遅れて上がるようにしています。3つ目は、文の頭で目が0.3秒だけ小さく跳ねる動きです。</p><p>まばたきも、最初は3.2秒ごとの一定間隔でしたが、2〜4.5秒の間でばらつかせ、文を言い終わった直後にも1回入れました。</p><div class=\"embed\"><iframe src=\"/embeds/explainer-video-character-narration-QIb2qI10.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p></p><h2 id=\"index_W02dAGsw\">側面のリングは、元の絵を消して楕円を描き直し、顔と別の周期で光らせた</h2><p>側面の緑のリングは、最初は元の絵の画素を明るくしていただけでした。</p><blockquote><p>側面の光っている部分をもっと丁寧に描写(光方が汚く見える)</p></blockquote><p>元の絵のリングは、生成動画を縮小した画像なので画素が粗く、明るくすると粗さが目立ちます。リングの画素に楕円を当てはめて位置と大きさを測り、元のリングはまわりの色で塗りつぶして消しました(OpenCV の inpaint。消したい範囲を、まわりの画素から推測した色で埋める処理)。その上に、4倍の大きさで描いた楕円を縮めて置き、ぼかした光を重ねています。光は体の外には出しません。</p><p></p><figure><img src=\"/media/s-693x876_v-fs_webp_42809200-9e99-483a-85d0-4093607ecd3e.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>次の指摘は、光と表情が一緒に動いていることでした。</p><blockquote><p>側面の光と表情の動きが同じタイミングで繰り返しているので、ずらしたり、合わせたりして躍動感(生命感)を出すと良さそう</p></blockquote><p>普段はずらし、要所だけそろえる動きにしました。リングは、声に0.2秒遅れて明るくなり、話し終わったあとはゆっくり暗くなります(つくときは速く、消えるときは遅い)。話していない間も、息をするように少し明滅します。この明滅の周期は3.7秒で、目の揺れ(2.8秒)と周期を変えてあるので、目とリングが同じ組み合わせで繰り返しません。そろえるのは、文の頭と、口を大きく開いた瞬間だけです。</p><blockquote><p>側面の光は強く光時と、弱く光ときを混ぜ込むと良さそう(ランダム性があるといい)</p></blockquote><p>ばらつきを4か所に入れました。</p><ul><li><p>声への反応の強さを、文ごとに変える(強い文と弱い文が混ざる)</p></li><li><p>口を大きく開いた瞬間の光り方を毎回変え、3回に1回ほどは光らない</p></li><li><p>息をするような明滅の強さを、ゆっくり不規則に変える</p></li><li><p>話していない間も、ときどき弱くまたたく</p></li></ul><p>ばらつきは乱数の種を7に固定して作っているので、同じスクリプトを何度動かしても同じ光り方になります。</p><p></p><h2 id=\"index_tXF5W1d5\">目はカプセル形にし、顔はディスプレイの辺に直線を当てはめて傾けた</h2><p>目の形は、元の動画の顔に合わせる指示がありました。</p><blockquote><p>目のデザインはこれに合わせて欲しい。</p></blockquote><p>元の動画の目は、楕円ではなく、上下の端が半円になった縦長のカプセル形で、まわりに白い光がにじんでいます。カプセル形に変え、光のにじみを足しました。にじみはディスプレイの中だけに収めています。</p><p>続けて、傾きの指摘がありました。</p><blockquote><p>目は顔のディスプレイに合わせて少しだけ斜めにすると良さそう</p></blockquote><p>ハコブくんは斜め前から見た構図なので、ディスプレイも少し傾いて見えます。まっすぐ描いた顔を、ディスプレイの四隅に合わせて射影変換(四角形を別の四角形にはめ込む変形)しました。最初は、ディスプレイの輪郭を4つの角の点に近似して四隅を取りましたが、合いませんでした。</p><blockquote><p>ちょっと目と口の描写が顔のディスプレいと合ってないね。微妙な差だけど。</p></blockquote><p>ディスプレイは角が丸いので、角の点を直接取ると、丸みに引っぱられて位置がずれます。最初の四隅だと上の辺まで傾いていて、顔が回って見えていました。上下左右の辺の、まっすぐな中央4割の点にそれぞれ直線を当てはめ、直線どうしの交点を角にしました。測り直すと、上の辺は0.18度でほぼ水平、左右の辺は約6度(5.97度と5.67度)、下の辺は2.16度傾いていました。</p><p></p><figure><img src=\"/media/s-840x876_v-fs_webp_6637392f-3158-4208-b51c-0fa6fd52d0e6.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>目と口の間隔も2回、ごく少しだけ直しています。</p><blockquote><p>ほんの少しだけだけど、目と口が近い方がデザインが近くなるね(ごくごく少しだけ)</p></blockquote><blockquote><p>目と口の位置がほんの少しだけ上に(本当に少しだけ)</p></blockquote><p>1回目は、目を2ピクセル下げ、口を1.5ピクセル上げて、間を3.5ピクセル詰めました。2回目は、間隔を保ったまま両方を1.5ピクセル上げています(どちらも元の画像の大きさでのピクセル数で、動画上ではその約8割の動きです)。スクリプトでは <code>EYE_SHIFT = 0.5</code> と <code>MOUTH_SHIFT = -3.0</code> の2つの数字で調整できます。</p><p></p><h2 id=\"index_fNoBvJtt\">頭の上は、凸包で埋めると奥の段差が消えて枕まで塗っていた</h2><p>頭の上は、3回直しています。元の動画では、ハコブくんの白い頭が後ろの白い枕と重なっていて、切り抜くと頭の上が薄く欠けます。</p><blockquote><p>あとサンプルの頭の上が切れてしまっているのを修正してください。</p></blockquote><p>1回目は、欠けた部分を含めた頭の輪郭の凸包(輪郭の外側に輪ゴムをかけたような、へこみのない形)で埋めました。</p><blockquote><p>頭の描写をもっと綺麗に処理して(元のデザイン見直してください)</p></blockquote><p>2回目は、凸包の角を大きく丸めてなめらかにしました。</p><blockquote><p>まだ頭の処理が汚いです。</p></blockquote><p>ここで、欠けた部分を下の色から塗り延ばす案を出しましたが、実行する前に止められました。</p><blockquote><p>元の動画があるから見直してください!</p></blockquote><p>元の動画の40枚のコマを並べ、そのうち8枚で頭の上を拡大すると、ハコブくんの頭は、画面のすぐ上の縁が低く、奥側がもう一段持ち上がった形をしていました。凸包は、この段差を直線で結んでしまいます。結んだ内側には後ろの枕が入っていたので、枕まで頭として塗り、形が崩れて汚く見えていました。</p><p>3回目は、凸包をやめました。切り抜きがはっきり頭と判断した範囲(透明度を表すアルファ値が60以上)をそのまま輪郭に使い、縁のギザギザだけを、4倍に拡大してぼかしてから切り直す方法でなめらかにしています。輪郭の内側は頭そのものなので、色は元のコマの画素で戻しました。縁の半透明の画素も元の色で戻したので、2回目まで出ていた暗い点線も消えています。</p><p></p><figure><img src=\"/media/s-882x1224_v-fms_webp_7f743859-c942-49dc-afff-0603e1d0848a.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_boeVOUIR\">ハコブくんの区画に字幕や図が重なったら、書き出す前に止める</h2><p>左下にキャラクターを置くと、字幕や図とぶつかります。試作を作る前に、置き方の決め事が必要だという話になりました。</p><blockquote><p>左端の顔の部分にテキストがかからないように設定しておくことが必要ですね。</p></blockquote><p>ハコブくんの「区画」を決め、そこには何も重ねないことにしました。区画は、実際に置いたハコブくんの体の範囲に余白20ピクセルを足した四角です。動画のルールで決めていた画面の作りとは、次の点が変わります。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>もとの決まり</p></th><th colspan=\"1\" rowspan=\"1\"><p>ハコブくんを置いたあと</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの区画</p></td><td colspan=\"1\" rowspan=\"1\"><p>なし</p></td><td colspan=\"1\" rowspan=\"1\"><p>左下の x=0〜380、y=746〜1080(上半身版)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>字幕の位置</p></td><td colspan=\"1\" rowspan=\"1\"><p>画面の中央、1行31字まで</p></td><td colspan=\"1\" rowspan=\"1\"><p>区画の右側(x=400〜1780)の中央、1行26字まで</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>出典の位置</p></td><td colspan=\"1\" rowspan=\"1\"><p>左端から140ピクセル</p></td><td colspan=\"1\" rowspan=\"1\"><p>左端から420ピクセル</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>図の範囲</p></td><td colspan=\"1\" rowspan=\"1\"><p>左右140ピクセルの余白</p></td><td colspan=\"1\" rowspan=\"1\"><p>左右140ピクセルのまま。区画に重なる場面は自動で止める</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ロゴの位置</p></td><td colspan=\"1\" rowspan=\"1\"><p>右から140ピクセル、上から46ピクセル</p></td><td colspan=\"1\" rowspan=\"1\"><p>右から56ピクセル、上から44ピクセル</p></td></tr></tbody></table></div><p></p><figure><img src=\"/media/s-1024x648_v-fs_webp_79281675-7ed6-4030-9539-7b81072bb026.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>ロゴは、途中まで試作が「Pixels to Motors」の文字を自分で描いていました。</p><blockquote><p>それと動画のデザインルールを確認して欲しい(どこかにデータがある。ロゴも修正)</p></blockquote><p>iCloud の制作ベースのフォルダを開くと、本番は scenes.py(場面ごとの図の HTML と CSS)、render.py(字幕の画像)、logo_dark.png(ロゴの画像)で画面を作っていました。試作が自前で描いていた部分は、章のタグの書き方や、大きい数字の単位の色も本番と違っていたので、図と字幕は本番の scenes.py をそのまま読み込んで描く形に作り直しています。そのあと、ロゴの位置に指摘がありました。</p><blockquote><p>右上のロゴはもっと端に寄せたほうがいいね。パディング取りすぎ。</p></blockquote><p>重なりの確かめ方は、目で見るのをやめ、書き出しの前の自動の確認にしました。図と字幕はブラウザ(Playwright で動かす Chromium)で描いているので、描いたあとに、見出し・数字・出典・ロゴ・字幕の1行ずつの位置を、ブラウザから実際の四角として取り出します(getBoundingClientRect)。その四角がハコブくんの区画と1ピクセルでも重なったら、どの要素がどこで重なったかを出して、書き出しを止めます。</p><p>図の範囲をどうするかは、2つの案から選んでもらいました。図の左端を420ピクセルに寄せたままにするか、本番どおり140ピクセルにして場面ごとの自動の確認に任せるかです。</p><blockquote><p>場面ごとに自動の重なり確認に任せて広げる</p></blockquote><p>上半身版では、12個の要素すべてが区画に重なりませんでした。同じ場面を全身版で書き出すと、区画が高い位置(y=580)から始まるので、「起動時に確保された量の 約28%」の見出しが重なり、書き出しが止まりました。要素の四角で判定しているので、文字が短くても段落の幅いっぱいが判定の対象になります。止まった場面は、図を区画より上に収めるか、左端を420ピクセルに寄せて作り直します。</p><h2 id=\"index_ZOcuqpzA\">全身版より上半身版の方が、図を読む邪魔をしなかった</h2><p>側面のリングを直すときに、全身版と上半身版の2つを作るように言われ、両方を書き出しました。全身版は高さ460ピクセルで体全体を、上半身版は幅340ピクセルで引き出しの取っ手の下あたりまでを置き、画面の下端に接しています。</p><blockquote><p>上半身のデザインがいいですね。全身は少し邪魔しているように見えます。</p></blockquote><p>全身版は、体の下半分(引き出しや車輪)が図と字幕の高さまで伸びていて、視線がそちらに引っぱられます。上半身版は、顔と側面のリングだけが画面の端から出ているので、図を読む邪魔をしません。上半身版を基本にしました。</p><p></p><figure><img src=\"/media/s-811x1056_v-fs_webp_f590a47e-2382-46cb-ad00-ad8d63bd22a9.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>最後の画面はこの形です。</p><p></p><figure><img src=\"/media/s-1920x1080_v-frms_webp_4418024c-4b1e-44f2-a658-4ee6646c54bb.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_H1Z4xux4\">決めたことは video_rules.md に3-1節と4-7節として書き足した</h2><p>ここまで決めたことは、動画制作のルール(video_rules.md)に書き足しました。ルールの文書はプロジェクトの文書と、iCloud の制作ベースのフォルダに同じ内容の写しがあるので、両方を同じ内容に更新しています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>節</p></th><th colspan=\"1\" rowspan=\"1\"><p>書き足したこと</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3-1(新設)</p></td><td colspan=\"1\" rowspan=\"1\"><p>ナレーションはハコブくんが話す。本人を見せる3つの決まり。概要欄に AI 音声と書く。ほかのチャンネルと同じ声を使わない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3-2</p></td><td colspan=\"1\" rowspan=\"1\"><p>仮の音声の速さを1.08倍から1.0倍に下げた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4-1</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの区画(左下・上半身・右向き)。図の範囲は140ピクセルのまま、場面ごとの自動の確認に任せる。ロゴの位置</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4-2・4-5</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの体と顔の色(目の白・口の水色・リングの緑)は、「オレンジと青以外の色を足さない」の対象外。影は付けない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4-6</p></td><td colspan=\"1\" rowspan=\"1\"><p>出典の左端を420ピクセルに移す</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4-7(新設)</p></td><td colspan=\"1\" rowspan=\"1\"><p>体の作り方(頭の上の直し方を含む)、顔の描き方、口・目・まばたき・リングの動きの数値、表情の付け方と使い分けの目安</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>字幕は1行26字まで、区画の右側の中央にそろえる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>台本の一人称「僕」を、本人の名前の三人称に言い換える。自己紹介の固定の文言をハコブくんが話す形にする</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>8</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの声、体の画像、本番のスクリプトへの組み込み、表情の使い分けを未決として追加</p></td></tr></tbody></table></div><p>表情は、目と頬を1文ごとに台本で指定し、口は話している間はどの表情でも音量で動かす、という決まりにしました。表情集の「_」の口のまま話すと、声と口が合わなくなるからです。使い分けの目安は次の表で、経営者向けの動画なので1本で使う表情は6つ前後にとどめます。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>表情</p></th><th colspan=\"1\" rowspan=\"1\"><p>使う場面</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>丸い目+開いた口</p></td><td colspan=\"1\" rowspan=\"1\"><p>基本。説明の文はほぼこれ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>丸い目+小さな笑み</p></td><td colspan=\"1\" rowspan=\"1\"><p>間、聞き手に戻るところ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>驚いた目+「o」の口</p></td><td colspan=\"1\" rowspan=\"1\"><p>動画の冒頭の問い、意外な数字が出るところ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ジト目</p></td><td colspan=\"1\" rowspan=\"1\"><p>「本当にそうでしょうか」と反論を出すところ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>目を閉じる</p></td><td colspan=\"1\" rowspan=\"1\"><p>「まとめると」、考えている間</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>困り眉</p></td><td colspan=\"1\" rowspan=\"1\"><p>失敗や限界の話</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>>▽<</p></td><td colspan=\"1\" rowspan=\"1\"><p>結論が言えたところ、うまくいった実験</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>「III_III」</p></td><td colspan=\"1\" rowspan=\"1\"><p>計算中・データを読み込んでいる場面</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ウインク、キラキラ目、照れ、>_<、目が泳ぐ</p></td><td colspan=\"1\" rowspan=\"1\"><p>アウトロや軽い一言だけ</p></td></tr></tbody></table></div><p>自己紹介の固定の文言は、もとの一人称の文をハコブくんが話す形に言い換え、「この番組は、本業のデザイナーをしながらロボティクスの研究と開発をしているエイイチが、自宅の机の上でロボットアームを動かした記録をもとにしています。」としました。この文言でよいかは、本人の確認待ちです。</p><p>ファイルの置き場所は次のとおりです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>ファイル</p></th><th colspan=\"1\" rowspan=\"1\"><p>置き場所</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>video_rules.md</p></td><td colspan=\"1\" rowspan=\"1\"><p>iCloud <code>PixelstoMotors/デジタルツイン学習R2R2R/動画/_制作ベース/video_rules.md</code> と、プロジェクトの文書 <code>claude/video_rules.md</code></p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>試作のスクリプトと入力の画像</p></td><td colspan=\"1\" rowspan=\"1\"><p>iCloud <code>動画/_制作ベース/hakobu_試作/</code>(make_demo.py、render_static.py、measure_face.py、cut_raw.png、full/g_05.png、元の生成動画)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>試作の動画(mp4)</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>/Volumes/KIOXIA/r2r2r-data/videos/hakobu_試作/</code></p></td></tr></tbody></table></div><p></p><h2 id=\"index_OMOijwC5\">まだ決まっていないのは、ハコブくんの声・体の画像・本番への組み込み・表情の4つ</h2><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>いまの状態</p></th><th colspan=\"1\" rowspan=\"1\"><p>決めること</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの声</p></td><td colspan=\"1\" rowspan=\"1\"><p>Open JTalk の仮の男声</p></td><td colspan=\"1\" rowspan=\"1\"><p>ElevenLabs の Voice Design で作るか、声優に頼んで作るか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>体の画像</p></td><td colspan=\"1\" rowspan=\"1\"><p>生成動画の1コマを切り抜き、頭の上とリングを直して使っている</p></td><td colspan=\"1\" rowspan=\"1\"><p>同じ構図で、無地の背景・リングなしの画像を生成し直すか。生成し直せば、頭の上とリングの直しが要らなくなる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>本番のスクリプトへの組み込み</p></td><td colspan=\"1\" rowspan=\"1\"><p>試作のスクリプトで、動きと配置を確かめた段階</p></td><td colspan=\"1\" rowspan=\"1\"><p>render.py と assemble.py に組み込む。台本(narration.py)に「表情」の欄を足す。表情集の顔をコードで描く。scenes.py のロゴの位置を直す(いまは試作の中で上書きしているだけ)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>表情の使い分け</p></td><td colspan=\"1\" rowspan=\"1\"><p>目安の表だけ</p></td><td colspan=\"1\" rowspan=\"1\"><p>1本作って、使う表情と場面を確定する</p></td></tr></tbody></table></div><p>試作は体を描いてから左右反転しているので、ウインクのように左右で形が違う表情は、このままだと左右が逆になります。本番では、体だけを先に反転し、顔は向きどおりに描く順番にします。</p><p>この4項目のうち3つは、この記事の作業のあとで決まりました。本番への組み込みは hakobu.py として済み、声は ElevenLabs の Voice Design を1か月試すことに、体の画像はいまのまま使うことになっています。字幕の位置も、そのあと画面の中央にそろえる形に変わりました。経緯は video_rules.md の更新履歴にあります。</p><p></p><h2 id=\"index_AtrH00k0\">試作スクリプトの全文</h2><p>使ったもの: Python 3.13.16、opencv-python 5.0.0、Pillow 12.3.0、numpy 2.5.3、pyopenjtalk 0.4.1、rembg 2.0.85(onnxruntime 1.29.0)、playwright 1.56.0(Chromium)、ffmpeg 6.1.1。フォントは Noto Sans CJK JP。</p><p>試作は Claude の作業環境(クラウドの Linux)で動かしました。Mac で動かすときは、<code>hakobu_試作/</code> を作業フォルダにして実行します。render_static.py は、1つ上の <code>_制作ベース/</code> にある本番の scenes.py と logo_dark.png を読み込みます。</p><h3 id=\"index_q54toAvd\">準備のコマンド(コマの取り出しと、体の切り抜き)</h3><p>元の生成動画は、共有されたときのファイル名が文字化けしていたため、<code>hakobu_試作/source_hakobu_hospital.mp4</code> という名前で保存し直しています。</p><pre><code class=\"language-bash\"># 動画の情報を見る\nffprobe -v error -show_entries format=duration:stream=codec_type,width,height,r_frame_rate -of compact source_hakobu_hospital.mp4\n\n# 1秒に4枚ずつ、40枚のコマを取り出す\nmkdir -p full\nffmpeg -v error -y -i source_hakobu_hospital.mp4 -vf \"fps=4\" full/g_%02d.png\n\n# 40枚に番号を振って並べる(コマを選ぶため)\nffmpeg -v error -y -i full/g_%02d.png -vf \"scale=212:-1,drawtext=text='%{n}':x=5:y=5:fontsize=24:fontcolor=red,tile=10x4\" sheet.png\n\n# rembg と、関連するライブラリを入れる\npip install --break-system-packages pyopenjtalk rembg onnxruntime\n</code></pre><pre><code class=\"language-python\"># 5枚目のコマから背景を消す(cut_raw.png)\nfrom rembg import remove, new_session\nfrom PIL import Image\nim = Image.open('full/g_05.png').convert('RGB')\nout = remove(im, session=new_session('isnet-general-use'))\nout.save('cut_raw.png'); print(out.size, out.getbbox())\n</code></pre><h3 id=\"index_TKcoDqjv\">measure_face.py(ディスプレイ・目・口・リングを測る)</h3><pre><code class=\"language-python\">\"\"\"ハコブくんの顔のディスプレイを測る(make_demo.py に書いた座標の出どころ)\n入力: cut_raw.png(元の動画の5コマ目を rembg の isnet-general-use で切り抜いた画像)\n出力: ディスプレイ・目・口・リング・体の範囲、口の色、ディスプレイの四隅\n\"\"\"\nimport numpy as np, cv2\nfrom PIL import Image\n\nraw = np.array(Image.open(\"cut_raw.png\").convert(\"RGBA\"))\nrgb = raw[..., :3].astype(int)\na = raw[..., 3]\nlum = rgb.mean(2)\n\n# 1. ディスプレイ(黒い画面):顔のあたりで一番大きい暗い領域\ny0, y1, x0, x1 = 330, 520, 300, 530\ndark = np.zeros(lum.shape, np.uint8)\ndark[y0:y1, x0:x1] = (lum[y0:y1, x0:x1] < 75)\nn, lab, st, _ = cv2.connectedComponentsWithStats(dark)\nk = 1 + np.argmax(st[1:, 4])\nscr = (lab == k).astype(np.uint8)\nprint(\"ディスプレイの外接矩形 (x, y, 幅, 高さ):\", st[k][:4].tolist())\n\n# 2. 元の目(白)と口(水色)\nreg = rgb[y0:y1, x0:x1]\nrl = reg.mean(2)\nwhite = (rl > 200) & (reg.std(2) < 25)\nblue = (reg[..., 2] > 180) & (reg[..., 0] < 150)\nfor name, m in [(\"白\", white), (\"水色\", blue)]:\n n2, lab2, st2, _ = cv2.connectedComponentsWithStats(m.astype(np.uint8))\n for j in range(1, n2):\n if st2[j, 4] > 80:\n x, y, w, h, area = st2[j].tolist()\n print(f\"{name}の領域: x={x + x0} y={y + y0} 幅={w} 高さ={h} 面積={area}\")\nprint(\"口の色(中央値 RGB):\", np.median(reg[blue], 0).astype(int).tolist())\n\n# 3. 側面の緑のリングと、体全体の範囲\nring = (rgb[..., 1] > 150) & (rgb[..., 1] - rgb[..., 0] > 40) & (rgb[..., 1] - rgb[..., 2] > 20)\nys, xs = np.where(ring)\nprint(\"リングの範囲:\", xs.min(), ys.min(), xs.max(), ys.max(), \"画素数\", len(xs))\nys, xs = np.where(a > 128)\nprint(\"体の範囲:\", xs.min(), ys.min(), xs.max(), ys.max())\n\n# 4. 頭の上の切り抜きの濃さ(行ごとに、アルファが一定以上の画素の数)\nprint(\"頭の上の行ごとの画素数(アルファ>200 / >60 / >10):\")\nfor y in range(320, 360, 4):\n row = a[y, 300:650]\n print(f\" y={y}: {(row > 200).sum()} / {(row > 60).sum()} / {(row > 10).sum()}\")\n\n# 5. ディスプレイの四隅\n# 角が丸いので角の点は直接取らない。上下左右の辺の、まっすぐな中央4割の点に直線を当てはめ、交点を角にする\nc = max(cv2.findContours(scr, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[0], key=cv2.contourArea)\nc = c.reshape(-1, 2).astype(np.float32)\nbx, by, bw, bh = cv2.boundingRect(c.astype(np.int32))\ncx, cy = bx + bw / 2, by + bh / 2\nsides = {\n \"上\": c[(c[:, 1] < cy - bh * 0.3) & (np.abs(c[:, 0] - cx) < bw * 0.2)],\n \"下\": c[(c[:, 1] > cy + bh * 0.3) & (np.abs(c[:, 0] - cx) < bw * 0.2)],\n \"左\": c[(c[:, 0] < cx - bw * 0.3) & (np.abs(c[:, 1] - cy) < bh * 0.2)],\n \"右\": c[(c[:, 0] > cx + bw * 0.3) & (np.abs(c[:, 1] - cy) < bh * 0.2)],\n}\nlines = {}\nfor nm, p in sides.items():\n vx, vy, px, py = cv2.fitLine(p, cv2.DIST_L2, 0, 0.01, 0.01).ravel()\n lines[nm] = (np.array([px, py]), np.array([vx, vy]))\n ang = np.degrees(np.arctan2(vy, vx))\n if nm in (\"左\", \"右\"):\n ang = 90 - abs(ang) # 縦の辺は、垂直からの傾き\n print(f\"{nm}の辺: 点 {len(p)} 個、傾き {ang:.2f}°\")\n\n\ndef cross(s1, s2):\n (p1, d1), (p2, d2) = lines[s1], lines[s2]\n t = np.linalg.solve(np.array([d1, -d2]).T, p2 - p1)\n return p1 + t[0] * d1\n\n\nquad = [cross(\"上\", \"左\"), cross(\"上\", \"右\"), cross(\"下\", \"右\"), cross(\"下\", \"左\")]\nprint(\"ディスプレイの四隅(左上・右上・右下・左下):\", [[round(float(v), 1) for v in q] for q in quad])\n\n# 参考: 角の点をそのまま取った場合(最初に使って、傾きを読み違えた方法)\nhull = cv2.convexHull(c.astype(np.int32))\nfor eps in np.linspace(2, 30, 57):\n ap = cv2.approxPolyDP(hull, eps, True)\n if len(ap) == 4:\n print(f\"参考・角の点を直接取った四隅(eps={eps}):\", ap.reshape(-1, 2).tolist())\n break\n</code></pre><h3 id=\"index_nK_87CBf\">make_demo.py(体の加工・顔の描画・口と目とリングの動き・重なりの確認・書き出し)</h3><pre><code class=\"language-python\">\"\"\"ハコブくん ナレーション試作(13秒ほど)\n使い方: python3 make_demo.py upper (上半身・基本) / python3 make_demo.py full (全身)\n python3 make_demo.py upper --dump (口・目・まばたき・リングの時間の値だけを timing.json に書き出して終わる)\n入力: cut_raw.png(元の動画の5コマ目を rembg で切り抜いた画像)、full/g_05.png(元の動画の5コマ目そのもの)\n必要なもの: pyopenjtalk(仮音声)、playwright と Chromium(図と字幕)、opencv-python、numpy、Pillow、ffmpeg\n- 動画の1コマから切り抜いた体に、画面の顔をコードで描く\n- 仮の音声(Open JTalk)の音量で口を4段階に切り替え、目・まばたき・側面のリングを動かす\n- ハコブくんの区画に文字が重なっていないかを自動で確認する\n\"\"\"\nimport subprocess, sys\nimport numpy as np, cv2, pyopenjtalk\nfrom PIL import Image, ImageDraw, ImageFont, ImageEnhance\n\nW, H, FPS = 1920, 1080, 30\nSR = 48000\nBG = (245, 245, 240)\nINK = (13, 13, 13)\nGRAY = (136, 136, 136)\nORANGE = (196, 71, 31)\nFONT_JP = \"/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc\"\nFONT_BLACK = \"/usr/share/fonts/opentype/noto/NotoSansCJK-Black.ttc\"\nFONT_REG = \"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc\"\n\n# ハコブくんの区画(ここには何も重ねない)\nZONE = (0, 600, 380, 1080)\nSUB_X0, SUB_X1 = 400, 1780\nFIG_X0 = 420\n\n\ndef font(path, size):\n return ImageFont.truetype(path, size, index=0) # index 0 = JP\n\n\n# ---------- 1. 体の切り抜きを整える ----------\nraw = np.array(Image.open(\"cut_raw.png\").convert(\"RGBA\"))\nalpha = raw[..., 3].copy()\nalpha[alpha < 60] = 0\nn, lab, st, _ = cv2.connectedComponentsWithStats((alpha > 0).astype(np.uint8))\nkeep = 1 + np.argmax(st[1:, 4])\nalpha[lab != keep] = 0\n# 頭の上:白い頭が白い枕と重なっていて、切り抜きが薄く欠けている。\n# 元の動画のコマを見ると、頭の上の輪郭は「画面の上の手前側の縁(低い)→ 奥の角で持ち上がる → 奥の縁(高い)→ 右の角で丸く下がる」形。\n# 凸包で埋めるとこの段差が直線になり、枕まで頭に含めてしまう(汚れの原因)。\n# そこで、切り抜きがはっきり頭と判断した範囲(アルファ60以上)をそのまま輪郭に使い、ギザギザだけをなめらかにする。\na_soft = np.array(Image.open(\"cut_raw.png\"))[..., 3]\nHEAD_Y1 = 425 # 肩の段差より上だけを対象にする\nhead = (a_soft > 60).astype(np.uint8)\nhead[HEAD_Y1:] = 0\nhead[:, :300] = 0\nn_h, lab_h, st_h, _ = cv2.connectedComponentsWithStats(head)\nhead = (lab_h == 1 + np.argmax(st_h[1:, 4])).astype(np.uint8)\ncnts, _ = cv2.findContours(head, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)\nhead = np.zeros_like(head)\ncv2.drawContours(head, cnts, -1, 1, -1) # 穴を埋める\n# 4倍に拡大 → ぼかして0.5で切り直す(ギザギザを曲線に) → 小さく角を丸める → 元の大きさへ\n_S = 4\n_big = cv2.resize(head.astype(np.float32) * 255, None, fx=_S, fy=_S, interpolation=cv2.INTER_LINEAR)\n_pad = 20 * _S\n_bigp = cv2.copyMakeBorder(_big, 0, _pad, 0, 0, cv2.BORDER_REPLICATE) # 下の切れ目が丸まらないように下へ延ばす\n_bigp = cv2.GaussianBlur(_bigp, (0, 0), 2.5 * _S)\n_bigp = ((_bigp > 127) * 255).astype(np.uint8)\n_k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * 4 * _S + 1, 2 * 4 * _S + 1))\n_bigp = cv2.morphologyEx(_bigp, cv2.MORPH_OPEN, _k)[: _big.shape[0]].astype(np.float32)\nhull_soft = cv2.resize(_bigp, (head.shape[1], head.shape[0]), interpolation=cv2.INTER_AREA) # 縁は自然な中間値\nhull_mask = (hull_soft > 127).astype(np.uint8)\nhull_soft[HEAD_Y1 - 3:] = 0\nalpha = np.maximum(alpha, hull_soft.astype(np.uint8))\nalpha[: HEAD_Y1 - 20] = hull_soft[: HEAD_Y1 - 20].astype(np.uint8) # 頭の上はこの輪郭だけで描く\n# 輪郭の内側は頭そのものなので、色は元のコマの画素をそのまま使う(切り抜きで暗くなった部分を戻す)\norig_rgb = np.array(Image.open(\"full/g_05.png\").convert(\"RGB\"))\nrestore = (hull_soft > 0) & (a_soft < 245) # 縁の半透明の画素も含める(暗い点線が出ないように)\nrestore[HEAD_Y1:] = False\nraw[restore, :3] = orig_rgb[restore]\nraw[..., 3] = alpha\n\n# 画面(黒いディスプレイ)の領域を見つけて、元の目と口を消す\nrgb = raw[..., :3].astype(int)\nlum = rgb.mean(2)\ny0, y1, x0, x1 = 330, 520, 300, 530\ndark = np.zeros(lum.shape, np.uint8)\ndark[y0:y1, x0:x1] = (lum[y0:y1, x0:x1] < 75)\nn, lab, st, _ = cv2.connectedComponentsWithStats(dark)\nk = 1 + np.argmax(st[1:, 4])\nscr = (lab == k).astype(np.uint8)\ncnts, _ = cv2.findContours(scr, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)\nscreen_mask = np.zeros_like(scr)\ncv2.drawContours(screen_mask, cnts, -1, 1, -1)\nscreen_mask = cv2.erode(screen_mask, np.ones((3, 3), np.uint8), 1)\nSCREEN_COLOR = (19, 19, 17)\nraw[screen_mask == 1, :3] = SCREEN_COLOR\n\n# 横の緑のリング\n# 元のリングは画素が粗いので、一度消して、楕円をコードで描き直す\ng = raw[..., :3].astype(int)\nring_core = (g[..., 1] > 150) & (g[..., 1] - g[..., 0] > 40) & (g[..., 1] - g[..., 2] > 20)\nring_core &= ~(screen_mask.astype(bool))\nys, xs = np.where(ring_core)\nRING = cv2.fitEllipse(np.stack([xs, ys], 1).astype(np.float32)) # ((cx,cy),(長径,短径),角度)\n(rcx, rcy), (rw, rh), rang = RING\nperim = np.pi * (3 * (rw + rh) / 2 - np.sqrt((3 * rw / 2 + rh / 2) * (rw / 2 + 3 * rh / 2)))\nRING_T = max(3.5, min(7.0, ring_core.sum() / perim)) # リングの太さ(px)\n# 緑がにじんでいる範囲ごと塗り直して消す\nhalo = ((g[..., 1] - g[..., 0] > 15) & (g[..., 1] - g[..., 2] > 5)) | ring_core\nhalo = cv2.dilate(halo.astype(np.uint8), np.ones((7, 7), np.uint8), 1)\nhalo[: int(rcy - rh / 2 - 15)] = 0\nhalo[int(rcy + rh / 2 + 15):] = 0\nhalo[:, : int(rcx - rw / 2 - 15)] = 0\nhalo[:, int(rcx + rw / 2 + 15):] = 0\nbgr = cv2.cvtColor(raw[..., :3].copy(), cv2.COLOR_RGB2BGR)\nbgr = cv2.inpaint(bgr, halo, 7, cv2.INPAINT_TELEA)\nraw[..., :3] = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)\n\n\ndef draw_ring(img, on):\n \"\"\"リングを4倍の解像度で描き、にじむ光を重ねる\"\"\"\n m = 30\n bx0, by0 = int(rcx - rw / 2 - m), int(rcy - rh / 2 - m)\n bw, bh = int(rw + 2 * m), int(rh + 2 * m)\n big = np.zeros((bh * SS, bw * SS), np.uint8)\n cv2.ellipse(big, ((int((rcx - bx0) * SS), int((rcy - by0) * SS)),\n (int(rw * SS), int(rh * SS)), rang),\n 255, int(RING_T * SS), cv2.LINE_AA)\n core = cv2.resize(big, (bw, bh), interpolation=cv2.INTER_AREA).astype(np.float32) / 255\n on = float(on) # 0(話していない)〜1(話している)\n glow = cv2.GaussianBlur(core, (0, 0), 3.5 + 1.5 * on)\n glow = np.clip(glow * (1.4 + 0.8 * on), 0, 1)\n region = np.array(img.crop((bx0, by0, bx0 + bw, by0 + bh))).astype(np.float32)\n a = region[..., 3:4] / 255\n rgb = region[..., :3]\n glow_col = np.array([70, 225, 130], np.float32)\n core_col = np.array([160, 240, 185], np.float32) * (1 - on) + np.array([215, 255, 228], np.float32) * on\n gk = (glow * (0.45 + 0.3 * on))[..., None] * a # 体の外には光を出さない\n rgb = rgb * (1 - gk) + glow_col * gk\n ck = core[..., None] * a\n rgb = rgb * (1 - ck) + core_col * ck\n region[..., :3] = rgb\n img.paste(Image.fromarray(region.astype(np.uint8)), (bx0, by0))\n\n\nbase_body = Image.fromarray(raw)\nbody_bbox = base_body.getbbox()\n\n# ---------- 2. 顔を描く(元の位置を実測した座標) ----------\nEYE_L = (362, 395, 384, 441)\nEYE_R = (446, 396, 469, 444)\nMOUTH = (378, 450, 446, 490)\nEYE_SHIFT = 0.5 # 目の上下位置(元画像の px。+で下)。顔全体を1.5px上げた\nMOUTH_SHIFT = -3.0 # 口の上下位置(目と口の間は 3.5px 詰めたまま)\nBLUE = (87, 181, 248) # 口の水色(元の動画の色 #57B5F8)\nWHITE = (246, 248, 252) # 目の白\nSS = 4 # 4倍で描いて縮める(縁をなめらかにする)\n\n\nFX0, FY0, FX1, FY1 = 320, 370, 520, 500 # 顔を描く範囲(元画像の座標)\n\n\ndef _pts(xs, ys):\n return np.stack([(np.asarray(xs) - FX0) * SS, (np.asarray(ys) - FY0) * SS], 1).astype(np.int32)\n\n\ndef mouth_mask(kind):\n \"\"\"口の形を4倍の解像度で作り、角を丸めて返す(0/255 の配列)\"\"\"\n w, h = (FX1 - FX0) * SS, (FY1 - FY0) * SS\n m = np.zeros((h, w), np.uint8)\n cx = (MOUTH[0] + MOUTH[2]) / 2\n top = MOUTH[1] + 2 + MOUTH_SHIFT\n t = np.linspace(0, 1, 60)\n if kind in (\"mid\", \"large\"):\n # 三日月形:上の辺はゆるく下がり、下の辺は深い弧\n hw, depth, sag, rr = (34, 38, 7, 6) if kind == \"large\" else (27, 25, 5, 5)\n xs = cx - hw + 2 * hw * t\n y_top = top + sag * (1 - (2 * t - 1) ** 2)\n y_bot = top + depth * np.sqrt(np.clip(1 - (2 * t - 1) ** 2, 0, 1))\n poly = np.concatenate([_pts(xs, y_top), _pts(xs[::-1], y_bot[::-1])])\n cv2.fillPoly(m, [poly], 255)\n elif kind == \"small\":\n # 小さく開いた口:上が少し広く、下が丸い\n hw, rr = 14, 6\n xs = cx - hw + 2 * hw * t\n y_top = top + 4 + 2 * (1 - (2 * t - 1) ** 2)\n ang = np.linspace(0, np.pi, 60)\n bx = cx + hw * np.cos(ang)\n by = top + 12 + 13 * np.sin(ang)\n poly = np.concatenate([_pts(xs, y_top), _pts(bx, by)])\n cv2.fillPoly(m, [poly], 255)\n elif kind == \"closed\":\n # 閉じた笑顔:太さのある弧、両端は丸い\n hw, sag, thick = 25, 9, 7\n for tt in np.linspace(0, 1, 200):\n x = cx - hw + 2 * hw * tt\n y = top + 7 + sag * (1 - (2 * tt - 1) ** 2)\n p = _pts([x], [y])[0]\n cv2.circle(m, (int(p[0]), int(p[1])), int(thick / 2 * SS), 255, -1)\n return m\n k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * rr * SS + 1, 2 * rr * SS + 1))\n return cv2.morphologyEx(m, cv2.MORPH_OPEN, k) # 角を丸める\n\n\nMOUTH_CACHE = {}\n\n# ディスプレイの四隅:画面の上下左右の辺のまっすぐな部分(角の丸みを除いた中央4割)に直線を当てはめ、\n# その交点を角とした(角の丸みに引っぱられないように)。上の辺はほぼ水平(0.2°)、左右の辺は約6°傾いている\nSCREEN_QUAD = np.float32([[336.8, 348.8], [510.2, 349.3], [495.1, 501.2], [321.5, 494.6]]) # 左上・右上・右下・左下\n_c = SCREEN_QUAD.mean(0)\n_w = (np.linalg.norm(SCREEN_QUAD[1] - SCREEN_QUAD[0]) + np.linalg.norm(SCREEN_QUAD[2] - SCREEN_QUAD[3])) / 2\n_h = (np.linalg.norm(SCREEN_QUAD[3] - SCREEN_QUAD[0]) + np.linalg.norm(SCREEN_QUAD[2] - SCREEN_QUAD[1])) / 2\nFLAT_RECT = np.float32([[_c[0] - _w / 2, _c[1] - _h / 2], [_c[0] + _w / 2, _c[1] - _h / 2],\n [_c[0] + _w / 2, _c[1] + _h / 2], [_c[0] - _w / 2, _c[1] + _h / 2]])\nTILT = 1.0 # 1.0 = ディスプレイと同じだけ傾ける。0.5 なら半分\n_quad = FLAT_RECT + TILT * (SCREEN_QUAD - FLAT_RECT)\n_to_layer = lambda p: (p - np.float32([FX0, FY0])) * SS\nH_FACE = cv2.getPerspectiveTransform(_to_layer(FLAT_RECT), _to_layer(_quad))\n\n\ndef warp_to_screen(la):\n h, w = la.shape[:2]\n a = la[..., 3:4].astype(np.float32) / 255\n pre = np.concatenate([la[..., :3].astype(np.float32) * a, a * 255], 2) # 縁が黒ずまないよう乗算済みで変形\n out = cv2.warpPerspective(pre, H_FACE, (w, h), flags=cv2.INTER_LINEAR)\n aa = out[..., 3:4] / 255\n rgb = np.where(aa > 1e-4, out[..., :3] / np.maximum(aa, 1e-4), 0)\n return np.concatenate([np.clip(rgb, 0, 255), out[..., 3:4]], 2).astype(np.uint8)\n\n\ndef draw_face(img, mouth, eyes_closed, eye_dy=0.0):\n fx0, fy0, fx1, fy1 = FX0, FY0, FX1, FY1\n layer = Image.new(\"RGBA\", ((fx1 - fx0) * SS, (fy1 - fy0) * SS), (0, 0, 0, 0))\n d = ImageDraw.Draw(layer)\n\n def r(b):\n return [(b[0] - fx0) * SS, (b[1] - fy0) * SS, (b[2] - fx0) * SS, (b[3] - fy0) * SS]\n\n for e in (EYE_L, EYE_R):\n e = (e[0], e[1] + eye_dy + EYE_SHIFT, e[2], e[3] + eye_dy + EYE_SHIFT) # 目だけ少し上下させる\n if eyes_closed:\n cy = (e[1] + e[3]) / 2\n d.rounded_rectangle(r((e[0] - 1, cy - 2.5, e[2] + 1, cy + 2.5)), radius=2.5 * SS, fill=WHITE)\n else:\n # 縦長のカプセル形(上下の端が半円)\n w = e[2] - e[0]\n d.rounded_rectangle(r(e), radius=w / 2 * SS, fill=WHITE)\n if mouth not in MOUTH_CACHE:\n MOUTH_CACHE[mouth] = mouth_mask(mouth)\n la = np.array(layer)\n mm = MOUTH_CACHE[mouth] > 0\n la[mm, :3] = BLUE\n la[mm, 3] = 255\n # 顔ごとディスプレイの傾きに合わせて斜めにする(まっすぐ描いた顔を、画面の四隅へ射影する)\n la = warp_to_screen(la)\n layer = Image.fromarray(la)\n layer = layer.resize((fx1 - fx0, fy1 - fy0), Image.LANCZOS)\n # 画面の外にはみ出さないよう、画面の形で切る\n m = Image.fromarray((screen_mask[fy0:fy1, fx0:fx1] * 255).astype(np.uint8))\n la = np.array(layer)\n inside = (np.array(m) > 0)\n la[..., 3] = (la[..., 3].astype(int) * inside).astype(np.uint8)\n # 目のまわりのにじむ光(画面の中だけ)\n eye_a = la[..., 3].astype(np.float32) / 255 * (la[..., 0] > 200)\n glow = cv2.GaussianBlur(eye_a, (0, 0), 2.6) * 0.75 * inside\n glow_layer = np.zeros_like(la)\n glow_layer[..., :3] = (205, 225, 255)\n glow_layer[..., 3] = np.clip(glow * 255, 0, 255).astype(np.uint8)\n img.alpha_composite(Image.fromarray(glow_layer), (fx0, fy0))\n img.alpha_composite(Image.fromarray(la), (fx0, fy0))\n\n\nMODE = sys.argv[1] if len(sys.argv) > 1 else \"upper\" # upper=上半身(基本) / full=全身\nif MODE == \"full\":\n CROP = body_bbox\n ROBOT_H = 460\n scale = ROBOT_H / (CROP[3] - CROP[1])\n ROBOT_W = round((CROP[2] - CROP[0]) * scale)\n ROBOT_Y = ZONE[3] - 20 - ROBOT_H\nelse:\n # 上半身:引き出しの取っ手の下あたりで切り、画面の下端に接して置く\n CROP = (body_bbox[0], body_bbox[1], body_bbox[2], 660)\n ROBOT_W = 340\n scale = ROBOT_W / (CROP[2] - CROP[0])\n ROBOT_H = round((CROP[3] - CROP[1]) * scale)\n ROBOT_Y = H - ROBOT_H\nROBOT_X = (ZONE[0] + ZONE[2]) // 2 - ROBOT_W // 2\n# ハコブくんの区画は、実際に置いた体の範囲+余白20pxで決める(上半身版は全身版より低い位置から始まる)\nZONE = (0, ROBOT_Y - 20, ROBOT_X + ROBOT_W + 20, H)\ncache = {}\n\n\ndef robot(mouth, eyes_closed, ring_on, eye_dy=0.0):\n key = (mouth, eyes_closed, ring_on, eye_dy)\n if key not in cache:\n img = base_body.copy()\n draw_ring(img, ring_on)\n draw_face(img, mouth, eyes_closed, eye_dy)\n img = img.crop(CROP).resize((ROBOT_W, ROBOT_H), Image.LANCZOS)\n img = img.transpose(Image.FLIP_LEFT_RIGHT) # 右向き(画面の中央を向く)\n cache[key] = img\n return cache[key]\n\n\n# ---------- 3. 仮の音声 ----------\nSCRIPT = [\n # (字幕, 読み上げ用, 後ろの間[秒], 間の顔)\n (\"ハコブくんです。\", \"ハコブくんです。\", 0.35, \"closed\"),\n (\"高いGPUを、買う必要はあるのでしょうか。\",\n \"たかいジーピーユーを、買う必要は、あるのでしょうか。\", 0.7, \"o\"),\n (\"エイイチが借りて測ると、\\n使ったメモリは約3割でした。\",\n \"エイイチが借りて測ると、使ったメモリは、約3割でした。\", 0.6, \"closed\"),\n]\n\naudio = []\ntimeline = [] # (開始フレーム, 終了フレーム, 字幕, 間の顔, 発話終了フレーム)\nt = 0\nfor sub, read, gap, gap_face in SCRIPT:\n x, sr = pyopenjtalk.tts(read, speed=1.0, half_tone=-1.0) # 1.08 → 1.0 に落とした\n assert sr == SR\n x = x / 32768.0\n seg = np.concatenate([x, np.zeros(int(gap * SR))])\n f_start = round(t * FPS)\n f_speech_end = round((t + len(x) / SR) * FPS)\n t += len(seg) / SR\n f_end = round(t * FPS)\n audio.append(seg)\n timeline.append((f_start, f_end, sub, gap_face, f_speech_end))\naudio = np.concatenate([np.zeros(int(0.3 * SR)), np.concatenate(audio)])\nlead = round(0.3 * FPS)\ntimeline = [(a + lead, b + lead, s, gf, e + lead) for a, b, s, gf, e in timeline]\ntotal_frames = timeline[-1][1] + round(0.3 * FPS)\naudio = np.concatenate([audio, np.zeros(max(0, int(total_frames / FPS * SR) - len(audio)))])\npeak = np.abs(audio).max()\naudio = audio / peak * 0.8\nimport wave\nwith wave.open(\"narration.wav\", \"wb\") as wf:\n wf.setnchannels(1); wf.setsampwidth(2); wf.setframerate(SR)\n wf.writeframes((audio * 32767).astype(np.int16).tobytes())\n\n# 音量 → 口の4段階(閉じる/小/中/大)\nMOUTHS = [\"closed\", \"small\", \"mid\", \"large\"]\nhop = SR // FPS\nrms = np.array([np.sqrt(np.mean(audio[i * hop:(i + 1) * hop] ** 2)) if i * hop < len(audio) else 0\n for i in range(total_frames)])\nref = np.percentile(rms[rms > 0.01], 90)\nlvl = rms / ref\nlvl_s = np.convolve(lvl, np.ones(3) / 3, mode=\"same\") # 音量を少しならす\ntarget = np.digitize(lvl_s, [0.15, 0.35, 0.65]) # 0..3\n# ちらつき防止: 1フレームだけの変化は前に合わせる\nfor i in range(1, total_frames - 1):\n if target[i] != target[i - 1] and target[i] != target[i + 1]:\n target[i] = target[i - 1]\n# 1フレームに1段階ずつしか変えない → 閉じる→大 の間に必ず小・中が入る\nstep = np.zeros(total_frames, int)\nMOUTH_HOLD = 2 # 口は2フレームごと(毎秒15回)にだけ変える\nfor i in range(total_frames):\n prev = step[i - 1] if i else 0\n if i % MOUTH_HOLD == 0:\n step[i] = prev + int(np.clip(target[i] - prev, -1, 1))\n else:\n step[i] = prev\nmouth_seq = [MOUTHS[s] for s in step]\n# 間の顔(問いかけのあとは小さく開いた口のまま止める)\nfor a, b, s, gf, e in timeline:\n for i in range(e + 2, b):\n if gf == \"o\":\n mouth_seq[i] = \"small\"\n# 目の上下:ゆっくりした揺れ + 口が開くほど少し上がる(元画像の px。0.25px 刻みにまとめる)\ntt = np.arange(total_frames) / FPS\nrng = np.random.default_rng(7)\nstarts = [a for a, b, s, gf, e in timeline]\nends = [e for a, b, s, gf, e in timeline]\n\n# ---- 目:ゆっくりした揺れ + 口に少し遅れて上がる + 文の頭で小さく跳ねる ----\nlift = np.convolve(step / 3.0, np.ones(11) / 11, mode=\"same\")\nlift = np.concatenate([np.zeros(3), lift[:-3]]) # 口より3フレーム(0.1秒)遅れて動く\nhop = np.zeros(total_frames)\nfor a in starts: # 文の頭:目がぴょこっと上がって戻る(0.3秒)\n n = np.arange(9)\n hop[a:a + 9] += np.sin(np.pi * n / 8)[: len(hop[a:a + 9])]\neye_dy = 1.6 * np.sin(2 * np.pi * tt / 2.8) - 2.6 * lift - 2.0 * hop\neye_dy = np.round(eye_dy * 4) / 4\n\n# ---- まばたき:間隔をばらつかせ、文の終わりの直後にも入れる ----\nblink = np.zeros(total_frames, bool)\nt_b = 1.2\nwhile t_b * FPS < total_frames:\n blink[int(t_b * FPS):int(t_b * FPS) + 4] = True\n t_b += rng.uniform(2.0, 4.5)\nfor e in ends:\n if not blink[max(0, e - FPS):e + 8].any():\n blink[e + 4:e + 8] = True\n\n# ---- 側面の光:顔とは別の周期で息をするように明滅し、声には少し遅れて追いかける ----\n# 息をするような明滅。強さそのものもゆっくり不規則に変わる\nnoise = np.convolve(rng.normal(0, 1, total_frames + 90), np.ones(90) / 90, mode=\"valid\")[:total_frames]\nnoise = (noise - noise.min()) / (noise.max() - noise.min() + 1e-9) # 0〜1 のゆっくりした揺らぎ\nbreath = (0.5 + 0.5 * np.sin(2 * np.pi * tt / 3.7 + 1.3)) * (0.4 + 0.9 * noise) # 目の揺れ(2.8秒)と周期をずらす\ntalk = (lvl_s > 0.15).astype(float)\nenv = np.zeros(total_frames)\nfor i in range(1, total_frames): # 立ち上がりは速く、消えるのはゆっくり\n k = 0.30 if talk[i] > env[i - 1] else 0.05\n env[i] = env[i - 1] + k * (talk[i] - env[i - 1])\nenv = np.concatenate([np.zeros(6), env[:-6]]) # 声より0.2秒遅れて光る\ngain = np.full(total_frames, 0.5)\nfor a, b, s_, gf, e in timeline: # 文ごとに、声への反応の強さを変える(強い文・弱い文が混ざる)\n gain[a:b] = rng.uniform(0.3, 0.75)\ngain = np.convolve(gain, np.ones(15) / 15, mode=\"same\")\npulse = np.zeros(total_frames)\n\n\ndef add_pulse(i, amp, tau):\n n = np.arange(min(int(tau * 3), total_frames - i))\n pulse[i:i + len(n)] = np.maximum(pulse[i:i + len(n)], amp * np.exp(-n / tau))\n\n\nlast = -999\nfor i in range(total_frames):\n # 文の頭と、大きく口を開いた瞬間:顔とそろえて光る。強さは毎回ばらつき、ときどき光らない\n if i in starts:\n add_pulse(i, rng.uniform(0.25, 0.45), 5); last = i\n elif step[i] == 3 and i - last > 15:\n if rng.random() < 0.7:\n add_pulse(i, rng.uniform(0.08, 0.4), rng.uniform(3, 7))\n last = i\n# 話していないときも、ときどき弱くまたたく\nt_tw = rng.uniform(1.0, 2.5)\nwhile t_tw * FPS < total_frames:\n add_pulse(int(t_tw * FPS), rng.uniform(0.06, 0.18), rng.uniform(4, 9))\n t_tw += rng.uniform(1.5, 4.0)\nring_lv = np.clip(0.10 + 0.2 * breath + gain * env + pulse, 0, 1)\nring_lv = np.round(ring_lv * 20) / 20\n\nif \"--dump\" in sys.argv: # 図解用に、時間の値だけを書き出して終わる\n import json\n json.dump({\"fps\": FPS, \"frames\": int(total_frames),\n \"voice\": np.round(np.clip(lvl_s, 0, 1.5), 3).tolist(),\n \"mouth\": [int(v) for v in step], \"eye_dy\": eye_dy.tolist(),\n \"blink\": [int(b) for b in blink], \"ring\": ring_lv.tolist(),\n \"sentences\": [[int(a), int(e)] for a, b, s, gf, e in timeline]},\n open(\"timing.json\", \"w\"))\n print(\"timing.json に書き出しました\")\n sys.exit(0)\n\n# ---------- 4. 画面の文字と図(本番と同じ HTML/CSS で描く) ----------\nimport json\nsubprocess.run([sys.executable, \"render_static.py\", json.dumps([s for a, b, s, gf, e in timeline], ensure_ascii=False)], check=True)\nstatic = Image.open(\"png/static.png\").convert(\"RGB\")\nsub_layers = {s: Image.open(f\"png/sub_{k}.png\").convert(\"RGBA\") for k, (a, b, s, gf, e) in enumerate(timeline)}\nboxes = json.load(open(\"png/boxes.json\"))\n\n# ---------- 5. 重なりの自動確認 ----------\ndef overlap(b1, b2):\n return not (b1[2] <= b2[0] or b2[2] <= b1[0] or b1[3] <= b2[1] or b2[3] <= b1[1])\n\nbad = [(nm, bb) for nm, bb in boxes if overlap(bb, ZONE)]\nprint(\"ハコブくんの区画:\", ZONE, \" 体の位置:\", (ROBOT_X, ROBOT_Y, ROBOT_X + ROBOT_W, ROBOT_Y + ROBOT_H))\nif bad:\n for nm, bb in bad:\n print(\" 重なり:\", nm, bb)\n sys.exit(\"区画に重なる要素があるため中止しました\")\nprint(f\"重なり確認: 文字・図・ロゴ {len(boxes)} 個、区画との重なり 0 件\")\n\n# ---------- 6. 書き出し ----------\ncmd = [\"ffmpeg\", \"-v\", \"error\", \"-y\", \"-f\", \"rawvideo\", \"-pix_fmt\", \"rgb24\", \"-s\", f\"{W}x{H}\",\n \"-r\", str(FPS), \"-i\", \"-\", \"-i\", \"narration.wav\", \"-c:v\", \"libx264\", \"-crf\", \"20\",\n \"-pix_fmt\", \"yuv420p\", \"-c:a\", \"aac\", \"-b:a\", \"192k\", \"-shortest\", f\"hakobu_demo_{MODE}.mp4\"]\np = subprocess.Popen(cmd, stdin=subprocess.PIPE)\ncur_sub = None\nfor i in range(total_frames):\n frame = static.copy().convert(\"RGBA\")\n for a, b, s, gf, e in timeline:\n if a <= i < b:\n frame.alpha_composite(sub_layers[s])\n frame.alpha_composite(robot(mouth_seq[i], bool(blink[i]), float(ring_lv[i]), float(eye_dy[i])), (ROBOT_X, ROBOT_Y))\n if i == round(total_frames * 0.42):\n frame.convert(\"RGB\").save(f\"hakobu_demo_{MODE}_still.png\")\n p.stdin.write(frame.convert(\"RGB\").tobytes())\np.stdin.close(); p.wait()\nprint(\"frames\", total_frames, \"seconds\", round(total_frames / FPS, 2))</code></pre><h3 id=\"index_BgCE00BU\">render_static.py(図と字幕を本番の scenes.py で描き、要素の位置を測る)</h3><pre><code class=\"language-python\">\"\"\"図と字幕を、本番(_制作ベース の scenes.py / render.py)と同じ HTML/CSS で描く。\nハコブくんを左下に置くために本番から変えた点:\n - 図の範囲(.wrap)は本番と同じ左端 140px のまま。ハコブくんの区画に重なる場面は、書き出し前の自動確認で止める\n - 出典(.src)の左端 140px → 420px\n - 字幕の中央 960px → 1090px、1行の最大幅 1640px → 1380px(1行26字まで)\n - ロゴ(.brand)を右端に寄せる:右 140px → 56px、上 46px → 44px\n各要素の位置は getBoundingClientRect で測り、png/boxes.json に書き出す(重なり確認用)。\n\"\"\"\nimport sys, os, json, html\n# 本番の scenes.py と logo_dark.png を読む。_制作ベース/hakobu_試作/ に置いたときは1つ上の _制作ベース を、\n# それ以外の場所では base/ に置いた写しを使う\n_here = os.path.dirname(os.path.abspath(__file__))\n_parent = os.path.dirname(_here)\nsys.path.insert(0, _parent if os.path.exists(os.path.join(_parent, \"scenes.py\")) else os.path.join(_here, \"base\"))\nimport scenes\nfrom playwright.sync_api import sync_playwright\n\nsubs = json.loads(sys.argv[1])\nos.makedirs(\"png\", exist_ok=True)\n\nHAKOBU_CSS = \"\"\"\n.src{left:420px !important}\n.brand{right:56px !important;top:44px !important}\n\"\"\"\nscenes.CSS = scenes.CSS + HAKOBU_CSS\n\nscene = scenes.page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">π0.5 をシミュレータで動かしたときの GPU メモリ(実使用の最大)</div>\n <div class=\"hero\">9,735<small>MiB</small></div>\n <div class=\"h2\" style=\"margin-top:36px\">起動時に確保された量の <span class=\"red\">約28%</span></div>\n</div>\n<div class=\"src\">出典:Pixels to Motors 実測(GPU:NVIDIA A40、π0.5-LIBERO)</div>\"\"\",\n tag=\"<b>2</b>買う前に借りて測る\")\n\nSUBCSS = \"\"\"*{margin:0;padding:0}html,body{width:1920px;height:1080px;background:transparent}\nsvg{position:absolute;left:0;top:0}\n.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}\"\"\"\nSUB_CX, SUB_MAXW = 1090, 1380\n\nMEASURE = \"\"\"() => [...document.querySelectorAll('.tag,.brand,.kicker,.hero,.h1,.h2,.h3,.src,.wrap *')]\n .filter(e => e.getBoundingClientRect().width > 0)\n .map(e => { const r = e.getBoundingClientRect();\n return [e.className || e.tagName, [r.left, r.top, r.right, r.bottom]]; })\"\"\"\n\nboxes = []\nwith sync_playwright() as p:\n b = p.chromium.launch()\n pg = b.new_page(viewport={\"width\": 1920, \"height\": 1080})\n pg.set_content(scene)\n pg.wait_for_timeout(100)\n pg.screenshot(path=\"png/static.png\")\n boxes += [[f\"図:{n}\", r] for n, r in pg.evaluate(MEASURE)]\n for k, s in enumerate(subs):\n lines = s.split(\"\\n\")\n assert all(len(ln) <= 26 for ln in lines), s\n svg_lines = \"\".join(f'<text x=\"{SUB_CX}\" text-anchor=\"middle\" class=\"t\">{html.escape(ln)}</text>' for ln in lines)\n pg.set_content(f\"\"\"<!doctype html><html><head><meta charset=\"utf-8\"><style>{SUBCSS}</style></head><body>\n<svg id=\"s\" width=\"1920\" height=\"1080\" viewBox=\"0 0 1920 1080\">{svg_lines}</svg></body></html>\"\"\")\n rects = pg.evaluate(f\"\"\"() => {{\n const ts=[...document.querySelectorAll('text.t')]; const lh=72; const n=ts.length;\n 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)); }}\n ts.forEach((t,k)=>{{ t.style.fontSize=fs+'px'; t.setAttribute('y', 1080-62-(n-1-k)*lh); }});\n return ts.map(t => {{ const r=t.getBoundingClientRect(); return [r.left-6, r.top-6, r.right+6, r.bottom+6]; }});\n }}\"\"\")\n boxes += [[f\"字幕{k}-{j}\", r] for j, r in enumerate(rects)]\n pg.screenshot(path=f\"png/sub_{k}.png\", omit_background=True)\n b.close()\njson.dump(boxes, open(\"png/boxes.json\", \"w\"), ensure_ascii=False)</code></pre><h3 id=\"index_Fr7ec4rQ\">edit_rules.py(video_rules.md に書き足したスクリプト)</h3><p>ルールの文書は、文章を打ち直すと途中が抜けることがあるので、置き換える文が文書の中にちょうど1か所あることを確かめてから置き換えるスクリプトで書き足しました。このスクリプトを動かしたあと、試作のフォルダの説明の1行だけを、次の sed で置き換えています。</p><pre><code class=\"language-python\">\"\"\"video_rules.md にハコブくんのナレーションで決めたことを書き足す(置換は1か所ずつ一致を確かめる)\"\"\"\nimport sys\n\nsrc, dst = sys.argv[1], sys.argv[2]\ns = open(src, encoding=\"utf-8\").read()\n\n\ndef rep(old, new):\n global s\n assert s.count(old) == 1, f\"一致が1か所ではない: {old[:40]}\"\n s = s.replace(old, new)\n\n\n# --- 冒頭 ---\nrep(\"最終更新: 2026-10-11\\n状態: 試作1本目「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」を作りながら決めたことをまとめた初版。まだ決まっていない項目は8節にまとめている。\",\n \"最終更新: 2026-10-12\\n状態: 試作1本目「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」を作りながら決めたことをまとめた初版に、ナレーションをハコブくんにする決定(2026-10-12)を足した版。まだ決まっていない項目は8節にまとめている。\")\n\n# --- 1節 ---\nrep(\"| 1 | 自己紹介 | 〜0:55 | ロゴのテロップを出し、毎回同じ文言で名乗る(2節の文言) |\",\n \"| 1 | 自己紹介 | 〜0:55 | ロゴと、エイイチの名前のテロップを出す。ハコブくんが毎回同じ文言で番組とエイイチを紹介する(2節の文言) |\")\nrep(\"| 8 | 見立て | 〜12:30 | 「ここからが僕の一番言いたいことです」と言ってから意見を話す |\",\n \"| 8 | 見立て | 〜12:30 | 「ここからはエイイチの見立てです」と言ってから意見を話す(意見が誰のものかをはっきりさせる) |\")\nrep(\"| 自分の立ち位置を明かす | 3 結論と立場、または本編1節の頭 | 「僕自身、仕事で使っていて、今回は得をする側なんです」「僕は〜否定派じゃないです。技術としては本物」 |\",\n \"| 自分の立ち位置を明かす | 3 結論と立場、または本編1節の頭 | 「僕自身、仕事で使っていて、今回は得をする側なんです」「僕は〜否定派じゃないです。技術としては本物」。このチャンネルではハコブくんが話すので「エイイチ自身、〜」と言い換える |\")\nrep(\"数字の言い方は5本とも同じだった。\",\n \"台本はすべてハコブくんが話す前提で書く。エイイチの体験と意見は「エイイチは」「エイイチの机の上では」と三人称で言う。\\n\\n数字の言い方は5本とも同じだった。\")\n\n# --- 2節 ---\nrep(\"「僕の機体とソフトの組み合わせでは」を付ける。\", \"「エイイチの機体とソフトの組み合わせでは」を付ける。\")\nrep(\"「配送開始を示す公式発表は、僕が確認した範囲では見つかっていません」\",\n \"「配送開始を示す公式発表は、エイイチが確認した範囲では見つかっていません」\")\nrep(\"自己紹介の文言は「僕は本業のデザイナーをしながら、ロボティクスの研究と開発をしています。自宅の机の上でロボットアームを動かして、その記録を公開しています。」に固定する。「秋田」「29年」「毎日」は入れない。\",\n \"自己紹介の文言は、ハコブくんが話す形の「この番組は、本業のデザイナーをしながらロボティクスの研究と開発をしているエイイチが、自宅の机の上でロボットアームを動かした記録をもとにしています。」に固定する(2026-10-12 に、それまでの一人称の文言を言い換えた。文言を変えるときはエイイチが決める)。「秋田」「29年」「毎日」は入れない。\")\n\n# --- 3節 ---\nrep(\"\"\"## 3. ナレーション(現状は仮音声)\n\nいまは仮の合成音声で作っている。本番の声は未決(8節)。\n\n- **エンジン:** Open JTalk、声は nitech_jp_atr503_m001(男声)。設定は 速さ 1.08、音の高さ -1、声質 0.55\"\"\",\n\"\"\"## 3. ナレーション(ハコブくん・声は仮音声)\n\n### 3-1. ナレーションはハコブくんが話す(2026-10-12 決定)\n\n- ナレーションは、Pixels to Motors のキャラクターのハコブくん(病院で物を運ぶロボット)が担当する。エイイチ本人の声と、本人の声の複製は使わない(本人の声が通らないため。複製しても声の性質はそのまま写る)\n- 動画の目的は、見た人から仕事の相談や声がけをもらうこと。キャラクターの後ろで本人が見えなくならないよう、次の3つを必ず入れる\n - 自己紹介の場面で、エイイチの名前のテロップと実機の映像を出す\n - 見立ての節は「ここからはエイイチの見立てです」と言ってから入る\n - 概要欄にプロフィールと連絡先を置く\n- 概要欄に「ナレーションは AI 音声です」と書く\n- 声は、他のチャンネルと同じ声(VOICEVOX のキャラクターの声など)を使わず、ハコブくん専用の声にする。「ハコブくん=有名キャラクターの声のロボット」と聞かれないようにするため。候補は8節\n- 高すぎない声で、ゆっくり丁寧に話す。経営者が13分聞いて疲れないことを基準にする。キャラクターらしい決まった語尾は本編で使わない(使うならジングルと最初の一言だけ)\n\n### 3-2. 仮音声の設定(ハコブくんの声が決まるまで)\n\n- **エンジン:** Open JTalk、声は nitech_jp_atr503_m001(男声)。設定は 速さ 1.0(2026-10-12 に 1.08 から下げた。ハコブくんの口の動きと合わせると速すぎたため)、音の高さ -1、声質 0.55\"\"\")\n\n# --- 4-1 ---\nrep(\"\"\"- 余白は左右140px。下の250pxは字幕のために空ける\n- 左上に章のタグ(章番号はオレンジ、見出しは薄い文字)、右上にロゴ(高さ34px)\"\"\",\n\"\"\"- 余白は左右140px。下の250pxは字幕のために空ける\n- 左上に章のタグ(章番号はオレンジ、見出しは薄い文字。左端から140px)、右上にロゴ(高さ34px。右端から56px、上端から44px。2026-10-12 に右140px・上46pxから端へ寄せた)\n- **ハコブくんの区画:** 左下に、ハコブくんの上半身を右向きで置く(全身より図の邪魔にならないため、上半身を基本にする)。体は幅340pxで x=20〜360px、画面の下端に接して置く(上端は y=766px)。区画は体の範囲に余白20pxを足した x=0〜380px、y=746〜1080px。この区画には字幕・図・出典・テロップを重ねない。体の画像を差し替えたら、区画は置いた体の範囲から計算し直す(スクリプトが自動で計算する)\n- **図の範囲:** 左右140pxのまま使う。ハコブくんの区画に重なるかどうかは、場面ごとに書き出しの前に自動で確かめる。図・文字の位置はブラウザで描いたときの実際の位置を測り、区画と1pxでも重なった場面は、場面の名前を出して書き出しを止める。止まった場面は、図を区画より上に収めるか、左端を420pxに寄せて作り直す\"\"\")\n\n# --- 4-2 ---\nrep(\"- 新しい色を足すときは、色覚の違いがあっても見分けられるかを検証してから使う(dataviz スキルの validate_palette.js。背景色 #F5F5F0 を指定する)\",\n \"- 新しい色を足すときは、色覚の違いがあっても見分けられるかを検証してから使う(dataviz スキルの validate_palette.js。背景色 #F5F5F0 を指定する)\\n- ハコブくんの体と顔の色(目の白 #F6F8FC、口の水色 #57B5F8、側面のリングの緑)は、この表と4-5の「オレンジと青以外の色を足さない」の対象外。図や文字には使わない\")\n\n# --- 4-5 ---\nrep(\"- 影、グラデーション\\n\", \"- 影、グラデーション(ハコブくんにも影は付けない。目のまわりのにじむ光と側面のリングの光は、ハコブくん自身が光っている表現なので例外)\\n\")\n\n# --- 4-6 と 4-7 ---\nrep(\"数字を出す画面には、左下(字幕の上)に出典を24pxの薄い文字で必ず入れる。\",\n \"数字を出す画面には、字幕の上に出典を24pxの薄い文字で必ず入れる。位置は左端から420px(左下のハコブくんの区画を避ける。もとは140px)。\")\nrep(\"\"\"模式図には「図は模式図」と入れる。\n\n---\"\"\", \"\"\"模式図には「図は模式図」と入れる。\n\n### 4-7. ハコブくんの描き方と動き\n\n試作(2026-10-12、`_制作ベース/hakobu_試作/`)で決めた作り方。数値はスクリプトの先頭近くにまとめてある。\n\n**体**\n- 元の動画(ハコブくんが病室にいる生成動画)の1コマから体を切り抜き、左右反転して右向きにする(画面の中央を向く)\n- 頭の上は、後ろの白い枕と重なっていて切り抜きが欠ける。元の動画では、頭の上は「画面の上の縁が低く、奥側がもう一段持ち上がった」形をしている。切り抜きがはっきり頭と判断した範囲を輪郭に使い、縁のギザギザだけをなめらかにする。色は元のコマの画素で戻す(欠けた部分を直線や凸包で埋めると、奥の段差が消えて枕まで頭として塗ってしまう)\n- 側面の緑のリングは、元の絵の画素が粗いので一度消し、楕円をコードで描き直す\n\n**顔(すべてコードで描く)**\n- 目: 白(#F6F8FC)の縦長のカプセル形(上下の端が半円)。まわりに白い光が少しにじむ。光は画面の外に出さない\n- 口: 水色(#57B5F8)。閉じた口(両端が丸い太い弧)・小(上が広く下が丸い形)・中・大(角の丸い三日月形)の4段階\n- 顔は、ディスプレイの四隅に合わせて射影し、ディスプレイと同じ傾きで描く。四隅は、上下左右の辺のまっすぐな部分にそれぞれ直線を当てはめた交点で決める(角が丸いので、角の点を直接取ると傾きを読み違える)\n- 本番では、体だけを先に反転し、顔は向きどおりに描く(試作は顔を描いてから体ごと反転している。左右で形が違う表情が逆になるため)\n\n**動き**\n- 口: ナレーションの音量で4段階を選ぶ。口の形は2フレームごと(毎秒15回)に、1段階ずつしか変えない。閉じた口と大きい口の間に、必ず小と中がはさまる\n- 目: 2.8秒の周期でゆっくり上下する。口が開くと0.1秒遅れて少し上がる。文の頭で、0.3秒だけ小さく跳ねる\n- まばたき: 2〜4.5秒の間でばらつかせる。文を言い終わった直後にも1回入れる\n- 側面のリング: 顔とは別の3.7秒の周期で、息をするように明滅する(明滅の強さもゆっくり不規則に変える)。声には0.2秒遅れて明るくなり、話し終わるとゆっくり暗くなる。声への反応の強さは文ごとに変える。文の頭と、大きく口を開いた瞬間だけは顔とそろえて強く光る(大きく口を開いたときは、3回に1回ほど光らない)。話していない間も、ときどき弱くまたたく\n\n**表情**\n- 1文ごとに、台本で表情を指定する(narration.py の1文の行に「表情」の欄を足す)\n- 目と頬は表情で決める。口は、話している間はどの表情でも音量で動かし、話していない間だけその表情の口に戻す(「_」の口のまま話すと、声と口が合わなくなるため)\n- 使い分けの目安(1本作って確定する。8節)\n\n| 表情 | 使う場面 |\n|---|---|\n| 丸い目+開いた口 | 基本。説明の文はほぼこれ |\n| 丸い目+小さな笑み | 間、聞き手に戻るところ |\n| 驚いた目+「o」の口 | 今日の問い、意外な数字が出るところ |\n| ジト目 | 「本当にそうでしょうか」と反論を出すところ |\n| 目を閉じる | 「まとめると」、考えている間 |\n| 困り眉 | 失敗や限界の話 |\n| >▽< | 結論が言えたところ、うまくいった実験 |\n| 「III_III」 | 計算中・データを読み込んでいる場面 |\n| ウインク、キラキラ目、照れ、>_<、目が泳ぐ | アウトロや軽い一言だけ |\n\n経営者向けの動画なので、1本で使う表情は上の6つ前後にとどめる。\n\n---\"\"\")\n\n# --- 5節 ---\nrep(\"- 2行まで、1行31字以内。改行は読点の位置で入れる。読点がなければ助詞の後ろで割る\\n- 位置は画面の下から62px\",\n \"- 2行まで、1行26字以内(ハコブくんの区画を避けるため。もとは31字)。改行は読点の位置で入れる。読点がなければ助詞の後ろで割る\\n- 位置は画面の下から62px。横はハコブくんの区画の右側(x=400〜1780px)の中央(x=1090px)にそろえ、1行の幅は1380pxまで\")\n\n# --- 6節 ---\nrep(\"- 映像は 1920×1080 に拡大して切り抜く。字幕の後ろに黒50%の帯(高さ230px)を敷く\",\n \"- 映像は 1920×1080 に拡大して切り抜く。字幕の後ろに黒50%の帯(高さ230px)を敷く。帯はハコブくんの区画の右(x=380px から右)だけに敷く\")\nrep(\"- 名前のテロップ: 薄い地に、ロゴ(高さ30px)・名前60px・肩書き30px(「デザイナー/ロボティクスの研究・開発」)。左の太い線は付けない\",\n \"- 名前のテロップ: 薄い地に、ロゴ(高さ30px)・名前60px・肩書き30px(「デザイナー/ロボティクスの研究・開発」)。左の太い線は付けない。ハコブくんの区画の外に置く\")\n\n# --- 7節 ---\nrep(\"\"\" 5. `assemble.py` で1文ずつ映像を作ってつなぎ、音声と合わせる(字幕の srt も出る)\n 6. 確認用に圧縮する\"\"\",\n\"\"\" 5. `assemble.py` で1文ずつ映像を作ってつなぎ、音声と合わせる(字幕の srt も出る)\n 6. 確認用に圧縮する\n- **ハコブくんの試作(`_制作ベース/hakobu_試作/`):** `python3 make_demo.py upper`(全身版は `full`)で、体の切り抜き・顔の描画・口と目とリングの動き・区画との重なり確認・書き出しまでを行う。図と字幕は `render_static.py` が本番の `scenes.py` を読み込んで描く(`base/` に scenes.py と logo_dark.png の写しを置く)。本番の render.py・assemble.py への組み込みはまだ(8節)\"\"\")\n\n# --- 8節 ---\nrep(\"| ナレーションの声 | Open JTalk の仮音声。抑揚が機械的 | 本人の声で録るか、品質の高い音声合成(VOICEVOX など。利用規約とクレジット表記の条件を確認)にするか。srt に1文ごとの時間が入っているので、それに合わせて録れば映像はそのまま使える |\",\n \"| ハコブくんの声 | ナレーションはハコブくんに決定(3-1)。声は Open JTalk の仮音声 | ハコブくん専用の声を、ElevenLabs の Voice Design(文章で声の特徴を書くと新しい声を作る機能。日本語の出来と、作った声を商用で使う条件は試して確かめる)で作るか、声優に依頼して作るか(毎回読んでもらうか、本人の同意を取って声を複製する。声の権利が一番はっきりする) |\\n| ハコブくんの体の画像 | 生成動画の1コマを切り抜き、頭の上とリングを修整して使っている | 同じ構図(斜め・右向き)で、無地の背景・リングなしの画像を生成し直すか。生成し直せば頭とリングの修整が要らなくなる |\\n| ハコブくんの本番組み込み | 試作(`hakobu_試作/`)で動きと配置を確認した段階 | render.py・assemble.py に組み込む。narration.py に「表情」の欄を足す。表情集の顔をコードで描く |\\n| 表情の使い分け | 4-7の目安の表のみ | 1本作って、使う表情と場面を確定する |\")\nrep(\"| ジングル | 文言だけ(「Pixels to Motors。こんにちは、エイイチです。」) | 音とロゴの動きを付けるか |\",\n \"| ジングル | 文言だけ(「Pixels to Motors。こんにちは、エイイチです。」) | ハコブくんが話す形の文言に言い換える。音とロゴの動きを付けるか |\")\n\n# --- 更新履歴 ---\nrep(\"## 更新履歴\\n\\n\",\n \"## 更新履歴\\n\\n- 2026-10-12: ナレーションをハコブくんにする決定を反映。3-1(ハコブくんが話す・本人を見せる3つの決まり・AI音声の明記・専用の声)と4-7(ハコブくんの描き方と動き・表情)を新設。4-1 にハコブくんの区画(左下・上半身・右向き)と、図の範囲を140pxのまま場面ごとの自動の重なり確認に任せる決まりを追加。ロゴを右56px・上44pxへ寄せた。字幕を1行26字・中央x=1090pxへ、出典を左420pxへ移した。ハコブくんの色を4-2・4-5の対象外にした。台本の一人称(僕)をエイイチの三人称に言い換え、自己紹介の固定文言をハコブくんが話す形にした。仮音声の速さを1.08から1.0に下げた。8節にハコブくんの声・体の画像・本番組み込み・表情の使い分けを追加。\\n\")\n\nopen(dst, \"w\", encoding=\"utf-8\").write(s)\nprint(\"ok\", len(s))</code></pre><pre><code class=\"language-bash\">sed -i 's#図と字幕は `render_static.py` が本番の `scenes.py` を読み込んで描く(`base/` に scenes.py と logo_dark.png の写しを置く)。#図と字幕は `render_static.py` が本番の `scenes.py` と `logo_dark.png`(1つ上の `_制作ベース` にあるもの)を読み込んで描く。入力の画像(`cut_raw.png`・`full/g_05.png`)も同じフォルダに置いてある。#' rules/video_rules.md</code></pre><p></p><h2 id=\"index_NfGgHGVW\">実行ログ</h2><h3 id=\"index_s_TBaVYv\">元の生成動画の情報(ffprobe)</h3><pre><code>stream|codec_type=video|width=848|height=1056|r_frame_rate=24/1\nstream|codec_type=audio|r_frame_rate=0/0\nstream|codec_type=video|width=848|height=1056|r_frame_rate=90000/1\nformat|duration=10.041667</code></pre><h3 id=\"index_ts_1basB\">ディスプレイ・目・口・リングの測定(measure_face.py)</h3><pre><code>ディスプレイの外接矩形 (x, y, 幅, 高さ): [325, 349, 183, 151]\n白の領域: x=482 y=344 幅=29 高さ=52 面積=172\n白の領域: x=362 y=395 幅=22 高さ=46 面積=799\n白の領域: x=305 y=396 幅=170 高さ=124 面積=2301\n白の領域: x=446 y=396 幅=23 高さ=48 面積=885\n水色の領域: x=378 y=450 幅=68 高さ=40 面積=2115\n口の色(中央値 RGB): [87, 181, 248]\nリングの範囲: 576 548 629 681 画素数 1793\n体の範囲: 291 328 648 904\n頭の上の行ごとの画素数(アルファ>200 / >60 / >10):\n y=320: 0 / 0 / 0\n y=324: 0 / 0 / 36\n y=328: 0 / 42 / 151\n y=332: 33 / 77 / 183\n y=336: 56 / 95 / 212\n y=340: 70 / 167 / 232\n y=344: 198 / 237 / 249\n y=348: 240 / 250 / 261\n y=352: 251 / 257 / 264\n y=356: 258 / 264 / 270\n上の辺: 点 74 個、傾き 0.18°\n下の辺: 点 74 個、傾き 2.16°\n左の辺: 点 60 個、傾き 5.97°\n右の辺: 点 61 個、傾き 5.67°\nディスプレイの四隅(左上・右上・右下・左下): [[336.8, 348.8], [510.2, 349.3], [495.1, 501.2], [321.5, 494.6]]\n参考・角の点を直接取った四隅(eps=9.0): [[504, 360], [488, 496], [327, 485], [346, 354]]</code></pre><h3 id=\"index_wL1s2RcT\">上半身版の書き出し(python3 make_demo.py upper)</h3><pre><code>ハコブくんの区画: (0, 746, 380, 1080) 体の位置: (20, 766, 360, 1080)\n重なり確認: 文字・図・ロゴ 12 個、区画との重なり 0 件\nframes 398 seconds 13.27\n終了コード: 0</code></pre><h3 id=\"index_qWOd32_u\">全身版の書き出しが、重なりの確認で止まった記録(python3 make_demo.py full)</h3><pre><code>ハコブくんの区画: (0, 580, 353, 1080) 体の位置: (47, 600, 333, 1060)\n 重なり: 図:h2 [140, 589.40625, 1780, 672.59375]\n区画に重なる要素があるため中止しました\n終了コード: 1</code></pre><h3 id=\"index_0eceRLsx\">書き出した上半身版の動画の情報(ffprobe)</h3><pre><code>stream|codec_name=h264|width=1920|height=1080|r_frame_rate=30/1\nstream|codec_name=aac|sample_rate=48000|r_frame_rate=0/0\nformat|duration=13.266667|size=530991\n</code></pre><h3 id=\"index_C2dvPBTh\">図解用に時間の値を書き出した記録(python3 make_demo.py upper --dump)</h3><pre><code>timing.json に書き出しました\nフレーム数 398 / 文ごとの開始と発話終了のフレーム [[9, 51], [62, 195], [216, 371]] / リングの明るさの範囲 0.15 〜 1.0 / まばたきのフレーム数 24</code></pre><p></p><h2 id=\"index_Saxw8HVI\">参考にしたページ</h2><ul><li><p><a href=\"https://elevenlabs.io/pricing\" data-has-link=\"true\">ElevenLabs:Pricing</a></p></li><li><p><a href=\"https://elevenlabs.io/docs/product-guides/voices/voice-design\" data-has-link=\"true\">ElevenLabs:Voice Design</a></p></li><li><p><a href=\"https://voicevox.hiroshiba.jp/term/\" data-has-link=\"true\">VOICEVOX:ソフトウェア利用規約</a></p></li><li><p><a href=\"https://support.google.com/youtube/answer/3376882?hl=ja\" data-has-link=\"true\">YouTube ヘルプ:オーディオライブラリの音楽と効果音を使う</a></p></li><li><p><a href=\"https://dova-s.jp/_contents/license/\" data-has-link=\"true\">DOVA-SYNDROME:音源利用ライセンス</a></p></li><li><p><a href=\"https://soundeffect-lab.info/agreement/\" data-has-link=\"true\">効果音ラボ:利用規約</a></p></li><li><p><a href=\"https://www.epidemicsound.com/pricing/\" data-has-link=\"true\">Epidemic Sound:Pricing</a></p></li><li><p><a href=\"https://github.com/danielgatis/rembg\" data-has-link=\"true\">rembg(GitHub)</a></p></li></ul>", "toc": [{"id": "index_Wy0UmY2h", "text": "動画の企画5本は、見た人が「この人に何を頼めるか」が分かる話を選んだ"}, {"id": "index__9qJii4D", "text": "ナレーション・BGM・効果音は、2026年10月11日時点の公式の条件で比べた"}, {"id": "index_F7VUaN1Y", "text": "ナレーションは、本人の声でも声の複製でもなく、ハコブくんに決めた"}, {"id": "index_O4m3dwLv", "text": "ハコブくんは画面の左下に置き、生成した10秒の動画の1コマから体を切り抜いた"}, {"id": "index_g2Fv9MC5", "text": "顔の目と口は、元の絵を消してコードで描き直した"}, {"id": "index_Ufl5ndIQ", "text": "口は音量で4段階に切り替え、1回に1段階ずつしか動かさない"}, {"id": "index_V8JsKfRO", "text": "目は2.8秒の周期で上下し、口より0.1秒遅れて上がる"}, {"id": "index_W02dAGsw", "text": "側面のリングは、元の絵を消して楕円を描き直し、顔と別の周期で光らせた"}, {"id": "index_tXF5W1d5", "text": "目はカプセル形にし、顔はディスプレイの辺に直線を当てはめて傾けた"}, {"id": "index_fNoBvJtt", "text": "頭の上は、凸包で埋めると奥の段差が消えて枕まで塗っていた"}, {"id": "index_boeVOUIR", "text": "ハコブくんの区画に字幕や図が重なったら、書き出す前に止める"}, {"id": "index_ZOcuqpzA", "text": "全身版より上半身版の方が、図を読む邪魔をしなかった"}, {"id": "index_H1Z4xux4", "text": "決めたことは video_rules.md に3-1節と4-7節として書き足した"}, {"id": "index_OMOijwC5", "text": "まだ決まっていないのは、ハコブくんの声・体の画像・本番への組み込み・表情の4つ"}, {"id": "index_AtrH00k0", "text": "試作スクリプトの全文"}, {"id": "index_NfGgHGVW", "text": "実行ログ"}, {"id": "index_Saxw8HVI", "text": "参考にしたページ"}]}
\ No newline at end of file
+{"day": "LOG331", "series": "ひとり研究所の裏側", "published": "2026-10-12T00:08:34.000Z", "title": "解説動画のナレーションをハコブくんに任せる ― 口と目と光をコードで動かし、文字が重ならない区画を決めた", "slug": "explainer-video-character-narration", "cover": null, "thumb": "/thumbs/r-explainer-video-character-narration.html", "lang": "JP", "description": "YouTube解説動画のナレーションを本人の声ではなくキャラクターのハコブくんに任せると決め、口・目・側面の光をコードで動かす試作を作った記録。字幕や図が重ならない区画と自動の重なり確認も決めた。", "updated": "2026-10-12T00:09:22.000Z", "body": "<!--TOC--><p></p><p>Pixels to Motors の YouTube 解説動画について、続きの作業を Claude と対話しながら進めました。前回(解説動画をAIと作る ― 型の分析から敵対的検証まで、試作1本で決めたこと)で試作1本と制作ルールができたので、今回は「何を話すか」と「誰が話すか」を決めています。</p><p>やったことは4つです。サイトの記事から、ロボティクスに関心のある経営者・PM・マネージャー・開発者に向けた動画の企画を5本立てる。ナレーションの声と、BGM・効果音の候補を、公式の利用条件で比べる。ナレーションを本人の声ではなく、Pixels to Motors のキャラクターのハコブくん(病院で物を運ぶロボット)にすると決める。ハコブくんを画面の左下に置き、口・目・側面の光をコードで動かす試作を作る。</p><p>試作は、フィードバックを受けて十数回作り直しました。口の形、目の動き、側面の光り方、頭の上の切り抜き、顔の傾き、置く大きさを、ひとつずつ直しています。本記事は、その過程で決めたことと、決めた理由の記録です。間違えた直し方(凸包で頭を埋めた、角の点で傾きを測った、など)も消さずに残しました。試作スクリプトの全文と実行ログは記事の後半に載せています。</p><p></p><h2 id=\"index_Wy0UmY2h\">動画の企画5本は、見た人が「この人に何を頼めるか」が分かる話を選んだ</h2><p>動画の目的は、見た人から仕事の相談や依頼をもらうことです。そこで、個人の体験談よりも、見る人が自分の会社やチームの判断に使える話を優先しました。サイトの記事を読み直し、経営者・PM・開発者に向けて5本の企画を立てています。</p><p>選ぶときの基準は、自分の机の上で実測した話が入っていることです。業界の解説だけなら、ほかの人にも作れます。実測の数字が入っていると、この人に頼むと何をしてくれるのかが見る人に伝わります。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>公開する順</p></th><th colspan=\"1\" rowspan=\"1\"><p>企画</p></th><th colspan=\"1\" rowspan=\"1\"><p>冒頭の一言</p></th><th colspan=\"1\" rowspan=\"1\"><p>元にした記事</p></th><th colspan=\"1\" rowspan=\"1\"><p>見た人に伝わること</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>AIエージェントにロボットのコードを書かせたら、原因を7回取り違えた</p></td><td colspan=\"1\" rowspan=\"1\"><p>テストは53件、全部通った。実機に出したら、アームの動きは前と何も変わっていなかった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG294、LOG273、LOG275、LOG170</p></td><td colspan=\"1\" rowspan=\"1\"><p>AIで書く・別のAIでレビューする・実機で小さく通す、という手順を組める</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィジカルAIの研究環境は、買う前に借りて測る</p></td><td colspan=\"1\" rowspan=\"1\"><p>GPUメモリは34,399MiB使われているように見えた。実際に使っていたのは8,486MiBだった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG326、LOG327</p></td><td colspan=\"1\" rowspan=\"1\"><p>本格導入の前の小さな検証(PoC)を、安く速く回せる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィジカルAIの導入は、ロボットを買う前の「仕事の切り出し」で止まっている</p></td><td colspan=\"1\" rowspan=\"1\"><p>清掃ロボットの管理画面に出る4項目は、故障を調べるためではなく、契約どおり掃除したことを示す請求書だった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG306、LOG314、LOG286</p></td><td colspan=\"1\" rowspan=\"1\"><p>導入前に、どの仕事をロボットに渡し、何を人に残すかを分けられる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>ロボットの「成功」を何で数えるか ― 完走したのに、ブロックを掴めていなかった</p></td><td colspan=\"1\" rowspan=\"1\"><p>アームは最後まで止まらずに動き切った。でも、ブロックは掴めていなかった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG246、LOG248、LOG268、LOG220</p></td><td colspan=\"1\" rowspan=\"1\"><p>評価の指標と、成否を記録する仕組みを設計できる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>ロボット企業の勝ち方は3つに分かれる ― 汎用モデル、環境の作り込み、用途の固定</p></td><td colspan=\"1\" rowspan=\"1\"><p>「人型ロボット40万台」と報じられたトヨタの計画は、既存の産業用ロボット40万台の頭脳を入れ替える計画だった</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG315、LOG282、LOG277、LOG329、LOG291</p></td><td colspan=\"1\" rowspan=\"1\"><p>業界の動きを構造で整理し、相手の会社の選択肢に落とせる</p></td></tr></tbody></table></div><p>公開する順は、自分の実測が中心の2本(AIエージェントの回、借りて測る回)を先にしました。ほかの人には作れない動画だからです。仕事の切り出しの回は、相談や契約にいちばん直接つながる企画ですが、実績の動画を2本見せてから出す方が説得力が出ると考えて3本目に置きました。</p><p>企画を立てる途中で、注意が要る点も3つ見つかりました。2本目は、速度やメモリの数字を必ず GPU の型番(NVIDIA A40)と組で言う。4本目は「安全装置を外した実験」の話が入るので、実験の条件(速さ、周りに人がいたか、止める手段)を言わないと、安全を軽く見ているように聞こえる。5本目は自分の実測がない回なので、会社の自己申告の数字は「会社の発表では」と言い、最後に自分の机の上で同じ判断をした場面を1つ入れる。</p><p></p><h2 id=\"index__9qJii4D\">ナレーション・BGM・効果音は、2026年10月11日時点の公式の条件で比べた</h2><p>音声は映像と別に作る前提なので、ナレーションの声、BGM、効果音の候補を、それぞれの公式ページの利用条件で確かめました。料金や条件は変わるので、表は2026年10月11日に確認した時点のものです。</p><p>ナレーションの候補は3つでした。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>方法</p></th><th colspan=\"1\" rowspan=\"1\"><p>条件(確認した時点)</p></th><th colspan=\"1\" rowspan=\"1\"><p>経営者向けの動画に合うか</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>本人の声で録る</p></td><td colspan=\"1\" rowspan=\"1\"><p>字幕ファイルに1文ごとの時間が入っているので、それに合わせて録れば映像はそのまま使える</p></td><td colspan=\"1\" rowspan=\"1\"><p>仕事の相談は人を見て決まるので、最初はこれを勧めた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ElevenLabs で声を複製する</p></td><td colspan=\"1\" rowspan=\"1\"><p>Creator プランは月11ドルの表示(初月の割引の表記あり)。商用ライセンス付きで、本格的な声の複製(Professional Voice Cloning)が1つ作れる</p></td><td colspan=\"1\" rowspan=\"1\"><p>録る時間が取れない回の代わりになる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>VOICEVOX</p></td><td colspan=\"1\" rowspan=\"1\"><p>商用・非商用を問わず無料。VOICEVOX を使ったことが分かるクレジットの表記が必要で、キャラクターごとの規約にも従う</p></td><td colspan=\"1\" rowspan=\"1\"><p>キャラクターの声は娯楽系の解説動画の印象が強く、相談先として見てもらいにくい</p></td></tr></tbody></table></div><p>BGM と効果音は次の候補です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>用途</p></th><th colspan=\"1\" rowspan=\"1\"><p>サービス</p></th><th colspan=\"1\" rowspan=\"1\"><p>条件(確認した時点)</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM・効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>YouTube オーディオライブラリ</p></td><td colspan=\"1\" rowspan=\"1\"><p>収益化している動画で使え、権利者からの著作権の申し立て(Content ID)が来ない。YouTube が著作権上安全と認めているのはここの音源だけ。Creative Commons の曲は概要欄にクレジットが要る</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM</p></td><td colspan=\"1\" rowspan=\"1\"><p>DOVA-SYNDROME</p></td><td colspan=\"1\" rowspan=\"1\"><p>無料、商用OK、クレジット不要。作曲者ごとに別の条件がある場合はそちらが優先。音楽が主役の動画やリミックスは禁止</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM・効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>Epidemic Sound</p></td><td colspan=\"1\" rowspan=\"1\"><p>有料。使用料の追加払いはない。広告に使うには Pro 以上のプランが要る</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>効果音ラボ</p></td><td colspan=\"1\" rowspan=\"1\"><p>無料、商用OK、YouTube の収益化OK、クレジット不要。効果音だけを流す動画は再配布として禁止</p></td></tr></tbody></table></div><p>効果音は、自分のロボットアームのサーボが回る音やグリッパーが閉じる音を録って、章の扉の音にする案も出しました。権利の心配がなく、このチャンネルにしかない音になります。入れる場所は、動画のルールで「打ち出し」と決めている場面(1本に6〜10場面)だけにしぼります。</p><p></p><h2 id=\"index_F7VUaN1Y\">ナレーションは、本人の声でも声の複製でもなく、ハコブくんに決めた</h2><p>声の候補を並べたあと、ナレーションをハコブくんにしたいという相談がありました。</p><blockquote><p>音声はハコブくんをナレーションキャラにしようと思いますが、自分の声の複製とどちらが良いですか?(自分の声は別に良いとは思わない。声が通らない)</p></blockquote><p>ハコブくんの方が良い、と答えました。理由は3つです。声の複製は元の声の性質をそのまま写すので、声が通らないのが不満なら、複製しても通らない声を毎回使うことになります。ハコブくんはすでに Pixels to Motors の4コママンガに出ているキャラクターで、開発ログ(day_log)の発信戦略にも「実機展開以降、ハコブくんをR2R2Rの概念解説役として再登場させる」と書いてありました。もう1つは、本人の実機の話を「エイイチの机の上では、こうなりました」と三人称で言えることです。動画のルールにある「自分の実機の話は、自分の条件の話として言う」が、言い方だけで自然に守れます。</p><p>ただ、目的が仕事の相談をもらうことなので、キャラクターの後ろで本人が見えなくなると効果が落ちます。そこで次の3つを必ず入れると決めました。</p><ul><li><p>自己紹介の場面で、本人の名前のテロップと実機の映像を出す</p></li><li><p>意見を話す節は「ここからはエイイチの見立てです」と言ってから入る(意見が誰のものかをはっきりさせる)</p></li><li><p>概要欄にプロフィールと連絡先を置き、「ナレーションは AI 音声です」と書く</p></li></ul><p>声は、ほかのチャンネルと同じ声(VOICEVOX のキャラクターの声など)を使わず、ハコブくん専用の声にします。「ハコブくん=有名なキャラクターの声のロボット」と聞かれないようにするためです。作り方の候補は2つで、ElevenLabs の Voice Design(文章で声の特徴を書くと新しい声を作る機能)か、声優に頼んで作ってもらうかです。Voice Design は、日本語での出来と、作った声を商用で使う条件が公式の説明に書かれていなかったので、試して確かめる必要があります。話し方は、高すぎない声でゆっくり丁寧に、経営者が13分聞いて疲れないことを基準にしました。</p><p></p><h2 id=\"index_O4m3dwLv\">ハコブくんは画面の左下に置き、生成した10秒の動画の1コマから体を切り抜いた</h2><p>置き方のイメージとして、参考にしている解説動画(前回、型を分析した3本のうち、会社への侵入と暗数の回)の画面が共有されました。画面の左下でサングラスの猫のキャラクターがマイクの前で話していて、字幕は猫の右側から始まっています。</p><blockquote><p>画面の左端にこのような形で話しているハコブくんを追加するイメージです。</p></blockquote><p>あわせて、病室にいるハコブくんの生成動画(10秒)が共有されました。最初に考えたのは、この動画をそのまま左下で繰り返し流す方法ですが、やめました。動画の中の口は声と関係なく開閉するので、話していない間も口が動きます。10秒の動画を13分繰り返すと、同じ動きの繰り返しだと気づかれます。背景の病室を毎フレーム切り抜くと、縁が揺れます。</p><p>そこで、体は動画の1コマを切り抜いた静止画にし、顔だけをコードで描く方法にしました。ハコブくんの顔はディスプレイなので、画面の中の目と口を描き替えれば話しているように見せられます。</p><p>動画は 848×1056 ピクセル・24fps・10.04秒でした。1秒に4枚ずつ、40枚のコマを取り出して並べ、全身と側面の緑のリングが見えている5枚目を選んでいます。背景の切り抜きには rembg(画像から人や物の背景を消すライブラリ)の isnet-general-use というモデルを使いました。</p><p></p><figure><img src=\"/media/s-1920x956_v-frms_webp_dd2ed6ca-70c6-4449-8572-3287b85ed201.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_g2Fv9MC5\">顔の目と口は、元の絵を消してコードで描き直した</h2><p>切り抜いた体のディスプレイを測り、元の目と口の位置と色を取りました(測定の全量は記事後半の実行ログにあります)。ディスプレイは横183×縦151ピクセル、元の口の色は #57B5F8 の水色です。元の目と口はディスプレイごと黒(#131311)で塗りつぶし、その上に同じ位置で目と口を描きます。</p><p>口はナレーションの音量に合わせて動かします。音声を1フレーム(1/30秒)ずつ区切って音量を測り、大きいほど口を大きく開く形にしました。最初の試作は、目が白い楕円、口が「閉じた笑顔/小さく開く/大きく開く」の3段階で、体は左を向いたままでした。</p><p>【画像】DAY331_03_first_prototype_faces.png</p><p>体の向きは、すぐに直しが入りました。</p><blockquote><p>ハコブくんは右向きがいいですね。</p></blockquote><p>左下に置くと、右を向けば画面の中央(図と字幕の側)を向くことになります。体の画像を左右反転して右向きにしました。</p><p>ここで1つ間違えています。その少し前に13種類の表情のイメージ(表情集)が共有されていて、表情集の顔は目も口も同じ薄い水色でした。右向きに直すときに、目と口を両方その水色(#BDE8FB)に変えてしまいました。</p><blockquote><p>色は目は白、口は水色のままでいいですよ</p></blockquote><p>表情集は形の参考で、色は元の動画のままでよかった、ということです。目を白、口を元の水色に戻しました。</p><p></p><figure><img src=\"/media/s-1920x1216_v-frms_webp_0e7f1609-60a9-4748-b723-9db00d3fde2a.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_Ufl5ndIQ\">口は音量で4段階に切り替え、1回に1段階ずつしか動かさない</h2><p>最初の口は、閉じた口からいきなり大きい口に変わることがあり、角も立っていました。</p><blockquote><p>口はもっと柔らかさを出すために角を丸くした方がいいですね(小さい口を途中に入れたりすると動きが出てより良いです)</p></blockquote><p>口の形を4段階にしました。閉じた口は両端が丸い太い弧、小さい口は上が少し広く下が丸い形、中と大は角を丸めた三日月形です。角は、4倍の大きさで形を描いてから、半径5〜6ピクセル相当の丸で角を削っています(モルフォロジーのオープン処理。小さな丸が入り込めない角だけが削れる)。</p><p>切り替え方も変えました。音量から決めた段階に向かって、1回に1段階ずつしか動かしません。閉じた口から大きい口へ行くときは、必ず小と中を通ります。</p><p>次の指摘は速さでした。</p><blockquote><p>口の動きや目の動きをもう気持ちだけゆっくりに動かしたほうがいいね(今は早すぎる。話し方も早すぎなのかもだけど)</p></blockquote><p>口の形を変えるのを、毎フレーム(1秒に30回)から2フレームごと(1秒に15回)にしました。仮の音声の話す速さも1.08倍から1.0倍に落としています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>最初</p></th><th colspan=\"1\" rowspan=\"1\"><p>最後</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>口の段階</p></td><td colspan=\"1\" rowspan=\"1\"><p>3段階(閉じる・小・大)</p></td><td colspan=\"1\" rowspan=\"1\"><p>4段階(閉じる・小・中・大)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>口の切り替え</p></td><td colspan=\"1\" rowspan=\"1\"><p>毎フレーム、段階を飛ばしてよい</p></td><td colspan=\"1\" rowspan=\"1\"><p>2フレームごと、1回に1段階</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>口の角</p></td><td colspan=\"1\" rowspan=\"1\"><p>尖っている</p></td><td colspan=\"1\" rowspan=\"1\"><p>半径5〜6ピクセル相当で丸める</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>仮の音声の速さ</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.08倍</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.0倍</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>問いかけの文のあとの口</p></td><td colspan=\"1\" rowspan=\"1\"><p>「o」の口</p></td><td colspan=\"1\" rowspan=\"1\"><p>小さく開いた口のまま止める</p></td></tr></tbody></table></div><p></p><h2 id=\"index_V8JsKfRO\">目は2.8秒の周期で上下し、口より0.1秒遅れて上がる</h2><blockquote><p>目が少し上下しているように描写すると表情がイキイキして見えます。</p></blockquote><p>目に3つの動きを足しました。1つ目は、ゆっくりした上下の揺れです。最初は1.9秒の周期でしたが、速すぎるという指摘のあと2.8秒に延ばしました。2つ目は、口が大きく開くほど目が少し上がる動きです。口と同時に動くと機械的に見えるので、口より3フレーム(0.1秒)遅れて上がるようにしています。3つ目は、文の頭で目が0.3秒だけ小さく跳ねる動きです。</p><p>まばたきも、最初は3.2秒ごとの一定間隔でしたが、2〜4.5秒の間でばらつかせ、文を言い終わった直後にも1回入れました。</p><div class=\"embed\"><iframe src=\"/embeds/explainer-video-character-narration-QIb2qI10.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p></p><h2 id=\"index_W02dAGsw\">側面のリングは、元の絵を消して楕円を描き直し、顔と別の周期で光らせた</h2><p>側面の緑のリングは、最初は元の絵の画素を明るくしていただけでした。</p><blockquote><p>側面の光っている部分をもっと丁寧に描写(光方が汚く見える)</p></blockquote><p>元の絵のリングは、生成動画を縮小した画像なので画素が粗く、明るくすると粗さが目立ちます。リングの画素に楕円を当てはめて位置と大きさを測り、元のリングはまわりの色で塗りつぶして消しました(OpenCV の inpaint。消したい範囲を、まわりの画素から推測した色で埋める処理)。その上に、4倍の大きさで描いた楕円を縮めて置き、ぼかした光を重ねています。光は体の外には出しません。</p><p></p><figure><img src=\"/media/s-693x876_v-fs_webp_42809200-9e99-483a-85d0-4093607ecd3e.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>次の指摘は、光と表情が一緒に動いていることでした。</p><blockquote><p>側面の光と表情の動きが同じタイミングで繰り返しているので、ずらしたり、合わせたりして躍動感(生命感)を出すと良さそう</p></blockquote><p>普段はずらし、要所だけそろえる動きにしました。リングは、声に0.2秒遅れて明るくなり、話し終わったあとはゆっくり暗くなります(つくときは速く、消えるときは遅い)。話していない間も、息をするように少し明滅します。この明滅の周期は3.7秒で、目の揺れ(2.8秒)と周期を変えてあるので、目とリングが同じ組み合わせで繰り返しません。そろえるのは、文の頭と、口を大きく開いた瞬間だけです。</p><blockquote><p>側面の光は強く光時と、弱く光ときを混ぜ込むと良さそう(ランダム性があるといい)</p></blockquote><p>ばらつきを4か所に入れました。</p><ul><li><p>声への反応の強さを、文ごとに変える(強い文と弱い文が混ざる)</p></li><li><p>口を大きく開いた瞬間の光り方を毎回変え、3回に1回ほどは光らない</p></li><li><p>息をするような明滅の強さを、ゆっくり不規則に変える</p></li><li><p>話していない間も、ときどき弱くまたたく</p></li></ul><p>ばらつきは乱数の種を7に固定して作っているので、同じスクリプトを何度動かしても同じ光り方になります。</p><p></p><h2 id=\"index_tXF5W1d5\">目はカプセル形にし、顔はディスプレイの辺に直線を当てはめて傾けた</h2><p>目の形は、元の動画の顔に合わせる指示がありました。</p><blockquote><p>目のデザインはこれに合わせて欲しい。</p></blockquote><p>元の動画の目は、楕円ではなく、上下の端が半円になった縦長のカプセル形で、まわりに白い光がにじんでいます。カプセル形に変え、光のにじみを足しました。にじみはディスプレイの中だけに収めています。</p><p>続けて、傾きの指摘がありました。</p><blockquote><p>目は顔のディスプレイに合わせて少しだけ斜めにすると良さそう</p></blockquote><p>ハコブくんは斜め前から見た構図なので、ディスプレイも少し傾いて見えます。まっすぐ描いた顔を、ディスプレイの四隅に合わせて射影変換(四角形を別の四角形にはめ込む変形)しました。最初は、ディスプレイの輪郭を4つの角の点に近似して四隅を取りましたが、合いませんでした。</p><blockquote><p>ちょっと目と口の描写が顔のディスプレいと合ってないね。微妙な差だけど。</p></blockquote><p>ディスプレイは角が丸いので、角の点を直接取ると、丸みに引っぱられて位置がずれます。最初の四隅だと上の辺まで傾いていて、顔が回って見えていました。上下左右の辺の、まっすぐな中央4割の点にそれぞれ直線を当てはめ、直線どうしの交点を角にしました。測り直すと、上の辺は0.18度でほぼ水平、左右の辺は約6度(5.97度と5.67度)、下の辺は2.16度傾いていました。</p><p></p><figure><img src=\"/media/s-840x876_v-fs_webp_6637392f-3158-4208-b51c-0fa6fd52d0e6.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>目と口の間隔も2回、ごく少しだけ直しています。</p><blockquote><p>ほんの少しだけだけど、目と口が近い方がデザインが近くなるね(ごくごく少しだけ)</p></blockquote><blockquote><p>目と口の位置がほんの少しだけ上に(本当に少しだけ)</p></blockquote><p>1回目は、目を2ピクセル下げ、口を1.5ピクセル上げて、間を3.5ピクセル詰めました。2回目は、間隔を保ったまま両方を1.5ピクセル上げています(どちらも元の画像の大きさでのピクセル数で、動画上ではその約8割の動きです)。スクリプトでは <code>EYE_SHIFT = 0.5</code> と <code>MOUTH_SHIFT = -3.0</code> の2つの数字で調整できます。</p><p></p><h2 id=\"index_fNoBvJtt\">頭の上は、凸包で埋めると奥の段差が消えて枕まで塗っていた</h2><p>頭の上は、3回直しています。元の動画では、ハコブくんの白い頭が後ろの白い枕と重なっていて、切り抜くと頭の上が薄く欠けます。</p><blockquote><p>あとサンプルの頭の上が切れてしまっているのを修正してください。</p></blockquote><p>1回目は、欠けた部分を含めた頭の輪郭の凸包(輪郭の外側に輪ゴムをかけたような、へこみのない形)で埋めました。</p><blockquote><p>頭の描写をもっと綺麗に処理して(元のデザイン見直してください)</p></blockquote><p>2回目は、凸包の角を大きく丸めてなめらかにしました。</p><blockquote><p>まだ頭の処理が汚いです。</p></blockquote><p>ここで、欠けた部分を下の色から塗り延ばす案を出しましたが、実行する前に止められました。</p><blockquote><p>元の動画があるから見直してください!</p></blockquote><p>元の動画の40枚のコマを並べ、そのうち8枚で頭の上を拡大すると、ハコブくんの頭は、画面のすぐ上の縁が低く、奥側がもう一段持ち上がった形をしていました。凸包は、この段差を直線で結んでしまいます。結んだ内側には後ろの枕が入っていたので、枕まで頭として塗り、形が崩れて汚く見えていました。</p><p>3回目は、凸包をやめました。切り抜きがはっきり頭と判断した範囲(透明度を表すアルファ値が60以上)をそのまま輪郭に使い、縁のギザギザだけを、4倍に拡大してぼかしてから切り直す方法でなめらかにしています。輪郭の内側は頭そのものなので、色は元のコマの画素で戻しました。縁の半透明の画素も元の色で戻したので、2回目まで出ていた暗い点線も消えています。</p><p></p><figure><img src=\"/media/s-882x1224_v-fms_webp_7f743859-c942-49dc-afff-0603e1d0848a.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_boeVOUIR\">ハコブくんの区画に字幕や図が重なったら、書き出す前に止める</h2><p>左下にキャラクターを置くと、字幕や図とぶつかります。試作を作る前に、置き方の決め事が必要だという話になりました。</p><blockquote><p>左端の顔の部分にテキストがかからないように設定しておくことが必要ですね。</p></blockquote><p>ハコブくんの「区画」を決め、そこには何も重ねないことにしました。区画は、実際に置いたハコブくんの体の範囲に余白20ピクセルを足した四角です。動画のルールで決めていた画面の作りとは、次の点が変わります。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>もとの決まり</p></th><th colspan=\"1\" rowspan=\"1\"><p>ハコブくんを置いたあと</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの区画</p></td><td colspan=\"1\" rowspan=\"1\"><p>なし</p></td><td colspan=\"1\" rowspan=\"1\"><p>左下の x=0〜380、y=746〜1080(上半身版)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>字幕の位置</p></td><td colspan=\"1\" rowspan=\"1\"><p>画面の中央、1行31字まで</p></td><td colspan=\"1\" rowspan=\"1\"><p>区画の右側(x=400〜1780)の中央、1行26字まで</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>出典の位置</p></td><td colspan=\"1\" rowspan=\"1\"><p>左端から140ピクセル</p></td><td colspan=\"1\" rowspan=\"1\"><p>左端から420ピクセル</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>図の範囲</p></td><td colspan=\"1\" rowspan=\"1\"><p>左右140ピクセルの余白</p></td><td colspan=\"1\" rowspan=\"1\"><p>左右140ピクセルのまま。区画に重なる場面は自動で止める</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ロゴの位置</p></td><td colspan=\"1\" rowspan=\"1\"><p>右から140ピクセル、上から46ピクセル</p></td><td colspan=\"1\" rowspan=\"1\"><p>右から56ピクセル、上から44ピクセル</p></td></tr></tbody></table></div><p></p><figure><img src=\"/media/s-1024x648_v-fs_webp_79281675-7ed6-4030-9539-7b81072bb026.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>ロゴは、途中まで試作が「Pixels to Motors」の文字を自分で描いていました。</p><blockquote><p>それと動画のデザインルールを確認して欲しい(どこかにデータがある。ロゴも修正)</p></blockquote><p>iCloud の制作ベースのフォルダを開くと、本番は scenes.py(場面ごとの図の HTML と CSS)、render.py(字幕の画像)、logo_dark.png(ロゴの画像)で画面を作っていました。試作が自前で描いていた部分は、章のタグの書き方や、大きい数字の単位の色も本番と違っていたので、図と字幕は本番の scenes.py をそのまま読み込んで描く形に作り直しています。そのあと、ロゴの位置に指摘がありました。</p><blockquote><p>右上のロゴはもっと端に寄せたほうがいいね。パディング取りすぎ。</p></blockquote><p>重なりの確かめ方は、目で見るのをやめ、書き出しの前の自動の確認にしました。図と字幕はブラウザ(Playwright で動かす Chromium)で描いているので、描いたあとに、見出し・数字・出典・ロゴ・字幕の1行ずつの位置を、ブラウザから実際の四角として取り出します(getBoundingClientRect)。その四角がハコブくんの区画と1ピクセルでも重なったら、どの要素がどこで重なったかを出して、書き出しを止めます。</p><p>図の範囲をどうするかは、2つの案から選んでもらいました。図の左端を420ピクセルに寄せたままにするか、本番どおり140ピクセルにして場面ごとの自動の確認に任せるかです。</p><blockquote><p>場面ごとに自動の重なり確認に任せて広げる</p></blockquote><p>上半身版では、12個の要素すべてが区画に重なりませんでした。同じ場面を全身版で書き出すと、区画が高い位置(y=580)から始まるので、「起動時に確保された量の 約28%」の見出しが重なり、書き出しが止まりました。要素の四角で判定しているので、文字が短くても段落の幅いっぱいが判定の対象になります。止まった場面は、図を区画より上に収めるか、左端を420ピクセルに寄せて作り直します。</p><h2 id=\"index_ZOcuqpzA\">全身版より上半身版の方が、図を読む邪魔をしなかった</h2><p>側面のリングを直すときに、全身版と上半身版の2つを作るように言われ、両方を書き出しました。全身版は高さ460ピクセルで体全体を、上半身版は幅340ピクセルで引き出しの取っ手の下あたりまでを置き、画面の下端に接しています。</p><blockquote><p>上半身のデザインがいいですね。全身は少し邪魔しているように見えます。</p></blockquote><p>全身版は、体の下半分(引き出しや車輪)が図と字幕の高さまで伸びていて、視線がそちらに引っぱられます。上半身版は、顔と側面のリングだけが画面の端から出ているので、図を読む邪魔をしません。上半身版を基本にしました。</p><p></p><figure><img src=\"/media/s-811x1056_v-fs_webp_f590a47e-2382-46cb-ad00-ad8d63bd22a9.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>最後の画面はこの形です。</p><p></p><figure><img src=\"/media/s-1920x1080_v-frms_webp_4418024c-4b1e-44f2-a658-4ee6646c54bb.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_H1Z4xux4\">決めたことは video_rules.md に3-1節と4-7節として書き足した</h2><p>ここまで決めたことは、動画制作のルール(video_rules.md)に書き足しました。ルールの文書はプロジェクトの文書と、iCloud の制作ベースのフォルダに同じ内容の写しがあるので、両方を同じ内容に更新しています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>節</p></th><th colspan=\"1\" rowspan=\"1\"><p>書き足したこと</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3-1(新設)</p></td><td colspan=\"1\" rowspan=\"1\"><p>ナレーションはハコブくんが話す。本人を見せる3つの決まり。概要欄に AI 音声と書く。ほかのチャンネルと同じ声を使わない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3-2</p></td><td colspan=\"1\" rowspan=\"1\"><p>仮の音声の速さを1.08倍から1.0倍に下げた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4-1</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの区画(左下・上半身・右向き)。図の範囲は140ピクセルのまま、場面ごとの自動の確認に任せる。ロゴの位置</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4-2・4-5</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの体と顔の色(目の白・口の水色・リングの緑)は、「オレンジと青以外の色を足さない」の対象外。影は付けない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4-6</p></td><td colspan=\"1\" rowspan=\"1\"><p>出典の左端を420ピクセルに移す</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4-7(新設)</p></td><td colspan=\"1\" rowspan=\"1\"><p>体の作り方(頭の上の直し方を含む)、顔の描き方、口・目・まばたき・リングの動きの数値、表情の付け方と使い分けの目安</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>字幕は1行26字まで、区画の右側の中央にそろえる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>台本の一人称「僕」を、本人の名前の三人称に言い換える。自己紹介の固定の文言をハコブくんが話す形にする</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>8</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの声、体の画像、本番のスクリプトへの組み込み、表情の使い分けを未決として追加</p></td></tr></tbody></table></div><p>表情は、目と頬を1文ごとに台本で指定し、口は話している間はどの表情でも音量で動かす、という決まりにしました。表情集の「_」の口のまま話すと、声と口が合わなくなるからです。使い分けの目安は次の表で、経営者向けの動画なので1本で使う表情は6つ前後にとどめます。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>表情</p></th><th colspan=\"1\" rowspan=\"1\"><p>使う場面</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>丸い目+開いた口</p></td><td colspan=\"1\" rowspan=\"1\"><p>基本。説明の文はほぼこれ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>丸い目+小さな笑み</p></td><td colspan=\"1\" rowspan=\"1\"><p>間、聞き手に戻るところ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>驚いた目+「o」の口</p></td><td colspan=\"1\" rowspan=\"1\"><p>動画の冒頭の問い、意外な数字が出るところ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ジト目</p></td><td colspan=\"1\" rowspan=\"1\"><p>「本当にそうでしょうか」と反論を出すところ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>目を閉じる</p></td><td colspan=\"1\" rowspan=\"1\"><p>「まとめると」、考えている間</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>困り眉</p></td><td colspan=\"1\" rowspan=\"1\"><p>失敗や限界の話</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>>▽<</p></td><td colspan=\"1\" rowspan=\"1\"><p>結論が言えたところ、うまくいった実験</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>「III_III」</p></td><td colspan=\"1\" rowspan=\"1\"><p>計算中・データを読み込んでいる場面</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ウインク、キラキラ目、照れ、>_<、目が泳ぐ</p></td><td colspan=\"1\" rowspan=\"1\"><p>アウトロや軽い一言だけ</p></td></tr></tbody></table></div><p>自己紹介の固定の文言は、もとの一人称の文をハコブくんが話す形に言い換え、「この番組は、本業のデザイナーをしながらロボティクスの研究と開発をしているエイイチが、自宅の机の上でロボットアームを動かした記録をもとにしています。」としました。この文言でよいかは、本人の確認待ちです。</p><p>ファイルの置き場所は次のとおりです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>ファイル</p></th><th colspan=\"1\" rowspan=\"1\"><p>置き場所</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>video_rules.md</p></td><td colspan=\"1\" rowspan=\"1\"><p>iCloud <code>PixelstoMotors/デジタルツイン学習R2R2R/動画/_制作ベース/video_rules.md</code> と、プロジェクトの文書 <code>claude/video_rules.md</code></p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>試作のスクリプトと入力の画像</p></td><td colspan=\"1\" rowspan=\"1\"><p>iCloud <code>動画/_制作ベース/hakobu_試作/</code>(make_demo.py、render_static.py、measure_face.py、cut_raw.png、full/g_05.png、元の生成動画)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>試作の動画(mp4)</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>/Volumes/KIOXIA/r2r2r-data/videos/hakobu_試作/</code></p></td></tr></tbody></table></div><p></p><h2 id=\"index_OMOijwC5\">まだ決まっていないのは、ハコブくんの声・体の画像・本番への組み込み・表情の4つ</h2><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>いまの状態</p></th><th colspan=\"1\" rowspan=\"1\"><p>決めること</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの声</p></td><td colspan=\"1\" rowspan=\"1\"><p>Open JTalk の仮の男声</p></td><td colspan=\"1\" rowspan=\"1\"><p>ElevenLabs の Voice Design で作るか、声優に頼んで作るか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>体の画像</p></td><td colspan=\"1\" rowspan=\"1\"><p>生成動画の1コマを切り抜き、頭の上とリングを直して使っている</p></td><td colspan=\"1\" rowspan=\"1\"><p>同じ構図で、無地の背景・リングなしの画像を生成し直すか。生成し直せば、頭の上とリングの直しが要らなくなる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>本番のスクリプトへの組み込み</p></td><td colspan=\"1\" rowspan=\"1\"><p>試作のスクリプトで、動きと配置を確かめた段階</p></td><td colspan=\"1\" rowspan=\"1\"><p>render.py と assemble.py に組み込む。台本(narration.py)に「表情」の欄を足す。表情集の顔をコードで描く。scenes.py のロゴの位置を直す(いまは試作の中で上書きしているだけ)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>表情の使い分け</p></td><td colspan=\"1\" rowspan=\"1\"><p>目安の表だけ</p></td><td colspan=\"1\" rowspan=\"1\"><p>1本作って、使う表情と場面を確定する</p></td></tr></tbody></table></div><p>試作は体を描いてから左右反転しているので、ウインクのように左右で形が違う表情は、このままだと左右が逆になります。本番では、体だけを先に反転し、顔は向きどおりに描く順番にします。</p><p>この4項目のうち3つは、この記事の作業のあとで決まりました。本番への組み込みは hakobu.py として済み、声は ElevenLabs の Voice Design を1か月試すことに、体の画像はいまのまま使うことになっています。字幕の位置も、そのあと画面の中央にそろえる形に変わりました。経緯は video_rules.md の更新履歴にあります。</p><p></p><h2 id=\"index_AtrH00k0\">試作スクリプトの全文</h2><p>使ったもの: Python 3.13.16、opencv-python 5.0.0、Pillow 12.3.0、numpy 2.5.3、pyopenjtalk 0.4.1、rembg 2.0.85(onnxruntime 1.29.0)、playwright 1.56.0(Chromium)、ffmpeg 6.1.1。フォントは Noto Sans CJK JP。</p><p>試作は Claude の作業環境(クラウドの Linux)で動かしました。Mac で動かすときは、<code>hakobu_試作/</code> を作業フォルダにして実行します。render_static.py は、1つ上の <code>_制作ベース/</code> にある本番の scenes.py と logo_dark.png を読み込みます。</p><h3 id=\"index_q54toAvd\">準備のコマンド(コマの取り出しと、体の切り抜き)</h3><p>元の生成動画は、共有されたときのファイル名が文字化けしていたため、<code>hakobu_試作/source_hakobu_hospital.mp4</code> という名前で保存し直しています。</p><pre><code class=\"language-bash\"># 動画の情報を見る\nffprobe -v error -show_entries format=duration:stream=codec_type,width,height,r_frame_rate -of compact source_hakobu_hospital.mp4\n\n# 1秒に4枚ずつ、40枚のコマを取り出す\nmkdir -p full\nffmpeg -v error -y -i source_hakobu_hospital.mp4 -vf \"fps=4\" full/g_%02d.png\n\n# 40枚に番号を振って並べる(コマを選ぶため)\nffmpeg -v error -y -i full/g_%02d.png -vf \"scale=212:-1,drawtext=text='%{n}':x=5:y=5:fontsize=24:fontcolor=red,tile=10x4\" sheet.png\n\n# rembg と、関連するライブラリを入れる\npip install --break-system-packages pyopenjtalk rembg onnxruntime\n</code></pre><pre><code class=\"language-python\"># 5枚目のコマから背景を消す(cut_raw.png)\nfrom rembg import remove, new_session\nfrom PIL import Image\nim = Image.open('full/g_05.png').convert('RGB')\nout = remove(im, session=new_session('isnet-general-use'))\nout.save('cut_raw.png'); print(out.size, out.getbbox())\n</code></pre><h3 id=\"index_TKcoDqjv\">measure_face.py(ディスプレイ・目・口・リングを測る)</h3><pre><code class=\"language-python\">\"\"\"ハコブくんの顔のディスプレイを測る(make_demo.py に書いた座標の出どころ)\n入力: cut_raw.png(元の動画の5コマ目を rembg の isnet-general-use で切り抜いた画像)\n出力: ディスプレイ・目・口・リング・体の範囲、口の色、ディスプレイの四隅\n\"\"\"\nimport numpy as np, cv2\nfrom PIL import Image\n\nraw = np.array(Image.open(\"cut_raw.png\").convert(\"RGBA\"))\nrgb = raw[..., :3].astype(int)\na = raw[..., 3]\nlum = rgb.mean(2)\n\n# 1. ディスプレイ(黒い画面):顔のあたりで一番大きい暗い領域\ny0, y1, x0, x1 = 330, 520, 300, 530\ndark = np.zeros(lum.shape, np.uint8)\ndark[y0:y1, x0:x1] = (lum[y0:y1, x0:x1] < 75)\nn, lab, st, _ = cv2.connectedComponentsWithStats(dark)\nk = 1 + np.argmax(st[1:, 4])\nscr = (lab == k).astype(np.uint8)\nprint(\"ディスプレイの外接矩形 (x, y, 幅, 高さ):\", st[k][:4].tolist())\n\n# 2. 元の目(白)と口(水色)\nreg = rgb[y0:y1, x0:x1]\nrl = reg.mean(2)\nwhite = (rl > 200) & (reg.std(2) < 25)\nblue = (reg[..., 2] > 180) & (reg[..., 0] < 150)\nfor name, m in [(\"白\", white), (\"水色\", blue)]:\n n2, lab2, st2, _ = cv2.connectedComponentsWithStats(m.astype(np.uint8))\n for j in range(1, n2):\n if st2[j, 4] > 80:\n x, y, w, h, area = st2[j].tolist()\n print(f\"{name}の領域: x={x + x0} y={y + y0} 幅={w} 高さ={h} 面積={area}\")\nprint(\"口の色(中央値 RGB):\", np.median(reg[blue], 0).astype(int).tolist())\n\n# 3. 側面の緑のリングと、体全体の範囲\nring = (rgb[..., 1] > 150) & (rgb[..., 1] - rgb[..., 0] > 40) & (rgb[..., 1] - rgb[..., 2] > 20)\nys, xs = np.where(ring)\nprint(\"リングの範囲:\", xs.min(), ys.min(), xs.max(), ys.max(), \"画素数\", len(xs))\nys, xs = np.where(a > 128)\nprint(\"体の範囲:\", xs.min(), ys.min(), xs.max(), ys.max())\n\n# 4. 頭の上の切り抜きの濃さ(行ごとに、アルファが一定以上の画素の数)\nprint(\"頭の上の行ごとの画素数(アルファ>200 / >60 / >10):\")\nfor y in range(320, 360, 4):\n row = a[y, 300:650]\n print(f\" y={y}: {(row > 200).sum()} / {(row > 60).sum()} / {(row > 10).sum()}\")\n\n# 5. ディスプレイの四隅\n# 角が丸いので角の点は直接取らない。上下左右の辺の、まっすぐな中央4割の点に直線を当てはめ、交点を角にする\nc = max(cv2.findContours(scr, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[0], key=cv2.contourArea)\nc = c.reshape(-1, 2).astype(np.float32)\nbx, by, bw, bh = cv2.boundingRect(c.astype(np.int32))\ncx, cy = bx + bw / 2, by + bh / 2\nsides = {\n \"上\": c[(c[:, 1] < cy - bh * 0.3) & (np.abs(c[:, 0] - cx) < bw * 0.2)],\n \"下\": c[(c[:, 1] > cy + bh * 0.3) & (np.abs(c[:, 0] - cx) < bw * 0.2)],\n \"左\": c[(c[:, 0] < cx - bw * 0.3) & (np.abs(c[:, 1] - cy) < bh * 0.2)],\n \"右\": c[(c[:, 0] > cx + bw * 0.3) & (np.abs(c[:, 1] - cy) < bh * 0.2)],\n}\nlines = {}\nfor nm, p in sides.items():\n vx, vy, px, py = cv2.fitLine(p, cv2.DIST_L2, 0, 0.01, 0.01).ravel()\n lines[nm] = (np.array([px, py]), np.array([vx, vy]))\n ang = np.degrees(np.arctan2(vy, vx))\n if nm in (\"左\", \"右\"):\n ang = 90 - abs(ang) # 縦の辺は、垂直からの傾き\n print(f\"{nm}の辺: 点 {len(p)} 個、傾き {ang:.2f}°\")\n\n\ndef cross(s1, s2):\n (p1, d1), (p2, d2) = lines[s1], lines[s2]\n t = np.linalg.solve(np.array([d1, -d2]).T, p2 - p1)\n return p1 + t[0] * d1\n\n\nquad = [cross(\"上\", \"左\"), cross(\"上\", \"右\"), cross(\"下\", \"右\"), cross(\"下\", \"左\")]\nprint(\"ディスプレイの四隅(左上・右上・右下・左下):\", [[round(float(v), 1) for v in q] for q in quad])\n\n# 参考: 角の点をそのまま取った場合(最初に使って、傾きを読み違えた方法)\nhull = cv2.convexHull(c.astype(np.int32))\nfor eps in np.linspace(2, 30, 57):\n ap = cv2.approxPolyDP(hull, eps, True)\n if len(ap) == 4:\n print(f\"参考・角の点を直接取った四隅(eps={eps}):\", ap.reshape(-1, 2).tolist())\n break\n</code></pre><h3 id=\"index_nK_87CBf\">make_demo.py(体の加工・顔の描画・口と目とリングの動き・重なりの確認・書き出し)</h3><pre><code class=\"language-python\">\"\"\"ハコブくん ナレーション試作(13秒ほど)\n使い方: python3 make_demo.py upper (上半身・基本) / python3 make_demo.py full (全身)\n python3 make_demo.py upper --dump (口・目・まばたき・リングの時間の値だけを timing.json に書き出して終わる)\n入力: cut_raw.png(元の動画の5コマ目を rembg で切り抜いた画像)、full/g_05.png(元の動画の5コマ目そのもの)\n必要なもの: pyopenjtalk(仮音声)、playwright と Chromium(図と字幕)、opencv-python、numpy、Pillow、ffmpeg\n- 動画の1コマから切り抜いた体に、画面の顔をコードで描く\n- 仮の音声(Open JTalk)の音量で口を4段階に切り替え、目・まばたき・側面のリングを動かす\n- ハコブくんの区画に文字が重なっていないかを自動で確認する\n\"\"\"\nimport subprocess, sys\nimport numpy as np, cv2, pyopenjtalk\nfrom PIL import Image, ImageDraw, ImageFont, ImageEnhance\n\nW, H, FPS = 1920, 1080, 30\nSR = 48000\nBG = (245, 245, 240)\nINK = (13, 13, 13)\nGRAY = (136, 136, 136)\nORANGE = (196, 71, 31)\nFONT_JP = \"/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc\"\nFONT_BLACK = \"/usr/share/fonts/opentype/noto/NotoSansCJK-Black.ttc\"\nFONT_REG = \"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc\"\n\n# ハコブくんの区画(ここには何も重ねない)\nZONE = (0, 600, 380, 1080)\nSUB_X0, SUB_X1 = 400, 1780\nFIG_X0 = 420\n\n\ndef font(path, size):\n return ImageFont.truetype(path, size, index=0) # index 0 = JP\n\n\n# ---------- 1. 体の切り抜きを整える ----------\nraw = np.array(Image.open(\"cut_raw.png\").convert(\"RGBA\"))\nalpha = raw[..., 3].copy()\nalpha[alpha < 60] = 0\nn, lab, st, _ = cv2.connectedComponentsWithStats((alpha > 0).astype(np.uint8))\nkeep = 1 + np.argmax(st[1:, 4])\nalpha[lab != keep] = 0\n# 頭の上:白い頭が白い枕と重なっていて、切り抜きが薄く欠けている。\n# 元の動画のコマを見ると、頭の上の輪郭は「画面の上の手前側の縁(低い)→ 奥の角で持ち上がる → 奥の縁(高い)→ 右の角で丸く下がる」形。\n# 凸包で埋めるとこの段差が直線になり、枕まで頭に含めてしまう(汚れの原因)。\n# そこで、切り抜きがはっきり頭と判断した範囲(アルファ60以上)をそのまま輪郭に使い、ギザギザだけをなめらかにする。\na_soft = np.array(Image.open(\"cut_raw.png\"))[..., 3]\nHEAD_Y1 = 425 # 肩の段差より上だけを対象にする\nhead = (a_soft > 60).astype(np.uint8)\nhead[HEAD_Y1:] = 0\nhead[:, :300] = 0\nn_h, lab_h, st_h, _ = cv2.connectedComponentsWithStats(head)\nhead = (lab_h == 1 + np.argmax(st_h[1:, 4])).astype(np.uint8)\ncnts, _ = cv2.findContours(head, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)\nhead = np.zeros_like(head)\ncv2.drawContours(head, cnts, -1, 1, -1) # 穴を埋める\n# 4倍に拡大 → ぼかして0.5で切り直す(ギザギザを曲線に) → 小さく角を丸める → 元の大きさへ\n_S = 4\n_big = cv2.resize(head.astype(np.float32) * 255, None, fx=_S, fy=_S, interpolation=cv2.INTER_LINEAR)\n_pad = 20 * _S\n_bigp = cv2.copyMakeBorder(_big, 0, _pad, 0, 0, cv2.BORDER_REPLICATE) # 下の切れ目が丸まらないように下へ延ばす\n_bigp = cv2.GaussianBlur(_bigp, (0, 0), 2.5 * _S)\n_bigp = ((_bigp > 127) * 255).astype(np.uint8)\n_k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * 4 * _S + 1, 2 * 4 * _S + 1))\n_bigp = cv2.morphologyEx(_bigp, cv2.MORPH_OPEN, _k)[: _big.shape[0]].astype(np.float32)\nhull_soft = cv2.resize(_bigp, (head.shape[1], head.shape[0]), interpolation=cv2.INTER_AREA) # 縁は自然な中間値\nhull_mask = (hull_soft > 127).astype(np.uint8)\nhull_soft[HEAD_Y1 - 3:] = 0\nalpha = np.maximum(alpha, hull_soft.astype(np.uint8))\nalpha[: HEAD_Y1 - 20] = hull_soft[: HEAD_Y1 - 20].astype(np.uint8) # 頭の上はこの輪郭だけで描く\n# 輪郭の内側は頭そのものなので、色は元のコマの画素をそのまま使う(切り抜きで暗くなった部分を戻す)\norig_rgb = np.array(Image.open(\"full/g_05.png\").convert(\"RGB\"))\nrestore = (hull_soft > 0) & (a_soft < 245) # 縁の半透明の画素も含める(暗い点線が出ないように)\nrestore[HEAD_Y1:] = False\nraw[restore, :3] = orig_rgb[restore]\nraw[..., 3] = alpha\n\n# 画面(黒いディスプレイ)の領域を見つけて、元の目と口を消す\nrgb = raw[..., :3].astype(int)\nlum = rgb.mean(2)\ny0, y1, x0, x1 = 330, 520, 300, 530\ndark = np.zeros(lum.shape, np.uint8)\ndark[y0:y1, x0:x1] = (lum[y0:y1, x0:x1] < 75)\nn, lab, st, _ = cv2.connectedComponentsWithStats(dark)\nk = 1 + np.argmax(st[1:, 4])\nscr = (lab == k).astype(np.uint8)\ncnts, _ = cv2.findContours(scr, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)\nscreen_mask = np.zeros_like(scr)\ncv2.drawContours(screen_mask, cnts, -1, 1, -1)\nscreen_mask = cv2.erode(screen_mask, np.ones((3, 3), np.uint8), 1)\nSCREEN_COLOR = (19, 19, 17)\nraw[screen_mask == 1, :3] = SCREEN_COLOR\n\n# 横の緑のリング\n# 元のリングは画素が粗いので、一度消して、楕円をコードで描き直す\ng = raw[..., :3].astype(int)\nring_core = (g[..., 1] > 150) & (g[..., 1] - g[..., 0] > 40) & (g[..., 1] - g[..., 2] > 20)\nring_core &= ~(screen_mask.astype(bool))\nys, xs = np.where(ring_core)\nRING = cv2.fitEllipse(np.stack([xs, ys], 1).astype(np.float32)) # ((cx,cy),(長径,短径),角度)\n(rcx, rcy), (rw, rh), rang = RING\nperim = np.pi * (3 * (rw + rh) / 2 - np.sqrt((3 * rw / 2 + rh / 2) * (rw / 2 + 3 * rh / 2)))\nRING_T = max(3.5, min(7.0, ring_core.sum() / perim)) # リングの太さ(px)\n# 緑がにじんでいる範囲ごと塗り直して消す\nhalo = ((g[..., 1] - g[..., 0] > 15) & (g[..., 1] - g[..., 2] > 5)) | ring_core\nhalo = cv2.dilate(halo.astype(np.uint8), np.ones((7, 7), np.uint8), 1)\nhalo[: int(rcy - rh / 2 - 15)] = 0\nhalo[int(rcy + rh / 2 + 15):] = 0\nhalo[:, : int(rcx - rw / 2 - 15)] = 0\nhalo[:, int(rcx + rw / 2 + 15):] = 0\nbgr = cv2.cvtColor(raw[..., :3].copy(), cv2.COLOR_RGB2BGR)\nbgr = cv2.inpaint(bgr, halo, 7, cv2.INPAINT_TELEA)\nraw[..., :3] = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)\n\n\ndef draw_ring(img, on):\n \"\"\"リングを4倍の解像度で描き、にじむ光を重ねる\"\"\"\n m = 30\n bx0, by0 = int(rcx - rw / 2 - m), int(rcy - rh / 2 - m)\n bw, bh = int(rw + 2 * m), int(rh + 2 * m)\n big = np.zeros((bh * SS, bw * SS), np.uint8)\n cv2.ellipse(big, ((int((rcx - bx0) * SS), int((rcy - by0) * SS)),\n (int(rw * SS), int(rh * SS)), rang),\n 255, int(RING_T * SS), cv2.LINE_AA)\n core = cv2.resize(big, (bw, bh), interpolation=cv2.INTER_AREA).astype(np.float32) / 255\n on = float(on) # 0(話していない)〜1(話している)\n glow = cv2.GaussianBlur(core, (0, 0), 3.5 + 1.5 * on)\n glow = np.clip(glow * (1.4 + 0.8 * on), 0, 1)\n region = np.array(img.crop((bx0, by0, bx0 + bw, by0 + bh))).astype(np.float32)\n a = region[..., 3:4] / 255\n rgb = region[..., :3]\n glow_col = np.array([70, 225, 130], np.float32)\n core_col = np.array([160, 240, 185], np.float32) * (1 - on) + np.array([215, 255, 228], np.float32) * on\n gk = (glow * (0.45 + 0.3 * on))[..., None] * a # 体の外には光を出さない\n rgb = rgb * (1 - gk) + glow_col * gk\n ck = core[..., None] * a\n rgb = rgb * (1 - ck) + core_col * ck\n region[..., :3] = rgb\n img.paste(Image.fromarray(region.astype(np.uint8)), (bx0, by0))\n\n\nbase_body = Image.fromarray(raw)\nbody_bbox = base_body.getbbox()\n\n# ---------- 2. 顔を描く(元の位置を実測した座標) ----------\nEYE_L = (362, 395, 384, 441)\nEYE_R = (446, 396, 469, 444)\nMOUTH = (378, 450, 446, 490)\nEYE_SHIFT = 0.5 # 目の上下位置(元画像の px。+で下)。顔全体を1.5px上げた\nMOUTH_SHIFT = -3.0 # 口の上下位置(目と口の間は 3.5px 詰めたまま)\nBLUE = (87, 181, 248) # 口の水色(元の動画の色 #57B5F8)\nWHITE = (246, 248, 252) # 目の白\nSS = 4 # 4倍で描いて縮める(縁をなめらかにする)\n\n\nFX0, FY0, FX1, FY1 = 320, 370, 520, 500 # 顔を描く範囲(元画像の座標)\n\n\ndef _pts(xs, ys):\n return np.stack([(np.asarray(xs) - FX0) * SS, (np.asarray(ys) - FY0) * SS], 1).astype(np.int32)\n\n\ndef mouth_mask(kind):\n \"\"\"口の形を4倍の解像度で作り、角を丸めて返す(0/255 の配列)\"\"\"\n w, h = (FX1 - FX0) * SS, (FY1 - FY0) * SS\n m = np.zeros((h, w), np.uint8)\n cx = (MOUTH[0] + MOUTH[2]) / 2\n top = MOUTH[1] + 2 + MOUTH_SHIFT\n t = np.linspace(0, 1, 60)\n if kind in (\"mid\", \"large\"):\n # 三日月形:上の辺はゆるく下がり、下の辺は深い弧\n hw, depth, sag, rr = (34, 38, 7, 6) if kind == \"large\" else (27, 25, 5, 5)\n xs = cx - hw + 2 * hw * t\n y_top = top + sag * (1 - (2 * t - 1) ** 2)\n y_bot = top + depth * np.sqrt(np.clip(1 - (2 * t - 1) ** 2, 0, 1))\n poly = np.concatenate([_pts(xs, y_top), _pts(xs[::-1], y_bot[::-1])])\n cv2.fillPoly(m, [poly], 255)\n elif kind == \"small\":\n # 小さく開いた口:上が少し広く、下が丸い\n hw, rr = 14, 6\n xs = cx - hw + 2 * hw * t\n y_top = top + 4 + 2 * (1 - (2 * t - 1) ** 2)\n ang = np.linspace(0, np.pi, 60)\n bx = cx + hw * np.cos(ang)\n by = top + 12 + 13 * np.sin(ang)\n poly = np.concatenate([_pts(xs, y_top), _pts(bx, by)])\n cv2.fillPoly(m, [poly], 255)\n elif kind == \"closed\":\n # 閉じた笑顔:太さのある弧、両端は丸い\n hw, sag, thick = 25, 9, 7\n for tt in np.linspace(0, 1, 200):\n x = cx - hw + 2 * hw * tt\n y = top + 7 + sag * (1 - (2 * tt - 1) ** 2)\n p = _pts([x], [y])[0]\n cv2.circle(m, (int(p[0]), int(p[1])), int(thick / 2 * SS), 255, -1)\n return m\n k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * rr * SS + 1, 2 * rr * SS + 1))\n return cv2.morphologyEx(m, cv2.MORPH_OPEN, k) # 角を丸める\n\n\nMOUTH_CACHE = {}\n\n# ディスプレイの四隅:画面の上下左右の辺のまっすぐな部分(角の丸みを除いた中央4割)に直線を当てはめ、\n# その交点を角とした(角の丸みに引っぱられないように)。上の辺はほぼ水平(0.2°)、左右の辺は約6°傾いている\nSCREEN_QUAD = np.float32([[336.8, 348.8], [510.2, 349.3], [495.1, 501.2], [321.5, 494.6]]) # 左上・右上・右下・左下\n_c = SCREEN_QUAD.mean(0)\n_w = (np.linalg.norm(SCREEN_QUAD[1] - SCREEN_QUAD[0]) + np.linalg.norm(SCREEN_QUAD[2] - SCREEN_QUAD[3])) / 2\n_h = (np.linalg.norm(SCREEN_QUAD[3] - SCREEN_QUAD[0]) + np.linalg.norm(SCREEN_QUAD[2] - SCREEN_QUAD[1])) / 2\nFLAT_RECT = np.float32([[_c[0] - _w / 2, _c[1] - _h / 2], [_c[0] + _w / 2, _c[1] - _h / 2],\n [_c[0] + _w / 2, _c[1] + _h / 2], [_c[0] - _w / 2, _c[1] + _h / 2]])\nTILT = 1.0 # 1.0 = ディスプレイと同じだけ傾ける。0.5 なら半分\n_quad = FLAT_RECT + TILT * (SCREEN_QUAD - FLAT_RECT)\n_to_layer = lambda p: (p - np.float32([FX0, FY0])) * SS\nH_FACE = cv2.getPerspectiveTransform(_to_layer(FLAT_RECT), _to_layer(_quad))\n\n\ndef warp_to_screen(la):\n h, w = la.shape[:2]\n a = la[..., 3:4].astype(np.float32) / 255\n pre = np.concatenate([la[..., :3].astype(np.float32) * a, a * 255], 2) # 縁が黒ずまないよう乗算済みで変形\n out = cv2.warpPerspective(pre, H_FACE, (w, h), flags=cv2.INTER_LINEAR)\n aa = out[..., 3:4] / 255\n rgb = np.where(aa > 1e-4, out[..., :3] / np.maximum(aa, 1e-4), 0)\n return np.concatenate([np.clip(rgb, 0, 255), out[..., 3:4]], 2).astype(np.uint8)\n\n\ndef draw_face(img, mouth, eyes_closed, eye_dy=0.0):\n fx0, fy0, fx1, fy1 = FX0, FY0, FX1, FY1\n layer = Image.new(\"RGBA\", ((fx1 - fx0) * SS, (fy1 - fy0) * SS), (0, 0, 0, 0))\n d = ImageDraw.Draw(layer)\n\n def r(b):\n return [(b[0] - fx0) * SS, (b[1] - fy0) * SS, (b[2] - fx0) * SS, (b[3] - fy0) * SS]\n\n for e in (EYE_L, EYE_R):\n e = (e[0], e[1] + eye_dy + EYE_SHIFT, e[2], e[3] + eye_dy + EYE_SHIFT) # 目だけ少し上下させる\n if eyes_closed:\n cy = (e[1] + e[3]) / 2\n d.rounded_rectangle(r((e[0] - 1, cy - 2.5, e[2] + 1, cy + 2.5)), radius=2.5 * SS, fill=WHITE)\n else:\n # 縦長のカプセル形(上下の端が半円)\n w = e[2] - e[0]\n d.rounded_rectangle(r(e), radius=w / 2 * SS, fill=WHITE)\n if mouth not in MOUTH_CACHE:\n MOUTH_CACHE[mouth] = mouth_mask(mouth)\n la = np.array(layer)\n mm = MOUTH_CACHE[mouth] > 0\n la[mm, :3] = BLUE\n la[mm, 3] = 255\n # 顔ごとディスプレイの傾きに合わせて斜めにする(まっすぐ描いた顔を、画面の四隅へ射影する)\n la = warp_to_screen(la)\n layer = Image.fromarray(la)\n layer = layer.resize((fx1 - fx0, fy1 - fy0), Image.LANCZOS)\n # 画面の外にはみ出さないよう、画面の形で切る\n m = Image.fromarray((screen_mask[fy0:fy1, fx0:fx1] * 255).astype(np.uint8))\n la = np.array(layer)\n inside = (np.array(m) > 0)\n la[..., 3] = (la[..., 3].astype(int) * inside).astype(np.uint8)\n # 目のまわりのにじむ光(画面の中だけ)\n eye_a = la[..., 3].astype(np.float32) / 255 * (la[..., 0] > 200)\n glow = cv2.GaussianBlur(eye_a, (0, 0), 2.6) * 0.75 * inside\n glow_layer = np.zeros_like(la)\n glow_layer[..., :3] = (205, 225, 255)\n glow_layer[..., 3] = np.clip(glow * 255, 0, 255).astype(np.uint8)\n img.alpha_composite(Image.fromarray(glow_layer), (fx0, fy0))\n img.alpha_composite(Image.fromarray(la), (fx0, fy0))\n\n\nMODE = sys.argv[1] if len(sys.argv) > 1 else \"upper\" # upper=上半身(基本) / full=全身\nif MODE == \"full\":\n CROP = body_bbox\n ROBOT_H = 460\n scale = ROBOT_H / (CROP[3] - CROP[1])\n ROBOT_W = round((CROP[2] - CROP[0]) * scale)\n ROBOT_Y = ZONE[3] - 20 - ROBOT_H\nelse:\n # 上半身:引き出しの取っ手の下あたりで切り、画面の下端に接して置く\n CROP = (body_bbox[0], body_bbox[1], body_bbox[2], 660)\n ROBOT_W = 340\n scale = ROBOT_W / (CROP[2] - CROP[0])\n ROBOT_H = round((CROP[3] - CROP[1]) * scale)\n ROBOT_Y = H - ROBOT_H\nROBOT_X = (ZONE[0] + ZONE[2]) // 2 - ROBOT_W // 2\n# ハコブくんの区画は、実際に置いた体の範囲+余白20pxで決める(上半身版は全身版より低い位置から始まる)\nZONE = (0, ROBOT_Y - 20, ROBOT_X + ROBOT_W + 20, H)\ncache = {}\n\n\ndef robot(mouth, eyes_closed, ring_on, eye_dy=0.0):\n key = (mouth, eyes_closed, ring_on, eye_dy)\n if key not in cache:\n img = base_body.copy()\n draw_ring(img, ring_on)\n draw_face(img, mouth, eyes_closed, eye_dy)\n img = img.crop(CROP).resize((ROBOT_W, ROBOT_H), Image.LANCZOS)\n img = img.transpose(Image.FLIP_LEFT_RIGHT) # 右向き(画面の中央を向く)\n cache[key] = img\n return cache[key]\n\n\n# ---------- 3. 仮の音声 ----------\nSCRIPT = [\n # (字幕, 読み上げ用, 後ろの間[秒], 間の顔)\n (\"ハコブくんです。\", \"ハコブくんです。\", 0.35, \"closed\"),\n (\"高いGPUを、買う必要はあるのでしょうか。\",\n \"たかいジーピーユーを、買う必要は、あるのでしょうか。\", 0.7, \"o\"),\n (\"エイイチが借りて測ると、\\n使ったメモリは約3割でした。\",\n \"エイイチが借りて測ると、使ったメモリは、約3割でした。\", 0.6, \"closed\"),\n]\n\naudio = []\ntimeline = [] # (開始フレーム, 終了フレーム, 字幕, 間の顔, 発話終了フレーム)\nt = 0\nfor sub, read, gap, gap_face in SCRIPT:\n x, sr = pyopenjtalk.tts(read, speed=1.0, half_tone=-1.0) # 1.08 → 1.0 に落とした\n assert sr == SR\n x = x / 32768.0\n seg = np.concatenate([x, np.zeros(int(gap * SR))])\n f_start = round(t * FPS)\n f_speech_end = round((t + len(x) / SR) * FPS)\n t += len(seg) / SR\n f_end = round(t * FPS)\n audio.append(seg)\n timeline.append((f_start, f_end, sub, gap_face, f_speech_end))\naudio = np.concatenate([np.zeros(int(0.3 * SR)), np.concatenate(audio)])\nlead = round(0.3 * FPS)\ntimeline = [(a + lead, b + lead, s, gf, e + lead) for a, b, s, gf, e in timeline]\ntotal_frames = timeline[-1][1] + round(0.3 * FPS)\naudio = np.concatenate([audio, np.zeros(max(0, int(total_frames / FPS * SR) - len(audio)))])\npeak = np.abs(audio).max()\naudio = audio / peak * 0.8\nimport wave\nwith wave.open(\"narration.wav\", \"wb\") as wf:\n wf.setnchannels(1); wf.setsampwidth(2); wf.setframerate(SR)\n wf.writeframes((audio * 32767).astype(np.int16).tobytes())\n\n# 音量 → 口の4段階(閉じる/小/中/大)\nMOUTHS = [\"closed\", \"small\", \"mid\", \"large\"]\nhop = SR // FPS\nrms = np.array([np.sqrt(np.mean(audio[i * hop:(i + 1) * hop] ** 2)) if i * hop < len(audio) else 0\n for i in range(total_frames)])\nref = np.percentile(rms[rms > 0.01], 90)\nlvl = rms / ref\nlvl_s = np.convolve(lvl, np.ones(3) / 3, mode=\"same\") # 音量を少しならす\ntarget = np.digitize(lvl_s, [0.15, 0.35, 0.65]) # 0..3\n# ちらつき防止: 1フレームだけの変化は前に合わせる\nfor i in range(1, total_frames - 1):\n if target[i] != target[i - 1] and target[i] != target[i + 1]:\n target[i] = target[i - 1]\n# 1フレームに1段階ずつしか変えない → 閉じる→大 の間に必ず小・中が入る\nstep = np.zeros(total_frames, int)\nMOUTH_HOLD = 2 # 口は2フレームごと(毎秒15回)にだけ変える\nfor i in range(total_frames):\n prev = step[i - 1] if i else 0\n if i % MOUTH_HOLD == 0:\n step[i] = prev + int(np.clip(target[i] - prev, -1, 1))\n else:\n step[i] = prev\nmouth_seq = [MOUTHS[s] for s in step]\n# 間の顔(問いかけのあとは小さく開いた口のまま止める)\nfor a, b, s, gf, e in timeline:\n for i in range(e + 2, b):\n if gf == \"o\":\n mouth_seq[i] = \"small\"\n# 目の上下:ゆっくりした揺れ + 口が開くほど少し上がる(元画像の px。0.25px 刻みにまとめる)\ntt = np.arange(total_frames) / FPS\nrng = np.random.default_rng(7)\nstarts = [a for a, b, s, gf, e in timeline]\nends = [e for a, b, s, gf, e in timeline]\n\n# ---- 目:ゆっくりした揺れ + 口に少し遅れて上がる + 文の頭で小さく跳ねる ----\nlift = np.convolve(step / 3.0, np.ones(11) / 11, mode=\"same\")\nlift = np.concatenate([np.zeros(3), lift[:-3]]) # 口より3フレーム(0.1秒)遅れて動く\nhop = np.zeros(total_frames)\nfor a in starts: # 文の頭:目がぴょこっと上がって戻る(0.3秒)\n n = np.arange(9)\n hop[a:a + 9] += np.sin(np.pi * n / 8)[: len(hop[a:a + 9])]\neye_dy = 1.6 * np.sin(2 * np.pi * tt / 2.8) - 2.6 * lift - 2.0 * hop\neye_dy = np.round(eye_dy * 4) / 4\n\n# ---- まばたき:間隔をばらつかせ、文の終わりの直後にも入れる ----\nblink = np.zeros(total_frames, bool)\nt_b = 1.2\nwhile t_b * FPS < total_frames:\n blink[int(t_b * FPS):int(t_b * FPS) + 4] = True\n t_b += rng.uniform(2.0, 4.5)\nfor e in ends:\n if not blink[max(0, e - FPS):e + 8].any():\n blink[e + 4:e + 8] = True\n\n# ---- 側面の光:顔とは別の周期で息をするように明滅し、声には少し遅れて追いかける ----\n# 息をするような明滅。強さそのものもゆっくり不規則に変わる\nnoise = np.convolve(rng.normal(0, 1, total_frames + 90), np.ones(90) / 90, mode=\"valid\")[:total_frames]\nnoise = (noise - noise.min()) / (noise.max() - noise.min() + 1e-9) # 0〜1 のゆっくりした揺らぎ\nbreath = (0.5 + 0.5 * np.sin(2 * np.pi * tt / 3.7 + 1.3)) * (0.4 + 0.9 * noise) # 目の揺れ(2.8秒)と周期をずらす\ntalk = (lvl_s > 0.15).astype(float)\nenv = np.zeros(total_frames)\nfor i in range(1, total_frames): # 立ち上がりは速く、消えるのはゆっくり\n k = 0.30 if talk[i] > env[i - 1] else 0.05\n env[i] = env[i - 1] + k * (talk[i] - env[i - 1])\nenv = np.concatenate([np.zeros(6), env[:-6]]) # 声より0.2秒遅れて光る\ngain = np.full(total_frames, 0.5)\nfor a, b, s_, gf, e in timeline: # 文ごとに、声への反応の強さを変える(強い文・弱い文が混ざる)\n gain[a:b] = rng.uniform(0.3, 0.75)\ngain = np.convolve(gain, np.ones(15) / 15, mode=\"same\")\npulse = np.zeros(total_frames)\n\n\ndef add_pulse(i, amp, tau):\n n = np.arange(min(int(tau * 3), total_frames - i))\n pulse[i:i + len(n)] = np.maximum(pulse[i:i + len(n)], amp * np.exp(-n / tau))\n\n\nlast = -999\nfor i in range(total_frames):\n # 文の頭と、大きく口を開いた瞬間:顔とそろえて光る。強さは毎回ばらつき、ときどき光らない\n if i in starts:\n add_pulse(i, rng.uniform(0.25, 0.45), 5); last = i\n elif step[i] == 3 and i - last > 15:\n if rng.random() < 0.7:\n add_pulse(i, rng.uniform(0.08, 0.4), rng.uniform(3, 7))\n last = i\n# 話していないときも、ときどき弱くまたたく\nt_tw = rng.uniform(1.0, 2.5)\nwhile t_tw * FPS < total_frames:\n add_pulse(int(t_tw * FPS), rng.uniform(0.06, 0.18), rng.uniform(4, 9))\n t_tw += rng.uniform(1.5, 4.0)\nring_lv = np.clip(0.10 + 0.2 * breath + gain * env + pulse, 0, 1)\nring_lv = np.round(ring_lv * 20) / 20\n\nif \"--dump\" in sys.argv: # 図解用に、時間の値だけを書き出して終わる\n import json\n json.dump({\"fps\": FPS, \"frames\": int(total_frames),\n \"voice\": np.round(np.clip(lvl_s, 0, 1.5), 3).tolist(),\n \"mouth\": [int(v) for v in step], \"eye_dy\": eye_dy.tolist(),\n \"blink\": [int(b) for b in blink], \"ring\": ring_lv.tolist(),\n \"sentences\": [[int(a), int(e)] for a, b, s, gf, e in timeline]},\n open(\"timing.json\", \"w\"))\n print(\"timing.json に書き出しました\")\n sys.exit(0)\n\n# ---------- 4. 画面の文字と図(本番と同じ HTML/CSS で描く) ----------\nimport json\nsubprocess.run([sys.executable, \"render_static.py\", json.dumps([s for a, b, s, gf, e in timeline], ensure_ascii=False)], check=True)\nstatic = Image.open(\"png/static.png\").convert(\"RGB\")\nsub_layers = {s: Image.open(f\"png/sub_{k}.png\").convert(\"RGBA\") for k, (a, b, s, gf, e) in enumerate(timeline)}\nboxes = json.load(open(\"png/boxes.json\"))\n\n# ---------- 5. 重なりの自動確認 ----------\ndef overlap(b1, b2):\n return not (b1[2] <= b2[0] or b2[2] <= b1[0] or b1[3] <= b2[1] or b2[3] <= b1[1])\n\nbad = [(nm, bb) for nm, bb in boxes if overlap(bb, ZONE)]\nprint(\"ハコブくんの区画:\", ZONE, \" 体の位置:\", (ROBOT_X, ROBOT_Y, ROBOT_X + ROBOT_W, ROBOT_Y + ROBOT_H))\nif bad:\n for nm, bb in bad:\n print(\" 重なり:\", nm, bb)\n sys.exit(\"区画に重なる要素があるため中止しました\")\nprint(f\"重なり確認: 文字・図・ロゴ {len(boxes)} 個、区画との重なり 0 件\")\n\n# ---------- 6. 書き出し ----------\ncmd = [\"ffmpeg\", \"-v\", \"error\", \"-y\", \"-f\", \"rawvideo\", \"-pix_fmt\", \"rgb24\", \"-s\", f\"{W}x{H}\",\n \"-r\", str(FPS), \"-i\", \"-\", \"-i\", \"narration.wav\", \"-c:v\", \"libx264\", \"-crf\", \"20\",\n \"-pix_fmt\", \"yuv420p\", \"-c:a\", \"aac\", \"-b:a\", \"192k\", \"-shortest\", f\"hakobu_demo_{MODE}.mp4\"]\np = subprocess.Popen(cmd, stdin=subprocess.PIPE)\ncur_sub = None\nfor i in range(total_frames):\n frame = static.copy().convert(\"RGBA\")\n for a, b, s, gf, e in timeline:\n if a <= i < b:\n frame.alpha_composite(sub_layers[s])\n frame.alpha_composite(robot(mouth_seq[i], bool(blink[i]), float(ring_lv[i]), float(eye_dy[i])), (ROBOT_X, ROBOT_Y))\n if i == round(total_frames * 0.42):\n frame.convert(\"RGB\").save(f\"hakobu_demo_{MODE}_still.png\")\n p.stdin.write(frame.convert(\"RGB\").tobytes())\np.stdin.close(); p.wait()\nprint(\"frames\", total_frames, \"seconds\", round(total_frames / FPS, 2))</code></pre><h3 id=\"index_BgCE00BU\">render_static.py(図と字幕を本番の scenes.py で描き、要素の位置を測る)</h3><pre><code class=\"language-python\">\"\"\"図と字幕を、本番(_制作ベース の scenes.py / render.py)と同じ HTML/CSS で描く。\nハコブくんを左下に置くために本番から変えた点:\n - 図の範囲(.wrap)は本番と同じ左端 140px のまま。ハコブくんの区画に重なる場面は、書き出し前の自動確認で止める\n - 出典(.src)の左端 140px → 420px\n - 字幕の中央 960px → 1090px、1行の最大幅 1640px → 1380px(1行26字まで)\n - ロゴ(.brand)を右端に寄せる:右 140px → 56px、上 46px → 44px\n各要素の位置は getBoundingClientRect で測り、png/boxes.json に書き出す(重なり確認用)。\n\"\"\"\nimport sys, os, json, html\n# 本番の scenes.py と logo_dark.png を読む。_制作ベース/hakobu_試作/ に置いたときは1つ上の _制作ベース を、\n# それ以外の場所では base/ に置いた写しを使う\n_here = os.path.dirname(os.path.abspath(__file__))\n_parent = os.path.dirname(_here)\nsys.path.insert(0, _parent if os.path.exists(os.path.join(_parent, \"scenes.py\")) else os.path.join(_here, \"base\"))\nimport scenes\nfrom playwright.sync_api import sync_playwright\n\nsubs = json.loads(sys.argv[1])\nos.makedirs(\"png\", exist_ok=True)\n\nHAKOBU_CSS = \"\"\"\n.src{left:420px !important}\n.brand{right:56px !important;top:44px !important}\n\"\"\"\nscenes.CSS = scenes.CSS + HAKOBU_CSS\n\nscene = scenes.page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">π0.5 をシミュレータで動かしたときの GPU メモリ(実使用の最大)</div>\n <div class=\"hero\">9,735<small>MiB</small></div>\n <div class=\"h2\" style=\"margin-top:36px\">起動時に確保された量の <span class=\"red\">約28%</span></div>\n</div>\n<div class=\"src\">出典:Pixels to Motors 実測(GPU:NVIDIA A40、π0.5-LIBERO)</div>\"\"\",\n tag=\"<b>2</b>買う前に借りて測る\")\n\nSUBCSS = \"\"\"*{margin:0;padding:0}html,body{width:1920px;height:1080px;background:transparent}\nsvg{position:absolute;left:0;top:0}\n.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}\"\"\"\nSUB_CX, SUB_MAXW = 1090, 1380\n\nMEASURE = \"\"\"() => [...document.querySelectorAll('.tag,.brand,.kicker,.hero,.h1,.h2,.h3,.src,.wrap *')]\n .filter(e => e.getBoundingClientRect().width > 0)\n .map(e => { const r = e.getBoundingClientRect();\n return [e.className || e.tagName, [r.left, r.top, r.right, r.bottom]]; })\"\"\"\n\nboxes = []\nwith sync_playwright() as p:\n b = p.chromium.launch()\n pg = b.new_page(viewport={\"width\": 1920, \"height\": 1080})\n pg.set_content(scene)\n pg.wait_for_timeout(100)\n pg.screenshot(path=\"png/static.png\")\n boxes += [[f\"図:{n}\", r] for n, r in pg.evaluate(MEASURE)]\n for k, s in enumerate(subs):\n lines = s.split(\"\\n\")\n assert all(len(ln) <= 26 for ln in lines), s\n svg_lines = \"\".join(f'<text x=\"{SUB_CX}\" text-anchor=\"middle\" class=\"t\">{html.escape(ln)}</text>' for ln in lines)\n pg.set_content(f\"\"\"<!doctype html><html><head><meta charset=\"utf-8\"><style>{SUBCSS}</style></head><body>\n<svg id=\"s\" width=\"1920\" height=\"1080\" viewBox=\"0 0 1920 1080\">{svg_lines}</svg></body></html>\"\"\")\n rects = pg.evaluate(f\"\"\"() => {{\n const ts=[...document.querySelectorAll('text.t')]; const lh=72; const n=ts.length;\n 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)); }}\n ts.forEach((t,k)=>{{ t.style.fontSize=fs+'px'; t.setAttribute('y', 1080-62-(n-1-k)*lh); }});\n return ts.map(t => {{ const r=t.getBoundingClientRect(); return [r.left-6, r.top-6, r.right+6, r.bottom+6]; }});\n }}\"\"\")\n boxes += [[f\"字幕{k}-{j}\", r] for j, r in enumerate(rects)]\n pg.screenshot(path=f\"png/sub_{k}.png\", omit_background=True)\n b.close()\njson.dump(boxes, open(\"png/boxes.json\", \"w\"), ensure_ascii=False)</code></pre><h3 id=\"index_Fr7ec4rQ\">edit_rules.py(video_rules.md に書き足したスクリプト)</h3><p>ルールの文書は、文章を打ち直すと途中が抜けることがあるので、置き換える文が文書の中にちょうど1か所あることを確かめてから置き換えるスクリプトで書き足しました。このスクリプトを動かしたあと、試作のフォルダの説明の1行だけを、次の sed で置き換えています。</p><pre><code class=\"language-python\">\"\"\"video_rules.md にハコブくんのナレーションで決めたことを書き足す(置換は1か所ずつ一致を確かめる)\"\"\"\nimport sys\n\nsrc, dst = sys.argv[1], sys.argv[2]\ns = open(src, encoding=\"utf-8\").read()\n\n\ndef rep(old, new):\n global s\n assert s.count(old) == 1, f\"一致が1か所ではない: {old[:40]}\"\n s = s.replace(old, new)\n\n\n# --- 冒頭 ---\nrep(\"最終更新: 2026-10-11\\n状態: 試作1本目「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」を作りながら決めたことをまとめた初版。まだ決まっていない項目は8節にまとめている。\",\n \"最終更新: 2026-10-12\\n状態: 試作1本目「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」を作りながら決めたことをまとめた初版に、ナレーションをハコブくんにする決定(2026-10-12)を足した版。まだ決まっていない項目は8節にまとめている。\")\n\n# --- 1節 ---\nrep(\"| 1 | 自己紹介 | 〜0:55 | ロゴのテロップを出し、毎回同じ文言で名乗る(2節の文言) |\",\n \"| 1 | 自己紹介 | 〜0:55 | ロゴと、エイイチの名前のテロップを出す。ハコブくんが毎回同じ文言で番組とエイイチを紹介する(2節の文言) |\")\nrep(\"| 8 | 見立て | 〜12:30 | 「ここからが僕の一番言いたいことです」と言ってから意見を話す |\",\n \"| 8 | 見立て | 〜12:30 | 「ここからはエイイチの見立てです」と言ってから意見を話す(意見が誰のものかをはっきりさせる) |\")\nrep(\"| 自分の立ち位置を明かす | 3 結論と立場、または本編1節の頭 | 「僕自身、仕事で使っていて、今回は得をする側なんです」「僕は〜否定派じゃないです。技術としては本物」 |\",\n \"| 自分の立ち位置を明かす | 3 結論と立場、または本編1節の頭 | 「僕自身、仕事で使っていて、今回は得をする側なんです」「僕は〜否定派じゃないです。技術としては本物」。このチャンネルではハコブくんが話すので「エイイチ自身、〜」と言い換える |\")\nrep(\"数字の言い方は5本とも同じだった。\",\n \"台本はすべてハコブくんが話す前提で書く。エイイチの体験と意見は「エイイチは」「エイイチの机の上では」と三人称で言う。\\n\\n数字の言い方は5本とも同じだった。\")\n\n# --- 2節 ---\nrep(\"「僕の機体とソフトの組み合わせでは」を付ける。\", \"「エイイチの機体とソフトの組み合わせでは」を付ける。\")\nrep(\"「配送開始を示す公式発表は、僕が確認した範囲では見つかっていません」\",\n \"「配送開始を示す公式発表は、エイイチが確認した範囲では見つかっていません」\")\nrep(\"自己紹介の文言は「僕は本業のデザイナーをしながら、ロボティクスの研究と開発をしています。自宅の机の上でロボットアームを動かして、その記録を公開しています。」に固定する。「秋田」「29年」「毎日」は入れない。\",\n \"自己紹介の文言は、ハコブくんが話す形の「この番組は、本業のデザイナーをしながらロボティクスの研究と開発をしているエイイチが、自宅の机の上でロボットアームを動かした記録をもとにしています。」に固定する(2026-10-12 に、それまでの一人称の文言を言い換えた。文言を変えるときはエイイチが決める)。「秋田」「29年」「毎日」は入れない。\")\n\n# --- 3節 ---\nrep(\"\"\"## 3. ナレーション(現状は仮音声)\n\nいまは仮の合成音声で作っている。本番の声は未決(8節)。\n\n- **エンジン:** Open JTalk、声は nitech_jp_atr503_m001(男声)。設定は 速さ 1.08、音の高さ -1、声質 0.55\"\"\",\n\"\"\"## 3. ナレーション(ハコブくん・声は仮音声)\n\n### 3-1. ナレーションはハコブくんが話す(2026-10-12 決定)\n\n- ナレーションは、Pixels to Motors のキャラクターのハコブくん(病院で物を運ぶロボット)が担当する。エイイチ本人の声と、本人の声の複製は使わない(本人の声が通らないため。複製しても声の性質はそのまま写る)\n- 動画の目的は、見た人から仕事の相談や声がけをもらうこと。キャラクターの後ろで本人が見えなくならないよう、次の3つを必ず入れる\n - 自己紹介の場面で、エイイチの名前のテロップと実機の映像を出す\n - 見立ての節は「ここからはエイイチの見立てです」と言ってから入る\n - 概要欄にプロフィールと連絡先を置く\n- 概要欄に「ナレーションは AI 音声です」と書く\n- 声は、他のチャンネルと同じ声(VOICEVOX のキャラクターの声など)を使わず、ハコブくん専用の声にする。「ハコブくん=有名キャラクターの声のロボット」と聞かれないようにするため。候補は8節\n- 高すぎない声で、ゆっくり丁寧に話す。経営者が13分聞いて疲れないことを基準にする。キャラクターらしい決まった語尾は本編で使わない(使うならジングルと最初の一言だけ)\n\n### 3-2. 仮音声の設定(ハコブくんの声が決まるまで)\n\n- **エンジン:** Open JTalk、声は nitech_jp_atr503_m001(男声)。設定は 速さ 1.0(2026-10-12 に 1.08 から下げた。ハコブくんの口の動きと合わせると速すぎたため)、音の高さ -1、声質 0.55\"\"\")\n\n# --- 4-1 ---\nrep(\"\"\"- 余白は左右140px。下の250pxは字幕のために空ける\n- 左上に章のタグ(章番号はオレンジ、見出しは薄い文字)、右上にロゴ(高さ34px)\"\"\",\n\"\"\"- 余白は左右140px。下の250pxは字幕のために空ける\n- 左上に章のタグ(章番号はオレンジ、見出しは薄い文字。左端から140px)、右上にロゴ(高さ34px。右端から56px、上端から44px。2026-10-12 に右140px・上46pxから端へ寄せた)\n- **ハコブくんの区画:** 左下に、ハコブくんの上半身を右向きで置く(全身より図の邪魔にならないため、上半身を基本にする)。体は幅340pxで x=20〜360px、画面の下端に接して置く(上端は y=766px)。区画は体の範囲に余白20pxを足した x=0〜380px、y=746〜1080px。この区画には字幕・図・出典・テロップを重ねない。体の画像を差し替えたら、区画は置いた体の範囲から計算し直す(スクリプトが自動で計算する)\n- **図の範囲:** 左右140pxのまま使う。ハコブくんの区画に重なるかどうかは、場面ごとに書き出しの前に自動で確かめる。図・文字の位置はブラウザで描いたときの実際の位置を測り、区画と1pxでも重なった場面は、場面の名前を出して書き出しを止める。止まった場面は、図を区画より上に収めるか、左端を420pxに寄せて作り直す\"\"\")\n\n# --- 4-2 ---\nrep(\"- 新しい色を足すときは、色覚の違いがあっても見分けられるかを検証してから使う(dataviz スキルの validate_palette.js。背景色 #F5F5F0 を指定する)\",\n \"- 新しい色を足すときは、色覚の違いがあっても見分けられるかを検証してから使う(dataviz スキルの validate_palette.js。背景色 #F5F5F0 を指定する)\\n- ハコブくんの体と顔の色(目の白 #F6F8FC、口の水色 #57B5F8、側面のリングの緑)は、この表と4-5の「オレンジと青以外の色を足さない」の対象外。図や文字には使わない\")\n\n# --- 4-5 ---\nrep(\"- 影、グラデーション\\n\", \"- 影、グラデーション(ハコブくんにも影は付けない。目のまわりのにじむ光と側面のリングの光は、ハコブくん自身が光っている表現なので例外)\\n\")\n\n# --- 4-6 と 4-7 ---\nrep(\"数字を出す画面には、左下(字幕の上)に出典を24pxの薄い文字で必ず入れる。\",\n \"数字を出す画面には、字幕の上に出典を24pxの薄い文字で必ず入れる。位置は左端から420px(左下のハコブくんの区画を避ける。もとは140px)。\")\nrep(\"\"\"模式図には「図は模式図」と入れる。\n\n---\"\"\", \"\"\"模式図には「図は模式図」と入れる。\n\n### 4-7. ハコブくんの描き方と動き\n\n試作(2026-10-12、`_制作ベース/hakobu_試作/`)で決めた作り方。数値はスクリプトの先頭近くにまとめてある。\n\n**体**\n- 元の動画(ハコブくんが病室にいる生成動画)の1コマから体を切り抜き、左右反転して右向きにする(画面の中央を向く)\n- 頭の上は、後ろの白い枕と重なっていて切り抜きが欠ける。元の動画では、頭の上は「画面の上の縁が低く、奥側がもう一段持ち上がった」形をしている。切り抜きがはっきり頭と判断した範囲を輪郭に使い、縁のギザギザだけをなめらかにする。色は元のコマの画素で戻す(欠けた部分を直線や凸包で埋めると、奥の段差が消えて枕まで頭として塗ってしまう)\n- 側面の緑のリングは、元の絵の画素が粗いので一度消し、楕円をコードで描き直す\n\n**顔(すべてコードで描く)**\n- 目: 白(#F6F8FC)の縦長のカプセル形(上下の端が半円)。まわりに白い光が少しにじむ。光は画面の外に出さない\n- 口: 水色(#57B5F8)。閉じた口(両端が丸い太い弧)・小(上が広く下が丸い形)・中・大(角の丸い三日月形)の4段階\n- 顔は、ディスプレイの四隅に合わせて射影し、ディスプレイと同じ傾きで描く。四隅は、上下左右の辺のまっすぐな部分にそれぞれ直線を当てはめた交点で決める(角が丸いので、角の点を直接取ると傾きを読み違える)\n- 本番では、体だけを先に反転し、顔は向きどおりに描く(試作は顔を描いてから体ごと反転している。左右で形が違う表情が逆になるため)\n\n**動き**\n- 口: ナレーションの音量で4段階を選ぶ。口の形は2フレームごと(毎秒15回)に、1段階ずつしか変えない。閉じた口と大きい口の間に、必ず小と中がはさまる\n- 目: 2.8秒の周期でゆっくり上下する。口が開くと0.1秒遅れて少し上がる。文の頭で、0.3秒だけ小さく跳ねる\n- まばたき: 2〜4.5秒の間でばらつかせる。文を言い終わった直後にも1回入れる\n- 側面のリング: 顔とは別の3.7秒の周期で、息をするように明滅する(明滅の強さもゆっくり不規則に変える)。声には0.2秒遅れて明るくなり、話し終わるとゆっくり暗くなる。声への反応の強さは文ごとに変える。文の頭と、大きく口を開いた瞬間だけは顔とそろえて強く光る(大きく口を開いたときは、3回に1回ほど光らない)。話していない間も、ときどき弱くまたたく\n\n**表情**\n- 1文ごとに、台本で表情を指定する(narration.py の1文の行に「表情」の欄を足す)\n- 目と頬は表情で決める。口は、話している間はどの表情でも音量で動かし、話していない間だけその表情の口に戻す(「_」の口のまま話すと、声と口が合わなくなるため)\n- 使い分けの目安(1本作って確定する。8節)\n\n| 表情 | 使う場面 |\n|---|---|\n| 丸い目+開いた口 | 基本。説明の文はほぼこれ |\n| 丸い目+小さな笑み | 間、聞き手に戻るところ |\n| 驚いた目+「o」の口 | 今日の問い、意外な数字が出るところ |\n| ジト目 | 「本当にそうでしょうか」と反論を出すところ |\n| 目を閉じる | 「まとめると」、考えている間 |\n| 困り眉 | 失敗や限界の話 |\n| >▽< | 結論が言えたところ、うまくいった実験 |\n| 「III_III」 | 計算中・データを読み込んでいる場面 |\n| ウインク、キラキラ目、照れ、>_<、目が泳ぐ | アウトロや軽い一言だけ |\n\n経営者向けの動画なので、1本で使う表情は上の6つ前後にとどめる。\n\n---\"\"\")\n\n# --- 5節 ---\nrep(\"- 2行まで、1行31字以内。改行は読点の位置で入れる。読点がなければ助詞の後ろで割る\\n- 位置は画面の下から62px\",\n \"- 2行まで、1行26字以内(ハコブくんの区画を避けるため。もとは31字)。改行は読点の位置で入れる。読点がなければ助詞の後ろで割る\\n- 位置は画面の下から62px。横はハコブくんの区画の右側(x=400〜1780px)の中央(x=1090px)にそろえ、1行の幅は1380pxまで\")\n\n# --- 6節 ---\nrep(\"- 映像は 1920×1080 に拡大して切り抜く。字幕の後ろに黒50%の帯(高さ230px)を敷く\",\n \"- 映像は 1920×1080 に拡大して切り抜く。字幕の後ろに黒50%の帯(高さ230px)を敷く。帯はハコブくんの区画の右(x=380px から右)だけに敷く\")\nrep(\"- 名前のテロップ: 薄い地に、ロゴ(高さ30px)・名前60px・肩書き30px(「デザイナー/ロボティクスの研究・開発」)。左の太い線は付けない\",\n \"- 名前のテロップ: 薄い地に、ロゴ(高さ30px)・名前60px・肩書き30px(「デザイナー/ロボティクスの研究・開発」)。左の太い線は付けない。ハコブくんの区画の外に置く\")\n\n# --- 7節 ---\nrep(\"\"\" 5. `assemble.py` で1文ずつ映像を作ってつなぎ、音声と合わせる(字幕の srt も出る)\n 6. 確認用に圧縮する\"\"\",\n\"\"\" 5. `assemble.py` で1文ずつ映像を作ってつなぎ、音声と合わせる(字幕の srt も出る)\n 6. 確認用に圧縮する\n- **ハコブくんの試作(`_制作ベース/hakobu_試作/`):** `python3 make_demo.py upper`(全身版は `full`)で、体の切り抜き・顔の描画・口と目とリングの動き・区画との重なり確認・書き出しまでを行う。図と字幕は `render_static.py` が本番の `scenes.py` を読み込んで描く(`base/` に scenes.py と logo_dark.png の写しを置く)。本番の render.py・assemble.py への組み込みはまだ(8節)\"\"\")\n\n# --- 8節 ---\nrep(\"| ナレーションの声 | Open JTalk の仮音声。抑揚が機械的 | 本人の声で録るか、品質の高い音声合成(VOICEVOX など。利用規約とクレジット表記の条件を確認)にするか。srt に1文ごとの時間が入っているので、それに合わせて録れば映像はそのまま使える |\",\n \"| ハコブくんの声 | ナレーションはハコブくんに決定(3-1)。声は Open JTalk の仮音声 | ハコブくん専用の声を、ElevenLabs の Voice Design(文章で声の特徴を書くと新しい声を作る機能。日本語の出来と、作った声を商用で使う条件は試して確かめる)で作るか、声優に依頼して作るか(毎回読んでもらうか、本人の同意を取って声を複製する。声の権利が一番はっきりする) |\\n| ハコブくんの体の画像 | 生成動画の1コマを切り抜き、頭の上とリングを修整して使っている | 同じ構図(斜め・右向き)で、無地の背景・リングなしの画像を生成し直すか。生成し直せば頭とリングの修整が要らなくなる |\\n| ハコブくんの本番組み込み | 試作(`hakobu_試作/`)で動きと配置を確認した段階 | render.py・assemble.py に組み込む。narration.py に「表情」の欄を足す。表情集の顔をコードで描く |\\n| 表情の使い分け | 4-7の目安の表のみ | 1本作って、使う表情と場面を確定する |\")\nrep(\"| ジングル | 文言だけ(「Pixels to Motors。こんにちは、エイイチです。」) | 音とロゴの動きを付けるか |\",\n \"| ジングル | 文言だけ(「Pixels to Motors。こんにちは、エイイチです。」) | ハコブくんが話す形の文言に言い換える。音とロゴの動きを付けるか |\")\n\n# --- 更新履歴 ---\nrep(\"## 更新履歴\\n\\n\",\n \"## 更新履歴\\n\\n- 2026-10-12: ナレーションをハコブくんにする決定を反映。3-1(ハコブくんが話す・本人を見せる3つの決まり・AI音声の明記・専用の声)と4-7(ハコブくんの描き方と動き・表情)を新設。4-1 にハコブくんの区画(左下・上半身・右向き)と、図の範囲を140pxのまま場面ごとの自動の重なり確認に任せる決まりを追加。ロゴを右56px・上44pxへ寄せた。字幕を1行26字・中央x=1090pxへ、出典を左420pxへ移した。ハコブくんの色を4-2・4-5の対象外にした。台本の一人称(僕)をエイイチの三人称に言い換え、自己紹介の固定文言をハコブくんが話す形にした。仮音声の速さを1.08から1.0に下げた。8節にハコブくんの声・体の画像・本番組み込み・表情の使い分けを追加。\\n\")\n\nopen(dst, \"w\", encoding=\"utf-8\").write(s)\nprint(\"ok\", len(s))</code></pre><pre><code class=\"language-bash\">sed -i 's#図と字幕は `render_static.py` が本番の `scenes.py` を読み込んで描く(`base/` に scenes.py と logo_dark.png の写しを置く)。#図と字幕は `render_static.py` が本番の `scenes.py` と `logo_dark.png`(1つ上の `_制作ベース` にあるもの)を読み込んで描く。入力の画像(`cut_raw.png`・`full/g_05.png`)も同じフォルダに置いてある。#' rules/video_rules.md</code></pre><p></p><h2 id=\"index_NfGgHGVW\">実行ログ</h2><h3 id=\"index_s_TBaVYv\">元の生成動画の情報(ffprobe)</h3><pre><code>stream|codec_type=video|width=848|height=1056|r_frame_rate=24/1\nstream|codec_type=audio|r_frame_rate=0/0\nstream|codec_type=video|width=848|height=1056|r_frame_rate=90000/1\nformat|duration=10.041667</code></pre><h3 id=\"index_ts_1basB\">ディスプレイ・目・口・リングの測定(measure_face.py)</h3><pre><code>ディスプレイの外接矩形 (x, y, 幅, 高さ): [325, 349, 183, 151]\n白の領域: x=482 y=344 幅=29 高さ=52 面積=172\n白の領域: x=362 y=395 幅=22 高さ=46 面積=799\n白の領域: x=305 y=396 幅=170 高さ=124 面積=2301\n白の領域: x=446 y=396 幅=23 高さ=48 面積=885\n水色の領域: x=378 y=450 幅=68 高さ=40 面積=2115\n口の色(中央値 RGB): [87, 181, 248]\nリングの範囲: 576 548 629 681 画素数 1793\n体の範囲: 291 328 648 904\n頭の上の行ごとの画素数(アルファ>200 / >60 / >10):\n y=320: 0 / 0 / 0\n y=324: 0 / 0 / 36\n y=328: 0 / 42 / 151\n y=332: 33 / 77 / 183\n y=336: 56 / 95 / 212\n y=340: 70 / 167 / 232\n y=344: 198 / 237 / 249\n y=348: 240 / 250 / 261\n y=352: 251 / 257 / 264\n y=356: 258 / 264 / 270\n上の辺: 点 74 個、傾き 0.18°\n下の辺: 点 74 個、傾き 2.16°\n左の辺: 点 60 個、傾き 5.97°\n右の辺: 点 61 個、傾き 5.67°\nディスプレイの四隅(左上・右上・右下・左下): [[336.8, 348.8], [510.2, 349.3], [495.1, 501.2], [321.5, 494.6]]\n参考・角の点を直接取った四隅(eps=9.0): [[504, 360], [488, 496], [327, 485], [346, 354]]</code></pre><h3 id=\"index_wL1s2RcT\">上半身版の書き出し(python3 make_demo.py upper)</h3><pre><code>ハコブくんの区画: (0, 746, 380, 1080) 体の位置: (20, 766, 360, 1080)\n重なり確認: 文字・図・ロゴ 12 個、区画との重なり 0 件\nframes 398 seconds 13.27\n終了コード: 0</code></pre><h3 id=\"index_qWOd32_u\">全身版の書き出しが、重なりの確認で止まった記録(python3 make_demo.py full)</h3><pre><code>ハコブくんの区画: (0, 580, 353, 1080) 体の位置: (47, 600, 333, 1060)\n 重なり: 図:h2 [140, 589.40625, 1780, 672.59375]\n区画に重なる要素があるため中止しました\n終了コード: 1</code></pre><h3 id=\"index_0eceRLsx\">書き出した上半身版の動画の情報(ffprobe)</h3><pre><code>stream|codec_name=h264|width=1920|height=1080|r_frame_rate=30/1\nstream|codec_name=aac|sample_rate=48000|r_frame_rate=0/0\nformat|duration=13.266667|size=530991\n</code></pre><h3 id=\"index_C2dvPBTh\">図解用に時間の値を書き出した記録(python3 make_demo.py upper --dump)</h3><pre><code>timing.json に書き出しました\nフレーム数 398 / 文ごとの開始と発話終了のフレーム [[9, 51], [62, 195], [216, 371]] / リングの明るさの範囲 0.15 〜 1.0 / まばたきのフレーム数 24</code></pre><p></p><h2 id=\"index_Saxw8HVI\">参考にしたページ</h2><ul><li><p><a href=\"https://elevenlabs.io/pricing\" data-has-link=\"true\">ElevenLabs:Pricing</a></p></li><li><p><a href=\"https://elevenlabs.io/docs/product-guides/voices/voice-design\" data-has-link=\"true\">ElevenLabs:Voice Design</a></p></li><li><p><a href=\"https://voicevox.hiroshiba.jp/term/\" data-has-link=\"true\">VOICEVOX:ソフトウェア利用規約</a></p></li><li><p><a href=\"https://support.google.com/youtube/answer/3376882?hl=ja\" data-has-link=\"true\">YouTube ヘルプ:オーディオライブラリの音楽と効果音を使う</a></p></li><li><p><a href=\"https://dova-s.jp/_contents/license/\" data-has-link=\"true\">DOVA-SYNDROME:音源利用ライセンス</a></p></li><li><p><a href=\"https://soundeffect-lab.info/agreement/\" data-has-link=\"true\">効果音ラボ:利用規約</a></p></li><li><p><a href=\"https://www.epidemicsound.com/pricing/\" data-has-link=\"true\">Epidemic Sound:Pricing</a></p></li><li><p><a href=\"https://github.com/danielgatis/rembg\" data-has-link=\"true\">rembg(GitHub)</a></p></li></ul>", "toc": [{"id": "index_Wy0UmY2h", "text": "動画の企画5本は、見た人が「この人に何を頼めるか」が分かる話を選んだ"}, {"id": "index__9qJii4D", "text": "ナレーション・BGM・効果音は、2026年10月11日時点の公式の条件で比べた"}, {"id": "index_F7VUaN1Y", "text": "ナレーションは、本人の声でも声の複製でもなく、ハコブくんに決めた"}, {"id": "index_O4m3dwLv", "text": "ハコブくんは画面の左下に置き、生成した10秒の動画の1コマから体を切り抜いた"}, {"id": "index_g2Fv9MC5", "text": "顔の目と口は、元の絵を消してコードで描き直した"}, {"id": "index_Ufl5ndIQ", "text": "口は音量で4段階に切り替え、1回に1段階ずつしか動かさない"}, {"id": "index_V8JsKfRO", "text": "目は2.8秒の周期で上下し、口より0.1秒遅れて上がる"}, {"id": "index_W02dAGsw", "text": "側面のリングは、元の絵を消して楕円を描き直し、顔と別の周期で光らせた"}, {"id": "index_tXF5W1d5", "text": "目はカプセル形にし、顔はディスプレイの辺に直線を当てはめて傾けた"}, {"id": "index_fNoBvJtt", "text": "頭の上は、凸包で埋めると奥の段差が消えて枕まで塗っていた"}, {"id": "index_boeVOUIR", "text": "ハコブくんの区画に字幕や図が重なったら、書き出す前に止める"}, {"id": "index_ZOcuqpzA", "text": "全身版より上半身版の方が、図を読む邪魔をしなかった"}, {"id": "index_H1Z4xux4", "text": "決めたことは video_rules.md に3-1節と4-7節として書き足した"}, {"id": "index_OMOijwC5", "text": "まだ決まっていないのは、ハコブくんの声・体の画像・本番への組み込み・表情の4つ"}, {"id": "index_AtrH00k0", "text": "試作スクリプトの全文"}, {"id": "index_NfGgHGVW", "text": "実行ログ"}, {"id": "index_Saxw8HVI", "text": "参考にしたページ"}]}
\ No newline at end of file
diff --git a/src/data/records/figma-design-token-consolidation.json b/src/data/records/figma-design-token-consolidation.json
index 2f54570..2668921 100644
--- a/src/data/records/figma-design-token-consolidation.json
+++ b/src/data/records/figma-design-token-consolidation.json
@@ -1 +1 @@
-{"day": "LOG303", "series": "ロボティクスデザイン", "published": "2026-07-31T01:07:33.000Z", "title": "Figmaのデザイントークンを整理する | 生hex 3,977箇所を変数化し、コントラスト不合格をゼロにするまで", "slug": "figma-design-token-consolidation", "cover": null, "thumb": "/thumbs/r-figma-design-token-consolidation.html", "lang": "JP", "description": "Figma MCPで既存UIの色を全て変数化。生hex3,977箇所をゼロにし、35色を17色へ統合、テキスト1,635件のコントラストをWCAG AA適合させた作業ログ。", "updated": "2026-07-31T01:08:12.000Z", "body": "<!--TOC--><p></p><p>ChromeのClaudeにFigmaコネクタを追加した。繋いだ先にあったのは、病院内で薬剤を運ぶ搬送ロボットのUIデザイン一式だった。アプリ、タブレット、PCダッシュボード、スタイルガイド。合計で約5,000ノードある。</p><p>このファイルには変数(デザイントークン)が既に整備されていた。プリミティブ29個、セマンティックトークン20個、Light/Nightの2モード。設計としてはかなり整っている。</p><p>にもかかわらず、画面はそれを1つも使っていなかった。全部が生のhex値で組まれていた。</p><p>そこから、色を変数に繋ぎ直し、重複した色を潰し、コントラストの不足を直すまでの記録を残す。途中で自分の書いたスクリプトが45ノードを壊した話も含めて、全量で書く。</p><p></p><h2 id=\"index_HG1_L3sK\">Figmaコネクタが読み取ったもの</h2><p>コネクタを繋いで、対象フレームのURLを渡すと、<code>get_design_context</code> がリファレンスコードとスクリーンショットとメタデータを返してくる。返ってきたのは390×1048のアプリ画面だった。</p><p>返却されたコードは React + Tailwind の形をしている。ただしこれは「参照用」であって、そのまま使うものではない。重要なのは、この出力から<strong>実際に使われている色が全て生のhexで書かれている</strong>と分かることだった。</p><pre><code class=\"language-jsx\"><div className=\"bg-[#f8f9fa] content-stretch flex flex-col items-start relative size-full\" data-node-id=\"12580:28\" data-name=\"Pharmacy Robot App\">\n <div className=\"bg-white content-stretch drop-shadow-[0px_2px_4px_rgba(0,0,0,0.05)] flex flex-col gap-[16px] items-start px-[20px] py-[16px] relative shrink-0 w-full\" data-node-id=\"12580:29\" data-name=\"Header\">\n <p className=\"[word-break:break-word] font-['Noto_Sans_JP:Bold'] font-bold leading-[24px] relative shrink-0 text-[#1a1a1a] text-[20px] whitespace-nowrap\" data-node-id=\"12580:31\">\n 薬剤搬送ロボット\n </p></code></pre><p><code>bg-[#f8f9fa]</code>、<code>text-[#1a1a1a]</code>。変数を参照していれば、ここには変数名が出る。出ていないということは、デザイン側で色が直接指定されているということだった。</p><p>この時点で「変数が無い」と判断したが、これは誤りだった。次の節でそれが分かる。</p><p></p><h2 id=\"index_S4iJx9Qp\">変数は存在していた。画面が使っていなかっただけだった</h2><p>ファイル側の状態を実測するスクリプトを書いた。何かを変更する前に、まず現物を読む。</p><pre><code class=\"language-javascript\">const collections = await figma.variables.getLocalVariableCollectionsAsync();\nconst colInfo = collections.map(c => ({\n name: c.name,\n id: c.id,\n varCount: c.variableIds.length,\n modes: c.modes.map(m => m.name)\n}));\n\nconst pages = figma.root.children.map(p => ({ id: p.id, name: p.name }));\n\nconst node = await figma.getNodeByIdAsync(\"12580:28\");\nconst nodeInfo = node ? {\n name: node.name,\n type: node.type,\n width: node.width,\n height: node.height,\n pageName: (() => { let n = node; while (n && n.type !== 'PAGE') n = n.parent; return n ? n.name : null; })()\n} : null;\n\nconst localStyles = {\n paint: (await figma.getLocalPaintStylesAsync()).length,\n text: (await figma.getLocalTextStylesAsync()).length,\n effect: (await figma.getLocalEffectStylesAsync()).length\n};\n\nreturn { collections: colInfo, pages, nodeInfo, localStyles };</code></pre><p>返ってきた実測値。</p><pre><code class=\"language-json\">{\n \"collections\": [\n {\"name\":\"Primitives(プリミティブ)\",\"id\":\"VariableCollectionId:645:51019\",\"varCount\":29,\"modes\":[\"Mode 1\"]},\n {\"name\":\"Tokens(トークン)\",\"id\":\"VariableCollectionId:1941:108641\",\"varCount\":20,\"modes\":[\"Light Mode\",\"Night Mode\"]},\n {\"name\":\"html.to.design\",\"id\":\"VariableCollectionId:5378:709\",\"varCount\":319,\"modes\":[\"Mode 1\"]}\n ],\n \"pages\": [\n {\"id\":\"0:1\",\"name\":\"Pixels to Motors\"},\n {\"id\":\"12580:25\",\"name\":\"テスト\"},\n {\"id\":\"1394:101746\",\"name\":\"ハコブくん4コママンガ\"},\n {\"id\":\"4462:4\",\"name\":\"サムネイル保管庫\"}\n ],\n \"nodeInfo\": {\"name\":\"Pharmacy Robot App\",\"type\":\"FRAME\",\"width\":390,\"height\":1048,\"pageName\":\"テスト\"},\n \"localStyles\": {\"paint\":82,\"text\":146,\"effect\":0}\n}</code></pre><p><code>Primitives</code> が29個、<code>Tokens</code> が20個。Tokensには Light Mode と Night Mode の2モードがある。中身を読むと、Tokens側は全てプリミティブへのエイリアスで構成されていた。</p><pre><code>Surface/Default → Light: Neutral/Off White / Night: Neutral/Gray Dark\nSurface/Container → Light: Neutral/White / Night: Neutral/Black\nText/Primary → Light: Neutral/Black / Night: Neutral/White\nText/Secondary → Light: Neutral/Gray Dark / Night: Neutral/Gray Pale\nBorder/Default → Light: Neutral/Gray Pale / Night: Neutral/Gray Dark\nAction/Primary/Bg → Light: Primary/Blue / Night: Primary/Light Blue\nStatus/Success/Fg → Light: Success/Success / Night: Success/Success Light</code></pre><p>素の色を持つ層(プリミティブ)と、意味を持つ層(トークン)が分かれていて、後者が前者を参照する。教科書通りの2層構造だった。</p><p>つまり問題は「トークンが無い」ではなく「<strong>画面がトークンを使っていない</strong>」だった。作るのではなく、繋ぐ作業になる。</p><p></p><h2 id=\"index_zgKsR6Q4\">先にコントラストを測ると、4色が基準に届いていなかった</h2><p>繋ぐ前に、そのまま繋いでいいのかを確認した。色をコントラスト比で測ると、4箇所がWCAG AA基準(通常テキストで4.5:1)に届いていない。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>箇所</p></th><th colspan=\"1\" rowspan=\"1\"><p>色</p></th><th colspan=\"1\" rowspan=\"1\"><p>背景</p></th><th colspan=\"1\" rowspan=\"1\"><p>比率</p></th><th colspan=\"1\" rowspan=\"1\"><p>判定</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>「75%」</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>#4CAF50</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>白</p></td><td colspan=\"1\" rowspan=\"1\"><p>2.78:1</p></td><td colspan=\"1\" rowspan=\"1\"><p>不合格</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>受け取り完了ボタン</p></td><td colspan=\"1\" rowspan=\"1\"><p>白</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>#4CAF50</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>2.78:1</p></td><td colspan=\"1\" rowspan=\"1\"><p>不合格</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ボトムナビ非アクティブ</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>#9E9E9E</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>白</p></td><td colspan=\"1\" rowspan=\"1\"><p>2.68:1</p></td><td colspan=\"1\" rowspan=\"1\"><p>不合格</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>「配送中」バッジ</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>#E65100</code></p></td><td colspan=\"1\" rowspan=\"1\"><p><code>#FFF3E0</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>3.46:1</p></td><td colspan=\"1\" rowspan=\"1\"><p>不合格</p></td></tr></tbody></table></div><p>薬剤を運ぶロボットのUIなので、状態が読めないことは実務上まずい。ここは繋ぐ前に色そのものを直す。</p><p>暗い側の色を2つ追加し、既存のセマンティックトークンの参照先を差し替えた。</p><pre><code class=\"language-javascript\">const hex = (s) => ({\n r: parseInt(s.slice(1,3),16)/255,\n g: parseInt(s.slice(3,5),16)/255,\n b: parseInt(s.slice(5,7),16)/255\n});\n\nconst prim = await figma.variables.getVariableCollectionByIdAsync(\"VariableCollectionId:645:51019\");\nconst tok = await figma.variables.getVariableCollectionByIdAsync(\"VariableCollectionId:1941:108641\");\nconst primMode = prim.modes[0].modeId;\nconst light = tok.modes.find(m => m.name === \"Light Mode\").modeId;\nconst night = tok.modes.find(m => m.name === \"Night Mode\").modeId;\n\nconst lookup = {};\nfor (const cid of [prim.variableIds, tok.variableIds].flat()) {\n const v = await figma.variables.getVariableByIdAsync(cid);\n lookup[v.name] = v;\n}\n\nconst created = [];\n\nconst successDark = figma.variables.createVariable(\"Success/Success Dark\", prim, \"COLOR\");\nsuccessDark.scopes = [\"ALL_SCOPES\"];\nsuccessDark.setValueForMode(primMode, hex(\"#2E7D32\"));\ncreated.push(successDark.id);\n\nconst deliveryDark = figma.variables.createVariable(\"Delivery/Delivery Dark\", prim, \"COLOR\");\ndeliveryDark.scopes = [\"ALL_SCOPES\"];\ndeliveryDark.setValueForMode(primMode, hex(\"#BF360C\"));\ncreated.push(deliveryDark.id);\n\nlookup[\"Status/Success/Fg\"].setValueForMode(light, { type: \"VARIABLE_ALIAS\", id: successDark.id });\nlookup[\"Status/Delivery/Fg\"].setValueForMode(light, { type: \"VARIABLE_ALIAS\", id: deliveryDark.id });\n\nconst actSuccessBg = figma.variables.createVariable(\"Action/Success/Bg\", tok, \"COLOR\");\nactSuccessBg.scopes = [\"FRAME_FILL\", \"SHAPE_FILL\"];\nactSuccessBg.setValueForMode(light, { type: \"VARIABLE_ALIAS\", id: successDark.id });\nactSuccessBg.setValueForMode(night, { type: \"VARIABLE_ALIAS\", id: lookup[\"Success/Success\"].id });\ncreated.push(actSuccessBg.id);\n\nconst actSuccessFg = figma.variables.createVariable(\"Action/Success/Fg\", tok, \"COLOR\");\nactSuccessFg.scopes = [\"TEXT_FILL\"];\nactSuccessFg.setValueForMode(light, { type: \"VARIABLE_ALIAS\", id: lookup[\"Neutral/White\"].id });\nactSuccessFg.setValueForMode(night, { type: \"VARIABLE_ALIAS\", id: lookup[\"Neutral/Black\"].id });\ncreated.push(actSuccessFg.id);\n\nconst navActive = figma.variables.createVariable(\"Nav/Active\", tok, \"COLOR\");\nnavActive.scopes = [\"TEXT_FILL\", \"SHAPE_FILL\"];\nnavActive.setValueForMode(light, { type: \"VARIABLE_ALIAS\", id: lookup[\"Primary/Dark\"].id });\nnavActive.setValueForMode(night, { type: \"VARIABLE_ALIAS\", id: lookup[\"Primary/Light Blue\"].id });\ncreated.push(navActive.id);\n\nconst navInactive = figma.variables.createVariable(\"Nav/Inactive\", tok, \"COLOR\");\nnavInactive.scopes = [\"TEXT_FILL\", \"SHAPE_FILL\"];\nnavInactive.setValueForMode(light, { type: \"VARIABLE_ALIAS\", id: lookup[\"Neutral/Gray Medium\"].id });\nnavInactive.setValueForMode(night, { type: \"VARIABLE_ALIAS\", id: lookup[\"Neutral/Gray Light\"].id });\ncreated.push(navInactive.id);\n\nreturn {\n createdVariableIds: created,\n repointed: [\"Status/Success/Fg\", \"Status/Delivery/Fg\"],\n primitiveCount: prim.variableIds.length,\n tokenCount: tok.variableIds.length\n};</code></pre><pre><code class=\"language-json\">{\n \"createdVariableIds\": [\"VariableID:12582:2\",\"VariableID:12582:3\",\"VariableID:12582:4\",\"VariableID:12582:5\",\"VariableID:12582:6\",\"VariableID:12582:7\"],\n \"repointed\": [\"Status/Success/Fg\",\"Status/Delivery/Fg\"],\n \"primitiveCount\": 31,\n \"tokenCount\": 24\n}</code></pre><p>変更後の比率。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>箇所</p></th><th colspan=\"1\" rowspan=\"1\"><p>変更</p></th><th colspan=\"1\" rowspan=\"1\"><p>比率</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>「75%」</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>#4CAF50</code> → <code>#2E7D32</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>2.78 → 5.13:1</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>受け取り完了ボタン</p></td><td colspan=\"1\" rowspan=\"1\"><p>背景を <code>#2E7D32</code> へ</p></td><td colspan=\"1\" rowspan=\"1\"><p>2.78 → 5.13:1</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ナビ非アクティブ</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>#9E9E9E</code> → <code>#757575</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>2.68 → 4.61:1</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>「配送中」バッジ</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>#E65100</code> → <code>#BF360C</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>3.46 → 5.11:1</p></td></tr></tbody></table></div><p></p><h2 id=\"index_7WSs2S7Y\">「Noto Sans JP SemiBold」は存在しなかった</h2><p>テキストの色を変えようとしたとき、スクリプトが止まった。</p><pre><code>Error: The font \"Noto Sans JP SemiBold\" could not be loaded.\nAvailable styles for \"Noto Sans JP\": [\"Black\",\"Bold\",\"DemiLight\",\"Light\",\"Medium\",\"Regular\",\"Thin\"]</code></pre><p>ファイルは <code>SemiBold</code> を参照しているが、その字形は環境に存在しない。フォールバックで表示されていただけだった。Figmaは欠落フォントのテキストノードを編集できないので、文字を変える前にフォントを実在するスタイルへ差し替える必要がある。</p><p>ページ内を数えると96ノードが該当した。</p><pre><code class=\"language-javascript\">const page = await figma.getNodeByIdAsync(\"12580:25\");\nawait figma.setCurrentPageAsync(page);\n\nconst texts = page.findAllWithCriteria({ types: [\"TEXT\"] });\n\nconst styleCount = {};\nconst semiBoldNodes = [];\n\nfor (const t of texts) {\n let segs;\n try { segs = t.getStyledTextSegments([\"fontName\"]); } catch (e) { continue; }\n let hit = false;\n for (const s of segs) {\n const key = `${s.fontName.family} ${s.fontName.style}`;\n styleCount[key] = (styleCount[key] || 0) + 1;\n if (s.fontName.style === \"SemiBold\") hit = true;\n }\n if (hit) semiBoldNodes.push(t.id);\n}\n\nreturn {\n totalTextNodes: texts.length,\n styleUsage: styleCount,\n semiBoldNodeCount: semiBoldNodes.length,\n sampleIds: semiBoldNodes.slice(0, 5)\n};</code></pre><pre><code class=\"language-json\">{\n \"totalTextNodes\": 321,\n \"styleUsage\": {\n \"Noto Sans JP Bold\": 79,\n \"Noto Sans JP Medium\": 50,\n \"Noto Sans JP SemiBold\": 96,\n \"Noto Sans JP Regular\": 96\n },\n \"semiBoldNodeCount\": 96,\n \"sampleIds\": [\"12580:49\",\"12580:58\",\"12580:71\",\"12580:92\",\"12580:99\"]\n}</code></pre><p>96ノードを Bold に置換した。置換後の構成。</p><pre><code class=\"language-json\">{\n \"changedCount\": 16,\n \"errors\": [],\n \"finalStyleUsage\": {\n \"Noto Sans JP Bold\": 175,\n \"Noto Sans JP Medium\": 50,\n \"Noto Sans JP Regular\": 96\n }\n}</code></pre><p>Bold が 79 から 175 に増え、SemiBold がゼロになった。ただしこれで見出し=Bold、小見出し=SemiBold、本文=Regular という3段階のうち中間が消えた。ウェイトの階層が2段階になったぶん、画面のメリハリは弱くなっている。フォントが実在しない以上、避けられない副作用だった。</p><p></p><h2 id=\"index_ZUW562PF\">hex完全一致で機械的に繋ぐと、見た目を変えずに生hexがゼロになる</h2><p>ここからが本題の接続作業になる。方針は単純で、<strong>生のhex値と完全に一致する変数があれば、そこへ繋ぐ</strong>。値が同じなので見た目は1ピクセルも変わらない。変わるのは構造だけになる。</p><p>ただし繋ぎ先の選び方には条件がある。同じ <code>#FFFFFF</code> でも、テキストなら <code>Text/Inverse</code>、面なら <code>Surface/Container</code> を選ぶべきで、これは変数の scopes(その変数がどのプロパティに使えるか)で判定できる。</p><pre><code class=\"language-javascript\">const page = await figma.getNodeByIdAsync(\"12580:25\");\nawait figma.setCurrentPageAsync(page);\n\nconst toHex = (c) => '#' + [c.r,c.g,c.b].map(v => Math.round(v*255).toString(16).padStart(2,'0')).join('').toUpperCase();\n\nconst prim = await figma.variables.getVariableCollectionByIdAsync(\"VariableCollectionId:645:51019\");\nconst tok = await figma.variables.getVariableCollectionByIdAsync(\"VariableCollectionId:1941:108641\");\nconst primMode = prim.modes[0].modeId;\nconst light = tok.modes.find(m => m.name === \"Light Mode\").modeId;\n\nconst primHex = {};\nconst cand = [];\nfor (const vid of prim.variableIds) {\n const v = await figma.variables.getVariableByIdAsync(vid);\n const val = v.valuesByMode[primMode];\n if (val && typeof val.r === \"number\") {\n primHex[v.id] = toHex(val);\n cand.push({ v, name: v.name, hex: toHex(val), scopes: v.scopes, layer: 0 });\n }\n}\nfor (const vid of tok.variableIds) {\n const v = await figma.variables.getVariableByIdAsync(vid);\n const val = v.valuesByMode[light];\n const h = val && val.type === \"VARIABLE_ALIAS\" ? primHex[val.id] : (val && typeof val.r === \"number\" ? toHex(val) : null);\n if (h) cand.push({ v, name: v.name, hex: h, scopes: v.scopes, layer: 1 });\n}\n\nconst score = (c, prop, isText) => {\n const s = c.scopes;\n const all = s.includes(\"ALL_SCOPES\");\n if (prop === \"strokes\") {\n if (!all && !s.includes(\"STROKE_COLOR\")) return -1;\n } else if (isText) {\n if (!all && !s.includes(\"TEXT_FILL\")) return -1;\n } else {\n if (!all && !s.includes(\"FRAME_FILL\") && !s.includes(\"SHAPE_FILL\")) return -1;\n }\n let n = c.layer * 10 + (all ? 0 : 4);\n if (prop === \"strokes\" && c.name.startsWith(\"Border/\")) n += 3;\n if (prop === \"fills\" && isText && (c.name.startsWith(\"Text/\") || c.name.endsWith(\"/Fg\"))) n += 3;\n if (prop === \"fills\" && !isText && (c.name.startsWith(\"Surface/\") || c.name.endsWith(\"/Bg\"))) n += 3;\n return n;\n};\n\nconst cache = {};\nconst pick = (hex, prop, isText) => {\n const k = hex + prop + isText;\n if (cache[k] !== undefined) return cache[k];\n let best = null, bs = -1;\n for (const c of cand) {\n if (c.hex !== hex) continue;\n const s = score(c, prop, isText);\n if (s > bs) { bs = s; best = c; }\n }\n cache[k] = best;\n return best;\n};\n\nconst tally = {};\nlet touched = 0, remaining = 0, unmatched = 0;\nconst errors = [];\n\nfor (const n of page.findAll(() => true)) {\n if (n.type === \"SECTION\") continue;\n let needs = false;\n for (const prop of [\"fills\",\"strokes\"]) {\n const ps = n[prop];\n if (!Array.isArray(ps)) continue;\n for (const p of ps) {\n if (p.type === \"SOLID\" && p.visible !== false && p.color && !(p.boundVariables && p.boundVariables.color)) needs = true;\n }\n }\n if (!needs) continue;\n if (touched >= 120) { remaining++; continue; }\n\n try {\n for (const prop of [\"fills\",\"strokes\"]) {\n const ps = n[prop];\n if (!Array.isArray(ps) || !ps.length) continue;\n const copy = JSON.parse(JSON.stringify(ps));\n let dirty = false;\n for (let i = 0; i < copy.length; i++) {\n const p = copy[i];\n if (p.type !== \"SOLID\" || p.visible === false || !p.color) continue;\n if (p.boundVariables && p.boundVariables.color) continue;\n const c = pick(toHex(p.color), prop, n.type === \"TEXT\");\n if (!c) { unmatched++; continue; }\n copy[i] = figma.variables.setBoundVariableForPaint(p, \"color\", c.v);\n tally[c.name] = (tally[c.name] || 0) + 1;\n dirty = true;\n }\n if (dirty) n[prop] = copy;\n }\n touched++;\n } catch (e) { errors.push(`${n.id}: ${e.message}`); }\n}\n\nreturn { touchedNodes: touched, remainingNodes: remaining, unmatched, distinctTokensUsed: Object.keys(tally).length, errors: errors.slice(0,5) };</code></pre><p><code>layer * 10</code> でトークン層をプリミティブ層より優先し、スコープが具体的なものに <code>+4</code>、名前の役割が一致するものに <code>+3</code> を足す。この重み付けで、同じ値を持つ複数の変数から適切な1つが選ばれる。</p><p>3バッチに分けて実行した結果。</p><pre><code class=\"language-json\">{\"touchedNodes\":120,\"remainingNodes\":857,\"unmatched\":0,\"distinctTokensUsed\":20,\"errors\":[]}\n{\"touchedNodes\":500,\"remainingNodes\":357,\"unmatched\":0,\"errors\":[]}\n{\"touchedPaintSets\":376,\"unmatched\":0,\"rawPaintsRemaining\":0,\"rawSample\":[],\n \"tally\":{\"Surface/Container\":61,\"Surface/Overlay\":13,\"Neutral/Gray Light\":21,\"Action/Primary/Bg\":10,\"Text/Inverse\":6,\"Text/Primary\":79,\"Neutral/Gray Pale\":13,\"Nav/Inactive\":21,\"Delivery/Delivery\":5,\"Primary/Blue Deep\":23,\"Action/Secondary/Bg\":3,\"Action/Secondary/Fg\":3,\"Border/Strong\":33,\"Border/Default\":11,\"Surface/Default\":4,\"Neutral/Gray Slate\":27,\"Text/Secondary\":7,\"Nav/Active\":3,\"Background/Background3\":1,\"Neutral/Gray Faint\":8,\"Neutral/Gray Lighter\":16,\"Warning/Warning\":4,\"Status/Warning/Fg\":1,\"Primary/Blue\":3},\n \"errors\":[]}</code></pre><p>最終確認。</p><pre><code class=\"language-json\">{\n \"boundPaints\": 1170,\n \"rawPaints\": 0,\n \"coverage\": \"100%\",\n \"distinctVariables\": 38,\n \"top\": [[\"Text/Primary\",175],[\"Surface/Container\",170],[\"Nav/Inactive\",105],[\"Border/Strong\",95],[\"Neutral/Gray Medium\",65],[\"Primary/Blue Deep\",57],[\"Status/Success/Fg\",51],[\"Neutral/Gray Light\",51],[\"Neutral/Gray Slate\",48],[\"Border/Default\",41],[\"Surface/Overlay\",34],[\"Neutral/Gray Lighter\",33]]\n}</code></pre><p>生hexがゼロになった。ここまでは順調だった。</p><p></p><h2 id=\"index_0NT3hX_1\">同じ「配送中」が4つの変数に割れていた</h2><p>機械的な一致で繋ぐと、見た目は保たれる。しかし<strong>意味は揃わない</strong>。</p><p>「配送中」というタグの文字色が、画面によって別の変数を参照している状態が見つかった。ある画面では <code>Status/Delivery/Fg</code>(<code>#BF360C</code>)、別の画面では <code>Delivery/Delivery</code>(<code>#E65100</code>)。色まで違う。</p><p>原因ははっきりしていた。先に <code>Status/Delivery/Fg</code> を <code>#BF360C</code> に変えていたので、生の <code>#E65100</code> だったテキストは完全一致せず、プリミティブの <code>Delivery/Delivery</code> に落ちていた。hex一致という基準を使う以上、避けられない副作用だった。</p><p>同じ役割が別の変数を使っている箇所を探す監査を書いた。</p><pre><code class=\"language-javascript\">const page = await figma.getNodeByIdAsync(\"12580:25\");\nawait figma.setCurrentPageAsync(page);\n\nconst nameCache = {};\nconst getName = async (id) => {\n if (!nameCache[id]) {\n const v = await figma.variables.getVariableByIdAsync(id);\n nameCache[id] = v ? v.name : id;\n }\n return nameCache[id];\n};\n\nconst boundOf = async (n, prop) => {\n const ps = n[prop];\n if (!Array.isArray(ps) || !ps.length) return null;\n const p = ps[0];\n if (p.type !== \"SOLID\" || !p.boundVariables || !p.boundVariables.color) return null;\n return await getName(p.boundVariables.color.id);\n};\n\nconst byLabel = {};\nfor (const t of page.findAllWithCriteria({ types: [\"TEXT\"] })) {\n const key = (t.characters || \"\").trim();\n if (!key || key.length > 12) continue;\n const v = await boundOf(t, \"fills\");\n if (!v) continue;\n if (!byLabel[key]) byLabel[key] = {};\n byLabel[key][v] = (byLabel[key][v] || 0) + 1;\n}\n\nconst byFrame = {};\nfor (const f of page.findAllWithCriteria({ types: [\"FRAME\"] })) {\n if (!/Badge|Status|Button|Tab|Chip/i.test(f.name)) continue;\n const v = await boundOf(f, \"fills\");\n if (!v) continue;\n if (!byFrame[f.name]) byFrame[f.name] = {};\n byFrame[f.name][v] = (byFrame[f.name][v] || 0) + 1;\n}\n\nconst conflicts = (obj) => Object.entries(obj)\n .filter(([, m]) => Object.keys(m).length > 1)\n .map(([k, m]) => ({ key: k, variables: m }));\n\nconst tok = await figma.variables.getVariableCollectionByIdAsync(\"VariableCollectionId:1941:108641\");\nconst tokenNames = new Set();\nfor (const vid of tok.variableIds) tokenNames.add((await figma.variables.getVariableByIdAsync(vid)).name);\n\nlet toToken = 0, toPrimitive = 0;\nfor (const n of page.findAll(() => true)) {\n if (n.type === \"SECTION\") continue;\n for (const prop of [\"fills\",\"strokes\"]) {\n const ps = n[prop]; if (!Array.isArray(ps)) continue;\n for (const p of ps) {\n if (p.type !== \"SOLID\" || !p.boundVariables || !p.boundVariables.color) continue;\n const nm = await getName(p.boundVariables.color.id);\n if (tokenNames.has(nm)) toToken++; else toPrimitive++;\n }\n }\n}\n\nreturn {\n labelConflicts: conflicts(byLabel),\n frameConflicts: conflicts(byFrame),\n layerUsage: { semanticToken: toToken, primitiveDirect: toPrimitive }\n};</code></pre><p>結果の一部。</p><pre><code class=\"language-json\">{\n \"labelConflicts\": [\n {\"key\":\"配送中\",\"variables\":{\"Text/Primary\":2,\"Status/Delivery/Fg\":1,\"Nav/Inactive\":3,\"Delivery/Delivery\":3,\"Status/Success/Fg\":1}},\n {\"key\":\"キャンセル\",\"variables\":{\"Text/Secondary\":2,\"Nav/Inactive\":2,\"Warning/Warning Dark\":1}},\n {\"key\":\"完了\",\"variables\":{\"Nav/Inactive\":3,\"Status/Success/Fg\":5}},\n {\"key\":\"ホーム\",\"variables\":{\"Nav/Active\":1,\"Neutral/Gray Light\":3,\"Action/Secondary/Fg\":1}}\n ],\n \"frameConflicts\": [\n {\"key\":\"Status Badge\",\"variables\":{\"Status/Delivery/Bg\":2,\"Action/Secondary/Bg\":1}}\n ],\n \"layerUsage\": {\"semanticToken\":776,\"primitiveDirect\":394}\n}</code></pre><p>1,170のうち394がプリミティブ直参照だった。セマンティック層を飛ばしているので、意味が表現できていない状態になる。</p><p>ただし監査結果を読むときに注意が要る。「配送中」が4つの変数に割れていると出るが、実際に確認すると3つは正しい使い分けだった。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>文脈</p></th><th colspan=\"1\" rowspan=\"1\"><p>変数</p></th><th colspan=\"1\" rowspan=\"1\"><p>役割</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>セクション見出しの「配送中」</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>Text/Primary</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>ただの見出し</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>バッジの「配送中」</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>Status/Delivery/Fg</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>状態表示</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>統計の項目名「配送中」</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>Nav/Inactive</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>数値に付く補助ラベル</p></td></tr></tbody></table></div><p>同じ言葉でも役割が違えば色が違うのは正しい。機械的な一致検査は「割れている」としか言えないので、1件ずつ何を指しているか見る必要があった。実際に誤りだったのは、ロボット一覧の「配送中」が1号機は緑、2号機はグレーになっていた箇所だけだった。</p><p></p><h2 id=\"index_kGHrydZx\">フレーム名の部分一致で45ノードを壊した</h2><p>ここで自分のスクリプトが事故を起こした。</p><p>「待機中」というステータスを青からグレーに移す作業で、対象を絞る条件をこう書いた。</p><pre><code class=\"language-javascript\">for (const f of root.findAll(n => n.type === \"FRAME\" && /badge|chip|status/i.test(n.name))) {\n const ts = f.findAllWithCriteria({ types: [\"TEXT\"] });\n if (!ts.length) continue;\n const hit = ts.some(t => PENDING.test(t.characters || \"\"));\n if (!hit) continue;\n if (bind(f, \"fills\", byName[\"Status/Pending/Bg\"])) badges++;\n for (const t of ts) if (bind(t, \"fills\", byName[\"Status/Pending/Fg\"])) texts++;\n}</code></pre><p><code>/badge|chip|status/i</code> がフレーム名に部分一致するものを対象にしている。ここに <code>Delivery Status Screen</code> という画面全体のフレームが該当した。その中に「待機中」というテキストが1つでもあれば、<code>findAllWithCriteria</code> が拾う<strong>画面内の全テキスト</strong>が Pending トークンで塗り潰される。</p><p>被害は45ノード。「詳細を見る」ボタン、統計の数値、ナビゲーションのラベル、配送IDまで巻き込まれた。</p><p>さらに悪いことに、気づいてロールバックしたときに一律 <code>Text/Primary</code> へ戻してしまった。この時点で<strong>元の色情報が失われた</strong>。「75%」が緑だったのか、「配送ID」がグレーだったのかが、ノードからは読めなくなった。</p><pre><code class=\"language-json\">{\n \"pendingRollback\": [\n {\"text\":\"配送状況\",\"to\":\"Text/Primary\"},\n {\"text\":\"配送中\",\"to\":\"Text/Primary\"},\n {\"text\":\"2\",\"to\":\"Text/Inverse\"},\n {\"text\":\"配送ID: #D2024\",\"to\":\"Text/Primary\"},\n {\"text\":\"3階 302号室\",\"to\":\"Text/Primary\"},\n {\"text\":\"到着予定: 14:25 \",\"to\":\"Text/Primary\"},\n {\"text\":\"出発完了\",\"to\":\"Text/Primary\"},\n {\"text\":\"75%\",\"to\":\"Text/Primary\"},\n {\"text\":\"追跡\",\"to\":\"Text/Primary\"},\n {\"text\":\"詳細を見る\",\"to\":\"Text/Inverse\"}\n ],\n \"textTokenMisuse\": 7\n}</code></pre><p>救いは、この画面と同一内容のコピーが別ページに存在したことだった。そちらは無傷だったので、参照元として使えた。</p><p>正しいトークン割り当てを、テキスト内容・フォントサイズ・親フレーム名の3つをキーにして抽出する。</p><pre><code class=\"language-javascript\">const page = await figma.getNodeByIdAsync(\"12580:25\");\nawait figma.setCurrentPageAsync(page);\nconst nameCache = {};\nconst getName = async (id) => { if(!nameCache[id]){const v=await figma.variables.getVariableByIdAsync(id); nameCache[id]=v?v.name:id;} return nameCache[id]; };\n\nconst ref = {};\nfor (const t of page.findAllWithCriteria({ types: [\"TEXT\"] })) {\n const ps = t.fills;\n if (!Array.isArray(ps) || !ps.length) continue;\n const q = ps[ps.length-1];\n if (q.type !== \"SOLID\" || !q.boundVariables || !q.boundVariables.color) continue;\n const v = await getName(q.boundVariables.color.id);\n const key = `${(t.characters||\"\").trim()}|${Math.round(t.fontSize)}|${t.parent ? t.parent.name : \"\"}`;\n if (!ref[key]) ref[key] = {};\n ref[key][v] = (ref[key][v] || 0) + 1;\n}\nconst map = {};\nfor (const [k, m] of Object.entries(ref)) {\n map[k] = Object.entries(m).sort((a,b)=>b[1]-a[1])[0][0];\n}\nreturn { entries: Object.keys(map).length, map };</code></pre><p>282件の対応表が取れた。抜粋。</p><pre><code>配送中|13|Status Badge → Status/Delivery/Fg\n75%|16|Progress Text → Status/Success/Fg\n配送ID: #D2024-0415|14|Delivery Info → Nav/Inactive\n詳細を見る|14|Details Button → Text/Inverse\n待機中|13|Status Badge → Status/Pending/Fg\n完了: 今日 13:45|12|Time Stamp → Text/Muted</code></pre><p>これを壊れたページに適用して130箇所を復元した。</p><pre><code class=\"language-json\">{\n \"applied\": 130,\n \"hits\": {\"Text/Primary\":52,\"Text/Muted\":30,\"Status/Delivery/Fg\":8,\"Status/Success/Fg\":13,\"Text/Secondary\":3,\"Text/Inverse\":10,\"Status/Warning/Fg\":3,\"Action/Secondary/Fg\":10,\"Status/Pending/Fg\":1}\n}</code></pre><p>参照先が無ければ手作業になっていた。一括処理の対象を「フレーム名の部分一致」で絞ったのが原因なので、以降はテキスト内容と構造で絞るように変えた。</p><p></p><h2 id=\"index_zodgOGH5\">重複と衝突は別の問題だった</h2><p>作業が一巡した時点で、トークンは35個になっていた。「多すぎないか」を判断するために、同じ値に解決されるトークンを機械的に洗い出した。</p><pre><code class=\"language-javascript\">const sig = {};\nconst tokens = [];\nfor (const vid of tok.variableIds) {\n const v = await figma.variables.getVariableByIdAsync(vid);\n const l = res(v.valuesByMode[light]), n = res(v.valuesByMode[night]);\n tokens.push({ name: v.name, light: l, night: n });\n const k = l + \"/\" + n;\n (sig[k] = sig[k] || []).push(v.name);\n}\nconst dupes = Object.entries(sig).filter(([,a]) => a.length > 1)\n .map(([k,a]) => ({ value: k, tokens: a }));</code></pre><pre><code class=\"language-json\">{\n \"duplicateValues\": [\n {\"value\":\"#FFFFFF/#1A1A1A\",\"tokens\":[\"Surface/Container\",\"Action/Primary/Fg\",\"Text/Inverse\",\"Action/Success/Fg\",\"Icon/Inverse\"]},\n {\"value\":\"#1A1A1A/#FFFFFF\",\"tokens\":[\"Text/Primary\",\"Icon/Strong\"]},\n {\"value\":\"#E3F2FD/#0277BD\",\"tokens\":[\"Action/Secondary/Bg\",\"Status/Pending/Bg\"]},\n {\"value\":\"#2E7D32/#81C784\",\"tokens\":[\"Status/Success/Fg\",\"Status/Success/Solid\"]},\n {\"value\":\"#E53935/#FFEBEE\",\"tokens\":[\"Status/Warning/Fg\",\"Status/Warning/Solid\"]},\n {\"value\":\"#BF360C/#FFF3E0\",\"tokens\":[\"Status/Delivery/Fg\",\"Status/Delivery/Solid\"]},\n {\"value\":\"#1565C0/#4FC3F7\",\"tokens\":[\"Nav/Active\",\"Status/Pending/Fg\"]}\n ]\n}</code></pre><p>35個のトークンが実質25色しか表現していない。だが、この重複には<strong>残すべきもの</strong>と<strong>潰すべきもの</strong>があった。</p><div class=\"embed\"><iframe src=\"/embeds/figma-design-token-consolidation-CwxN66LU.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p><code>Text/Inverse</code> と <code>Surface/Container</code> はどちらも白だが、軸が違う(文字か面か)。将来テーマを増やしたときに別々に動く。これは設計として正しい重複で、潰すと表現力が落ちる。</p><p>一方 <code>Icon/Strong</code> は <code>Text/Primary</code> と完全に同じだった。このデザインではアイコンと文字が常に同色なので、アイコン用の軸を分けた判断自体が過剰だった。これは潰すべき重複になる。</p><p>そして本当の問題は、重複ではなく<strong>衝突</strong>のほうにあった。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>色</p></th><th colspan=\"1\" rowspan=\"1\"><p>意味1</p></th><th colspan=\"1\" rowspan=\"1\"><p>意味2</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>#1565C0</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>Nav/Active(現在地)</p></td><td colspan=\"1\" rowspan=\"1\"><p>Status/Pending/Fg(待機中)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>#E3F2FD</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>Action/Secondary/Bg(押せる)</p></td><td colspan=\"1\" rowspan=\"1\"><p>Status/Pending/Bg(待機中)</p></td></tr></tbody></table></div><p>青が「押せる」「現在地」「待機中」の3役を兼ねていた。画面を見た人は、青いものが操作対象なのか状態表示なのか区別できない。</p><p>これはトークンを減らして解決する問題ではない。<strong>逆に分けるべき</strong>箇所だった。「待機中」を青から外してグレー系に移し、青は操作と現在地に残した。</p><p>同じ構造の問題は青の3色にもあった。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>色</p></th><th colspan=\"1\" rowspan=\"1\"><p>用途</p></th><th colspan=\"1\" rowspan=\"1\"><p>白文字とのコントラスト</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>#2196F3</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>Action/Primary/Bg(「詳細を見る」ボタン)</p></td><td colspan=\"1\" rowspan=\"1\"><p>3.12:1</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>#1976D2</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>混入色 57箇所</p></td><td colspan=\"1\" rowspan=\"1\"><p>4.60:1</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>#1565C0</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>Nav/Active</p></td><td colspan=\"1\" rowspan=\"1\"><p>5.75:1</p></td></tr></tbody></table></div><p>一番明るい <code>#2196F3</code> が一番使われていて、しかも一番読みにくい。<code>#1565C0</code> に統合したことで3色が1色になり、ボタンのコントラストも同時に直った。</p><p>整理の結果、トークンは35個から30個に減り、色は19色になった。<strong>トークンは増えて色は減る</strong>。意味の種類を細かく定義するほど、同じ色を使い回せるようになる。</p><p></p><h2 id=\"index_oH00RcGT\">残りのページに同じ処理をかけると、35色が17色になった</h2><p>テストページで確立した手順を、本体のページにある4つのセクション(スタイルガイド、タブレットUI、アプリUI、PCダッシュボード)へ適用した。合計約4,700ノード、生hex 2,951箇所。</p><p>事前調査で、既に変数が入っている900箇所が<strong>全てプリミティブ直参照</strong>だと分かった。セマンティック層を経由していないので、トークンの値を変えても反映されない状態になっていた。</p><p>パレットに無い色も7つ出てきた。</p><pre><code class=\"language-json\">{\n \"distinctRawColors\": 35,\n \"totalRawPaints\": 2951,\n \"exactMatches\": 28,\n \"needsDecision\": [\n {\"hex\":\"#757575\",\"count\":454,\"match\":\"Neutral/Gray Slate #6B7280\",\"dist\":15},\n {\"hex\":\"#FF9800\",\"count\":56,\"match\":\"Accent/Amber #FFB300\",\"dist\":27},\n {\"hex\":\"#000000\",\"count\":6,\"match\":\"Neutral/Black #1A1A1A\",\"dist\":45},\n {\"hex\":\"#EDEDED\",\"count\":4,\"match\":\"Neutral/Gray Faint #F0F0F0\",\"dist\":5},\n {\"hex\":\"#7B1FA2\",\"count\":4,\"match\":\"Neutral/Gray Slate #6B7280\",\"dist\":91},\n {\"hex\":\"#F57C00\",\"count\":4,\"match\":\"Delivery/Delivery #E65100\",\"dist\":46},\n {\"hex\":\"#F3E5F5\",\"count\":2,\"match\":\"Neutral/Gray Faint #F0F0F0\",\"dist\":12}\n ]\n}</code></pre><p><code>#FF9800</code> の正体を調べると、PCダッシュボードのステータスドット、タイムラインアイコン、「配送中」テキストに使われていた。つまり <code>#E65100</code> と同じ意味を、別の色で表現していた箇所だった。白背景でのコントラストは 2.16:1 で、テキストとしては極めて読みにくい。</p><p>紫の <code>#7B1FA2</code> はもっと分かりやすかった。タブレットUIの「Card 5」に使われていたが、<strong>同じカードの別コピーでは橙</strong>(<code>#FFF3E0</code>)だった。意図的な色分けではなく、コピー間で崩れた残骸だった。橙に揃えることで紫が消えた。</p><p>用途別に振り分けるマッピングを書いた。</p><pre><code class=\"language-javascript\">const roleMap = (hex, prop, isText) => {\n const T = isText && prop === \"fills\";\n const solidFill = prop === \"fills\" && !isText;\n switch (hex) {\n case \"#FF9800\": case \"#F57C00\": case \"#E65100\": case \"#BF360C\": case \"#7B1FA2\":\n return \"Status/Delivery/Fg\";\n case \"#FFF3E0\": case \"#F3E5F5\": return \"Status/Delivery/Bg\";\n case \"#4CAF50\": case \"#2E7D32\": case \"#81C784\": return \"Status/Success/Fg\";\n case \"#E8F5E9\": return \"Status/Success/Bg\";\n case \"#E53935\": case \"#C62828\": return \"Status/Warning/Fg\";\n case \"#FFEBEE\": return \"Status/Warning/Bg\";\n case \"#2196F3\": case \"#1976D2\": case \"#1565C0\": case \"#0277BD\": case \"#03A9F4\":\n case \"#4FC3F7\":\n return solidFill ? \"Action/Primary/Bg\" : \"Action/Secondary/Fg\";\n case \"#E3F2FD\": case \"#E1F5FE\": case \"#BBDEFB\": case \"#90CAF9\": case \"#B3E5FC\":\n case \"#BDE8FB\":\n return \"Action/Secondary/Bg\";\n case \"#FFFFFF\": return solidFill ? \"Surface/Container\" : \"Text/Inverse\";\n case \"#1A1A1A\": case \"#000000\": return \"Text/Primary\";\n case \"#424242\": return \"Text/Secondary\";\n case \"#757575\": case \"#6B6B6B\": case \"#6B7280\": case \"#9E9E9E\":\n return T ? \"Text/Muted\" : \"Icon/Default\";\n case \"#BDBDBD\": return \"Icon/Subtle\";\n case \"#E0E0E0\": case \"#F0F0F0\": case \"#EDEDED\": return \"Border/Default\";\n case \"#F5F5F5\": return \"Surface/Overlay\";\n case \"#F8F9FA\": return \"Surface/Default\";\n case \"#FFB300\": case \"#FFB74D\": return \"Rating/Active\";\n }\n return null;\n};</code></pre><p>3バッチで全ノードを処理した結果。</p><pre><code class=\"language-json\">{\"touched\":700,\"remaining\":3002,\"unmapped\":{\"#4FC3F7\":5,\"#BBDEFB\":1,\"#90CAF9\":1,\"#B3E5FC\":1}}\n{\"touched\":1600,\"remaining\":1410,\"unmapped\":{\"#BDE8FB\":4}}\n{\"touched\":1414,\"remaining\":0,\"unmapped\":{}}</code></pre><p>最終状態。</p><pre><code class=\"language-json\">{\n \"boundPaints\": 3843,\n \"rawPaints\": 0,\n \"textChecked\": 1314,\n \"distinctColors\": 17,\n \"palette\": [[\"#6B6B6B\",917],[\"#FFFFFF\",621],[\"#1565C0\",508],[\"#1A1A1A\",506],[\"#2E7D32\",313],[\"#E0E0E0\",201],[\"#BF360C\",159],[\"#424242\",131],[\"#F5F5F5\",104],[\"#BDBDBD\",80],[\"#F8F9FA\",80],[\"#C62828\",75],[\"#E3F2FD\",63],[\"#E8F5E9\",38],[\"#FFF3E0\",31],[\"#FFB300\",9],[\"#FFEBEE\",7]]\n}</code></pre><p>35色が17色になった。</p><p></p><h2 id=\"index_qb_dGX_Y\">コントラストは、1色を変えるだけで33件直った</h2><p>全テキストのコントラストを測る監査を書いた。各テキストノードから親をたどって最初に不透明な塗りを持つ祖先を探し、その色を背景とみなして比率を出す。</p><pre><code class=\"language-javascript\">const lum = (c) => {\n const f = (v) => v <= 0.03928 ? v/12.92 : Math.pow((v+0.055)/1.055, 2.4);\n return 0.2126*f(c.r) + 0.7152*f(c.g) + 0.0722*f(c.b);\n};\nconst ratio = (a,b) => { const L1=lum(a), L2=lum(b); const hi=Math.max(L1,L2), lo=Math.min(L1,L2); return (hi+0.05)/(lo+0.05); };\nconst solidFill = (n) => {\n const ps = n.fills;\n if (!Array.isArray(ps)) return null;\n for (let i = ps.length-1; i >= 0; i--) if (ps[i].type === \"SOLID\" && ps[i].visible !== false && (ps[i].opacity ?? 1) > 0.9) return ps[i].color;\n return null;\n};\n\nconst fails = []; let checked = 0;\nfor (const t of page.findAllWithCriteria({ types: [\"TEXT\"] })) {\n const fg = solidFill(t);\n if (!fg) continue;\n let p = t.parent, bg = null;\n while (p && p.type !== \"PAGE\") { const c = solidFill(p); if (c) { bg = c; break; } p = p.parent; }\n if (!bg) continue;\n const r = ratio(fg, bg);\n const bold = /Bold|Black|Medium/i.test(t.fontName && t.fontName.style ? t.fontName.style : \"\");\n const large = t.fontSize >= 24 || (t.fontSize >= 18.66 && bold);\n const need = large ? 3.0 : 4.5;\n checked++;\n if (r < need) fails.push({ text: (t.characters||\"\").slice(0,14), size: t.fontSize, fg: toHex(fg), bg: toHex(bg), ratio: Math.round(r*100)/100, need });\n}</code></pre><p>321件を測って33件が不合格だった。全て 4.03〜4.37:1 の僅差で、原因は2つに集約された。</p><pre><code class=\"language-json\">{\n \"textNodesChecked\": 321,\n \"failures\": [\n {\"text\":\"2分前\",\"size\":11,\"fg\":\"#757575\",\"bg\":\"#E3F2FD\",\"ratio\":4.03,\"need\":4.5},\n {\"text\":\"完了\",\"size\":13,\"fg\":\"#757575\",\"bg\":\"#F5F5F5\",\"ratio\":4.23,\"need\":4.5},\n {\"text\":\"キャンセル\",\"size\":12,\"fg\":\"#E53935\",\"bg\":\"#FFEBEE\",\"ratio\":3.7,\"need\":4.5},\n {\"text\":\"配送ID: #D2024-0\",\"size\":13,\"fg\":\"#757575\",\"bg\":\"#F5F5F5\",\"ratio\":4.23,\"need\":4.5},\n {\"text\":\"ご利用ありがとうございました\",\"size\":14,\"fg\":\"#757575\",\"bg\":\"#F8F9FA\",\"ratio\":4.37,\"need\":4.5},\n {\"text\":\"ロボット2号機が配送中です\",\"size\":14,\"fg\":\"#757575\",\"bg\":\"#FFF3E0\",\"ratio\":4.2,\"need\":4.5},\n {\"text\":\"中止\",\"size\":16,\"fg\":\"#E53935\",\"bg\":\"#FFFFFF\",\"ratio\":4.23,\"need\":4.5}\n ]\n}\n</code></pre><p><code>#757575</code> は白背景では 4.61:1 で合格していた。ところが <code>#F5F5F5</code> や <code>#E3F2FD</code> のような淡い色の上に乗ると 4.03 まで落ちる。カード内や通知内で使われているので、実際には多くが不合格だった。<strong>白でだけ測っていたら見逃していた。</strong></p><p>修正はトークンの参照先を変えるだけで済んだ。</p><pre><code class=\"language-javascript\">byName[\"Neutral/Gray Medium\"].setValueForMode(primMode, { r: 0x6B/255, g: 0x6B/255, b: 0x6B/255 });\nbyName[\"Status/Warning/Fg\"].setValueForMode(light, A(\"Warning/Warning Dark\"));\nbyName[\"Border/Strong\"].setValueForMode(light, A(\"Neutral/Gray Medium\"));\n</code></pre><p><code>Neutral/Gray Medium</code> を <code>#757575</code> から <code>#6B6B6B</code> に1段階暗くしただけで、<code>Text/Muted</code>・<code>Nav/Inactive</code>・<code>Icon/Default</code> が一斉に改善し、342箇所が直った。</p><pre><code class=\"language-json\">{\"textNodesChecked\":321,\"failures\":[],\"boundPaints\":1170,\"rawPaints\":0,\"distinctColors\":19}\n</code></pre><p>本体ページ側も同様に測り、1,314件中0件不合格になった。残った5件はフロアマップのラベルで、監査が親をたどる仕組みのため、下に敷かれた兄弟図形を見ていない誤検知だった。実際の背景を調べると 5.13〜5.75:1 で合格していた。</p><pre><code class=\"language-json\">[\n {\"text\":\"エレベーター\",\"size\":24,\"actualBackdrop\":{\"node\":\"Vector\",\"hex\":\"#6B6B6B\",\"ratio\":5.33}},\n {\"text\":\"1\",\"size\":14,\"actualBackdrop\":{\"node\":\"Vector\",\"hex\":\"#1565C0\",\"ratio\":5.75}},\n {\"text\":\"2\",\"size\":14,\"actualBackdrop\":{\"node\":\"Vector\",\"hex\":\"#BF360C\",\"ratio\":5.6}},\n {\"text\":\"3\",\"size\":14,\"actualBackdrop\":{\"node\":\"Vector\",\"hex\":\"#2E7D32\",\"ratio\":5.13}}\n]</code></pre><p></p><h2 id=\"index_i0iCDq9T\">押せるものと状態表示が同じ色をしていた</h2><p>最後に見つかったのは、色の数ではなく<strong>役割の見分けがつかない</strong>という問題だった。</p><p>「稼働中」というステータスバッジと、「管理者」「更新」というボタンが、どちらも薄い青地に青文字だった。同じ配色なので、見ただけではどちらが押せるのか分からない。</p><p>該当する要素を洗い出して分類した。</p><pre><code class=\"language-javascript\">const CONTROL = /button|btn|chip|tab$|tab |action|filter|refresh|toggle|switch|link|user|account|menu item|pagination|selector/i;\nconst STATUS = /badge|status|label|state|pill/i;\n\nconst rows = [];\nfor (const secId of [\"1185:84984\",\"1329:97699\",\"101:2995\"]) {\n const root = await figma.getNodeByIdAsync(secId);\n for (const f of root.findAll(n => n.type === \"FRAME\")) {\n const bg = await varOf(f, \"fills\");\n if (bg !== \"Action/Secondary/Bg\") continue;\n const t = f.findAllWithCriteria({ types: [\"TEXT\"] })[0];\n if (!t) continue;\n const fg = await varOf(t, \"fills\");\n const isControl = CONTROL.test(f.name);\n const isStatus = STATUS.test(f.name) && !isControl;\n rows.push({\n section: root.name.replace(\"薬剤搬送ロボット-\",\"\"),\n id: f.id, node: f.name, text: (t.characters||\"\").slice(0,10),\n size: `${Math.round(f.width)}x${Math.round(f.height)}`,\n fg, kind: isControl ? \"操作\" : isStatus ? \"状態\" : \"不明\"\n });\n }\n}</code></pre><pre><code class=\"language-json\">{\"total\":32,\"byKind\":{\"不明\":11,\"状態\":8,\"操作\":13}}</code></pre><p>32件が同じ薄青ピル構成で、うち13件が操作(管理者×4、更新、割当×2、追跡、エクスポート、詳細×4)、8件が状態表示だった。残り11件は幅200px超の情報パネルで、ピルではないので対象外とした。</p><p>操作側だけを白文字+黒地に移した。新しくトークンを2つ追加している。</p><pre><code class=\"language-javascript\">mk(\"Action/Neutral/Bg\", [\"FRAME_FILL\",\"SHAPE_FILL\"], \"Neutral/Black\", \"Neutral/White\");\nmk(\"Action/Neutral/Fg\", [\"TEXT_FILL\",\"STROKE_COLOR\",\"SHAPE_FILL\"], \"Neutral/White\", \"Neutral/Black\");\n</code></pre><p>コントラストは 17.4:1 になった。状態表示は薄青のまま残したので、色で役割が分かれる。</p><p></p><h2 id=\"index_RiZnIe2p\">結果</h2><p>2ページ・5区画に対して行った作業の集計。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>指標</p></th><th colspan=\"1\" rowspan=\"1\"><p>作業前</p></th><th colspan=\"1\" rowspan=\"1\"><p>作業後</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>生hexのペイント</p></td><td colspan=\"1\" rowspan=\"1\"><p>3,977</p></td><td colspan=\"1\" rowspan=\"1\"><p>0</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>変数接続</p></td><td colspan=\"1\" rowspan=\"1\"><p>1,044</p></td><td colspan=\"1\" rowspan=\"1\"><p>5,013</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>使用色(テストページ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>26</p></td><td colspan=\"1\" rowspan=\"1\"><p>19</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>使用色(本体4区画)</p></td><td colspan=\"1\" rowspan=\"1\"><p>35</p></td><td colspan=\"1\" rowspan=\"1\"><p>17</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>プリミティブ</p></td><td colspan=\"1\" rowspan=\"1\"><p>29</p></td><td colspan=\"1\" rowspan=\"1\"><p>36</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>セマンティックトークン</p></td><td colspan=\"1\" rowspan=\"1\"><p>20</p></td><td colspan=\"1\" rowspan=\"1\"><p>32</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>AA不合格のテキスト</p></td><td colspan=\"1\" rowspan=\"1\"><p>多数</p></td><td colspan=\"1\" rowspan=\"1\"><p>0(1,635件検証)</p></td></tr></tbody></table></div><p>作業を通して分かったことが3つある。</p><p><strong>hex完全一致は安全だが、意味は揃わない。</strong> 値が同じ変数に繋ぐ限り見た目は変わらないので、事故のリスクはほぼ無い。ただし「同じ役割が同じ変数を使う」状態には自動的にはならない。役割で揃えるには、ノードの構造や文脈を見て1件ずつ判断するしかなかった。</p><p><strong>コントラストは白背景だけで測ると見逃す。</strong> <code>#757575</code> は白の上では合格していた。淡い背景の上に乗った瞬間に落ちる。実際に使われている組み合わせで測って初めて33件が出てきた。</p><p><strong>色を減らす作業は、トークンを増やす作業だった。</strong> 重複したトークンは潰すべきだが、1つの色が複数の意味を兼ねている「衝突」は、分けなければ解決しない。最終的にトークンは20個から32個に増え、色は35色から17色に減った。意味の種類を細かく定義するほど、同じ色を使い回せるようになる。</p><p>そして、一括処理の対象を絞る条件は慎重に書く必要がある。<code>/badge|chip|status/i</code> という4文字の正規表現が <code>Delivery Status Screen</code> に一致して45ノードを壊した。同一内容のコピーが別ページに残っていたから復元できたが、無ければ手作業になっていた。</p><h5>※ プロジェクト名「R2R2R」は、UC Berkeley・Toyota Research Instituteの研究「Real2Render2Real」(Yu, Fu et al., 2025)の概念に触発されています。現実空間をデジタルツイン化し、仮想空間での試行から得た知見を実世界のモーター駆動へ還元する——その設計と実装過程を毎日記録しています。技術アプローチおよび目的は独自のものです。</h5>", "toc": [{"id": "index_HG1_L3sK", "text": "Figmaコネクタが読み取ったもの"}, {"id": "index_S4iJx9Qp", "text": "変数は存在していた。画面が使っていなかっただけだった"}, {"id": "index_zgKsR6Q4", "text": "先にコントラストを測ると、4色が基準に届いていなかった"}, {"id": "index_7WSs2S7Y", "text": "「Noto Sans JP SemiBold」は存在しなかった"}, {"id": "index_ZUW562PF", "text": "hex完全一致で機械的に繋ぐと、見た目を変えずに生hexがゼロになる"}, {"id": "index_0NT3hX_1", "text": "同じ「配送中」が4つの変数に割れていた"}, {"id": "index_kGHrydZx", "text": "フレーム名の部分一致で45ノードを壊した"}, {"id": "index_zodgOGH5", "text": "重複と衝突は別の問題だった"}, {"id": "index_oH00RcGT", "text": "残りのページに同じ処理をかけると、35色が17色になった"}, {"id": "index_qb_dGX_Y", "text": "コントラストは、1色を変えるだけで33件直った"}, {"id": "index_i0iCDq9T", "text": "押せるものと状態表示が同じ色をしていた"}, {"id": "index_RiZnIe2p", "text": "結果"}]}
\ No newline at end of file
+{"day": "LOG303", "series": "HRI", "published": "2026-07-31T01:07:33.000Z", "title": "Figmaのデザイントークンを整理する | 生hex 3,977箇所を変数化し、コントラスト不合格をゼロにするまで", "slug": "figma-design-token-consolidation", "cover": null, "thumb": "/thumbs/r-figma-design-token-consolidation.html", "lang": "JP", "description": "Figma MCPで既存UIの色を全て変数化。生hex3,977箇所をゼロにし、35色を17色へ統合、テキスト1,635件のコントラストをWCAG AA適合させた作業ログ。", "updated": "2026-07-31T01:08:12.000Z", "body": "<!--TOC--><p></p><p>ChromeのClaudeにFigmaコネクタを追加した。繋いだ先にあったのは、病院内で薬剤を運ぶ搬送ロボットのUIデザイン一式だった。アプリ、タブレット、PCダッシュボード、スタイルガイド。合計で約5,000ノードある。</p><p>このファイルには変数(デザイントークン)が既に整備されていた。プリミティブ29個、セマンティックトークン20個、Light/Nightの2モード。設計としてはかなり整っている。</p><p>にもかかわらず、画面はそれを1つも使っていなかった。全部が生のhex値で組まれていた。</p><p>そこから、色を変数に繋ぎ直し、重複した色を潰し、コントラストの不足を直すまでの記録を残す。途中で自分の書いたスクリプトが45ノードを壊した話も含めて、全量で書く。</p><p></p><h2 id=\"index_HG1_L3sK\">Figmaコネクタが読み取ったもの</h2><p>コネクタを繋いで、対象フレームのURLを渡すと、<code>get_design_context</code> がリファレンスコードとスクリーンショットとメタデータを返してくる。返ってきたのは390×1048のアプリ画面だった。</p><p>返却されたコードは React + Tailwind の形をしている。ただしこれは「参照用」であって、そのまま使うものではない。重要なのは、この出力から<strong>実際に使われている色が全て生のhexで書かれている</strong>と分かることだった。</p><pre><code class=\"language-jsx\"><div className=\"bg-[#f8f9fa] content-stretch flex flex-col items-start relative size-full\" data-node-id=\"12580:28\" data-name=\"Pharmacy Robot App\">\n <div className=\"bg-white content-stretch drop-shadow-[0px_2px_4px_rgba(0,0,0,0.05)] flex flex-col gap-[16px] items-start px-[20px] py-[16px] relative shrink-0 w-full\" data-node-id=\"12580:29\" data-name=\"Header\">\n <p className=\"[word-break:break-word] font-['Noto_Sans_JP:Bold'] font-bold leading-[24px] relative shrink-0 text-[#1a1a1a] text-[20px] whitespace-nowrap\" data-node-id=\"12580:31\">\n 薬剤搬送ロボット\n </p></code></pre><p><code>bg-[#f8f9fa]</code>、<code>text-[#1a1a1a]</code>。変数を参照していれば、ここには変数名が出る。出ていないということは、デザイン側で色が直接指定されているということだった。</p><p>この時点で「変数が無い」と判断したが、これは誤りだった。次の節でそれが分かる。</p><p></p><h2 id=\"index_S4iJx9Qp\">変数は存在していた。画面が使っていなかっただけだった</h2><p>ファイル側の状態を実測するスクリプトを書いた。何かを変更する前に、まず現物を読む。</p><pre><code class=\"language-javascript\">const collections = await figma.variables.getLocalVariableCollectionsAsync();\nconst colInfo = collections.map(c => ({\n name: c.name,\n id: c.id,\n varCount: c.variableIds.length,\n modes: c.modes.map(m => m.name)\n}));\n\nconst pages = figma.root.children.map(p => ({ id: p.id, name: p.name }));\n\nconst node = await figma.getNodeByIdAsync(\"12580:28\");\nconst nodeInfo = node ? {\n name: node.name,\n type: node.type,\n width: node.width,\n height: node.height,\n pageName: (() => { let n = node; while (n && n.type !== 'PAGE') n = n.parent; return n ? n.name : null; })()\n} : null;\n\nconst localStyles = {\n paint: (await figma.getLocalPaintStylesAsync()).length,\n text: (await figma.getLocalTextStylesAsync()).length,\n effect: (await figma.getLocalEffectStylesAsync()).length\n};\n\nreturn { collections: colInfo, pages, nodeInfo, localStyles };</code></pre><p>返ってきた実測値。</p><pre><code class=\"language-json\">{\n \"collections\": [\n {\"name\":\"Primitives(プリミティブ)\",\"id\":\"VariableCollectionId:645:51019\",\"varCount\":29,\"modes\":[\"Mode 1\"]},\n {\"name\":\"Tokens(トークン)\",\"id\":\"VariableCollectionId:1941:108641\",\"varCount\":20,\"modes\":[\"Light Mode\",\"Night Mode\"]},\n {\"name\":\"html.to.design\",\"id\":\"VariableCollectionId:5378:709\",\"varCount\":319,\"modes\":[\"Mode 1\"]}\n ],\n \"pages\": [\n {\"id\":\"0:1\",\"name\":\"Pixels to Motors\"},\n {\"id\":\"12580:25\",\"name\":\"テスト\"},\n {\"id\":\"1394:101746\",\"name\":\"ハコブくん4コママンガ\"},\n {\"id\":\"4462:4\",\"name\":\"サムネイル保管庫\"}\n ],\n \"nodeInfo\": {\"name\":\"Pharmacy Robot App\",\"type\":\"FRAME\",\"width\":390,\"height\":1048,\"pageName\":\"テスト\"},\n \"localStyles\": {\"paint\":82,\"text\":146,\"effect\":0}\n}</code></pre><p><code>Primitives</code> が29個、<code>Tokens</code> が20個。Tokensには Light Mode と Night Mode の2モードがある。中身を読むと、Tokens側は全てプリミティブへのエイリアスで構成されていた。</p><pre><code>Surface/Default → Light: Neutral/Off White / Night: Neutral/Gray Dark\nSurface/Container → Light: Neutral/White / Night: Neutral/Black\nText/Primary → Light: Neutral/Black / Night: Neutral/White\nText/Secondary → Light: Neutral/Gray Dark / Night: Neutral/Gray Pale\nBorder/Default → Light: Neutral/Gray Pale / Night: Neutral/Gray Dark\nAction/Primary/Bg → Light: Primary/Blue / Night: Primary/Light Blue\nStatus/Success/Fg → Light: Success/Success / Night: Success/Success Light</code></pre><p>素の色を持つ層(プリミティブ)と、意味を持つ層(トークン)が分かれていて、後者が前者を参照する。教科書通りの2層構造だった。</p><p>つまり問題は「トークンが無い」ではなく「<strong>画面がトークンを使っていない</strong>」だった。作るのではなく、繋ぐ作業になる。</p><p></p><h2 id=\"index_zgKsR6Q4\">先にコントラストを測ると、4色が基準に届いていなかった</h2><p>繋ぐ前に、そのまま繋いでいいのかを確認した。色をコントラスト比で測ると、4箇所がWCAG AA基準(通常テキストで4.5:1)に届いていない。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>箇所</p></th><th colspan=\"1\" rowspan=\"1\"><p>色</p></th><th colspan=\"1\" rowspan=\"1\"><p>背景</p></th><th colspan=\"1\" rowspan=\"1\"><p>比率</p></th><th colspan=\"1\" rowspan=\"1\"><p>判定</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>「75%」</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>#4CAF50</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>白</p></td><td colspan=\"1\" rowspan=\"1\"><p>2.78:1</p></td><td colspan=\"1\" rowspan=\"1\"><p>不合格</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>受け取り完了ボタン</p></td><td colspan=\"1\" rowspan=\"1\"><p>白</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>#4CAF50</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>2.78:1</p></td><td colspan=\"1\" rowspan=\"1\"><p>不合格</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ボトムナビ非アクティブ</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>#9E9E9E</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>白</p></td><td colspan=\"1\" rowspan=\"1\"><p>2.68:1</p></td><td colspan=\"1\" rowspan=\"1\"><p>不合格</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>「配送中」バッジ</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>#E65100</code></p></td><td colspan=\"1\" rowspan=\"1\"><p><code>#FFF3E0</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>3.46:1</p></td><td colspan=\"1\" rowspan=\"1\"><p>不合格</p></td></tr></tbody></table></div><p>薬剤を運ぶロボットのUIなので、状態が読めないことは実務上まずい。ここは繋ぐ前に色そのものを直す。</p><p>暗い側の色を2つ追加し、既存のセマンティックトークンの参照先を差し替えた。</p><pre><code class=\"language-javascript\">const hex = (s) => ({\n r: parseInt(s.slice(1,3),16)/255,\n g: parseInt(s.slice(3,5),16)/255,\n b: parseInt(s.slice(5,7),16)/255\n});\n\nconst prim = await figma.variables.getVariableCollectionByIdAsync(\"VariableCollectionId:645:51019\");\nconst tok = await figma.variables.getVariableCollectionByIdAsync(\"VariableCollectionId:1941:108641\");\nconst primMode = prim.modes[0].modeId;\nconst light = tok.modes.find(m => m.name === \"Light Mode\").modeId;\nconst night = tok.modes.find(m => m.name === \"Night Mode\").modeId;\n\nconst lookup = {};\nfor (const cid of [prim.variableIds, tok.variableIds].flat()) {\n const v = await figma.variables.getVariableByIdAsync(cid);\n lookup[v.name] = v;\n}\n\nconst created = [];\n\nconst successDark = figma.variables.createVariable(\"Success/Success Dark\", prim, \"COLOR\");\nsuccessDark.scopes = [\"ALL_SCOPES\"];\nsuccessDark.setValueForMode(primMode, hex(\"#2E7D32\"));\ncreated.push(successDark.id);\n\nconst deliveryDark = figma.variables.createVariable(\"Delivery/Delivery Dark\", prim, \"COLOR\");\ndeliveryDark.scopes = [\"ALL_SCOPES\"];\ndeliveryDark.setValueForMode(primMode, hex(\"#BF360C\"));\ncreated.push(deliveryDark.id);\n\nlookup[\"Status/Success/Fg\"].setValueForMode(light, { type: \"VARIABLE_ALIAS\", id: successDark.id });\nlookup[\"Status/Delivery/Fg\"].setValueForMode(light, { type: \"VARIABLE_ALIAS\", id: deliveryDark.id });\n\nconst actSuccessBg = figma.variables.createVariable(\"Action/Success/Bg\", tok, \"COLOR\");\nactSuccessBg.scopes = [\"FRAME_FILL\", \"SHAPE_FILL\"];\nactSuccessBg.setValueForMode(light, { type: \"VARIABLE_ALIAS\", id: successDark.id });\nactSuccessBg.setValueForMode(night, { type: \"VARIABLE_ALIAS\", id: lookup[\"Success/Success\"].id });\ncreated.push(actSuccessBg.id);\n\nconst actSuccessFg = figma.variables.createVariable(\"Action/Success/Fg\", tok, \"COLOR\");\nactSuccessFg.scopes = [\"TEXT_FILL\"];\nactSuccessFg.setValueForMode(light, { type: \"VARIABLE_ALIAS\", id: lookup[\"Neutral/White\"].id });\nactSuccessFg.setValueForMode(night, { type: \"VARIABLE_ALIAS\", id: lookup[\"Neutral/Black\"].id });\ncreated.push(actSuccessFg.id);\n\nconst navActive = figma.variables.createVariable(\"Nav/Active\", tok, \"COLOR\");\nnavActive.scopes = [\"TEXT_FILL\", \"SHAPE_FILL\"];\nnavActive.setValueForMode(light, { type: \"VARIABLE_ALIAS\", id: lookup[\"Primary/Dark\"].id });\nnavActive.setValueForMode(night, { type: \"VARIABLE_ALIAS\", id: lookup[\"Primary/Light Blue\"].id });\ncreated.push(navActive.id);\n\nconst navInactive = figma.variables.createVariable(\"Nav/Inactive\", tok, \"COLOR\");\nnavInactive.scopes = [\"TEXT_FILL\", \"SHAPE_FILL\"];\nnavInactive.setValueForMode(light, { type: \"VARIABLE_ALIAS\", id: lookup[\"Neutral/Gray Medium\"].id });\nnavInactive.setValueForMode(night, { type: \"VARIABLE_ALIAS\", id: lookup[\"Neutral/Gray Light\"].id });\ncreated.push(navInactive.id);\n\nreturn {\n createdVariableIds: created,\n repointed: [\"Status/Success/Fg\", \"Status/Delivery/Fg\"],\n primitiveCount: prim.variableIds.length,\n tokenCount: tok.variableIds.length\n};</code></pre><pre><code class=\"language-json\">{\n \"createdVariableIds\": [\"VariableID:12582:2\",\"VariableID:12582:3\",\"VariableID:12582:4\",\"VariableID:12582:5\",\"VariableID:12582:6\",\"VariableID:12582:7\"],\n \"repointed\": [\"Status/Success/Fg\",\"Status/Delivery/Fg\"],\n \"primitiveCount\": 31,\n \"tokenCount\": 24\n}</code></pre><p>変更後の比率。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>箇所</p></th><th colspan=\"1\" rowspan=\"1\"><p>変更</p></th><th colspan=\"1\" rowspan=\"1\"><p>比率</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>「75%」</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>#4CAF50</code> → <code>#2E7D32</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>2.78 → 5.13:1</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>受け取り完了ボタン</p></td><td colspan=\"1\" rowspan=\"1\"><p>背景を <code>#2E7D32</code> へ</p></td><td colspan=\"1\" rowspan=\"1\"><p>2.78 → 5.13:1</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ナビ非アクティブ</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>#9E9E9E</code> → <code>#757575</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>2.68 → 4.61:1</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>「配送中」バッジ</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>#E65100</code> → <code>#BF360C</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>3.46 → 5.11:1</p></td></tr></tbody></table></div><p></p><h2 id=\"index_7WSs2S7Y\">「Noto Sans JP SemiBold」は存在しなかった</h2><p>テキストの色を変えようとしたとき、スクリプトが止まった。</p><pre><code>Error: The font \"Noto Sans JP SemiBold\" could not be loaded.\nAvailable styles for \"Noto Sans JP\": [\"Black\",\"Bold\",\"DemiLight\",\"Light\",\"Medium\",\"Regular\",\"Thin\"]</code></pre><p>ファイルは <code>SemiBold</code> を参照しているが、その字形は環境に存在しない。フォールバックで表示されていただけだった。Figmaは欠落フォントのテキストノードを編集できないので、文字を変える前にフォントを実在するスタイルへ差し替える必要がある。</p><p>ページ内を数えると96ノードが該当した。</p><pre><code class=\"language-javascript\">const page = await figma.getNodeByIdAsync(\"12580:25\");\nawait figma.setCurrentPageAsync(page);\n\nconst texts = page.findAllWithCriteria({ types: [\"TEXT\"] });\n\nconst styleCount = {};\nconst semiBoldNodes = [];\n\nfor (const t of texts) {\n let segs;\n try { segs = t.getStyledTextSegments([\"fontName\"]); } catch (e) { continue; }\n let hit = false;\n for (const s of segs) {\n const key = `${s.fontName.family} ${s.fontName.style}`;\n styleCount[key] = (styleCount[key] || 0) + 1;\n if (s.fontName.style === \"SemiBold\") hit = true;\n }\n if (hit) semiBoldNodes.push(t.id);\n}\n\nreturn {\n totalTextNodes: texts.length,\n styleUsage: styleCount,\n semiBoldNodeCount: semiBoldNodes.length,\n sampleIds: semiBoldNodes.slice(0, 5)\n};</code></pre><pre><code class=\"language-json\">{\n \"totalTextNodes\": 321,\n \"styleUsage\": {\n \"Noto Sans JP Bold\": 79,\n \"Noto Sans JP Medium\": 50,\n \"Noto Sans JP SemiBold\": 96,\n \"Noto Sans JP Regular\": 96\n },\n \"semiBoldNodeCount\": 96,\n \"sampleIds\": [\"12580:49\",\"12580:58\",\"12580:71\",\"12580:92\",\"12580:99\"]\n}</code></pre><p>96ノードを Bold に置換した。置換後の構成。</p><pre><code class=\"language-json\">{\n \"changedCount\": 16,\n \"errors\": [],\n \"finalStyleUsage\": {\n \"Noto Sans JP Bold\": 175,\n \"Noto Sans JP Medium\": 50,\n \"Noto Sans JP Regular\": 96\n }\n}</code></pre><p>Bold が 79 から 175 に増え、SemiBold がゼロになった。ただしこれで見出し=Bold、小見出し=SemiBold、本文=Regular という3段階のうち中間が消えた。ウェイトの階層が2段階になったぶん、画面のメリハリは弱くなっている。フォントが実在しない以上、避けられない副作用だった。</p><p></p><h2 id=\"index_ZUW562PF\">hex完全一致で機械的に繋ぐと、見た目を変えずに生hexがゼロになる</h2><p>ここからが本題の接続作業になる。方針は単純で、<strong>生のhex値と完全に一致する変数があれば、そこへ繋ぐ</strong>。値が同じなので見た目は1ピクセルも変わらない。変わるのは構造だけになる。</p><p>ただし繋ぎ先の選び方には条件がある。同じ <code>#FFFFFF</code> でも、テキストなら <code>Text/Inverse</code>、面なら <code>Surface/Container</code> を選ぶべきで、これは変数の scopes(その変数がどのプロパティに使えるか)で判定できる。</p><pre><code class=\"language-javascript\">const page = await figma.getNodeByIdAsync(\"12580:25\");\nawait figma.setCurrentPageAsync(page);\n\nconst toHex = (c) => '#' + [c.r,c.g,c.b].map(v => Math.round(v*255).toString(16).padStart(2,'0')).join('').toUpperCase();\n\nconst prim = await figma.variables.getVariableCollectionByIdAsync(\"VariableCollectionId:645:51019\");\nconst tok = await figma.variables.getVariableCollectionByIdAsync(\"VariableCollectionId:1941:108641\");\nconst primMode = prim.modes[0].modeId;\nconst light = tok.modes.find(m => m.name === \"Light Mode\").modeId;\n\nconst primHex = {};\nconst cand = [];\nfor (const vid of prim.variableIds) {\n const v = await figma.variables.getVariableByIdAsync(vid);\n const val = v.valuesByMode[primMode];\n if (val && typeof val.r === \"number\") {\n primHex[v.id] = toHex(val);\n cand.push({ v, name: v.name, hex: toHex(val), scopes: v.scopes, layer: 0 });\n }\n}\nfor (const vid of tok.variableIds) {\n const v = await figma.variables.getVariableByIdAsync(vid);\n const val = v.valuesByMode[light];\n const h = val && val.type === \"VARIABLE_ALIAS\" ? primHex[val.id] : (val && typeof val.r === \"number\" ? toHex(val) : null);\n if (h) cand.push({ v, name: v.name, hex: h, scopes: v.scopes, layer: 1 });\n}\n\nconst score = (c, prop, isText) => {\n const s = c.scopes;\n const all = s.includes(\"ALL_SCOPES\");\n if (prop === \"strokes\") {\n if (!all && !s.includes(\"STROKE_COLOR\")) return -1;\n } else if (isText) {\n if (!all && !s.includes(\"TEXT_FILL\")) return -1;\n } else {\n if (!all && !s.includes(\"FRAME_FILL\") && !s.includes(\"SHAPE_FILL\")) return -1;\n }\n let n = c.layer * 10 + (all ? 0 : 4);\n if (prop === \"strokes\" && c.name.startsWith(\"Border/\")) n += 3;\n if (prop === \"fills\" && isText && (c.name.startsWith(\"Text/\") || c.name.endsWith(\"/Fg\"))) n += 3;\n if (prop === \"fills\" && !isText && (c.name.startsWith(\"Surface/\") || c.name.endsWith(\"/Bg\"))) n += 3;\n return n;\n};\n\nconst cache = {};\nconst pick = (hex, prop, isText) => {\n const k = hex + prop + isText;\n if (cache[k] !== undefined) return cache[k];\n let best = null, bs = -1;\n for (const c of cand) {\n if (c.hex !== hex) continue;\n const s = score(c, prop, isText);\n if (s > bs) { bs = s; best = c; }\n }\n cache[k] = best;\n return best;\n};\n\nconst tally = {};\nlet touched = 0, remaining = 0, unmatched = 0;\nconst errors = [];\n\nfor (const n of page.findAll(() => true)) {\n if (n.type === \"SECTION\") continue;\n let needs = false;\n for (const prop of [\"fills\",\"strokes\"]) {\n const ps = n[prop];\n if (!Array.isArray(ps)) continue;\n for (const p of ps) {\n if (p.type === \"SOLID\" && p.visible !== false && p.color && !(p.boundVariables && p.boundVariables.color)) needs = true;\n }\n }\n if (!needs) continue;\n if (touched >= 120) { remaining++; continue; }\n\n try {\n for (const prop of [\"fills\",\"strokes\"]) {\n const ps = n[prop];\n if (!Array.isArray(ps) || !ps.length) continue;\n const copy = JSON.parse(JSON.stringify(ps));\n let dirty = false;\n for (let i = 0; i < copy.length; i++) {\n const p = copy[i];\n if (p.type !== \"SOLID\" || p.visible === false || !p.color) continue;\n if (p.boundVariables && p.boundVariables.color) continue;\n const c = pick(toHex(p.color), prop, n.type === \"TEXT\");\n if (!c) { unmatched++; continue; }\n copy[i] = figma.variables.setBoundVariableForPaint(p, \"color\", c.v);\n tally[c.name] = (tally[c.name] || 0) + 1;\n dirty = true;\n }\n if (dirty) n[prop] = copy;\n }\n touched++;\n } catch (e) { errors.push(`${n.id}: ${e.message}`); }\n}\n\nreturn { touchedNodes: touched, remainingNodes: remaining, unmatched, distinctTokensUsed: Object.keys(tally).length, errors: errors.slice(0,5) };</code></pre><p><code>layer * 10</code> でトークン層をプリミティブ層より優先し、スコープが具体的なものに <code>+4</code>、名前の役割が一致するものに <code>+3</code> を足す。この重み付けで、同じ値を持つ複数の変数から適切な1つが選ばれる。</p><p>3バッチに分けて実行した結果。</p><pre><code class=\"language-json\">{\"touchedNodes\":120,\"remainingNodes\":857,\"unmatched\":0,\"distinctTokensUsed\":20,\"errors\":[]}\n{\"touchedNodes\":500,\"remainingNodes\":357,\"unmatched\":0,\"errors\":[]}\n{\"touchedPaintSets\":376,\"unmatched\":0,\"rawPaintsRemaining\":0,\"rawSample\":[],\n \"tally\":{\"Surface/Container\":61,\"Surface/Overlay\":13,\"Neutral/Gray Light\":21,\"Action/Primary/Bg\":10,\"Text/Inverse\":6,\"Text/Primary\":79,\"Neutral/Gray Pale\":13,\"Nav/Inactive\":21,\"Delivery/Delivery\":5,\"Primary/Blue Deep\":23,\"Action/Secondary/Bg\":3,\"Action/Secondary/Fg\":3,\"Border/Strong\":33,\"Border/Default\":11,\"Surface/Default\":4,\"Neutral/Gray Slate\":27,\"Text/Secondary\":7,\"Nav/Active\":3,\"Background/Background3\":1,\"Neutral/Gray Faint\":8,\"Neutral/Gray Lighter\":16,\"Warning/Warning\":4,\"Status/Warning/Fg\":1,\"Primary/Blue\":3},\n \"errors\":[]}</code></pre><p>最終確認。</p><pre><code class=\"language-json\">{\n \"boundPaints\": 1170,\n \"rawPaints\": 0,\n \"coverage\": \"100%\",\n \"distinctVariables\": 38,\n \"top\": [[\"Text/Primary\",175],[\"Surface/Container\",170],[\"Nav/Inactive\",105],[\"Border/Strong\",95],[\"Neutral/Gray Medium\",65],[\"Primary/Blue Deep\",57],[\"Status/Success/Fg\",51],[\"Neutral/Gray Light\",51],[\"Neutral/Gray Slate\",48],[\"Border/Default\",41],[\"Surface/Overlay\",34],[\"Neutral/Gray Lighter\",33]]\n}</code></pre><p>生hexがゼロになった。ここまでは順調だった。</p><p></p><h2 id=\"index_0NT3hX_1\">同じ「配送中」が4つの変数に割れていた</h2><p>機械的な一致で繋ぐと、見た目は保たれる。しかし<strong>意味は揃わない</strong>。</p><p>「配送中」というタグの文字色が、画面によって別の変数を参照している状態が見つかった。ある画面では <code>Status/Delivery/Fg</code>(<code>#BF360C</code>)、別の画面では <code>Delivery/Delivery</code>(<code>#E65100</code>)。色まで違う。</p><p>原因ははっきりしていた。先に <code>Status/Delivery/Fg</code> を <code>#BF360C</code> に変えていたので、生の <code>#E65100</code> だったテキストは完全一致せず、プリミティブの <code>Delivery/Delivery</code> に落ちていた。hex一致という基準を使う以上、避けられない副作用だった。</p><p>同じ役割が別の変数を使っている箇所を探す監査を書いた。</p><pre><code class=\"language-javascript\">const page = await figma.getNodeByIdAsync(\"12580:25\");\nawait figma.setCurrentPageAsync(page);\n\nconst nameCache = {};\nconst getName = async (id) => {\n if (!nameCache[id]) {\n const v = await figma.variables.getVariableByIdAsync(id);\n nameCache[id] = v ? v.name : id;\n }\n return nameCache[id];\n};\n\nconst boundOf = async (n, prop) => {\n const ps = n[prop];\n if (!Array.isArray(ps) || !ps.length) return null;\n const p = ps[0];\n if (p.type !== \"SOLID\" || !p.boundVariables || !p.boundVariables.color) return null;\n return await getName(p.boundVariables.color.id);\n};\n\nconst byLabel = {};\nfor (const t of page.findAllWithCriteria({ types: [\"TEXT\"] })) {\n const key = (t.characters || \"\").trim();\n if (!key || key.length > 12) continue;\n const v = await boundOf(t, \"fills\");\n if (!v) continue;\n if (!byLabel[key]) byLabel[key] = {};\n byLabel[key][v] = (byLabel[key][v] || 0) + 1;\n}\n\nconst byFrame = {};\nfor (const f of page.findAllWithCriteria({ types: [\"FRAME\"] })) {\n if (!/Badge|Status|Button|Tab|Chip/i.test(f.name)) continue;\n const v = await boundOf(f, \"fills\");\n if (!v) continue;\n if (!byFrame[f.name]) byFrame[f.name] = {};\n byFrame[f.name][v] = (byFrame[f.name][v] || 0) + 1;\n}\n\nconst conflicts = (obj) => Object.entries(obj)\n .filter(([, m]) => Object.keys(m).length > 1)\n .map(([k, m]) => ({ key: k, variables: m }));\n\nconst tok = await figma.variables.getVariableCollectionByIdAsync(\"VariableCollectionId:1941:108641\");\nconst tokenNames = new Set();\nfor (const vid of tok.variableIds) tokenNames.add((await figma.variables.getVariableByIdAsync(vid)).name);\n\nlet toToken = 0, toPrimitive = 0;\nfor (const n of page.findAll(() => true)) {\n if (n.type === \"SECTION\") continue;\n for (const prop of [\"fills\",\"strokes\"]) {\n const ps = n[prop]; if (!Array.isArray(ps)) continue;\n for (const p of ps) {\n if (p.type !== \"SOLID\" || !p.boundVariables || !p.boundVariables.color) continue;\n const nm = await getName(p.boundVariables.color.id);\n if (tokenNames.has(nm)) toToken++; else toPrimitive++;\n }\n }\n}\n\nreturn {\n labelConflicts: conflicts(byLabel),\n frameConflicts: conflicts(byFrame),\n layerUsage: { semanticToken: toToken, primitiveDirect: toPrimitive }\n};</code></pre><p>結果の一部。</p><pre><code class=\"language-json\">{\n \"labelConflicts\": [\n {\"key\":\"配送中\",\"variables\":{\"Text/Primary\":2,\"Status/Delivery/Fg\":1,\"Nav/Inactive\":3,\"Delivery/Delivery\":3,\"Status/Success/Fg\":1}},\n {\"key\":\"キャンセル\",\"variables\":{\"Text/Secondary\":2,\"Nav/Inactive\":2,\"Warning/Warning Dark\":1}},\n {\"key\":\"完了\",\"variables\":{\"Nav/Inactive\":3,\"Status/Success/Fg\":5}},\n {\"key\":\"ホーム\",\"variables\":{\"Nav/Active\":1,\"Neutral/Gray Light\":3,\"Action/Secondary/Fg\":1}}\n ],\n \"frameConflicts\": [\n {\"key\":\"Status Badge\",\"variables\":{\"Status/Delivery/Bg\":2,\"Action/Secondary/Bg\":1}}\n ],\n \"layerUsage\": {\"semanticToken\":776,\"primitiveDirect\":394}\n}</code></pre><p>1,170のうち394がプリミティブ直参照だった。セマンティック層を飛ばしているので、意味が表現できていない状態になる。</p><p>ただし監査結果を読むときに注意が要る。「配送中」が4つの変数に割れていると出るが、実際に確認すると3つは正しい使い分けだった。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>文脈</p></th><th colspan=\"1\" rowspan=\"1\"><p>変数</p></th><th colspan=\"1\" rowspan=\"1\"><p>役割</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>セクション見出しの「配送中」</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>Text/Primary</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>ただの見出し</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>バッジの「配送中」</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>Status/Delivery/Fg</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>状態表示</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>統計の項目名「配送中」</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>Nav/Inactive</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>数値に付く補助ラベル</p></td></tr></tbody></table></div><p>同じ言葉でも役割が違えば色が違うのは正しい。機械的な一致検査は「割れている」としか言えないので、1件ずつ何を指しているか見る必要があった。実際に誤りだったのは、ロボット一覧の「配送中」が1号機は緑、2号機はグレーになっていた箇所だけだった。</p><p></p><h2 id=\"index_kGHrydZx\">フレーム名の部分一致で45ノードを壊した</h2><p>ここで自分のスクリプトが事故を起こした。</p><p>「待機中」というステータスを青からグレーに移す作業で、対象を絞る条件をこう書いた。</p><pre><code class=\"language-javascript\">for (const f of root.findAll(n => n.type === \"FRAME\" && /badge|chip|status/i.test(n.name))) {\n const ts = f.findAllWithCriteria({ types: [\"TEXT\"] });\n if (!ts.length) continue;\n const hit = ts.some(t => PENDING.test(t.characters || \"\"));\n if (!hit) continue;\n if (bind(f, \"fills\", byName[\"Status/Pending/Bg\"])) badges++;\n for (const t of ts) if (bind(t, \"fills\", byName[\"Status/Pending/Fg\"])) texts++;\n}</code></pre><p><code>/badge|chip|status/i</code> がフレーム名に部分一致するものを対象にしている。ここに <code>Delivery Status Screen</code> という画面全体のフレームが該当した。その中に「待機中」というテキストが1つでもあれば、<code>findAllWithCriteria</code> が拾う<strong>画面内の全テキスト</strong>が Pending トークンで塗り潰される。</p><p>被害は45ノード。「詳細を見る」ボタン、統計の数値、ナビゲーションのラベル、配送IDまで巻き込まれた。</p><p>さらに悪いことに、気づいてロールバックしたときに一律 <code>Text/Primary</code> へ戻してしまった。この時点で<strong>元の色情報が失われた</strong>。「75%」が緑だったのか、「配送ID」がグレーだったのかが、ノードからは読めなくなった。</p><pre><code class=\"language-json\">{\n \"pendingRollback\": [\n {\"text\":\"配送状況\",\"to\":\"Text/Primary\"},\n {\"text\":\"配送中\",\"to\":\"Text/Primary\"},\n {\"text\":\"2\",\"to\":\"Text/Inverse\"},\n {\"text\":\"配送ID: #D2024\",\"to\":\"Text/Primary\"},\n {\"text\":\"3階 302号室\",\"to\":\"Text/Primary\"},\n {\"text\":\"到着予定: 14:25 \",\"to\":\"Text/Primary\"},\n {\"text\":\"出発完了\",\"to\":\"Text/Primary\"},\n {\"text\":\"75%\",\"to\":\"Text/Primary\"},\n {\"text\":\"追跡\",\"to\":\"Text/Primary\"},\n {\"text\":\"詳細を見る\",\"to\":\"Text/Inverse\"}\n ],\n \"textTokenMisuse\": 7\n}</code></pre><p>救いは、この画面と同一内容のコピーが別ページに存在したことだった。そちらは無傷だったので、参照元として使えた。</p><p>正しいトークン割り当てを、テキスト内容・フォントサイズ・親フレーム名の3つをキーにして抽出する。</p><pre><code class=\"language-javascript\">const page = await figma.getNodeByIdAsync(\"12580:25\");\nawait figma.setCurrentPageAsync(page);\nconst nameCache = {};\nconst getName = async (id) => { if(!nameCache[id]){const v=await figma.variables.getVariableByIdAsync(id); nameCache[id]=v?v.name:id;} return nameCache[id]; };\n\nconst ref = {};\nfor (const t of page.findAllWithCriteria({ types: [\"TEXT\"] })) {\n const ps = t.fills;\n if (!Array.isArray(ps) || !ps.length) continue;\n const q = ps[ps.length-1];\n if (q.type !== \"SOLID\" || !q.boundVariables || !q.boundVariables.color) continue;\n const v = await getName(q.boundVariables.color.id);\n const key = `${(t.characters||\"\").trim()}|${Math.round(t.fontSize)}|${t.parent ? t.parent.name : \"\"}`;\n if (!ref[key]) ref[key] = {};\n ref[key][v] = (ref[key][v] || 0) + 1;\n}\nconst map = {};\nfor (const [k, m] of Object.entries(ref)) {\n map[k] = Object.entries(m).sort((a,b)=>b[1]-a[1])[0][0];\n}\nreturn { entries: Object.keys(map).length, map };</code></pre><p>282件の対応表が取れた。抜粋。</p><pre><code>配送中|13|Status Badge → Status/Delivery/Fg\n75%|16|Progress Text → Status/Success/Fg\n配送ID: #D2024-0415|14|Delivery Info → Nav/Inactive\n詳細を見る|14|Details Button → Text/Inverse\n待機中|13|Status Badge → Status/Pending/Fg\n完了: 今日 13:45|12|Time Stamp → Text/Muted</code></pre><p>これを壊れたページに適用して130箇所を復元した。</p><pre><code class=\"language-json\">{\n \"applied\": 130,\n \"hits\": {\"Text/Primary\":52,\"Text/Muted\":30,\"Status/Delivery/Fg\":8,\"Status/Success/Fg\":13,\"Text/Secondary\":3,\"Text/Inverse\":10,\"Status/Warning/Fg\":3,\"Action/Secondary/Fg\":10,\"Status/Pending/Fg\":1}\n}</code></pre><p>参照先が無ければ手作業になっていた。一括処理の対象を「フレーム名の部分一致」で絞ったのが原因なので、以降はテキスト内容と構造で絞るように変えた。</p><p></p><h2 id=\"index_zodgOGH5\">重複と衝突は別の問題だった</h2><p>作業が一巡した時点で、トークンは35個になっていた。「多すぎないか」を判断するために、同じ値に解決されるトークンを機械的に洗い出した。</p><pre><code class=\"language-javascript\">const sig = {};\nconst tokens = [];\nfor (const vid of tok.variableIds) {\n const v = await figma.variables.getVariableByIdAsync(vid);\n const l = res(v.valuesByMode[light]), n = res(v.valuesByMode[night]);\n tokens.push({ name: v.name, light: l, night: n });\n const k = l + \"/\" + n;\n (sig[k] = sig[k] || []).push(v.name);\n}\nconst dupes = Object.entries(sig).filter(([,a]) => a.length > 1)\n .map(([k,a]) => ({ value: k, tokens: a }));</code></pre><pre><code class=\"language-json\">{\n \"duplicateValues\": [\n {\"value\":\"#FFFFFF/#1A1A1A\",\"tokens\":[\"Surface/Container\",\"Action/Primary/Fg\",\"Text/Inverse\",\"Action/Success/Fg\",\"Icon/Inverse\"]},\n {\"value\":\"#1A1A1A/#FFFFFF\",\"tokens\":[\"Text/Primary\",\"Icon/Strong\"]},\n {\"value\":\"#E3F2FD/#0277BD\",\"tokens\":[\"Action/Secondary/Bg\",\"Status/Pending/Bg\"]},\n {\"value\":\"#2E7D32/#81C784\",\"tokens\":[\"Status/Success/Fg\",\"Status/Success/Solid\"]},\n {\"value\":\"#E53935/#FFEBEE\",\"tokens\":[\"Status/Warning/Fg\",\"Status/Warning/Solid\"]},\n {\"value\":\"#BF360C/#FFF3E0\",\"tokens\":[\"Status/Delivery/Fg\",\"Status/Delivery/Solid\"]},\n {\"value\":\"#1565C0/#4FC3F7\",\"tokens\":[\"Nav/Active\",\"Status/Pending/Fg\"]}\n ]\n}</code></pre><p>35個のトークンが実質25色しか表現していない。だが、この重複には<strong>残すべきもの</strong>と<strong>潰すべきもの</strong>があった。</p><div class=\"embed\"><iframe src=\"/embeds/figma-design-token-consolidation-CwxN66LU.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p><code>Text/Inverse</code> と <code>Surface/Container</code> はどちらも白だが、軸が違う(文字か面か)。将来テーマを増やしたときに別々に動く。これは設計として正しい重複で、潰すと表現力が落ちる。</p><p>一方 <code>Icon/Strong</code> は <code>Text/Primary</code> と完全に同じだった。このデザインではアイコンと文字が常に同色なので、アイコン用の軸を分けた判断自体が過剰だった。これは潰すべき重複になる。</p><p>そして本当の問題は、重複ではなく<strong>衝突</strong>のほうにあった。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>色</p></th><th colspan=\"1\" rowspan=\"1\"><p>意味1</p></th><th colspan=\"1\" rowspan=\"1\"><p>意味2</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>#1565C0</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>Nav/Active(現在地)</p></td><td colspan=\"1\" rowspan=\"1\"><p>Status/Pending/Fg(待機中)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>#E3F2FD</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>Action/Secondary/Bg(押せる)</p></td><td colspan=\"1\" rowspan=\"1\"><p>Status/Pending/Bg(待機中)</p></td></tr></tbody></table></div><p>青が「押せる」「現在地」「待機中」の3役を兼ねていた。画面を見た人は、青いものが操作対象なのか状態表示なのか区別できない。</p><p>これはトークンを減らして解決する問題ではない。<strong>逆に分けるべき</strong>箇所だった。「待機中」を青から外してグレー系に移し、青は操作と現在地に残した。</p><p>同じ構造の問題は青の3色にもあった。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>色</p></th><th colspan=\"1\" rowspan=\"1\"><p>用途</p></th><th colspan=\"1\" rowspan=\"1\"><p>白文字とのコントラスト</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>#2196F3</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>Action/Primary/Bg(「詳細を見る」ボタン)</p></td><td colspan=\"1\" rowspan=\"1\"><p>3.12:1</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>#1976D2</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>混入色 57箇所</p></td><td colspan=\"1\" rowspan=\"1\"><p>4.60:1</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>#1565C0</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>Nav/Active</p></td><td colspan=\"1\" rowspan=\"1\"><p>5.75:1</p></td></tr></tbody></table></div><p>一番明るい <code>#2196F3</code> が一番使われていて、しかも一番読みにくい。<code>#1565C0</code> に統合したことで3色が1色になり、ボタンのコントラストも同時に直った。</p><p>整理の結果、トークンは35個から30個に減り、色は19色になった。<strong>トークンは増えて色は減る</strong>。意味の種類を細かく定義するほど、同じ色を使い回せるようになる。</p><p></p><h2 id=\"index_oH00RcGT\">残りのページに同じ処理をかけると、35色が17色になった</h2><p>テストページで確立した手順を、本体のページにある4つのセクション(スタイルガイド、タブレットUI、アプリUI、PCダッシュボード)へ適用した。合計約4,700ノード、生hex 2,951箇所。</p><p>事前調査で、既に変数が入っている900箇所が<strong>全てプリミティブ直参照</strong>だと分かった。セマンティック層を経由していないので、トークンの値を変えても反映されない状態になっていた。</p><p>パレットに無い色も7つ出てきた。</p><pre><code class=\"language-json\">{\n \"distinctRawColors\": 35,\n \"totalRawPaints\": 2951,\n \"exactMatches\": 28,\n \"needsDecision\": [\n {\"hex\":\"#757575\",\"count\":454,\"match\":\"Neutral/Gray Slate #6B7280\",\"dist\":15},\n {\"hex\":\"#FF9800\",\"count\":56,\"match\":\"Accent/Amber #FFB300\",\"dist\":27},\n {\"hex\":\"#000000\",\"count\":6,\"match\":\"Neutral/Black #1A1A1A\",\"dist\":45},\n {\"hex\":\"#EDEDED\",\"count\":4,\"match\":\"Neutral/Gray Faint #F0F0F0\",\"dist\":5},\n {\"hex\":\"#7B1FA2\",\"count\":4,\"match\":\"Neutral/Gray Slate #6B7280\",\"dist\":91},\n {\"hex\":\"#F57C00\",\"count\":4,\"match\":\"Delivery/Delivery #E65100\",\"dist\":46},\n {\"hex\":\"#F3E5F5\",\"count\":2,\"match\":\"Neutral/Gray Faint #F0F0F0\",\"dist\":12}\n ]\n}</code></pre><p><code>#FF9800</code> の正体を調べると、PCダッシュボードのステータスドット、タイムラインアイコン、「配送中」テキストに使われていた。つまり <code>#E65100</code> と同じ意味を、別の色で表現していた箇所だった。白背景でのコントラストは 2.16:1 で、テキストとしては極めて読みにくい。</p><p>紫の <code>#7B1FA2</code> はもっと分かりやすかった。タブレットUIの「Card 5」に使われていたが、<strong>同じカードの別コピーでは橙</strong>(<code>#FFF3E0</code>)だった。意図的な色分けではなく、コピー間で崩れた残骸だった。橙に揃えることで紫が消えた。</p><p>用途別に振り分けるマッピングを書いた。</p><pre><code class=\"language-javascript\">const roleMap = (hex, prop, isText) => {\n const T = isText && prop === \"fills\";\n const solidFill = prop === \"fills\" && !isText;\n switch (hex) {\n case \"#FF9800\": case \"#F57C00\": case \"#E65100\": case \"#BF360C\": case \"#7B1FA2\":\n return \"Status/Delivery/Fg\";\n case \"#FFF3E0\": case \"#F3E5F5\": return \"Status/Delivery/Bg\";\n case \"#4CAF50\": case \"#2E7D32\": case \"#81C784\": return \"Status/Success/Fg\";\n case \"#E8F5E9\": return \"Status/Success/Bg\";\n case \"#E53935\": case \"#C62828\": return \"Status/Warning/Fg\";\n case \"#FFEBEE\": return \"Status/Warning/Bg\";\n case \"#2196F3\": case \"#1976D2\": case \"#1565C0\": case \"#0277BD\": case \"#03A9F4\":\n case \"#4FC3F7\":\n return solidFill ? \"Action/Primary/Bg\" : \"Action/Secondary/Fg\";\n case \"#E3F2FD\": case \"#E1F5FE\": case \"#BBDEFB\": case \"#90CAF9\": case \"#B3E5FC\":\n case \"#BDE8FB\":\n return \"Action/Secondary/Bg\";\n case \"#FFFFFF\": return solidFill ? \"Surface/Container\" : \"Text/Inverse\";\n case \"#1A1A1A\": case \"#000000\": return \"Text/Primary\";\n case \"#424242\": return \"Text/Secondary\";\n case \"#757575\": case \"#6B6B6B\": case \"#6B7280\": case \"#9E9E9E\":\n return T ? \"Text/Muted\" : \"Icon/Default\";\n case \"#BDBDBD\": return \"Icon/Subtle\";\n case \"#E0E0E0\": case \"#F0F0F0\": case \"#EDEDED\": return \"Border/Default\";\n case \"#F5F5F5\": return \"Surface/Overlay\";\n case \"#F8F9FA\": return \"Surface/Default\";\n case \"#FFB300\": case \"#FFB74D\": return \"Rating/Active\";\n }\n return null;\n};</code></pre><p>3バッチで全ノードを処理した結果。</p><pre><code class=\"language-json\">{\"touched\":700,\"remaining\":3002,\"unmapped\":{\"#4FC3F7\":5,\"#BBDEFB\":1,\"#90CAF9\":1,\"#B3E5FC\":1}}\n{\"touched\":1600,\"remaining\":1410,\"unmapped\":{\"#BDE8FB\":4}}\n{\"touched\":1414,\"remaining\":0,\"unmapped\":{}}</code></pre><p>最終状態。</p><pre><code class=\"language-json\">{\n \"boundPaints\": 3843,\n \"rawPaints\": 0,\n \"textChecked\": 1314,\n \"distinctColors\": 17,\n \"palette\": [[\"#6B6B6B\",917],[\"#FFFFFF\",621],[\"#1565C0\",508],[\"#1A1A1A\",506],[\"#2E7D32\",313],[\"#E0E0E0\",201],[\"#BF360C\",159],[\"#424242\",131],[\"#F5F5F5\",104],[\"#BDBDBD\",80],[\"#F8F9FA\",80],[\"#C62828\",75],[\"#E3F2FD\",63],[\"#E8F5E9\",38],[\"#FFF3E0\",31],[\"#FFB300\",9],[\"#FFEBEE\",7]]\n}</code></pre><p>35色が17色になった。</p><p></p><h2 id=\"index_qb_dGX_Y\">コントラストは、1色を変えるだけで33件直った</h2><p>全テキストのコントラストを測る監査を書いた。各テキストノードから親をたどって最初に不透明な塗りを持つ祖先を探し、その色を背景とみなして比率を出す。</p><pre><code class=\"language-javascript\">const lum = (c) => {\n const f = (v) => v <= 0.03928 ? v/12.92 : Math.pow((v+0.055)/1.055, 2.4);\n return 0.2126*f(c.r) + 0.7152*f(c.g) + 0.0722*f(c.b);\n};\nconst ratio = (a,b) => { const L1=lum(a), L2=lum(b); const hi=Math.max(L1,L2), lo=Math.min(L1,L2); return (hi+0.05)/(lo+0.05); };\nconst solidFill = (n) => {\n const ps = n.fills;\n if (!Array.isArray(ps)) return null;\n for (let i = ps.length-1; i >= 0; i--) if (ps[i].type === \"SOLID\" && ps[i].visible !== false && (ps[i].opacity ?? 1) > 0.9) return ps[i].color;\n return null;\n};\n\nconst fails = []; let checked = 0;\nfor (const t of page.findAllWithCriteria({ types: [\"TEXT\"] })) {\n const fg = solidFill(t);\n if (!fg) continue;\n let p = t.parent, bg = null;\n while (p && p.type !== \"PAGE\") { const c = solidFill(p); if (c) { bg = c; break; } p = p.parent; }\n if (!bg) continue;\n const r = ratio(fg, bg);\n const bold = /Bold|Black|Medium/i.test(t.fontName && t.fontName.style ? t.fontName.style : \"\");\n const large = t.fontSize >= 24 || (t.fontSize >= 18.66 && bold);\n const need = large ? 3.0 : 4.5;\n checked++;\n if (r < need) fails.push({ text: (t.characters||\"\").slice(0,14), size: t.fontSize, fg: toHex(fg), bg: toHex(bg), ratio: Math.round(r*100)/100, need });\n}</code></pre><p>321件を測って33件が不合格だった。全て 4.03〜4.37:1 の僅差で、原因は2つに集約された。</p><pre><code class=\"language-json\">{\n \"textNodesChecked\": 321,\n \"failures\": [\n {\"text\":\"2分前\",\"size\":11,\"fg\":\"#757575\",\"bg\":\"#E3F2FD\",\"ratio\":4.03,\"need\":4.5},\n {\"text\":\"完了\",\"size\":13,\"fg\":\"#757575\",\"bg\":\"#F5F5F5\",\"ratio\":4.23,\"need\":4.5},\n {\"text\":\"キャンセル\",\"size\":12,\"fg\":\"#E53935\",\"bg\":\"#FFEBEE\",\"ratio\":3.7,\"need\":4.5},\n {\"text\":\"配送ID: #D2024-0\",\"size\":13,\"fg\":\"#757575\",\"bg\":\"#F5F5F5\",\"ratio\":4.23,\"need\":4.5},\n {\"text\":\"ご利用ありがとうございました\",\"size\":14,\"fg\":\"#757575\",\"bg\":\"#F8F9FA\",\"ratio\":4.37,\"need\":4.5},\n {\"text\":\"ロボット2号機が配送中です\",\"size\":14,\"fg\":\"#757575\",\"bg\":\"#FFF3E0\",\"ratio\":4.2,\"need\":4.5},\n {\"text\":\"中止\",\"size\":16,\"fg\":\"#E53935\",\"bg\":\"#FFFFFF\",\"ratio\":4.23,\"need\":4.5}\n ]\n}\n</code></pre><p><code>#757575</code> は白背景では 4.61:1 で合格していた。ところが <code>#F5F5F5</code> や <code>#E3F2FD</code> のような淡い色の上に乗ると 4.03 まで落ちる。カード内や通知内で使われているので、実際には多くが不合格だった。<strong>白でだけ測っていたら見逃していた。</strong></p><p>修正はトークンの参照先を変えるだけで済んだ。</p><pre><code class=\"language-javascript\">byName[\"Neutral/Gray Medium\"].setValueForMode(primMode, { r: 0x6B/255, g: 0x6B/255, b: 0x6B/255 });\nbyName[\"Status/Warning/Fg\"].setValueForMode(light, A(\"Warning/Warning Dark\"));\nbyName[\"Border/Strong\"].setValueForMode(light, A(\"Neutral/Gray Medium\"));\n</code></pre><p><code>Neutral/Gray Medium</code> を <code>#757575</code> から <code>#6B6B6B</code> に1段階暗くしただけで、<code>Text/Muted</code>・<code>Nav/Inactive</code>・<code>Icon/Default</code> が一斉に改善し、342箇所が直った。</p><pre><code class=\"language-json\">{\"textNodesChecked\":321,\"failures\":[],\"boundPaints\":1170,\"rawPaints\":0,\"distinctColors\":19}\n</code></pre><p>本体ページ側も同様に測り、1,314件中0件不合格になった。残った5件はフロアマップのラベルで、監査が親をたどる仕組みのため、下に敷かれた兄弟図形を見ていない誤検知だった。実際の背景を調べると 5.13〜5.75:1 で合格していた。</p><pre><code class=\"language-json\">[\n {\"text\":\"エレベーター\",\"size\":24,\"actualBackdrop\":{\"node\":\"Vector\",\"hex\":\"#6B6B6B\",\"ratio\":5.33}},\n {\"text\":\"1\",\"size\":14,\"actualBackdrop\":{\"node\":\"Vector\",\"hex\":\"#1565C0\",\"ratio\":5.75}},\n {\"text\":\"2\",\"size\":14,\"actualBackdrop\":{\"node\":\"Vector\",\"hex\":\"#BF360C\",\"ratio\":5.6}},\n {\"text\":\"3\",\"size\":14,\"actualBackdrop\":{\"node\":\"Vector\",\"hex\":\"#2E7D32\",\"ratio\":5.13}}\n]</code></pre><p></p><h2 id=\"index_i0iCDq9T\">押せるものと状態表示が同じ色をしていた</h2><p>最後に見つかったのは、色の数ではなく<strong>役割の見分けがつかない</strong>という問題だった。</p><p>「稼働中」というステータスバッジと、「管理者」「更新」というボタンが、どちらも薄い青地に青文字だった。同じ配色なので、見ただけではどちらが押せるのか分からない。</p><p>該当する要素を洗い出して分類した。</p><pre><code class=\"language-javascript\">const CONTROL = /button|btn|chip|tab$|tab |action|filter|refresh|toggle|switch|link|user|account|menu item|pagination|selector/i;\nconst STATUS = /badge|status|label|state|pill/i;\n\nconst rows = [];\nfor (const secId of [\"1185:84984\",\"1329:97699\",\"101:2995\"]) {\n const root = await figma.getNodeByIdAsync(secId);\n for (const f of root.findAll(n => n.type === \"FRAME\")) {\n const bg = await varOf(f, \"fills\");\n if (bg !== \"Action/Secondary/Bg\") continue;\n const t = f.findAllWithCriteria({ types: [\"TEXT\"] })[0];\n if (!t) continue;\n const fg = await varOf(t, \"fills\");\n const isControl = CONTROL.test(f.name);\n const isStatus = STATUS.test(f.name) && !isControl;\n rows.push({\n section: root.name.replace(\"薬剤搬送ロボット-\",\"\"),\n id: f.id, node: f.name, text: (t.characters||\"\").slice(0,10),\n size: `${Math.round(f.width)}x${Math.round(f.height)}`,\n fg, kind: isControl ? \"操作\" : isStatus ? \"状態\" : \"不明\"\n });\n }\n}</code></pre><pre><code class=\"language-json\">{\"total\":32,\"byKind\":{\"不明\":11,\"状態\":8,\"操作\":13}}</code></pre><p>32件が同じ薄青ピル構成で、うち13件が操作(管理者×4、更新、割当×2、追跡、エクスポート、詳細×4)、8件が状態表示だった。残り11件は幅200px超の情報パネルで、ピルではないので対象外とした。</p><p>操作側だけを白文字+黒地に移した。新しくトークンを2つ追加している。</p><pre><code class=\"language-javascript\">mk(\"Action/Neutral/Bg\", [\"FRAME_FILL\",\"SHAPE_FILL\"], \"Neutral/Black\", \"Neutral/White\");\nmk(\"Action/Neutral/Fg\", [\"TEXT_FILL\",\"STROKE_COLOR\",\"SHAPE_FILL\"], \"Neutral/White\", \"Neutral/Black\");\n</code></pre><p>コントラストは 17.4:1 になった。状態表示は薄青のまま残したので、色で役割が分かれる。</p><p></p><h2 id=\"index_RiZnIe2p\">結果</h2><p>2ページ・5区画に対して行った作業の集計。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>指標</p></th><th colspan=\"1\" rowspan=\"1\"><p>作業前</p></th><th colspan=\"1\" rowspan=\"1\"><p>作業後</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>生hexのペイント</p></td><td colspan=\"1\" rowspan=\"1\"><p>3,977</p></td><td colspan=\"1\" rowspan=\"1\"><p>0</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>変数接続</p></td><td colspan=\"1\" rowspan=\"1\"><p>1,044</p></td><td colspan=\"1\" rowspan=\"1\"><p>5,013</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>使用色(テストページ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>26</p></td><td colspan=\"1\" rowspan=\"1\"><p>19</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>使用色(本体4区画)</p></td><td colspan=\"1\" rowspan=\"1\"><p>35</p></td><td colspan=\"1\" rowspan=\"1\"><p>17</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>プリミティブ</p></td><td colspan=\"1\" rowspan=\"1\"><p>29</p></td><td colspan=\"1\" rowspan=\"1\"><p>36</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>セマンティックトークン</p></td><td colspan=\"1\" rowspan=\"1\"><p>20</p></td><td colspan=\"1\" rowspan=\"1\"><p>32</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>AA不合格のテキスト</p></td><td colspan=\"1\" rowspan=\"1\"><p>多数</p></td><td colspan=\"1\" rowspan=\"1\"><p>0(1,635件検証)</p></td></tr></tbody></table></div><p>作業を通して分かったことが3つある。</p><p><strong>hex完全一致は安全だが、意味は揃わない。</strong> 値が同じ変数に繋ぐ限り見た目は変わらないので、事故のリスクはほぼ無い。ただし「同じ役割が同じ変数を使う」状態には自動的にはならない。役割で揃えるには、ノードの構造や文脈を見て1件ずつ判断するしかなかった。</p><p><strong>コントラストは白背景だけで測ると見逃す。</strong> <code>#757575</code> は白の上では合格していた。淡い背景の上に乗った瞬間に落ちる。実際に使われている組み合わせで測って初めて33件が出てきた。</p><p><strong>色を減らす作業は、トークンを増やす作業だった。</strong> 重複したトークンは潰すべきだが、1つの色が複数の意味を兼ねている「衝突」は、分けなければ解決しない。最終的にトークンは20個から32個に増え、色は35色から17色に減った。意味の種類を細かく定義するほど、同じ色を使い回せるようになる。</p><p>そして、一括処理の対象を絞る条件は慎重に書く必要がある。<code>/badge|chip|status/i</code> という4文字の正規表現が <code>Delivery Status Screen</code> に一致して45ノードを壊した。同一内容のコピーが別ページに残っていたから復元できたが、無ければ手作業になっていた。</p><h5>※ プロジェクト名「R2R2R」は、UC Berkeley・Toyota Research Instituteの研究「Real2Render2Real」(Yu, Fu et al., 2025)の概念に触発されています。現実空間をデジタルツイン化し、仮想空間での試行から得た知見を実世界のモーター駆動へ還元する——その設計と実装過程を毎日記録しています。技術アプローチおよび目的は独自のものです。</h5>", "toc": [{"id": "index_HG1_L3sK", "text": "Figmaコネクタが読み取ったもの"}, {"id": "index_S4iJx9Qp", "text": "変数は存在していた。画面が使っていなかっただけだった"}, {"id": "index_zgKsR6Q4", "text": "先にコントラストを測ると、4色が基準に届いていなかった"}, {"id": "index_7WSs2S7Y", "text": "「Noto Sans JP SemiBold」は存在しなかった"}, {"id": "index_ZUW562PF", "text": "hex完全一致で機械的に繋ぐと、見た目を変えずに生hexがゼロになる"}, {"id": "index_0NT3hX_1", "text": "同じ「配送中」が4つの変数に割れていた"}, {"id": "index_kGHrydZx", "text": "フレーム名の部分一致で45ノードを壊した"}, {"id": "index_zodgOGH5", "text": "重複と衝突は別の問題だった"}, {"id": "index_oH00RcGT", "text": "残りのページに同じ処理をかけると、35色が17色になった"}, {"id": "index_qb_dGX_Y", "text": "コントラストは、1色を変えるだけで33件直った"}, {"id": "index_i0iCDq9T", "text": "押せるものと状態表示が同じ色をしていた"}, {"id": "index_RiZnIe2p", "text": "結果"}]}
\ No newline at end of file
diff --git a/src/data/records/first-video-end-to-end.json b/src/data/records/first-video-end-to-end.json
index e51bc70..9a61956 100644
--- a/src/data/records/first-video-end-to-end.json
+++ b/src/data/records/first-video-end-to-end.json
@@ -1 +1 @@
-{"day": "LOG333", "series": "ロボティクスデザイン", "published": "2026-10-12T07:58:00.000Z", "title": "解説動画の1本目を公開まで通す ― 声・BGM・間・直し・サムネイルを部品ごとに決め、23手順にまとめた", "slug": "first-video-end-to-end", "cover": null, "thumb": "/thumbs/r-first-video-end-to-end.html", "lang": "JP", "description": "解説動画「フィジカルAIの現在地」をハコブくんの声で作り直し、YouTubeに公開した。声・BGM・効果音・間・不具合の直し・概要欄・サムネイルを部品ごとに決め、次の動画から使う23手順にまとめた。", "updated": "2026-10-12T23:58:54.000Z", "body": "<!--TOC--><p></p><hr><h2 id=\"index_5Npkigty\">title: \"解説動画の1本目を公開まで通す ― 声・BGM・間・直し・サムネイルを部品ごとに決め、23手順にまとめた\" slug: first-video-end-to-end date: 2026-10-12 day: 333 meta_description: \"解説動画「フィジカルAIの現在地」をハコブくんの声で作り直し、YouTubeに公開した。声・BGM・効果音・間・不具合の直し・概要欄・サムネイルを部品ごとに決め、次の動画から使う23手順にまとめた。\" tags: [YouTube, 解説動画, ハコブくん, ElevenLabs, BGM, 効果音, ffmpeg, Python, Playwright, サムネイル, 動画制作]</h2><p>試作で作った解説動画「フィジカルAIの現在地」を、ナレーション役のハコブくんの声で作り直し、YouTube に公開しました。公開したタイトルは「人型ロボットの出荷は4倍。ロボットにも「ChatGPTの瞬間」は来たのか|フィジカルAIの現在地」、長さは12分29秒です。</p><p>台本・図・字幕・ハコブくんの動きは、これまでの記事で作ってきました(構成と台本の検証は LOG330、ハコブくんの口と目の動きは LOG331、チャンネルの見た目は LOG332)。この記事は、そこから公開までに決めたことの記録です。声、BGM と効果音、文と文の間、見つかった不具合の直し、YouTube の概要欄、サムネイル、公開した直後に見えたこと、の順に書いています。最後に、次の動画から同じ順番で作れるようにまとめた23の手順を載せました。</p><p>作業は、Claude のクラウドの作業環境でスクリプトを動かし、ElevenLabs で声と音楽を作る所だけを手元の Mac で動かす形で進めました。ElevenLabs の API キーは Mac の中だけで使い、Claude には渡していません。API キーが写っている画面の写真は、この記事に載せていません。</p><p>作ったスクリプトは、記事の後半にすべて全文で載せています。書き出しのログと、測った数値も全量で載せています。</p><p></p><figure><img src=\"/media/s-1280x720_v-fms_webp_da0a9183-719d-40ae-8829-cf3de0ab3f27.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_R8Gtcv3i\">公開した動画は12分29秒・146文・効果音45個</h2><p>公開した動画の中身は、次のとおりです。数値は、YouTube に上げたファイルを ffprobe(動画の中身を調べるツール)と ffmpeg の音量測定で測ったものです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>値</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>長さ</p></td><td colspan=\"1\" rowspan=\"1\"><p>12分29秒(749.1秒・22,473コマ)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>文の数</p></td><td colspan=\"1\" rowspan=\"1\"><p>146文(字幕の字数で約4,300字)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ナレーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくん(ElevenLabs のボイスデザインで作った声・モデル Eleven v4)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM</p></td><td colspan=\"1\" rowspan=\"1\"><p>ElevenLabs Music で作った1曲(bgm_明るいB_2・60秒)を最初から最後までループ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>45個。場面の切り替えの「ホワーン」37個、大きな流れの変わり目の「ポロロン」7個、自己紹介の「キラリン」1個</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>音の大きさ</p></td><td colspan=\"1\" rowspan=\"1\"><p>-16.0 LUFS(YouTube で聞く大きさの目安にそろえた)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>映像</p></td><td colspan=\"1\" rowspan=\"1\"><p>H.264、1920×1080、30コマ/秒</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>上げたファイル</p></td><td colspan=\"1\" rowspan=\"1\"><p>65,655,407 バイト(約66MB)</p></td></tr></tbody></table></div><p>動画は、冒頭で「今年の上半期に出荷された人型ロボットは約4倍」という推計を出し、「ロボットにも ChatGPT のような瞬間はもう来たのか」を問いにしています。答えは「写真を見せて判断させる入口は簡単になった。物を確実に動かすところは、まだ手間がかかる」です。そのあと3つの章(出荷の中身、机の上のロボットアームで分かったこと、データの壁)を見て、ロボットのニュースを見るときの2つの問いで終わります。</p><p>チャンネルは <a href=\"https://www.youtube.com/@PixelstoMotors\" data-has-link=\"true\">Pixels to Motors の YouTube</a> です。</p><h2 id=\"index_iJ5F55aG\">ハコブくんの声は、ElevenLabs のボイスデザインで作った「候補6」に決めた</h2><p>ナレーションはハコブくんが話すと決めています(LOG331)。声は、ほかのチャンネルと同じ声を使わず、ハコブくん専用に作ります。ElevenLabs のボイスデザイン(声の特徴を文章で書くと、新しい声の候補を作る機能)を有料プランで1か月試すことにしました。</p><p>声の候補をいくつか作って聞き比べ、「候補6(明るい30代前半のお手伝いロボット)」の2つ目のパターンに決めました。落ち着いた30代の声(候補1)は、読みは正確でしたが、ハコブくんの見た目に合いませんでした。</p><p>読み上げのモデルは、冒頭の14文を Multilingual v2 と Eleven v4 で読ませて聞き比べ、Eleven v4 にしました。話す速さは既定のままです。1秒に約6.8字で、仮の音声(Open JTalk)の約5.2字より速く、動画は仮の音声の版より短くなりました。</p><p>声づくりは、手元の Mac で <code>tts_eleven.py</code> を動かして1文ずつ mp3 にし、それを Claude の作業環境で <code>tts_import.py</code> が書き出し用の wav に変える流れです。文の前後の無音は 0.05 秒を残して切り、文と文の間は台本に書いた「後ろの間」で決めます。</p><h2 id=\"index_qsSC6Gf9\">「エイイチ」の抑揚は、ひらがなの「えいいち」で渡すとそろった</h2><p>通しで聞くと、「エイイチ」という名前の抑揚が文ごとに違っていました。</p><blockquote><p>エイイチのイントネーションが揃っていないので、全て同じイントネーションに修正してください(語尾が下がる方が通常のイントネーションかな)</p></blockquote><p>ElevenLabs に渡す文の書き方で、読み方が変わります。そこで <code>tts_eleven.py</code> に、台本ではなく試しの文だけを音声にする <code>--try</code> を足し、同じ文を4通りの書き方で作りました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>番号</p></th><th colspan=\"1\" rowspan=\"1\"><p>ElevenLabs に渡した書き方</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>エイイチ(カタカナ)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>栄一(漢字)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>えいいち(ひらがな)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>エーイチ(伸ばす音)</p></td></tr></tbody></table></div><blockquote><p>3回が正しい発音でした。</p></blockquote><p>ひらがなの「えいいち」が、語尾の下がる読みでそろいました。台本 <code>narration.py</code> に <code>ELEVEN_READ</code>(ElevenLabs に渡すときだけ文字を置きかえる表)を作り、「エイイチ」を「えいいち」に置きかえています。字幕はカタカナのままです。</p><p>もう1か所、読み間違いが見つかりました。</p><blockquote><p>3分16秒あたりのセリフが、裏表裏になっています(テキストは合っている)</p></blockquote><p>37番の文で、「裏表」が「うらおもてうら」のように読まれていました。字幕の文字は合っていて、音声だけが違います。<code>ELEVEN_READ</code> に「裏表 → うらおもて」を足し、その文だけ作り直しました。仮の音声用の読み替え表 <code>READ</code> とは別に持っているのは、同じ語でも、エンジンによって間違え方が違うためです。</p><h2 id=\"index_NvWums4o\">BGM は、コードで作った音から ElevenLabs Music の生楽器の曲に替えた</h2><p>BGM は、最初にコードで仮の曲を作りました(<code>sound_synth.py</code>。やわらかい和音と小さな粒の音を数式で鳴らしたもの)。混ぜ方を確かめるための仮の曲です。</p><blockquote><p>音源はシンセサイザーっぽいのではなくて、ほんのもう少しだけ温かみのある音源が良さそう(テーマが真面目なので、少し音源には彩りを加えたい的な)</p></blockquote><p>コードでピアノ風・ギターとマリンバ風・ピッツィカート風の3曲を作り直しましたが、方向は変わりませんでした。</p><blockquote><p>音源はもっと生音っぽい方がいいかな。音源使えるのある?</p></blockquote><p>そこで、使える音源を調べて表にしました(<code>音源_候補.md</code>。全文は後半)。条件は、YouTube で収益化しても使えること、クレジット表記の条件を守れること、ほかのチャンネルと同じ曲になりにくいことです。第一候補を ElevenLabs Music(文章で曲の雰囲気を書くと、新しい曲を作る機能)にしました。声と同じプランで使え、新しく作る曲なので、ほかのチャンネルと同じ曲にならないためです。</p><p>ElevenLabs Music を API から使うと、最初は次のエラーで止まりました。</p><pre><code>HTTP 401 missing_permissions music_generation\n</code></pre><p>使っていた API キーに、音楽を作る許可がありませんでした。新しいキーを作り直し、「読み上げ・効果音・音楽生成」の3つだけを許可しました。前のキーは、以前に画面の写真に写ったことがあったため、削除を勧めました(削除したかは、この記事の時点で確かめていません)。キーは、Mac のターミナルで画面に出ない形で入れています。</p><pre><code>read -s ELEVENLABS_API_KEY && export ELEVENLABS_API_KEY\n</code></pre><p>曲は <code>sound_eleven.py</code> で60秒ずつ作りました。最初の4曲(冒頭用2曲・本編用2曲)は、聞くと暗く感じました。</p><blockquote><p>もう少し明るい曲調がいいかな。暗く聞こえる</p></blockquote><p>長調・明るめを指定して、2種類の書き方で2曲ずつ作り直しました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>曲</p></th><th colspan=\"1\" rowspan=\"1\"><p>指定した内容(要点)</p></th><th colspan=\"1\" rowspan=\"1\"><p>結果</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>bgm_冒頭_1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>生の楽器、ギター・ピアノ・ピッツィカート・マリンバ、100 BPM、好奇心と希望</p></td><td colspan=\"1\" rowspan=\"1\"><p>暗く聞こえた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>bgm_本編_1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>生の楽器、フェルトピアノ・ギター・チェロとバイオリン、90 BPM、落ち着いて真面目</p></td><td colspan=\"1\" rowspan=\"1\"><p>暗く聞こえた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>bgm_明るいA_1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>長調、ギター・明るいピアノ・鉄琴・手拍子、105 BPM、晴れた朝のように</p></td><td colspan=\"1\" rowspan=\"1\"><p>A_1 を冒頭で試した</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>bgm_明るいB_1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>長調、ピアノとギターが主、弦・ピッツィカート・鉄琴、100 BPM、前向きで落ち着いた明るさ</p></td><td colspan=\"1\" rowspan=\"1\"><p><strong>B_2 に決めた</strong></p></td></tr></tbody></table></div><blockquote><p>bgm_明るいB_2.mp3 bgm_明るいA_1.mp3 で動画作成してみて</p></blockquote><p>冒頭15秒を2曲それぞれで書き出して聞き比べ、B_2 に決まりました。指定した文章の全文は、後半の「作った曲の記録」にあります。</p><p>曲は60秒なので、12分29秒の動画ではループします。ElevenLabs の曲は最後の数秒で小さくなって終わるため、そのままつなぐと1分ごとに音が消えます。<code>audio_mix.py</code> で、曲のふつうの大きさより10dB以上小さくなる終わりの部分を切り、つなぎ目を2秒重ねてループさせました。</p><h2 id=\"index_845PdZbU\">BGM の大きさは、話している間 -23dB・0.8秒以上の間 -16dB にした</h2><p>BGM は、ハコブくんが話している間だけ自動で小さくし、話していない間で少し戻します。値はナレーションの大きさとの差で、-23dB はナレーションのおよそ14分の1の音圧です。</p><p></p><figure><img src=\"/media/s-1800x780_v-frms_webp_a3a3705d-d886-470c-8be2-f4a70eed6f10.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>この値に決まるまでに、4回動かしました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>段階</p></th><th colspan=\"1\" rowspan=\"1\"><p>話している間</p></th><th colspan=\"1\" rowspan=\"1\"><p>0.8秒以上の間</p></th><th colspan=\"1\" rowspan=\"1\"><p>きっかけ</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>-22dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>-15dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>最初の値</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>-15dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>-9dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>「音源が聞こえないね。もう少しだけ音量上げるか、音源だけ聴きたい」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>-22dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>-15dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>明るい曲に替えたら大きく聞こえた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>-23dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>-16dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>「音源の音量90%くらいにして」</p></td></tr></tbody></table></div><blockquote><p>曲調を変えたら音量が大きく聞こえてしまうようになったので、前の音量に戻してください。冒頭15秒_bgm_明るいB_2が良さそうです。</p></blockquote><blockquote><p>いいですね。音源の音量90%くらいにして、この動画を仕上げてみてください。</p></blockquote><p>同じ dB の値でも、曲が変わると聞こえ方が変わりました。明るい曲は高い音(鉄琴や明るいピアノ)が多く、低い音の多い曲よりナレーションの上に出て聞こえたためと考えています。90%は、音圧で1dB下げることにあたります(20×log10(0.9) ≒ -0.9dB)。</p><p>話している所の決め方は、次のとおりです。</p><ul><li><p>各文の読み上げの範囲を、前に0.15秒、後ろに0.3秒広げる</p></li><li><p>広げたあとの間が0.5秒未満なら、話している扱いのまま(もとの間で約0.8秒未満。BGM が文ごとに上下しないため)</p></li><li><p>0.3秒かけてなめらかに上げ下げする</p></li></ul><p>グラフのとおり、BGM が戻るのは、場面の切り替えの直前(1.2秒の間)、章の見出しのあと(1.7秒)、大きな流れの変わり目(1.8秒の間)だけです。ふつうの文の間(0.35秒)では、BGM は下がったままです。</p><p>後ろの間の長さを選ぶと、BGM の大きさと鳴る効果音がどう変わるかを、次の図で動かして確かめられます。</p><div class=\"embed\"><iframe src=\"/embeds/first-video-end-to-end-VBQ8Fbsp.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p></p><h2 id=\"index_SkptrlzU\">場面の切り替えは「ホワーン」、大きな流れの変わり目は「キラリン」と「ポロロン」</h2><p>効果音は、最初にコードで5種類作りました(ぽん・こつ・しゅっ・ちん・ひゅい)。冒頭15秒で試したあと、使い方を絞りました。</p><blockquote><p>最後のホワーンって効果音を、場面切り替えに入れてみたい。</p></blockquote><blockquote><p>ホワーンって効果音だけを、場面切り替えに使って</p></blockquote><blockquote><p>効果音はもう少し音量を大きく。</p></blockquote><p>効果音は「ホワーン」(se_rise)だけにし、ナレーションより12dB小さい値から6dB小さい値に上げました。場面(台本の1文ごとに書いた場面の名前)が変わる文の頭から0.15秒前に、自動で鳴らします。</p><p>次に、話の大きな区切りがどこか分かるようにしました。</p><blockquote><p>それと動画の流れの中で少し間があるポイントがあるといいので、大きな流れの変化の時にはその間を設定することはできる?</p></blockquote><blockquote><p>もしくはそのときは効果音を違うものにするか。</p></blockquote><p>大きな流れの変わり目(フック→自己紹介、自己紹介→「今日の問い」、各章の始まり、答え、エンディング)では、直前の文の後ろの間を1.8秒にし、ホワーンの代わりに別の音を鳴らすことにしました。別の音は、コードで6つ作りました(マリンバ・ハープ・ピアノ・鉄琴・ふくらむ和音・風)。</p><blockquote><p>5と2で動画に入れ込んでみて(自己紹介のあたりで入れると良さそう。音源だけでも可です)</p></blockquote><blockquote><p>自己紹介は5のキラリンがいいね。他の切り替えポイントで2のポロロン使ったりと使い分けが良さそうだね(とりあえず自己紹介は5で確定)</p></blockquote><p>自己紹介に入る所は「キラリン」(鉄琴の高い3音)、そのほかの変わり目は「ポロロン」(マリンバの上がる3音)にしました。ポロロンを場所ごとにどう使い分けるかは、まだ試している段階です。</p><p>そのあと、ふつうの場面の切り替えにも、もう少し間が欲しいという指摘がありました。</p><blockquote><p>ホワーンという中の切り替えの時にもう一拍だけ間がある方がいいな。</p></blockquote><p>ホワーンが鳴る所の直前の文の後ろの間を、1.2秒にしました。公開した台本の146文は、後ろの間で分けると次のとおりです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>どこ</p></th><th colspan=\"1\" rowspan=\"1\"><p>後ろの間</p></th><th colspan=\"1\" rowspan=\"1\"><p>文の数</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ふつうの文</p></td><td colspan=\"1\" rowspan=\"1\"><p>0.35秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>96</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>話の小さな区切り</p></td><td colspan=\"1\" rowspan=\"1\"><p>0.4〜0.6秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>4</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ふつうの場面の切り替え(ホワーンの前)</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.2秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>32</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>章の見出しを読む文(「1つ目、…」など)と、最後のあいさつ</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.7秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>5</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>大きな流れの変わり目(キラリン・ポロロンの前)</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.8秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>8</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>字幕のない最後の文(エンディングの余白)</p></td><td colspan=\"1\" rowspan=\"1\"><p>6.0秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>1</p></td></tr></tbody></table></div><p>章の見出しは、読んだすぐあとに場面が切り替わるので、見出しの1.2秒に、切り替えの一拍を足して1.7秒にしています。</p><p>どの文で何が鳴ったかは、書き出しのログに45行で出ています(後半に全量)。</p><h2 id=\"index_ttUdYCtI\">自己紹介の2文目を「研究と開発」と「見立て」に直した</h2><p>確認用の動画を見て、自己紹介の文と、字幕の後ろの帯に指摘がありました。</p><blockquote><p>ここの自己紹介おかしくないか? それとナレーションのテキストの下に薄い四角枠が敷かれているのが必要ないかな。</p></blockquote><blockquote><p>ロボットアームを動かした記録を紹介しているわけではないよ(もちろんそういう時もあるだろうけど)</p></blockquote><p>前の文は「題材は、本業のデザイナーをしながら、自宅の机の上でロボットアームを動かした記録です。」でした。これだと、毎回ロボットアームの記録を見せる番組だと受け取られます。この番組は、フィジカルAIを解説する番組です。Claude がいくつか言い方を出し、次の文に決めました。</p><blockquote><p>エイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。その中で分かったことと、そこから考えた見立てを、ハコブくんがお伝えします。こっちがいいかな。その下はすこし冗長だね。</p></blockquote><p>「結論」ではなく「見立て」にしたのは、今の時点での判断で、新しい事実が出たら変わるからです。</p><p>名前のテロップも分けました。ハコブくんが自分の紹介をしている間は「フィジカルAI解説/案内役:ハコブくん(AI音声)」、本人を紹介する文から「エイイチ」を出します。台本では、この文を「title」から「title_eiichi」という別の場面に分けて、テロップを替えています。映像は同じなので、この切り替えではホワーンを鳴らしません。</p><p>字幕の後ろの薄い黒い帯は、実機の映像の上で字幕を読みやすくするために敷いていたものです。字幕の文字には黒い縁があり、帯がなくても読めるため、外しました。</p><p></p><figure><img src=\"/media/s-1979x632_v-frms_webp_552cefd2-62a2-43d2-bba1-e1e5e23b08c9.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_jcwDZAwf\">右下のモヤモヤは、透かしを消した跡だった</h2><blockquote><p>55秒くらいに右端下がモヤモヤしているのを見つけました。同じようなモヤモヤが他にもないか調べて修正してください。</p></blockquote><p>自己紹介の場面の映像は、以前に動画編集サイト(Clideo)で書き出した版(DAY123.mp4)を使っていました。この版には右下にサイトの透かし(ロゴ)が入っていて、ffmpeg の delogo(指定した範囲を周りの色でぼかして埋める機能)で消していました。その跡が、マットの格子の上で縦の筋のように見えていました。</p><p>同じように透かし入りの版を使っていた映像は、もう1本(DAY124.mp4。章2の実機の場面)ありました。2本とも、透かしの入っていない元の撮影ファイル(DAY123.MOV・DAY124.mov)に替え、<code>scenes.py</code> の映像の指定で、透かしを消す処理を使わない設定(3つ目の値を False)にしました。</p><p><code>assemble.py</code> は、1文ずつ背景の映像を作り、中身が変わった文だけを作り直す仕組みです。どの文を作り直すかは、背景の中身から作った指紋(ハッシュ)で判断しています。この指紋に、使う映像のファイル名と開始秒が入っていませんでした。そのままでは、映像を替えても作り直されないため、指紋に映像の指定を足しました。</p><p></p><figure><img src=\"/media/s-1874x512_v-frms_webp_bca07aad-c04e-4174-a202-e8fd93911724.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_mZmow8P1\">30cm の図の説明を、図の下の2行に移した</h2><blockquote><p>30cm以上違うはのところは新しく内容を修正したので確認してください。前の動画か台本を確認して修正してください。</p></blockquote><p>章2の「計算ツールとデジタルツインで、爪先の位置が30cm以上食い違った」という図で、説明の行が右へはみ出し、「グリッパー」が「グリッパ」と「ー」の2行に折れていました。説明を図の下に移し、中央にそろえた2行にしました。1行目が「アームが外れた距離ではない。爪先の位置の決め方が違っていた」、2行目が「計算ツールには、グリッパーの形が入っていない」です。</p><p></p><figure><img src=\"/media/s-1897x632_v-frms_webp_e4c6b745-4cda-4da9-8bf1-663eb0d60dcb.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_RXyGoUij\">YouTube の概要欄は、目次の時間を書き出しから自動で入れた</h2><blockquote><p>YouTubeの動画内に入れる動画概要は追加情報などをまとめておいてください!</p></blockquote><p>概要欄は、動画の作業フォルダの <code>概要欄_下書き.md</code> に書き、<code>description.py</code> で貼り付け用の <code>概要欄.txt</code> にします。目次の時間は、下書きに <code>{t:26}</code> のように文番号で書くと、書き出しのときに記録した、その文が始まる時間に置きかわります。声や間を変えて時間がずれても、下書きを直す必要がありません。</p><p><code>description.py</code> は、YouTube の目次(チャプター)の条件を満たしているかも確かめます。最初が 0:00 で始まり、3つ以上あり、それぞれ10秒以上あることです。満たさないとき、5,000字を超えるとき、「【要記入】」が残っているときは止まります。</p><p>並べる順は、冒頭の数字、「今日の問い」、先に答え、この動画で見る3つ、目次、出典、この番組について、連絡先とリンク、チャンネル登録のお願いとハッシュタグ3つです。連絡先とリンクは、次のように決めました。</p><blockquote><p>メールアドレス、xとリンクドインかな。</p></blockquote><p>出典のリンクは、すべて開いて確かめてから載せました。Smart Analytics Global の推計は、公開されているページが見つからなかったため、リンクなしで名前と時期だけを書いています。1X NEO の「一般のお客さんへの配送開始」は、公式発表が見つからなかったことを、確かめた日付と一緒に書きました。</p><p>公開した概要欄の全文は、後半の「書き出した概要欄」にあります。</p><h2 id=\"index_WGBasXjY\">タイトルを決め、「子ども向け」は「いいえ」にした</h2><blockquote><p>修正したいところはまだまだありつつも、とりあえず合格点です!YouTubeにアップロードします!</p></blockquote><p>タイトルは、Claude が3案を出し、「人型ロボットの出荷は4倍。ロボットにも「ChatGPTの瞬間」は来たのか|フィジカルAIの現在地」に決めました。冒頭の数字、動画の問い、シリーズ名の順です。</p><p>YouTube に上げるときは、動画が子ども向けかどうかを必ず選びます。ハコブくんはキャラクターですが、動画は大人の視聴者に向けた解説なので「いいえ、子ども向けではありません」にしました。子ども向けにすると、コメントや、ほかの動画の紹介などの機能が使えなくなります。</p><p></p><figure><img src=\"/media/s-1270x654_v-fms_webp_24ce4e2f-fed4-448a-a044-254e3578f394.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_ua9kJv2c\">サムネイルは5回作り直した</h2><blockquote><p>それからサムネイルが必要ですね。ChatGptに作らせるので指示を考えてください。</p></blockquote><p>最初は、ChatGPT に画像を作らせるための指示文を Claude が書き、ChatGPT で作りました。</p><blockquote><p>chatGPTのサムネイルフォントが微妙だし、背景写真が違うしだね。</p></blockquote><blockquote><p>今の動画のフォント周りのデザインをベースにサムネイルの背景を入れる感じでまずは惹きつけられるサムネイルを目指したいです。</p></blockquote><p>画像を作る AI は、日本語の文字の形が崩れやすく、背景のロボットアームも実物と違う形になりました。そこで、文字は画像の AI に任せず、<code>thumbnail.py</code> がコードで組む形にしました。文字と色は動画と同じもの(Noto Sans CJK JP の太字、オレンジ #C4471F、紙色 #F5F5F0、黒 #0D0D0D)を使い、HTML で組んで Playwright(ブラウザを自動で動かす道具)で画像にします。背景は、実機の映像の1コマです。文字をコードで組めば、誤字や崩れた文字は出ません。</p><p></p><figure><img src=\"/media/s-1864x1225_v-frms_webp_4be2ec7d-a345-4e3d-b8e9-88f8d4885fa8.png\" alt=\"\"><figcaption></figcaption></figure><p></p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>回</p></th><th colspan=\"1\" rowspan=\"1\"><p>作ったもの</p></th><th colspan=\"1\" rowspan=\"1\"><p>指摘</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>ChatGPT で作った案</p></td><td colspan=\"1\" rowspan=\"1\"><p>文字の形と、背景の写真が違う</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>案A(紙色と写真)・案B(黒と写真)・案C(大きなハコブくん)</p></td><td colspan=\"1\" rowspan=\"1\"><p>「これがまぁまぁ良さそうだけど。ハコブくんが小さいの修正と右向きがいいね。レイアウト別バージョンを生成」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>案Bの別の組み方3つ(B1・B2・B3)。最初は元の体のまま</p></td><td colspan=\"1\" rowspan=\"1\"><p>「ハコブくんの頭凹んでいるよ!」「最初から作り直したら?」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3'</p></td><td colspan=\"1\" rowspan=\"1\"><p>体を作り直して B1〜B3 を出し直した</p></td><td colspan=\"1\" rowspan=\"1\"><p>「ハコブくん大きすぎ。ハコブくんの顔が変(口と目の配置や大きさ確認)背景を全部写真に変更。テキストを大きく目立つように。写真の上にテキストを置くので、黒縁をつけること。」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>案D(全面写真・黒い縁の大きな文字・ハコブくん右下)</p></td><td colspan=\"1\" rowspan=\"1\"><p>「ハコブくんは左下済み(大きさ少しだけ大きく)人型ロボット出荷 約4倍の部分は黒縁無し。」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>案E(写真のコマを替え、ハコブくん左下)</p></td><td colspan=\"1\" rowspan=\"1\"><p>公開に使った</p></td></tr></tbody></table></div><h2 id=\"index_kbIhNmO_\">ハコブくんの体は、ChatGPT で無地の背景から作り直した</h2><p>サムネイルでハコブくんを大きく出すと、頭の上がへこんで見えました。動画で使っている体の画像は、生成動画の1コマから切り抜いたもので、頭の上が背景と見分けにくく、切り抜きが欠けていたためです。動画では小さく出すので目立ちませんでしたが、サムネイルの大きさでは分かりました。</p><blockquote><p>最初から作り直したら?</p></blockquote><p>ChatGPT で、無地の背景・画面は真っ黒・側面の緑の光の輪はそのまま、という条件でハコブくんの体を作り直しました。</p><p></p><figure><img src=\"/media/s-1328x612_v-fms_webp_86d9d594-b517-4b6d-99b5-8485b97925a6.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>作り直した体は、次の手順でサムネイル用の画像にしました。</p><ol><li><p>切り抜き: OpenCV の GrabCut(指定した四角の中で、前景と背景を色の分布から分ける方法)を、体を囲む四角(GrabCut に渡した値は 230, 170, 680, 1040)で8回くり返して前景を出した。いちばん大きなかたまりだけを残し、中の穴を埋めた。最初に試した、背景の色から塗りつぶす方法は、体の白い部分まで背景として消えた</p></li><li><p>画面の四隅: 黒い画面の四隅の位置を測った(左上 356,262・右上 592,260・右下 569,491・左下 314,485)</p></li><li><p>顔: 画面に、白い目と青い口を描いた</p></li><li><p>向き: 透明でない範囲で切り、左右を反転して右向きにした</p></li></ol><p></p><figure><img src=\"/media/s-1277x420_v-fms_webp_f178bd99-0535-4ee4-a1e8-bca01a47d139.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>3の顔は、最初、動画のハコブくんを見ながら、目と口の位置と大きさを画面の割合で決めて描きました。目が縦に長く上に寄り、口が画面の半分の幅で大きく開いていて、動画のハコブくんと違う顔になりました。</p><p>顔を描き直すとき、目と口を自前で描くのをやめました。動画の <code>hakobu.py</code> が持っている目と口の描き方(<code>eye_mask</code>・<code>mouth_mask</code>)でそのまま描き、動画の体の画面を長方形にならしたものから、新しい体の画面の四隅へ写しています。画面の大きさに対する目と口の大きさと位置が、動画のハコブくんと同じになります。<code>make_new_face.py</code>(全文は後半)がこの処理です。</p><p>1の切り抜きと2の四隅の測定は、その場でコマンドとして動かしたため、スクリプトのファイルが残っていません。結果の <code>cut_new.png</code> と <code>screen_quad_new.npy</code>(四隅の値)は <code>_制作ベース/hakobu/</code> に置いてあり、3と4は <code>make_new_face.py</code> と、そのあとに切り抜きと反転をしたコマンドで作っています。</p><h2 id=\"index_gTZdlqKn\">サムネイルの背景は、ハコブくんがアームに重ならないコマを選んだ</h2><p>ハコブくんを左下に置くと、最初に使っていた写真(DAY124.mov の28秒、アームが赤いブロックを掴む瞬間)では、ちょうど左下に爪とブロックがあり、ハコブくんで隠れます。写真を拡大したり、ずらしたりしても、爪は左下から出ませんでした。</p><p>同じ映像の別のコマを2秒ごとに並べて探し、39.8秒(アームがブロックを箱に置く瞬間)を選びました。このコマは左下がマットだけで、爪とブロックと箱が画面の中央から右下に入ります。</p><p></p><figure><img src=\"/media/s-1980x632_v-frms_webp_c0aa477f-7805-473d-acb6-7dfc5d782633.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>公開したサムネイル(案E)の組み方は、次のとおりです。</p><ul><li><p>背景は写真を全面に敷き、右上だけを少し暗くして文字を読みやすくする</p></li><li><p>「ロボットにも/ChatGPTの瞬間/もう来た?」は、1920×1080 のとき150・200・170px の大きさで、24px の黒い縁を付ける</p></li><li><p>上の札「人型ロボット出荷 約4倍」は、オレンジの札に白い文字。黒い縁は付けない</p></li><li><p>ハコブくんは左下の隅に、高さ600px で右(アーム)を向けて置く。「本当に?」の吹き出しは右側</p></li><li><p>1920×1080 で作って 1280×720 に縮め、YouTube の上限の2MB 以下にする(公開版は約1MB)</p></li></ul><h2 id=\"index_NqzL8c9Y\">公開した直後に見えた2つのこと:字幕が2重に出る、画質が低い</h2><blockquote><p>アップしました!</p></blockquote><p>公開したページを開くと、YouTube の字幕(黒い帯に白い文字)が、動画に焼き込んだ字幕の上に重なって、同じ文が2重に出ていました。</p><p></p><figure><img src=\"/media/s-1932x1606_v-frms_webp_e2dcc461-2987-4422-86aa-134d6e44985c.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>動画には字幕が焼き込んであります。字幕のファイル(srt)を YouTube に字幕として登録していると、視聴者が字幕をオンにしたときに2重になります。登録しておくと検索や、音を出せない人への字幕として役に立つ一方、焼き込んだ字幕と重なります。登録を残すかどうかは、これから決めます。</p><blockquote><p>動画の画質こんなもん?アップしてすぐだから?</p></blockquote><p></p><figure><img src=\"/media/s-3024x1912_v-frms_webp_3eb563d7-8909-410f-9e2a-a4a74c38cc55.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>YouTube は、上げた直後は低い画質の版を先に公開し、HD(1080p)の版はあとから作ります。文字のふちがにじんでいたのは、このためと考えています。再生画面の歯車の「画質」に 1080p が出ていれば、HD の版はできています。</p><p>一方、上げたファイルの映像は、測ると約0.49Mbps(488,631 ビット/秒)でした。<code>assemble.py</code> は crf 20(画質を一定に保つ設定。数字が小さいほど高画質)で書き出していて、図と無地の背景が多い動画なので、この値でも書き出したファイルの文字はくっきりしています。ただ YouTube は上げたファイルをもう一度圧縮するので、HD の版でも文字が甘く見えるようなら、2560×1440 に拡大して書き出し直す方法を試す候補にしています。YouTube は 1440p 以上の動画には、画質のよい圧縮方式を使うためです。HD の版での見え方は、まだ確かめていません。</p><h2 id=\"index_CTcfDe5k\">チャンネル名は「フィジカルAI解説|Pixels to Motors」、まだ反映されていない</h2><blockquote><p>チャンネルの名前なんだっけ?(更新されていない)</p></blockquote><p>チャンネル名は「フィジカルAI解説|Pixels to Motors」に決めています(LOG332)。公開した時点では、YouTube ではまだ「Pixels to Motors」と表示されていました。</p><blockquote><p>何度も修正しすぎて反映されなくなってるかも・</p></blockquote><p>YouTube のチャンネル名は、14日間に2回までしか変えられず、変えた名前が表示されるまでに数日かかることもあります(<a href=\"https://network.mobile.rakuten.co.jp/sumakatsu/contents/articles/2026/00533/\" data-has-link=\"true\">楽天モバイルの解説記事</a>)。YouTube Studio の「カスタマイズ」→「基本情報」の名前の欄が新しい名前になっていれば、表示が追いつくのを待ちます。古いままで保存できなければ、2回の上限に当たっています。どちらだったかは、この記事の時点で確かめていません。</p><h2 id=\"index_qiv3oQpt\">X と LinkedIn の投稿文は、数字と机の上の実感を先に置いた</h2><blockquote><p>Xとlinkdinに投稿するので、投稿内容を考えてください。わかりやすくかつ、興味を持ちやすい投稿内容になるように注意してください。</p></blockquote><p>X は140字(URL を含む)に収めるため、冒頭の数字(約4倍)、動画の問い、机の上のロボットアームで650回以上試した実感(写真で判断させるのは簡単、物を確実に動かすのはまだ手間)の3つだけにしました。X の数え方では、日本語1字が2、URL が23で、280以内に収まれば140字以内です。作った文は266でした。</p><p>LinkedIn は英語のあとに日本語を並べ、動画の中の具体的な話(計算ツールとデジタルツインで爪先の位置が30cm以上食い違ったこと、肩の関節に90度ちょっとを超える値を送るとエラーなしで動かないことがあったこと)を入れました。30cm は「アームが30cm外れた」と読まれやすいので、コメント欄への補足で、爪先の位置の決め方が違っていた話だと書き足しています。ハッシュタグは、記事の投稿で使う #R2R2R ではなく、動画の話題で探す人が使う #フィジカルAI・#PhysicalAI にしました。</p><h2 id=\"index_YMC72TM8\">Claude の返事が英語になり、2回聞き返された</h2><p>作業の途中で、Claude の返事が英語になったことが2回ありました。</p><blockquote><p>英語で書いてあってわからないよ?</p></blockquote><blockquote><p>英語だよ?</p></blockquote><p>1回目は、使える音源の候補を調べて返したとき、2回目は、公開したページを見たあとの返事です。どちらも日本語で書き直しました。このプロジェクトの返事は日本語で書くと決まっています。</p><h2 id=\"index_ZKdeas3Q\">Mac へ送ったファイルが、途中で変わることがあった</h2><p>作ったファイルは、Claude の作業環境から、Mac とつながった経路で iCloud と外付けの SSD(KIOXIA)に送っています。この経路で、3つのことが起きました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>起きたこと</p></th><th colspan=\"1\" rowspan=\"1\"><p>どう分かったか</p></th><th colspan=\"1\" rowspan=\"1\"><p>どうしたか</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>約28MB を超えるファイルが送れない(HTTP 400)</p></td><td colspan=\"1\" rowspan=\"1\"><p>送る道具がエラーを返した</p></td><td colspan=\"1\" rowspan=\"1\"><p>10MB ずつに分けて送り、Mac でつないだ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>分けた動画の先頭の1つ、透明な部分のある PNG(ハコブくんの切り抜きなど)が、届いたときに中身が変わっていた</p></td><td colspan=\"1\" rowspan=\"1\"><p>送る前と届いたあとの指紋(md5。ファイルの中身から作る32桁の値)が違った。PNG は届いた方が大きかった</p></td><td colspan=\"1\" rowspan=\"1\"><p>gzip で圧縮して送り、Mac で戻した。戻したあとの指紋が一致するのを確かめた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Mac にあるスクリプト(assemble.py・2本の scenes.py)が、古い版に戻っていた</p></td><td colspan=\"1\" rowspan=\"1\"><p>Mac と作業環境のファイルの指紋を比べた</p></td><td colspan=\"1\" rowspan=\"1\"><p>送り直して、指紋が一致するのを確かめた</p></td></tr></tbody></table></div><p>YouTube に上げた動画ファイルは、作業環境の指紋 <code>d9f6def3f1a01d9aee5c76c9d6516bc0</code> と、Mac でつないだあとの指紋が一致しています。送ったあとは、毎回指紋を比べることにしました。</p><h2 id=\"index_lMTio9tY\">次の動画から使う「1本を作る流れ」23手順</h2><blockquote><p>動画の作り方の手順をまとめてありますか?他の台本を使って動画を量産していきますよ。</p></blockquote><p>作る手順は、動画のルール <code>video_rules.md</code> の中に分かれて書かれていて、サムネイル・アップロード・投稿の手順はどこにもありませんでした。そこで <code>video_rules.md</code> の先頭に「0節 1本を作る流れ」を足し、台本から投稿までを23の手順にまとめました。「フィジカルAIの現在地」で実際に通した順番です。担当の <strong>エ</strong> は本人、<strong>C</strong> は Claude です。追記した0節を、ファイルのまま載せます。</p><pre><code class=\"language-markdown\">### 準備(1本ごとに1回)\n\n1. **エ** どの台本を動画にするか決めて、Claude に伝える\n2. **C** iCloud の `動画/` に作業フォルダ `{動画名}/` を作る。前の動画の作業フォルダから `narration.py`・`scenes.py`・`sound.py`・`thumb.py`・`概要欄_下書き.md` を写して、書き換える元にする\n\n### 台本(2節・3節)\n\n3. **C** 台本を `narration.py` に書く。1文ごとに(場面の名前、表情、字幕の文、後ろの間)。自己紹介の2文(2節10)と、間の取り方(2節12)は前の動画と同じにする\n4. **C** 読みの直しを書く。`READ`(仮音声用)と `ELEVEN_READ`(ElevenLabs 用)。「エイイチ」は ElevenLabs には「えいいち」で渡す(3-1)\n5. **C** `make_docs.py` で読み物の台本と codex の依頼を作る。**エ** 内容を確かめる。数字と出典は、公開前に元のページを開いて確かめる\n6. **エ** 台本を確定する(ここで決まった文が、声のお金を使う単位になる)\n\n### 声(3-1)\n\n7. **エ** Mac のターミナルで、作業フォルダに移って声を作る:\n `read -s ELEVENLABS_API_KEY && export ELEVENLABS_API_KEY`(キーを貼って Enter。画面には出ない)\n `python3 ../_制作ベース/tts_eleven.py --voice gWJpc8whMBqnF7Y0kc9f --name ハコブ`\n 読み方を比べたい語があるときは `--try \"文1|文2\"` で試しの音声だけ作る\n8. **C** `tts_import.py voice_eleven/ハコブ` で取り込む。読み間違い(例: 裏表が「うらおもてうら」)が見つかったら、`ELEVEN_READ` を直し、**エ** がその文だけ `--lines 番号` で作り直す\n\n### 音(8節の BGM・効果音の行)\n\n9. **C** `sound.py` に BGM と効果音の置き方を書く。いまの決まり: BGM は `bgm_明るいB_2` を最初から最後までループ、話している間 -23dB・間 -16dB。場面の切り替えはホワーン(-6dB)、自己紹介はキラリン、大きな流れの変わり目はポロロン\n10. **C** `audio_mix.py --first N` で音だけ書き出し、**エ** が冒頭を聞いて大きさを確かめる\n\n### 図と字幕(4節・5節・6節)\n\n11. **C** `scenes.py` に場面ごとの図を書く。実機映像は `src/` に置き、`VIDEO` に場面名と開始秒を書く。透かし入りの映像は使わない(6節)\n12. **C** `render.py` で図・テロップ・字幕を作る。ハコブくんの区画と重なったら止まるので、直して通す\n13. **C** `preview.py 文番号…` で、気になる文の画面を静止画で出す。**エ** 確かめる\n\n### 書き出しと確認(7節)\n\n14. **C** `assemble.py` で全体を書き出す(14分で30分ほど)。確認用(30MB 以下)をチャットに送り、KIOXIA の `videos/{動画名}/` にも置く\n15. **エ** 通しで見て、直す所を時間つきで伝える(例:「3分16秒の読みがおかしい」「55秒の右下がモヤモヤ」)\n16. **C** 直して書き出し直す(中身が変わった文だけ作り直すので、2回目からは速い)。15〜16 を合格まで繰り返す\n17. **C** アップロード用(crf 20)を KIOXIA に置く。大きいファイルは分けて送り、Mac でつなぎ、指紋(md5)が一致するのを確かめる\n\n### YouTube に出すもの(7節)\n\n18. **C** `概要欄_下書き.md` を書き、`description.py` で `概要欄.txt` を作る。目次の時間は自動で入る。リンクは開いて確かめてから載せる\n19. **C** タイトル案を3つ出す。**エ** 決める\n20. **C** `thumb.py` に文言と写真を書き、`thumbnail.py` でサムネイルを作る(8節のサムネイルの行)。写真は、ハコブくんを左下に置いてもアームや物に重ならないコマを実機映像から選ぶ。**エ** 確かめる\n21. **エ** YouTube にアップロードする。設定: 子ども向けは「いいえ」。字幕ファイル(srt)は、映像に字幕が入っているので、上げると字幕をオンにした人には2重に出る(上げるかはエイイチが決める)。公開直後は低い画質で再生され、HD(1080p)は数十分〜1時間後に出る\n\n### 公開のあと\n\n22. **C** X と LinkedIn の投稿文を作る(書き方は article_rules.md の SNS 節。X は URL 込み140字・LinkedIn は英語→日本語と、コメント欄への補足)。**エ** 動画の URL を入れて投稿する\n23. **C** この動画で決めたこと・直したことを、本ファイルの該当の節と更新履歴に書く。iCloud の `_制作ベース` とプロジェクトの `claude/video_rules.md` の両方に保存する\n\n### 前の動画でつまずいた所(先に気をつける)\n\n- ElevenLabs の API キーには「音声の読み上げ・効果音・音楽生成」の3つの許可が要る(音楽生成がないと BGM が作れない)\n- Claude から Mac へ送ったファイルが途中で変わることがある(大きい動画・透明部分のある PNG)。送ったあとは指紋(md5)を比べ、違えば圧縮して送り直す\n- Mac 側のファイルが古い版に戻っていたことがある。書き出しの前に、Mac と Claude の作業環境でスクリプトの指紋がそろっているか確かめる\n</code></pre><h2 id=\"index_B9rTkmMC\">まだ決まっていないこと</h2><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>いまの状態</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ポロロンの使い分け</p></td><td colspan=\"1\" rowspan=\"1\"><p>大きな流れの変わり目7か所すべてで同じポロロンを鳴らしている。場所ごとに音を変えるかは試し中</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ジングルとアウトロの文言</p></td><td colspan=\"1\" rowspan=\"1\"><p>「Pixels to Motors。案内役のハコブくんです。」とエンディングの文は、毎回同じにするか未定</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>字幕ファイルの登録</p></td><td colspan=\"1\" rowspan=\"1\"><p>焼き込んだ字幕と2重になる。登録を残すかは未定</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>HD の版の画質</p></td><td colspan=\"1\" rowspan=\"1\"><p>1080p の版での文字の見え方をまだ見ていない。甘ければ 2560×1440 で書き出し直す</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>チャンネル名の反映</p></td><td colspan=\"1\" rowspan=\"1\"><p>「フィジカルAI解説|Pixels to Motors」が表示されていない。上限に当たったかを確かめる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>動画のハコブくんの体</p></td><td colspan=\"1\" rowspan=\"1\"><p>作り直した体は、サムネイルだけで使っている。動画にも使うなら、hakobu.py の画面の四隅・頭の位置・光の輪の位置を測り直し、表情を一覧で確かめる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1X NEO の配送</p></td><td colspan=\"1\" rowspan=\"1\"><p>「配送開始を示す公式発表が見つからない」は 2026年10月12日に確かめた内容。公式発表が出たら、概要欄を直す</p></td></tr></tbody></table></div><h2 id=\"index_XNI1szIP\">この記事で作った・直したスクリプトの全文</h2><p>動画ごとの作業フォルダ(<code>動画/フィジカルAIの現在地/</code>)のファイルと、どの動画でも使う部品を置く <code>_制作ベース/</code> のファイルです。<code>_制作ベース/</code> のうち、この記事の作業で新しく作ったもの、または直したものを載せています。図とハコブくんの描き方(<code>scene_kit.py</code>・<code>hakobu.py</code>・<code>render.py</code>・<code>subtitle.py</code>)は、LOG331 から変えていないので載せていません。</p><h3 id=\"index_OykTUSdQ\">_制作ベース/timing.py</h3><p>新規。文ごとの尺(コマ数)と、ナレーションのつなぎ方を決める。assemble.py と audio_mix.py が同じ規則を使うために分けた(51行)。</p><pre><code class=\"language-python\">\"\"\"文ごとの尺(フレーム)と、ナレーションの音をつなぐ規則。assemble.py と audio_mix.py が同じものを使う。\"\"\"\nimport json, wave\nimport numpy as np\nfrom subtitle import pages as sub_pages\n\nFPS, SR = 30, 48000\n\n\ndef load(first=None):\n idx = json.load(open('tts/index.json'))\n return idx[:first] if first else idx\n\n\ndef layout(idx):\n \"\"\"各文に frames(間を含む尺)・speech(読み上げの長さ)・start(開始フレーム)・parts(字幕の枚の切り替え)を足す。全体のフレーム数を返す\"\"\"\n t = 0\n for o in idx:\n o['frames'] = max(1, round((o['dur'] + o['pause']) * FPS))\n o['speech'] = round(o['dur'] * FPS)\n o['start'] = t\n t += o['frames']\n # 「|」で分けた字幕は、読み上げの長さを字数で割って切り替える\n pages = sub_pages(o['text']) if o['text'] else []\n cuts, acc, total = [0], 0, sum(len(p) for p in pages)\n for p in pages[:-1]:\n acc += len(p)\n cuts.append(round(o['speech'] * acc / total))\n cuts.append(o['frames'])\n o['parts'] = [(k, cuts[k], cuts[k + 1]) for k in range(len(pages))] or [(None, 0, o['frames'])]\n return t\n\n\ndef narration(idx):\n \"\"\"各文の音声を、尺ちょうどに合わせてつないだ int16 モノラル\"\"\"\n parts = []\n for o in idx:\n need = o['frames'] * SR // FPS\n if o['text']:\n with wave.open(f\"tts/{o['i']:03d}.wav\") as w:\n a = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16)\n else:\n a = np.zeros(0, dtype=np.int16)\n a = a[:need]\n parts.append(np.concatenate([a, np.zeros(need - len(a), dtype=np.int16)]))\n return np.concatenate(parts)\n\n\ndef write_wav(path, a, ch=1):\n \"\"\"a は int16(モノラルは1次元、ステレオは (n, 2))\"\"\"\n with wave.open(path, 'wb') as w:\n w.setnchannels(ch); w.setsampwidth(2); w.setframerate(SR); w.writeframes(np.ascontiguousarray(a).tobytes())\n</code></pre><h3 id=\"index_P6iLbpu7\">_制作ベース/audio_mix.py</h3><p>新規。ナレーションに BGM と効果音を混ぜる。話している間は BGM を下げ、0.8秒以上の間で戻す。曲の終わりを切ってループする(173行)。</p><pre><code class=\"language-python\">\"\"\"ナレーションに BGM と効果音を混ぜる。音の置き方は、動画の作業フォルダの sound.py に書く。\n使い方(動画の作業フォルダで)\n python3 <_制作ベース>/audio_mix.py [--first N] 音だけ先に確かめる → 音の確認.m4a(映像は作らない)\n assemble.py は sound.py があると、これを呼んで mix.wav を使う\n\nsound.py の書き方\n BGM = [dict(file='bgm_仮', start=0, end=None, level=-22, gap=-15, fade_in=0.8, fade_out=2.0, offset=0.0)]\n file 作業フォルダからの相対パス(sound/bgm_main.mp3 など)。見つからなければ <_制作ベース>/sound/{file}.wav\n start 流し始める文の番号。end は止める文の番号(その文は含まない。None は最後まで)\n level 話している間の大きさ。ナレーションより何 dB 小さくするか(-22 なら、ナレーションの約 1/12 の音圧)\n gap 話していない間(0.8 秒以上の間)の大きさ。少し上げて、間が空っぽに聞こえないようにする\n fade_in / fade_out 入りと終わりのフェード(秒)。offset は BGM のどこから流すか(秒)\n 曲の終わりの小さくなっていく所は切り、BGM が区間より短いときは 2 秒重ねてループする\n SE = [(文番号, 文の頭から何秒後, 音, ナレーションより何dB)]\n 音は file と同じ探し方(se_pop など)。例: (2, 0.0, 'se_pop', -8)\n SCENE_SE = dict(file='se_rise', level=-12, offset=-0.15)\n 場面(narration.py の場面の名前)が変わる文ごとに、自動で同じ音を入れる(動画の最初の文は入れない)。\n offset は場面が変わる瞬間から何秒ずらすか(マイナスで少し前から鳴らし、音の山を切り替えに合わせる)\n skip=['場面名', ...] に書いた場面へ切り替わるときは鳴らさない(同じ映像のままテロップだけ替える場面など)\n CHAPTER_SE = dict(file='se_chapter', level=-8, offset=-0.3, scenes=['c1', 'c2', ...])\n scenes に書いた場面(章の始まりなど、大きな流れの変わり目)へ切り替わるときは、SCENE_SE の代わりにこの音を鳴らす\n files={'場面名': '音', ...} を書くと、その場面だけ別の音にする(例: 自己紹介だけキラリン)\n\"\"\"\nimport os, subprocess, sys\nimport numpy as np\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, '.')\nsys.path.insert(1, HERE)\nfrom timing import FPS, SR, load, layout, narration, write_wav\n\nWIN = int(0.05 * SR)\n\n\ndef find(name):\n for p in (name, os.path.join(HERE, 'sound', name), os.path.join(HERE, 'sound', name + '.wav')):\n if os.path.isfile(p):\n return p\n sys.exit(f'音のファイルが見つかりません: {name}(作業フォルダからの相対パスか、_制作ベース/sound/ の名前で書く)')\n\n\ndef read(path):\n \"\"\"どの形式でも 48kHz ステレオの float にする\"\"\"\n raw = subprocess.run(['ffmpeg', '-v', 'error', '-i', path, '-ac', '2', '-ar', str(SR), '-f', 's16le', '-'],\n capture_output=True, check=True).stdout\n return np.frombuffer(raw, dtype=np.int16).reshape(-1, 2).astype(np.float32) / 32768.0\n\n\ndef win_rms(a):\n m = a.mean(axis=1) if a.ndim == 2 else a\n n = len(m) // WIN\n if n == 0:\n return np.array([np.sqrt(np.mean(m ** 2)) + 1e-9])\n return np.sqrt((m[:n * WIN].reshape(n, WIN) ** 2).mean(axis=1)) + 1e-9\n\n\ndef db(x):\n return 10 ** (x / 20)\n\n\ndef cut_tail(a):\n \"\"\"曲の終わりで音が小さくなっていく所(ふつうの大きさより10dB以上小さい所)を切る。ループでつなぐとき、途中で音が消えないようにする\"\"\"\n r = win_rms(a)\n loud = np.where(r > np.median(r[r > 1e-3]) * db(-10))[0]\n if len(loud) == 0:\n return a\n return a[:min(len(a), (loud[-1] + 1) * WIN)]\n\n\ndef loop_to(a, n, xf=SR):\n \"\"\"a を n サンプルまでループで伸ばす(つなぎ目は xf サンプル重ねる)\"\"\"\n if len(a) >= n:\n return a[:n]\n xf = min(xf, len(a) // 4)\n out = a.copy()\n ramp = np.linspace(0, 1, xf)[:, None]\n while len(out) < n:\n head = out[:-xf]\n cross = out[-xf:] * (1 - ramp) + a[:xf] * ramp\n out = np.concatenate([head, cross, a[xf:]])\n return out[:n]\n\n\ndef smooth(x, sec):\n k = max(1, int(sec * 100))\n return np.convolve(np.pad(x, (k, k), mode='edge'), np.ones(k) / k, mode='same')[k:-k]\n\n\ndef mix(idx, pcm, out='mix.wav'):\n ns = {}\n exec(open('sound.py', encoding='utf-8').read(), ns)\n BGM, SE = ns.get('BGM', []), list(ns.get('SE', []))\n sc = ns.get('SCENE_SE')\n ch = ns.get('CHAPTER_SE') or {}\n if sc or ch:\n for prev, o in zip(idx, idx[1:]):\n if o['scene'] == prev['scene']:\n continue\n if o['scene'] in ch.get('scenes', []): # 大きな流れの変わり目は、別の音にする\n f = ch.get('files', {}).get(o['scene'], ch['file']) # 場面ごとに音を替えられる(files={'title': 'se_chapter_glock'})\n SE.append((o['i'], ch.get('offset', -0.3), f, ch.get('level', -8)))\n elif sc and o['scene'] not in sc.get('skip', []):\n SE.append((o['i'], sc.get('offset', -0.15), sc['file'], sc.get('level', -12)))\n voice = pcm.astype(np.float32) / 32768.0\n n = len(voice)\n ref = np.percentile(win_rms(voice), 90) # ナレーションの「ふつうに話している」大きさ\n total = n / SR\n by_i = {o['i']: o for o in idx}\n\n # 話している所(各文の読み上げ)を、前0.15秒・後0.3秒広げる。0.8 秒未満の間は話している扱いのまま(BGM が細かく上下しない)\n ctl = np.zeros(int(total * 100) + 1)\n for o in idx:\n if o['text']:\n a = max(0, int((o['start'] / FPS - 0.15) * 100)); b = int(((o['start'] + o['speech']) / FPS + 0.3) * 100)\n ctl[a:b] = 1\n gaps = np.where(np.diff(np.concatenate([[1], ctl, [1]])) != 0)[0].reshape(-1, 2)\n for a, b in gaps:\n if (b - a) < 50: # 0.5 秒(広げた後)= もとの間で約 0.8〜0.95 秒未満\n ctl[a:b] = 1\n ctl = smooth(ctl, 0.3)\n\n bus = np.zeros((n, 2), dtype=np.float32)\n for m in BGM:\n src = read(find(m['file']))\n src = src[int(m.get('offset', 0) * SR):]\n g = ref / np.median(win_rms(src)[win_rms(src) > 1e-3])\n src = cut_tail(src)\n s0 = by_i[m.get('start', 0)]['start'] * SR // FPS if m.get('start', 0) in by_i else 0\n e = m.get('end')\n e0 = by_i[e]['start'] * SR // FPS if e is not None and e in by_i else n\n seg = loop_to(src, e0 - s0, xf=2 * SR) * g\n # 話している間は level、間は gap(控えめに 100 Hz で決めてから、サンプルに伸ばす)\n t = (np.arange(s0, e0) / SR * 100).astype(int).clip(0, len(ctl) - 1)\n lv = db(m.get('level', -22)) * ctl[t] + db(m.get('gap', -15)) * (1 - ctl[t])\n fi, fo = int(m.get('fade_in', 0.8) * SR), int(m.get('fade_out', 2.0) * SR)\n fade = np.ones(e0 - s0, dtype=np.float32)\n if fi:\n fade[:fi] = np.linspace(0, 1, min(fi, len(fade)))[:len(fade[:fi])]\n if fo:\n fade[-fo:] = np.minimum(fade[-fo:], np.linspace(1, 0, min(fo, len(fade))))\n bus[s0:e0] += seg * (lv * fade)[:, None]\n\n log = []\n for line, off, name, level in SE:\n if line not in by_i:\n continue\n src = read(find(name))\n g = ref / win_rms(src).max() * db(level)\n s0 = max(0, int((by_i[line]['start'] / FPS + off) * SR))\n k = min(len(src), n - s0)\n if k > 0:\n bus[s0:s0 + k] += src[:k] * g\n log.append(f\" {s0 / SR:6.2f}秒 文{line:03d} {name} {level}dB 「{by_i[line]['text'][:16]}」\")\n\n out_a = voice[:, None] + bus\n peak = np.abs(out_a).max()\n if peak > 0.98:\n out_a *= 0.98 / peak # 割れないように全体を下げる(最後の loudnorm で大きさはそろう)\n write_wav(out, (out_a * 32767).astype(np.int16), ch=2)\n print(f'BGM {len(BGM)}本・効果音 {len(log)}個 → {out}')\n print('\\n'.join(log))\n return out\n\n\nif __name__ == '__main__':\n args = sys.argv[1:]\n first = int(args[args.index('--first') + 1]) if '--first' in args else None\n idx = load(first)\n layout(idx)\n pcm = narration(idx)\n f = mix(idx, pcm)\n subprocess.run(['ffmpeg', '-v', 'error', '-y', '-i', f, '-af', 'loudnorm=I=-16:TP=-1.5:LRA=11',\n '-c:a', 'aac', '-b:a', '160k', '-ar', '48000', '音の確認.m4a'], check=True)\n print('音の確認.m4a')\n</code></pre><h3 id=\"index_F7ztL1Qa\">_制作ベース/assemble.py</h3><p>直した。sound.py があれば audio_mix.py を呼ぶ。字幕の後ろの帯をやめた。映像の指定を背景の指紋に入れた。前の場面の映像の続きから流す指定(開始秒 None)を足した(144行)。</p><pre><code class=\"language-python\">\"\"\"1文ずつ背景の映像を作ってつなぎ、ハコブくんを重ねて、音声と合わせる。\n使い方: 動画の作業フォルダで(render.py のあと)\n python3 <_制作ベース>/assemble.py [--first N] [--force] [--name 動画名]\n --first N 最初の N 文だけ書き出す(確かめ用)\n --force 背景の映像を作り直す(ふだんは、できているものは使い回す)\n音: 作業フォルダに sound.py があれば、BGM・効果音を混ぜる(audio_mix.py。音だけ先に確かめるときは audio_mix.py を単独で動かす)\n出力: out.mp4(作業用・crf 20・-16 LUFS)、out_確認用.mp4(crf 29・30MB以下)、out.srt、timeline.json\n\"\"\"\nimport json, subprocess, os, sys, wave, hashlib\nimport numpy as np\nfrom concurrent.futures import ThreadPoolExecutor\nfrom PIL import Image\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, '.')\nsys.path.insert(1, HERE)\nos.environ.setdefault(\"P2M_BASE\", HERE)\nfrom scenes import VIDEO\nfrom hakobu import Hakobu\nfrom subtitle import pages as sub_pages\nfrom timing import FPS, SR, load, layout, narration, write_wav\n\nargs = sys.argv[1:]\nfirst = int(args[args.index('--first') + 1]) if '--first' in args else None\nforce = '--force' in args\nidx = load(first)\nos.makedirs('chunks', exist_ok=True); os.makedirs('comp', exist_ok=True)\nhk = Hakobu()\n\n# ---------- 尺(フレーム単位)と字幕の枚(timing.py。audio_mix.py と同じ規則) ----------\ntotal_frames = layout(idx)\n\n# 実機の映像の場面: 同じ場面が続く間は、続きから再生する。開始秒が None の場面は、前の場面の映像の続きから再生する\n# (同じ映像のまま、テロップだけ替えたいとき。2026-10-12)\nprev, off = None, 0.0\nfor o in idx:\n sc = o['scene']\n if sc in VIDEO:\n if sc != prev and VIDEO[sc][1] is not None:\n off = VIDEO[sc][1]\n o['voff'] = off\n off += o['frames'] / FPS\n prev = sc\n\n# 背景(字幕まで入れて、ハコブくんは入れない)。最後に1回だけ圧縮し直すので、ここは画質を高めに保つ\nENC = ['-c:v', 'libx264', '-preset', 'ultrafast', '-crf', '12', '-pix_fmt', 'yuv420p', '-r', str(FPS), '-an']\njobs = []\nfor o in idx:\n for k, a, b in o['parts']:\n jobs.append((o, k, a, b))\n\n\ndef job(j):\n o, k, a, b = j\n tag = f\"{o['i']:03d}\" + (f\"_{k}\" if k is not None else \"\")\n # 中身(場面・字幕・尺・映像の位置・図と字幕の画像)が変わったら作り直すよう、ファイル名に指紋を入れる\n sub_png = f\"png/sub/{o['i']:03d}_{k}.png\" if k is not None else \"\"\n fp = [o['scene'], o['text'], a, b, round(o.get('voff', 0), 3), VIDEO.get(o['scene'])] # 映像のファイルや透かしの設定が変わっても作り直す\n for f in (f\"png/scene/{o['scene']}.png\", f\"png/ovl/{o['scene']}.png\", sub_png):\n if f and os.path.exists(f):\n fp.append(hashlib.md5(open(f, 'rb').read()).hexdigest())\n out = f\"chunks/{tag}_{hashlib.md5(repr(fp).encode()).hexdigest()[:8]}.mp4\"\n if os.path.exists(out) and not force:\n return out\n sub = f\"png/sub/{o['i']:03d}_{k}.png\" if k is not None else None\n sc, n = o['scene'], str(b - a)\n if sc in VIDEO:\n src, _, wm = VIDEO[sc]\n dl = 'delogo=x=2740:y=1840:w=1010:h=220,' if wm else ''\n ins = ['-ss', f\"{o['voff'] + a / FPS:.3f}\", '-stream_loop', '-1', '-i', src, '-loop', '1', '-i', f'png/ovl/{sc}.png']\n fc = (f\"[0:v]{dl}scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080,fps={FPS},setsar=1[b];\" # 字幕の後ろの黒い帯は敷かない(2026-10-12 エイイチの指示)\n f\"[b][1:v]overlay[c]\")\n if sub:\n ins += ['-loop', '1', '-i', sub]\n fc += \";[c][2:v]overlay[v]\"\n else:\n fc += \";[c]null[v]\"\n cmd = ['ffmpeg', '-v', 'error', '-y'] + ins + ['-filter_complex', fc, '-map', '[v]', '-frames:v', n] + ENC + [out]\n else:\n comp = f\"comp/{tag}.png\"\n im = Image.open(f'png/scene/{sc}.png').convert('RGBA')\n if sub:\n im.alpha_composite(Image.open(sub).convert('RGBA'))\n im.convert('RGB').save(comp)\n cmd = ['ffmpeg', '-v', 'error', '-y', '-loop', '1', '-framerate', str(FPS), '-i', comp, '-frames:v', n,\n '-tune', 'stillimage'] + ENC + [out]\n subprocess.run(cmd, check=True)\n return out\n\n\nwith ThreadPoolExecutor(max(1, min(3, os.cpu_count() or 1))) as ex:\n outs = list(ex.map(job, jobs))\nopen('chunks/list.txt', 'w').write(''.join(f\"file '{os.path.basename(x)}'\\n\" for x in outs))\nsubprocess.run(['ffmpeg', '-v', 'error', '-y', '-f', 'concat', '-safe', '0', '-i', 'chunks/list.txt', '-c', 'copy', 'bg.mp4'], check=True)\n\n# ---------- 音声: 各文を尺ちょうどに合わせて連結。sound.py があれば BGM・効果音を混ぜる(audio_mix.py) ----------\npcm = narration(idx)\nwrite_wav('narration.wav', pcm)\naudio_in = 'narration.wav'\nif os.path.exists('sound.py'):\n from audio_mix import mix\n audio_in = mix(idx, pcm) # mix.wav(ステレオ)。ハコブくんの口はナレーションだけで動かす\n\n# ---------- 字幕 SRT(「|」で分けた枚は別の行にする) ----------\ndef ts(f):\n ms = round(f / FPS * 1000); h, ms = divmod(ms, 3600000); m, ms = divmod(ms, 60000); s, ms = divmod(ms, 1000)\n return f\"{h:02d}:{m:02d}:{s:02d},{ms:03d}\"\nsrt, k = [], 1\nfor o in idx:\n if not o['text']:\n continue\n pages = sub_pages(o['text'])\n for (pk, a, b), ptxt in zip(o['parts'], pages):\n end = min(b, o['speech']) if pk == len(pages) - 1 else b\n srt.append(f\"{k}\\n{ts(o['start'] + a)} --> {ts(o['start'] + end)}\\n{ptxt}\\n\"); k += 1\nopen('out.srt', 'w', encoding='utf-8').write('\\n'.join(srt))\njson.dump(idx, open('timeline.json', 'w'), ensure_ascii=False)\n\n# ---------- ハコブくん: 表情と口・目・リングの動きを決めて、背景に重ねる ----------\ntimeline = [(o['start'], o['start'] + o['frames'], o['start'] + o['speech'], o.get('face', 'base'), bool(o['text'])) for o in idx]\nparams = hk.animate(timeline, pcm.astype(np.float32) / 32768.0)\ncmd = ['ffmpeg', '-v', 'error', '-y', '-i', 'bg.mp4',\n '-f', 'rawvideo', '-pix_fmt', 'rgba', '-s', f'{hk.w}x{hk.h}', '-r', str(FPS), '-i', '-',\n '-i', audio_in,\n '-filter_complex', f'[0:v][1:v]overlay=x={hk.x}:y={hk.y}:eof_action=pass[v]',\n '-map', '[v]', '-map', '2:a', '-c:v', 'libx264', '-preset', 'medium', '-crf', '20', '-pix_fmt', 'yuv420p',\n '-af', 'loudnorm=I=-16:TP=-1.5:LRA=11', '-c:a', 'aac', '-b:a', '192k', '-ar', '48000',\n '-movflags', '+faststart', '-frames:v', str(total_frames), 'out.mp4']\np = subprocess.Popen(cmd, stdin=subprocess.PIPE)\nfor i in range(total_frames):\n p.stdin.write(hk.frame(params[i]).tobytes())\np.stdin.close()\nif p.wait():\n sys.exit('ハコブくんを重ねる書き出しに失敗しました')\nstill = round(total_frames * 0.42)\nsubprocess.run(['ffmpeg', '-v', 'error', '-y', '-ss', f'{still / FPS:.2f}', '-i', 'out.mp4', '-frames:v', '1', 'out_still.png'], check=True)\n\n# ---------- 確認用(チャットで送る方): crf 29・音声 112kbps・30MB 以下 ----------\nfor crf in (29, 32, 35):\n subprocess.run(['ffmpeg', '-v', 'error', '-y', '-i', 'out.mp4', '-c:v', 'libx264', '-preset', 'medium', '-crf', str(crf),\n '-c:a', 'aac', '-b:a', '112k', '-movflags', '+faststart', 'out_確認用.mp4'], check=True)\n mb = os.path.getsize('out_確認用.mp4') / 1e6\n if mb <= 30:\n break\nprint(f'frames {total_frames} = {total_frames / FPS:.1f}s 確認用 {mb:.1f}MB (crf {crf})')\n</code></pre><h3 id=\"index_WrI3e7rF\">_制作ベース/preview.py</h3><p>直した。字幕の後ろの帯をやめ、前の場面の映像の続き(開始秒 None)を、書き出しの記録から読むようにした(54行)。</p><pre><code class=\"language-python\">\"\"\"確かめ用の静止画。動画を書き出さずに、指定した文の画面(図または実機映像の1コマ+字幕+ハコブくん)を1枚ずつ出す。\n使い方: 動画の作業フォルダで(render.py のあと) python3 <_制作ベース>/preview.py 8 9 12\n出力: preview/{文番号}_{字幕の枚}.png と、並べた preview/一覧.png\n\"\"\"\nimport json, os, sys, subprocess\nfrom PIL import Image\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, '.')\nsys.path.insert(1, HERE)\nos.environ.setdefault(\"P2M_BASE\", HERE)\nfrom scenes import VIDEO\nfrom hakobu import Hakobu, EXPR\nfrom subtitle import pages\n\nidx = {o['i']: o for o in json.load(open('tts/index.json'))}\nnums = [int(x) for x in sys.argv[1:]] or [0]\nos.makedirs('preview', exist_ok=True)\nhk = Hakobu()\nouts = []\nfor i in nums:\n o = idx[i]\n sc = o['scene']\n if sc in VIDEO:\n src, start, wm = VIDEO[sc]\n start = o.get('voff', start) if start is None else start # 続きから再生する場面は、書き出しのときの位置(timeline.json)を使う\n if start is None:\n tl = {t['i']: t for t in json.load(open('timeline.json'))} if os.path.exists('timeline.json') else {}\n start = tl.get(i, {}).get('voff', 0.0)\n tmp = f'preview/_v{i}.png'\n subprocess.run(['ffmpeg', '-v', 'error', '-y', '-ss', f'{start:.2f}', '-i', src, '-frames:v', '1',\n '-vf', ('delogo=x=2740:y=1840:w=1010:h=220,' if wm else '') + 'scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080', tmp], check=True)\n bg = Image.open(tmp).convert('RGBA')\n bg.alpha_composite(Image.open(f'png/ovl/{sc}.png').convert('RGBA'))\n else:\n bg = Image.open(f'png/scene/{sc}.png').convert('RGBA')\n ex = EXPR[o.get('face', 'base')]\n face = hk.frame((ex['eyes'], ex['brows'], 'mid', 0.4, 0.0))\n n = len(pages(o['text'])) if o['text'] else 1\n for k in range(n):\n im = bg.copy()\n sub = f'png/sub/{i:03d}_{k}.png'\n if os.path.exists(sub):\n im.alpha_composite(Image.open(sub).convert('RGBA'))\n im.alpha_composite(face, (hk.x, hk.y))\n path = f'preview/{i:03d}_{k}.png'\n im.convert('RGB').save(path)\n outs.append(path)\ncols = 2\nw, h = 960, 540\nsheet = Image.new('RGB', (w * cols, h * ((len(outs) + cols - 1) // cols)), 'white')\nfor k, p in enumerate(outs):\n sheet.paste(Image.open(p).resize((w, h)), ((k % cols) * w, (k // cols) * h))\nsheet.save('preview/一覧.png')\nprint('\\n'.join(outs))\n</code></pre><h3 id=\"index_Y7zJS_Te\">_制作ベース/sound_synth.py</h3><p>新規。効果音(ホワーン・キラリン・ポロロンなど)と、仮の BGM をコードで作る(386行)。</p><pre><code class=\"language-python\">\"\"\"効果音と仮のBGMを、コードで作る(音源サイトに頼らない。だれでも同じ音を作り直せる)。\n使い方: python3 <_制作ベース>/sound_synth.py → <_制作ベース>/sound/ に wav を書く(warm を付けると温かみの BGM、bgm3 を付けると BGM の候補3種を作る)\n se_pop.wav ぽん(図・数字が出る。いちばんよく使う)\n se_tick.wav こつ(小さな切り替え。字幕の枚が変わる・箇条書きが1つ出る)\n se_whoosh.wav しゅっ(章や場面が大きく変わる)\n se_ding.wav ちん(結論・大事な数字)\n se_rise.wav ひゅい(驚き・問いかけ)。エイイチは「ホワーン」と呼ぶ。場面の切り替えに使う\n se_chapter.wav ポロン(章の始まりなど、大きな流れの変わり目。2026-10-12 追加)\n se_chapter_marimba.wav ポロロン(マリンバの上がる3音)。se_chapter の候補\n se_chapter_harp.wav シャララン(ハープ風に弦をはじいて上がる)。se_chapter の候補\n se_chapter_piano.wav ポーン(やわらかいピアノの和音を1つ)。se_chapter の候補\n se_chapter_glock.wav キラリン(鉄琴の高い3音)。se_chapter の候補\n se_chapter_swell.wav フワァ(弦の和音がふくらんで消える)。se_chapter の候補\n se_chapter_wind.wav シャラン(ウィンドチャイム)。se_chapter の候補\n bgm_仮.wav 仮のBGM(電子音。32秒で切れ目なくループ)\n bgm_仮_温かみ.wav 仮のBGM(はじく弦・やわらかいピアノ・弦の和音・ウッドベース風。2026-10-12 エイイチの「もう少し温かみ」の指示で追加)\n ElevenLabs Music などの本番のBGMができるまでの、方向を確かめる用\n bgm_1_ピアノ / bgm_2_ギターとマリンバ / bgm_3_ピッツィカート BGM の候補3種(2026-10-12)\n音の高さは、ナレーション(男声・おおむね100〜300Hz)とぶつからないよう、効果音は 500Hz より上を中心にしている。\n\"\"\"\nimport os, sys\nimport numpy as np\nfrom timing import SR, write_wav\n\nHERE = os.path.dirname(os.path.abspath(__file__))\nOUT = os.path.join(HERE, 'sound')\nrng = np.random.default_rng(7)\n\n\ndef t_(dur):\n return np.arange(int(dur * SR)) / SR\n\n\ndef env(n, attack=0.004, decay=0.15):\n t = np.arange(n) / SR\n return np.minimum(1, t / attack) * np.exp(-t / decay)\n\n\ndef norm(a, peak=0.9):\n return a / (np.abs(a).max() + 1e-9) * peak\n\n\ndef lowpass(a, cutoff):\n \"\"\"1次のローパス(やわらかくするだけ)\"\"\"\n k = np.exp(-2 * np.pi * cutoff / SR)\n out = np.empty_like(a); y = 0.0\n for i, x in enumerate(a):\n y = (1 - k) * x + k * y; out[i] = y\n return out\n\n\ndef pop():\n t = t_(0.12)\n f = 520 + 520 * np.exp(-t / 0.018) # 高い所から素早く下がる\n a = np.sin(2 * np.pi * np.cumsum(f) / SR) * env(len(t), 0.002, 0.035)\n a += 0.25 * np.sin(2 * np.pi * 2 * np.cumsum(f) / SR) * env(len(t), 0.001, 0.015)\n return norm(a)\n\n\ndef tick():\n t = t_(0.05)\n a = np.sin(2 * np.pi * 1800 * t) * env(len(t), 0.0008, 0.008)\n a += 0.4 * lowpass(rng.standard_normal(len(t)), 4000) * env(len(t), 0.0005, 0.004)\n return norm(a, 0.7)\n\n\ndef whoosh():\n d = 0.55; t = t_(d); n = len(t)\n noise = rng.standard_normal(n)\n # 帯域を低→高へ動かす(2つのローパスの差で帯域を作る)\n out = np.empty(n); y1 = y2 = 0.0\n for i in range(n):\n p = i / n\n c1 = 300 + 3200 * p ** 1.5; c2 = c1 * 0.35\n k1 = np.exp(-2 * np.pi * c1 / SR); k2 = np.exp(-2 * np.pi * c2 / SR)\n y1 = (1 - k1) * noise[i] + k1 * y1; y2 = (1 - k2) * noise[i] + k2 * y2\n out[i] = y1 - y2\n shape = np.sin(np.pi * np.minimum(1, t / d)) ** 2 * (t / d) ** 0.4\n return norm(out * shape, 0.8)\n\n\ndef ding():\n t = t_(1.2)\n f0 = 1046.5 # C6\n a = (np.sin(2 * np.pi * f0 * t) * np.exp(-t / 0.45)\n + 0.35 * np.sin(2 * np.pi * f0 * 2.0 * t) * np.exp(-t / 0.2)\n + 0.12 * np.sin(2 * np.pi * f0 * 3.0 * t) * np.exp(-t / 0.08))\n a *= np.minimum(1, t / 0.003)\n return norm(a, 0.8)\n\n\ndef rise():\n t = t_(0.35)\n f = 600 * (2 ** (t / 0.35 * 1.0)) # 1オクターブ上がる\n a = np.sin(2 * np.pi * np.cumsum(f) / SR)\n a *= np.sin(np.pi * t / 0.35) ** 1.5\n return norm(a, 0.7)\n\n\ndef chapter():\n \"\"\"章の変わり目の「ポロン」。やわらかい鐘の音を2つ(ミ→ラ)、少しずらして鳴らす。ホワーンより長く、落ち着いた音\"\"\"\n d = 1.6; t = t_(d); out = np.zeros(len(t))\n for k, (f0, at, g) in enumerate([(659.3, 0.0, 1.0), (880.0, 0.13, 0.85)]):\n tt = np.clip(t - at, 0, None); on = (t >= at)\n a = (np.sin(2 * np.pi * f0 * tt) * np.exp(-tt / 0.55) + 0.3 * np.sin(2 * np.pi * f0 * 2 * tt) * np.exp(-tt / 0.25)\n + 0.1 * np.sin(2 * np.pi * f0 * 3.01 * tt) * np.exp(-tt / 0.1))\n out += g * a * on * np.minimum(1, tt / 0.004)\n return norm(lowpass(out, 6000), 0.8)\n\n\ndef bgm_placeholder():\n \"\"\"やわらかいパッドと、小さな粒の音(アルペジオ)。テンポ 96・4小節×2=32秒・ループで切れ目が出ないよう、はみ出した余韻は頭に回す\"\"\"\n bpm = 96; beat = 60 / bpm; bars = 8; L = int(bars * 4 * beat * SR)\n buf = np.zeros((L, 2))\n chords = [[57, 60, 64, 67], [53, 57, 60, 64], [48, 52, 55, 59], [55, 59, 62, 64]] # Am7 Fmaj7 Cmaj7 G6(MIDI番号)\n hz = lambda m: 440 * 2 ** ((m - 69) / 12)\n\n def add(sig, start, pan=0.0):\n i0 = int(start * SR); idx = (np.arange(len(sig)) + i0) % L\n np.add.at(buf[:, 0], idx, sig * (1 - pan) * 0.5 + sig * 0.5 * (1 - abs(pan)))\n np.add.at(buf[:, 1], idx, sig * (1 + pan) * 0.5 + sig * 0.5 * (1 - abs(pan)))\n\n for b in range(bars):\n ch = chords[b % 4]\n d = 4 * beat\n t = t_(d + 1.5)\n pad_env = np.minimum(1, t / 0.9) * np.where(t < d, 1, np.exp(-(t - d) / 0.6))\n for k, m in enumerate(ch):\n f = hz(m)\n s = sum(np.sin(2 * np.pi * f * (1 + det) * t + ph) for det, ph in ((-0.003, 0), (0.003, 1.3)))\n s += 0.2 * np.sin(2 * np.pi * 2 * f * t)\n add(0.05 * s * pad_env, b * d, pan=(k - 1.5) * 0.25)\n bass = hz(ch[0] - 12)\n tb = t_(d)\n add(0.10 * np.sin(2 * np.pi * bass * tb) * np.minimum(1, tb / 0.05) * np.exp(-tb / 2.5), b * d)\n # 8分音符の粒(上の2音を行き来する)\n for s8 in range(8):\n m = ch[[1, 2, 3, 2, 1, 2, 3, 2][s8]] + 12\n tp = t_(0.6)\n note = np.sin(2 * np.pi * hz(m) * tp) * np.exp(-tp / 0.18) * np.minimum(1, tp / 0.004)\n vel = 0.05 if s8 % 2 == 0 else 0.032\n add(vel * note, b * d + s8 * beat / 2, pan=0.35 if s8 % 2 else -0.35)\n buf[:, 0] = lowpass(buf[:, 0], 5000); buf[:, 1] = lowpass(buf[:, 1], 5000)\n return buf / (np.abs(buf).max() + 1e-9) * 0.8\n\n\ndef pluck(f, dur, bright=0.5):\n \"\"\"はじいた弦(Karplus-Strong)。bright が小さいほど丸い音\"\"\"\n n = int(dur * SR); p = max(2, int(SR / f))\n buf = lowpass(rng.uniform(-1, 1, p), 1500 + 5000 * bright)\n out = np.empty(n)\n for i in range(n):\n out[i] = buf[i % p]\n buf[i % p] = 0.996 * 0.5 * (buf[i % p] + buf[(i + 1) % p])\n return out * np.minimum(1, np.arange(n) / (0.002 * SR))\n\n\ndef felt_piano(f, dur):\n \"\"\"フェルトを挟んだピアノ風(倍音を少し伸ばしてずらし、高い倍音ほど早く消す)\"\"\"\n t = t_(dur)\n a = sum((0.6 ** k) * np.sin(2 * np.pi * f * (k + 1) * (1 + 0.0004 * k * k) * t) * np.exp(-t / (1.8 / (k + 1) ** 0.8))\n for k in range(6))\n return a * np.minimum(1, t / 0.006)\n\n\ndef strings(f, dur):\n \"\"\"弦の和音のパッド(ゆっくり入り、少しビブラート)\"\"\"\n t = t_(dur)\n vib = 1 + 0.003 * np.sin(2 * np.pi * 5.2 * t)\n a = sum(np.sin(2 * np.pi * f * k * vib * t + k) / k ** 1.3 for k in range(1, 7))\n a += 0.6 * sum(np.sin(2 * np.pi * f * 1.002 * k * vib * t) / k ** 1.3 for k in range(1, 7))\n return a * np.minimum(1, t / 1.2) * np.minimum(1, (dur - t) / 0.8)\n\n\ndef bgm_warm():\n \"\"\"テンポ 88・8小節。ギターの分散和音、ピアノの和音、弦、ウッドベース風。ループで切れ目が出ないよう余韻は頭に回す\"\"\"\n bpm = 88; beat = 60 / bpm; bars = 8; L = int(bars * 4 * beat * SR)\n buf = np.zeros((L, 2))\n hz = lambda m: 440 * 2 ** ((m - 69) / 12)\n prog = [[48, 55, 64, 67, 72], [45, 52, 60, 64, 69], [41, 53, 57, 60, 65], [43, 55, 59, 62, 67]] # C Am F G(ギターの押さえ方に近い並び)\n\n def add(sig, start, pan=0.0, gain=1.0):\n i0 = int(start * SR); idx = (np.arange(len(sig)) + i0) % L\n np.add.at(buf[:, 0], idx, sig * gain * (1 - max(pan, 0)))\n np.add.at(buf[:, 1], idx, sig * gain * (1 + min(pan, 0)))\n\n for b in range(bars):\n ch = prog[b % 4]; d = 4 * beat; t0 = b * d\n # ギター: 8分音符の分散和音(指ではじく)\n pat = [0, 2, 3, 4, 1, 3, 2, 4]\n for k, j in enumerate(pat):\n vel = 0.22 if k % 2 == 0 else 0.15\n add(pluck(hz(ch[j]), 1.6, bright=0.35), t0 + k * beat / 2 + rng.uniform(0, 0.008), pan=-0.35, gain=vel)\n # ピアノ: 小節の頭と3拍目に、高い所で和音を置く\n for beat_at, g in ((0, 0.10), (2, 0.06)):\n for m in ch[2:]:\n add(felt_piano(hz(m + 12), 2.4), t0 + beat_at * beat + 0.01, pan=0.3, gain=g)\n # 弦: 和音の3音を長く\n for m in ch[1:4]:\n add(lowpass(strings(hz(m), d + 0.6), 2500), t0, pan=0.0, gain=0.018)\n # ウッドベース風: 1拍目と3拍目\n for beat_at, m in ((0, ch[0] - 12), (2, ch[0] - 12 + (7 if b % 2 else 0))):\n add(pluck(hz(m), 1.2, bright=0.1), t0 + beat_at * beat, gain=0.5)\n buf[:, 0] = lowpass(buf[:, 0], 6000); buf[:, 1] = lowpass(buf[:, 1], 6000)\n return buf / (np.abs(buf).max() + 1e-9) * 0.8\n\n# ---------- BGM の候補(2026-10-12 エイイチの指示で3種。どれも電子音を避け、楽器に近い音で組む) ----------\nHZ = lambda m: 440 * 2 ** ((m - 69) / 12)\n\n\nclass Loop:\n \"\"\"小節数ぶんの輪(ループ)の入れ物。はみ出した余韻は頭に回すので、つなぎ目が出ない\"\"\"\n def __init__(self, bpm, bars):\n self.beat = 60 / bpm; self.bar = 4 * self.beat\n self.L = int(bars * self.bar * SR); self.buf = np.zeros((self.L, 2))\n\n def add(self, sig, start, pan=0.0, gain=1.0):\n i0 = int(start * SR); idx = (np.arange(len(sig)) + i0) % self.L\n np.add.at(self.buf[:, 0], idx, sig * gain * (1 - max(pan, 0)))\n np.add.at(self.buf[:, 1], idx, sig * gain * (1 + min(pan, 0)))\n\n def out(self, cutoff=6500):\n b = self.buf\n b[:, 0] = lowpass(b[:, 0], cutoff); b[:, 1] = lowpass(b[:, 1], cutoff)\n return b / (np.abs(b).max() + 1e-9) * 0.8\n\n\ndef marimba(f, dur=1.0):\n \"\"\"木の鍵盤(マリンバ)。倍音が 1・3.9・9.2 倍にあり、すぐ消える\"\"\"\n t = t_(dur)\n a = (np.sin(2 * np.pi * f * t) * np.exp(-t / 0.35) + 0.25 * np.sin(2 * np.pi * f * 3.9 * t) * np.exp(-t / 0.06)\n + 0.08 * np.sin(2 * np.pi * f * 9.2 * t) * np.exp(-t / 0.02))\n return a * np.minimum(1, t / 0.002)\n\n\ndef pizz(f, dur=0.7):\n \"\"\"弦をはじく(ピッツィカート)。ギターより短く丸い\"\"\"\n a = pluck(f, dur, bright=0.2)\n return a * np.exp(-t_(dur)[:len(a)] / 0.18)\n\n\ndef shaker(dur=0.09):\n t = t_(dur)\n n = rng.standard_normal(len(t))\n return (n - lowpass(n, 5000)) * np.sin(np.pi * t / dur) ** 2\n\n\ndef bgm_piano():\n \"\"\"1. ピアノ中心・ゆったり(テンポ76)。ピアノの分散和音に弦がうすく重なる。考える場面向き。\n 分散和音はナレーションの声の高さ(男声 100〜300Hz)とぶつからないよう、1オクターブ上で弾く\"\"\"\n lp = Loop(76, 8)\n prog = [[41, 53, 57, 60, 64], [40, 52, 55, 59, 62], [38, 50, 53, 57, 60], [36, 48, 52, 55, 59]] # Fmaj7 Em7 Dm7 Cmaj7\n mel = [72, None, 71, None, 69, None, 67, None] # 小節の頭にだけ置く、ゆっくり下がる音\n for b in range(8):\n ch = prog[b % 4]; t0 = b * lp.bar\n for k, j in enumerate([1, 2, 3, 4, 3, 2, 3, 4]):\n lp.add(felt_piano(HZ(ch[j] + 12), 2.6), t0 + k * lp.beat / 2 + rng.uniform(0, 0.01), pan=0.15, gain=0.09 if k % 2 == 0 else 0.065)\n lp.add(felt_piano(HZ(ch[0]), 3.5), t0, gain=0.12)\n if mel[b]:\n lp.add(felt_piano(HZ(mel[b] + 12), 3.0), t0 + 0.02, pan=-0.2, gain=0.06)\n for m in ch[1:4]:\n lp.add(lowpass(strings(HZ(m), lp.bar + 0.8), 2200), t0, gain=0.014)\n return lp.out(6000)\n\n\ndef bgm_guitar_marimba():\n \"\"\"2. ギターとマリンバ・明るめ(テンポ96)。ギターの刻みに、木の鍵盤の短いフレーズ。フック・導入向き\"\"\"\n lp = Loop(96, 8)\n prog = [[48, 55, 64, 67, 72], [47, 55, 62, 67, 71], [45, 52, 60, 64, 69], [41, 53, 57, 60, 65]] # C G/B Am F\n riff = [(0, 0), (0.75, 2), (1.5, 1), (2.5, 2), (3, 3)] # (拍, 和音の何番目) マリンバ\n for b in range(8):\n ch = prog[b % 4]; t0 = b * lp.bar\n for k, j in enumerate([0, 2, 3, 4, 2, 3, 4, 3]):\n lp.add(pluck(HZ(ch[j]), 1.4, bright=0.4), t0 + k * lp.beat / 2 + rng.uniform(0, 0.006), pan=-0.4, gain=0.2 if k % 2 == 0 else 0.13)\n if b % 2 == 1:\n for at, j in riff:\n lp.add(marimba(HZ(ch[2:][j % 3] + 12)), t0 + at * lp.beat, pan=0.4, gain=0.16)\n for at in (0, 2):\n lp.add(pluck(HZ(ch[0] - 12), 1.1, bright=0.1), t0 + at * lp.beat, gain=0.5)\n for k in range(8):\n lp.add(shaker(), t0 + k * lp.beat / 2 + lp.beat / 4, pan=0.2, gain=0.05 if k % 2 else 0.03)\n return lp.out(4000) # はじく音は高い成分が多いので、丸めて温かくする\n\n\ndef bgm_pizzicato():\n \"\"\"3. 弦のピッツィカートとピアノ・少し先が気になる感じ(テンポ104)。ドキュメンタリーの解説向き\"\"\"\n lp = Loop(104, 8)\n prog = [[45, 57, 60, 64], [41, 57, 60, 65], [36, 55, 60, 64], [43, 55, 59, 62]] # Am F C G\n for b in range(8):\n ch = prog[b % 4]; t0 = b * lp.bar\n for k, j in enumerate([1, 2, 3, 2, 1, 2, 3, 2]):\n lp.add(pizz(HZ(ch[j])), t0 + k * lp.beat / 2, pan=-0.3 if k % 2 else 0.3, gain=0.22 if k % 2 == 0 else 0.15)\n for at, m in ((0, ch[0] - 12), (1.5, ch[0] - 12), (2, ch[0] - 5)):\n lp.add(pizz(HZ(m), 0.9), t0 + at * lp.beat, gain=0.45)\n for m in ch[1:]:\n lp.add(felt_piano(HZ(m + 12), 2.2), t0 + 0.01, pan=0.1, gain=0.05)\n if b >= 4: # 後半だけ弦の和音が入り、少しふくらむ\n for m in ch[1:]:\n lp.add(lowpass(strings(HZ(m), lp.bar + 0.6), 2400), t0, gain=0.012)\n return lp.out(3800)\n\n\nBGMS = {'bgm_1_ピアノ': bgm_piano, 'bgm_2_ギターとマリンバ': bgm_guitar_marimba, 'bgm_3_ピッツィカート': bgm_pizzicato}\n\n\n# ---------- 大きな流れの変わり目の音の候補(2026-10-12 エイイチの指示で3つ) ----------\ndef chapter_marimba():\n \"\"\"ポロロン: マリンバで ド・ミ・ソ と上がる\"\"\"\n out = np.zeros(int(1.6 * SR))\n for k, f in enumerate([523.3, 659.3, 784.0]):\n a = marimba(f, 1.2) * (0.8 if k < 2 else 1.0)\n i = int(k * 0.09 * SR); out[i:i + len(a)] += a[:len(out) - i]\n return norm(lowpass(out, 7000), 0.8)\n\n\ndef chapter_harp():\n \"\"\"シャララン: 弦をはじく音を6つ、ハープのように素早く上がる\"\"\"\n out = np.zeros(int(2.0 * SR))\n for k, m in enumerate([60, 64, 67, 72, 76, 79]):\n a = pluck(440 * 2 ** ((m - 69) / 12), 1.6, bright=0.3) * (0.6 + 0.08 * k)\n i = int(k * 0.055 * SR); out[i:i + len(a)] += a[:len(out) - i]\n return norm(lowpass(out, 6000), 0.8)\n\n\ndef chapter_piano():\n \"\"\"ポーン: やわらかいピアノで ド・ソ・ミ・ソ の和音を1つ(低い音から少しずつずらして弾く)\"\"\"\n out = np.zeros(int(2.2 * SR))\n for k, m in enumerate([48, 55, 64, 67]):\n a = felt_piano(440 * 2 ** ((m - 69) / 12), 2.2) * (1.0 if k == 0 else 0.7)\n i = int(k * 0.025 * SR); out[i:i + len(a)] += a[:len(out) - i]\n return norm(lowpass(out, 5000), 0.8)\n\n\nCHAPTER_CANDS = {'se_chapter_marimba': chapter_marimba, 'se_chapter_harp': chapter_harp, 'se_chapter_piano': chapter_piano}\n\n\ndef chapter_glock():\n \"\"\"キラリン: 鉄琴(グロッケン)で ミ・ソ・ド と上がる。倍音は 1・2.76・5.4 倍(金属の棒の音)\"\"\"\n out = np.zeros(int(1.8 * SR)); t = t_(1.6)\n for k, f in enumerate([1318.5, 1568.0, 2093.0]):\n a = (np.sin(2 * np.pi * f * t) * np.exp(-t / 0.7) + 0.25 * np.sin(2 * np.pi * f * 2.76 * t) * np.exp(-t / 0.15)\n + 0.08 * np.sin(2 * np.pi * f * 5.4 * t) * np.exp(-t / 0.05)) * np.minimum(1, t / 0.001)\n i = int(k * 0.08 * SR); out[i:i + len(a)] += a[:len(out) - i] * (0.7 + 0.15 * k)\n return norm(lowpass(out, 9000), 0.7)\n\n\ndef chapter_swell():\n \"\"\"フワァ: 弦の和音(ド・ミ・ソ・ド)が0.8秒でふくらみ、1秒で消える\"\"\"\n d = 1.9; t = t_(d)\n a = sum(lowpass(strings(440 * 2 ** ((m - 69) / 12), d), 3000) for m in [60, 64, 67, 72])\n shape = np.where(t < 0.8, np.sin(np.pi / 2 * t / 0.8) ** 2, np.exp(-(t - 0.8) / 0.35))\n return norm(a * shape, 0.8)\n\n\ndef chapter_wind():\n \"\"\"シャラン: 細い金属の筒(ウィンドチャイム)が、0.5秒の間に9回ふれあう\"\"\"\n out = np.zeros(int(2.0 * SR)); t = t_(1.4); r = np.random.default_rng(11)\n scale = [1760.0, 1975.5, 2217.5, 2637.0, 2960.0, 3520.0]\n for k in range(9):\n f = scale[r.integers(len(scale))]\n a = (np.sin(2 * np.pi * f * t) + 0.3 * np.sin(2 * np.pi * f * 2.7 * t) * np.exp(-t / 0.2)) * np.exp(-t / 0.6) * np.minimum(1, t / 0.001)\n i = int((k * 0.055 + r.uniform(0, 0.02)) * SR); out[i:i + len(a)] += a[:len(out) - i] * r.uniform(0.5, 1.0)\n return norm(lowpass(out, 10000), 0.6)\n\n\nCHAPTER_CANDS.update({'se_chapter_glock': chapter_glock, 'se_chapter_swell': chapter_swell, 'se_chapter_wind': chapter_wind})\n\n\nif __name__ == '__main__':\n os.makedirs(OUT, exist_ok=True)\n for name, f in [('se_pop', pop), ('se_tick', tick), ('se_whoosh', whoosh), ('se_ding', ding), ('se_rise', rise), ('se_chapter', chapter)]:\n write_wav(os.path.join(OUT, f'{name}.wav'), (f() * 32767).astype(np.int16))\n print(name)\n for name, f in CHAPTER_CANDS.items():\n write_wav(os.path.join(OUT, f'{name}.wav'), (f() * 32767).astype(np.int16))\n print(name)\n if 'bgm3' in sys.argv:\n for name, f in BGMS.items():\n write_wav(os.path.join(OUT, f'{name}.wav'), (f() * 32767).astype(np.int16), ch=2)\n print(name)\n sys.exit()\n if 'warm' not in sys.argv:\n write_wav(os.path.join(OUT, 'bgm_仮.wav'), (bgm_placeholder() * 32767).astype(np.int16), ch=2)\n print('bgm_仮')\n write_wav(os.path.join(OUT, 'bgm_仮_温かみ.wav'), (bgm_warm() * 32767).astype(np.int16), ch=2)\n print('bgm_仮_温かみ')\n</code></pre><h3 id=\"index_0alA5Ix0\">_制作ベース/sound_eleven.py</h3><p>新規。ElevenLabs で BGM と効果音を作る。API キーを使うので Mac で動かす(71行)。</p><pre><code class=\"language-python\">\"\"\"BGM と効果音を ElevenLabs で作る。エイイチの Mac で動かす(API キーを Claude に渡さないため。tts_eleven.py と同じ)。\n\n準備: ターミナルで read -s ELEVENLABS_API_KEY && export ELEVENLABS_API_KEY\n (キーを貼って Enter。画面には出ない)\n\n使い方(動画の作業フォルダで)\n BGM: python3 ../_制作ベース/sound_eleven.py music --name bgm_hook --sec 60 --prompt \"(説明文)\"\n 効果音: python3 ../_制作ベース/sound_eleven.py sfx --name se_pop2 --sec 0.5 --prompt \"(説明文)\"\n --name 保存する名前(sound/{名前}.mp3 になる。sound.py の file にこの名前のパスを書く)\n --sec 長さ(秒)。BGM は 3〜600、効果音は 0.5〜30\n --prompt どんな音か(英語の方が安定する。説明文の候補は 音源_候補.md)\n --count 同じ説明文で何本作るか(既定 1。2 以上だと bgm_hook_1.mp3, bgm_hook_2.mp3…)\n --loop 効果音を切れ目なくループする音にする(環境音など)\nBGM は歌なし(force_instrumental)で作る。\nStarter プランの決まり(2026-10-12 に ElevenLabs の Eleven Music の規約で確認):\n 商用に使える(映画・テレビ・ラジオ・ゲームの一部を除く)。クレジット表記はいらない。\n 作れる曲の長さは月 17 分まで。曲そのものを音楽配信サービスに出すのは禁止(動画の BGM に使うのはこれに当たらない)\n有料プランの間に作った音は、解約したあとも商用に使える(無料プランに戻ってから作った音は使えない)\n\"\"\"\nimport argparse, json, os, sys, time, urllib.request, urllib.error\n\nap = argparse.ArgumentParser()\nap.add_argument('kind', choices=['music', 'sfx'])\nap.add_argument('--name', required=True)\nap.add_argument('--sec', type=float, required=True)\nap.add_argument('--prompt', required=True)\nap.add_argument('--count', type=int, default=1)\nap.add_argument('--loop', action='store_true')\nap.add_argument('--model', default='')\nargs = ap.parse_args()\nkey = os.environ.get('ELEVENLABS_API_KEY')\nif not key:\n sys.exit('ELEVENLABS_API_KEY が入っていません。ターミナルで read -s ELEVENLABS_API_KEY && export ELEVENLABS_API_KEY を実行してから動かしてください')\n\nos.makedirs('sound', exist_ok=True)\nif args.kind == 'music':\n if not 3 <= args.sec <= 600:\n sys.exit('BGM の長さは 3〜600 秒')\n url = 'https://api.elevenlabs.io/v1/music?output_format=mp3_44100_128'\n body = {'prompt': args.prompt, 'music_length_ms': int(args.sec * 1000), 'force_instrumental': True}\nelse:\n if not 0.5 <= args.sec <= 30:\n sys.exit('効果音の長さは 0.5〜30 秒')\n url = 'https://api.elevenlabs.io/v1/sound-generation?output_format=mp3_44100_128'\n body = {'text': args.prompt, 'duration_seconds': args.sec, 'prompt_influence': 0.5}\n if args.loop:\n body['loop'] = True\nif args.model:\n body['model_id'] = args.model\n\nlog = os.path.join('sound', '作った記録.jsonl')\nfor k in range(args.count):\n name = args.name if args.count == 1 else f'{args.name}_{k + 1}'\n path = os.path.join('sound', f'{name}.mp3')\n if os.path.exists(path):\n sys.exit(f'{path} はもうあります(上書きしない)。--name を変えてください')\n req = urllib.request.Request(url, data=json.dumps(body).encode(),\n headers={'xi-api-key': key, 'Content-Type': 'application/json'})\n try:\n with urllib.request.urlopen(req, timeout=600) as r:\n open(path, 'wb').write(r.read())\n extra = {h: r.headers.get(h) for h in ('song-id', 'character-cost') if r.headers.get(h)}\n except urllib.error.HTTPError as e:\n sys.exit(f'{name} で止まりました: {e.code} {e.read().decode(errors=\"ignore\")[:300]}')\n except urllib.error.URLError as e:\n sys.exit(f'{name} で止まりました(ElevenLabs につながらない): {e.reason}')\n with open(log, 'a', encoding='utf-8') as f:\n f.write(json.dumps(dict(file=path, kind=args.kind, sec=args.sec, prompt=args.prompt, loop=args.loop,\n model=args.model or '既定', time=time.strftime('%Y-%m-%d %H:%M'), **extra),\n ensure_ascii=False) + '\\n')\n print(f'できあがり: {path}')\n</code></pre><h3 id=\"index_ZuLtrq6q\">_制作ベース/tts_eleven.py</h3><p>直した。読みを確かめる試しの文だけを作る --try を足した。Mac で動かす(114行)。</p><pre><code class=\"language-python\">\"\"\"ハコブくんの声(ElevenLabs)で、台本を1文ずつ音声にする。エイイチの Mac で動かす(API キーを Claude に渡さないため)。\n\n準備(1回だけ)\n 1. ElevenLabs の有料プランに入る(無料プランの音声は商用に使えない)\n 2. Voice Design で声を作って保存し、その声の Voice ID を控える(Voices の一覧で声を開くと出る)\n 3. ターミナルで API キーを入れる(キーは画面やファイルに残さない): export ELEVENLABS_API_KEY=(キー)\n\n使い方(動画の作業フォルダで。例: 動画/AIエージェントの取り違え/)\n python3 ../_制作ベース/tts_eleven.py --voice VOICE_ID --name 候補A --lines 0-12\n --lines 作る文の番号(0 から。「0-12」「5,8,9」のように書く。省略するとすべて)\n --model eleven_v4(既定。2026-10-12 に決定)/eleven_multilingual_v2/eleven_v3\n --seed 同じ値なら、なるべく同じ読み方になる(既定 7)\n --try 台本ではなく、試しの文を作る(「|」で区切って何文でも。例: --try \"エイイチは…。|栄一は…。\")\n 出力は voice_eleven/{名前}/試し_01.mp3 … と、どの文かの一覧 試し.txt\n出力: voice_eleven/{名前}/{番号}.mp3 と、作った設定の記録 voice_eleven/{名前}/設定.json\nこのあと Claude が tts_import.py で 48kHz の wav にして、口の動きと字幕に使う。\n使う文字数(クレジット)は、文の字数の合計。1本約4,100字。作り直した分も数える\n\"\"\"\nimport argparse, json, os, sys, time, urllib.request, urllib.error\nsys.path.insert(0, '.')\nfrom narration import SEGS\ntry:\n from narration import parse\nexcept ImportError:\n def parse(s):\n return s[0], \"base\", s[1], (s[2] if len(s) > 2 else 0.35)\ntry:\n from narration import ELEVEN_READ # ElevenLabs が読み間違えた語だけ、ここで読みを直す(字幕は変えない)\nexcept ImportError:\n ELEVEN_READ = []\n\nap = argparse.ArgumentParser()\nap.add_argument('--voice', required=True)\nap.add_argument('--name', required=True)\nap.add_argument('--lines', default='')\nap.add_argument('--model', default='eleven_v4') # 2026-10-12 エイイチが v4 に決めた(冒頭14文で v2 と聞き比べ)\nap.add_argument('--seed', type=int, default=7)\nap.add_argument('--stability', type=float, default=0.55)\nap.add_argument('--similarity', type=float, default=0.75)\nap.add_argument('--try', dest='trial', default='')\nargs = ap.parse_args()\nkey = os.environ.get('ELEVENLABS_API_KEY')\nif not key:\n sys.exit('ELEVENLABS_API_KEY が入っていません。ターミナルで export ELEVENLABS_API_KEY=(キー) を実行してから動かしてください')\n\n\ndef nums(spec, n):\n if not spec:\n return list(range(n))\n out = []\n for part in spec.split(','):\n if '-' in part:\n a, b = part.split('-'); out += list(range(int(a), int(b) + 1))\n else:\n out.append(int(part))\n return [i for i in out if 0 <= i < n]\n\n\ndef speak_text(t):\n t = t.replace('|', '').replace('「', '').replace('」', '')\n for a, b in ELEVEN_READ:\n t = t.replace(a, b)\n return t\n\n\ntexts = [parse(s)[2] for s in SEGS]\noutdir = os.path.join('voice_eleven', args.name)\nif args.trial: # 試しの文だけを作る(台本の読みの直し ELEVEN_READ は使わない。書いたとおりに読ませて聞き比べる)\n os.makedirs(outdir, exist_ok=True)\n lines = [t for t in args.trial.split('|') if t]\n with open(os.path.join(outdir, '試し.txt'), 'w', encoding='utf-8') as f:\n for k, t in enumerate(lines, 1):\n f.write(f'試し_{k:02d}: {t}\\n')\n for k, t in enumerate(lines, 1):\n body = {'text': t, 'model_id': args.model, 'seed': args.seed,\n 'voice_settings': {'stability': args.stability, 'similarity_boost': args.similarity}}\n req = urllib.request.Request(\n f'https://api.elevenlabs.io/v1/text-to-speech/{args.voice}?output_format=mp3_44100_128',\n data=json.dumps(body).encode(), headers={'xi-api-key': key, 'Content-Type': 'application/json'})\n try:\n with urllib.request.urlopen(req, timeout=120) as r:\n open(os.path.join(outdir, f'試し_{k:02d}.mp3'), 'wb').write(r.read())\n except urllib.error.HTTPError as e:\n sys.exit(f'試し_{k:02d} で止まりました: {e.code} {e.read().decode(errors=\"ignore\")[:300]}')\n print(f'試し_{k:02d} {t}')\n print(f'できあがり: {outdir} 使った文字数 約{sum(len(t) for t in lines)}字')\n sys.exit(0)\nos.makedirs(outdir, exist_ok=True)\njson.dump(dict(vars(args), time=time.strftime('%Y-%m-%d %H:%M')), open(os.path.join(outdir, '設定.json'), 'w'),\n ensure_ascii=False, indent=1)\nused = 0\nfor i in nums(args.lines, len(SEGS)):\n if not texts[i]:\n continue\n prev = next((speak_text(texts[j]) for j in range(i - 1, -1, -1) if texts[j]), None)\n nxt = next((speak_text(texts[j]) for j in range(i + 1, len(texts)) if texts[j]), None)\n body = {'text': speak_text(texts[i]), 'model_id': args.model, 'seed': args.seed,\n 'voice_settings': {'stability': args.stability, 'similarity_boost': args.similarity}}\n if args.model.startswith('eleven_multilingual'): # 前後の文を渡すと、文のつながりの抑揚が自然になる(対応を確かめたモデルだけ)\n body['previous_text'] = prev\n body['next_text'] = nxt\n req = urllib.request.Request(\n f'https://api.elevenlabs.io/v1/text-to-speech/{args.voice}?output_format=mp3_44100_128',\n data=json.dumps(body).encode(), headers={'xi-api-key': key, 'Content-Type': 'application/json'})\n try:\n with urllib.request.urlopen(req, timeout=120) as r:\n open(os.path.join(outdir, f'{i:03d}.mp3'), 'wb').write(r.read())\n except urllib.error.HTTPError as e:\n sys.exit(f'{i:03d} で止まりました: {e.code} {e.read().decode(errors=\"ignore\")[:300]}')\n except urllib.error.URLError as e:\n sys.exit(f'{i:03d} で止まりました(ElevenLabs につながらない): {e.reason}')\n used += len(body['text'])\n print(f'{i:03d} {body[\"text\"][:30]}')\nprint(f'できあがり: {outdir} 使った文字数 約{used}字')\n</code></pre><h3 id=\"index_HLGEVABj\">_制作ベース/tts_import.py</h3><p>ElevenLabs の mp3 を、書き出し用の 48kHz の wav にする(44行)。</p><pre><code class=\"language-python\">\"\"\"ElevenLabs などで作った1文ずつの音声を、書き出しで使う形(tts/{番号}.wav・48kHz モノラル と tts/index.json)にする。\n使い方: 動画の作業フォルダで python3 <_制作ベース>/tts_import.py voice_eleven/候補A\n- その文の音声がないときは、いまの tts/ の音声(仮音声)をそのまま使う。声の聞き比べで、一部の文だけ差し替えられる\n- 文の前後の無音は 0.05 秒を残して切る(間は narration.py の「後ろの間」で決めるため)\n\"\"\"\nimport json, os, subprocess, sys, wave\nimport numpy as np\nsys.path.insert(0, '.')\nfrom narration import SEGS\ntry:\n from narration import parse\nexcept ImportError:\n def parse(s):\n return s[0], \"base\", s[1], (s[2] if len(s) > 2 else 0.35)\n\nSR = 48000\nsrc = sys.argv[1]\nos.makedirs('tts', exist_ok=True)\nold = {o['i']: o for o in json.load(open('tts/index.json'))} if os.path.exists('tts/index.json') else {}\nout, replaced = [], 0\nfor i, s in enumerate(SEGS):\n scene, face, text, pause = parse(s)\n mp3 = os.path.join(src, f'{i:03d}.mp3')\n d, note = 0.0, ''\n if text and os.path.exists(mp3):\n raw = subprocess.run(['ffmpeg', '-v', 'error', '-i', mp3, '-ac', '1', '-ar', str(SR), '-f', 's16le', '-'],\n capture_output=True, check=True).stdout\n a = np.frombuffer(raw, dtype=np.int16)\n loud = np.where(np.abs(a) > 300)[0]\n if len(loud):\n pad = int(0.05 * SR)\n a = a[max(0, loud[0] - pad):min(len(a), loud[-1] + pad)]\n with wave.open(f'tts/{i:03d}.wav', 'wb') as w:\n w.setnchannels(1); w.setsampwidth(2); w.setframerate(SR); w.writeframes(a.tobytes())\n d, note = len(a) / SR, f'voice:{os.path.basename(src.rstrip(\"/\"))}'\n replaced += 1\n elif text and i in old and old[i]['text'] == text:\n d, note = old[i]['dur'], old[i].get('note', '')\n elif text:\n sys.exit(f'{i:03d} の音声がありません({mp3} も、同じ文の仮音声もない)。tts2.py で仮音声を作ってから取り込む')\n out.append(dict(i=i, scene=scene, face=face, text=text, dur=d, pause=pause, note=note))\njson.dump(out, open('tts/index.json', 'w'), ensure_ascii=False, indent=0)\ntot = sum(o['dur'] + o['pause'] for o in out)\nprint(f'差し替えた文 {replaced} 全体 {int(tot // 60)}:{int(tot % 60):02d}')\n</code></pre><h3 id=\"index_zP2G4G0g\">_制作ベース/description.py</h3><p>新規。概要欄の下書きの {t:文番号} を時間に置きかえ、目次の条件を確かめる(40行)。</p><pre><code class=\"language-python\">\"\"\"YouTube の概要欄を、書き出しの時間に合わせて仕上げる。\n使い方(動画の作業フォルダで、assemble.py のあと)\n python3 <_制作ベース>/description.py (--draft を付けると、【要記入】が残っていても 概要欄_確認用.txt に書く)\n読むもの: 概要欄_下書き.md(目次の時間を {t:文番号} と書く)と timeline.json(assemble.py が書く)\n書くもの: 概要欄.txt(YouTube にそのまま貼る。{t:9} が、9番の文が始まる時間「0:56」などに置きかわる)\n- YouTube の目次(チャプター)は、最初が 0:00 で、3つ以上・それぞれ10秒以上が条件。満たさないときは止まる\n- 概要欄は 5,000 字まで\n- 「【要記入:…】」が残っていると止まる(連絡先や SNS の URL など、エイイチが決めて書く所)\n\"\"\"\nimport json, re, sys\n\nFPS = 30\ntl = {o['i']: o for o in json.load(open('timeline.json'))}\nsrc = open('概要欄_下書き.md', encoding='utf-8').read()\n# 「<!-- -->」で囲んだメモは貼らない\nsrc = re.sub(r'<!--.*?-->\\n?', '', src, flags=re.S)\n\n\ndef ts(i):\n if i not in tl:\n sys.exit(f'{i}番の文が timeline.json にありません(全体を書き出してから動かす)')\n s = int(tl[i]['start'] / FPS)\n return f'{s // 3600}:{s % 3600 // 60:02d}:{s % 60:02d}' if s >= 3600 else f'{s // 60}:{s % 60:02d}'\n\n\nout = re.sub(r'\\{t:(\\d+)\\}', lambda m: ts(int(m.group(1))), src).strip() + '\\n'\n# 目次の条件を確かめる\ntimes = [t for t in re.findall(r'^(\\d+:\\d{2}(?::\\d{2})?) ', out, flags=re.M)]\nsecs = [sum(int(x) * 60 ** k for k, x in enumerate(reversed(t.split(':')))) for t in times]\nif times:\n if secs[0] != 0 or len(secs) < 3 or any(b - a < 10 for a, b in zip(secs, secs[1:])):\n sys.exit(f'目次が YouTube の条件を満たしていません: {times}')\ndraft = '--draft' in sys.argv # 「【要記入】」が残っていても、確かめ用に 概要欄_確認用.txt を書く\nif '【' in out and '要記入' in out and not draft:\n sys.exit('「【要記入】」が残っています: ' + ', '.join(re.findall(r'【要記入[^】]*】', out)))\nif len(out) > 5000:\n sys.exit(f'概要欄が {len(out)} 字あります(5,000 字まで)')\nname = '概要欄_確認用.txt' if draft else '概要欄.txt'\nopen(name, 'w', encoding='utf-8').write(out)\nprint(f'{name} を書きました({len(out)} 字・目次 {len(times)} 個)')\n</code></pre><h3 id=\"index_3UslsPCD\">_制作ベース/thumbnail.py</h3><p>新規。サムネイルを動画と同じ文字と色で、HTML で組んで画像にする(214行)。</p><pre><code class=\"language-python\">\"\"\"YouTube のサムネイルを、動画と同じ文字・色(scene_kit.py)で作る。文字は画像生成 AI に任せず、ここで組む(文字の間違いが起きない)。\n使い方(動画の作業フォルダで)\n python3 <_制作ベース>/thumbnail.py\n読むもの: thumb.py(文言・背景の写真・案の指定)\n書くもの: thumb/{案の名前}.png(1280×720。YouTube の上限 2MB 以下)と thumb/一覧.png(並べて比べる用・スマホの小さい表示の大きさも添える)\n\nthumb.py の書き方\n PHOTO = 'サムネ素材/素材2_実機のアーム.png' # 背景の写真(1920×1080 以上)\n HAKOBU = True # 左下にハコブくんを置く\n LABEL = '本当に?' # ハコブくんの横の小さな札\n KICKER = '人型ロボット出荷 約4倍' # 上の小さな一言(オレンジ)\n LINES = ['ロボットにも', '<span class=\"red\">ChatGPTの瞬間</span>', 'もう来た?']\n VARIANTS = ['split', 'dark', 'bold'] # 作る案(split 紙色と写真/dark 黒と写真/bold 大きなハコブくんと縁取り文字)\n BOLD_LINES = ['人型ロボット出荷 約4倍', 'ChatGPTの瞬間', 'もう来た?'] # bold 案の3行(なければ KICKER と LINES)\n\"\"\"\nimport base64, os, sys, subprocess\nsys.path.insert(0, '.')\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(1, HERE)\nfrom scene_kit import BG, INK, RED, SUB, LOGO\nfrom PIL import Image\nfrom playwright.sync_api import sync_playwright\n\nns = {}\nexec(open('thumb.py', encoding='utf-8').read(), ns)\nos.makedirs('thumb', exist_ok=True)\n\n\ndef data_uri(path):\n ext = os.path.splitext(path)[1].lower().lstrip('.')\n return f\"data:image/{'jpeg' if ext in ('jpg', 'jpeg') else 'png'};base64,\" + base64.b64encode(open(path, 'rb').read()).decode()\n\n\nphoto = data_uri(ns['PHOTO'])\ndef _hakobu_uri():\n \"\"\"ハコブくんの体(右向き)。2026-10-12 から、作り直した体に顔を描いた hakobu/thumb_body_right.png を使う\n (元の体は頭の上が切り抜けておらず、大きく出すと頭がへこんで見えた)。なければ動画と同じ体を使う\"\"\"\n import io\n new = os.path.join(HERE, 'hakobu', 'thumb_body_right.png')\n if os.path.exists(new):\n return data_uri(new)\n from hakobu import Hakobu, FX0, FY0\n H = Hakobu(mode='full')\n eyes = ('open', 'open')\n b = H.body.copy(); H._ring(b, 0.7)\n img = b.crop(H.crop_draw)\n gl, la = H._face_layers(eyes, None, 'mid', 0.0)\n ox, oy = FX0 - H.crop_draw[0], FY0 - H.crop_draw[1]\n img.alpha_composite(gl, (ox, oy)); img.alpha_composite(la, (ox, oy))\n img = img.crop((H.crop[0] - H.crop_draw[0], H.crop[1] - H.crop_draw[1], H.crop[2] - H.crop_draw[0], H.crop[3] - H.crop_draw[1]))\n img = img.transpose(Image.FLIP_LEFT_RIGHT)\n img = img.crop(img.getchannel('A').point(lambda v: 255 if v > 60 else 0).getbbox())\n img.save('thumb/_hakobu.png')\n buf = io.BytesIO(); img.save(buf, 'PNG')\n return 'data:image/png;base64,' + base64.b64encode(buf.getvalue()).decode()\n\n\nhk = _hakobu_uri() if ns.get('HAKOBU', True) else None\nkicker, lines, label = ns.get('KICKER', ''), ns['LINES'], ns.get('LABEL', '')\npos = ns.get('PHOTO_POS', '20% 50%') # 写真のどこを見せるか(CSS の background-position)\n\nBASE = f\"\"\"*{{margin:0;padding:0;box-sizing:border-box}}\nhtml,body{{width:1920px;height:1080px;overflow:hidden;font-family:'Noto Sans CJK JP',sans-serif;color:{INK}}}\n.red{{color:{RED}}}\n.kicker{{display:inline-block;font-size:56px;font-weight:900;color:#fff;background:{RED};padding:10px 30px;border-radius:12px;letter-spacing:.02em}}\n.main{{font-weight:900;line-height:1.16;letter-spacing:-.02em}}\n.hk{{position:absolute;left:24px;bottom:-90px;height:430px}}\n.label{{position:absolute;left:350px;bottom:250px;background:#fff;color:{INK};font-size:48px;font-weight:900;padding:12px 26px;border-radius:14px;box-shadow:0 6px 24px rgba(0,0,0,.25)}}\n.label:after{{content:'';position:absolute;left:-22px;bottom:22px;border:14px solid transparent;border-right:14px solid #fff}}\n.brand{{position:absolute;right:56px;top:44px;height:40px}}\n\"\"\"\n\n\ndef hakobu_html():\n if not hk:\n return ''\n lab = f'<div class=\"label\">{label}</div>' if label else ''\n return f'<img class=\"hk\" src=\"{hk}\">{lab}'\n\n\ndef html(kind):\n k = f'<div class=\"kicker\">{kicker}</div>' if kicker else ''\n body = '<br>'.join(lines)\n if kind == 'split': # 左に動画と同じ紙色の面と文字、右に写真\n return f\"\"\"<style>{BASE}\nbody{{background:{BG}}}\n.ph{{position:absolute;right:0;top:0;width:820px;height:1080px;background:url({photo}) {pos}/cover}}\n.txt{{position:absolute;left:96px;top:84px;width:1060px}}\n.main{{font-size:136px;margin-top:28px}}\n</style><div class=\"ph\"></div><div class=\"txt\">{k}<div class=\"main\">{body}</div></div>{hakobu_html()}<img class=\"brand\" src=\"{LOGO}\">\"\"\"\n if kind == 'card': # 写真を全面に敷き、動画の名前テロップと同じ薄い紙色の札に文字\n return f\"\"\"<style>{BASE}\nbody{{background:url({photo}) {pos}/cover}}\n.txt{{position:absolute;left:70px;top:70px;background:rgba(245,245,240,.95);border-radius:24px;padding:44px 60px 52px}}\n.main{{font-size:124px;margin-top:24px}}\n</style><div class=\"txt\">{k}<div class=\"main\">{body}</div></div>{hakobu_html()}\"\"\"\n if kind == 'dark': # 案B: 写真を右へずらして敷き、左は黒に溶かして白い文字。ハコブくんは左下で大きめ、右(アーム)を向く\n return f\"\"\"<style>{BASE}\nbody{{background:#111}}\n.ph{{position:absolute;left:720px;top:0;width:1920px;height:1080px;background:url({photo}) 0 50%/cover}}\n.shade{{position:absolute;inset:0;background:linear-gradient(90deg,rgba(13,13,13,1) 0%,rgba(13,13,13,1) 38%,rgba(13,13,13,.6) 48%,rgba(13,13,13,0) 64%)}}\n.txt{{position:absolute;left:96px;top:70px;width:1150px}}\n.main{{font-size:122px;margin-top:22px;color:#fff;text-shadow:0 4px 18px rgba(0,0,0,.6)}}\n.main .red{{color:#E25A2C}}\n.hk2{{position:absolute;left:40px;bottom:-120px;height:540px}}\n.label{{left:380px;bottom:300px}}\n</style><div class=\"ph\"></div><div class=\"shade\"></div><div class=\"txt\">{k}<div class=\"main\">{body}</div></div><img class=\"hk2\" src=\"{hk}\"><div class=\"label\">{label}</div>\"\"\"\n if kind == 'dark2': # 案B2: ハコブくんを左に大きく(画面の高さいっぱい)。文字は右上、写真は右下\n return f\"\"\"<style>{BASE}\nbody{{background:#111}}\n.ph{{position:absolute;left:560px;top:120px;width:1920px;height:1080px;background:url({photo}) 0 50%/cover}}\n.shade{{position:absolute;inset:0;background:linear-gradient(90deg,rgba(13,13,13,1) 0%,rgba(13,13,13,1) 30%,rgba(13,13,13,0) 46%),linear-gradient(180deg,rgba(13,13,13,1) 0%,rgba(13,13,13,.92) 30%,rgba(13,13,13,0) 58%)}}\n.txt{{position:absolute;right:70px;top:60px;text-align:right}}\n.kicker{{font-size:52px}}\n.main{{font-size:128px;margin-top:20px;color:#fff;text-shadow:0 4px 18px rgba(0,0,0,.7)}}\n.main .red{{color:#E25A2C}}\n.hk2{{position:absolute;left:30px;bottom:-110px;height:900px}}\n.label{{left:90px;top:40px;bottom:auto}}\n.label:after{{left:60px;bottom:-26px;border:14px solid transparent;border-top:14px solid #fff}}\n</style><div class=\"ph\"></div><div class=\"shade\"></div><img class=\"hk2\" src=\"{hk}\"><div class=\"label\">{label}</div><div class=\"txt\">{k}<div class=\"main\">{body}</div></div>\"\"\"\n if kind == 'dark3': # 案B3: 上に横いっぱいの文字(黒地)、下に写真。ハコブくんは左下で大きく、アームを見る\n L = ns.get('BOLD_LINES', [kicker] + lines)\n return f\"\"\"<style>{BASE}\nbody{{background:#111}}\n.ph{{position:absolute;left:470px;top:380px;width:1620px;height:911px;background:url({photo}) 0 40%/cover}}\n.shade{{position:absolute;left:0;right:0;top:380px;height:220px;background:linear-gradient(180deg,#111 0%,rgba(17,17,17,0) 100%)}}\n.shadeL{{position:absolute;left:470px;top:380px;width:260px;bottom:0;background:linear-gradient(90deg,#111 0%,rgba(17,17,17,0) 100%)}}\n.top{{position:absolute;left:0;right:0;top:46px;text-align:center;color:#fff;font-weight:900;line-height:1.12}}\n.top .a{{font-size:78px}} .top .b{{font-size:150px;letter-spacing:-.02em}} .top .b .red{{color:#E25A2C}}\n.hk2{{position:absolute;left:40px;bottom:-90px;height:700px}}\n.label{{left:360px;bottom:520px}}\n</style><div class=\"ph\"></div><div class=\"shade\"></div><div class=\"shadeL\"></div>\n<div class=\"top\"><div class=\"a\"><span style=\"background:{RED};padding:4px 26px;border-radius:12px;font-size:60px\">{L[0]}</span></div>\n<div class=\"b\"><span class=\"red\">{L[1]}</span> {L[2]}</div></div><img class=\"hk2\" src=\"{hk}\"><div class=\"label\">{label}</div>\"\"\"\n if kind in ('photo', 'photo2', 'photo3'):\n # 2026-10-12 エイイチの指示: 背景は全部写真。文字は大きく、写真の上に置くので黒い縁を付ける(上の札は縁なし)\n # photo : 文字は左上、ハコブくんは右下で左(アーム)を向く\n # photo2: 文字は右上、ハコブくんは右下で左を向く\n # photo3: 文字は右上、ハコブくんは左下の隅で右(アーム)を向く。写真は PHOTO3(ハコブくんがアームと箱に重ならないコマ)\n L = [kicker] + lines\n right = kind != 'photo'\n left_hk = kind == 'photo3'\n ph = data_uri(ns['PHOTO3']) if left_hk and ns.get('PHOTO3') else photo\n OUT = f\"-webkit-text-stroke:24px {INK};paint-order:stroke fill;text-shadow:0 6px 18px rgba(0,0,0,.55)\"\n if left_hk:\n HK = f\".hk3{{{{position:absolute;left:10px;bottom:-190px;height:{ns.get('HK_H', 600)}px}}}}\"\n LB = f\"\"\".label{{{{left:{ns.get('HK_H', 600) * 0.62 + 10:.0f}px;bottom:300px;border:6px solid {INK};font-size:54px}}}}\n.label:after{{{{left:-30px;bottom:24px;border:16px solid transparent;border-right:16px solid {INK}}}}}\"\"\"\n else:\n HK = f\".hk3{{{{position:absolute;right:{'30' if right else '60'}px;bottom:-170px;height:{'520' if right else '640'}px;transform:scaleX(-1)}}}}\"\n LB = f\"\"\".label{{{{left:auto;right:{'330' if right else '420'}px;bottom:{'190' if right else '360'}px;border:6px solid {INK};font-size:54px}}}}\n.label:after{{{{left:auto;right:-30px;bottom:24px;border:16px solid transparent;border-left:16px solid {INK}}}}}\"\"\"\n HK, LB = HK.replace('{{', '{').replace('}}', '}'), LB.replace('{{', '{').replace('}}', '}')\n return f\"\"\"<style>{BASE}\nbody{{background:url({ph}) {ns.get('PHOTO_POS_FULL', '50% 50%')}/cover}}\n.dim{{position:absolute;inset:0;background:linear-gradient({'200deg' if right else '160deg'},rgba(0,0,0,.45) 0%,rgba(0,0,0,.15) 45%,rgba(0,0,0,0) 70%)}}\n.txt{{position:absolute;{'right:56px;text-align:right' if right else 'left:60px'};top:40px;font-weight:900;color:#fff;line-height:1.1;letter-spacing:-.02em}}\n.kick{{display:inline-block;font-size:66px;background:{RED};color:#fff;padding:8px 30px 12px;border-radius:14px;letter-spacing:.02em;box-shadow:0 6px 18px rgba(0,0,0,.35)}}\n.l1{{font-size:150px;margin-top:22px;{OUT}}}\n.l2{{font-size:{'200' if right else '212'}px;{OUT}}}\n.l2 .red{{color:#F0602C}}\n.l3{{font-size:{'170' if right else '176'}px;{OUT}}}\n{HK}\n{LB}\n</style><div class=\"dim\"></div>\n<div class=\"txt\"><div class=\"kick\">{L[0]}</div><div class=\"l1\">{L[1]}</div><div class=\"l2\">{L[2]}</div><div class=\"l3\">{L[3]}</div></div>\n<img class=\"hk3\" src=\"{hk}\"><div class=\"label\">{label}</div>\"\"\"\n if kind == 'bold': # ChatGPT 案の構図(左に大きなハコブくんと吹き出し、右に縁取りの太い文字)を、本物の写真と動画の文字で作る\n L = ns.get('BOLD_LINES', [kicker] + lines)\n rows = ''.join(f'<div class=\"r{i}\">{t}</div>' for i, t in enumerate(L))\n return f\"\"\"<style>{BASE}\nbody{{background:#151515}}\n.ph{{position:absolute;left:700px;top:-30px;width:2040px;height:1148px;background:url({photo}) 0 50%/cover}}\n.fade{{position:absolute;inset:0;background:linear-gradient(90deg,#151515 0%,#151515 36%,rgba(21,21,21,0) 50%),linear-gradient(180deg,rgba(0,0,0,.35) 0%,rgba(0,0,0,0) 55%)}}\n.txt{{position:absolute;right:50px;top:70px;text-align:right;font-weight:900;line-height:1.08;letter-spacing:-.01em;color:#fff;\n -webkit-text-stroke:26px {INK};paint-order:stroke fill}}\n.r0{{font-size:92px}} .r1{{font-size:156px;color:{RED};margin-top:14px}} .r2{{font-size:190px;margin-top:6px}}\n.r1 .red{{color:{RED}}}\n.big{{position:absolute;left:30px;bottom:-50px;height:760px}}\n.bubble{{position:absolute;left:110px;top:70px;background:#fff;border:8px solid {INK};border-radius:22px;padding:14px 34px;font-size:76px;font-weight:900;color:{INK}}}\n.bubble:after{{content:'';position:absolute;left:150px;bottom:-44px;border:22px solid transparent;border-top:22px solid {INK}}}\n</style><div class=\"ph\"></div><div class=\"fade\"></div><img class=\"big\" src=\"{hk}\"><div class=\"bubble\">{label}</div><div class=\"txt\">{rows}</div>\"\"\"\n raise SystemExit(f'案の名前が分かりません: {kind}')\n\n\nouts = []\nwith sync_playwright() as p:\n b = p.chromium.launch()\n pg = b.new_page(viewport={'width': 1920, 'height': 1080})\n for kind in ns.get('VARIANTS', ['split', 'card', 'dark']):\n pg.set_content('<!doctype html><html><head><meta charset=\"utf-8\"></head><body>' + html(kind) + '</body></html>')\n pg.wait_for_timeout(300)\n big = f'thumb/_{kind}_1920.png'\n pg.screenshot(path=big)\n im = Image.open(big).convert('RGB').resize((1280, 720), Image.LANCZOS)\n out = f'thumb/{kind}.png'\n im.save(out, optimize=True)\n if os.path.getsize(out) > 2_000_000:\n out = f'thumb/{kind}.jpg'; im.save(out, quality=92)\n os.remove(big)\n outs.append((kind, out))\n b.close()\n\n# 一覧: 原寸の半分と、スマホの一覧で出る大きさ(幅 168px 前後)を並べる\nW = 640 + 24 + 168\nsheet = Image.new('RGB', (W, len(outs) * (360 + 24)), (30, 30, 30))\nfor k, (kind, out) in enumerate(outs):\n im = Image.open(out)\n y = k * (360 + 24)\n sheet.paste(im.resize((640, 360), Image.LANCZOS), (0, y))\n sheet.paste(im.resize((168, 94), Image.LANCZOS), (640 + 24, y))\nsheet.save('thumb/一覧.png')\nfor kind, out in outs:\n print(kind, out, f'{os.path.getsize(out) / 1e6:.2f}MB')\n</code></pre><h3 id=\"index_TxNsgURJ\">_制作ベース/hakobu/make_new_face.py</h3><p>新規。作り直した体の画面に、動画と同じ描き方の顔を写す(64行)。</p><pre><code class=\"language-python\">\"\"\"新しい体(cut_new.png)のディスプレイに、動画と同じ顔を描く。サムネイル用(2026-10-12)\n2026-10-12 エイイチの指摘「顔が変(口と目の配置や大きさ)」で作り直し:\n 自前で描いた目と口をやめ、動画の hakobu.py と同じ描き方(eye_mask / mouth_mask)で描いた顔を、\n 動画のディスプレイ(SCREEN_QUAD を長方形にならしたもの)から新しい体のディスプレイ(screen_quad_new.npy)へ写す。\n 画面の大きさに対する目と口の大きさ・位置は、動画のハコブくんとそのまま同じになる\n使い方: python3 make_new_face.py [目 口] 例: python3 make_new_face.py open mid(既定)\n\"\"\"\nimport os, sys\nimport numpy as np, cv2\nfrom PIL import Image\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, os.path.dirname(HERE))\nimport hakobu as hk\n\n\ndef face_on_new_body(eyes=('open', 'open'), mouth='mid', out='body_new_face.png'):\n im = np.array(Image.open(os.path.join(HERE, 'cut_new.png'))).astype(np.float32)\n q = np.load(os.path.join(HERE, 'screen_quad_new.npy')).astype(np.float32) # 左上・右上・右下・左下\n SS, FX0, FY0, FX1, FY1 = hk.SS, hk.FX0, hk.FY0, hk.FX1, hk.FY1\n hh, ww = (FY1 - FY0) * SS, (FX1 - FX0) * SS\n # 1. 動画と同じ描き方で、顔を画面での向きに描く\n white = np.zeros((hh, ww), np.uint8)\n for kind, box, side in ((eyes[0], hk.V_EYE_LEFT, -1), (eyes[1], hk.V_EYE_RIGHT, 1)):\n em = np.zeros_like(white)\n hk.eye_mask(em, kind, box, side, 0.0)\n white = np.maximum(white, em)\n blue = hk.mouth_mask(mouth)\n la = np.zeros((hh, ww, 4), np.float32)\n la[white > 0, :3] = hk.WHITE\n la[..., 3] = white\n la[blue > 0, :3] = hk.BLUE\n la[..., 3] = np.maximum(la[..., 3], blue)\n la = np.ascontiguousarray(la[:, ::-1]) # 動画と同じく反転して、体の元の向きに戻す\n # 2. 動画のディスプレイをならした長方形(hakobu.py の flat)→ 新しい体のディスプレイ\n S = hk.SCREEN_QUAD\n c = S.mean(0)\n w_ = (np.linalg.norm(S[1] - S[0]) + np.linalg.norm(S[2] - S[3])) / 2\n h_ = (np.linalg.norm(S[3] - S[0]) + np.linalg.norm(S[2] - S[1])) / 2\n flat = np.float32([[c[0] - w_ / 2, c[1] - h_ / 2], [c[0] + w_ / 2, c[1] - h_ / 2],\n [c[0] + w_ / 2, c[1] + h_ / 2], [c[0] - w_ / 2, c[1] + h_ / 2]])\n src = (flat - np.float32([FX0, FY0])) * SS\n M = cv2.getPerspectiveTransform(src, q)\n h, w = im.shape[:2]\n a = la[..., 3:4] / 255\n pre = np.concatenate([la[..., :3] * a, a * 255], 2)\n wl = cv2.warpPerspective(pre, M, (w, h), flags=cv2.INTER_AREA)\n aa = wl[..., 3:4] / 255\n rgb = np.where(aa > 1e-4, wl[..., :3] / np.maximum(aa, 1e-4), 0)\n # 3. 目のまわりのにじむ光(動画と同じ色。体の大きさに合わせてぼかしを広げる)\n scale = np.linalg.norm(q[1] - q[0]) / w_\n eye_a = aa[..., 0] * (rgb[..., 0] > 200)\n glow = cv2.GaussianBlur(eye_a, (0, 0), 2.6 * scale) * 0.75\n for col, al in ((np.float32([205, 225, 255]), glow[..., None]), (rgb, aa)):\n im[..., :3] = im[..., :3] * (1 - al) + col * al\n Image.fromarray(im.clip(0, 255).astype(np.uint8)).save(os.path.join(HERE, out))\n return out\n\n\nif __name__ == '__main__':\n args = sys.argv[1:]\n if len(args) == 2:\n print(face_on_new_body((args[0], args[0]), args[1]))\n else:\n print(face_on_new_body())\n</code></pre><h3 id=\"index_zd9AxR5a\">フィジカルAIの現在地/narration.py</h3><p>台本。1文ごとに(場面、表情、字幕、後ろの間)。読み替えの表 READ と ELEVEN_READ もここ(186行)。</p><pre><code class=\"language-python\"># -*- coding: utf-8 -*-\n# 台本「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」ハコブくん版\n# もとは DAY330 の試作(codex の敵対的検証を反映した版)。2026-10-12 に、ハコブくんが話す形(エイイチは三人称)に直した。\n# 変えたのは、一人称→三人称、自己紹介(確定した文言)、見立ての入り方、NEO の確認日(10月12日)、アウトロ、1文ごとの表情。事実と数字は変えていない\n# 各要素: (場面の名前, 表情, 字幕の文, 後ろの間[秒]) 間を省略すると 0.35 秒\n# 字幕の文の中の「|」: 読み上げは1文のまま、字幕だけをそこで次の枚に替える\nFACES = (\"base\", \"smile\", \"surprise\", \"doubt\", \"think\", \"trouble\", \"joy\", \"wink\")\n\nSEGS = [\n ('hook_ship', 'base', '調査会社Counterpointの推計では、今年の上半期に出荷された人型ロボットは、'),\n ('hook_ship', 'base', '世界で2万2000台を超えました。前の年の同じ時期の、およそ4倍です。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('hook_forecast', 'surprise', '年末までには、5万台を超えると予想されています。'),\n ('hook_forecast', 'base', '家事をする人型ロボットの予約販売も、去年の秋から始まっています。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('hook_question', 'base', 'こういうニュースを見ると、ChatGPTが出たときみたいに、'),\n ('hook_question', 'doubt', 'ロボットも一気に賢くなって、家に来るのはもうすぐ、って感じませんか。', 1.8), # 大きな流れの変わり目の間\n ('title', 'smile', 'Pixels to Motors。案内役のハコブくんです。'),\n ('title', 'base', 'この番組では、フィジカルAIを、現場で使える形にするまでを解説します。'),\n ('title_eiichi', 'base', 'エイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。|その中で分かったことと、そこから考えた見立てを、ハコブくんがお伝えします。', 1.8), # 大きな流れの変わり目の間\n ('question', 'surprise', '今日の問いは、ロボットにもChatGPTのような瞬間は、もう来たのか、です。'),\n ('question', 'base', 'ここでいうChatGPTの瞬間は、専門家じゃなくても、'),\n ('question', 'base', 'いろんな仕事にすぐ使えるようになる転換点、という意味です。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('define', 'base', 'フィジカルAIっていうのは、現実の世界を認識して、行動するAIのことです。'),\n ('define', 'base', '自動運転もここに入りますが、'),\n ('define', 'base', '今日は主に、アームや人型ロボットが物を扱う話に絞ります。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('answer', 'base', '先に結論を言っちゃうと、写真を見せて判断させる入口は、ぐっと簡単になりました。'),\n ('answer', 'trouble', 'でも、物を確実に動かすところは、まだ手間がかかるんですよ。'),\n ('answer', 'base', 'その手間は大きく2つあります。'),\n ('answer', 'base', '動作と結びついたデータが集めにくいこと。そして、機体や環境とのすり合わせです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('stance', 'base', '先に言っておくと、フィジカルAIはまだ使えない、と言いたいわけじゃないんです。'),\n ('stance', 'base', 'どこまで来ていて、どこで手間がかかっているのかを、分けて見たいんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('toc', 'base', '大きく3つあります。'),\n ('toc', 'base', '出荷が4倍になったロボットは、何をしているのか。'),\n ('toc', 'base', 'エイイチの机の上のロボットアームで、何が簡単で、何が難しかったのか。'),\n ('toc', 'base', 'そして、データの壁はどれくらい高くて、誰がどう越えようとしているのか。'),\n ('toc', 'base', '最後に、エイイチの見立てを話します。', 1.8), # 大きな流れの変わり目の間\n ('c1', 'base', '1つ目、出荷が4倍になったロボットは、何をしているのか、です。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('use', 'base', 'Counterpointの推計では、出荷された人型ロボットの6割以上が、'),\n ('use', 'base', '展示やイベントで動きを見せる用途と、研究やデータ生産の用途なんですよ。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('datacollect', 'base', 'データ生産っていうのは、ロボットが学ぶための動きの記録を作ることです。'),\n ('datacollect', 'base', '人が遠隔で動かして記録するやり方も、自律で動いた記録を残すやり方もあります。'),\n ('datacollect', 'doubt', 'ただ、研究向けの台数が全部データ集めに使われる、という意味ではありません。'),\n ('datacollect', 'base', 'それでも、用途の分類に「データ生産」という項目があること自体が、'),\n ('datacollect', 'base', '今の段階をよく表していると思います。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('conflict', 'base', '一方で、別の調査会社のSmart Analytics Globalは、'),\n ('conflict', 'base', '同じ半年の出荷を約1万9100台と推計していて、'),\n ('conflict', 'base', '産業・商業用途が7割を超えた、としています。'),\n ('conflict', 'base', '総数も分類も違うので、この2つは、単純な裏表としては比べられません。'),\n ('conflict', 'doubt', 'それに、出荷台数は、そのまま自律的に働いている台数でもないんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('china', 'surprise', 'もう1つ、SAGの推計では、出荷の97%以上が中国のメーカーです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('neo', 'base', '家のほうも見てみます。'),\n ('neo', 'base', 'アメリカの1Xという会社は、家事をする人型ロボット、NEOの予約を、去年の10月に始めました。'),\n ('neo', 'base', '2万ドルの早期購入枠は、今年配送の予定で、月499ドルのプランは、その後です。'),\n ('neo', 'base', '最初は基本的な自律機能だけで、知らない作業は、'),\n ('neo', 'base', '予約した時間に、1Xの担当者が遠隔で手伝う仕組みなんですよ。'),\n ('neo', 'base', '10月12日の時点で、一般のお客さんへの配送開始を示す公式発表は、'),\n ('neo', 'base', 'エイイチが確認した範囲では、見つかっていません。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('neo_point', 'surprise', 'これ、ちょっと考えさせられませんか。'),\n ('neo_point', 'think', '売っているのはロボットだけじゃなくて、遠隔で手伝う人と、'),\n ('neo_point', 'think', 'そこで動きを覚えていく仕組みごとなんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('bridge1', 'base', '工場でも家でも、使いながらデータを集める動きが出てきているんですよ。'),\n ('bridge1', 'doubt', 'じゃあ、物を動かすのは、何がそんなに大変なのか。'),\n ('bridge1', 'base', 'まず、エイイチの机の上の話をします。', 1.8), # 大きな流れの変わり目の間\n ('c2', 'base', '2つ目、エイイチの机の上のロボットアームで、何が簡単で、何が難しかったのか、です。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('v_real', 'base', 'ここは、エイイチ自身の話から始めます。'),\n ('v_real', 'base', 'エイイチは自宅の机で、mechArm 270という小さなロボットアームを動かしています。'),\n ('v_real', 'base', '赤いブロックを掴んで、箱に置く。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('trials', 'base', 'それだけの作業を、失敗も含めて650回以上試して、全部台帳に残しています。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('easy', 'smile', 'まず、簡単だったほうから話します。掴めたかどうかの判定です。'),\n ('easy', 'smile', 'iPhoneで撮った写真をAIに見せて、「ブロックを掴めていますか」と聞くと、'),\n ('easy', 'joy', 'はい、いいえで答えが返ってくる。この入口は、驚くほど簡単でした。'),\n ('easy', 'doubt', 'ただ、答えが返ることと、毎回正しいことは別です。'),\n ('easy', 'smile', 'どこまで任せられるかは、失敗例も含めて確かめる必要があります。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('hard', 'trouble', '難しかったのは、動かすほうでした。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gap', 'base', 'ロボットアームの爪先がどこに来るかは、普通は関節の角度から計算で出します。'),\n ('gap', 'base', 'ところが、ikpyという計算ツールで出した爪先の位置と、', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('v_twin', 'base', 'パソコンの中に作ったアームの分身、デジタルツインで動かした位置が、', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gap', 'surprise', 'エイイチの設定では、同じ関節の角度なのに、30cm以上食い違ったんです。'),\n ('gap', 'doubt', 'これは、アームが30cm外れた、という意味ではありません。'),\n ('gap', 'base', '計算ツールには、グリッパーの形も、デジタルツインの物理も入っていなくて、爪先の位置の決め方そのものが違っていたんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('v_scan', 'base', 'そこでエイイチは、分身の中でアームを動かして、'),\n ('v_scan', 'base', '関節の角度と爪先の位置の対応表を作りました。'),\n ('v_scan', 'base', 'これは計算の表で、実機で全部の点を測ったわけではありません。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('silent', 'trouble', 'もう1つ困ったのが、黙る失敗です。'),\n ('silent', 'trouble', 'エイイチの機体とソフトの組み合わせでは、肩の関節に、90度ちょっとを超える値を送ると、'),\n ('silent', 'trouble', 'エラーが出ないのに、アームが動かないことがありました。'),\n ('silent', 'trouble', 'ログを1行ずつ見て、やっと気づきました。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('point2', 'surprise', 'でも、ポイントはここなんです。'),\n ('point2', 'think', 'この2つは、AIの学習データが足りない、という話とは別です。'),\n ('point2', 'think', 'モデルや制御が、エイイチの機体と噛み合っているかを確かめる、すり合わせの仕事でした。'),\n ('point2', 'think', 'ほかの環境で学んだことを持ってくる研究も進んでいます。'),\n ('point2', 'think', 'それでも、エイイチの構成でちゃんと動くかは、ここで確かめるしかなかったんです。', 0.6),\n ('point2', 'think', 'じゃあ、もう1つの手間、データのほうは、業界全体でどうなっているのか。', 1.8), # 大きな流れの変わり目の間\n ('c3', 'base', '3つ目、データの壁はどれくらい高くて、誰がどう越えようとしているのか、です。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('years', 'base', 'カリフォルニア大学バークレー校のロボット研究者、'),\n ('years', 'base', 'ケン・ゴールドバーグさんが、去年、こんな概算を紹介しています。'),\n ('years', 'base', '画像と言葉を扱うAIの学習資料を、'),\n ('years', 'base', '人が読んだり見たりする時間に換算すると、約10万年規模になる。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('wall', 'trouble', 'それに比べて、ロボットが手を動かすための、動作と結びついた経験は、集めにくい。'),\n ('wall', 'trouble', 'ゴールドバーグさんは、これを「10万年のデータの壁」と呼んでいます。'),\n ('wall', 'trouble', 'ただ、ロボットにも同じ量が必要だと分かったわけではありません。'),\n ('wall', 'trouble', '集めにくい、という問題提起なんです。', 0.6),\n ('wall', 'doubt', 'ここで、じゃあロボットにChatGPTの瞬間は当分来ない、'),\n ('wall', 'smile', 'と言いたくなるじゃないですか。でも、ちょっと待ってほしいんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gen', 'base', 'アメリカのGeneralistという会社は、人が手で作業する様子を、'),\n ('gen', 'base', '安い装着型の機材で記録して集めています。'),\n ('gen', 'surprise', '会社の発表では、去年の11月に27万時間、今年の4月に50万時間を超えました。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gen2', 'base', 'さらに8月には、簡単で短い作業なら、'),\n ('gen2', 'base', '数秒の実演を見せるだけで、新しい作業をこなす例も発表しています。'),\n ('gen2', 'doubt', 'ただ、本人たちも、作業は簡単で短く、成功率はまだ控えめだと書いています。'),\n ('gen2', 'base', 'それでも、「動かすには毎回大量の専用データが必要」とは、言い切れなくなってきました。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('compare', 'think', 'ここで1つ、注意があります。'),\n ('compare', 'base', '作業を記録した時間と、10万年という数字は、そのまま比べられません。'),\n ('compare', 'base', '何倍足りないかより、いろんな場面の経験を、どれだけ広く集められるかが問題なんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('dirt', 'think', 'つまり、壁はまだ高い。でも、壁の前でみんなが土を積み始めている。'),\n ('dirt', 'think', '1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、'),\n ('dirt', 'think', 'Generalistが人の手作業を記録しているのも、同じ壁に向かっている動きなんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('notall', 'smile', 'ただ、ここで「なんだ、まだ全然ダメなのか」で'),\n ('notall', 'smile', '片付けてしまうのも違うんです。'),\n ('notall', 'smile', '写真を見せて判断させる入口は、エイイチの机の上でも、もう普通に使えています。', 0.6),\n ('notall', 'doubt', 'じゃあ、この2つが混ざっている今、ロボットのニュースの何を見ればいいのか。', 1.8), # 大きな流れの変わり目の間\n ('c4', 'base', 'ここからは、エイイチの見立てです。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('q', 'surprise', 'ロボットのニュースを見たときの問いが、2つに変わるんですよ。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('q1', 'base', '1つは、そのロボットは自律で動いているのか、人が遠隔で動かしているのか、です。'),\n ('q1', 'base', 'デモ動画を見るときは、自律か遠隔操作か、'),\n ('q1', 'base', '再生速度や、人が手を出した条件が書かれているかを見ます。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('q2', 'base', 'もう1つは、その仕事は「見て判断する」で済むのか、'),\n ('q2', 'base', '「触れて動かす」精度が要るのか、です。'),\n ('q2', 'base', '棚の商品が足りているかを見るのは、前者です。'),\n ('q2', 'base', '柔らかい物を潰さずに掴むのは、後者です。'),\n ('q2', 'base', '前者は、手軽に試せるところまで来ています。ただ、どこまで任せられるかは、現場で確かめる必要がある。'),\n ('q2', 'base', '後者は、その現場の機体と環境に合わせて、すり合わせる仕事が、まだ残っています。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gofe', 'base', 'ゴールドバーグさんは、昔ながらのエンジニアリングと学習を組み合わせて、'),\n ('gofe', 'base', '現場で役に立つ仕事をしながら、データを集めていくのが近道だ、と言っています。'),\n ('gofe', 'base', 'エイイチの机の上も、AIと従来の制御を組み合わせる点では通じています。'),\n ('gofe', 'base', 'ただ、現場で働きながらデータが増える仕組みとは、まだ規模も段階も違います。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('motion', 'base', '作る側から見ると、もう1つあります。'),\n ('motion', 'base', 'データの話と並行して、もう1つ要るのが「どう動くか」の設計です。'),\n ('motion', 'base', '例えばロボットが人に物を渡すとき、'),\n ('motion', 'base', '何秒待って手を開けば、人は安心して受け取れるのか。'),\n ('motion', 'base', 'これは、成功か失敗かの判定だけでは出てこない。'),\n ('motion', 'base', '人が安心できる動きを設計する仕事も、同時に必要だと、エイイチは見ています。', 1.8), # 大きな流れの変わり目の間\n ('answer2', 'base', 'じゃあ、冒頭の問いに答えますね。', 0.4),\n ('answer2', 'base', 'エイイチの机の上では、写真を見せて判断させる入口は、ずっと簡単になりました。'),\n ('answer2', 'base', '一方、物を確実に動かすには、機体や環境とのすり合わせが、まだ必要でした。'),\n ('answer2', 'base', 'その負担を減らす研究は、急速に進んでいます。'),\n ('answer2', 'trouble', 'でも、いろんな家で、安全に家事を任せられる転換点まで来たかは、まだ判断できません。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('action', 'base', 'ロボットのデモ動画を見たら、'),\n ('action', 'base', '「これは自律なのか、何回やって、何回うまくいったのか」を、'),\n ('action', 'base', '一度だけ考えてみてください。'),\n ('action', 'smile', 'それだけで、同じ動画の見え方が、かなり変わると思います。', 1.8), # 大きな流れの変わり目の間\n ('outro', 'smile', '今日はここまでです。'),\n ('outro', 'base', 'この動画が気に入ったら、高評価とチャンネル登録をお願いします。'),\n ('outro', 'base', 'ロボットの開発や導入の相談は、概要欄の連絡先から、エイイチに直接どうぞ。'),\n ('outro', 'wink', 'コメントは、エイイチが全部読んでいます。それではまた。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('sources', 'smile', '', 6.0),\n]\n\n# 仮音声(Open JTalk)用の読み(字幕は元の表記のまま)\nREAD = [('Pixels to Motors', 'ピクセルズ トゥー モーターズ'), ('Smart Analytics Global', 'スマート アナリティクス グローバル'), ('Counterpoint', 'カウンターポイント'), ('ChatGPT', 'チャットジーピーティー'), ('Generalist', 'ジェネラリスト'), ('mechArm 270', 'メックアーム ニーナナゼロ'), ('iPhone', 'アイフォーン'), ('ikpy', 'アイケーパイ'), ('NEO', 'ネオ'), ('1X', 'ワンエックス'), ('SAG', 'エスエージー'), ('AI', 'エーアイ'), ('30cm', 'さんじゅっセンチ'), ('爪先', 'つめさき'), ('1万9100台', 'いちまんきゅうせんひゃくだい'), ('値を送る', 'あたいを送る'), ('フィジカルAI', 'フィジカルエーアイ'), ('「', ''), ('」', ''), ('|', '')]\n\n# ElevenLabs の声で読み間違えた語だけ、読みを直す(tts_eleven.py が使う。字幕は変えない)\nELEVEN_READ = [\n ('エイイチ', 'えいいち'), # 2026-10-12 カタカナだと文ごとに抑揚が変わった。ひらがなで語尾が下がる読みにそろう(エイイチが4通りを聞き比べて決めた)\n ('裏表', 'うらおもて'), # 2026-10-12 37番で「うらおもてうら」のように読まれた(エイイチが見つけた)\n]\n\n# 仮音声の間延び対策:ひと息が長い文だけ、読み上げ用に読点を足す(字幕は変えない)\nOVERRIDE = {'同じ半年の出荷を約1万9100台と推計していて、': '同じ半年の出荷を、約1万9100台と、推計していて、', '工場でも家でも、使いながらデータを集める動きが出てきているんですよ。': '工場でも家でも、使いながら、データを集める動きが、出てきているんですよ。', 'ほかの環境で学んだことを持ってくる研究も進んでいます。': 'ほかの環境で学んだことを、持ってくる研究も、進んでいます。', 'ここで、じゃあロボットにChatGPTの瞬間は当分来ない、': 'ここで、じゃあ、ロボットに、ChatGPTの瞬間は、当分来ない、', '1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、': '1Xが、家の中で、遠隔で手伝いながら、動きを覚えさせているのも、'}\n\n\ndef reading(t):\n t = OVERRIDE.get(t, t)\n for a, b in READ:\n t = t.replace(a, b)\n return t\n\n\ndef parse(seg):\n \"\"\"1行を (場面, 表情, 字幕, 間) にそろえる\"\"\"\n if len(seg) >= 3 and seg[1] in FACES:\n scene, face, text = seg[0], seg[1], seg[2]\n pause = seg[3] if len(seg) > 3 else 0.35\n else:\n scene, face, text = seg[0], \"base\", seg[1]\n pause = seg[2] if len(seg) > 2 else 0.35\n return scene, face, text, pause\n</code></pre><h3 id=\"index__EpedxB3\">フィジカルAIの現在地/scenes.py</h3><p>場面ごとの図と、実機の映像の指定(VIDEO)(363行)。</p><pre><code class=\"language-python\"># -*- coding: utf-8 -*-\n# 動画「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」ハコブくん版の場面。共通部品は _制作ベース/scene_kit.py\n# 2026-10-12: DAY330 の試作の場面を、ハコブくんの区画(左下)と三人称の台本に合わせて直した\nimport os as _os, sys as _sys\n_here = _os.path.dirname(_os.path.abspath(__file__))\nfor _c in (_os.environ.get(\"P2M_BASE\", \"\"), _os.path.join(_here, \"..\", \"base\"), _os.path.join(_here, \"..\", \"_制作ベース\")):\n if _c and _os.path.exists(_os.path.join(_c, \"scene_kit.py\")):\n _sys.path.insert(0, _c); break\nfrom scene_kit import *\n\nT1 = \"<b>1</b>出荷されたロボットは何をしているのか\"\nT2 = \"<b>2</b>机の上のロボットアームで、簡単だったこと・難しかったこと\"\nT3 = \"<b>3</b>データの壁の高さと、越えようとしている会社\"\nT4 = \"<b>4</b>エイイチの見立て\"\n\nS = {}\n\nS[\"hook_ship\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期 人型ロボットの世界出荷(推計)</div>\n <div class=\"hero\">22,000<small>台超</small></div>\n <div class=\"h2\" style=\"margin-top:36px\">前年同期の <span class=\"red\">約4倍</span></div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)</div>\"\"\")\n\nS[\"hook_forecast\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年通年の予想</div>\n <div class=\"hero\">50,000<small>台超</small></div>\n <div class=\"h3\" style=\"margin-top:44px\">家事をする人型ロボットの予約販売も、2025年秋から</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)/1X</div>\"\"\")\n\nS[\"hook_question\"] = page(\"\"\"\n<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div class=\"h1\">ロボットも、ChatGPTのときみたいに<br><span class=\"red\">一気に賢くなる?</span></div>\n</div>\"\"\")\n\nS[\"question\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">今日の問い</div>\n <div class=\"h1\">ロボットにも<span class=\"red\">ChatGPTの瞬間</span>は<br>もう来たのか</div>\n <div class=\"h3 muted\" style=\"margin-top:44px\">ChatGPTの瞬間=専門家でなくても、<br>いろんな仕事にすぐ使えるようになる転換点</div>\n</div>\"\"\")\n\nS[\"define\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">フィジカルAI とは</div>\n <div class=\"h2\">現実の世界を認識して、<span class=\"red\">行動する</span>AI</div>\n <div class=\"row\" style=\"margin-top:56px;gap:30px\">\n <div class=\"box\" style=\"border:4px solid #0D0D0D\">ロボットアーム</div>\n <div class=\"box\" style=\"border:4px solid #0D0D0D\">人型ロボット</div>\n <div class=\"box muted\" style=\"background:transparent;border:2px dashed #D6D6D1\">自動運転<br><span style=\"font-size:30px\">今日は扱わない</span></div>\n </div>\n</div>\"\"\")\n\nS[\"answer\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">先に結論</div>\n {two(\"写真を見せて判断させる入口\", \"ぐっと簡単になった\", \"物を確実に動かすところ\", \"まだ手間がかかる\")}\n <div class=\"h3\" style=\"margin-top:56px\">手間は2つ<br>① <span class=\"red\">動作と結びついたデータ</span>が集めにくい<br>② <span class=\"red\">機体や環境とのすり合わせ</span></div>\n</div>\"\"\")\n\nS[\"stance\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2 muted\" style=\"text-decoration:line-through;text-decoration-thickness:6px\">フィジカルAIはまだ使えない</div>\n <div class=\"h1\" style=\"margin-top:48px\">どこまで来ていて、<br>どこで<span class=\"red\">手間がかかっているのか</span>を、<br>分けて見る</div>\n</div>\"\"\")\n\nS[\"toc\"] = page(\"\"\"\n<div class=\"wrap\">\n <ul class=\"list\">\n <li><span class=\"n\">1</span>出荷が4倍になったロボットは、何をしているのか</li>\n <li><span class=\"n\">2</span>机の上のロボットアームで、何が簡単で何が難しかったか</li>\n <li><span class=\"n\">3</span>データの壁はどれくらい高く、誰がどう越えようとしているか</li>\n <li class=\"dim\"><span class=\"n\">+</span>エイイチの見立て</li>\n </ul>\n</div>\"\"\")\n\nS[\"c1\"] = chapter(\"1\", \"出荷が4倍になったロボットは、<br>何をしているのか\")\nS[\"c2\"] = chapter(\"2\", \"机の上のロボットアームで、<br>何が簡単で、何が難しかったのか\")\nS[\"c3\"] = chapter(\"3\", \"データの壁はどれくらい高くて、<br>誰がどう越えようとしているのか\")\nS[\"c4\"] = chapter(\"4\", \"エイイチの見立て\")\n\nS[\"use\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期の人型ロボット出荷の用途(Counterpointの推計)</div>\n {hbar([(\"展示・イベント + 研究・データ生産 6割以上\", 62, RED, \"#fff\"), (\"その他\", 38, \"#D3D3CE\", INK)])}\n <div class=\"h3\" style=\"margin-top:44px\">出荷の <span class=\"red\">6割以上</span> が、<br>動きを見せる用途と、研究・データ生産の用途</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)。帯の長さは「6割以上」を表す目安</div>\"\"\", tag=T1)\n\nS[\"datacollect\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">データ生産 = ロボットが学ぶための、動きの記録を作ること</div>\n <div class=\"row\" style=\"gap:30px\">\n <div class=\"box\">人が遠隔で動かして<br>記録する</div>\n <div class=\"box\">自律で動いた記録を<br>残す</div>\n </div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">※研究向けの台数が、全部データ集めに使われるわけではない</div>\n <div class=\"h3\" style=\"margin-top:20px\">分類に「データ生産」がある = 今の段階を表している</div>\n</div>\"\"\", tag=T1)\n\nS[\"conflict\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">同じ2026年上半期の推計</div>\n {two(\"Counterpoint\", \"見せる用途+研究・データ生産<br>6割以上\", \"Smart Analytics Global(SAG)\", \"産業・商業用途<br>7割超\", c1=\"\", c2=\"\", size=52, note1=\"出荷 22,000台超\", note2=\"出荷 約19,100台\")}\n <div class=\"h3\" style=\"margin-top:46px\">総数も分類も違う → <span class=\"red\">単純な裏表としては比べられない</span></div>\n <div class=\"h3 muted\" style=\"margin-top:10px\">出荷台数 ≠ 自律的に働いている台数</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)/Smart Analytics Global(2026年8月)</div>\"\"\", tag=T1)\n\nS[\"china\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期の人型ロボット出荷のうち、中国メーカーの割合(SAGの推計)</div>\n <div class=\"hero\">97<small>%以上</small></div>\n</div>\n<div class=\"src\">出典:Smart Analytics Global(2026年8月)</div>\"\"\", tag=T1)\n\nS[\"neo\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">1X「NEO」 家事をする人型ロボット(2025年10月 予約開始)</div>\n {two(\"値段と配送\", \"2万ドル:2026年配送予定<br>月499ドル:その後\", \"動き方\", \"最初は基本的な自律機能<br>知らない作業は<br>担当者が遠隔で手伝う\", c1=\"\", c2=\"\", size=46)}\n <div class=\"h3\" style=\"margin-top:40px\">2026年10月12日時点:一般顧客への配送開始の公式発表は<span class=\"red\">確認できず</span></div>\n</div>\n<div class=\"src\">出典:1X 公式の発表・注文ページ(2026年10月12日確認)</div>\"\"\", tag=T1)\n\nS[\"neo_point\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">売っているのは</div>\n <div class=\"flow\">\n <div class=\"box\">ロボット</div><div class=\"arrow\">+</div>\n <div class=\"box\">遠隔で<br>手伝う人</div><div class=\"arrow\">+</div>\n <div class=\"box\" style=\"border:4px solid {RED}\">動きを<br>覚えていく仕組み</div>\n </div>\n</div>\"\"\", tag=T1)\n\nS[\"bridge1\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">工場でも家でも、<br><span class=\"red\">使いながらデータを集める</span>動き</div>\n <div class=\"h2 muted\" style=\"margin-top:56px\">では、物を動かすのは<br>何がそんなに大変なのか</div>\n</div>\"\"\", tag=T1)\n\nS[\"trials\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">赤いブロックを掴んで、箱に置く</div>\n <div class=\"hero\">650<small>回以上</small></div>\n <div class=\"h3\" style=\"margin-top:36px\">失敗も含めた試行回数。全部台帳に記録</div>\n</div>\n<div class=\"src\">PICK→PLACE の通し試行(台帳 pick_place_ledger_v3.csv、2026年6月18日〜7月20日で656回)</div>\"\"\", tag=T2)\n\nS[\"easy\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{BLUE}\">簡単だった:掴めたかの判定</div>\n <div class=\"flow\">\n <div class=\"box\">iPhoneで<br>撮った写真</div><div class=\"arrow\">→</div>\n <div class=\"box\" style=\"flex:1.5\">AIに聞く<br><span style=\"font-size:32px;white-space:nowrap\" class=\"muted\">「ブロックを掴めていますか」</span></div><div class=\"arrow\">→</div>\n <div class=\"box\" style=\"border:4px solid {BLUE}\"><span class=\"blue\">はい</span>/いいえ</div>\n </div>\n <div class=\"h3\" style=\"margin-top:48px\">ただし、<span class=\"red\">答えが返る ≠ 毎回正しい</span></div>\n</div>\"\"\", tag=T2)\n\nS[\"hard\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">難しかった</div>\n <div class=\"h1\">動かすほう</div>\n</div>\"\"\", tag=T2)\n\nS[\"gap\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">エイイチの設定で、同じ関節の角度を入れたときの爪先の位置</div>\n <div style=\"position:relative;height:400px;margin-top:10px\">\n <div style=\"position:absolute;left:60px;top:250px;right:60px;height:4px;background:{RULE}\"></div>\n <div style=\"position:absolute;left:150px;top:218px;width:68px;height:68px;border-radius:50%;background:#fff;border:8px solid {SUB}\"></div>\n <div style=\"position:absolute;left:4px;top:310px;width:440px;text-align:center;font-size:36px;font-weight:700;color:{SUB}\">計算ツール(ikpy)</div>\n <div style=\"position:absolute;right:150px;top:218px;width:68px;height:68px;border-radius:50%;background:{RED}\"></div>\n <div style=\"position:absolute;right:4px;top:310px;width:440px;text-align:center;font-size:36px;font-weight:700;color:{RED}\">デジタルツイン</div>\n <div style=\"position:absolute;left:184px;right:184px;top:140px;height:4px;background:{INK}\"></div>\n <div style=\"position:absolute;left:182px;top:120px;width:4px;height:44px;background:{INK}\"></div>\n <div style=\"position:absolute;right:182px;top:120px;width:4px;height:44px;background:{INK}\"></div>\n <div style=\"position:absolute;left:0;right:0;top:20px;text-align:center;font-size:72px;font-weight:900\"><span class=\"red\">30cm以上</span> 食い違う</div>\n </div>\n <!-- 2026-10-12 説明の1行が右にはみ出して「グリッパ/ー」で折り返していたので、図の下に2行で中央にそろえた(DAY330 で足した説明。内容は同じ) -->\n <div class=\"h3\" style=\"text-align:center;font-size:38px;margin-top:-10px\">アームが外れた距離ではない。<span class=\"red\">爪先の位置の決め方</span>が違っていた</div>\n <div class=\"h3 muted\" style=\"text-align:center;font-size:32px;margin-top:10px\">計算ツールには、グリッパーの形が入っていない</div>\n</div>\n<div class=\"src\">図は模式図</div>\"\"\", tag=T2)\n\nS[\"silent\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">黙る失敗</div>\n <div class=\"flow\">\n <div class=\"box\">肩の関節に<br><span class=\"red\">90度ちょっと</span>を超える値</div><div class=\"arrow\">→</div>\n <div class=\"box\">エラーは出ない<br>アームも動かない</div>\n </div>\n <div class=\"h3 muted\" style=\"margin-top:44px\">エイイチの機体とソフトの組み合わせで起きたこと</div>\n</div>\"\"\", tag=T2)\n\nS[\"point2\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">この2つは</div>\n <div class=\"h2 muted\" style=\"text-decoration:line-through;text-decoration-thickness:5px\">AIの学習データが足りない</div>\n <div class=\"h1\" style=\"margin-top:30px\"><span class=\"red\">すり合わせ</span>の仕事</div>\n <div class=\"h3\" style=\"margin-top:20px\">モデルや制御が、自分の機体と噛み合っているかを確かめる</div>\n</div>\"\"\", tag=T2)\n\nS[\"years\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">画像と言葉を扱うAIの学習資料を、人が読む・見る時間に換算すると</div>\n <div class=\"hero\">約10<small>万年規模</small></div>\n</div>\n<div class=\"src\">出典:Ken Goldberg, Science Robotics(2025年8月, Editorial)</div>\"\"\", tag=T3)\n\nS[\"wall\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"h1\">10万年の<span class=\"red\">データの壁</span></div>\n <div style=\"margin-top:50px\">{two(\"画像と言葉\", \"大量にある\", \"動作と結びついた経験\", \"集めにくい\", size=58)}</div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">※ロボットにも同じ量が必要だと分かったわけではない</div>\n</div>\n<div class=\"src\">出典:Ken Goldberg, Science Robotics(2025年8月)</div>\"\"\", tag=T3)\n\nS[\"gen\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">Generalist が集めた、人の手作業の記録(会社の発表)</div>\n <div style=\"display:flex;flex-direction:column;gap:30px;margin-top:10px\">\n <div style=\"display:flex;align-items:center;gap:30px\"><div style=\"width:260px;font-size:36px;font-weight:700;color:{SUB}\">2025年11月</div>\n <div style=\"width:{int(1060*27/50)}px;height:110px;background:#D3D3CE;border-radius:0 6px 6px 0\"></div><div style=\"font-size:52px;font-weight:900;white-space:nowrap\">27万時間</div></div>\n <div style=\"display:flex;align-items:center;gap:30px\"><div style=\"width:260px;font-size:36px;font-weight:700;color:{SUB}\">2026年4月</div>\n <div style=\"width:1060px;height:110px;background:{RED};border-radius:0 6px 6px 0\"></div><div style=\"font-size:52px;font-weight:900;white-space:nowrap\">50万時間超</div></div>\n </div>\n <div class=\"h3\" style=\"margin-top:44px\">安い装着型の機材で記録</div>\n</div>\n<div class=\"src\">出典:Generalist(GEN-0 2025年11月/GEN-1 2026年4月)</div>\"\"\", tag=T3)\n\nS[\"gen2\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">Generalist GEN-1.5(2026年8月)</div>\n <div class=\"h2\">簡単で短い作業なら、<br><span class=\"red\">数秒の実演</span>で新しい作業をこなす例</div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">本人たちも:作業は簡単で短く、成功率はまだ控えめ</div>\n</div>\n<div class=\"src\">出典:Generalist「GEN-1.5」(2026年8月19日)</div>\"\"\", tag=T3)\n\nS[\"compare\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">作業を記録した時間と、10万年は<br><span class=\"red\">そのまま比べられない</span></div>\n <div class=\"h3\" style=\"margin-top:50px\"><span class=\"muted\">何倍足りないか</span> より <span class=\"red\">どれだけ広く集められるか</span></div>\n</div>\"\"\", tag=T3)\n\nS[\"dirt\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">壁はまだ高い。でも、<br><span class=\"red\">壁の前で土を積み始めている</span></div>\n <ul class=\"list\" style=\"margin-top:30px\">\n <li style=\"font-size:40px\"><span class=\"n\">・</span>1X:家の中で遠隔で手伝いながら、動きを覚えさせる</li>\n <li style=\"font-size:40px\"><span class=\"n\">・</span>Generalist:人の手作業を装着型の機材で記録する</li>\n </ul>\n</div>\"\"\", tag=T3)\n\nS[\"notall\"] = page(f\"\"\"\n<div class=\"wrap\">\n {two(\"写真を見せて判断させる入口\", \"もう普通に使える\", \"物を確実に動かす\", \"すり合わせとデータが<br>まだ要る\", size=56)}\n <div class=\"h3\" style=\"margin-top:48px\">「まだ全然ダメ」でも「もう来た」でもない</div>\n</div>\"\"\", tag=T3)\n\nS[\"q\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h1\">ロボットのニュースを見る<br><span class=\"red\">問いが2つ</span>に変わる</div>\n</div>\"\"\", tag=T4)\n\nS[\"q1\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">問い 1</div>\n <div class=\"h1\">自律で動いているのか、<br>人が遠隔で動かしているのか</div>\n <div class=\"h3 muted\" style=\"margin-top:36px\">デモ動画で見るところ:自律か遠隔か/再生速度/人が手を出した条件</div>\n</div>\"\"\", tag=T4)\n\nS[\"q2\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">問い 2</div>\n <div class=\"h2\" style=\"margin-bottom:40px\">その仕事には、どちらが要るのか</div>\n {two(\"見て判断する\", \"手軽に試せる<br><span style='font-size:34px;color:#888888'>任せる範囲は現場で確かめる</span>\", \"触れて動かす精度\", \"現場の機体と環境に<br>すり合わせる仕事が残る\", size=48, note1=\"例:棚の商品が足りているか見る\", note2=\"例:柔らかい物を潰さずに掴む\")}\n</div>\"\"\", tag=T4)\n\nS[\"gofe\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">ゴールドバーグさんの近道</div>\n <div class=\"flow\">\n <div class=\"box\">昔ながらの工学<br>+学習</div><div class=\"arrow\">→</div>\n <div class=\"box\">現場で役に立つ<br>仕事をする</div><div class=\"arrow\">→</div>\n <div class=\"box\">働きながら<br>データが増える</div>\n </div>\n <div class=\"h3\" style=\"margin-top:48px\">エイイチの机の上:組み合わせる点は通じる。<span class=\"muted\">規模も段階もまだ違う</span></div>\n</div>\"\"\", tag=T4)\n\nS[\"motion\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">データの話と並行して要るもの</div>\n <div class=\"h1\"><span class=\"red\">どう動くか</span>の設計</div>\n <div class=\"h3\" style=\"margin-top:36px\">ロボットが人に物を渡すとき、<br>何秒待って手を開けば、人は安心して受け取れるのか</div>\n <div class=\"h3 muted\" style=\"margin-top:28px\">成功か失敗かの判定だけでは出てこない</div>\n</div>\"\"\", tag=T4)\n\nS[\"answer2\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">冒頭の問いへの答え</div>\n {two(\"写真を見せて判断させる入口\", \"ずっと簡単になった\", \"物を確実に動かす\", \"すり合わせが<br>まだ必要\", size=58)}\n <div class=\"h3\" style=\"margin-top:44px\">負担を減らす研究は急速に進んでいる。<br>家で家事を任せられる転換点かは、<span class=\"red\">まだ判断できない</span></div>\n</div>\"\"\")\n\nS[\"action\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">ロボットのデモ動画を見たら</div>\n <div class=\"h1\">自律なのか、何回やって、<br><span class=\"red\">何回うまくいった</span>のか</div>\n</div>\"\"\")\n\nS[\"sources\"] = page(\"\"\"\n<div class=\"wrap\" style=\"left:420px;bottom:90px;justify-content:flex-start;top:120px\">\n <div class=\"h2\" style=\"margin-bottom:30px\">出典</div>\n <div class=\"p\" style=\"font-size:29px;line-height:1.75\">\n 人型ロボットの出荷:Counterpoint Research(2026年8月)/Smart Analytics Global(2026年8月)<br>\n 1X NEO:1X 公式の発表・注文ページ(2026年10月12日確認)<br>\n 10万年のデータの壁:Ken Goldberg, Science Robotics(2025年8月, Editorial)<br>\n Generalist:GEN-0(2025年11月)、GEN-1(2026年4月)、GEN-1.5(2026年8月)の公式発表<br>\n ほかの機体から学ぶ研究の例:Gemini Robotics 1.5 技術報告(Google DeepMind)<br>\n 実機の数字:Pixels to Motors の台帳(pick_place_ledger_v3.csv)、公開記事 DAY257<br>\n <span class=\"muted\">ナレーションは AI 音声(ElevenLabs)。話しているのは Pixels to Motors のキャラクター、ハコブくん</span>\n </div>\n</div>\"\"\")\n\n\n# --- 強弱をつける場面の上書き(2026-10-11)---\nS[\"hook_question\"] = punch(\"こういうニュースを見ると\", [\"ロボットも\", \"一気に賢くなる?\"])\nS[\"answer\"] = punch(\"先に結論\", ['見て判断する入口は<span class=\"blue\">簡単に</span>', '物を動かすのは<span class=\"red\">まだ手間</span>'], sub=\"手間は2つ:動作と結びついたデータ/機体や環境とのすり合わせ\", size=110)\nS[\"c1\"] = chapter2(\"1つ目\", \"出荷の中身\", \"出荷が4倍になったロボットは、何をしているのか\")\nS[\"c2\"] = chapter2(\"2つ目\", \"机の上の実感\", \"何が簡単で、何が難しかったのか\")\nS[\"c3\"] = chapter2(\"3つ目\", \"データの壁\", \"どれくらい高くて、誰がどう越えようとしているのか\")\nS[\"c4\"] = chapter2(\"最後に\", \"ニュースの見方\", \"エイイチの見立て\")\nS[\"china\"] = dots(97, \"2026年上半期の人型ロボット出荷のうち\", \"中国メーカー\", \"97%\", note=\"SAGの推計では「97%以上」\", src=\"出典:Smart Analytics Global(2026年8月)\", tag=T1)\nS[\"point2\"] = punch(\"この2つは\", [\"データ不足じゃない\", '<span class=\"red\">すり合わせ</span>の仕事'], sub=\"モデルや制御が、自分の機体と噛み合っているかを確かめる\", tag=T2)\nS[\"wall\"] = punch(\"ゴールドバーグさんの問題提起\", ['10万年の', '<span class=\"red\">データの壁</span>'], sub=\"動作と結びついた経験は集めにくい(同じ量が必要と分かったわけではない)\", kc=\"muted\", tag=T3)\nS[\"compare\"] = punch(\"ひとつ注意\", ['<span class=\"muted\">何倍足りないか</span>より', '<span class=\"red\">どれだけ広く</span>集めるか'], sub=\"作業を記録した時間と10万年は、そのまま比べられない\", size=110, tag=T3)\nS[\"answer2\"] = punch(\"冒頭の問いへの答え\", ['判断する入口は<span class=\"blue\">来た</span>', '家事を任せるのは<span class=\"red\">まだ</span>'], sub=\"負担を減らす研究は急速に進んでいる\", size=110)\n\nO = {\n # 2026-10-12 ハコブくんが話す間は番組とハコブくんの名前、エイイチを紹介する文からエイイチの名前を出す(エイイチの指示)\n \"title\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">フィジカルAI解説</div><div class=\"c\">案内役:ハコブくん(AI音声)</div></div>'),\n \"title_eiichi\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">エイイチ</div><div class=\"c\">デザイナー/ロボティクスの研究・開発</div></div>'),\n \"v_real\": overlay('<div class=\"cap\">実機:mechArm 270-Pi 赤いブロックを掴んで箱に置く</div>'),\n \"v_twin\": overlay('<div class=\"cap\">パソコンの中のアームの分身(Unity のデジタルツイン)</div>'),\n \"v_scan\": overlay('<div class=\"cap\">デジタルツインの中で動かして、関節の角度と爪先の位置の対応表を作る</div>'),\n \"outro\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">相談は概要欄の連絡先から</div><div class=\"c\">高評価・チャンネル登録/コメントはエイイチが全部読んでいます</div></div>'),\n}\n\n# 動画シーン: (ソースファイル, 開始秒, 透かし消しの有無)\n# src/ の中身(元は iCloud の 動画/ にある。2026-10-12 前の版の動画と1コマずつ照らして確かめた):\n# c1.mov = DAY123.MOV / c2.mov = DAY124.mov(どちらも撮ったままの元の映像。DAY123.mp4・DAY124.mp4 は Clideo の透かし入りで、透かしを消すと右下がモヤモヤになったので使わない。2026-10-12)/ c3.mov = デジタルツイン_260416.mov(アームが並ぶ画面)/ c4.mov = ロボットアーム失敗_260416.mov(アーム1台と箱)\nVIDEO = {\n \"title\": (\"src/c1.mov\", 0.0, False),\n \"title_eiichi\": (\"src/c1.mov\", None, False), # None = 前の場面(title)の映像の続きから\n \"v_real\": (\"src/c2.mov\", 22.0, False),\n \"v_twin\": (\"src/c4.mov\", 0.0, False),\n \"v_scan\": (\"src/c3.mov\", 0.0, False),\n \"outro\": (\"src/c1.mov\", 8.0, False),\n}\n</code></pre><h3 id=\"index_53PHekBl\">フィジカルAIの現在地/sound.py</h3><p>BGM と効果音の置き方(23行)。</p><pre><code class=\"language-python\"># 音の置き方(audio_mix.py が読む。書き方は audio_mix.py の先頭)\n# 2026-10-12 冒頭の BGM は ElevenLabs Music で作った bgm_明るいB_2(エイイチが選んだ。説明文は sound/作った記録.jsonl)\n\n# 大きさ: 話している間はナレーションより23dB小さく、間は16dB小さく(2026-10-12 エイイチの指示。-22/-15 から約90%に下げた。明るい曲では -15/-9 だと大きすぎた)\nBGM = [\n # 2026-10-12 仕上げ版: 最初から最後まで同じ曲をループで流す(本編・ジングル用の曲は未定)\n dict(file='sound/bgm_明るいB_2.mp3', start=0, end=None, level=-23, gap=-16, fade_in=0.6, fade_out=3.0),\n]\n\nSE = [\n # (文番号, 文の頭から何秒後, 音, ナレーションより何dB)\n # 2026-10-12 エイイチの指示で、効果音は場面切り替えのホワーンだけにした(始まりの「しゅっ」は外した)\n]\n\n# 場面が切り替わるたびに「ホワーン」(se_rise)を入れる。効果音はこれだけ(2026-10-12 エイイチの指示)\nSCENE_SE = dict(file='se_rise', level=-6, offset=-0.15, skip=['title_eiichi']) # 2026-10-12 エイイチの指示で -12 から 6dB 上げた。title→title_eiichi は同じ映像が続くので鳴らさない\n\n# 大きな流れの変わり目(自己紹介・今日の問い・各章の始まり・答え・アウトロ)は、ホワーンの代わりに別の音を鳴らす。\n# 直前の文の後ろに 1.8 秒の間がある(narration.py)。2026-10-12 エイイチの指示\n# 自己紹介(title): キラリン(se_chapter_glock。鉄琴の高い3音)… 2026-10-12 エイイチが決定\n# そのほか: ポロロン(se_chapter_marimba。マリンバの上がる3音)… 試し中。場所ごとの使い分けはこれから決める\nCHAPTER_SE = dict(file='se_chapter_marimba', files={'title': 'se_chapter_glock'}, level=-8, offset=-0.3,\n scenes=['title', 'question', 'c1', 'c2', 'c3', 'c4', 'answer2', 'outro'])\n</code></pre><h3 id=\"index_uVDSAdpJ\">フィジカルAIの現在地/thumb.py</h3><p>サムネイルの文言と写真(14行)。</p><pre><code class=\"language-python\"># サムネイルの文言と背景(thumbnail.py が読む。書き方は thumbnail.py の先頭)\n# 2026-10-12 動画と同じ文字・色で作る(エイイチの指示)。文言はタイトル案1に合わせた\nPHOTO = 'サムネ素材/素材2_実機のアーム.png' # 実機のアームが赤いブロックを掴む瞬間(DAY124.mov の28秒)\nPHOTO_POS = '6% 50%' # split の右の面で、ブロックを掴む爪先からアームの根元までが入る位置\nHAKOBU = True\nLABEL = '本当に?'\nKICKER = '人型ロボット出荷 約4倍'\nLINES = ['ロボットにも', '<span class=\"red\">ChatGPTの瞬間</span>', 'もう来た?']\nVARIANTS = ['photo3'] # 2026-10-12 背景を全部写真に(エイイチの指示。dark/dark2/dark3 → photo2 → photo3)\n# photo3 の写真: ハコブくんを左下の隅に置くので、アームが箱の上にいるコマ(左下のマットが空く)。DAY124.mov の39.8秒\nPHOTO3 = 'サムネ素材/素材3_箱に置く瞬間.png'\nHK_H = 600 # ハコブくんの高さ(1920×1080 のとき。photo2 の 520 より少し大きく)\nPHOTO_POS_FULL = '50% 50%'\nBOLD_LINES = ['人型ロボット出荷 約4倍', 'ChatGPTの瞬間', 'もう来た?']\n</code></pre><h3 id=\"index_Wfwum3Uk\">フィジカルAIの現在地/概要欄_下書き.md</h3><p>概要欄の下書き(67行)。</p><pre><code class=\"language-markdown\"><!--\nYouTube の概要欄の下書き(2026-10-12)。{t:文番号} は、その文が始まる時間に置きかわる。\n全体を書き出したあと、python3 <_制作ベース>/description.py で 概要欄.txt を作り、それを貼る。\n「【要記入:…】」はエイイチが決めて書く。\n-->\n今年の上半期に出荷された人型ロボットは、世界で2万2000台を超えました。前の年の同じ時期の、およそ4倍です(Counterpoint の推計)。\n\nロボットにも、ChatGPT のような瞬間はもう来たのか。\n\n先に答えを言うと、写真を見せて判断させる入口は、ぐっと簡単になりました。でも、物を確実に動かすところは、まだ手間がかかります。手間は2つ。動作と結びついたデータが集めにくいこと。そして、機体や環境とのすり合わせです。\n\nこの動画では、\n\n・出荷が4倍になったロボットは、何をしているのか\n・机の上のロボットアームで、何が簡単で、何が難しかったのか\n・データの壁はどれくらい高くて、誰がどう越えようとしているのか\n\nの3つを見たうえで、ロボットのニュースを見るときの2つの問いを解説します。\n\n▼目次\n0:00 人型ロボットの出荷は4倍。ロボットにもChatGPTの瞬間?\n{t:6} この番組について\n{t:9} 今日の問いと、先に結論\n{t:26} 1. 出荷の中身:4倍になったロボットは何をしているのか\n{t:53} 2. 机の上の実感:何が簡単で、何が難しかったのか\n{t:83} 3. データの壁:どれくらい高く、誰がどう越えようとしているのか\n{t:111} エイイチの見立て:ニュースを見るときの2つの問い\n{t:132} 冒頭の問いへの答え\n{t:141} エンディング\n\n▼出典\n・人型ロボットの出荷:Counterpoint Research(2026年8月20日)の推計\nhttps://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026\n・同じく出荷の推計:Smart Analytics Global(2026年8月)\n・1X NEO:1X 公式の製品・注文ページ(2026年10月12日に確認。一般のお客さんへの配送開始を示す公式発表は、この日の時点で見つかっていない)\nhttps://www.1x.tech/neo\n・10万年のデータの壁:Ken Goldberg「GOFE can close the 100,000-year robot data gap」Science Robotics(2025年8月, Editorial)\nhttps://doi.org/10.1126/scirobotics.aea7390\n・Generalist の会社の公式発表:GEN-0(2025年11月)、GEN-1(2026年4月)、GEN-1.5(2026年8月)\nhttps://generalistai.com/blog/gen-0\nhttps://generalistai.com/blog/gen-1\nhttps://generalistai.com/blog/gen-1.5\n・ほかの機体から学ぶ研究の例:Gemini Robotics 1.5 技術報告(Google DeepMind)\nhttps://arxiv.org/abs/2510.03342\n・実機の数字:Pixels to Motors の試行の台帳(pick_place_ledger_v3.csv)、公開記事 DAY257\nhttps://adaptdesign.jp/pixelstomotors/record/why-z-correction-failed-twin-poses\n<!-- 2026-10-12 リンクを確認: Counterpoint・1X・Generalist の3本・arXiv はページが開くことを確かめた。Science Robotics は DOI(バークレーの研究室が公開している PDF のファイル名と同じ番号)。Smart Analytics Global は公式の発表ページが見つからなかったので、リンクなし -->\n\n▼この番組について\n「フィジカルAI解説|Pixels to Motors」は、フィジカルAIを、現場で使える形にするまでを解説する番組です。\nエイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。その中で分かったことと、そこから考えた見立てを、案内役のハコブくんがお伝えします。\n\n・ナレーションは AI 音声(ElevenLabs)です。話しているのは Pixels to Motors のキャラクター、ハコブくんです\n・BGM は ElevenLabs Music で作った曲、効果音はコードで作った音です\n\n▼ロボットの開発・導入のご相談\nメール:pixelstomotors@adaptdesign.jp\n\n▼リンク\nWebサイト:https://adaptdesign.jp/pixelstomotors\nGitHub:https://github.com/PixelstoMotors\nX:https://x.com/adaptdesignjp\nLinkedIn:https://www.linkedin.com/in/eiichi-shimazu\n\nチャンネル登録・高評価もよろしくお願いします。コメントは、エイイチが全部読んでいます。\n\n#フィジカルAI #人型ロボット #ロボット\n</code></pre><h3 id=\"index_cfdrc8jp\">フィジカルAIの現在地/plot_ducking.py</h3><p>この記事の BGM のグラフを描いたスクリプト(69行)。</p><pre><code class=\"language-python\">\"\"\"記事 DAY333 の図: 20〜80秒のナレーションの波形と、audio_mix.py が決める BGM の大きさを並べて描く。\n使い方: 動画の作業フォルダで python3 plot_ducking.py\n\"\"\"\nimport sys\nimport numpy as np\nsys.path.insert(0, '.')\nsys.path.insert(1, '../_制作ベース')\nsys.path.insert(2, '../base')\nfrom timing import load, layout, narration, FPS, SR\nimport audio_mix as am\nimport matplotlib\nmatplotlib.use('Agg')\nimport matplotlib.pyplot as plt\nfrom matplotlib import font_manager\n\nFONT = '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc'\nfont_manager.fontManager.addfont(FONT)\nplt.rcParams['font.family'] = font_manager.FontProperties(fname=FONT).get_name()\n\nidx = load(None)\nlayout(idx)\nvoice = narration(idx).astype(np.float32) / 32768\ntotal = len(voice) / SR\n\n# audio_mix.py の mix() と同じ手順で「話している所」を決める\nctl = np.zeros(int(total * 100) + 1)\nfor o in idx:\n if o['text']:\n a = max(0, int((o['start'] / FPS - 0.15) * 100))\n b = int(((o['start'] + o['speech']) / FPS + 0.3) * 100)\n ctl[a:b] = 1\ngaps = np.where(np.diff(np.concatenate([[1], ctl, [1]])) != 0)[0].reshape(-1, 2)\nfor a, b in gaps:\n if b - a < 50:\n ctl[a:b] = 1\nctl = am.smooth(ctl, 0.3)\nlv = 20 * np.log10(am.db(-23) * ctl + am.db(-16) * (1 - ctl))\n\nt0, t1 = 20, 80\nt = np.arange(len(ctl)) / 100\nm = (t >= t0) & (t <= t1)\nfig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 5.2), sharex=True, gridspec_kw={'height_ratios': [1, 1.2]})\ns0, s1 = int(t0 * SR), int(t1 * SR)\nv = voice[s0:s1]\nstep = 200\nenv = np.abs(v[:len(v) // step * step].reshape(-1, step)).max(1)\ntv = t0 + np.arange(len(env)) * step / SR\nax1.fill_between(tv, -env, env, color='#0d0d0d', lw=0)\nax1.set_ylabel('ナレーション')\nax1.set_yticks([])\nax2.plot(t[m], lv[m], color='#C4471F', lw=2)\nax2.set_ylim(-25, -14)\nax2.set_yticks([-23, -16])\nax2.set_yticklabels(['-23dB 話している間', '-16dB 0.8秒以上の間'])\n# 効果音の位置(assemble_full.log の時刻)\nses = [(22.78, 'ホワーン'), (33.40, 'キラリン'), (55.20, 'ポロロン'), (71.38, 'ホワーン')]\nfor x, l in ses:\n if t0 <= x <= t1:\n for a in (ax1, ax2):\n a.axvline(x, color='#1A6C98', lw=1, ls='--')\n ax2.text(x + 0.4, -14.6, l, color='#1A6C98', fontsize=10)\nax2.set_xlabel('動画の時間(秒)')\nfor a in (ax1, ax2):\n for sp in ('top', 'right'):\n a.spines[sp].set_visible(False)\nfig.suptitle('「フィジカルAIの現在地」20〜80秒:話している間は BGM を下げ、間で戻す(audio_mix.py)', fontsize=12)\nfig.tight_layout()\nfig.savefig('DAY333_02_bgm_ducking.png', dpi=150, facecolor='#F5F5F0')\nprint('DAY333_02_bgm_ducking.png')\n</code></pre><p></p><h2 id=\"index_MBHw9jHD\">書き出しのログと測った数値</h2><h3 id=\"index_Q1bHZ06h\">assemble.py の書き出しのログ(全量)</h3><p>最後に全体を書き出したときのログです。1行目が BGM と効果音の数、そのあとが効果音を鳴らした時刻・文番号・音・大きさ・その文の先頭16字、最後の行が全体のコマ数と確認用ファイルの大きさです。145番の文は、字幕のないエンディングの文です。</p><pre><code>BGM 1本・効果音 45個 → mix.wav\n 13.25秒 文002 se_rise -6dB 「年末までには、5万台を超えると予」\n 22.78秒 文004 se_rise -6dB 「こういうニュースを見ると、Cha」\n 33.40秒 文006 se_chapter_glock -8dB 「Pixels to Motors」\n 55.20秒 文009 se_chapter_marimba -8dB 「今日の問いは、ロボットにもCha」\n 71.38秒 文012 se_rise -6dB 「フィジカルAIっていうのは、現実」\n 85.32秒 文015 se_rise -6dB 「先に結論を言っちゃうと、写真を見」\n 106.02秒 文019 se_rise -6dB 「先に言っておくと、フィジカルAI」\n 117.05秒 文021 se_rise -6dB 「大きく3つあります。」\n 137.90秒 文026 se_chapter_marimba -8dB 「1つ目、出荷が4倍になったロボッ」\n 144.62秒 文027 se_rise -6dB 「Counterpointの推計で」\n 155.95秒 文029 se_rise -6dB 「データ生産っていうのは、ロボット」\n 181.92秒 文034 se_rise -6dB 「一方で、別の調査会社のSmart」\n 207.78秒 文039 se_rise -6dB 「もう1つ、SAGの推計では、出荷」\n 215.22秒 文040 se_rise -6dB 「家のほうも見てみます。」\n 251.52秒 文047 se_rise -6dB 「これ、ちょっと考えさせられません」\n 262.98秒 文050 se_rise -6dB 「工場でも家でも、使いながらデータ」\n 276.63秒 文053 se_chapter_marimba -8dB 「2つ目、エイイチの机の上のロボッ」\n 285.45秒 文054 se_rise -6dB 「ここは、エイイチ自身の話から始め」\n 298.85秒 文057 se_rise -6dB 「それだけの作業を、失敗も含めて6」\n 306.35秒 文058 se_rise -6dB 「まず、簡単だったほうから話します」\n 332.85秒 文063 se_rise -6dB 「難しかったのは、動かすほうでした」\n 336.58秒 文064 se_rise -6dB 「ロボットアームの爪先がどこに来る」\n 347.92秒 文066 se_rise -6dB 「パソコンの中に作ったアームの分身」\n 353.82秒 文067 se_rise -6dB 「エイイチの設定では、同じ関節の角」\n 373.52秒 文070 se_rise -6dB 「そこでエイイチは、分身の中でアー」\n 387.08秒 文073 se_rise -6dB 「もう1つ困ったのが、黙る失敗です」\n 404.65秒 文077 se_rise -6dB 「でも、ポイントはここなんです。」\n 434.47秒 文083 se_chapter_marimba -8dB 「3つ目、データの壁はどれくらい高」\n 442.28秒 文084 se_rise -6dB 「カリフォルニア大学バークレー校の」\n 459.62秒 文088 se_rise -6dB 「それに比べて、ロボットが手を動か」\n 488.68秒 文094 se_rise -6dB 「アメリカのGeneralistと」\n 505.52秒 文097 se_rise -6dB 「さらに8月には、簡単で短い作業な」\n 527.92秒 文101 se_rise -6dB 「ここで1つ、注意があります。」\n 542.65秒 文104 se_rise -6dB 「つまり、壁はまだ高い。でも、壁の」\n 561.08秒 文107 se_rise -6dB 「ただ、ここで「なんだ、まだ全然ダ」\n 581.20秒 文111 se_chapter_marimba -8dB 「ここからは、エイイチの見立てです」\n 585.28秒 文112 se_rise -6dB 「ロボットのニュースを見たときの問」\n 590.05秒 文113 se_rise -6dB 「1つは、そのロボットは自律で動い」\n 605.52秒 文116 se_rise -6dB 「もう1つは、その仕事は「見て判断」\n 634.32秒 文122 se_rise -6dB 「ゴールドバーグさんは、昔ながらの」\n 657.38秒 文126 se_rise -6dB 「作る側から見ると、もう1つありま」\n 684.30秒 文132 se_chapter_marimba -8dB 「じゃあ、冒頭の問いに答えますね。」\n 710.45秒 文137 se_rise -6dB 「ロボットのデモ動画を見たら、」\n 725.13秒 文141 se_chapter_marimba -8dB 「今日はここまでです。」\n 742.95秒 文145 se_rise -6dB 「」\nframes 22473 = 749.1s 確認用 29.4MB (crf 29)\n</code></pre><h3 id=\"index_vKASe5en\">上げたファイルの中身(ffprobe)</h3><pre><code>$ ffprobe -v error -show_entries format=duration,bit_rate:stream=codec_name,width,height,r_frame_rate,sample_rate,bit_rate -of default=nw=1 out.mp4\ncodec_name=h264\nwidth=1920\nheight=1080\nr_frame_rate=30/1\nbit_rate=488631\ncodec_name=aac\nsample_rate=48000\nr_frame_rate=0/0\nbit_rate=203703\nduration=749.100000\nbit_rate=701165\n</code></pre><p>1つ目の codec_name から bit_rate までが映像(488,631 ビット/秒)、2つ目が音声(AAC・48kHz・203,703 ビット/秒)、最後の duration と bit_rate がファイル全体です。</p><h3 id=\"index_gpWY2NxQ\">音の大きさ(ffmpeg の ebur128)</h3><pre><code>$ ffmpeg -nostats -i out.mp4 -af ebur128 -f null - 2>&1 | grep -A8 \"Summary\"\n[Parsed_ebur128_0 @ 0x5569ec070940] Summary:\n\n Integrated loudness:\n I: -16.0 LUFS\n Threshold: -26.4 LUFS\n\n Loudness range:\n LRA: 3.1 LU\n Threshold: -36.4 LUFS\n</code></pre><h3 id=\"index_DJoi5KGb\">ファイルの指紋(md5)</h3><pre><code>$ md5sum out.mp4 out_確認用.mp4\nd9f6def3f1a01d9aee5c76c9d6516bc0 out.mp4\n06c2afe18606354bd366a2e4224fab31 out_確認用.mp4\n</code></pre><h3 id=\"index_uLT3z9rw\">作った曲の記録(sound/作った記録.jsonl・全量)</h3><p><code>sound_eleven.py</code> が、曲を作るたびに1行ずつ書き足す記録です。prompt が ElevenLabs Music に渡した文章です。</p><pre><code>{\"file\": \"sound/bgm_本編_1.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Warm, gentle instrumental background music recorded with real acoustic instruments, for a thoughtful explainer about robots and AI. Felt piano and fingerpicked acoustic guitar, soft cello and violin, upright bass, very quiet brushed drums. Natural room sound, like a small live session. Around 90 BPM, calm and hopeful, a touch of color but serious in tone. No synthesizers, no electronic sounds, no vocals. A simple melody that stays out of the narrator's way.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:26\", \"song-id\": \"angU6pw2wgogpMCVOVzl\"}\n{\"file\": \"sound/bgm_本編_2.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Warm, gentle instrumental background music recorded with real acoustic instruments, for a thoughtful explainer about robots and AI. Felt piano and fingerpicked acoustic guitar, soft cello and violin, upright bass, very quiet brushed drums. Natural room sound, like a small live session. Around 90 BPM, calm and hopeful, a touch of color but serious in tone. No synthesizers, no electronic sounds, no vocals. A simple melody that stays out of the narrator's way.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:26\", \"song-id\": \"rGaTcW0xZPfECyYFmCtW\"}\n{\"file\": \"sound/bgm_冒頭_1.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Bright but gentle instrumental music played on real acoustic instruments, for the opening of a serious technology explainer. Acoustic guitar, piano, light pizzicato strings and soft marimba, natural room sound. Around 100 BPM, curious and hopeful, not cheesy. No synthesizers, no electronic sounds, no vocals, no dramatic hits.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:26\", \"song-id\": \"B0CIEZhLC948zw7GsAcP\"}\n{\"file\": \"sound/bgm_冒頭_2.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Bright but gentle instrumental music played on real acoustic instruments, for the opening of a serious technology explainer. Acoustic guitar, piano, light pizzicato strings and soft marimba, natural room sound. Around 100 BPM, curious and hopeful, not cheesy. No synthesizers, no electronic sounds, no vocals, no dramatic hits.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:27\", \"song-id\": \"5NbSMGUTgt0DXRjOTTB1\"}\n{\"file\": \"sound/bgm_明るいA_1.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Bright, warm and uplifting instrumental music played on real acoustic instruments, in a major key, for a friendly technology explainer about robots. Strummed and fingerpicked acoustic guitar, bright piano, glockenspiel accents, light hand claps and soft shaker, warm upright bass. Natural room sound. Around 105 BPM, cheerful and optimistic like a sunny morning, but not childish or cheesy. No synthesizers, no electronic sounds, no vocals. A simple melody that stays out of the narrator's way.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:31\", \"song-id\": \"Hd4Qnr113KBdkYe8qE9k\"}\n{\"file\": \"sound/bgm_明るいA_2.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Bright, warm and uplifting instrumental music played on real acoustic instruments, in a major key, for a friendly technology explainer about robots. Strummed and fingerpicked acoustic guitar, bright piano, glockenspiel accents, light hand claps and soft shaker, warm upright bass. Natural room sound. Around 105 BPM, cheerful and optimistic like a sunny morning, but not childish or cheesy. No synthesizers, no electronic sounds, no vocals. A simple melody that stays out of the narrator's way.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:31\", \"song-id\": \"bQUshWUbu4Y7vH4a43QI\"}\n{\"file\": \"sound/bgm_明るいB_1.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Hopeful, bright instrumental music on real acoustic instruments, in a major key, for a positive and curious explainer about the future of robots. Piano and acoustic guitar leading, warm strings, light pizzicato, a few glockenspiel notes, soft percussion. Natural room sound. Around 100 BPM, optimistic and forward-moving, bright but calm enough to sit under narration. No synthesizers, no electronic sounds, no vocals, no big build-ups.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:31\", \"song-id\": \"qyMIv2n4wM3BGvVoNqKR\"}\n{\"file\": \"sound/bgm_明るいB_2.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Hopeful, bright instrumental music on real acoustic instruments, in a major key, for a positive and curious explainer about the future of robots. Piano and acoustic guitar leading, warm strings, light pizzicato, a few glockenspiel notes, soft percussion. Natural room sound. Around 100 BPM, optimistic and forward-moving, bright but calm enough to sit under narration. No synthesizers, no electronic sounds, no vocals, no big build-ups.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:31\", \"song-id\": \"19pIxHwtXfiNptitO2FF\"}\n</code></pre><h2 id=\"index_azDryZvY\">書き出した概要欄(概要欄.txt・全量)</h2><p><code>description.py</code> が書き出し、YouTube に貼ったものです。</p><pre><code>今年の上半期に出荷された人型ロボットは、世界で2万2000台を超えました。前の年の同じ時期の、およそ4倍です(Counterpoint の推計)。\n\nロボットにも、ChatGPT のような瞬間はもう来たのか。\n\n先に答えを言うと、写真を見せて判断させる入口は、ぐっと簡単になりました。でも、物を確実に動かすところは、まだ手間がかかります。手間は2つ。動作と結びついたデータが集めにくいこと。そして、機体や環境とのすり合わせです。\n\nこの動画では、\n\n・出荷が4倍になったロボットは、何をしているのか\n・机の上のロボットアームで、何が簡単で、何が難しかったのか\n・データの壁はどれくらい高くて、誰がどう越えようとしているのか\n\nの3つを見たうえで、ロボットのニュースを見るときの2つの問いを解説します。\n\n▼目次\n0:00 人型ロボットの出荷は4倍。ロボットにもChatGPTの瞬間?\n0:33 この番組について\n0:55 今日の問いと、先に結論\n2:18 1. 出荷の中身:4倍になったロボットは何をしているのか\n4:36 2. 机の上の実感:何が簡単で、何が難しかったのか\n7:14 3. データの壁:どれくらい高く、誰がどう越えようとしているのか\n9:41 エイイチの見立て:ニュースを見るときの2つの問い\n11:24 冒頭の問いへの答え\n12:05 エンディング\n\n▼出典\n・人型ロボットの出荷:Counterpoint Research(2026年8月20日)の推計\nhttps://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026\n・同じく出荷の推計:Smart Analytics Global(2026年8月)\n・1X NEO:1X 公式の製品・注文ページ(2026年10月12日に確認。一般のお客さんへの配送開始を示す公式発表は、この日の時点で見つかっていない)\nhttps://www.1x.tech/neo\n・10万年のデータの壁:Ken Goldberg「GOFE can close the 100,000-year robot data gap」Science Robotics(2025年8月, Editorial)\nhttps://doi.org/10.1126/scirobotics.aea7390\n・Generalist の会社の公式発表:GEN-0(2025年11月)、GEN-1(2026年4月)、GEN-1.5(2026年8月)\nhttps://generalistai.com/blog/gen-0\nhttps://generalistai.com/blog/gen-1\nhttps://generalistai.com/blog/gen-1.5\n・ほかの機体から学ぶ研究の例:Gemini Robotics 1.5 技術報告(Google DeepMind)\nhttps://arxiv.org/abs/2510.03342\n・実機の数字:Pixels to Motors の試行の台帳(pick_place_ledger_v3.csv)、公開記事 DAY257\nhttps://adaptdesign.jp/pixelstomotors/record/why-z-correction-failed-twin-poses\n\n▼この番組について\n「フィジカルAI解説|Pixels to Motors」は、フィジカルAIを、現場で使える形にするまでを解説する番組です。\nエイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。その中で分かったことと、そこから考えた見立てを、案内役のハコブくんがお伝えします。\n\n・ナレーションは AI 音声(ElevenLabs)です。話しているのは Pixels to Motors のキャラクター、ハコブくんです\n・BGM は ElevenLabs Music で作った曲、効果音はコードで作った音です\n\n▼ロボットの開発・導入のご相談\nメール:pixelstomotors@adaptdesign.jp\n\n▼リンク\nWebサイト:https://adaptdesign.jp/pixelstomotors\nGitHub:https://github.com/PixelstoMotors\nX:https://x.com/adaptdesignjp\nLinkedIn:https://www.linkedin.com/in/eiichi-shimazu\n\nチャンネル登録・高評価もよろしくお願いします。コメントは、エイイチが全部読んでいます。\n\n#フィジカルAI #人型ロボット #ロボット\n</code></pre><h2 id=\"index_zetfem2o\">BGM と効果音の候補を調べた表(音源_候補.md・全量)</h2><p>規約は 2026年10月12日に公式のページで確かめた内容です。規約は変わるので、使う前にもう一度確かめます。</p><pre><code># BGM と効果音:どこから持ってくるか\n\n2026-10-12 作成。video_rules.md 8節「BGM・効果音」の判断材料。\n\n## 条件\n\n- YouTube で収益化しても使える\n- クレジット表記の条件を守れる(できれば表記なしで使える)\n- ほかのチャンネルと同じ曲になりにくい(声と同じ考え方。3-1)\n- ナレーションが聞き取れる音量に下げる。話している間は BGM を下げ、間で少し戻す(audio_mix.py が自動でやる)\n\n## 候補(規約は 2026-10-12 に公式のページで確かめた。変わるので、使う前にもう一度見る)\n\n| 用途 | 候補 | 収益化 | クレジット | 気をつけること |\n|---|---|---|---|---|\n| BGM(第一候補) | ElevenLabs Music(いまの Starter プランで使える) | 使える(映画・テレビ・ラジオ・ゲームの一部を除く) | Starter 以上はいらない(無料プランは必要) | 作れるのは月17分まで。曲そのものを音楽配信に出すのは禁止。新しく作る曲なので、ほかのチャンネルと同じ曲にならない |\n| BGM(安全側の控え) | YouTube オーディオライブラリ(YouTube Studio の中) | 使える。ここの曲には Content ID の申し立てが来ないと YouTube が書いている | 「帰属表示が不要」で絞り込めば、いらない | YouTube 以外(X・LinkedIn に切り抜きを載せるなど)での扱いは、ヘルプに書かれていない。多くのチャンネルが使うので、同じ曲になりやすい |\n| BGM(生の演奏・有料) | Audiostock(日本のサービス。人が演奏・録音した曲が多い) | YouTube で使える(動画配信者プランは本人のアカウントだけ) | 公式のライセンスのページに記載なし(使う前に確かめる) | 単品 1,210円〜(買い切り)。定額の動画配信者プランは月858円(年払い)。値段は公式ページの 2024年12月1日時点の表示。多くの曲が Content ID に登録されているが、ふつうは警告が出ない設定とのこと。定額で使った曲は、解約後に新しい動画では使えない(単品なら使える) |\n| BGM(控え) | OpenTracks(旧 DOVA-SYNDROME。2026年9月15日に名前が変わった) | 背景音楽としてなら商用も可 | いらない | 海外の団体が「音楽出版権」を不正に主張して、Content ID の申し立てが来る例があるとサイト自身が書いている(広告が付かない・収益の一部を取られることがある)。そのたびに異議を出す手間がかかる |\n| 効果音(第一候補) | コードで作る(sound_synth.py。もう作った) | 問題なし(自分で作った音) | いらない | 単純な電子音だけ。だれでも同じ音を作り直せるので、「再現できる」の考え方に合う |\n| 効果音(控え) | 効果音ラボ | 使える(「YouTube(収益化しても可)」と明記) | いらない | 効果音を Content ID に登録するのは禁止(他の人の利用に影響するため)。効果音だけの動画は作らない |\n| 効果音(控え) | ElevenLabs の効果音(sound_eleven.py sfx) | 有料プランで作った音は商用可 | いらない | 現実の音(モーター音・机に物を置く音など)が欲しいとき向き |\n\n外したもの: Pixabay の音楽(無料・クレジット不要だが、Content ID の扱いが公式のライセンス要約に書かれていない)。魔王魂・甘茶の音楽工房などのほかの無料サイトは、今回は調べていない。上の候補で足りなければ調べる。\n\n2026-10-12 エイイチ: BGM は生の楽器の音に近い方がよい。コードで作った BGM(bgm_1〜3・bgm_仮_温かみ)は方向を確かめる用にとどめる。\n\n## コードで作った効果音(_制作ベース/sound/。2026-10-12)\n\n| 名前 | 音 | 使う所 |\n|---|---|---|\n| se_pop | ぽん | 図・数字が出る。いちばんよく使う |\n| se_tick | こつ | 小さな切り替え。箇条書きが1つ出る |\n| se_whoosh | しゅっ | 章や場面が大きく変わる |\n| se_ding | ちん | 結論・大事な数字 |\n| se_rise | ひゅい | 驚き・問いかけ |\n| bgm_仮 | やわらかい和音と小さな粒の音(テンポ96・32秒でループ) | 本番の BGM ができるまでの仮。混ぜ方を確かめるためのもの |\n\n効果音は、ナレーションの声(おおむね 100〜300Hz)とぶつからないよう、500Hz より上を中心にしている。入れすぎると疲れるので、1分に2〜3個を目安にする。\n\n## ElevenLabs Music で作る BGM の説明文(英語と日本語訳)\n\nMac で、動画の作業フォルダから(キーの入れ方は tts_eleven.py と同じ):\n\n```\npython3 ../_制作ベース/sound_eleven.py music --name bgm_hook --sec 60 --count 2 --prompt \"(下の英文)\"\n```\n\n### A. 本編の後ろで流す曲(いちばん長く流れる)\n\n> Calm, curious instrumental background music for a technology explainer video about robots and AI. Soft electric piano and light plucked synth arpeggios, warm pads, gentle low bass, very soft minimal drums. Around 95 BPM, steady and unobtrusive. No strong melody that would compete with a narrator's voice. No vocals. Clean modern mix.\n\n日本語訳:ロボットとAIの解説動画の後ろで流す、落ち着いて、少し先が気になる感じの曲。やわらかいエレクトリックピアノ、軽くはじく電子音のアルペジオ、温かい和音、低くやさしいベース、ごく小さなドラム。テンポ95前後で一定、目立たない。ナレーションとぶつかる強いメロディは入れない。歌なし。\n\n### B. フック(動画の最初の数十秒)\n\n> Short instrumental intro bed for a tech news explainer, slightly brighter and more forward than background music. Pulsing soft synth, light percussion, a sense of something starting. Around 100 BPM. No vocals, no big drops, no dramatic trailer hits.\n\n日本語訳:技術ニュースの解説の冒頭で流す、短めの曲。本編の曲より少し明るく前に出る。脈打つやわらかい電子音、軽い打楽器、何かが始まる感じ。テンポ100前後。歌なし。盛り上げの落差や、映画の予告編のような強い音は入れない。\n\n### C. ジングル(「Pixels to Motors。案内役のハコブくんです。」の後ろ)\n\n> A 4-second friendly logo sting for a robotics channel. Soft synth bell chord with a gentle rising shimmer, ends cleanly on a warm chord. No vocals.\n\n日本語訳:ロボットのチャンネルの、4秒の親しみのあるロゴの音。やわらかい電子のベルの和音に、ゆっくり上がるきらめき。温かい和音できれいに終わる。歌なし。\n\n(C は --sec 4。A・B は 60 秒で作り、短ければ audio_mix.py がループでつなぐ)\n\n## 決める基準\n\n1. ナレーションの後ろで、言葉が聞き取りにくくならないか(音の確認.m4a をイヤホンとスピーカーの両方で聞く)\n2. 13分流して、うるさく感じないか\n3. ハコブくんの見た目と番組の落ち着いた調子に合うか</code></pre><p></p><h2 id=\"index_QVHbh5dK\">参照した情報</h2><ul><li><p><a href=\"https://network.mobile.rakuten.co.jp/sumakatsu/contents/articles/2026/00533/\" data-has-link=\"true\">YouTube のチャンネル名の変更方法(楽天モバイル スマ活)</a></p></li><li><p><a href=\"https://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026\" data-has-link=\"true\">Counterpoint Research:2026年上半期の人型ロボット出荷の推計(2026年8月20日)</a></p></li><li><p><a href=\"https://www.youtube.com/@PixelstoMotors\" data-has-link=\"true\">Pixels to Motors の YouTube チャンネル</a></p></li><li><p><a href=\"/\" data-has-link=\"true\">Pixels to Motors</a></p></li></ul>", "toc": [{"id": "index_5Npkigty", "text": "title: \"解説動画の1本目を公開まで通す ― 声・BGM・間・直し・サムネイルを部品ごとに決め、23手順にまとめた\" slug: first-video-end-to-end date: 2026-10-12 day: 333 meta_description: \"解説動画「フィジカルAIの現在地」をハコブくんの声で作り直し、YouTubeに公開した。声・BGM・効果音・間・不具合の直し・概要欄・サムネイルを部品ごとに決め、次の動画から使う23手順にまとめた。\" tags: [YouTube, 解説動画, ハコブくん, ElevenLabs, BGM, 効果音, ffmpeg, Python, Playwright, サムネイル, 動画制作]"}, {"id": "index_R8Gtcv3i", "text": "公開した動画は12分29秒・146文・効果音45個"}, {"id": "index_iJ5F55aG", "text": "ハコブくんの声は、ElevenLabs のボイスデザインで作った「候補6」に決めた"}, {"id": "index_qsSC6Gf9", "text": "「エイイチ」の抑揚は、ひらがなの「えいいち」で渡すとそろった"}, {"id": "index_NvWums4o", "text": "BGM は、コードで作った音から ElevenLabs Music の生楽器の曲に替えた"}, {"id": "index_845PdZbU", "text": "BGM の大きさは、話している間 -23dB・0.8秒以上の間 -16dB にした"}, {"id": "index_SkptrlzU", "text": "場面の切り替えは「ホワーン」、大きな流れの変わり目は「キラリン」と「ポロロン」"}, {"id": "index_ttUdYCtI", "text": "自己紹介の2文目を「研究と開発」と「見立て」に直した"}, {"id": "index_jcwDZAwf", "text": "右下のモヤモヤは、透かしを消した跡だった"}, {"id": "index_mZmow8P1", "text": "30cm の図の説明を、図の下の2行に移した"}, {"id": "index_RXyGoUij", "text": "YouTube の概要欄は、目次の時間を書き出しから自動で入れた"}, {"id": "index_WGBasXjY", "text": "タイトルを決め、「子ども向け」は「いいえ」にした"}, {"id": "index_ua9kJv2c", "text": "サムネイルは5回作り直した"}, {"id": "index_kbIhNmO_", "text": "ハコブくんの体は、ChatGPT で無地の背景から作り直した"}, {"id": "index_gTZdlqKn", "text": "サムネイルの背景は、ハコブくんがアームに重ならないコマを選んだ"}, {"id": "index_NqzL8c9Y", "text": "公開した直後に見えた2つのこと:字幕が2重に出る、画質が低い"}, {"id": "index_CTcfDe5k", "text": "チャンネル名は「フィジカルAI解説|Pixels to Motors」、まだ反映されていない"}, {"id": "index_qiv3oQpt", "text": "X と LinkedIn の投稿文は、数字と机の上の実感を先に置いた"}, {"id": "index_YMC72TM8", "text": "Claude の返事が英語になり、2回聞き返された"}, {"id": "index_ZKdeas3Q", "text": "Mac へ送ったファイルが、途中で変わることがあった"}, {"id": "index_lMTio9tY", "text": "次の動画から使う「1本を作る流れ」23手順"}, {"id": "index_B9rTkmMC", "text": "まだ決まっていないこと"}, {"id": "index_XNI1szIP", "text": "この記事で作った・直したスクリプトの全文"}, {"id": "index_MBHw9jHD", "text": "書き出しのログと測った数値"}, {"id": "index_azDryZvY", "text": "書き出した概要欄(概要欄.txt・全量)"}, {"id": "index_zetfem2o", "text": "BGM と効果音の候補を調べた表(音源_候補.md・全量)"}, {"id": "index_QVHbh5dK", "text": "参照した情報"}]}
\ No newline at end of file
+{"day": "LOG333", "series": "ひとり研究所の裏側", "published": "2026-10-12T07:58:00.000Z", "title": "解説動画の1本目を公開まで通す ― 声・BGM・間・直し・サムネイルを部品ごとに決め、23手順にまとめた", "slug": "first-video-end-to-end", "cover": null, "thumb": "/thumbs/r-first-video-end-to-end.html", "lang": "JP", "description": "解説動画「フィジカルAIの現在地」をハコブくんの声で作り直し、YouTubeに公開した。声・BGM・効果音・間・不具合の直し・概要欄・サムネイルを部品ごとに決め、次の動画から使う23手順にまとめた。", "updated": "2026-10-12T23:58:54.000Z", "body": "<!--TOC--><p></p><hr><h2 id=\"index_5Npkigty\">title: \"解説動画の1本目を公開まで通す ― 声・BGM・間・直し・サムネイルを部品ごとに決め、23手順にまとめた\" slug: first-video-end-to-end date: 2026-10-12 day: 333 meta_description: \"解説動画「フィジカルAIの現在地」をハコブくんの声で作り直し、YouTubeに公開した。声・BGM・効果音・間・不具合の直し・概要欄・サムネイルを部品ごとに決め、次の動画から使う23手順にまとめた。\" tags: [YouTube, 解説動画, ハコブくん, ElevenLabs, BGM, 効果音, ffmpeg, Python, Playwright, サムネイル, 動画制作]</h2><p>試作で作った解説動画「フィジカルAIの現在地」を、ナレーション役のハコブくんの声で作り直し、YouTube に公開しました。公開したタイトルは「人型ロボットの出荷は4倍。ロボットにも「ChatGPTの瞬間」は来たのか|フィジカルAIの現在地」、長さは12分29秒です。</p><p>台本・図・字幕・ハコブくんの動きは、これまでの記事で作ってきました(構成と台本の検証は LOG330、ハコブくんの口と目の動きは LOG331、チャンネルの見た目は LOG332)。この記事は、そこから公開までに決めたことの記録です。声、BGM と効果音、文と文の間、見つかった不具合の直し、YouTube の概要欄、サムネイル、公開した直後に見えたこと、の順に書いています。最後に、次の動画から同じ順番で作れるようにまとめた23の手順を載せました。</p><p>作業は、Claude のクラウドの作業環境でスクリプトを動かし、ElevenLabs で声と音楽を作る所だけを手元の Mac で動かす形で進めました。ElevenLabs の API キーは Mac の中だけで使い、Claude には渡していません。API キーが写っている画面の写真は、この記事に載せていません。</p><p>作ったスクリプトは、記事の後半にすべて全文で載せています。書き出しのログと、測った数値も全量で載せています。</p><p></p><figure><img src=\"/media/s-1280x720_v-fms_webp_da0a9183-719d-40ae-8829-cf3de0ab3f27.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_R8Gtcv3i\">公開した動画は12分29秒・146文・効果音45個</h2><p>公開した動画の中身は、次のとおりです。数値は、YouTube に上げたファイルを ffprobe(動画の中身を調べるツール)と ffmpeg の音量測定で測ったものです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>値</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>長さ</p></td><td colspan=\"1\" rowspan=\"1\"><p>12分29秒(749.1秒・22,473コマ)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>文の数</p></td><td colspan=\"1\" rowspan=\"1\"><p>146文(字幕の字数で約4,300字)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ナレーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくん(ElevenLabs のボイスデザインで作った声・モデル Eleven v4)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM</p></td><td colspan=\"1\" rowspan=\"1\"><p>ElevenLabs Music で作った1曲(bgm_明るいB_2・60秒)を最初から最後までループ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>45個。場面の切り替えの「ホワーン」37個、大きな流れの変わり目の「ポロロン」7個、自己紹介の「キラリン」1個</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>音の大きさ</p></td><td colspan=\"1\" rowspan=\"1\"><p>-16.0 LUFS(YouTube で聞く大きさの目安にそろえた)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>映像</p></td><td colspan=\"1\" rowspan=\"1\"><p>H.264、1920×1080、30コマ/秒</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>上げたファイル</p></td><td colspan=\"1\" rowspan=\"1\"><p>65,655,407 バイト(約66MB)</p></td></tr></tbody></table></div><p>動画は、冒頭で「今年の上半期に出荷された人型ロボットは約4倍」という推計を出し、「ロボットにも ChatGPT のような瞬間はもう来たのか」を問いにしています。答えは「写真を見せて判断させる入口は簡単になった。物を確実に動かすところは、まだ手間がかかる」です。そのあと3つの章(出荷の中身、机の上のロボットアームで分かったこと、データの壁)を見て、ロボットのニュースを見るときの2つの問いで終わります。</p><p>チャンネルは <a href=\"https://www.youtube.com/@PixelstoMotors\" data-has-link=\"true\">Pixels to Motors の YouTube</a> です。</p><h2 id=\"index_iJ5F55aG\">ハコブくんの声は、ElevenLabs のボイスデザインで作った「候補6」に決めた</h2><p>ナレーションはハコブくんが話すと決めています(LOG331)。声は、ほかのチャンネルと同じ声を使わず、ハコブくん専用に作ります。ElevenLabs のボイスデザイン(声の特徴を文章で書くと、新しい声の候補を作る機能)を有料プランで1か月試すことにしました。</p><p>声の候補をいくつか作って聞き比べ、「候補6(明るい30代前半のお手伝いロボット)」の2つ目のパターンに決めました。落ち着いた30代の声(候補1)は、読みは正確でしたが、ハコブくんの見た目に合いませんでした。</p><p>読み上げのモデルは、冒頭の14文を Multilingual v2 と Eleven v4 で読ませて聞き比べ、Eleven v4 にしました。話す速さは既定のままです。1秒に約6.8字で、仮の音声(Open JTalk)の約5.2字より速く、動画は仮の音声の版より短くなりました。</p><p>声づくりは、手元の Mac で <code>tts_eleven.py</code> を動かして1文ずつ mp3 にし、それを Claude の作業環境で <code>tts_import.py</code> が書き出し用の wav に変える流れです。文の前後の無音は 0.05 秒を残して切り、文と文の間は台本に書いた「後ろの間」で決めます。</p><h2 id=\"index_qsSC6Gf9\">「エイイチ」の抑揚は、ひらがなの「えいいち」で渡すとそろった</h2><p>通しで聞くと、「エイイチ」という名前の抑揚が文ごとに違っていました。</p><blockquote><p>エイイチのイントネーションが揃っていないので、全て同じイントネーションに修正してください(語尾が下がる方が通常のイントネーションかな)</p></blockquote><p>ElevenLabs に渡す文の書き方で、読み方が変わります。そこで <code>tts_eleven.py</code> に、台本ではなく試しの文だけを音声にする <code>--try</code> を足し、同じ文を4通りの書き方で作りました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>番号</p></th><th colspan=\"1\" rowspan=\"1\"><p>ElevenLabs に渡した書き方</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>エイイチ(カタカナ)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>栄一(漢字)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>えいいち(ひらがな)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>エーイチ(伸ばす音)</p></td></tr></tbody></table></div><blockquote><p>3回が正しい発音でした。</p></blockquote><p>ひらがなの「えいいち」が、語尾の下がる読みでそろいました。台本 <code>narration.py</code> に <code>ELEVEN_READ</code>(ElevenLabs に渡すときだけ文字を置きかえる表)を作り、「エイイチ」を「えいいち」に置きかえています。字幕はカタカナのままです。</p><p>もう1か所、読み間違いが見つかりました。</p><blockquote><p>3分16秒あたりのセリフが、裏表裏になっています(テキストは合っている)</p></blockquote><p>37番の文で、「裏表」が「うらおもてうら」のように読まれていました。字幕の文字は合っていて、音声だけが違います。<code>ELEVEN_READ</code> に「裏表 → うらおもて」を足し、その文だけ作り直しました。仮の音声用の読み替え表 <code>READ</code> とは別に持っているのは、同じ語でも、エンジンによって間違え方が違うためです。</p><h2 id=\"index_NvWums4o\">BGM は、コードで作った音から ElevenLabs Music の生楽器の曲に替えた</h2><p>BGM は、最初にコードで仮の曲を作りました(<code>sound_synth.py</code>。やわらかい和音と小さな粒の音を数式で鳴らしたもの)。混ぜ方を確かめるための仮の曲です。</p><blockquote><p>音源はシンセサイザーっぽいのではなくて、ほんのもう少しだけ温かみのある音源が良さそう(テーマが真面目なので、少し音源には彩りを加えたい的な)</p></blockquote><p>コードでピアノ風・ギターとマリンバ風・ピッツィカート風の3曲を作り直しましたが、方向は変わりませんでした。</p><blockquote><p>音源はもっと生音っぽい方がいいかな。音源使えるのある?</p></blockquote><p>そこで、使える音源を調べて表にしました(<code>音源_候補.md</code>。全文は後半)。条件は、YouTube で収益化しても使えること、クレジット表記の条件を守れること、ほかのチャンネルと同じ曲になりにくいことです。第一候補を ElevenLabs Music(文章で曲の雰囲気を書くと、新しい曲を作る機能)にしました。声と同じプランで使え、新しく作る曲なので、ほかのチャンネルと同じ曲にならないためです。</p><p>ElevenLabs Music を API から使うと、最初は次のエラーで止まりました。</p><pre><code>HTTP 401 missing_permissions music_generation\n</code></pre><p>使っていた API キーに、音楽を作る許可がありませんでした。新しいキーを作り直し、「読み上げ・効果音・音楽生成」の3つだけを許可しました。前のキーは、以前に画面の写真に写ったことがあったため、削除を勧めました(削除したかは、この記事の時点で確かめていません)。キーは、Mac のターミナルで画面に出ない形で入れています。</p><pre><code>read -s ELEVENLABS_API_KEY && export ELEVENLABS_API_KEY\n</code></pre><p>曲は <code>sound_eleven.py</code> で60秒ずつ作りました。最初の4曲(冒頭用2曲・本編用2曲)は、聞くと暗く感じました。</p><blockquote><p>もう少し明るい曲調がいいかな。暗く聞こえる</p></blockquote><p>長調・明るめを指定して、2種類の書き方で2曲ずつ作り直しました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>曲</p></th><th colspan=\"1\" rowspan=\"1\"><p>指定した内容(要点)</p></th><th colspan=\"1\" rowspan=\"1\"><p>結果</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>bgm_冒頭_1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>生の楽器、ギター・ピアノ・ピッツィカート・マリンバ、100 BPM、好奇心と希望</p></td><td colspan=\"1\" rowspan=\"1\"><p>暗く聞こえた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>bgm_本編_1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>生の楽器、フェルトピアノ・ギター・チェロとバイオリン、90 BPM、落ち着いて真面目</p></td><td colspan=\"1\" rowspan=\"1\"><p>暗く聞こえた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>bgm_明るいA_1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>長調、ギター・明るいピアノ・鉄琴・手拍子、105 BPM、晴れた朝のように</p></td><td colspan=\"1\" rowspan=\"1\"><p>A_1 を冒頭で試した</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>bgm_明るいB_1・2</p></td><td colspan=\"1\" rowspan=\"1\"><p>長調、ピアノとギターが主、弦・ピッツィカート・鉄琴、100 BPM、前向きで落ち着いた明るさ</p></td><td colspan=\"1\" rowspan=\"1\"><p><strong>B_2 に決めた</strong></p></td></tr></tbody></table></div><blockquote><p>bgm_明るいB_2.mp3 bgm_明るいA_1.mp3 で動画作成してみて</p></blockquote><p>冒頭15秒を2曲それぞれで書き出して聞き比べ、B_2 に決まりました。指定した文章の全文は、後半の「作った曲の記録」にあります。</p><p>曲は60秒なので、12分29秒の動画ではループします。ElevenLabs の曲は最後の数秒で小さくなって終わるため、そのままつなぐと1分ごとに音が消えます。<code>audio_mix.py</code> で、曲のふつうの大きさより10dB以上小さくなる終わりの部分を切り、つなぎ目を2秒重ねてループさせました。</p><h2 id=\"index_845PdZbU\">BGM の大きさは、話している間 -23dB・0.8秒以上の間 -16dB にした</h2><p>BGM は、ハコブくんが話している間だけ自動で小さくし、話していない間で少し戻します。値はナレーションの大きさとの差で、-23dB はナレーションのおよそ14分の1の音圧です。</p><p></p><figure><img src=\"/media/s-1800x780_v-frms_webp_a3a3705d-d886-470c-8be2-f4a70eed6f10.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>この値に決まるまでに、4回動かしました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>段階</p></th><th colspan=\"1\" rowspan=\"1\"><p>話している間</p></th><th colspan=\"1\" rowspan=\"1\"><p>0.8秒以上の間</p></th><th colspan=\"1\" rowspan=\"1\"><p>きっかけ</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>-22dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>-15dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>最初の値</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>-15dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>-9dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>「音源が聞こえないね。もう少しだけ音量上げるか、音源だけ聴きたい」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>-22dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>-15dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>明るい曲に替えたら大きく聞こえた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>-23dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>-16dB</p></td><td colspan=\"1\" rowspan=\"1\"><p>「音源の音量90%くらいにして」</p></td></tr></tbody></table></div><blockquote><p>曲調を変えたら音量が大きく聞こえてしまうようになったので、前の音量に戻してください。冒頭15秒_bgm_明るいB_2が良さそうです。</p></blockquote><blockquote><p>いいですね。音源の音量90%くらいにして、この動画を仕上げてみてください。</p></blockquote><p>同じ dB の値でも、曲が変わると聞こえ方が変わりました。明るい曲は高い音(鉄琴や明るいピアノ)が多く、低い音の多い曲よりナレーションの上に出て聞こえたためと考えています。90%は、音圧で1dB下げることにあたります(20×log10(0.9) ≒ -0.9dB)。</p><p>話している所の決め方は、次のとおりです。</p><ul><li><p>各文の読み上げの範囲を、前に0.15秒、後ろに0.3秒広げる</p></li><li><p>広げたあとの間が0.5秒未満なら、話している扱いのまま(もとの間で約0.8秒未満。BGM が文ごとに上下しないため)</p></li><li><p>0.3秒かけてなめらかに上げ下げする</p></li></ul><p>グラフのとおり、BGM が戻るのは、場面の切り替えの直前(1.2秒の間)、章の見出しのあと(1.7秒)、大きな流れの変わり目(1.8秒の間)だけです。ふつうの文の間(0.35秒)では、BGM は下がったままです。</p><p>後ろの間の長さを選ぶと、BGM の大きさと鳴る効果音がどう変わるかを、次の図で動かして確かめられます。</p><div class=\"embed\"><iframe src=\"/embeds/first-video-end-to-end-VBQ8Fbsp.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p></p><h2 id=\"index_SkptrlzU\">場面の切り替えは「ホワーン」、大きな流れの変わり目は「キラリン」と「ポロロン」</h2><p>効果音は、最初にコードで5種類作りました(ぽん・こつ・しゅっ・ちん・ひゅい)。冒頭15秒で試したあと、使い方を絞りました。</p><blockquote><p>最後のホワーンって効果音を、場面切り替えに入れてみたい。</p></blockquote><blockquote><p>ホワーンって効果音だけを、場面切り替えに使って</p></blockquote><blockquote><p>効果音はもう少し音量を大きく。</p></blockquote><p>効果音は「ホワーン」(se_rise)だけにし、ナレーションより12dB小さい値から6dB小さい値に上げました。場面(台本の1文ごとに書いた場面の名前)が変わる文の頭から0.15秒前に、自動で鳴らします。</p><p>次に、話の大きな区切りがどこか分かるようにしました。</p><blockquote><p>それと動画の流れの中で少し間があるポイントがあるといいので、大きな流れの変化の時にはその間を設定することはできる?</p></blockquote><blockquote><p>もしくはそのときは効果音を違うものにするか。</p></blockquote><p>大きな流れの変わり目(フック→自己紹介、自己紹介→「今日の問い」、各章の始まり、答え、エンディング)では、直前の文の後ろの間を1.8秒にし、ホワーンの代わりに別の音を鳴らすことにしました。別の音は、コードで6つ作りました(マリンバ・ハープ・ピアノ・鉄琴・ふくらむ和音・風)。</p><blockquote><p>5と2で動画に入れ込んでみて(自己紹介のあたりで入れると良さそう。音源だけでも可です)</p></blockquote><blockquote><p>自己紹介は5のキラリンがいいね。他の切り替えポイントで2のポロロン使ったりと使い分けが良さそうだね(とりあえず自己紹介は5で確定)</p></blockquote><p>自己紹介に入る所は「キラリン」(鉄琴の高い3音)、そのほかの変わり目は「ポロロン」(マリンバの上がる3音)にしました。ポロロンを場所ごとにどう使い分けるかは、まだ試している段階です。</p><p>そのあと、ふつうの場面の切り替えにも、もう少し間が欲しいという指摘がありました。</p><blockquote><p>ホワーンという中の切り替えの時にもう一拍だけ間がある方がいいな。</p></blockquote><p>ホワーンが鳴る所の直前の文の後ろの間を、1.2秒にしました。公開した台本の146文は、後ろの間で分けると次のとおりです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>どこ</p></th><th colspan=\"1\" rowspan=\"1\"><p>後ろの間</p></th><th colspan=\"1\" rowspan=\"1\"><p>文の数</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ふつうの文</p></td><td colspan=\"1\" rowspan=\"1\"><p>0.35秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>96</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>話の小さな区切り</p></td><td colspan=\"1\" rowspan=\"1\"><p>0.4〜0.6秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>4</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ふつうの場面の切り替え(ホワーンの前)</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.2秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>32</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>章の見出しを読む文(「1つ目、…」など)と、最後のあいさつ</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.7秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>5</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>大きな流れの変わり目(キラリン・ポロロンの前)</p></td><td colspan=\"1\" rowspan=\"1\"><p>1.8秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>8</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>字幕のない最後の文(エンディングの余白)</p></td><td colspan=\"1\" rowspan=\"1\"><p>6.0秒</p></td><td colspan=\"1\" rowspan=\"1\"><p>1</p></td></tr></tbody></table></div><p>章の見出しは、読んだすぐあとに場面が切り替わるので、見出しの1.2秒に、切り替えの一拍を足して1.7秒にしています。</p><p>どの文で何が鳴ったかは、書き出しのログに45行で出ています(後半に全量)。</p><h2 id=\"index_ttUdYCtI\">自己紹介の2文目を「研究と開発」と「見立て」に直した</h2><p>確認用の動画を見て、自己紹介の文と、字幕の後ろの帯に指摘がありました。</p><blockquote><p>ここの自己紹介おかしくないか? それとナレーションのテキストの下に薄い四角枠が敷かれているのが必要ないかな。</p></blockquote><blockquote><p>ロボットアームを動かした記録を紹介しているわけではないよ(もちろんそういう時もあるだろうけど)</p></blockquote><p>前の文は「題材は、本業のデザイナーをしながら、自宅の机の上でロボットアームを動かした記録です。」でした。これだと、毎回ロボットアームの記録を見せる番組だと受け取られます。この番組は、フィジカルAIを解説する番組です。Claude がいくつか言い方を出し、次の文に決めました。</p><blockquote><p>エイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。その中で分かったことと、そこから考えた見立てを、ハコブくんがお伝えします。こっちがいいかな。その下はすこし冗長だね。</p></blockquote><p>「結論」ではなく「見立て」にしたのは、今の時点での判断で、新しい事実が出たら変わるからです。</p><p>名前のテロップも分けました。ハコブくんが自分の紹介をしている間は「フィジカルAI解説/案内役:ハコブくん(AI音声)」、本人を紹介する文から「エイイチ」を出します。台本では、この文を「title」から「title_eiichi」という別の場面に分けて、テロップを替えています。映像は同じなので、この切り替えではホワーンを鳴らしません。</p><p>字幕の後ろの薄い黒い帯は、実機の映像の上で字幕を読みやすくするために敷いていたものです。字幕の文字には黒い縁があり、帯がなくても読めるため、外しました。</p><p></p><figure><img src=\"/media/s-1979x632_v-frms_webp_552cefd2-62a2-43d2-bba1-e1e5e23b08c9.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_jcwDZAwf\">右下のモヤモヤは、透かしを消した跡だった</h2><blockquote><p>55秒くらいに右端下がモヤモヤしているのを見つけました。同じようなモヤモヤが他にもないか調べて修正してください。</p></blockquote><p>自己紹介の場面の映像は、以前に動画編集サイト(Clideo)で書き出した版(DAY123.mp4)を使っていました。この版には右下にサイトの透かし(ロゴ)が入っていて、ffmpeg の delogo(指定した範囲を周りの色でぼかして埋める機能)で消していました。その跡が、マットの格子の上で縦の筋のように見えていました。</p><p>同じように透かし入りの版を使っていた映像は、もう1本(DAY124.mp4。章2の実機の場面)ありました。2本とも、透かしの入っていない元の撮影ファイル(DAY123.MOV・DAY124.mov)に替え、<code>scenes.py</code> の映像の指定で、透かしを消す処理を使わない設定(3つ目の値を False)にしました。</p><p><code>assemble.py</code> は、1文ずつ背景の映像を作り、中身が変わった文だけを作り直す仕組みです。どの文を作り直すかは、背景の中身から作った指紋(ハッシュ)で判断しています。この指紋に、使う映像のファイル名と開始秒が入っていませんでした。そのままでは、映像を替えても作り直されないため、指紋に映像の指定を足しました。</p><p></p><figure><img src=\"/media/s-1874x512_v-frms_webp_bca07aad-c04e-4174-a202-e8fd93911724.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_mZmow8P1\">30cm の図の説明を、図の下の2行に移した</h2><blockquote><p>30cm以上違うはのところは新しく内容を修正したので確認してください。前の動画か台本を確認して修正してください。</p></blockquote><p>章2の「計算ツールとデジタルツインで、爪先の位置が30cm以上食い違った」という図で、説明の行が右へはみ出し、「グリッパー」が「グリッパ」と「ー」の2行に折れていました。説明を図の下に移し、中央にそろえた2行にしました。1行目が「アームが外れた距離ではない。爪先の位置の決め方が違っていた」、2行目が「計算ツールには、グリッパーの形が入っていない」です。</p><p></p><figure><img src=\"/media/s-1897x632_v-frms_webp_e4c6b745-4cda-4da9-8bf1-663eb0d60dcb.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_RXyGoUij\">YouTube の概要欄は、目次の時間を書き出しから自動で入れた</h2><blockquote><p>YouTubeの動画内に入れる動画概要は追加情報などをまとめておいてください!</p></blockquote><p>概要欄は、動画の作業フォルダの <code>概要欄_下書き.md</code> に書き、<code>description.py</code> で貼り付け用の <code>概要欄.txt</code> にします。目次の時間は、下書きに <code>{t:26}</code> のように文番号で書くと、書き出しのときに記録した、その文が始まる時間に置きかわります。声や間を変えて時間がずれても、下書きを直す必要がありません。</p><p><code>description.py</code> は、YouTube の目次(チャプター)の条件を満たしているかも確かめます。最初が 0:00 で始まり、3つ以上あり、それぞれ10秒以上あることです。満たさないとき、5,000字を超えるとき、「【要記入】」が残っているときは止まります。</p><p>並べる順は、冒頭の数字、「今日の問い」、先に答え、この動画で見る3つ、目次、出典、この番組について、連絡先とリンク、チャンネル登録のお願いとハッシュタグ3つです。連絡先とリンクは、次のように決めました。</p><blockquote><p>メールアドレス、xとリンクドインかな。</p></blockquote><p>出典のリンクは、すべて開いて確かめてから載せました。Smart Analytics Global の推計は、公開されているページが見つからなかったため、リンクなしで名前と時期だけを書いています。1X NEO の「一般のお客さんへの配送開始」は、公式発表が見つからなかったことを、確かめた日付と一緒に書きました。</p><p>公開した概要欄の全文は、後半の「書き出した概要欄」にあります。</p><h2 id=\"index_WGBasXjY\">タイトルを決め、「子ども向け」は「いいえ」にした</h2><blockquote><p>修正したいところはまだまだありつつも、とりあえず合格点です!YouTubeにアップロードします!</p></blockquote><p>タイトルは、Claude が3案を出し、「人型ロボットの出荷は4倍。ロボットにも「ChatGPTの瞬間」は来たのか|フィジカルAIの現在地」に決めました。冒頭の数字、動画の問い、シリーズ名の順です。</p><p>YouTube に上げるときは、動画が子ども向けかどうかを必ず選びます。ハコブくんはキャラクターですが、動画は大人の視聴者に向けた解説なので「いいえ、子ども向けではありません」にしました。子ども向けにすると、コメントや、ほかの動画の紹介などの機能が使えなくなります。</p><p></p><figure><img src=\"/media/s-1270x654_v-fms_webp_24ce4e2f-fed4-448a-a044-254e3578f394.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_ua9kJv2c\">サムネイルは5回作り直した</h2><blockquote><p>それからサムネイルが必要ですね。ChatGptに作らせるので指示を考えてください。</p></blockquote><p>最初は、ChatGPT に画像を作らせるための指示文を Claude が書き、ChatGPT で作りました。</p><blockquote><p>chatGPTのサムネイルフォントが微妙だし、背景写真が違うしだね。</p></blockquote><blockquote><p>今の動画のフォント周りのデザインをベースにサムネイルの背景を入れる感じでまずは惹きつけられるサムネイルを目指したいです。</p></blockquote><p>画像を作る AI は、日本語の文字の形が崩れやすく、背景のロボットアームも実物と違う形になりました。そこで、文字は画像の AI に任せず、<code>thumbnail.py</code> がコードで組む形にしました。文字と色は動画と同じもの(Noto Sans CJK JP の太字、オレンジ #C4471F、紙色 #F5F5F0、黒 #0D0D0D)を使い、HTML で組んで Playwright(ブラウザを自動で動かす道具)で画像にします。背景は、実機の映像の1コマです。文字をコードで組めば、誤字や崩れた文字は出ません。</p><p></p><figure><img src=\"/media/s-1864x1225_v-frms_webp_4be2ec7d-a345-4e3d-b8e9-88f8d4885fa8.png\" alt=\"\"><figcaption></figcaption></figure><p></p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>回</p></th><th colspan=\"1\" rowspan=\"1\"><p>作ったもの</p></th><th colspan=\"1\" rowspan=\"1\"><p>指摘</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>ChatGPT で作った案</p></td><td colspan=\"1\" rowspan=\"1\"><p>文字の形と、背景の写真が違う</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>案A(紙色と写真)・案B(黒と写真)・案C(大きなハコブくん)</p></td><td colspan=\"1\" rowspan=\"1\"><p>「これがまぁまぁ良さそうだけど。ハコブくんが小さいの修正と右向きがいいね。レイアウト別バージョンを生成」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>案Bの別の組み方3つ(B1・B2・B3)。最初は元の体のまま</p></td><td colspan=\"1\" rowspan=\"1\"><p>「ハコブくんの頭凹んでいるよ!」「最初から作り直したら?」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3'</p></td><td colspan=\"1\" rowspan=\"1\"><p>体を作り直して B1〜B3 を出し直した</p></td><td colspan=\"1\" rowspan=\"1\"><p>「ハコブくん大きすぎ。ハコブくんの顔が変(口と目の配置や大きさ確認)背景を全部写真に変更。テキストを大きく目立つように。写真の上にテキストを置くので、黒縁をつけること。」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>案D(全面写真・黒い縁の大きな文字・ハコブくん右下)</p></td><td colspan=\"1\" rowspan=\"1\"><p>「ハコブくんは左下済み(大きさ少しだけ大きく)人型ロボット出荷 約4倍の部分は黒縁無し。」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>案E(写真のコマを替え、ハコブくん左下)</p></td><td colspan=\"1\" rowspan=\"1\"><p>公開に使った</p></td></tr></tbody></table></div><h2 id=\"index_kbIhNmO_\">ハコブくんの体は、ChatGPT で無地の背景から作り直した</h2><p>サムネイルでハコブくんを大きく出すと、頭の上がへこんで見えました。動画で使っている体の画像は、生成動画の1コマから切り抜いたもので、頭の上が背景と見分けにくく、切り抜きが欠けていたためです。動画では小さく出すので目立ちませんでしたが、サムネイルの大きさでは分かりました。</p><blockquote><p>最初から作り直したら?</p></blockquote><p>ChatGPT で、無地の背景・画面は真っ黒・側面の緑の光の輪はそのまま、という条件でハコブくんの体を作り直しました。</p><p></p><figure><img src=\"/media/s-1328x612_v-fms_webp_86d9d594-b517-4b6d-99b5-8485b97925a6.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>作り直した体は、次の手順でサムネイル用の画像にしました。</p><ol><li><p>切り抜き: OpenCV の GrabCut(指定した四角の中で、前景と背景を色の分布から分ける方法)を、体を囲む四角(GrabCut に渡した値は 230, 170, 680, 1040)で8回くり返して前景を出した。いちばん大きなかたまりだけを残し、中の穴を埋めた。最初に試した、背景の色から塗りつぶす方法は、体の白い部分まで背景として消えた</p></li><li><p>画面の四隅: 黒い画面の四隅の位置を測った(左上 356,262・右上 592,260・右下 569,491・左下 314,485)</p></li><li><p>顔: 画面に、白い目と青い口を描いた</p></li><li><p>向き: 透明でない範囲で切り、左右を反転して右向きにした</p></li></ol><p></p><figure><img src=\"/media/s-1277x420_v-fms_webp_f178bd99-0535-4ee4-a1e8-bca01a47d139.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>3の顔は、最初、動画のハコブくんを見ながら、目と口の位置と大きさを画面の割合で決めて描きました。目が縦に長く上に寄り、口が画面の半分の幅で大きく開いていて、動画のハコブくんと違う顔になりました。</p><p>顔を描き直すとき、目と口を自前で描くのをやめました。動画の <code>hakobu.py</code> が持っている目と口の描き方(<code>eye_mask</code>・<code>mouth_mask</code>)でそのまま描き、動画の体の画面を長方形にならしたものから、新しい体の画面の四隅へ写しています。画面の大きさに対する目と口の大きさと位置が、動画のハコブくんと同じになります。<code>make_new_face.py</code>(全文は後半)がこの処理です。</p><p>1の切り抜きと2の四隅の測定は、その場でコマンドとして動かしたため、スクリプトのファイルが残っていません。結果の <code>cut_new.png</code> と <code>screen_quad_new.npy</code>(四隅の値)は <code>_制作ベース/hakobu/</code> に置いてあり、3と4は <code>make_new_face.py</code> と、そのあとに切り抜きと反転をしたコマンドで作っています。</p><h2 id=\"index_gTZdlqKn\">サムネイルの背景は、ハコブくんがアームに重ならないコマを選んだ</h2><p>ハコブくんを左下に置くと、最初に使っていた写真(DAY124.mov の28秒、アームが赤いブロックを掴む瞬間)では、ちょうど左下に爪とブロックがあり、ハコブくんで隠れます。写真を拡大したり、ずらしたりしても、爪は左下から出ませんでした。</p><p>同じ映像の別のコマを2秒ごとに並べて探し、39.8秒(アームがブロックを箱に置く瞬間)を選びました。このコマは左下がマットだけで、爪とブロックと箱が画面の中央から右下に入ります。</p><p></p><figure><img src=\"/media/s-1980x632_v-frms_webp_c0aa477f-7805-473d-acb6-7dfc5d782633.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>公開したサムネイル(案E)の組み方は、次のとおりです。</p><ul><li><p>背景は写真を全面に敷き、右上だけを少し暗くして文字を読みやすくする</p></li><li><p>「ロボットにも/ChatGPTの瞬間/もう来た?」は、1920×1080 のとき150・200・170px の大きさで、24px の黒い縁を付ける</p></li><li><p>上の札「人型ロボット出荷 約4倍」は、オレンジの札に白い文字。黒い縁は付けない</p></li><li><p>ハコブくんは左下の隅に、高さ600px で右(アーム)を向けて置く。「本当に?」の吹き出しは右側</p></li><li><p>1920×1080 で作って 1280×720 に縮め、YouTube の上限の2MB 以下にする(公開版は約1MB)</p></li></ul><h2 id=\"index_NqzL8c9Y\">公開した直後に見えた2つのこと:字幕が2重に出る、画質が低い</h2><blockquote><p>アップしました!</p></blockquote><p>公開したページを開くと、YouTube の字幕(黒い帯に白い文字)が、動画に焼き込んだ字幕の上に重なって、同じ文が2重に出ていました。</p><p></p><figure><img src=\"/media/s-1932x1606_v-frms_webp_e2dcc461-2987-4422-86aa-134d6e44985c.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>動画には字幕が焼き込んであります。字幕のファイル(srt)を YouTube に字幕として登録していると、視聴者が字幕をオンにしたときに2重になります。登録しておくと検索や、音を出せない人への字幕として役に立つ一方、焼き込んだ字幕と重なります。登録を残すかどうかは、これから決めます。</p><blockquote><p>動画の画質こんなもん?アップしてすぐだから?</p></blockquote><p></p><figure><img src=\"/media/s-3024x1912_v-frms_webp_3eb563d7-8909-410f-9e2a-a4a74c38cc55.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>YouTube は、上げた直後は低い画質の版を先に公開し、HD(1080p)の版はあとから作ります。文字のふちがにじんでいたのは、このためと考えています。再生画面の歯車の「画質」に 1080p が出ていれば、HD の版はできています。</p><p>一方、上げたファイルの映像は、測ると約0.49Mbps(488,631 ビット/秒)でした。<code>assemble.py</code> は crf 20(画質を一定に保つ設定。数字が小さいほど高画質)で書き出していて、図と無地の背景が多い動画なので、この値でも書き出したファイルの文字はくっきりしています。ただ YouTube は上げたファイルをもう一度圧縮するので、HD の版でも文字が甘く見えるようなら、2560×1440 に拡大して書き出し直す方法を試す候補にしています。YouTube は 1440p 以上の動画には、画質のよい圧縮方式を使うためです。HD の版での見え方は、まだ確かめていません。</p><h2 id=\"index_CTcfDe5k\">チャンネル名は「フィジカルAI解説|Pixels to Motors」、まだ反映されていない</h2><blockquote><p>チャンネルの名前なんだっけ?(更新されていない)</p></blockquote><p>チャンネル名は「フィジカルAI解説|Pixels to Motors」に決めています(LOG332)。公開した時点では、YouTube ではまだ「Pixels to Motors」と表示されていました。</p><blockquote><p>何度も修正しすぎて反映されなくなってるかも・</p></blockquote><p>YouTube のチャンネル名は、14日間に2回までしか変えられず、変えた名前が表示されるまでに数日かかることもあります(<a href=\"https://network.mobile.rakuten.co.jp/sumakatsu/contents/articles/2026/00533/\" data-has-link=\"true\">楽天モバイルの解説記事</a>)。YouTube Studio の「カスタマイズ」→「基本情報」の名前の欄が新しい名前になっていれば、表示が追いつくのを待ちます。古いままで保存できなければ、2回の上限に当たっています。どちらだったかは、この記事の時点で確かめていません。</p><h2 id=\"index_qiv3oQpt\">X と LinkedIn の投稿文は、数字と机の上の実感を先に置いた</h2><blockquote><p>Xとlinkdinに投稿するので、投稿内容を考えてください。わかりやすくかつ、興味を持ちやすい投稿内容になるように注意してください。</p></blockquote><p>X は140字(URL を含む)に収めるため、冒頭の数字(約4倍)、動画の問い、机の上のロボットアームで650回以上試した実感(写真で判断させるのは簡単、物を確実に動かすのはまだ手間)の3つだけにしました。X の数え方では、日本語1字が2、URL が23で、280以内に収まれば140字以内です。作った文は266でした。</p><p>LinkedIn は英語のあとに日本語を並べ、動画の中の具体的な話(計算ツールとデジタルツインで爪先の位置が30cm以上食い違ったこと、肩の関節に90度ちょっとを超える値を送るとエラーなしで動かないことがあったこと)を入れました。30cm は「アームが30cm外れた」と読まれやすいので、コメント欄への補足で、爪先の位置の決め方が違っていた話だと書き足しています。ハッシュタグは、記事の投稿で使う #R2R2R ではなく、動画の話題で探す人が使う #フィジカルAI・#PhysicalAI にしました。</p><h2 id=\"index_YMC72TM8\">Claude の返事が英語になり、2回聞き返された</h2><p>作業の途中で、Claude の返事が英語になったことが2回ありました。</p><blockquote><p>英語で書いてあってわからないよ?</p></blockquote><blockquote><p>英語だよ?</p></blockquote><p>1回目は、使える音源の候補を調べて返したとき、2回目は、公開したページを見たあとの返事です。どちらも日本語で書き直しました。このプロジェクトの返事は日本語で書くと決まっています。</p><h2 id=\"index_ZKdeas3Q\">Mac へ送ったファイルが、途中で変わることがあった</h2><p>作ったファイルは、Claude の作業環境から、Mac とつながった経路で iCloud と外付けの SSD(KIOXIA)に送っています。この経路で、3つのことが起きました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>起きたこと</p></th><th colspan=\"1\" rowspan=\"1\"><p>どう分かったか</p></th><th colspan=\"1\" rowspan=\"1\"><p>どうしたか</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>約28MB を超えるファイルが送れない(HTTP 400)</p></td><td colspan=\"1\" rowspan=\"1\"><p>送る道具がエラーを返した</p></td><td colspan=\"1\" rowspan=\"1\"><p>10MB ずつに分けて送り、Mac でつないだ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>分けた動画の先頭の1つ、透明な部分のある PNG(ハコブくんの切り抜きなど)が、届いたときに中身が変わっていた</p></td><td colspan=\"1\" rowspan=\"1\"><p>送る前と届いたあとの指紋(md5。ファイルの中身から作る32桁の値)が違った。PNG は届いた方が大きかった</p></td><td colspan=\"1\" rowspan=\"1\"><p>gzip で圧縮して送り、Mac で戻した。戻したあとの指紋が一致するのを確かめた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Mac にあるスクリプト(assemble.py・2本の scenes.py)が、古い版に戻っていた</p></td><td colspan=\"1\" rowspan=\"1\"><p>Mac と作業環境のファイルの指紋を比べた</p></td><td colspan=\"1\" rowspan=\"1\"><p>送り直して、指紋が一致するのを確かめた</p></td></tr></tbody></table></div><p>YouTube に上げた動画ファイルは、作業環境の指紋 <code>d9f6def3f1a01d9aee5c76c9d6516bc0</code> と、Mac でつないだあとの指紋が一致しています。送ったあとは、毎回指紋を比べることにしました。</p><h2 id=\"index_lMTio9tY\">次の動画から使う「1本を作る流れ」23手順</h2><blockquote><p>動画の作り方の手順をまとめてありますか?他の台本を使って動画を量産していきますよ。</p></blockquote><p>作る手順は、動画のルール <code>video_rules.md</code> の中に分かれて書かれていて、サムネイル・アップロード・投稿の手順はどこにもありませんでした。そこで <code>video_rules.md</code> の先頭に「0節 1本を作る流れ」を足し、台本から投稿までを23の手順にまとめました。「フィジカルAIの現在地」で実際に通した順番です。担当の <strong>エ</strong> は本人、<strong>C</strong> は Claude です。追記した0節を、ファイルのまま載せます。</p><pre><code class=\"language-markdown\">### 準備(1本ごとに1回)\n\n1. **エ** どの台本を動画にするか決めて、Claude に伝える\n2. **C** iCloud の `動画/` に作業フォルダ `{動画名}/` を作る。前の動画の作業フォルダから `narration.py`・`scenes.py`・`sound.py`・`thumb.py`・`概要欄_下書き.md` を写して、書き換える元にする\n\n### 台本(2節・3節)\n\n3. **C** 台本を `narration.py` に書く。1文ごとに(場面の名前、表情、字幕の文、後ろの間)。自己紹介の2文(2節10)と、間の取り方(2節12)は前の動画と同じにする\n4. **C** 読みの直しを書く。`READ`(仮音声用)と `ELEVEN_READ`(ElevenLabs 用)。「エイイチ」は ElevenLabs には「えいいち」で渡す(3-1)\n5. **C** `make_docs.py` で読み物の台本と codex の依頼を作る。**エ** 内容を確かめる。数字と出典は、公開前に元のページを開いて確かめる\n6. **エ** 台本を確定する(ここで決まった文が、声のお金を使う単位になる)\n\n### 声(3-1)\n\n7. **エ** Mac のターミナルで、作業フォルダに移って声を作る:\n `read -s ELEVENLABS_API_KEY && export ELEVENLABS_API_KEY`(キーを貼って Enter。画面には出ない)\n `python3 ../_制作ベース/tts_eleven.py --voice gWJpc8whMBqnF7Y0kc9f --name ハコブ`\n 読み方を比べたい語があるときは `--try \"文1|文2\"` で試しの音声だけ作る\n8. **C** `tts_import.py voice_eleven/ハコブ` で取り込む。読み間違い(例: 裏表が「うらおもてうら」)が見つかったら、`ELEVEN_READ` を直し、**エ** がその文だけ `--lines 番号` で作り直す\n\n### 音(8節の BGM・効果音の行)\n\n9. **C** `sound.py` に BGM と効果音の置き方を書く。いまの決まり: BGM は `bgm_明るいB_2` を最初から最後までループ、話している間 -23dB・間 -16dB。場面の切り替えはホワーン(-6dB)、自己紹介はキラリン、大きな流れの変わり目はポロロン\n10. **C** `audio_mix.py --first N` で音だけ書き出し、**エ** が冒頭を聞いて大きさを確かめる\n\n### 図と字幕(4節・5節・6節)\n\n11. **C** `scenes.py` に場面ごとの図を書く。実機映像は `src/` に置き、`VIDEO` に場面名と開始秒を書く。透かし入りの映像は使わない(6節)\n12. **C** `render.py` で図・テロップ・字幕を作る。ハコブくんの区画と重なったら止まるので、直して通す\n13. **C** `preview.py 文番号…` で、気になる文の画面を静止画で出す。**エ** 確かめる\n\n### 書き出しと確認(7節)\n\n14. **C** `assemble.py` で全体を書き出す(14分で30分ほど)。確認用(30MB 以下)をチャットに送り、KIOXIA の `videos/{動画名}/` にも置く\n15. **エ** 通しで見て、直す所を時間つきで伝える(例:「3分16秒の読みがおかしい」「55秒の右下がモヤモヤ」)\n16. **C** 直して書き出し直す(中身が変わった文だけ作り直すので、2回目からは速い)。15〜16 を合格まで繰り返す\n17. **C** アップロード用(crf 20)を KIOXIA に置く。大きいファイルは分けて送り、Mac でつなぎ、指紋(md5)が一致するのを確かめる\n\n### YouTube に出すもの(7節)\n\n18. **C** `概要欄_下書き.md` を書き、`description.py` で `概要欄.txt` を作る。目次の時間は自動で入る。リンクは開いて確かめてから載せる\n19. **C** タイトル案を3つ出す。**エ** 決める\n20. **C** `thumb.py` に文言と写真を書き、`thumbnail.py` でサムネイルを作る(8節のサムネイルの行)。写真は、ハコブくんを左下に置いてもアームや物に重ならないコマを実機映像から選ぶ。**エ** 確かめる\n21. **エ** YouTube にアップロードする。設定: 子ども向けは「いいえ」。字幕ファイル(srt)は、映像に字幕が入っているので、上げると字幕をオンにした人には2重に出る(上げるかはエイイチが決める)。公開直後は低い画質で再生され、HD(1080p)は数十分〜1時間後に出る\n\n### 公開のあと\n\n22. **C** X と LinkedIn の投稿文を作る(書き方は article_rules.md の SNS 節。X は URL 込み140字・LinkedIn は英語→日本語と、コメント欄への補足)。**エ** 動画の URL を入れて投稿する\n23. **C** この動画で決めたこと・直したことを、本ファイルの該当の節と更新履歴に書く。iCloud の `_制作ベース` とプロジェクトの `claude/video_rules.md` の両方に保存する\n\n### 前の動画でつまずいた所(先に気をつける)\n\n- ElevenLabs の API キーには「音声の読み上げ・効果音・音楽生成」の3つの許可が要る(音楽生成がないと BGM が作れない)\n- Claude から Mac へ送ったファイルが途中で変わることがある(大きい動画・透明部分のある PNG)。送ったあとは指紋(md5)を比べ、違えば圧縮して送り直す\n- Mac 側のファイルが古い版に戻っていたことがある。書き出しの前に、Mac と Claude の作業環境でスクリプトの指紋がそろっているか確かめる\n</code></pre><h2 id=\"index_B9rTkmMC\">まだ決まっていないこと</h2><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>いまの状態</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ポロロンの使い分け</p></td><td colspan=\"1\" rowspan=\"1\"><p>大きな流れの変わり目7か所すべてで同じポロロンを鳴らしている。場所ごとに音を変えるかは試し中</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ジングルとアウトロの文言</p></td><td colspan=\"1\" rowspan=\"1\"><p>「Pixels to Motors。案内役のハコブくんです。」とエンディングの文は、毎回同じにするか未定</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>字幕ファイルの登録</p></td><td colspan=\"1\" rowspan=\"1\"><p>焼き込んだ字幕と2重になる。登録を残すかは未定</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>HD の版の画質</p></td><td colspan=\"1\" rowspan=\"1\"><p>1080p の版での文字の見え方をまだ見ていない。甘ければ 2560×1440 で書き出し直す</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>チャンネル名の反映</p></td><td colspan=\"1\" rowspan=\"1\"><p>「フィジカルAI解説|Pixels to Motors」が表示されていない。上限に当たったかを確かめる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>動画のハコブくんの体</p></td><td colspan=\"1\" rowspan=\"1\"><p>作り直した体は、サムネイルだけで使っている。動画にも使うなら、hakobu.py の画面の四隅・頭の位置・光の輪の位置を測り直し、表情を一覧で確かめる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1X NEO の配送</p></td><td colspan=\"1\" rowspan=\"1\"><p>「配送開始を示す公式発表が見つからない」は 2026年10月12日に確かめた内容。公式発表が出たら、概要欄を直す</p></td></tr></tbody></table></div><h2 id=\"index_XNI1szIP\">この記事で作った・直したスクリプトの全文</h2><p>動画ごとの作業フォルダ(<code>動画/フィジカルAIの現在地/</code>)のファイルと、どの動画でも使う部品を置く <code>_制作ベース/</code> のファイルです。<code>_制作ベース/</code> のうち、この記事の作業で新しく作ったもの、または直したものを載せています。図とハコブくんの描き方(<code>scene_kit.py</code>・<code>hakobu.py</code>・<code>render.py</code>・<code>subtitle.py</code>)は、LOG331 から変えていないので載せていません。</p><h3 id=\"index_OykTUSdQ\">_制作ベース/timing.py</h3><p>新規。文ごとの尺(コマ数)と、ナレーションのつなぎ方を決める。assemble.py と audio_mix.py が同じ規則を使うために分けた(51行)。</p><pre><code class=\"language-python\">\"\"\"文ごとの尺(フレーム)と、ナレーションの音をつなぐ規則。assemble.py と audio_mix.py が同じものを使う。\"\"\"\nimport json, wave\nimport numpy as np\nfrom subtitle import pages as sub_pages\n\nFPS, SR = 30, 48000\n\n\ndef load(first=None):\n idx = json.load(open('tts/index.json'))\n return idx[:first] if first else idx\n\n\ndef layout(idx):\n \"\"\"各文に frames(間を含む尺)・speech(読み上げの長さ)・start(開始フレーム)・parts(字幕の枚の切り替え)を足す。全体のフレーム数を返す\"\"\"\n t = 0\n for o in idx:\n o['frames'] = max(1, round((o['dur'] + o['pause']) * FPS))\n o['speech'] = round(o['dur'] * FPS)\n o['start'] = t\n t += o['frames']\n # 「|」で分けた字幕は、読み上げの長さを字数で割って切り替える\n pages = sub_pages(o['text']) if o['text'] else []\n cuts, acc, total = [0], 0, sum(len(p) for p in pages)\n for p in pages[:-1]:\n acc += len(p)\n cuts.append(round(o['speech'] * acc / total))\n cuts.append(o['frames'])\n o['parts'] = [(k, cuts[k], cuts[k + 1]) for k in range(len(pages))] or [(None, 0, o['frames'])]\n return t\n\n\ndef narration(idx):\n \"\"\"各文の音声を、尺ちょうどに合わせてつないだ int16 モノラル\"\"\"\n parts = []\n for o in idx:\n need = o['frames'] * SR // FPS\n if o['text']:\n with wave.open(f\"tts/{o['i']:03d}.wav\") as w:\n a = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16)\n else:\n a = np.zeros(0, dtype=np.int16)\n a = a[:need]\n parts.append(np.concatenate([a, np.zeros(need - len(a), dtype=np.int16)]))\n return np.concatenate(parts)\n\n\ndef write_wav(path, a, ch=1):\n \"\"\"a は int16(モノラルは1次元、ステレオは (n, 2))\"\"\"\n with wave.open(path, 'wb') as w:\n w.setnchannels(ch); w.setsampwidth(2); w.setframerate(SR); w.writeframes(np.ascontiguousarray(a).tobytes())\n</code></pre><h3 id=\"index_P6iLbpu7\">_制作ベース/audio_mix.py</h3><p>新規。ナレーションに BGM と効果音を混ぜる。話している間は BGM を下げ、0.8秒以上の間で戻す。曲の終わりを切ってループする(173行)。</p><pre><code class=\"language-python\">\"\"\"ナレーションに BGM と効果音を混ぜる。音の置き方は、動画の作業フォルダの sound.py に書く。\n使い方(動画の作業フォルダで)\n python3 <_制作ベース>/audio_mix.py [--first N] 音だけ先に確かめる → 音の確認.m4a(映像は作らない)\n assemble.py は sound.py があると、これを呼んで mix.wav を使う\n\nsound.py の書き方\n BGM = [dict(file='bgm_仮', start=0, end=None, level=-22, gap=-15, fade_in=0.8, fade_out=2.0, offset=0.0)]\n file 作業フォルダからの相対パス(sound/bgm_main.mp3 など)。見つからなければ <_制作ベース>/sound/{file}.wav\n start 流し始める文の番号。end は止める文の番号(その文は含まない。None は最後まで)\n level 話している間の大きさ。ナレーションより何 dB 小さくするか(-22 なら、ナレーションの約 1/12 の音圧)\n gap 話していない間(0.8 秒以上の間)の大きさ。少し上げて、間が空っぽに聞こえないようにする\n fade_in / fade_out 入りと終わりのフェード(秒)。offset は BGM のどこから流すか(秒)\n 曲の終わりの小さくなっていく所は切り、BGM が区間より短いときは 2 秒重ねてループする\n SE = [(文番号, 文の頭から何秒後, 音, ナレーションより何dB)]\n 音は file と同じ探し方(se_pop など)。例: (2, 0.0, 'se_pop', -8)\n SCENE_SE = dict(file='se_rise', level=-12, offset=-0.15)\n 場面(narration.py の場面の名前)が変わる文ごとに、自動で同じ音を入れる(動画の最初の文は入れない)。\n offset は場面が変わる瞬間から何秒ずらすか(マイナスで少し前から鳴らし、音の山を切り替えに合わせる)\n skip=['場面名', ...] に書いた場面へ切り替わるときは鳴らさない(同じ映像のままテロップだけ替える場面など)\n CHAPTER_SE = dict(file='se_chapter', level=-8, offset=-0.3, scenes=['c1', 'c2', ...])\n scenes に書いた場面(章の始まりなど、大きな流れの変わり目)へ切り替わるときは、SCENE_SE の代わりにこの音を鳴らす\n files={'場面名': '音', ...} を書くと、その場面だけ別の音にする(例: 自己紹介だけキラリン)\n\"\"\"\nimport os, subprocess, sys\nimport numpy as np\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, '.')\nsys.path.insert(1, HERE)\nfrom timing import FPS, SR, load, layout, narration, write_wav\n\nWIN = int(0.05 * SR)\n\n\ndef find(name):\n for p in (name, os.path.join(HERE, 'sound', name), os.path.join(HERE, 'sound', name + '.wav')):\n if os.path.isfile(p):\n return p\n sys.exit(f'音のファイルが見つかりません: {name}(作業フォルダからの相対パスか、_制作ベース/sound/ の名前で書く)')\n\n\ndef read(path):\n \"\"\"どの形式でも 48kHz ステレオの float にする\"\"\"\n raw = subprocess.run(['ffmpeg', '-v', 'error', '-i', path, '-ac', '2', '-ar', str(SR), '-f', 's16le', '-'],\n capture_output=True, check=True).stdout\n return np.frombuffer(raw, dtype=np.int16).reshape(-1, 2).astype(np.float32) / 32768.0\n\n\ndef win_rms(a):\n m = a.mean(axis=1) if a.ndim == 2 else a\n n = len(m) // WIN\n if n == 0:\n return np.array([np.sqrt(np.mean(m ** 2)) + 1e-9])\n return np.sqrt((m[:n * WIN].reshape(n, WIN) ** 2).mean(axis=1)) + 1e-9\n\n\ndef db(x):\n return 10 ** (x / 20)\n\n\ndef cut_tail(a):\n \"\"\"曲の終わりで音が小さくなっていく所(ふつうの大きさより10dB以上小さい所)を切る。ループでつなぐとき、途中で音が消えないようにする\"\"\"\n r = win_rms(a)\n loud = np.where(r > np.median(r[r > 1e-3]) * db(-10))[0]\n if len(loud) == 0:\n return a\n return a[:min(len(a), (loud[-1] + 1) * WIN)]\n\n\ndef loop_to(a, n, xf=SR):\n \"\"\"a を n サンプルまでループで伸ばす(つなぎ目は xf サンプル重ねる)\"\"\"\n if len(a) >= n:\n return a[:n]\n xf = min(xf, len(a) // 4)\n out = a.copy()\n ramp = np.linspace(0, 1, xf)[:, None]\n while len(out) < n:\n head = out[:-xf]\n cross = out[-xf:] * (1 - ramp) + a[:xf] * ramp\n out = np.concatenate([head, cross, a[xf:]])\n return out[:n]\n\n\ndef smooth(x, sec):\n k = max(1, int(sec * 100))\n return np.convolve(np.pad(x, (k, k), mode='edge'), np.ones(k) / k, mode='same')[k:-k]\n\n\ndef mix(idx, pcm, out='mix.wav'):\n ns = {}\n exec(open('sound.py', encoding='utf-8').read(), ns)\n BGM, SE = ns.get('BGM', []), list(ns.get('SE', []))\n sc = ns.get('SCENE_SE')\n ch = ns.get('CHAPTER_SE') or {}\n if sc or ch:\n for prev, o in zip(idx, idx[1:]):\n if o['scene'] == prev['scene']:\n continue\n if o['scene'] in ch.get('scenes', []): # 大きな流れの変わり目は、別の音にする\n f = ch.get('files', {}).get(o['scene'], ch['file']) # 場面ごとに音を替えられる(files={'title': 'se_chapter_glock'})\n SE.append((o['i'], ch.get('offset', -0.3), f, ch.get('level', -8)))\n elif sc and o['scene'] not in sc.get('skip', []):\n SE.append((o['i'], sc.get('offset', -0.15), sc['file'], sc.get('level', -12)))\n voice = pcm.astype(np.float32) / 32768.0\n n = len(voice)\n ref = np.percentile(win_rms(voice), 90) # ナレーションの「ふつうに話している」大きさ\n total = n / SR\n by_i = {o['i']: o for o in idx}\n\n # 話している所(各文の読み上げ)を、前0.15秒・後0.3秒広げる。0.8 秒未満の間は話している扱いのまま(BGM が細かく上下しない)\n ctl = np.zeros(int(total * 100) + 1)\n for o in idx:\n if o['text']:\n a = max(0, int((o['start'] / FPS - 0.15) * 100)); b = int(((o['start'] + o['speech']) / FPS + 0.3) * 100)\n ctl[a:b] = 1\n gaps = np.where(np.diff(np.concatenate([[1], ctl, [1]])) != 0)[0].reshape(-1, 2)\n for a, b in gaps:\n if (b - a) < 50: # 0.5 秒(広げた後)= もとの間で約 0.8〜0.95 秒未満\n ctl[a:b] = 1\n ctl = smooth(ctl, 0.3)\n\n bus = np.zeros((n, 2), dtype=np.float32)\n for m in BGM:\n src = read(find(m['file']))\n src = src[int(m.get('offset', 0) * SR):]\n g = ref / np.median(win_rms(src)[win_rms(src) > 1e-3])\n src = cut_tail(src)\n s0 = by_i[m.get('start', 0)]['start'] * SR // FPS if m.get('start', 0) in by_i else 0\n e = m.get('end')\n e0 = by_i[e]['start'] * SR // FPS if e is not None and e in by_i else n\n seg = loop_to(src, e0 - s0, xf=2 * SR) * g\n # 話している間は level、間は gap(控えめに 100 Hz で決めてから、サンプルに伸ばす)\n t = (np.arange(s0, e0) / SR * 100).astype(int).clip(0, len(ctl) - 1)\n lv = db(m.get('level', -22)) * ctl[t] + db(m.get('gap', -15)) * (1 - ctl[t])\n fi, fo = int(m.get('fade_in', 0.8) * SR), int(m.get('fade_out', 2.0) * SR)\n fade = np.ones(e0 - s0, dtype=np.float32)\n if fi:\n fade[:fi] = np.linspace(0, 1, min(fi, len(fade)))[:len(fade[:fi])]\n if fo:\n fade[-fo:] = np.minimum(fade[-fo:], np.linspace(1, 0, min(fo, len(fade))))\n bus[s0:e0] += seg * (lv * fade)[:, None]\n\n log = []\n for line, off, name, level in SE:\n if line not in by_i:\n continue\n src = read(find(name))\n g = ref / win_rms(src).max() * db(level)\n s0 = max(0, int((by_i[line]['start'] / FPS + off) * SR))\n k = min(len(src), n - s0)\n if k > 0:\n bus[s0:s0 + k] += src[:k] * g\n log.append(f\" {s0 / SR:6.2f}秒 文{line:03d} {name} {level}dB 「{by_i[line]['text'][:16]}」\")\n\n out_a = voice[:, None] + bus\n peak = np.abs(out_a).max()\n if peak > 0.98:\n out_a *= 0.98 / peak # 割れないように全体を下げる(最後の loudnorm で大きさはそろう)\n write_wav(out, (out_a * 32767).astype(np.int16), ch=2)\n print(f'BGM {len(BGM)}本・効果音 {len(log)}個 → {out}')\n print('\\n'.join(log))\n return out\n\n\nif __name__ == '__main__':\n args = sys.argv[1:]\n first = int(args[args.index('--first') + 1]) if '--first' in args else None\n idx = load(first)\n layout(idx)\n pcm = narration(idx)\n f = mix(idx, pcm)\n subprocess.run(['ffmpeg', '-v', 'error', '-y', '-i', f, '-af', 'loudnorm=I=-16:TP=-1.5:LRA=11',\n '-c:a', 'aac', '-b:a', '160k', '-ar', '48000', '音の確認.m4a'], check=True)\n print('音の確認.m4a')\n</code></pre><h3 id=\"index_F7ztL1Qa\">_制作ベース/assemble.py</h3><p>直した。sound.py があれば audio_mix.py を呼ぶ。字幕の後ろの帯をやめた。映像の指定を背景の指紋に入れた。前の場面の映像の続きから流す指定(開始秒 None)を足した(144行)。</p><pre><code class=\"language-python\">\"\"\"1文ずつ背景の映像を作ってつなぎ、ハコブくんを重ねて、音声と合わせる。\n使い方: 動画の作業フォルダで(render.py のあと)\n python3 <_制作ベース>/assemble.py [--first N] [--force] [--name 動画名]\n --first N 最初の N 文だけ書き出す(確かめ用)\n --force 背景の映像を作り直す(ふだんは、できているものは使い回す)\n音: 作業フォルダに sound.py があれば、BGM・効果音を混ぜる(audio_mix.py。音だけ先に確かめるときは audio_mix.py を単独で動かす)\n出力: out.mp4(作業用・crf 20・-16 LUFS)、out_確認用.mp4(crf 29・30MB以下)、out.srt、timeline.json\n\"\"\"\nimport json, subprocess, os, sys, wave, hashlib\nimport numpy as np\nfrom concurrent.futures import ThreadPoolExecutor\nfrom PIL import Image\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, '.')\nsys.path.insert(1, HERE)\nos.environ.setdefault(\"P2M_BASE\", HERE)\nfrom scenes import VIDEO\nfrom hakobu import Hakobu\nfrom subtitle import pages as sub_pages\nfrom timing import FPS, SR, load, layout, narration, write_wav\n\nargs = sys.argv[1:]\nfirst = int(args[args.index('--first') + 1]) if '--first' in args else None\nforce = '--force' in args\nidx = load(first)\nos.makedirs('chunks', exist_ok=True); os.makedirs('comp', exist_ok=True)\nhk = Hakobu()\n\n# ---------- 尺(フレーム単位)と字幕の枚(timing.py。audio_mix.py と同じ規則) ----------\ntotal_frames = layout(idx)\n\n# 実機の映像の場面: 同じ場面が続く間は、続きから再生する。開始秒が None の場面は、前の場面の映像の続きから再生する\n# (同じ映像のまま、テロップだけ替えたいとき。2026-10-12)\nprev, off = None, 0.0\nfor o in idx:\n sc = o['scene']\n if sc in VIDEO:\n if sc != prev and VIDEO[sc][1] is not None:\n off = VIDEO[sc][1]\n o['voff'] = off\n off += o['frames'] / FPS\n prev = sc\n\n# 背景(字幕まで入れて、ハコブくんは入れない)。最後に1回だけ圧縮し直すので、ここは画質を高めに保つ\nENC = ['-c:v', 'libx264', '-preset', 'ultrafast', '-crf', '12', '-pix_fmt', 'yuv420p', '-r', str(FPS), '-an']\njobs = []\nfor o in idx:\n for k, a, b in o['parts']:\n jobs.append((o, k, a, b))\n\n\ndef job(j):\n o, k, a, b = j\n tag = f\"{o['i']:03d}\" + (f\"_{k}\" if k is not None else \"\")\n # 中身(場面・字幕・尺・映像の位置・図と字幕の画像)が変わったら作り直すよう、ファイル名に指紋を入れる\n sub_png = f\"png/sub/{o['i']:03d}_{k}.png\" if k is not None else \"\"\n fp = [o['scene'], o['text'], a, b, round(o.get('voff', 0), 3), VIDEO.get(o['scene'])] # 映像のファイルや透かしの設定が変わっても作り直す\n for f in (f\"png/scene/{o['scene']}.png\", f\"png/ovl/{o['scene']}.png\", sub_png):\n if f and os.path.exists(f):\n fp.append(hashlib.md5(open(f, 'rb').read()).hexdigest())\n out = f\"chunks/{tag}_{hashlib.md5(repr(fp).encode()).hexdigest()[:8]}.mp4\"\n if os.path.exists(out) and not force:\n return out\n sub = f\"png/sub/{o['i']:03d}_{k}.png\" if k is not None else None\n sc, n = o['scene'], str(b - a)\n if sc in VIDEO:\n src, _, wm = VIDEO[sc]\n dl = 'delogo=x=2740:y=1840:w=1010:h=220,' if wm else ''\n ins = ['-ss', f\"{o['voff'] + a / FPS:.3f}\", '-stream_loop', '-1', '-i', src, '-loop', '1', '-i', f'png/ovl/{sc}.png']\n fc = (f\"[0:v]{dl}scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080,fps={FPS},setsar=1[b];\" # 字幕の後ろの黒い帯は敷かない(2026-10-12 エイイチの指示)\n f\"[b][1:v]overlay[c]\")\n if sub:\n ins += ['-loop', '1', '-i', sub]\n fc += \";[c][2:v]overlay[v]\"\n else:\n fc += \";[c]null[v]\"\n cmd = ['ffmpeg', '-v', 'error', '-y'] + ins + ['-filter_complex', fc, '-map', '[v]', '-frames:v', n] + ENC + [out]\n else:\n comp = f\"comp/{tag}.png\"\n im = Image.open(f'png/scene/{sc}.png').convert('RGBA')\n if sub:\n im.alpha_composite(Image.open(sub).convert('RGBA'))\n im.convert('RGB').save(comp)\n cmd = ['ffmpeg', '-v', 'error', '-y', '-loop', '1', '-framerate', str(FPS), '-i', comp, '-frames:v', n,\n '-tune', 'stillimage'] + ENC + [out]\n subprocess.run(cmd, check=True)\n return out\n\n\nwith ThreadPoolExecutor(max(1, min(3, os.cpu_count() or 1))) as ex:\n outs = list(ex.map(job, jobs))\nopen('chunks/list.txt', 'w').write(''.join(f\"file '{os.path.basename(x)}'\\n\" for x in outs))\nsubprocess.run(['ffmpeg', '-v', 'error', '-y', '-f', 'concat', '-safe', '0', '-i', 'chunks/list.txt', '-c', 'copy', 'bg.mp4'], check=True)\n\n# ---------- 音声: 各文を尺ちょうどに合わせて連結。sound.py があれば BGM・効果音を混ぜる(audio_mix.py) ----------\npcm = narration(idx)\nwrite_wav('narration.wav', pcm)\naudio_in = 'narration.wav'\nif os.path.exists('sound.py'):\n from audio_mix import mix\n audio_in = mix(idx, pcm) # mix.wav(ステレオ)。ハコブくんの口はナレーションだけで動かす\n\n# ---------- 字幕 SRT(「|」で分けた枚は別の行にする) ----------\ndef ts(f):\n ms = round(f / FPS * 1000); h, ms = divmod(ms, 3600000); m, ms = divmod(ms, 60000); s, ms = divmod(ms, 1000)\n return f\"{h:02d}:{m:02d}:{s:02d},{ms:03d}\"\nsrt, k = [], 1\nfor o in idx:\n if not o['text']:\n continue\n pages = sub_pages(o['text'])\n for (pk, a, b), ptxt in zip(o['parts'], pages):\n end = min(b, o['speech']) if pk == len(pages) - 1 else b\n srt.append(f\"{k}\\n{ts(o['start'] + a)} --> {ts(o['start'] + end)}\\n{ptxt}\\n\"); k += 1\nopen('out.srt', 'w', encoding='utf-8').write('\\n'.join(srt))\njson.dump(idx, open('timeline.json', 'w'), ensure_ascii=False)\n\n# ---------- ハコブくん: 表情と口・目・リングの動きを決めて、背景に重ねる ----------\ntimeline = [(o['start'], o['start'] + o['frames'], o['start'] + o['speech'], o.get('face', 'base'), bool(o['text'])) for o in idx]\nparams = hk.animate(timeline, pcm.astype(np.float32) / 32768.0)\ncmd = ['ffmpeg', '-v', 'error', '-y', '-i', 'bg.mp4',\n '-f', 'rawvideo', '-pix_fmt', 'rgba', '-s', f'{hk.w}x{hk.h}', '-r', str(FPS), '-i', '-',\n '-i', audio_in,\n '-filter_complex', f'[0:v][1:v]overlay=x={hk.x}:y={hk.y}:eof_action=pass[v]',\n '-map', '[v]', '-map', '2:a', '-c:v', 'libx264', '-preset', 'medium', '-crf', '20', '-pix_fmt', 'yuv420p',\n '-af', 'loudnorm=I=-16:TP=-1.5:LRA=11', '-c:a', 'aac', '-b:a', '192k', '-ar', '48000',\n '-movflags', '+faststart', '-frames:v', str(total_frames), 'out.mp4']\np = subprocess.Popen(cmd, stdin=subprocess.PIPE)\nfor i in range(total_frames):\n p.stdin.write(hk.frame(params[i]).tobytes())\np.stdin.close()\nif p.wait():\n sys.exit('ハコブくんを重ねる書き出しに失敗しました')\nstill = round(total_frames * 0.42)\nsubprocess.run(['ffmpeg', '-v', 'error', '-y', '-ss', f'{still / FPS:.2f}', '-i', 'out.mp4', '-frames:v', '1', 'out_still.png'], check=True)\n\n# ---------- 確認用(チャットで送る方): crf 29・音声 112kbps・30MB 以下 ----------\nfor crf in (29, 32, 35):\n subprocess.run(['ffmpeg', '-v', 'error', '-y', '-i', 'out.mp4', '-c:v', 'libx264', '-preset', 'medium', '-crf', str(crf),\n '-c:a', 'aac', '-b:a', '112k', '-movflags', '+faststart', 'out_確認用.mp4'], check=True)\n mb = os.path.getsize('out_確認用.mp4') / 1e6\n if mb <= 30:\n break\nprint(f'frames {total_frames} = {total_frames / FPS:.1f}s 確認用 {mb:.1f}MB (crf {crf})')\n</code></pre><h3 id=\"index_WrI3e7rF\">_制作ベース/preview.py</h3><p>直した。字幕の後ろの帯をやめ、前の場面の映像の続き(開始秒 None)を、書き出しの記録から読むようにした(54行)。</p><pre><code class=\"language-python\">\"\"\"確かめ用の静止画。動画を書き出さずに、指定した文の画面(図または実機映像の1コマ+字幕+ハコブくん)を1枚ずつ出す。\n使い方: 動画の作業フォルダで(render.py のあと) python3 <_制作ベース>/preview.py 8 9 12\n出力: preview/{文番号}_{字幕の枚}.png と、並べた preview/一覧.png\n\"\"\"\nimport json, os, sys, subprocess\nfrom PIL import Image\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, '.')\nsys.path.insert(1, HERE)\nos.environ.setdefault(\"P2M_BASE\", HERE)\nfrom scenes import VIDEO\nfrom hakobu import Hakobu, EXPR\nfrom subtitle import pages\n\nidx = {o['i']: o for o in json.load(open('tts/index.json'))}\nnums = [int(x) for x in sys.argv[1:]] or [0]\nos.makedirs('preview', exist_ok=True)\nhk = Hakobu()\nouts = []\nfor i in nums:\n o = idx[i]\n sc = o['scene']\n if sc in VIDEO:\n src, start, wm = VIDEO[sc]\n start = o.get('voff', start) if start is None else start # 続きから再生する場面は、書き出しのときの位置(timeline.json)を使う\n if start is None:\n tl = {t['i']: t for t in json.load(open('timeline.json'))} if os.path.exists('timeline.json') else {}\n start = tl.get(i, {}).get('voff', 0.0)\n tmp = f'preview/_v{i}.png'\n subprocess.run(['ffmpeg', '-v', 'error', '-y', '-ss', f'{start:.2f}', '-i', src, '-frames:v', '1',\n '-vf', ('delogo=x=2740:y=1840:w=1010:h=220,' if wm else '') + 'scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080', tmp], check=True)\n bg = Image.open(tmp).convert('RGBA')\n bg.alpha_composite(Image.open(f'png/ovl/{sc}.png').convert('RGBA'))\n else:\n bg = Image.open(f'png/scene/{sc}.png').convert('RGBA')\n ex = EXPR[o.get('face', 'base')]\n face = hk.frame((ex['eyes'], ex['brows'], 'mid', 0.4, 0.0))\n n = len(pages(o['text'])) if o['text'] else 1\n for k in range(n):\n im = bg.copy()\n sub = f'png/sub/{i:03d}_{k}.png'\n if os.path.exists(sub):\n im.alpha_composite(Image.open(sub).convert('RGBA'))\n im.alpha_composite(face, (hk.x, hk.y))\n path = f'preview/{i:03d}_{k}.png'\n im.convert('RGB').save(path)\n outs.append(path)\ncols = 2\nw, h = 960, 540\nsheet = Image.new('RGB', (w * cols, h * ((len(outs) + cols - 1) // cols)), 'white')\nfor k, p in enumerate(outs):\n sheet.paste(Image.open(p).resize((w, h)), ((k % cols) * w, (k // cols) * h))\nsheet.save('preview/一覧.png')\nprint('\\n'.join(outs))\n</code></pre><h3 id=\"index_Y7zJS_Te\">_制作ベース/sound_synth.py</h3><p>新規。効果音(ホワーン・キラリン・ポロロンなど)と、仮の BGM をコードで作る(386行)。</p><pre><code class=\"language-python\">\"\"\"効果音と仮のBGMを、コードで作る(音源サイトに頼らない。だれでも同じ音を作り直せる)。\n使い方: python3 <_制作ベース>/sound_synth.py → <_制作ベース>/sound/ に wav を書く(warm を付けると温かみの BGM、bgm3 を付けると BGM の候補3種を作る)\n se_pop.wav ぽん(図・数字が出る。いちばんよく使う)\n se_tick.wav こつ(小さな切り替え。字幕の枚が変わる・箇条書きが1つ出る)\n se_whoosh.wav しゅっ(章や場面が大きく変わる)\n se_ding.wav ちん(結論・大事な数字)\n se_rise.wav ひゅい(驚き・問いかけ)。エイイチは「ホワーン」と呼ぶ。場面の切り替えに使う\n se_chapter.wav ポロン(章の始まりなど、大きな流れの変わり目。2026-10-12 追加)\n se_chapter_marimba.wav ポロロン(マリンバの上がる3音)。se_chapter の候補\n se_chapter_harp.wav シャララン(ハープ風に弦をはじいて上がる)。se_chapter の候補\n se_chapter_piano.wav ポーン(やわらかいピアノの和音を1つ)。se_chapter の候補\n se_chapter_glock.wav キラリン(鉄琴の高い3音)。se_chapter の候補\n se_chapter_swell.wav フワァ(弦の和音がふくらんで消える)。se_chapter の候補\n se_chapter_wind.wav シャラン(ウィンドチャイム)。se_chapter の候補\n bgm_仮.wav 仮のBGM(電子音。32秒で切れ目なくループ)\n bgm_仮_温かみ.wav 仮のBGM(はじく弦・やわらかいピアノ・弦の和音・ウッドベース風。2026-10-12 エイイチの「もう少し温かみ」の指示で追加)\n ElevenLabs Music などの本番のBGMができるまでの、方向を確かめる用\n bgm_1_ピアノ / bgm_2_ギターとマリンバ / bgm_3_ピッツィカート BGM の候補3種(2026-10-12)\n音の高さは、ナレーション(男声・おおむね100〜300Hz)とぶつからないよう、効果音は 500Hz より上を中心にしている。\n\"\"\"\nimport os, sys\nimport numpy as np\nfrom timing import SR, write_wav\n\nHERE = os.path.dirname(os.path.abspath(__file__))\nOUT = os.path.join(HERE, 'sound')\nrng = np.random.default_rng(7)\n\n\ndef t_(dur):\n return np.arange(int(dur * SR)) / SR\n\n\ndef env(n, attack=0.004, decay=0.15):\n t = np.arange(n) / SR\n return np.minimum(1, t / attack) * np.exp(-t / decay)\n\n\ndef norm(a, peak=0.9):\n return a / (np.abs(a).max() + 1e-9) * peak\n\n\ndef lowpass(a, cutoff):\n \"\"\"1次のローパス(やわらかくするだけ)\"\"\"\n k = np.exp(-2 * np.pi * cutoff / SR)\n out = np.empty_like(a); y = 0.0\n for i, x in enumerate(a):\n y = (1 - k) * x + k * y; out[i] = y\n return out\n\n\ndef pop():\n t = t_(0.12)\n f = 520 + 520 * np.exp(-t / 0.018) # 高い所から素早く下がる\n a = np.sin(2 * np.pi * np.cumsum(f) / SR) * env(len(t), 0.002, 0.035)\n a += 0.25 * np.sin(2 * np.pi * 2 * np.cumsum(f) / SR) * env(len(t), 0.001, 0.015)\n return norm(a)\n\n\ndef tick():\n t = t_(0.05)\n a = np.sin(2 * np.pi * 1800 * t) * env(len(t), 0.0008, 0.008)\n a += 0.4 * lowpass(rng.standard_normal(len(t)), 4000) * env(len(t), 0.0005, 0.004)\n return norm(a, 0.7)\n\n\ndef whoosh():\n d = 0.55; t = t_(d); n = len(t)\n noise = rng.standard_normal(n)\n # 帯域を低→高へ動かす(2つのローパスの差で帯域を作る)\n out = np.empty(n); y1 = y2 = 0.0\n for i in range(n):\n p = i / n\n c1 = 300 + 3200 * p ** 1.5; c2 = c1 * 0.35\n k1 = np.exp(-2 * np.pi * c1 / SR); k2 = np.exp(-2 * np.pi * c2 / SR)\n y1 = (1 - k1) * noise[i] + k1 * y1; y2 = (1 - k2) * noise[i] + k2 * y2\n out[i] = y1 - y2\n shape = np.sin(np.pi * np.minimum(1, t / d)) ** 2 * (t / d) ** 0.4\n return norm(out * shape, 0.8)\n\n\ndef ding():\n t = t_(1.2)\n f0 = 1046.5 # C6\n a = (np.sin(2 * np.pi * f0 * t) * np.exp(-t / 0.45)\n + 0.35 * np.sin(2 * np.pi * f0 * 2.0 * t) * np.exp(-t / 0.2)\n + 0.12 * np.sin(2 * np.pi * f0 * 3.0 * t) * np.exp(-t / 0.08))\n a *= np.minimum(1, t / 0.003)\n return norm(a, 0.8)\n\n\ndef rise():\n t = t_(0.35)\n f = 600 * (2 ** (t / 0.35 * 1.0)) # 1オクターブ上がる\n a = np.sin(2 * np.pi * np.cumsum(f) / SR)\n a *= np.sin(np.pi * t / 0.35) ** 1.5\n return norm(a, 0.7)\n\n\ndef chapter():\n \"\"\"章の変わり目の「ポロン」。やわらかい鐘の音を2つ(ミ→ラ)、少しずらして鳴らす。ホワーンより長く、落ち着いた音\"\"\"\n d = 1.6; t = t_(d); out = np.zeros(len(t))\n for k, (f0, at, g) in enumerate([(659.3, 0.0, 1.0), (880.0, 0.13, 0.85)]):\n tt = np.clip(t - at, 0, None); on = (t >= at)\n a = (np.sin(2 * np.pi * f0 * tt) * np.exp(-tt / 0.55) + 0.3 * np.sin(2 * np.pi * f0 * 2 * tt) * np.exp(-tt / 0.25)\n + 0.1 * np.sin(2 * np.pi * f0 * 3.01 * tt) * np.exp(-tt / 0.1))\n out += g * a * on * np.minimum(1, tt / 0.004)\n return norm(lowpass(out, 6000), 0.8)\n\n\ndef bgm_placeholder():\n \"\"\"やわらかいパッドと、小さな粒の音(アルペジオ)。テンポ 96・4小節×2=32秒・ループで切れ目が出ないよう、はみ出した余韻は頭に回す\"\"\"\n bpm = 96; beat = 60 / bpm; bars = 8; L = int(bars * 4 * beat * SR)\n buf = np.zeros((L, 2))\n chords = [[57, 60, 64, 67], [53, 57, 60, 64], [48, 52, 55, 59], [55, 59, 62, 64]] # Am7 Fmaj7 Cmaj7 G6(MIDI番号)\n hz = lambda m: 440 * 2 ** ((m - 69) / 12)\n\n def add(sig, start, pan=0.0):\n i0 = int(start * SR); idx = (np.arange(len(sig)) + i0) % L\n np.add.at(buf[:, 0], idx, sig * (1 - pan) * 0.5 + sig * 0.5 * (1 - abs(pan)))\n np.add.at(buf[:, 1], idx, sig * (1 + pan) * 0.5 + sig * 0.5 * (1 - abs(pan)))\n\n for b in range(bars):\n ch = chords[b % 4]\n d = 4 * beat\n t = t_(d + 1.5)\n pad_env = np.minimum(1, t / 0.9) * np.where(t < d, 1, np.exp(-(t - d) / 0.6))\n for k, m in enumerate(ch):\n f = hz(m)\n s = sum(np.sin(2 * np.pi * f * (1 + det) * t + ph) for det, ph in ((-0.003, 0), (0.003, 1.3)))\n s += 0.2 * np.sin(2 * np.pi * 2 * f * t)\n add(0.05 * s * pad_env, b * d, pan=(k - 1.5) * 0.25)\n bass = hz(ch[0] - 12)\n tb = t_(d)\n add(0.10 * np.sin(2 * np.pi * bass * tb) * np.minimum(1, tb / 0.05) * np.exp(-tb / 2.5), b * d)\n # 8分音符の粒(上の2音を行き来する)\n for s8 in range(8):\n m = ch[[1, 2, 3, 2, 1, 2, 3, 2][s8]] + 12\n tp = t_(0.6)\n note = np.sin(2 * np.pi * hz(m) * tp) * np.exp(-tp / 0.18) * np.minimum(1, tp / 0.004)\n vel = 0.05 if s8 % 2 == 0 else 0.032\n add(vel * note, b * d + s8 * beat / 2, pan=0.35 if s8 % 2 else -0.35)\n buf[:, 0] = lowpass(buf[:, 0], 5000); buf[:, 1] = lowpass(buf[:, 1], 5000)\n return buf / (np.abs(buf).max() + 1e-9) * 0.8\n\n\ndef pluck(f, dur, bright=0.5):\n \"\"\"はじいた弦(Karplus-Strong)。bright が小さいほど丸い音\"\"\"\n n = int(dur * SR); p = max(2, int(SR / f))\n buf = lowpass(rng.uniform(-1, 1, p), 1500 + 5000 * bright)\n out = np.empty(n)\n for i in range(n):\n out[i] = buf[i % p]\n buf[i % p] = 0.996 * 0.5 * (buf[i % p] + buf[(i + 1) % p])\n return out * np.minimum(1, np.arange(n) / (0.002 * SR))\n\n\ndef felt_piano(f, dur):\n \"\"\"フェルトを挟んだピアノ風(倍音を少し伸ばしてずらし、高い倍音ほど早く消す)\"\"\"\n t = t_(dur)\n a = sum((0.6 ** k) * np.sin(2 * np.pi * f * (k + 1) * (1 + 0.0004 * k * k) * t) * np.exp(-t / (1.8 / (k + 1) ** 0.8))\n for k in range(6))\n return a * np.minimum(1, t / 0.006)\n\n\ndef strings(f, dur):\n \"\"\"弦の和音のパッド(ゆっくり入り、少しビブラート)\"\"\"\n t = t_(dur)\n vib = 1 + 0.003 * np.sin(2 * np.pi * 5.2 * t)\n a = sum(np.sin(2 * np.pi * f * k * vib * t + k) / k ** 1.3 for k in range(1, 7))\n a += 0.6 * sum(np.sin(2 * np.pi * f * 1.002 * k * vib * t) / k ** 1.3 for k in range(1, 7))\n return a * np.minimum(1, t / 1.2) * np.minimum(1, (dur - t) / 0.8)\n\n\ndef bgm_warm():\n \"\"\"テンポ 88・8小節。ギターの分散和音、ピアノの和音、弦、ウッドベース風。ループで切れ目が出ないよう余韻は頭に回す\"\"\"\n bpm = 88; beat = 60 / bpm; bars = 8; L = int(bars * 4 * beat * SR)\n buf = np.zeros((L, 2))\n hz = lambda m: 440 * 2 ** ((m - 69) / 12)\n prog = [[48, 55, 64, 67, 72], [45, 52, 60, 64, 69], [41, 53, 57, 60, 65], [43, 55, 59, 62, 67]] # C Am F G(ギターの押さえ方に近い並び)\n\n def add(sig, start, pan=0.0, gain=1.0):\n i0 = int(start * SR); idx = (np.arange(len(sig)) + i0) % L\n np.add.at(buf[:, 0], idx, sig * gain * (1 - max(pan, 0)))\n np.add.at(buf[:, 1], idx, sig * gain * (1 + min(pan, 0)))\n\n for b in range(bars):\n ch = prog[b % 4]; d = 4 * beat; t0 = b * d\n # ギター: 8分音符の分散和音(指ではじく)\n pat = [0, 2, 3, 4, 1, 3, 2, 4]\n for k, j in enumerate(pat):\n vel = 0.22 if k % 2 == 0 else 0.15\n add(pluck(hz(ch[j]), 1.6, bright=0.35), t0 + k * beat / 2 + rng.uniform(0, 0.008), pan=-0.35, gain=vel)\n # ピアノ: 小節の頭と3拍目に、高い所で和音を置く\n for beat_at, g in ((0, 0.10), (2, 0.06)):\n for m in ch[2:]:\n add(felt_piano(hz(m + 12), 2.4), t0 + beat_at * beat + 0.01, pan=0.3, gain=g)\n # 弦: 和音の3音を長く\n for m in ch[1:4]:\n add(lowpass(strings(hz(m), d + 0.6), 2500), t0, pan=0.0, gain=0.018)\n # ウッドベース風: 1拍目と3拍目\n for beat_at, m in ((0, ch[0] - 12), (2, ch[0] - 12 + (7 if b % 2 else 0))):\n add(pluck(hz(m), 1.2, bright=0.1), t0 + beat_at * beat, gain=0.5)\n buf[:, 0] = lowpass(buf[:, 0], 6000); buf[:, 1] = lowpass(buf[:, 1], 6000)\n return buf / (np.abs(buf).max() + 1e-9) * 0.8\n\n# ---------- BGM の候補(2026-10-12 エイイチの指示で3種。どれも電子音を避け、楽器に近い音で組む) ----------\nHZ = lambda m: 440 * 2 ** ((m - 69) / 12)\n\n\nclass Loop:\n \"\"\"小節数ぶんの輪(ループ)の入れ物。はみ出した余韻は頭に回すので、つなぎ目が出ない\"\"\"\n def __init__(self, bpm, bars):\n self.beat = 60 / bpm; self.bar = 4 * self.beat\n self.L = int(bars * self.bar * SR); self.buf = np.zeros((self.L, 2))\n\n def add(self, sig, start, pan=0.0, gain=1.0):\n i0 = int(start * SR); idx = (np.arange(len(sig)) + i0) % self.L\n np.add.at(self.buf[:, 0], idx, sig * gain * (1 - max(pan, 0)))\n np.add.at(self.buf[:, 1], idx, sig * gain * (1 + min(pan, 0)))\n\n def out(self, cutoff=6500):\n b = self.buf\n b[:, 0] = lowpass(b[:, 0], cutoff); b[:, 1] = lowpass(b[:, 1], cutoff)\n return b / (np.abs(b).max() + 1e-9) * 0.8\n\n\ndef marimba(f, dur=1.0):\n \"\"\"木の鍵盤(マリンバ)。倍音が 1・3.9・9.2 倍にあり、すぐ消える\"\"\"\n t = t_(dur)\n a = (np.sin(2 * np.pi * f * t) * np.exp(-t / 0.35) + 0.25 * np.sin(2 * np.pi * f * 3.9 * t) * np.exp(-t / 0.06)\n + 0.08 * np.sin(2 * np.pi * f * 9.2 * t) * np.exp(-t / 0.02))\n return a * np.minimum(1, t / 0.002)\n\n\ndef pizz(f, dur=0.7):\n \"\"\"弦をはじく(ピッツィカート)。ギターより短く丸い\"\"\"\n a = pluck(f, dur, bright=0.2)\n return a * np.exp(-t_(dur)[:len(a)] / 0.18)\n\n\ndef shaker(dur=0.09):\n t = t_(dur)\n n = rng.standard_normal(len(t))\n return (n - lowpass(n, 5000)) * np.sin(np.pi * t / dur) ** 2\n\n\ndef bgm_piano():\n \"\"\"1. ピアノ中心・ゆったり(テンポ76)。ピアノの分散和音に弦がうすく重なる。考える場面向き。\n 分散和音はナレーションの声の高さ(男声 100〜300Hz)とぶつからないよう、1オクターブ上で弾く\"\"\"\n lp = Loop(76, 8)\n prog = [[41, 53, 57, 60, 64], [40, 52, 55, 59, 62], [38, 50, 53, 57, 60], [36, 48, 52, 55, 59]] # Fmaj7 Em7 Dm7 Cmaj7\n mel = [72, None, 71, None, 69, None, 67, None] # 小節の頭にだけ置く、ゆっくり下がる音\n for b in range(8):\n ch = prog[b % 4]; t0 = b * lp.bar\n for k, j in enumerate([1, 2, 3, 4, 3, 2, 3, 4]):\n lp.add(felt_piano(HZ(ch[j] + 12), 2.6), t0 + k * lp.beat / 2 + rng.uniform(0, 0.01), pan=0.15, gain=0.09 if k % 2 == 0 else 0.065)\n lp.add(felt_piano(HZ(ch[0]), 3.5), t0, gain=0.12)\n if mel[b]:\n lp.add(felt_piano(HZ(mel[b] + 12), 3.0), t0 + 0.02, pan=-0.2, gain=0.06)\n for m in ch[1:4]:\n lp.add(lowpass(strings(HZ(m), lp.bar + 0.8), 2200), t0, gain=0.014)\n return lp.out(6000)\n\n\ndef bgm_guitar_marimba():\n \"\"\"2. ギターとマリンバ・明るめ(テンポ96)。ギターの刻みに、木の鍵盤の短いフレーズ。フック・導入向き\"\"\"\n lp = Loop(96, 8)\n prog = [[48, 55, 64, 67, 72], [47, 55, 62, 67, 71], [45, 52, 60, 64, 69], [41, 53, 57, 60, 65]] # C G/B Am F\n riff = [(0, 0), (0.75, 2), (1.5, 1), (2.5, 2), (3, 3)] # (拍, 和音の何番目) マリンバ\n for b in range(8):\n ch = prog[b % 4]; t0 = b * lp.bar\n for k, j in enumerate([0, 2, 3, 4, 2, 3, 4, 3]):\n lp.add(pluck(HZ(ch[j]), 1.4, bright=0.4), t0 + k * lp.beat / 2 + rng.uniform(0, 0.006), pan=-0.4, gain=0.2 if k % 2 == 0 else 0.13)\n if b % 2 == 1:\n for at, j in riff:\n lp.add(marimba(HZ(ch[2:][j % 3] + 12)), t0 + at * lp.beat, pan=0.4, gain=0.16)\n for at in (0, 2):\n lp.add(pluck(HZ(ch[0] - 12), 1.1, bright=0.1), t0 + at * lp.beat, gain=0.5)\n for k in range(8):\n lp.add(shaker(), t0 + k * lp.beat / 2 + lp.beat / 4, pan=0.2, gain=0.05 if k % 2 else 0.03)\n return lp.out(4000) # はじく音は高い成分が多いので、丸めて温かくする\n\n\ndef bgm_pizzicato():\n \"\"\"3. 弦のピッツィカートとピアノ・少し先が気になる感じ(テンポ104)。ドキュメンタリーの解説向き\"\"\"\n lp = Loop(104, 8)\n prog = [[45, 57, 60, 64], [41, 57, 60, 65], [36, 55, 60, 64], [43, 55, 59, 62]] # Am F C G\n for b in range(8):\n ch = prog[b % 4]; t0 = b * lp.bar\n for k, j in enumerate([1, 2, 3, 2, 1, 2, 3, 2]):\n lp.add(pizz(HZ(ch[j])), t0 + k * lp.beat / 2, pan=-0.3 if k % 2 else 0.3, gain=0.22 if k % 2 == 0 else 0.15)\n for at, m in ((0, ch[0] - 12), (1.5, ch[0] - 12), (2, ch[0] - 5)):\n lp.add(pizz(HZ(m), 0.9), t0 + at * lp.beat, gain=0.45)\n for m in ch[1:]:\n lp.add(felt_piano(HZ(m + 12), 2.2), t0 + 0.01, pan=0.1, gain=0.05)\n if b >= 4: # 後半だけ弦の和音が入り、少しふくらむ\n for m in ch[1:]:\n lp.add(lowpass(strings(HZ(m), lp.bar + 0.6), 2400), t0, gain=0.012)\n return lp.out(3800)\n\n\nBGMS = {'bgm_1_ピアノ': bgm_piano, 'bgm_2_ギターとマリンバ': bgm_guitar_marimba, 'bgm_3_ピッツィカート': bgm_pizzicato}\n\n\n# ---------- 大きな流れの変わり目の音の候補(2026-10-12 エイイチの指示で3つ) ----------\ndef chapter_marimba():\n \"\"\"ポロロン: マリンバで ド・ミ・ソ と上がる\"\"\"\n out = np.zeros(int(1.6 * SR))\n for k, f in enumerate([523.3, 659.3, 784.0]):\n a = marimba(f, 1.2) * (0.8 if k < 2 else 1.0)\n i = int(k * 0.09 * SR); out[i:i + len(a)] += a[:len(out) - i]\n return norm(lowpass(out, 7000), 0.8)\n\n\ndef chapter_harp():\n \"\"\"シャララン: 弦をはじく音を6つ、ハープのように素早く上がる\"\"\"\n out = np.zeros(int(2.0 * SR))\n for k, m in enumerate([60, 64, 67, 72, 76, 79]):\n a = pluck(440 * 2 ** ((m - 69) / 12), 1.6, bright=0.3) * (0.6 + 0.08 * k)\n i = int(k * 0.055 * SR); out[i:i + len(a)] += a[:len(out) - i]\n return norm(lowpass(out, 6000), 0.8)\n\n\ndef chapter_piano():\n \"\"\"ポーン: やわらかいピアノで ド・ソ・ミ・ソ の和音を1つ(低い音から少しずつずらして弾く)\"\"\"\n out = np.zeros(int(2.2 * SR))\n for k, m in enumerate([48, 55, 64, 67]):\n a = felt_piano(440 * 2 ** ((m - 69) / 12), 2.2) * (1.0 if k == 0 else 0.7)\n i = int(k * 0.025 * SR); out[i:i + len(a)] += a[:len(out) - i]\n return norm(lowpass(out, 5000), 0.8)\n\n\nCHAPTER_CANDS = {'se_chapter_marimba': chapter_marimba, 'se_chapter_harp': chapter_harp, 'se_chapter_piano': chapter_piano}\n\n\ndef chapter_glock():\n \"\"\"キラリン: 鉄琴(グロッケン)で ミ・ソ・ド と上がる。倍音は 1・2.76・5.4 倍(金属の棒の音)\"\"\"\n out = np.zeros(int(1.8 * SR)); t = t_(1.6)\n for k, f in enumerate([1318.5, 1568.0, 2093.0]):\n a = (np.sin(2 * np.pi * f * t) * np.exp(-t / 0.7) + 0.25 * np.sin(2 * np.pi * f * 2.76 * t) * np.exp(-t / 0.15)\n + 0.08 * np.sin(2 * np.pi * f * 5.4 * t) * np.exp(-t / 0.05)) * np.minimum(1, t / 0.001)\n i = int(k * 0.08 * SR); out[i:i + len(a)] += a[:len(out) - i] * (0.7 + 0.15 * k)\n return norm(lowpass(out, 9000), 0.7)\n\n\ndef chapter_swell():\n \"\"\"フワァ: 弦の和音(ド・ミ・ソ・ド)が0.8秒でふくらみ、1秒で消える\"\"\"\n d = 1.9; t = t_(d)\n a = sum(lowpass(strings(440 * 2 ** ((m - 69) / 12), d), 3000) for m in [60, 64, 67, 72])\n shape = np.where(t < 0.8, np.sin(np.pi / 2 * t / 0.8) ** 2, np.exp(-(t - 0.8) / 0.35))\n return norm(a * shape, 0.8)\n\n\ndef chapter_wind():\n \"\"\"シャラン: 細い金属の筒(ウィンドチャイム)が、0.5秒の間に9回ふれあう\"\"\"\n out = np.zeros(int(2.0 * SR)); t = t_(1.4); r = np.random.default_rng(11)\n scale = [1760.0, 1975.5, 2217.5, 2637.0, 2960.0, 3520.0]\n for k in range(9):\n f = scale[r.integers(len(scale))]\n a = (np.sin(2 * np.pi * f * t) + 0.3 * np.sin(2 * np.pi * f * 2.7 * t) * np.exp(-t / 0.2)) * np.exp(-t / 0.6) * np.minimum(1, t / 0.001)\n i = int((k * 0.055 + r.uniform(0, 0.02)) * SR); out[i:i + len(a)] += a[:len(out) - i] * r.uniform(0.5, 1.0)\n return norm(lowpass(out, 10000), 0.6)\n\n\nCHAPTER_CANDS.update({'se_chapter_glock': chapter_glock, 'se_chapter_swell': chapter_swell, 'se_chapter_wind': chapter_wind})\n\n\nif __name__ == '__main__':\n os.makedirs(OUT, exist_ok=True)\n for name, f in [('se_pop', pop), ('se_tick', tick), ('se_whoosh', whoosh), ('se_ding', ding), ('se_rise', rise), ('se_chapter', chapter)]:\n write_wav(os.path.join(OUT, f'{name}.wav'), (f() * 32767).astype(np.int16))\n print(name)\n for name, f in CHAPTER_CANDS.items():\n write_wav(os.path.join(OUT, f'{name}.wav'), (f() * 32767).astype(np.int16))\n print(name)\n if 'bgm3' in sys.argv:\n for name, f in BGMS.items():\n write_wav(os.path.join(OUT, f'{name}.wav'), (f() * 32767).astype(np.int16), ch=2)\n print(name)\n sys.exit()\n if 'warm' not in sys.argv:\n write_wav(os.path.join(OUT, 'bgm_仮.wav'), (bgm_placeholder() * 32767).astype(np.int16), ch=2)\n print('bgm_仮')\n write_wav(os.path.join(OUT, 'bgm_仮_温かみ.wav'), (bgm_warm() * 32767).astype(np.int16), ch=2)\n print('bgm_仮_温かみ')\n</code></pre><h3 id=\"index_0alA5Ix0\">_制作ベース/sound_eleven.py</h3><p>新規。ElevenLabs で BGM と効果音を作る。API キーを使うので Mac で動かす(71行)。</p><pre><code class=\"language-python\">\"\"\"BGM と効果音を ElevenLabs で作る。エイイチの Mac で動かす(API キーを Claude に渡さないため。tts_eleven.py と同じ)。\n\n準備: ターミナルで read -s ELEVENLABS_API_KEY && export ELEVENLABS_API_KEY\n (キーを貼って Enter。画面には出ない)\n\n使い方(動画の作業フォルダで)\n BGM: python3 ../_制作ベース/sound_eleven.py music --name bgm_hook --sec 60 --prompt \"(説明文)\"\n 効果音: python3 ../_制作ベース/sound_eleven.py sfx --name se_pop2 --sec 0.5 --prompt \"(説明文)\"\n --name 保存する名前(sound/{名前}.mp3 になる。sound.py の file にこの名前のパスを書く)\n --sec 長さ(秒)。BGM は 3〜600、効果音は 0.5〜30\n --prompt どんな音か(英語の方が安定する。説明文の候補は 音源_候補.md)\n --count 同じ説明文で何本作るか(既定 1。2 以上だと bgm_hook_1.mp3, bgm_hook_2.mp3…)\n --loop 効果音を切れ目なくループする音にする(環境音など)\nBGM は歌なし(force_instrumental)で作る。\nStarter プランの決まり(2026-10-12 に ElevenLabs の Eleven Music の規約で確認):\n 商用に使える(映画・テレビ・ラジオ・ゲームの一部を除く)。クレジット表記はいらない。\n 作れる曲の長さは月 17 分まで。曲そのものを音楽配信サービスに出すのは禁止(動画の BGM に使うのはこれに当たらない)\n有料プランの間に作った音は、解約したあとも商用に使える(無料プランに戻ってから作った音は使えない)\n\"\"\"\nimport argparse, json, os, sys, time, urllib.request, urllib.error\n\nap = argparse.ArgumentParser()\nap.add_argument('kind', choices=['music', 'sfx'])\nap.add_argument('--name', required=True)\nap.add_argument('--sec', type=float, required=True)\nap.add_argument('--prompt', required=True)\nap.add_argument('--count', type=int, default=1)\nap.add_argument('--loop', action='store_true')\nap.add_argument('--model', default='')\nargs = ap.parse_args()\nkey = os.environ.get('ELEVENLABS_API_KEY')\nif not key:\n sys.exit('ELEVENLABS_API_KEY が入っていません。ターミナルで read -s ELEVENLABS_API_KEY && export ELEVENLABS_API_KEY を実行してから動かしてください')\n\nos.makedirs('sound', exist_ok=True)\nif args.kind == 'music':\n if not 3 <= args.sec <= 600:\n sys.exit('BGM の長さは 3〜600 秒')\n url = 'https://api.elevenlabs.io/v1/music?output_format=mp3_44100_128'\n body = {'prompt': args.prompt, 'music_length_ms': int(args.sec * 1000), 'force_instrumental': True}\nelse:\n if not 0.5 <= args.sec <= 30:\n sys.exit('効果音の長さは 0.5〜30 秒')\n url = 'https://api.elevenlabs.io/v1/sound-generation?output_format=mp3_44100_128'\n body = {'text': args.prompt, 'duration_seconds': args.sec, 'prompt_influence': 0.5}\n if args.loop:\n body['loop'] = True\nif args.model:\n body['model_id'] = args.model\n\nlog = os.path.join('sound', '作った記録.jsonl')\nfor k in range(args.count):\n name = args.name if args.count == 1 else f'{args.name}_{k + 1}'\n path = os.path.join('sound', f'{name}.mp3')\n if os.path.exists(path):\n sys.exit(f'{path} はもうあります(上書きしない)。--name を変えてください')\n req = urllib.request.Request(url, data=json.dumps(body).encode(),\n headers={'xi-api-key': key, 'Content-Type': 'application/json'})\n try:\n with urllib.request.urlopen(req, timeout=600) as r:\n open(path, 'wb').write(r.read())\n extra = {h: r.headers.get(h) for h in ('song-id', 'character-cost') if r.headers.get(h)}\n except urllib.error.HTTPError as e:\n sys.exit(f'{name} で止まりました: {e.code} {e.read().decode(errors=\"ignore\")[:300]}')\n except urllib.error.URLError as e:\n sys.exit(f'{name} で止まりました(ElevenLabs につながらない): {e.reason}')\n with open(log, 'a', encoding='utf-8') as f:\n f.write(json.dumps(dict(file=path, kind=args.kind, sec=args.sec, prompt=args.prompt, loop=args.loop,\n model=args.model or '既定', time=time.strftime('%Y-%m-%d %H:%M'), **extra),\n ensure_ascii=False) + '\\n')\n print(f'できあがり: {path}')\n</code></pre><h3 id=\"index_ZuLtrq6q\">_制作ベース/tts_eleven.py</h3><p>直した。読みを確かめる試しの文だけを作る --try を足した。Mac で動かす(114行)。</p><pre><code class=\"language-python\">\"\"\"ハコブくんの声(ElevenLabs)で、台本を1文ずつ音声にする。エイイチの Mac で動かす(API キーを Claude に渡さないため)。\n\n準備(1回だけ)\n 1. ElevenLabs の有料プランに入る(無料プランの音声は商用に使えない)\n 2. Voice Design で声を作って保存し、その声の Voice ID を控える(Voices の一覧で声を開くと出る)\n 3. ターミナルで API キーを入れる(キーは画面やファイルに残さない): export ELEVENLABS_API_KEY=(キー)\n\n使い方(動画の作業フォルダで。例: 動画/AIエージェントの取り違え/)\n python3 ../_制作ベース/tts_eleven.py --voice VOICE_ID --name 候補A --lines 0-12\n --lines 作る文の番号(0 から。「0-12」「5,8,9」のように書く。省略するとすべて)\n --model eleven_v4(既定。2026-10-12 に決定)/eleven_multilingual_v2/eleven_v3\n --seed 同じ値なら、なるべく同じ読み方になる(既定 7)\n --try 台本ではなく、試しの文を作る(「|」で区切って何文でも。例: --try \"エイイチは…。|栄一は…。\")\n 出力は voice_eleven/{名前}/試し_01.mp3 … と、どの文かの一覧 試し.txt\n出力: voice_eleven/{名前}/{番号}.mp3 と、作った設定の記録 voice_eleven/{名前}/設定.json\nこのあと Claude が tts_import.py で 48kHz の wav にして、口の動きと字幕に使う。\n使う文字数(クレジット)は、文の字数の合計。1本約4,100字。作り直した分も数える\n\"\"\"\nimport argparse, json, os, sys, time, urllib.request, urllib.error\nsys.path.insert(0, '.')\nfrom narration import SEGS\ntry:\n from narration import parse\nexcept ImportError:\n def parse(s):\n return s[0], \"base\", s[1], (s[2] if len(s) > 2 else 0.35)\ntry:\n from narration import ELEVEN_READ # ElevenLabs が読み間違えた語だけ、ここで読みを直す(字幕は変えない)\nexcept ImportError:\n ELEVEN_READ = []\n\nap = argparse.ArgumentParser()\nap.add_argument('--voice', required=True)\nap.add_argument('--name', required=True)\nap.add_argument('--lines', default='')\nap.add_argument('--model', default='eleven_v4') # 2026-10-12 エイイチが v4 に決めた(冒頭14文で v2 と聞き比べ)\nap.add_argument('--seed', type=int, default=7)\nap.add_argument('--stability', type=float, default=0.55)\nap.add_argument('--similarity', type=float, default=0.75)\nap.add_argument('--try', dest='trial', default='')\nargs = ap.parse_args()\nkey = os.environ.get('ELEVENLABS_API_KEY')\nif not key:\n sys.exit('ELEVENLABS_API_KEY が入っていません。ターミナルで export ELEVENLABS_API_KEY=(キー) を実行してから動かしてください')\n\n\ndef nums(spec, n):\n if not spec:\n return list(range(n))\n out = []\n for part in spec.split(','):\n if '-' in part:\n a, b = part.split('-'); out += list(range(int(a), int(b) + 1))\n else:\n out.append(int(part))\n return [i for i in out if 0 <= i < n]\n\n\ndef speak_text(t):\n t = t.replace('|', '').replace('「', '').replace('」', '')\n for a, b in ELEVEN_READ:\n t = t.replace(a, b)\n return t\n\n\ntexts = [parse(s)[2] for s in SEGS]\noutdir = os.path.join('voice_eleven', args.name)\nif args.trial: # 試しの文だけを作る(台本の読みの直し ELEVEN_READ は使わない。書いたとおりに読ませて聞き比べる)\n os.makedirs(outdir, exist_ok=True)\n lines = [t for t in args.trial.split('|') if t]\n with open(os.path.join(outdir, '試し.txt'), 'w', encoding='utf-8') as f:\n for k, t in enumerate(lines, 1):\n f.write(f'試し_{k:02d}: {t}\\n')\n for k, t in enumerate(lines, 1):\n body = {'text': t, 'model_id': args.model, 'seed': args.seed,\n 'voice_settings': {'stability': args.stability, 'similarity_boost': args.similarity}}\n req = urllib.request.Request(\n f'https://api.elevenlabs.io/v1/text-to-speech/{args.voice}?output_format=mp3_44100_128',\n data=json.dumps(body).encode(), headers={'xi-api-key': key, 'Content-Type': 'application/json'})\n try:\n with urllib.request.urlopen(req, timeout=120) as r:\n open(os.path.join(outdir, f'試し_{k:02d}.mp3'), 'wb').write(r.read())\n except urllib.error.HTTPError as e:\n sys.exit(f'試し_{k:02d} で止まりました: {e.code} {e.read().decode(errors=\"ignore\")[:300]}')\n print(f'試し_{k:02d} {t}')\n print(f'できあがり: {outdir} 使った文字数 約{sum(len(t) for t in lines)}字')\n sys.exit(0)\nos.makedirs(outdir, exist_ok=True)\njson.dump(dict(vars(args), time=time.strftime('%Y-%m-%d %H:%M')), open(os.path.join(outdir, '設定.json'), 'w'),\n ensure_ascii=False, indent=1)\nused = 0\nfor i in nums(args.lines, len(SEGS)):\n if not texts[i]:\n continue\n prev = next((speak_text(texts[j]) for j in range(i - 1, -1, -1) if texts[j]), None)\n nxt = next((speak_text(texts[j]) for j in range(i + 1, len(texts)) if texts[j]), None)\n body = {'text': speak_text(texts[i]), 'model_id': args.model, 'seed': args.seed,\n 'voice_settings': {'stability': args.stability, 'similarity_boost': args.similarity}}\n if args.model.startswith('eleven_multilingual'): # 前後の文を渡すと、文のつながりの抑揚が自然になる(対応を確かめたモデルだけ)\n body['previous_text'] = prev\n body['next_text'] = nxt\n req = urllib.request.Request(\n f'https://api.elevenlabs.io/v1/text-to-speech/{args.voice}?output_format=mp3_44100_128',\n data=json.dumps(body).encode(), headers={'xi-api-key': key, 'Content-Type': 'application/json'})\n try:\n with urllib.request.urlopen(req, timeout=120) as r:\n open(os.path.join(outdir, f'{i:03d}.mp3'), 'wb').write(r.read())\n except urllib.error.HTTPError as e:\n sys.exit(f'{i:03d} で止まりました: {e.code} {e.read().decode(errors=\"ignore\")[:300]}')\n except urllib.error.URLError as e:\n sys.exit(f'{i:03d} で止まりました(ElevenLabs につながらない): {e.reason}')\n used += len(body['text'])\n print(f'{i:03d} {body[\"text\"][:30]}')\nprint(f'できあがり: {outdir} 使った文字数 約{used}字')\n</code></pre><h3 id=\"index_HLGEVABj\">_制作ベース/tts_import.py</h3><p>ElevenLabs の mp3 を、書き出し用の 48kHz の wav にする(44行)。</p><pre><code class=\"language-python\">\"\"\"ElevenLabs などで作った1文ずつの音声を、書き出しで使う形(tts/{番号}.wav・48kHz モノラル と tts/index.json)にする。\n使い方: 動画の作業フォルダで python3 <_制作ベース>/tts_import.py voice_eleven/候補A\n- その文の音声がないときは、いまの tts/ の音声(仮音声)をそのまま使う。声の聞き比べで、一部の文だけ差し替えられる\n- 文の前後の無音は 0.05 秒を残して切る(間は narration.py の「後ろの間」で決めるため)\n\"\"\"\nimport json, os, subprocess, sys, wave\nimport numpy as np\nsys.path.insert(0, '.')\nfrom narration import SEGS\ntry:\n from narration import parse\nexcept ImportError:\n def parse(s):\n return s[0], \"base\", s[1], (s[2] if len(s) > 2 else 0.35)\n\nSR = 48000\nsrc = sys.argv[1]\nos.makedirs('tts', exist_ok=True)\nold = {o['i']: o for o in json.load(open('tts/index.json'))} if os.path.exists('tts/index.json') else {}\nout, replaced = [], 0\nfor i, s in enumerate(SEGS):\n scene, face, text, pause = parse(s)\n mp3 = os.path.join(src, f'{i:03d}.mp3')\n d, note = 0.0, ''\n if text and os.path.exists(mp3):\n raw = subprocess.run(['ffmpeg', '-v', 'error', '-i', mp3, '-ac', '1', '-ar', str(SR), '-f', 's16le', '-'],\n capture_output=True, check=True).stdout\n a = np.frombuffer(raw, dtype=np.int16)\n loud = np.where(np.abs(a) > 300)[0]\n if len(loud):\n pad = int(0.05 * SR)\n a = a[max(0, loud[0] - pad):min(len(a), loud[-1] + pad)]\n with wave.open(f'tts/{i:03d}.wav', 'wb') as w:\n w.setnchannels(1); w.setsampwidth(2); w.setframerate(SR); w.writeframes(a.tobytes())\n d, note = len(a) / SR, f'voice:{os.path.basename(src.rstrip(\"/\"))}'\n replaced += 1\n elif text and i in old and old[i]['text'] == text:\n d, note = old[i]['dur'], old[i].get('note', '')\n elif text:\n sys.exit(f'{i:03d} の音声がありません({mp3} も、同じ文の仮音声もない)。tts2.py で仮音声を作ってから取り込む')\n out.append(dict(i=i, scene=scene, face=face, text=text, dur=d, pause=pause, note=note))\njson.dump(out, open('tts/index.json', 'w'), ensure_ascii=False, indent=0)\ntot = sum(o['dur'] + o['pause'] for o in out)\nprint(f'差し替えた文 {replaced} 全体 {int(tot // 60)}:{int(tot % 60):02d}')\n</code></pre><h3 id=\"index_zP2G4G0g\">_制作ベース/description.py</h3><p>新規。概要欄の下書きの {t:文番号} を時間に置きかえ、目次の条件を確かめる(40行)。</p><pre><code class=\"language-python\">\"\"\"YouTube の概要欄を、書き出しの時間に合わせて仕上げる。\n使い方(動画の作業フォルダで、assemble.py のあと)\n python3 <_制作ベース>/description.py (--draft を付けると、【要記入】が残っていても 概要欄_確認用.txt に書く)\n読むもの: 概要欄_下書き.md(目次の時間を {t:文番号} と書く)と timeline.json(assemble.py が書く)\n書くもの: 概要欄.txt(YouTube にそのまま貼る。{t:9} が、9番の文が始まる時間「0:56」などに置きかわる)\n- YouTube の目次(チャプター)は、最初が 0:00 で、3つ以上・それぞれ10秒以上が条件。満たさないときは止まる\n- 概要欄は 5,000 字まで\n- 「【要記入:…】」が残っていると止まる(連絡先や SNS の URL など、エイイチが決めて書く所)\n\"\"\"\nimport json, re, sys\n\nFPS = 30\ntl = {o['i']: o for o in json.load(open('timeline.json'))}\nsrc = open('概要欄_下書き.md', encoding='utf-8').read()\n# 「<!-- -->」で囲んだメモは貼らない\nsrc = re.sub(r'<!--.*?-->\\n?', '', src, flags=re.S)\n\n\ndef ts(i):\n if i not in tl:\n sys.exit(f'{i}番の文が timeline.json にありません(全体を書き出してから動かす)')\n s = int(tl[i]['start'] / FPS)\n return f'{s // 3600}:{s % 3600 // 60:02d}:{s % 60:02d}' if s >= 3600 else f'{s // 60}:{s % 60:02d}'\n\n\nout = re.sub(r'\\{t:(\\d+)\\}', lambda m: ts(int(m.group(1))), src).strip() + '\\n'\n# 目次の条件を確かめる\ntimes = [t for t in re.findall(r'^(\\d+:\\d{2}(?::\\d{2})?) ', out, flags=re.M)]\nsecs = [sum(int(x) * 60 ** k for k, x in enumerate(reversed(t.split(':')))) for t in times]\nif times:\n if secs[0] != 0 or len(secs) < 3 or any(b - a < 10 for a, b in zip(secs, secs[1:])):\n sys.exit(f'目次が YouTube の条件を満たしていません: {times}')\ndraft = '--draft' in sys.argv # 「【要記入】」が残っていても、確かめ用に 概要欄_確認用.txt を書く\nif '【' in out and '要記入' in out and not draft:\n sys.exit('「【要記入】」が残っています: ' + ', '.join(re.findall(r'【要記入[^】]*】', out)))\nif len(out) > 5000:\n sys.exit(f'概要欄が {len(out)} 字あります(5,000 字まで)')\nname = '概要欄_確認用.txt' if draft else '概要欄.txt'\nopen(name, 'w', encoding='utf-8').write(out)\nprint(f'{name} を書きました({len(out)} 字・目次 {len(times)} 個)')\n</code></pre><h3 id=\"index_3UslsPCD\">_制作ベース/thumbnail.py</h3><p>新規。サムネイルを動画と同じ文字と色で、HTML で組んで画像にする(214行)。</p><pre><code class=\"language-python\">\"\"\"YouTube のサムネイルを、動画と同じ文字・色(scene_kit.py)で作る。文字は画像生成 AI に任せず、ここで組む(文字の間違いが起きない)。\n使い方(動画の作業フォルダで)\n python3 <_制作ベース>/thumbnail.py\n読むもの: thumb.py(文言・背景の写真・案の指定)\n書くもの: thumb/{案の名前}.png(1280×720。YouTube の上限 2MB 以下)と thumb/一覧.png(並べて比べる用・スマホの小さい表示の大きさも添える)\n\nthumb.py の書き方\n PHOTO = 'サムネ素材/素材2_実機のアーム.png' # 背景の写真(1920×1080 以上)\n HAKOBU = True # 左下にハコブくんを置く\n LABEL = '本当に?' # ハコブくんの横の小さな札\n KICKER = '人型ロボット出荷 約4倍' # 上の小さな一言(オレンジ)\n LINES = ['ロボットにも', '<span class=\"red\">ChatGPTの瞬間</span>', 'もう来た?']\n VARIANTS = ['split', 'dark', 'bold'] # 作る案(split 紙色と写真/dark 黒と写真/bold 大きなハコブくんと縁取り文字)\n BOLD_LINES = ['人型ロボット出荷 約4倍', 'ChatGPTの瞬間', 'もう来た?'] # bold 案の3行(なければ KICKER と LINES)\n\"\"\"\nimport base64, os, sys, subprocess\nsys.path.insert(0, '.')\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(1, HERE)\nfrom scene_kit import BG, INK, RED, SUB, LOGO\nfrom PIL import Image\nfrom playwright.sync_api import sync_playwright\n\nns = {}\nexec(open('thumb.py', encoding='utf-8').read(), ns)\nos.makedirs('thumb', exist_ok=True)\n\n\ndef data_uri(path):\n ext = os.path.splitext(path)[1].lower().lstrip('.')\n return f\"data:image/{'jpeg' if ext in ('jpg', 'jpeg') else 'png'};base64,\" + base64.b64encode(open(path, 'rb').read()).decode()\n\n\nphoto = data_uri(ns['PHOTO'])\ndef _hakobu_uri():\n \"\"\"ハコブくんの体(右向き)。2026-10-12 から、作り直した体に顔を描いた hakobu/thumb_body_right.png を使う\n (元の体は頭の上が切り抜けておらず、大きく出すと頭がへこんで見えた)。なければ動画と同じ体を使う\"\"\"\n import io\n new = os.path.join(HERE, 'hakobu', 'thumb_body_right.png')\n if os.path.exists(new):\n return data_uri(new)\n from hakobu import Hakobu, FX0, FY0\n H = Hakobu(mode='full')\n eyes = ('open', 'open')\n b = H.body.copy(); H._ring(b, 0.7)\n img = b.crop(H.crop_draw)\n gl, la = H._face_layers(eyes, None, 'mid', 0.0)\n ox, oy = FX0 - H.crop_draw[0], FY0 - H.crop_draw[1]\n img.alpha_composite(gl, (ox, oy)); img.alpha_composite(la, (ox, oy))\n img = img.crop((H.crop[0] - H.crop_draw[0], H.crop[1] - H.crop_draw[1], H.crop[2] - H.crop_draw[0], H.crop[3] - H.crop_draw[1]))\n img = img.transpose(Image.FLIP_LEFT_RIGHT)\n img = img.crop(img.getchannel('A').point(lambda v: 255 if v > 60 else 0).getbbox())\n img.save('thumb/_hakobu.png')\n buf = io.BytesIO(); img.save(buf, 'PNG')\n return 'data:image/png;base64,' + base64.b64encode(buf.getvalue()).decode()\n\n\nhk = _hakobu_uri() if ns.get('HAKOBU', True) else None\nkicker, lines, label = ns.get('KICKER', ''), ns['LINES'], ns.get('LABEL', '')\npos = ns.get('PHOTO_POS', '20% 50%') # 写真のどこを見せるか(CSS の background-position)\n\nBASE = f\"\"\"*{{margin:0;padding:0;box-sizing:border-box}}\nhtml,body{{width:1920px;height:1080px;overflow:hidden;font-family:'Noto Sans CJK JP',sans-serif;color:{INK}}}\n.red{{color:{RED}}}\n.kicker{{display:inline-block;font-size:56px;font-weight:900;color:#fff;background:{RED};padding:10px 30px;border-radius:12px;letter-spacing:.02em}}\n.main{{font-weight:900;line-height:1.16;letter-spacing:-.02em}}\n.hk{{position:absolute;left:24px;bottom:-90px;height:430px}}\n.label{{position:absolute;left:350px;bottom:250px;background:#fff;color:{INK};font-size:48px;font-weight:900;padding:12px 26px;border-radius:14px;box-shadow:0 6px 24px rgba(0,0,0,.25)}}\n.label:after{{content:'';position:absolute;left:-22px;bottom:22px;border:14px solid transparent;border-right:14px solid #fff}}\n.brand{{position:absolute;right:56px;top:44px;height:40px}}\n\"\"\"\n\n\ndef hakobu_html():\n if not hk:\n return ''\n lab = f'<div class=\"label\">{label}</div>' if label else ''\n return f'<img class=\"hk\" src=\"{hk}\">{lab}'\n\n\ndef html(kind):\n k = f'<div class=\"kicker\">{kicker}</div>' if kicker else ''\n body = '<br>'.join(lines)\n if kind == 'split': # 左に動画と同じ紙色の面と文字、右に写真\n return f\"\"\"<style>{BASE}\nbody{{background:{BG}}}\n.ph{{position:absolute;right:0;top:0;width:820px;height:1080px;background:url({photo}) {pos}/cover}}\n.txt{{position:absolute;left:96px;top:84px;width:1060px}}\n.main{{font-size:136px;margin-top:28px}}\n</style><div class=\"ph\"></div><div class=\"txt\">{k}<div class=\"main\">{body}</div></div>{hakobu_html()}<img class=\"brand\" src=\"{LOGO}\">\"\"\"\n if kind == 'card': # 写真を全面に敷き、動画の名前テロップと同じ薄い紙色の札に文字\n return f\"\"\"<style>{BASE}\nbody{{background:url({photo}) {pos}/cover}}\n.txt{{position:absolute;left:70px;top:70px;background:rgba(245,245,240,.95);border-radius:24px;padding:44px 60px 52px}}\n.main{{font-size:124px;margin-top:24px}}\n</style><div class=\"txt\">{k}<div class=\"main\">{body}</div></div>{hakobu_html()}\"\"\"\n if kind == 'dark': # 案B: 写真を右へずらして敷き、左は黒に溶かして白い文字。ハコブくんは左下で大きめ、右(アーム)を向く\n return f\"\"\"<style>{BASE}\nbody{{background:#111}}\n.ph{{position:absolute;left:720px;top:0;width:1920px;height:1080px;background:url({photo}) 0 50%/cover}}\n.shade{{position:absolute;inset:0;background:linear-gradient(90deg,rgba(13,13,13,1) 0%,rgba(13,13,13,1) 38%,rgba(13,13,13,.6) 48%,rgba(13,13,13,0) 64%)}}\n.txt{{position:absolute;left:96px;top:70px;width:1150px}}\n.main{{font-size:122px;margin-top:22px;color:#fff;text-shadow:0 4px 18px rgba(0,0,0,.6)}}\n.main .red{{color:#E25A2C}}\n.hk2{{position:absolute;left:40px;bottom:-120px;height:540px}}\n.label{{left:380px;bottom:300px}}\n</style><div class=\"ph\"></div><div class=\"shade\"></div><div class=\"txt\">{k}<div class=\"main\">{body}</div></div><img class=\"hk2\" src=\"{hk}\"><div class=\"label\">{label}</div>\"\"\"\n if kind == 'dark2': # 案B2: ハコブくんを左に大きく(画面の高さいっぱい)。文字は右上、写真は右下\n return f\"\"\"<style>{BASE}\nbody{{background:#111}}\n.ph{{position:absolute;left:560px;top:120px;width:1920px;height:1080px;background:url({photo}) 0 50%/cover}}\n.shade{{position:absolute;inset:0;background:linear-gradient(90deg,rgba(13,13,13,1) 0%,rgba(13,13,13,1) 30%,rgba(13,13,13,0) 46%),linear-gradient(180deg,rgba(13,13,13,1) 0%,rgba(13,13,13,.92) 30%,rgba(13,13,13,0) 58%)}}\n.txt{{position:absolute;right:70px;top:60px;text-align:right}}\n.kicker{{font-size:52px}}\n.main{{font-size:128px;margin-top:20px;color:#fff;text-shadow:0 4px 18px rgba(0,0,0,.7)}}\n.main .red{{color:#E25A2C}}\n.hk2{{position:absolute;left:30px;bottom:-110px;height:900px}}\n.label{{left:90px;top:40px;bottom:auto}}\n.label:after{{left:60px;bottom:-26px;border:14px solid transparent;border-top:14px solid #fff}}\n</style><div class=\"ph\"></div><div class=\"shade\"></div><img class=\"hk2\" src=\"{hk}\"><div class=\"label\">{label}</div><div class=\"txt\">{k}<div class=\"main\">{body}</div></div>\"\"\"\n if kind == 'dark3': # 案B3: 上に横いっぱいの文字(黒地)、下に写真。ハコブくんは左下で大きく、アームを見る\n L = ns.get('BOLD_LINES', [kicker] + lines)\n return f\"\"\"<style>{BASE}\nbody{{background:#111}}\n.ph{{position:absolute;left:470px;top:380px;width:1620px;height:911px;background:url({photo}) 0 40%/cover}}\n.shade{{position:absolute;left:0;right:0;top:380px;height:220px;background:linear-gradient(180deg,#111 0%,rgba(17,17,17,0) 100%)}}\n.shadeL{{position:absolute;left:470px;top:380px;width:260px;bottom:0;background:linear-gradient(90deg,#111 0%,rgba(17,17,17,0) 100%)}}\n.top{{position:absolute;left:0;right:0;top:46px;text-align:center;color:#fff;font-weight:900;line-height:1.12}}\n.top .a{{font-size:78px}} .top .b{{font-size:150px;letter-spacing:-.02em}} .top .b .red{{color:#E25A2C}}\n.hk2{{position:absolute;left:40px;bottom:-90px;height:700px}}\n.label{{left:360px;bottom:520px}}\n</style><div class=\"ph\"></div><div class=\"shade\"></div><div class=\"shadeL\"></div>\n<div class=\"top\"><div class=\"a\"><span style=\"background:{RED};padding:4px 26px;border-radius:12px;font-size:60px\">{L[0]}</span></div>\n<div class=\"b\"><span class=\"red\">{L[1]}</span> {L[2]}</div></div><img class=\"hk2\" src=\"{hk}\"><div class=\"label\">{label}</div>\"\"\"\n if kind in ('photo', 'photo2', 'photo3'):\n # 2026-10-12 エイイチの指示: 背景は全部写真。文字は大きく、写真の上に置くので黒い縁を付ける(上の札は縁なし)\n # photo : 文字は左上、ハコブくんは右下で左(アーム)を向く\n # photo2: 文字は右上、ハコブくんは右下で左を向く\n # photo3: 文字は右上、ハコブくんは左下の隅で右(アーム)を向く。写真は PHOTO3(ハコブくんがアームと箱に重ならないコマ)\n L = [kicker] + lines\n right = kind != 'photo'\n left_hk = kind == 'photo3'\n ph = data_uri(ns['PHOTO3']) if left_hk and ns.get('PHOTO3') else photo\n OUT = f\"-webkit-text-stroke:24px {INK};paint-order:stroke fill;text-shadow:0 6px 18px rgba(0,0,0,.55)\"\n if left_hk:\n HK = f\".hk3{{{{position:absolute;left:10px;bottom:-190px;height:{ns.get('HK_H', 600)}px}}}}\"\n LB = f\"\"\".label{{{{left:{ns.get('HK_H', 600) * 0.62 + 10:.0f}px;bottom:300px;border:6px solid {INK};font-size:54px}}}}\n.label:after{{{{left:-30px;bottom:24px;border:16px solid transparent;border-right:16px solid {INK}}}}}\"\"\"\n else:\n HK = f\".hk3{{{{position:absolute;right:{'30' if right else '60'}px;bottom:-170px;height:{'520' if right else '640'}px;transform:scaleX(-1)}}}}\"\n LB = f\"\"\".label{{{{left:auto;right:{'330' if right else '420'}px;bottom:{'190' if right else '360'}px;border:6px solid {INK};font-size:54px}}}}\n.label:after{{{{left:auto;right:-30px;bottom:24px;border:16px solid transparent;border-left:16px solid {INK}}}}}\"\"\"\n HK, LB = HK.replace('{{', '{').replace('}}', '}'), LB.replace('{{', '{').replace('}}', '}')\n return f\"\"\"<style>{BASE}\nbody{{background:url({ph}) {ns.get('PHOTO_POS_FULL', '50% 50%')}/cover}}\n.dim{{position:absolute;inset:0;background:linear-gradient({'200deg' if right else '160deg'},rgba(0,0,0,.45) 0%,rgba(0,0,0,.15) 45%,rgba(0,0,0,0) 70%)}}\n.txt{{position:absolute;{'right:56px;text-align:right' if right else 'left:60px'};top:40px;font-weight:900;color:#fff;line-height:1.1;letter-spacing:-.02em}}\n.kick{{display:inline-block;font-size:66px;background:{RED};color:#fff;padding:8px 30px 12px;border-radius:14px;letter-spacing:.02em;box-shadow:0 6px 18px rgba(0,0,0,.35)}}\n.l1{{font-size:150px;margin-top:22px;{OUT}}}\n.l2{{font-size:{'200' if right else '212'}px;{OUT}}}\n.l2 .red{{color:#F0602C}}\n.l3{{font-size:{'170' if right else '176'}px;{OUT}}}\n{HK}\n{LB}\n</style><div class=\"dim\"></div>\n<div class=\"txt\"><div class=\"kick\">{L[0]}</div><div class=\"l1\">{L[1]}</div><div class=\"l2\">{L[2]}</div><div class=\"l3\">{L[3]}</div></div>\n<img class=\"hk3\" src=\"{hk}\"><div class=\"label\">{label}</div>\"\"\"\n if kind == 'bold': # ChatGPT 案の構図(左に大きなハコブくんと吹き出し、右に縁取りの太い文字)を、本物の写真と動画の文字で作る\n L = ns.get('BOLD_LINES', [kicker] + lines)\n rows = ''.join(f'<div class=\"r{i}\">{t}</div>' for i, t in enumerate(L))\n return f\"\"\"<style>{BASE}\nbody{{background:#151515}}\n.ph{{position:absolute;left:700px;top:-30px;width:2040px;height:1148px;background:url({photo}) 0 50%/cover}}\n.fade{{position:absolute;inset:0;background:linear-gradient(90deg,#151515 0%,#151515 36%,rgba(21,21,21,0) 50%),linear-gradient(180deg,rgba(0,0,0,.35) 0%,rgba(0,0,0,0) 55%)}}\n.txt{{position:absolute;right:50px;top:70px;text-align:right;font-weight:900;line-height:1.08;letter-spacing:-.01em;color:#fff;\n -webkit-text-stroke:26px {INK};paint-order:stroke fill}}\n.r0{{font-size:92px}} .r1{{font-size:156px;color:{RED};margin-top:14px}} .r2{{font-size:190px;margin-top:6px}}\n.r1 .red{{color:{RED}}}\n.big{{position:absolute;left:30px;bottom:-50px;height:760px}}\n.bubble{{position:absolute;left:110px;top:70px;background:#fff;border:8px solid {INK};border-radius:22px;padding:14px 34px;font-size:76px;font-weight:900;color:{INK}}}\n.bubble:after{{content:'';position:absolute;left:150px;bottom:-44px;border:22px solid transparent;border-top:22px solid {INK}}}\n</style><div class=\"ph\"></div><div class=\"fade\"></div><img class=\"big\" src=\"{hk}\"><div class=\"bubble\">{label}</div><div class=\"txt\">{rows}</div>\"\"\"\n raise SystemExit(f'案の名前が分かりません: {kind}')\n\n\nouts = []\nwith sync_playwright() as p:\n b = p.chromium.launch()\n pg = b.new_page(viewport={'width': 1920, 'height': 1080})\n for kind in ns.get('VARIANTS', ['split', 'card', 'dark']):\n pg.set_content('<!doctype html><html><head><meta charset=\"utf-8\"></head><body>' + html(kind) + '</body></html>')\n pg.wait_for_timeout(300)\n big = f'thumb/_{kind}_1920.png'\n pg.screenshot(path=big)\n im = Image.open(big).convert('RGB').resize((1280, 720), Image.LANCZOS)\n out = f'thumb/{kind}.png'\n im.save(out, optimize=True)\n if os.path.getsize(out) > 2_000_000:\n out = f'thumb/{kind}.jpg'; im.save(out, quality=92)\n os.remove(big)\n outs.append((kind, out))\n b.close()\n\n# 一覧: 原寸の半分と、スマホの一覧で出る大きさ(幅 168px 前後)を並べる\nW = 640 + 24 + 168\nsheet = Image.new('RGB', (W, len(outs) * (360 + 24)), (30, 30, 30))\nfor k, (kind, out) in enumerate(outs):\n im = Image.open(out)\n y = k * (360 + 24)\n sheet.paste(im.resize((640, 360), Image.LANCZOS), (0, y))\n sheet.paste(im.resize((168, 94), Image.LANCZOS), (640 + 24, y))\nsheet.save('thumb/一覧.png')\nfor kind, out in outs:\n print(kind, out, f'{os.path.getsize(out) / 1e6:.2f}MB')\n</code></pre><h3 id=\"index_TxNsgURJ\">_制作ベース/hakobu/make_new_face.py</h3><p>新規。作り直した体の画面に、動画と同じ描き方の顔を写す(64行)。</p><pre><code class=\"language-python\">\"\"\"新しい体(cut_new.png)のディスプレイに、動画と同じ顔を描く。サムネイル用(2026-10-12)\n2026-10-12 エイイチの指摘「顔が変(口と目の配置や大きさ)」で作り直し:\n 自前で描いた目と口をやめ、動画の hakobu.py と同じ描き方(eye_mask / mouth_mask)で描いた顔を、\n 動画のディスプレイ(SCREEN_QUAD を長方形にならしたもの)から新しい体のディスプレイ(screen_quad_new.npy)へ写す。\n 画面の大きさに対する目と口の大きさ・位置は、動画のハコブくんとそのまま同じになる\n使い方: python3 make_new_face.py [目 口] 例: python3 make_new_face.py open mid(既定)\n\"\"\"\nimport os, sys\nimport numpy as np, cv2\nfrom PIL import Image\nHERE = os.path.dirname(os.path.abspath(__file__))\nsys.path.insert(0, os.path.dirname(HERE))\nimport hakobu as hk\n\n\ndef face_on_new_body(eyes=('open', 'open'), mouth='mid', out='body_new_face.png'):\n im = np.array(Image.open(os.path.join(HERE, 'cut_new.png'))).astype(np.float32)\n q = np.load(os.path.join(HERE, 'screen_quad_new.npy')).astype(np.float32) # 左上・右上・右下・左下\n SS, FX0, FY0, FX1, FY1 = hk.SS, hk.FX0, hk.FY0, hk.FX1, hk.FY1\n hh, ww = (FY1 - FY0) * SS, (FX1 - FX0) * SS\n # 1. 動画と同じ描き方で、顔を画面での向きに描く\n white = np.zeros((hh, ww), np.uint8)\n for kind, box, side in ((eyes[0], hk.V_EYE_LEFT, -1), (eyes[1], hk.V_EYE_RIGHT, 1)):\n em = np.zeros_like(white)\n hk.eye_mask(em, kind, box, side, 0.0)\n white = np.maximum(white, em)\n blue = hk.mouth_mask(mouth)\n la = np.zeros((hh, ww, 4), np.float32)\n la[white > 0, :3] = hk.WHITE\n la[..., 3] = white\n la[blue > 0, :3] = hk.BLUE\n la[..., 3] = np.maximum(la[..., 3], blue)\n la = np.ascontiguousarray(la[:, ::-1]) # 動画と同じく反転して、体の元の向きに戻す\n # 2. 動画のディスプレイをならした長方形(hakobu.py の flat)→ 新しい体のディスプレイ\n S = hk.SCREEN_QUAD\n c = S.mean(0)\n w_ = (np.linalg.norm(S[1] - S[0]) + np.linalg.norm(S[2] - S[3])) / 2\n h_ = (np.linalg.norm(S[3] - S[0]) + np.linalg.norm(S[2] - S[1])) / 2\n flat = np.float32([[c[0] - w_ / 2, c[1] - h_ / 2], [c[0] + w_ / 2, c[1] - h_ / 2],\n [c[0] + w_ / 2, c[1] + h_ / 2], [c[0] - w_ / 2, c[1] + h_ / 2]])\n src = (flat - np.float32([FX0, FY0])) * SS\n M = cv2.getPerspectiveTransform(src, q)\n h, w = im.shape[:2]\n a = la[..., 3:4] / 255\n pre = np.concatenate([la[..., :3] * a, a * 255], 2)\n wl = cv2.warpPerspective(pre, M, (w, h), flags=cv2.INTER_AREA)\n aa = wl[..., 3:4] / 255\n rgb = np.where(aa > 1e-4, wl[..., :3] / np.maximum(aa, 1e-4), 0)\n # 3. 目のまわりのにじむ光(動画と同じ色。体の大きさに合わせてぼかしを広げる)\n scale = np.linalg.norm(q[1] - q[0]) / w_\n eye_a = aa[..., 0] * (rgb[..., 0] > 200)\n glow = cv2.GaussianBlur(eye_a, (0, 0), 2.6 * scale) * 0.75\n for col, al in ((np.float32([205, 225, 255]), glow[..., None]), (rgb, aa)):\n im[..., :3] = im[..., :3] * (1 - al) + col * al\n Image.fromarray(im.clip(0, 255).astype(np.uint8)).save(os.path.join(HERE, out))\n return out\n\n\nif __name__ == '__main__':\n args = sys.argv[1:]\n if len(args) == 2:\n print(face_on_new_body((args[0], args[0]), args[1]))\n else:\n print(face_on_new_body())\n</code></pre><h3 id=\"index_zd9AxR5a\">フィジカルAIの現在地/narration.py</h3><p>台本。1文ごとに(場面、表情、字幕、後ろの間)。読み替えの表 READ と ELEVEN_READ もここ(186行)。</p><pre><code class=\"language-python\"># -*- coding: utf-8 -*-\n# 台本「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」ハコブくん版\n# もとは DAY330 の試作(codex の敵対的検証を反映した版)。2026-10-12 に、ハコブくんが話す形(エイイチは三人称)に直した。\n# 変えたのは、一人称→三人称、自己紹介(確定した文言)、見立ての入り方、NEO の確認日(10月12日)、アウトロ、1文ごとの表情。事実と数字は変えていない\n# 各要素: (場面の名前, 表情, 字幕の文, 後ろの間[秒]) 間を省略すると 0.35 秒\n# 字幕の文の中の「|」: 読み上げは1文のまま、字幕だけをそこで次の枚に替える\nFACES = (\"base\", \"smile\", \"surprise\", \"doubt\", \"think\", \"trouble\", \"joy\", \"wink\")\n\nSEGS = [\n ('hook_ship', 'base', '調査会社Counterpointの推計では、今年の上半期に出荷された人型ロボットは、'),\n ('hook_ship', 'base', '世界で2万2000台を超えました。前の年の同じ時期の、およそ4倍です。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('hook_forecast', 'surprise', '年末までには、5万台を超えると予想されています。'),\n ('hook_forecast', 'base', '家事をする人型ロボットの予約販売も、去年の秋から始まっています。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('hook_question', 'base', 'こういうニュースを見ると、ChatGPTが出たときみたいに、'),\n ('hook_question', 'doubt', 'ロボットも一気に賢くなって、家に来るのはもうすぐ、って感じませんか。', 1.8), # 大きな流れの変わり目の間\n ('title', 'smile', 'Pixels to Motors。案内役のハコブくんです。'),\n ('title', 'base', 'この番組では、フィジカルAIを、現場で使える形にするまでを解説します。'),\n ('title_eiichi', 'base', 'エイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。|その中で分かったことと、そこから考えた見立てを、ハコブくんがお伝えします。', 1.8), # 大きな流れの変わり目の間\n ('question', 'surprise', '今日の問いは、ロボットにもChatGPTのような瞬間は、もう来たのか、です。'),\n ('question', 'base', 'ここでいうChatGPTの瞬間は、専門家じゃなくても、'),\n ('question', 'base', 'いろんな仕事にすぐ使えるようになる転換点、という意味です。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('define', 'base', 'フィジカルAIっていうのは、現実の世界を認識して、行動するAIのことです。'),\n ('define', 'base', '自動運転もここに入りますが、'),\n ('define', 'base', '今日は主に、アームや人型ロボットが物を扱う話に絞ります。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('answer', 'base', '先に結論を言っちゃうと、写真を見せて判断させる入口は、ぐっと簡単になりました。'),\n ('answer', 'trouble', 'でも、物を確実に動かすところは、まだ手間がかかるんですよ。'),\n ('answer', 'base', 'その手間は大きく2つあります。'),\n ('answer', 'base', '動作と結びついたデータが集めにくいこと。そして、機体や環境とのすり合わせです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('stance', 'base', '先に言っておくと、フィジカルAIはまだ使えない、と言いたいわけじゃないんです。'),\n ('stance', 'base', 'どこまで来ていて、どこで手間がかかっているのかを、分けて見たいんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('toc', 'base', '大きく3つあります。'),\n ('toc', 'base', '出荷が4倍になったロボットは、何をしているのか。'),\n ('toc', 'base', 'エイイチの机の上のロボットアームで、何が簡単で、何が難しかったのか。'),\n ('toc', 'base', 'そして、データの壁はどれくらい高くて、誰がどう越えようとしているのか。'),\n ('toc', 'base', '最後に、エイイチの見立てを話します。', 1.8), # 大きな流れの変わり目の間\n ('c1', 'base', '1つ目、出荷が4倍になったロボットは、何をしているのか、です。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('use', 'base', 'Counterpointの推計では、出荷された人型ロボットの6割以上が、'),\n ('use', 'base', '展示やイベントで動きを見せる用途と、研究やデータ生産の用途なんですよ。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('datacollect', 'base', 'データ生産っていうのは、ロボットが学ぶための動きの記録を作ることです。'),\n ('datacollect', 'base', '人が遠隔で動かして記録するやり方も、自律で動いた記録を残すやり方もあります。'),\n ('datacollect', 'doubt', 'ただ、研究向けの台数が全部データ集めに使われる、という意味ではありません。'),\n ('datacollect', 'base', 'それでも、用途の分類に「データ生産」という項目があること自体が、'),\n ('datacollect', 'base', '今の段階をよく表していると思います。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('conflict', 'base', '一方で、別の調査会社のSmart Analytics Globalは、'),\n ('conflict', 'base', '同じ半年の出荷を約1万9100台と推計していて、'),\n ('conflict', 'base', '産業・商業用途が7割を超えた、としています。'),\n ('conflict', 'base', '総数も分類も違うので、この2つは、単純な裏表としては比べられません。'),\n ('conflict', 'doubt', 'それに、出荷台数は、そのまま自律的に働いている台数でもないんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('china', 'surprise', 'もう1つ、SAGの推計では、出荷の97%以上が中国のメーカーです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('neo', 'base', '家のほうも見てみます。'),\n ('neo', 'base', 'アメリカの1Xという会社は、家事をする人型ロボット、NEOの予約を、去年の10月に始めました。'),\n ('neo', 'base', '2万ドルの早期購入枠は、今年配送の予定で、月499ドルのプランは、その後です。'),\n ('neo', 'base', '最初は基本的な自律機能だけで、知らない作業は、'),\n ('neo', 'base', '予約した時間に、1Xの担当者が遠隔で手伝う仕組みなんですよ。'),\n ('neo', 'base', '10月12日の時点で、一般のお客さんへの配送開始を示す公式発表は、'),\n ('neo', 'base', 'エイイチが確認した範囲では、見つかっていません。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('neo_point', 'surprise', 'これ、ちょっと考えさせられませんか。'),\n ('neo_point', 'think', '売っているのはロボットだけじゃなくて、遠隔で手伝う人と、'),\n ('neo_point', 'think', 'そこで動きを覚えていく仕組みごとなんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('bridge1', 'base', '工場でも家でも、使いながらデータを集める動きが出てきているんですよ。'),\n ('bridge1', 'doubt', 'じゃあ、物を動かすのは、何がそんなに大変なのか。'),\n ('bridge1', 'base', 'まず、エイイチの机の上の話をします。', 1.8), # 大きな流れの変わり目の間\n ('c2', 'base', '2つ目、エイイチの机の上のロボットアームで、何が簡単で、何が難しかったのか、です。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('v_real', 'base', 'ここは、エイイチ自身の話から始めます。'),\n ('v_real', 'base', 'エイイチは自宅の机で、mechArm 270という小さなロボットアームを動かしています。'),\n ('v_real', 'base', '赤いブロックを掴んで、箱に置く。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('trials', 'base', 'それだけの作業を、失敗も含めて650回以上試して、全部台帳に残しています。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('easy', 'smile', 'まず、簡単だったほうから話します。掴めたかどうかの判定です。'),\n ('easy', 'smile', 'iPhoneで撮った写真をAIに見せて、「ブロックを掴めていますか」と聞くと、'),\n ('easy', 'joy', 'はい、いいえで答えが返ってくる。この入口は、驚くほど簡単でした。'),\n ('easy', 'doubt', 'ただ、答えが返ることと、毎回正しいことは別です。'),\n ('easy', 'smile', 'どこまで任せられるかは、失敗例も含めて確かめる必要があります。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('hard', 'trouble', '難しかったのは、動かすほうでした。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gap', 'base', 'ロボットアームの爪先がどこに来るかは、普通は関節の角度から計算で出します。'),\n ('gap', 'base', 'ところが、ikpyという計算ツールで出した爪先の位置と、', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('v_twin', 'base', 'パソコンの中に作ったアームの分身、デジタルツインで動かした位置が、', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gap', 'surprise', 'エイイチの設定では、同じ関節の角度なのに、30cm以上食い違ったんです。'),\n ('gap', 'doubt', 'これは、アームが30cm外れた、という意味ではありません。'),\n ('gap', 'base', '計算ツールには、グリッパーの形も、デジタルツインの物理も入っていなくて、爪先の位置の決め方そのものが違っていたんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('v_scan', 'base', 'そこでエイイチは、分身の中でアームを動かして、'),\n ('v_scan', 'base', '関節の角度と爪先の位置の対応表を作りました。'),\n ('v_scan', 'base', 'これは計算の表で、実機で全部の点を測ったわけではありません。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('silent', 'trouble', 'もう1つ困ったのが、黙る失敗です。'),\n ('silent', 'trouble', 'エイイチの機体とソフトの組み合わせでは、肩の関節に、90度ちょっとを超える値を送ると、'),\n ('silent', 'trouble', 'エラーが出ないのに、アームが動かないことがありました。'),\n ('silent', 'trouble', 'ログを1行ずつ見て、やっと気づきました。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('point2', 'surprise', 'でも、ポイントはここなんです。'),\n ('point2', 'think', 'この2つは、AIの学習データが足りない、という話とは別です。'),\n ('point2', 'think', 'モデルや制御が、エイイチの機体と噛み合っているかを確かめる、すり合わせの仕事でした。'),\n ('point2', 'think', 'ほかの環境で学んだことを持ってくる研究も進んでいます。'),\n ('point2', 'think', 'それでも、エイイチの構成でちゃんと動くかは、ここで確かめるしかなかったんです。', 0.6),\n ('point2', 'think', 'じゃあ、もう1つの手間、データのほうは、業界全体でどうなっているのか。', 1.8), # 大きな流れの変わり目の間\n ('c3', 'base', '3つ目、データの壁はどれくらい高くて、誰がどう越えようとしているのか、です。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('years', 'base', 'カリフォルニア大学バークレー校のロボット研究者、'),\n ('years', 'base', 'ケン・ゴールドバーグさんが、去年、こんな概算を紹介しています。'),\n ('years', 'base', '画像と言葉を扱うAIの学習資料を、'),\n ('years', 'base', '人が読んだり見たりする時間に換算すると、約10万年規模になる。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('wall', 'trouble', 'それに比べて、ロボットが手を動かすための、動作と結びついた経験は、集めにくい。'),\n ('wall', 'trouble', 'ゴールドバーグさんは、これを「10万年のデータの壁」と呼んでいます。'),\n ('wall', 'trouble', 'ただ、ロボットにも同じ量が必要だと分かったわけではありません。'),\n ('wall', 'trouble', '集めにくい、という問題提起なんです。', 0.6),\n ('wall', 'doubt', 'ここで、じゃあロボットにChatGPTの瞬間は当分来ない、'),\n ('wall', 'smile', 'と言いたくなるじゃないですか。でも、ちょっと待ってほしいんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gen', 'base', 'アメリカのGeneralistという会社は、人が手で作業する様子を、'),\n ('gen', 'base', '安い装着型の機材で記録して集めています。'),\n ('gen', 'surprise', '会社の発表では、去年の11月に27万時間、今年の4月に50万時間を超えました。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gen2', 'base', 'さらに8月には、簡単で短い作業なら、'),\n ('gen2', 'base', '数秒の実演を見せるだけで、新しい作業をこなす例も発表しています。'),\n ('gen2', 'doubt', 'ただ、本人たちも、作業は簡単で短く、成功率はまだ控えめだと書いています。'),\n ('gen2', 'base', 'それでも、「動かすには毎回大量の専用データが必要」とは、言い切れなくなってきました。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('compare', 'think', 'ここで1つ、注意があります。'),\n ('compare', 'base', '作業を記録した時間と、10万年という数字は、そのまま比べられません。'),\n ('compare', 'base', '何倍足りないかより、いろんな場面の経験を、どれだけ広く集められるかが問題なんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('dirt', 'think', 'つまり、壁はまだ高い。でも、壁の前でみんなが土を積み始めている。'),\n ('dirt', 'think', '1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、'),\n ('dirt', 'think', 'Generalistが人の手作業を記録しているのも、同じ壁に向かっている動きなんです。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('notall', 'smile', 'ただ、ここで「なんだ、まだ全然ダメなのか」で'),\n ('notall', 'smile', '片付けてしまうのも違うんです。'),\n ('notall', 'smile', '写真を見せて判断させる入口は、エイイチの机の上でも、もう普通に使えています。', 0.6),\n ('notall', 'doubt', 'じゃあ、この2つが混ざっている今、ロボットのニュースの何を見ればいいのか。', 1.8), # 大きな流れの変わり目の間\n ('c4', 'base', 'ここからは、エイイチの見立てです。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('q', 'surprise', 'ロボットのニュースを見たときの問いが、2つに変わるんですよ。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('q1', 'base', '1つは、そのロボットは自律で動いているのか、人が遠隔で動かしているのか、です。'),\n ('q1', 'base', 'デモ動画を見るときは、自律か遠隔操作か、'),\n ('q1', 'base', '再生速度や、人が手を出した条件が書かれているかを見ます。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('q2', 'base', 'もう1つは、その仕事は「見て判断する」で済むのか、'),\n ('q2', 'base', '「触れて動かす」精度が要るのか、です。'),\n ('q2', 'base', '棚の商品が足りているかを見るのは、前者です。'),\n ('q2', 'base', '柔らかい物を潰さずに掴むのは、後者です。'),\n ('q2', 'base', '前者は、手軽に試せるところまで来ています。ただ、どこまで任せられるかは、現場で確かめる必要がある。'),\n ('q2', 'base', '後者は、その現場の機体と環境に合わせて、すり合わせる仕事が、まだ残っています。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('gofe', 'base', 'ゴールドバーグさんは、昔ながらのエンジニアリングと学習を組み合わせて、'),\n ('gofe', 'base', '現場で役に立つ仕事をしながら、データを集めていくのが近道だ、と言っています。'),\n ('gofe', 'base', 'エイイチの机の上も、AIと従来の制御を組み合わせる点では通じています。'),\n ('gofe', 'base', 'ただ、現場で働きながらデータが増える仕組みとは、まだ規模も段階も違います。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('motion', 'base', '作る側から見ると、もう1つあります。'),\n ('motion', 'base', 'データの話と並行して、もう1つ要るのが「どう動くか」の設計です。'),\n ('motion', 'base', '例えばロボットが人に物を渡すとき、'),\n ('motion', 'base', '何秒待って手を開けば、人は安心して受け取れるのか。'),\n ('motion', 'base', 'これは、成功か失敗かの判定だけでは出てこない。'),\n ('motion', 'base', '人が安心できる動きを設計する仕事も、同時に必要だと、エイイチは見ています。', 1.8), # 大きな流れの変わり目の間\n ('answer2', 'base', 'じゃあ、冒頭の問いに答えますね。', 0.4),\n ('answer2', 'base', 'エイイチの机の上では、写真を見せて判断させる入口は、ずっと簡単になりました。'),\n ('answer2', 'base', '一方、物を確実に動かすには、機体や環境とのすり合わせが、まだ必要でした。'),\n ('answer2', 'base', 'その負担を減らす研究は、急速に進んでいます。'),\n ('answer2', 'trouble', 'でも、いろんな家で、安全に家事を任せられる転換点まで来たかは、まだ判断できません。', 1.2), # 場面の切り替えの前の一拍(ホワーンの前)\n ('action', 'base', 'ロボットのデモ動画を見たら、'),\n ('action', 'base', '「これは自律なのか、何回やって、何回うまくいったのか」を、'),\n ('action', 'base', '一度だけ考えてみてください。'),\n ('action', 'smile', 'それだけで、同じ動画の見え方が、かなり変わると思います。', 1.8), # 大きな流れの変わり目の間\n ('outro', 'smile', '今日はここまでです。'),\n ('outro', 'base', 'この動画が気に入ったら、高評価とチャンネル登録をお願いします。'),\n ('outro', 'base', 'ロボットの開発や導入の相談は、概要欄の連絡先から、エイイチに直接どうぞ。'),\n ('outro', 'wink', 'コメントは、エイイチが全部読んでいます。それではまた。', 1.7), # 場面の切り替えの前の一拍(ホワーンの前)\n ('sources', 'smile', '', 6.0),\n]\n\n# 仮音声(Open JTalk)用の読み(字幕は元の表記のまま)\nREAD = [('Pixels to Motors', 'ピクセルズ トゥー モーターズ'), ('Smart Analytics Global', 'スマート アナリティクス グローバル'), ('Counterpoint', 'カウンターポイント'), ('ChatGPT', 'チャットジーピーティー'), ('Generalist', 'ジェネラリスト'), ('mechArm 270', 'メックアーム ニーナナゼロ'), ('iPhone', 'アイフォーン'), ('ikpy', 'アイケーパイ'), ('NEO', 'ネオ'), ('1X', 'ワンエックス'), ('SAG', 'エスエージー'), ('AI', 'エーアイ'), ('30cm', 'さんじゅっセンチ'), ('爪先', 'つめさき'), ('1万9100台', 'いちまんきゅうせんひゃくだい'), ('値を送る', 'あたいを送る'), ('フィジカルAI', 'フィジカルエーアイ'), ('「', ''), ('」', ''), ('|', '')]\n\n# ElevenLabs の声で読み間違えた語だけ、読みを直す(tts_eleven.py が使う。字幕は変えない)\nELEVEN_READ = [\n ('エイイチ', 'えいいち'), # 2026-10-12 カタカナだと文ごとに抑揚が変わった。ひらがなで語尾が下がる読みにそろう(エイイチが4通りを聞き比べて決めた)\n ('裏表', 'うらおもて'), # 2026-10-12 37番で「うらおもてうら」のように読まれた(エイイチが見つけた)\n]\n\n# 仮音声の間延び対策:ひと息が長い文だけ、読み上げ用に読点を足す(字幕は変えない)\nOVERRIDE = {'同じ半年の出荷を約1万9100台と推計していて、': '同じ半年の出荷を、約1万9100台と、推計していて、', '工場でも家でも、使いながらデータを集める動きが出てきているんですよ。': '工場でも家でも、使いながら、データを集める動きが、出てきているんですよ。', 'ほかの環境で学んだことを持ってくる研究も進んでいます。': 'ほかの環境で学んだことを、持ってくる研究も、進んでいます。', 'ここで、じゃあロボットにChatGPTの瞬間は当分来ない、': 'ここで、じゃあ、ロボットに、ChatGPTの瞬間は、当分来ない、', '1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、': '1Xが、家の中で、遠隔で手伝いながら、動きを覚えさせているのも、'}\n\n\ndef reading(t):\n t = OVERRIDE.get(t, t)\n for a, b in READ:\n t = t.replace(a, b)\n return t\n\n\ndef parse(seg):\n \"\"\"1行を (場面, 表情, 字幕, 間) にそろえる\"\"\"\n if len(seg) >= 3 and seg[1] in FACES:\n scene, face, text = seg[0], seg[1], seg[2]\n pause = seg[3] if len(seg) > 3 else 0.35\n else:\n scene, face, text = seg[0], \"base\", seg[1]\n pause = seg[2] if len(seg) > 2 else 0.35\n return scene, face, text, pause\n</code></pre><h3 id=\"index__EpedxB3\">フィジカルAIの現在地/scenes.py</h3><p>場面ごとの図と、実機の映像の指定(VIDEO)(363行)。</p><pre><code class=\"language-python\"># -*- coding: utf-8 -*-\n# 動画「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」ハコブくん版の場面。共通部品は _制作ベース/scene_kit.py\n# 2026-10-12: DAY330 の試作の場面を、ハコブくんの区画(左下)と三人称の台本に合わせて直した\nimport os as _os, sys as _sys\n_here = _os.path.dirname(_os.path.abspath(__file__))\nfor _c in (_os.environ.get(\"P2M_BASE\", \"\"), _os.path.join(_here, \"..\", \"base\"), _os.path.join(_here, \"..\", \"_制作ベース\")):\n if _c and _os.path.exists(_os.path.join(_c, \"scene_kit.py\")):\n _sys.path.insert(0, _c); break\nfrom scene_kit import *\n\nT1 = \"<b>1</b>出荷されたロボットは何をしているのか\"\nT2 = \"<b>2</b>机の上のロボットアームで、簡単だったこと・難しかったこと\"\nT3 = \"<b>3</b>データの壁の高さと、越えようとしている会社\"\nT4 = \"<b>4</b>エイイチの見立て\"\n\nS = {}\n\nS[\"hook_ship\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期 人型ロボットの世界出荷(推計)</div>\n <div class=\"hero\">22,000<small>台超</small></div>\n <div class=\"h2\" style=\"margin-top:36px\">前年同期の <span class=\"red\">約4倍</span></div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)</div>\"\"\")\n\nS[\"hook_forecast\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年通年の予想</div>\n <div class=\"hero\">50,000<small>台超</small></div>\n <div class=\"h3\" style=\"margin-top:44px\">家事をする人型ロボットの予約販売も、2025年秋から</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)/1X</div>\"\"\")\n\nS[\"hook_question\"] = page(\"\"\"\n<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div class=\"h1\">ロボットも、ChatGPTのときみたいに<br><span class=\"red\">一気に賢くなる?</span></div>\n</div>\"\"\")\n\nS[\"question\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">今日の問い</div>\n <div class=\"h1\">ロボットにも<span class=\"red\">ChatGPTの瞬間</span>は<br>もう来たのか</div>\n <div class=\"h3 muted\" style=\"margin-top:44px\">ChatGPTの瞬間=専門家でなくても、<br>いろんな仕事にすぐ使えるようになる転換点</div>\n</div>\"\"\")\n\nS[\"define\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">フィジカルAI とは</div>\n <div class=\"h2\">現実の世界を認識して、<span class=\"red\">行動する</span>AI</div>\n <div class=\"row\" style=\"margin-top:56px;gap:30px\">\n <div class=\"box\" style=\"border:4px solid #0D0D0D\">ロボットアーム</div>\n <div class=\"box\" style=\"border:4px solid #0D0D0D\">人型ロボット</div>\n <div class=\"box muted\" style=\"background:transparent;border:2px dashed #D6D6D1\">自動運転<br><span style=\"font-size:30px\">今日は扱わない</span></div>\n </div>\n</div>\"\"\")\n\nS[\"answer\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">先に結論</div>\n {two(\"写真を見せて判断させる入口\", \"ぐっと簡単になった\", \"物を確実に動かすところ\", \"まだ手間がかかる\")}\n <div class=\"h3\" style=\"margin-top:56px\">手間は2つ<br>① <span class=\"red\">動作と結びついたデータ</span>が集めにくい<br>② <span class=\"red\">機体や環境とのすり合わせ</span></div>\n</div>\"\"\")\n\nS[\"stance\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2 muted\" style=\"text-decoration:line-through;text-decoration-thickness:6px\">フィジカルAIはまだ使えない</div>\n <div class=\"h1\" style=\"margin-top:48px\">どこまで来ていて、<br>どこで<span class=\"red\">手間がかかっているのか</span>を、<br>分けて見る</div>\n</div>\"\"\")\n\nS[\"toc\"] = page(\"\"\"\n<div class=\"wrap\">\n <ul class=\"list\">\n <li><span class=\"n\">1</span>出荷が4倍になったロボットは、何をしているのか</li>\n <li><span class=\"n\">2</span>机の上のロボットアームで、何が簡単で何が難しかったか</li>\n <li><span class=\"n\">3</span>データの壁はどれくらい高く、誰がどう越えようとしているか</li>\n <li class=\"dim\"><span class=\"n\">+</span>エイイチの見立て</li>\n </ul>\n</div>\"\"\")\n\nS[\"c1\"] = chapter(\"1\", \"出荷が4倍になったロボットは、<br>何をしているのか\")\nS[\"c2\"] = chapter(\"2\", \"机の上のロボットアームで、<br>何が簡単で、何が難しかったのか\")\nS[\"c3\"] = chapter(\"3\", \"データの壁はどれくらい高くて、<br>誰がどう越えようとしているのか\")\nS[\"c4\"] = chapter(\"4\", \"エイイチの見立て\")\n\nS[\"use\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期の人型ロボット出荷の用途(Counterpointの推計)</div>\n {hbar([(\"展示・イベント + 研究・データ生産 6割以上\", 62, RED, \"#fff\"), (\"その他\", 38, \"#D3D3CE\", INK)])}\n <div class=\"h3\" style=\"margin-top:44px\">出荷の <span class=\"red\">6割以上</span> が、<br>動きを見せる用途と、研究・データ生産の用途</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)。帯の長さは「6割以上」を表す目安</div>\"\"\", tag=T1)\n\nS[\"datacollect\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">データ生産 = ロボットが学ぶための、動きの記録を作ること</div>\n <div class=\"row\" style=\"gap:30px\">\n <div class=\"box\">人が遠隔で動かして<br>記録する</div>\n <div class=\"box\">自律で動いた記録を<br>残す</div>\n </div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">※研究向けの台数が、全部データ集めに使われるわけではない</div>\n <div class=\"h3\" style=\"margin-top:20px\">分類に「データ生産」がある = 今の段階を表している</div>\n</div>\"\"\", tag=T1)\n\nS[\"conflict\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">同じ2026年上半期の推計</div>\n {two(\"Counterpoint\", \"見せる用途+研究・データ生産<br>6割以上\", \"Smart Analytics Global(SAG)\", \"産業・商業用途<br>7割超\", c1=\"\", c2=\"\", size=52, note1=\"出荷 22,000台超\", note2=\"出荷 約19,100台\")}\n <div class=\"h3\" style=\"margin-top:46px\">総数も分類も違う → <span class=\"red\">単純な裏表としては比べられない</span></div>\n <div class=\"h3 muted\" style=\"margin-top:10px\">出荷台数 ≠ 自律的に働いている台数</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)/Smart Analytics Global(2026年8月)</div>\"\"\", tag=T1)\n\nS[\"china\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期の人型ロボット出荷のうち、中国メーカーの割合(SAGの推計)</div>\n <div class=\"hero\">97<small>%以上</small></div>\n</div>\n<div class=\"src\">出典:Smart Analytics Global(2026年8月)</div>\"\"\", tag=T1)\n\nS[\"neo\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">1X「NEO」 家事をする人型ロボット(2025年10月 予約開始)</div>\n {two(\"値段と配送\", \"2万ドル:2026年配送予定<br>月499ドル:その後\", \"動き方\", \"最初は基本的な自律機能<br>知らない作業は<br>担当者が遠隔で手伝う\", c1=\"\", c2=\"\", size=46)}\n <div class=\"h3\" style=\"margin-top:40px\">2026年10月12日時点:一般顧客への配送開始の公式発表は<span class=\"red\">確認できず</span></div>\n</div>\n<div class=\"src\">出典:1X 公式の発表・注文ページ(2026年10月12日確認)</div>\"\"\", tag=T1)\n\nS[\"neo_point\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">売っているのは</div>\n <div class=\"flow\">\n <div class=\"box\">ロボット</div><div class=\"arrow\">+</div>\n <div class=\"box\">遠隔で<br>手伝う人</div><div class=\"arrow\">+</div>\n <div class=\"box\" style=\"border:4px solid {RED}\">動きを<br>覚えていく仕組み</div>\n </div>\n</div>\"\"\", tag=T1)\n\nS[\"bridge1\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">工場でも家でも、<br><span class=\"red\">使いながらデータを集める</span>動き</div>\n <div class=\"h2 muted\" style=\"margin-top:56px\">では、物を動かすのは<br>何がそんなに大変なのか</div>\n</div>\"\"\", tag=T1)\n\nS[\"trials\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">赤いブロックを掴んで、箱に置く</div>\n <div class=\"hero\">650<small>回以上</small></div>\n <div class=\"h3\" style=\"margin-top:36px\">失敗も含めた試行回数。全部台帳に記録</div>\n</div>\n<div class=\"src\">PICK→PLACE の通し試行(台帳 pick_place_ledger_v3.csv、2026年6月18日〜7月20日で656回)</div>\"\"\", tag=T2)\n\nS[\"easy\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{BLUE}\">簡単だった:掴めたかの判定</div>\n <div class=\"flow\">\n <div class=\"box\">iPhoneで<br>撮った写真</div><div class=\"arrow\">→</div>\n <div class=\"box\" style=\"flex:1.5\">AIに聞く<br><span style=\"font-size:32px;white-space:nowrap\" class=\"muted\">「ブロックを掴めていますか」</span></div><div class=\"arrow\">→</div>\n <div class=\"box\" style=\"border:4px solid {BLUE}\"><span class=\"blue\">はい</span>/いいえ</div>\n </div>\n <div class=\"h3\" style=\"margin-top:48px\">ただし、<span class=\"red\">答えが返る ≠ 毎回正しい</span></div>\n</div>\"\"\", tag=T2)\n\nS[\"hard\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">難しかった</div>\n <div class=\"h1\">動かすほう</div>\n</div>\"\"\", tag=T2)\n\nS[\"gap\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">エイイチの設定で、同じ関節の角度を入れたときの爪先の位置</div>\n <div style=\"position:relative;height:400px;margin-top:10px\">\n <div style=\"position:absolute;left:60px;top:250px;right:60px;height:4px;background:{RULE}\"></div>\n <div style=\"position:absolute;left:150px;top:218px;width:68px;height:68px;border-radius:50%;background:#fff;border:8px solid {SUB}\"></div>\n <div style=\"position:absolute;left:4px;top:310px;width:440px;text-align:center;font-size:36px;font-weight:700;color:{SUB}\">計算ツール(ikpy)</div>\n <div style=\"position:absolute;right:150px;top:218px;width:68px;height:68px;border-radius:50%;background:{RED}\"></div>\n <div style=\"position:absolute;right:4px;top:310px;width:440px;text-align:center;font-size:36px;font-weight:700;color:{RED}\">デジタルツイン</div>\n <div style=\"position:absolute;left:184px;right:184px;top:140px;height:4px;background:{INK}\"></div>\n <div style=\"position:absolute;left:182px;top:120px;width:4px;height:44px;background:{INK}\"></div>\n <div style=\"position:absolute;right:182px;top:120px;width:4px;height:44px;background:{INK}\"></div>\n <div style=\"position:absolute;left:0;right:0;top:20px;text-align:center;font-size:72px;font-weight:900\"><span class=\"red\">30cm以上</span> 食い違う</div>\n </div>\n <!-- 2026-10-12 説明の1行が右にはみ出して「グリッパ/ー」で折り返していたので、図の下に2行で中央にそろえた(DAY330 で足した説明。内容は同じ) -->\n <div class=\"h3\" style=\"text-align:center;font-size:38px;margin-top:-10px\">アームが外れた距離ではない。<span class=\"red\">爪先の位置の決め方</span>が違っていた</div>\n <div class=\"h3 muted\" style=\"text-align:center;font-size:32px;margin-top:10px\">計算ツールには、グリッパーの形が入っていない</div>\n</div>\n<div class=\"src\">図は模式図</div>\"\"\", tag=T2)\n\nS[\"silent\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">黙る失敗</div>\n <div class=\"flow\">\n <div class=\"box\">肩の関節に<br><span class=\"red\">90度ちょっと</span>を超える値</div><div class=\"arrow\">→</div>\n <div class=\"box\">エラーは出ない<br>アームも動かない</div>\n </div>\n <div class=\"h3 muted\" style=\"margin-top:44px\">エイイチの機体とソフトの組み合わせで起きたこと</div>\n</div>\"\"\", tag=T2)\n\nS[\"point2\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">この2つは</div>\n <div class=\"h2 muted\" style=\"text-decoration:line-through;text-decoration-thickness:5px\">AIの学習データが足りない</div>\n <div class=\"h1\" style=\"margin-top:30px\"><span class=\"red\">すり合わせ</span>の仕事</div>\n <div class=\"h3\" style=\"margin-top:20px\">モデルや制御が、自分の機体と噛み合っているかを確かめる</div>\n</div>\"\"\", tag=T2)\n\nS[\"years\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">画像と言葉を扱うAIの学習資料を、人が読む・見る時間に換算すると</div>\n <div class=\"hero\">約10<small>万年規模</small></div>\n</div>\n<div class=\"src\">出典:Ken Goldberg, Science Robotics(2025年8月, Editorial)</div>\"\"\", tag=T3)\n\nS[\"wall\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"h1\">10万年の<span class=\"red\">データの壁</span></div>\n <div style=\"margin-top:50px\">{two(\"画像と言葉\", \"大量にある\", \"動作と結びついた経験\", \"集めにくい\", size=58)}</div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">※ロボットにも同じ量が必要だと分かったわけではない</div>\n</div>\n<div class=\"src\">出典:Ken Goldberg, Science Robotics(2025年8月)</div>\"\"\", tag=T3)\n\nS[\"gen\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">Generalist が集めた、人の手作業の記録(会社の発表)</div>\n <div style=\"display:flex;flex-direction:column;gap:30px;margin-top:10px\">\n <div style=\"display:flex;align-items:center;gap:30px\"><div style=\"width:260px;font-size:36px;font-weight:700;color:{SUB}\">2025年11月</div>\n <div style=\"width:{int(1060*27/50)}px;height:110px;background:#D3D3CE;border-radius:0 6px 6px 0\"></div><div style=\"font-size:52px;font-weight:900;white-space:nowrap\">27万時間</div></div>\n <div style=\"display:flex;align-items:center;gap:30px\"><div style=\"width:260px;font-size:36px;font-weight:700;color:{SUB}\">2026年4月</div>\n <div style=\"width:1060px;height:110px;background:{RED};border-radius:0 6px 6px 0\"></div><div style=\"font-size:52px;font-weight:900;white-space:nowrap\">50万時間超</div></div>\n </div>\n <div class=\"h3\" style=\"margin-top:44px\">安い装着型の機材で記録</div>\n</div>\n<div class=\"src\">出典:Generalist(GEN-0 2025年11月/GEN-1 2026年4月)</div>\"\"\", tag=T3)\n\nS[\"gen2\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">Generalist GEN-1.5(2026年8月)</div>\n <div class=\"h2\">簡単で短い作業なら、<br><span class=\"red\">数秒の実演</span>で新しい作業をこなす例</div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">本人たちも:作業は簡単で短く、成功率はまだ控えめ</div>\n</div>\n<div class=\"src\">出典:Generalist「GEN-1.5」(2026年8月19日)</div>\"\"\", tag=T3)\n\nS[\"compare\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">作業を記録した時間と、10万年は<br><span class=\"red\">そのまま比べられない</span></div>\n <div class=\"h3\" style=\"margin-top:50px\"><span class=\"muted\">何倍足りないか</span> より <span class=\"red\">どれだけ広く集められるか</span></div>\n</div>\"\"\", tag=T3)\n\nS[\"dirt\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">壁はまだ高い。でも、<br><span class=\"red\">壁の前で土を積み始めている</span></div>\n <ul class=\"list\" style=\"margin-top:30px\">\n <li style=\"font-size:40px\"><span class=\"n\">・</span>1X:家の中で遠隔で手伝いながら、動きを覚えさせる</li>\n <li style=\"font-size:40px\"><span class=\"n\">・</span>Generalist:人の手作業を装着型の機材で記録する</li>\n </ul>\n</div>\"\"\", tag=T3)\n\nS[\"notall\"] = page(f\"\"\"\n<div class=\"wrap\">\n {two(\"写真を見せて判断させる入口\", \"もう普通に使える\", \"物を確実に動かす\", \"すり合わせとデータが<br>まだ要る\", size=56)}\n <div class=\"h3\" style=\"margin-top:48px\">「まだ全然ダメ」でも「もう来た」でもない</div>\n</div>\"\"\", tag=T3)\n\nS[\"q\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h1\">ロボットのニュースを見る<br><span class=\"red\">問いが2つ</span>に変わる</div>\n</div>\"\"\", tag=T4)\n\nS[\"q1\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">問い 1</div>\n <div class=\"h1\">自律で動いているのか、<br>人が遠隔で動かしているのか</div>\n <div class=\"h3 muted\" style=\"margin-top:36px\">デモ動画で見るところ:自律か遠隔か/再生速度/人が手を出した条件</div>\n</div>\"\"\", tag=T4)\n\nS[\"q2\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">問い 2</div>\n <div class=\"h2\" style=\"margin-bottom:40px\">その仕事には、どちらが要るのか</div>\n {two(\"見て判断する\", \"手軽に試せる<br><span style='font-size:34px;color:#888888'>任せる範囲は現場で確かめる</span>\", \"触れて動かす精度\", \"現場の機体と環境に<br>すり合わせる仕事が残る\", size=48, note1=\"例:棚の商品が足りているか見る\", note2=\"例:柔らかい物を潰さずに掴む\")}\n</div>\"\"\", tag=T4)\n\nS[\"gofe\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">ゴールドバーグさんの近道</div>\n <div class=\"flow\">\n <div class=\"box\">昔ながらの工学<br>+学習</div><div class=\"arrow\">→</div>\n <div class=\"box\">現場で役に立つ<br>仕事をする</div><div class=\"arrow\">→</div>\n <div class=\"box\">働きながら<br>データが増える</div>\n </div>\n <div class=\"h3\" style=\"margin-top:48px\">エイイチの机の上:組み合わせる点は通じる。<span class=\"muted\">規模も段階もまだ違う</span></div>\n</div>\"\"\", tag=T4)\n\nS[\"motion\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">データの話と並行して要るもの</div>\n <div class=\"h1\"><span class=\"red\">どう動くか</span>の設計</div>\n <div class=\"h3\" style=\"margin-top:36px\">ロボットが人に物を渡すとき、<br>何秒待って手を開けば、人は安心して受け取れるのか</div>\n <div class=\"h3 muted\" style=\"margin-top:28px\">成功か失敗かの判定だけでは出てこない</div>\n</div>\"\"\", tag=T4)\n\nS[\"answer2\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">冒頭の問いへの答え</div>\n {two(\"写真を見せて判断させる入口\", \"ずっと簡単になった\", \"物を確実に動かす\", \"すり合わせが<br>まだ必要\", size=58)}\n <div class=\"h3\" style=\"margin-top:44px\">負担を減らす研究は急速に進んでいる。<br>家で家事を任せられる転換点かは、<span class=\"red\">まだ判断できない</span></div>\n</div>\"\"\")\n\nS[\"action\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">ロボットのデモ動画を見たら</div>\n <div class=\"h1\">自律なのか、何回やって、<br><span class=\"red\">何回うまくいった</span>のか</div>\n</div>\"\"\")\n\nS[\"sources\"] = page(\"\"\"\n<div class=\"wrap\" style=\"left:420px;bottom:90px;justify-content:flex-start;top:120px\">\n <div class=\"h2\" style=\"margin-bottom:30px\">出典</div>\n <div class=\"p\" style=\"font-size:29px;line-height:1.75\">\n 人型ロボットの出荷:Counterpoint Research(2026年8月)/Smart Analytics Global(2026年8月)<br>\n 1X NEO:1X 公式の発表・注文ページ(2026年10月12日確認)<br>\n 10万年のデータの壁:Ken Goldberg, Science Robotics(2025年8月, Editorial)<br>\n Generalist:GEN-0(2025年11月)、GEN-1(2026年4月)、GEN-1.5(2026年8月)の公式発表<br>\n ほかの機体から学ぶ研究の例:Gemini Robotics 1.5 技術報告(Google DeepMind)<br>\n 実機の数字:Pixels to Motors の台帳(pick_place_ledger_v3.csv)、公開記事 DAY257<br>\n <span class=\"muted\">ナレーションは AI 音声(ElevenLabs)。話しているのは Pixels to Motors のキャラクター、ハコブくん</span>\n </div>\n</div>\"\"\")\n\n\n# --- 強弱をつける場面の上書き(2026-10-11)---\nS[\"hook_question\"] = punch(\"こういうニュースを見ると\", [\"ロボットも\", \"一気に賢くなる?\"])\nS[\"answer\"] = punch(\"先に結論\", ['見て判断する入口は<span class=\"blue\">簡単に</span>', '物を動かすのは<span class=\"red\">まだ手間</span>'], sub=\"手間は2つ:動作と結びついたデータ/機体や環境とのすり合わせ\", size=110)\nS[\"c1\"] = chapter2(\"1つ目\", \"出荷の中身\", \"出荷が4倍になったロボットは、何をしているのか\")\nS[\"c2\"] = chapter2(\"2つ目\", \"机の上の実感\", \"何が簡単で、何が難しかったのか\")\nS[\"c3\"] = chapter2(\"3つ目\", \"データの壁\", \"どれくらい高くて、誰がどう越えようとしているのか\")\nS[\"c4\"] = chapter2(\"最後に\", \"ニュースの見方\", \"エイイチの見立て\")\nS[\"china\"] = dots(97, \"2026年上半期の人型ロボット出荷のうち\", \"中国メーカー\", \"97%\", note=\"SAGの推計では「97%以上」\", src=\"出典:Smart Analytics Global(2026年8月)\", tag=T1)\nS[\"point2\"] = punch(\"この2つは\", [\"データ不足じゃない\", '<span class=\"red\">すり合わせ</span>の仕事'], sub=\"モデルや制御が、自分の機体と噛み合っているかを確かめる\", tag=T2)\nS[\"wall\"] = punch(\"ゴールドバーグさんの問題提起\", ['10万年の', '<span class=\"red\">データの壁</span>'], sub=\"動作と結びついた経験は集めにくい(同じ量が必要と分かったわけではない)\", kc=\"muted\", tag=T3)\nS[\"compare\"] = punch(\"ひとつ注意\", ['<span class=\"muted\">何倍足りないか</span>より', '<span class=\"red\">どれだけ広く</span>集めるか'], sub=\"作業を記録した時間と10万年は、そのまま比べられない\", size=110, tag=T3)\nS[\"answer2\"] = punch(\"冒頭の問いへの答え\", ['判断する入口は<span class=\"blue\">来た</span>', '家事を任せるのは<span class=\"red\">まだ</span>'], sub=\"負担を減らす研究は急速に進んでいる\", size=110)\n\nO = {\n # 2026-10-12 ハコブくんが話す間は番組とハコブくんの名前、エイイチを紹介する文からエイイチの名前を出す(エイイチの指示)\n \"title\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">フィジカルAI解説</div><div class=\"c\">案内役:ハコブくん(AI音声)</div></div>'),\n \"title_eiichi\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">エイイチ</div><div class=\"c\">デザイナー/ロボティクスの研究・開発</div></div>'),\n \"v_real\": overlay('<div class=\"cap\">実機:mechArm 270-Pi 赤いブロックを掴んで箱に置く</div>'),\n \"v_twin\": overlay('<div class=\"cap\">パソコンの中のアームの分身(Unity のデジタルツイン)</div>'),\n \"v_scan\": overlay('<div class=\"cap\">デジタルツインの中で動かして、関節の角度と爪先の位置の対応表を作る</div>'),\n \"outro\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">相談は概要欄の連絡先から</div><div class=\"c\">高評価・チャンネル登録/コメントはエイイチが全部読んでいます</div></div>'),\n}\n\n# 動画シーン: (ソースファイル, 開始秒, 透かし消しの有無)\n# src/ の中身(元は iCloud の 動画/ にある。2026-10-12 前の版の動画と1コマずつ照らして確かめた):\n# c1.mov = DAY123.MOV / c2.mov = DAY124.mov(どちらも撮ったままの元の映像。DAY123.mp4・DAY124.mp4 は Clideo の透かし入りで、透かしを消すと右下がモヤモヤになったので使わない。2026-10-12)/ c3.mov = デジタルツイン_260416.mov(アームが並ぶ画面)/ c4.mov = ロボットアーム失敗_260416.mov(アーム1台と箱)\nVIDEO = {\n \"title\": (\"src/c1.mov\", 0.0, False),\n \"title_eiichi\": (\"src/c1.mov\", None, False), # None = 前の場面(title)の映像の続きから\n \"v_real\": (\"src/c2.mov\", 22.0, False),\n \"v_twin\": (\"src/c4.mov\", 0.0, False),\n \"v_scan\": (\"src/c3.mov\", 0.0, False),\n \"outro\": (\"src/c1.mov\", 8.0, False),\n}\n</code></pre><h3 id=\"index_53PHekBl\">フィジカルAIの現在地/sound.py</h3><p>BGM と効果音の置き方(23行)。</p><pre><code class=\"language-python\"># 音の置き方(audio_mix.py が読む。書き方は audio_mix.py の先頭)\n# 2026-10-12 冒頭の BGM は ElevenLabs Music で作った bgm_明るいB_2(エイイチが選んだ。説明文は sound/作った記録.jsonl)\n\n# 大きさ: 話している間はナレーションより23dB小さく、間は16dB小さく(2026-10-12 エイイチの指示。-22/-15 から約90%に下げた。明るい曲では -15/-9 だと大きすぎた)\nBGM = [\n # 2026-10-12 仕上げ版: 最初から最後まで同じ曲をループで流す(本編・ジングル用の曲は未定)\n dict(file='sound/bgm_明るいB_2.mp3', start=0, end=None, level=-23, gap=-16, fade_in=0.6, fade_out=3.0),\n]\n\nSE = [\n # (文番号, 文の頭から何秒後, 音, ナレーションより何dB)\n # 2026-10-12 エイイチの指示で、効果音は場面切り替えのホワーンだけにした(始まりの「しゅっ」は外した)\n]\n\n# 場面が切り替わるたびに「ホワーン」(se_rise)を入れる。効果音はこれだけ(2026-10-12 エイイチの指示)\nSCENE_SE = dict(file='se_rise', level=-6, offset=-0.15, skip=['title_eiichi']) # 2026-10-12 エイイチの指示で -12 から 6dB 上げた。title→title_eiichi は同じ映像が続くので鳴らさない\n\n# 大きな流れの変わり目(自己紹介・今日の問い・各章の始まり・答え・アウトロ)は、ホワーンの代わりに別の音を鳴らす。\n# 直前の文の後ろに 1.8 秒の間がある(narration.py)。2026-10-12 エイイチの指示\n# 自己紹介(title): キラリン(se_chapter_glock。鉄琴の高い3音)… 2026-10-12 エイイチが決定\n# そのほか: ポロロン(se_chapter_marimba。マリンバの上がる3音)… 試し中。場所ごとの使い分けはこれから決める\nCHAPTER_SE = dict(file='se_chapter_marimba', files={'title': 'se_chapter_glock'}, level=-8, offset=-0.3,\n scenes=['title', 'question', 'c1', 'c2', 'c3', 'c4', 'answer2', 'outro'])\n</code></pre><h3 id=\"index_uVDSAdpJ\">フィジカルAIの現在地/thumb.py</h3><p>サムネイルの文言と写真(14行)。</p><pre><code class=\"language-python\"># サムネイルの文言と背景(thumbnail.py が読む。書き方は thumbnail.py の先頭)\n# 2026-10-12 動画と同じ文字・色で作る(エイイチの指示)。文言はタイトル案1に合わせた\nPHOTO = 'サムネ素材/素材2_実機のアーム.png' # 実機のアームが赤いブロックを掴む瞬間(DAY124.mov の28秒)\nPHOTO_POS = '6% 50%' # split の右の面で、ブロックを掴む爪先からアームの根元までが入る位置\nHAKOBU = True\nLABEL = '本当に?'\nKICKER = '人型ロボット出荷 約4倍'\nLINES = ['ロボットにも', '<span class=\"red\">ChatGPTの瞬間</span>', 'もう来た?']\nVARIANTS = ['photo3'] # 2026-10-12 背景を全部写真に(エイイチの指示。dark/dark2/dark3 → photo2 → photo3)\n# photo3 の写真: ハコブくんを左下の隅に置くので、アームが箱の上にいるコマ(左下のマットが空く)。DAY124.mov の39.8秒\nPHOTO3 = 'サムネ素材/素材3_箱に置く瞬間.png'\nHK_H = 600 # ハコブくんの高さ(1920×1080 のとき。photo2 の 520 より少し大きく)\nPHOTO_POS_FULL = '50% 50%'\nBOLD_LINES = ['人型ロボット出荷 約4倍', 'ChatGPTの瞬間', 'もう来た?']\n</code></pre><h3 id=\"index_Wfwum3Uk\">フィジカルAIの現在地/概要欄_下書き.md</h3><p>概要欄の下書き(67行)。</p><pre><code class=\"language-markdown\"><!--\nYouTube の概要欄の下書き(2026-10-12)。{t:文番号} は、その文が始まる時間に置きかわる。\n全体を書き出したあと、python3 <_制作ベース>/description.py で 概要欄.txt を作り、それを貼る。\n「【要記入:…】」はエイイチが決めて書く。\n-->\n今年の上半期に出荷された人型ロボットは、世界で2万2000台を超えました。前の年の同じ時期の、およそ4倍です(Counterpoint の推計)。\n\nロボットにも、ChatGPT のような瞬間はもう来たのか。\n\n先に答えを言うと、写真を見せて判断させる入口は、ぐっと簡単になりました。でも、物を確実に動かすところは、まだ手間がかかります。手間は2つ。動作と結びついたデータが集めにくいこと。そして、機体や環境とのすり合わせです。\n\nこの動画では、\n\n・出荷が4倍になったロボットは、何をしているのか\n・机の上のロボットアームで、何が簡単で、何が難しかったのか\n・データの壁はどれくらい高くて、誰がどう越えようとしているのか\n\nの3つを見たうえで、ロボットのニュースを見るときの2つの問いを解説します。\n\n▼目次\n0:00 人型ロボットの出荷は4倍。ロボットにもChatGPTの瞬間?\n{t:6} この番組について\n{t:9} 今日の問いと、先に結論\n{t:26} 1. 出荷の中身:4倍になったロボットは何をしているのか\n{t:53} 2. 机の上の実感:何が簡単で、何が難しかったのか\n{t:83} 3. データの壁:どれくらい高く、誰がどう越えようとしているのか\n{t:111} エイイチの見立て:ニュースを見るときの2つの問い\n{t:132} 冒頭の問いへの答え\n{t:141} エンディング\n\n▼出典\n・人型ロボットの出荷:Counterpoint Research(2026年8月20日)の推計\nhttps://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026\n・同じく出荷の推計:Smart Analytics Global(2026年8月)\n・1X NEO:1X 公式の製品・注文ページ(2026年10月12日に確認。一般のお客さんへの配送開始を示す公式発表は、この日の時点で見つかっていない)\nhttps://www.1x.tech/neo\n・10万年のデータの壁:Ken Goldberg「GOFE can close the 100,000-year robot data gap」Science Robotics(2025年8月, Editorial)\nhttps://doi.org/10.1126/scirobotics.aea7390\n・Generalist の会社の公式発表:GEN-0(2025年11月)、GEN-1(2026年4月)、GEN-1.5(2026年8月)\nhttps://generalistai.com/blog/gen-0\nhttps://generalistai.com/blog/gen-1\nhttps://generalistai.com/blog/gen-1.5\n・ほかの機体から学ぶ研究の例:Gemini Robotics 1.5 技術報告(Google DeepMind)\nhttps://arxiv.org/abs/2510.03342\n・実機の数字:Pixels to Motors の試行の台帳(pick_place_ledger_v3.csv)、公開記事 DAY257\nhttps://adaptdesign.jp/pixelstomotors/record/why-z-correction-failed-twin-poses\n<!-- 2026-10-12 リンクを確認: Counterpoint・1X・Generalist の3本・arXiv はページが開くことを確かめた。Science Robotics は DOI(バークレーの研究室が公開している PDF のファイル名と同じ番号)。Smart Analytics Global は公式の発表ページが見つからなかったので、リンクなし -->\n\n▼この番組について\n「フィジカルAI解説|Pixels to Motors」は、フィジカルAIを、現場で使える形にするまでを解説する番組です。\nエイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。その中で分かったことと、そこから考えた見立てを、案内役のハコブくんがお伝えします。\n\n・ナレーションは AI 音声(ElevenLabs)です。話しているのは Pixels to Motors のキャラクター、ハコブくんです\n・BGM は ElevenLabs Music で作った曲、効果音はコードで作った音です\n\n▼ロボットの開発・導入のご相談\nメール:pixelstomotors@adaptdesign.jp\n\n▼リンク\nWebサイト:https://adaptdesign.jp/pixelstomotors\nGitHub:https://github.com/PixelstoMotors\nX:https://x.com/adaptdesignjp\nLinkedIn:https://www.linkedin.com/in/eiichi-shimazu\n\nチャンネル登録・高評価もよろしくお願いします。コメントは、エイイチが全部読んでいます。\n\n#フィジカルAI #人型ロボット #ロボット\n</code></pre><h3 id=\"index_cfdrc8jp\">フィジカルAIの現在地/plot_ducking.py</h3><p>この記事の BGM のグラフを描いたスクリプト(69行)。</p><pre><code class=\"language-python\">\"\"\"記事 DAY333 の図: 20〜80秒のナレーションの波形と、audio_mix.py が決める BGM の大きさを並べて描く。\n使い方: 動画の作業フォルダで python3 plot_ducking.py\n\"\"\"\nimport sys\nimport numpy as np\nsys.path.insert(0, '.')\nsys.path.insert(1, '../_制作ベース')\nsys.path.insert(2, '../base')\nfrom timing import load, layout, narration, FPS, SR\nimport audio_mix as am\nimport matplotlib\nmatplotlib.use('Agg')\nimport matplotlib.pyplot as plt\nfrom matplotlib import font_manager\n\nFONT = '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc'\nfont_manager.fontManager.addfont(FONT)\nplt.rcParams['font.family'] = font_manager.FontProperties(fname=FONT).get_name()\n\nidx = load(None)\nlayout(idx)\nvoice = narration(idx).astype(np.float32) / 32768\ntotal = len(voice) / SR\n\n# audio_mix.py の mix() と同じ手順で「話している所」を決める\nctl = np.zeros(int(total * 100) + 1)\nfor o in idx:\n if o['text']:\n a = max(0, int((o['start'] / FPS - 0.15) * 100))\n b = int(((o['start'] + o['speech']) / FPS + 0.3) * 100)\n ctl[a:b] = 1\ngaps = np.where(np.diff(np.concatenate([[1], ctl, [1]])) != 0)[0].reshape(-1, 2)\nfor a, b in gaps:\n if b - a < 50:\n ctl[a:b] = 1\nctl = am.smooth(ctl, 0.3)\nlv = 20 * np.log10(am.db(-23) * ctl + am.db(-16) * (1 - ctl))\n\nt0, t1 = 20, 80\nt = np.arange(len(ctl)) / 100\nm = (t >= t0) & (t <= t1)\nfig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 5.2), sharex=True, gridspec_kw={'height_ratios': [1, 1.2]})\ns0, s1 = int(t0 * SR), int(t1 * SR)\nv = voice[s0:s1]\nstep = 200\nenv = np.abs(v[:len(v) // step * step].reshape(-1, step)).max(1)\ntv = t0 + np.arange(len(env)) * step / SR\nax1.fill_between(tv, -env, env, color='#0d0d0d', lw=0)\nax1.set_ylabel('ナレーション')\nax1.set_yticks([])\nax2.plot(t[m], lv[m], color='#C4471F', lw=2)\nax2.set_ylim(-25, -14)\nax2.set_yticks([-23, -16])\nax2.set_yticklabels(['-23dB 話している間', '-16dB 0.8秒以上の間'])\n# 効果音の位置(assemble_full.log の時刻)\nses = [(22.78, 'ホワーン'), (33.40, 'キラリン'), (55.20, 'ポロロン'), (71.38, 'ホワーン')]\nfor x, l in ses:\n if t0 <= x <= t1:\n for a in (ax1, ax2):\n a.axvline(x, color='#1A6C98', lw=1, ls='--')\n ax2.text(x + 0.4, -14.6, l, color='#1A6C98', fontsize=10)\nax2.set_xlabel('動画の時間(秒)')\nfor a in (ax1, ax2):\n for sp in ('top', 'right'):\n a.spines[sp].set_visible(False)\nfig.suptitle('「フィジカルAIの現在地」20〜80秒:話している間は BGM を下げ、間で戻す(audio_mix.py)', fontsize=12)\nfig.tight_layout()\nfig.savefig('DAY333_02_bgm_ducking.png', dpi=150, facecolor='#F5F5F0')\nprint('DAY333_02_bgm_ducking.png')\n</code></pre><p></p><h2 id=\"index_MBHw9jHD\">書き出しのログと測った数値</h2><h3 id=\"index_Q1bHZ06h\">assemble.py の書き出しのログ(全量)</h3><p>最後に全体を書き出したときのログです。1行目が BGM と効果音の数、そのあとが効果音を鳴らした時刻・文番号・音・大きさ・その文の先頭16字、最後の行が全体のコマ数と確認用ファイルの大きさです。145番の文は、字幕のないエンディングの文です。</p><pre><code>BGM 1本・効果音 45個 → mix.wav\n 13.25秒 文002 se_rise -6dB 「年末までには、5万台を超えると予」\n 22.78秒 文004 se_rise -6dB 「こういうニュースを見ると、Cha」\n 33.40秒 文006 se_chapter_glock -8dB 「Pixels to Motors」\n 55.20秒 文009 se_chapter_marimba -8dB 「今日の問いは、ロボットにもCha」\n 71.38秒 文012 se_rise -6dB 「フィジカルAIっていうのは、現実」\n 85.32秒 文015 se_rise -6dB 「先に結論を言っちゃうと、写真を見」\n 106.02秒 文019 se_rise -6dB 「先に言っておくと、フィジカルAI」\n 117.05秒 文021 se_rise -6dB 「大きく3つあります。」\n 137.90秒 文026 se_chapter_marimba -8dB 「1つ目、出荷が4倍になったロボッ」\n 144.62秒 文027 se_rise -6dB 「Counterpointの推計で」\n 155.95秒 文029 se_rise -6dB 「データ生産っていうのは、ロボット」\n 181.92秒 文034 se_rise -6dB 「一方で、別の調査会社のSmart」\n 207.78秒 文039 se_rise -6dB 「もう1つ、SAGの推計では、出荷」\n 215.22秒 文040 se_rise -6dB 「家のほうも見てみます。」\n 251.52秒 文047 se_rise -6dB 「これ、ちょっと考えさせられません」\n 262.98秒 文050 se_rise -6dB 「工場でも家でも、使いながらデータ」\n 276.63秒 文053 se_chapter_marimba -8dB 「2つ目、エイイチの机の上のロボッ」\n 285.45秒 文054 se_rise -6dB 「ここは、エイイチ自身の話から始め」\n 298.85秒 文057 se_rise -6dB 「それだけの作業を、失敗も含めて6」\n 306.35秒 文058 se_rise -6dB 「まず、簡単だったほうから話します」\n 332.85秒 文063 se_rise -6dB 「難しかったのは、動かすほうでした」\n 336.58秒 文064 se_rise -6dB 「ロボットアームの爪先がどこに来る」\n 347.92秒 文066 se_rise -6dB 「パソコンの中に作ったアームの分身」\n 353.82秒 文067 se_rise -6dB 「エイイチの設定では、同じ関節の角」\n 373.52秒 文070 se_rise -6dB 「そこでエイイチは、分身の中でアー」\n 387.08秒 文073 se_rise -6dB 「もう1つ困ったのが、黙る失敗です」\n 404.65秒 文077 se_rise -6dB 「でも、ポイントはここなんです。」\n 434.47秒 文083 se_chapter_marimba -8dB 「3つ目、データの壁はどれくらい高」\n 442.28秒 文084 se_rise -6dB 「カリフォルニア大学バークレー校の」\n 459.62秒 文088 se_rise -6dB 「それに比べて、ロボットが手を動か」\n 488.68秒 文094 se_rise -6dB 「アメリカのGeneralistと」\n 505.52秒 文097 se_rise -6dB 「さらに8月には、簡単で短い作業な」\n 527.92秒 文101 se_rise -6dB 「ここで1つ、注意があります。」\n 542.65秒 文104 se_rise -6dB 「つまり、壁はまだ高い。でも、壁の」\n 561.08秒 文107 se_rise -6dB 「ただ、ここで「なんだ、まだ全然ダ」\n 581.20秒 文111 se_chapter_marimba -8dB 「ここからは、エイイチの見立てです」\n 585.28秒 文112 se_rise -6dB 「ロボットのニュースを見たときの問」\n 590.05秒 文113 se_rise -6dB 「1つは、そのロボットは自律で動い」\n 605.52秒 文116 se_rise -6dB 「もう1つは、その仕事は「見て判断」\n 634.32秒 文122 se_rise -6dB 「ゴールドバーグさんは、昔ながらの」\n 657.38秒 文126 se_rise -6dB 「作る側から見ると、もう1つありま」\n 684.30秒 文132 se_chapter_marimba -8dB 「じゃあ、冒頭の問いに答えますね。」\n 710.45秒 文137 se_rise -6dB 「ロボットのデモ動画を見たら、」\n 725.13秒 文141 se_chapter_marimba -8dB 「今日はここまでです。」\n 742.95秒 文145 se_rise -6dB 「」\nframes 22473 = 749.1s 確認用 29.4MB (crf 29)\n</code></pre><h3 id=\"index_vKASe5en\">上げたファイルの中身(ffprobe)</h3><pre><code>$ ffprobe -v error -show_entries format=duration,bit_rate:stream=codec_name,width,height,r_frame_rate,sample_rate,bit_rate -of default=nw=1 out.mp4\ncodec_name=h264\nwidth=1920\nheight=1080\nr_frame_rate=30/1\nbit_rate=488631\ncodec_name=aac\nsample_rate=48000\nr_frame_rate=0/0\nbit_rate=203703\nduration=749.100000\nbit_rate=701165\n</code></pre><p>1つ目の codec_name から bit_rate までが映像(488,631 ビット/秒)、2つ目が音声(AAC・48kHz・203,703 ビット/秒)、最後の duration と bit_rate がファイル全体です。</p><h3 id=\"index_gpWY2NxQ\">音の大きさ(ffmpeg の ebur128)</h3><pre><code>$ ffmpeg -nostats -i out.mp4 -af ebur128 -f null - 2>&1 | grep -A8 \"Summary\"\n[Parsed_ebur128_0 @ 0x5569ec070940] Summary:\n\n Integrated loudness:\n I: -16.0 LUFS\n Threshold: -26.4 LUFS\n\n Loudness range:\n LRA: 3.1 LU\n Threshold: -36.4 LUFS\n</code></pre><h3 id=\"index_DJoi5KGb\">ファイルの指紋(md5)</h3><pre><code>$ md5sum out.mp4 out_確認用.mp4\nd9f6def3f1a01d9aee5c76c9d6516bc0 out.mp4\n06c2afe18606354bd366a2e4224fab31 out_確認用.mp4\n</code></pre><h3 id=\"index_uLT3z9rw\">作った曲の記録(sound/作った記録.jsonl・全量)</h3><p><code>sound_eleven.py</code> が、曲を作るたびに1行ずつ書き足す記録です。prompt が ElevenLabs Music に渡した文章です。</p><pre><code>{\"file\": \"sound/bgm_本編_1.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Warm, gentle instrumental background music recorded with real acoustic instruments, for a thoughtful explainer about robots and AI. Felt piano and fingerpicked acoustic guitar, soft cello and violin, upright bass, very quiet brushed drums. Natural room sound, like a small live session. Around 90 BPM, calm and hopeful, a touch of color but serious in tone. No synthesizers, no electronic sounds, no vocals. A simple melody that stays out of the narrator's way.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:26\", \"song-id\": \"angU6pw2wgogpMCVOVzl\"}\n{\"file\": \"sound/bgm_本編_2.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Warm, gentle instrumental background music recorded with real acoustic instruments, for a thoughtful explainer about robots and AI. Felt piano and fingerpicked acoustic guitar, soft cello and violin, upright bass, very quiet brushed drums. Natural room sound, like a small live session. Around 90 BPM, calm and hopeful, a touch of color but serious in tone. No synthesizers, no electronic sounds, no vocals. A simple melody that stays out of the narrator's way.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:26\", \"song-id\": \"rGaTcW0xZPfECyYFmCtW\"}\n{\"file\": \"sound/bgm_冒頭_1.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Bright but gentle instrumental music played on real acoustic instruments, for the opening of a serious technology explainer. Acoustic guitar, piano, light pizzicato strings and soft marimba, natural room sound. Around 100 BPM, curious and hopeful, not cheesy. No synthesizers, no electronic sounds, no vocals, no dramatic hits.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:26\", \"song-id\": \"B0CIEZhLC948zw7GsAcP\"}\n{\"file\": \"sound/bgm_冒頭_2.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Bright but gentle instrumental music played on real acoustic instruments, for the opening of a serious technology explainer. Acoustic guitar, piano, light pizzicato strings and soft marimba, natural room sound. Around 100 BPM, curious and hopeful, not cheesy. No synthesizers, no electronic sounds, no vocals, no dramatic hits.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:27\", \"song-id\": \"5NbSMGUTgt0DXRjOTTB1\"}\n{\"file\": \"sound/bgm_明るいA_1.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Bright, warm and uplifting instrumental music played on real acoustic instruments, in a major key, for a friendly technology explainer about robots. Strummed and fingerpicked acoustic guitar, bright piano, glockenspiel accents, light hand claps and soft shaker, warm upright bass. Natural room sound. Around 105 BPM, cheerful and optimistic like a sunny morning, but not childish or cheesy. No synthesizers, no electronic sounds, no vocals. A simple melody that stays out of the narrator's way.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:31\", \"song-id\": \"Hd4Qnr113KBdkYe8qE9k\"}\n{\"file\": \"sound/bgm_明るいA_2.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Bright, warm and uplifting instrumental music played on real acoustic instruments, in a major key, for a friendly technology explainer about robots. Strummed and fingerpicked acoustic guitar, bright piano, glockenspiel accents, light hand claps and soft shaker, warm upright bass. Natural room sound. Around 105 BPM, cheerful and optimistic like a sunny morning, but not childish or cheesy. No synthesizers, no electronic sounds, no vocals. A simple melody that stays out of the narrator's way.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:31\", \"song-id\": \"bQUshWUbu4Y7vH4a43QI\"}\n{\"file\": \"sound/bgm_明るいB_1.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Hopeful, bright instrumental music on real acoustic instruments, in a major key, for a positive and curious explainer about the future of robots. Piano and acoustic guitar leading, warm strings, light pizzicato, a few glockenspiel notes, soft percussion. Natural room sound. Around 100 BPM, optimistic and forward-moving, bright but calm enough to sit under narration. No synthesizers, no electronic sounds, no vocals, no big build-ups.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:31\", \"song-id\": \"qyMIv2n4wM3BGvVoNqKR\"}\n{\"file\": \"sound/bgm_明るいB_2.mp3\", \"kind\": \"music\", \"sec\": 60.0, \"prompt\": \"Hopeful, bright instrumental music on real acoustic instruments, in a major key, for a positive and curious explainer about the future of robots. Piano and acoustic guitar leading, warm strings, light pizzicato, a few glockenspiel notes, soft percussion. Natural room sound. Around 100 BPM, optimistic and forward-moving, bright but calm enough to sit under narration. No synthesizers, no electronic sounds, no vocals, no big build-ups.\", \"loop\": false, \"model\": \"既定\", \"time\": \"2026-10-12 11:31\", \"song-id\": \"19pIxHwtXfiNptitO2FF\"}\n</code></pre><h2 id=\"index_azDryZvY\">書き出した概要欄(概要欄.txt・全量)</h2><p><code>description.py</code> が書き出し、YouTube に貼ったものです。</p><pre><code>今年の上半期に出荷された人型ロボットは、世界で2万2000台を超えました。前の年の同じ時期の、およそ4倍です(Counterpoint の推計)。\n\nロボットにも、ChatGPT のような瞬間はもう来たのか。\n\n先に答えを言うと、写真を見せて判断させる入口は、ぐっと簡単になりました。でも、物を確実に動かすところは、まだ手間がかかります。手間は2つ。動作と結びついたデータが集めにくいこと。そして、機体や環境とのすり合わせです。\n\nこの動画では、\n\n・出荷が4倍になったロボットは、何をしているのか\n・机の上のロボットアームで、何が簡単で、何が難しかったのか\n・データの壁はどれくらい高くて、誰がどう越えようとしているのか\n\nの3つを見たうえで、ロボットのニュースを見るときの2つの問いを解説します。\n\n▼目次\n0:00 人型ロボットの出荷は4倍。ロボットにもChatGPTの瞬間?\n0:33 この番組について\n0:55 今日の問いと、先に結論\n2:18 1. 出荷の中身:4倍になったロボットは何をしているのか\n4:36 2. 机の上の実感:何が簡単で、何が難しかったのか\n7:14 3. データの壁:どれくらい高く、誰がどう越えようとしているのか\n9:41 エイイチの見立て:ニュースを見るときの2つの問い\n11:24 冒頭の問いへの答え\n12:05 エンディング\n\n▼出典\n・人型ロボットの出荷:Counterpoint Research(2026年8月20日)の推計\nhttps://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026\n・同じく出荷の推計:Smart Analytics Global(2026年8月)\n・1X NEO:1X 公式の製品・注文ページ(2026年10月12日に確認。一般のお客さんへの配送開始を示す公式発表は、この日の時点で見つかっていない)\nhttps://www.1x.tech/neo\n・10万年のデータの壁:Ken Goldberg「GOFE can close the 100,000-year robot data gap」Science Robotics(2025年8月, Editorial)\nhttps://doi.org/10.1126/scirobotics.aea7390\n・Generalist の会社の公式発表:GEN-0(2025年11月)、GEN-1(2026年4月)、GEN-1.5(2026年8月)\nhttps://generalistai.com/blog/gen-0\nhttps://generalistai.com/blog/gen-1\nhttps://generalistai.com/blog/gen-1.5\n・ほかの機体から学ぶ研究の例:Gemini Robotics 1.5 技術報告(Google DeepMind)\nhttps://arxiv.org/abs/2510.03342\n・実機の数字:Pixels to Motors の試行の台帳(pick_place_ledger_v3.csv)、公開記事 DAY257\nhttps://adaptdesign.jp/pixelstomotors/record/why-z-correction-failed-twin-poses\n\n▼この番組について\n「フィジカルAI解説|Pixels to Motors」は、フィジカルAIを、現場で使える形にするまでを解説する番組です。\nエイイチは、本業のデザイナーをしながら、ロボティクスの研究と開発をしています。その中で分かったことと、そこから考えた見立てを、案内役のハコブくんがお伝えします。\n\n・ナレーションは AI 音声(ElevenLabs)です。話しているのは Pixels to Motors のキャラクター、ハコブくんです\n・BGM は ElevenLabs Music で作った曲、効果音はコードで作った音です\n\n▼ロボットの開発・導入のご相談\nメール:pixelstomotors@adaptdesign.jp\n\n▼リンク\nWebサイト:https://adaptdesign.jp/pixelstomotors\nGitHub:https://github.com/PixelstoMotors\nX:https://x.com/adaptdesignjp\nLinkedIn:https://www.linkedin.com/in/eiichi-shimazu\n\nチャンネル登録・高評価もよろしくお願いします。コメントは、エイイチが全部読んでいます。\n\n#フィジカルAI #人型ロボット #ロボット\n</code></pre><h2 id=\"index_zetfem2o\">BGM と効果音の候補を調べた表(音源_候補.md・全量)</h2><p>規約は 2026年10月12日に公式のページで確かめた内容です。規約は変わるので、使う前にもう一度確かめます。</p><pre><code># BGM と効果音:どこから持ってくるか\n\n2026-10-12 作成。video_rules.md 8節「BGM・効果音」の判断材料。\n\n## 条件\n\n- YouTube で収益化しても使える\n- クレジット表記の条件を守れる(できれば表記なしで使える)\n- ほかのチャンネルと同じ曲になりにくい(声と同じ考え方。3-1)\n- ナレーションが聞き取れる音量に下げる。話している間は BGM を下げ、間で少し戻す(audio_mix.py が自動でやる)\n\n## 候補(規約は 2026-10-12 に公式のページで確かめた。変わるので、使う前にもう一度見る)\n\n| 用途 | 候補 | 収益化 | クレジット | 気をつけること |\n|---|---|---|---|---|\n| BGM(第一候補) | ElevenLabs Music(いまの Starter プランで使える) | 使える(映画・テレビ・ラジオ・ゲームの一部を除く) | Starter 以上はいらない(無料プランは必要) | 作れるのは月17分まで。曲そのものを音楽配信に出すのは禁止。新しく作る曲なので、ほかのチャンネルと同じ曲にならない |\n| BGM(安全側の控え) | YouTube オーディオライブラリ(YouTube Studio の中) | 使える。ここの曲には Content ID の申し立てが来ないと YouTube が書いている | 「帰属表示が不要」で絞り込めば、いらない | YouTube 以外(X・LinkedIn に切り抜きを載せるなど)での扱いは、ヘルプに書かれていない。多くのチャンネルが使うので、同じ曲になりやすい |\n| BGM(生の演奏・有料) | Audiostock(日本のサービス。人が演奏・録音した曲が多い) | YouTube で使える(動画配信者プランは本人のアカウントだけ) | 公式のライセンスのページに記載なし(使う前に確かめる) | 単品 1,210円〜(買い切り)。定額の動画配信者プランは月858円(年払い)。値段は公式ページの 2024年12月1日時点の表示。多くの曲が Content ID に登録されているが、ふつうは警告が出ない設定とのこと。定額で使った曲は、解約後に新しい動画では使えない(単品なら使える) |\n| BGM(控え) | OpenTracks(旧 DOVA-SYNDROME。2026年9月15日に名前が変わった) | 背景音楽としてなら商用も可 | いらない | 海外の団体が「音楽出版権」を不正に主張して、Content ID の申し立てが来る例があるとサイト自身が書いている(広告が付かない・収益の一部を取られることがある)。そのたびに異議を出す手間がかかる |\n| 効果音(第一候補) | コードで作る(sound_synth.py。もう作った) | 問題なし(自分で作った音) | いらない | 単純な電子音だけ。だれでも同じ音を作り直せるので、「再現できる」の考え方に合う |\n| 効果音(控え) | 効果音ラボ | 使える(「YouTube(収益化しても可)」と明記) | いらない | 効果音を Content ID に登録するのは禁止(他の人の利用に影響するため)。効果音だけの動画は作らない |\n| 効果音(控え) | ElevenLabs の効果音(sound_eleven.py sfx) | 有料プランで作った音は商用可 | いらない | 現実の音(モーター音・机に物を置く音など)が欲しいとき向き |\n\n外したもの: Pixabay の音楽(無料・クレジット不要だが、Content ID の扱いが公式のライセンス要約に書かれていない)。魔王魂・甘茶の音楽工房などのほかの無料サイトは、今回は調べていない。上の候補で足りなければ調べる。\n\n2026-10-12 エイイチ: BGM は生の楽器の音に近い方がよい。コードで作った BGM(bgm_1〜3・bgm_仮_温かみ)は方向を確かめる用にとどめる。\n\n## コードで作った効果音(_制作ベース/sound/。2026-10-12)\n\n| 名前 | 音 | 使う所 |\n|---|---|---|\n| se_pop | ぽん | 図・数字が出る。いちばんよく使う |\n| se_tick | こつ | 小さな切り替え。箇条書きが1つ出る |\n| se_whoosh | しゅっ | 章や場面が大きく変わる |\n| se_ding | ちん | 結論・大事な数字 |\n| se_rise | ひゅい | 驚き・問いかけ |\n| bgm_仮 | やわらかい和音と小さな粒の音(テンポ96・32秒でループ) | 本番の BGM ができるまでの仮。混ぜ方を確かめるためのもの |\n\n効果音は、ナレーションの声(おおむね 100〜300Hz)とぶつからないよう、500Hz より上を中心にしている。入れすぎると疲れるので、1分に2〜3個を目安にする。\n\n## ElevenLabs Music で作る BGM の説明文(英語と日本語訳)\n\nMac で、動画の作業フォルダから(キーの入れ方は tts_eleven.py と同じ):\n\n```\npython3 ../_制作ベース/sound_eleven.py music --name bgm_hook --sec 60 --count 2 --prompt \"(下の英文)\"\n```\n\n### A. 本編の後ろで流す曲(いちばん長く流れる)\n\n> Calm, curious instrumental background music for a technology explainer video about robots and AI. Soft electric piano and light plucked synth arpeggios, warm pads, gentle low bass, very soft minimal drums. Around 95 BPM, steady and unobtrusive. No strong melody that would compete with a narrator's voice. No vocals. Clean modern mix.\n\n日本語訳:ロボットとAIの解説動画の後ろで流す、落ち着いて、少し先が気になる感じの曲。やわらかいエレクトリックピアノ、軽くはじく電子音のアルペジオ、温かい和音、低くやさしいベース、ごく小さなドラム。テンポ95前後で一定、目立たない。ナレーションとぶつかる強いメロディは入れない。歌なし。\n\n### B. フック(動画の最初の数十秒)\n\n> Short instrumental intro bed for a tech news explainer, slightly brighter and more forward than background music. Pulsing soft synth, light percussion, a sense of something starting. Around 100 BPM. No vocals, no big drops, no dramatic trailer hits.\n\n日本語訳:技術ニュースの解説の冒頭で流す、短めの曲。本編の曲より少し明るく前に出る。脈打つやわらかい電子音、軽い打楽器、何かが始まる感じ。テンポ100前後。歌なし。盛り上げの落差や、映画の予告編のような強い音は入れない。\n\n### C. ジングル(「Pixels to Motors。案内役のハコブくんです。」の後ろ)\n\n> A 4-second friendly logo sting for a robotics channel. Soft synth bell chord with a gentle rising shimmer, ends cleanly on a warm chord. No vocals.\n\n日本語訳:ロボットのチャンネルの、4秒の親しみのあるロゴの音。やわらかい電子のベルの和音に、ゆっくり上がるきらめき。温かい和音できれいに終わる。歌なし。\n\n(C は --sec 4。A・B は 60 秒で作り、短ければ audio_mix.py がループでつなぐ)\n\n## 決める基準\n\n1. ナレーションの後ろで、言葉が聞き取りにくくならないか(音の確認.m4a をイヤホンとスピーカーの両方で聞く)\n2. 13分流して、うるさく感じないか\n3. ハコブくんの見た目と番組の落ち着いた調子に合うか</code></pre><p></p><h2 id=\"index_QVHbh5dK\">参照した情報</h2><ul><li><p><a href=\"https://network.mobile.rakuten.co.jp/sumakatsu/contents/articles/2026/00533/\" data-has-link=\"true\">YouTube のチャンネル名の変更方法(楽天モバイル スマ活)</a></p></li><li><p><a href=\"https://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026\" data-has-link=\"true\">Counterpoint Research:2026年上半期の人型ロボット出荷の推計(2026年8月20日)</a></p></li><li><p><a href=\"https://www.youtube.com/@PixelstoMotors\" data-has-link=\"true\">Pixels to Motors の YouTube チャンネル</a></p></li><li><p><a href=\"/\" data-has-link=\"true\">Pixels to Motors</a></p></li></ul>", "toc": [{"id": "index_5Npkigty", "text": "title: \"解説動画の1本目を公開まで通す ― 声・BGM・間・直し・サムネイルを部品ごとに決め、23手順にまとめた\" slug: first-video-end-to-end date: 2026-10-12 day: 333 meta_description: \"解説動画「フィジカルAIの現在地」をハコブくんの声で作り直し、YouTubeに公開した。声・BGM・効果音・間・不具合の直し・概要欄・サムネイルを部品ごとに決め、次の動画から使う23手順にまとめた。\" tags: [YouTube, 解説動画, ハコブくん, ElevenLabs, BGM, 効果音, ffmpeg, Python, Playwright, サムネイル, 動画制作]"}, {"id": "index_R8Gtcv3i", "text": "公開した動画は12分29秒・146文・効果音45個"}, {"id": "index_iJ5F55aG", "text": "ハコブくんの声は、ElevenLabs のボイスデザインで作った「候補6」に決めた"}, {"id": "index_qsSC6Gf9", "text": "「エイイチ」の抑揚は、ひらがなの「えいいち」で渡すとそろった"}, {"id": "index_NvWums4o", "text": "BGM は、コードで作った音から ElevenLabs Music の生楽器の曲に替えた"}, {"id": "index_845PdZbU", "text": "BGM の大きさは、話している間 -23dB・0.8秒以上の間 -16dB にした"}, {"id": "index_SkptrlzU", "text": "場面の切り替えは「ホワーン」、大きな流れの変わり目は「キラリン」と「ポロロン」"}, {"id": "index_ttUdYCtI", "text": "自己紹介の2文目を「研究と開発」と「見立て」に直した"}, {"id": "index_jcwDZAwf", "text": "右下のモヤモヤは、透かしを消した跡だった"}, {"id": "index_mZmow8P1", "text": "30cm の図の説明を、図の下の2行に移した"}, {"id": "index_RXyGoUij", "text": "YouTube の概要欄は、目次の時間を書き出しから自動で入れた"}, {"id": "index_WGBasXjY", "text": "タイトルを決め、「子ども向け」は「いいえ」にした"}, {"id": "index_ua9kJv2c", "text": "サムネイルは5回作り直した"}, {"id": "index_kbIhNmO_", "text": "ハコブくんの体は、ChatGPT で無地の背景から作り直した"}, {"id": "index_gTZdlqKn", "text": "サムネイルの背景は、ハコブくんがアームに重ならないコマを選んだ"}, {"id": "index_NqzL8c9Y", "text": "公開した直後に見えた2つのこと:字幕が2重に出る、画質が低い"}, {"id": "index_CTcfDe5k", "text": "チャンネル名は「フィジカルAI解説|Pixels to Motors」、まだ反映されていない"}, {"id": "index_qiv3oQpt", "text": "X と LinkedIn の投稿文は、数字と机の上の実感を先に置いた"}, {"id": "index_YMC72TM8", "text": "Claude の返事が英語になり、2回聞き返された"}, {"id": "index_ZKdeas3Q", "text": "Mac へ送ったファイルが、途中で変わることがあった"}, {"id": "index_lMTio9tY", "text": "次の動画から使う「1本を作る流れ」23手順"}, {"id": "index_B9rTkmMC", "text": "まだ決まっていないこと"}, {"id": "index_XNI1szIP", "text": "この記事で作った・直したスクリプトの全文"}, {"id": "index_MBHw9jHD", "text": "書き出しのログと測った数値"}, {"id": "index_azDryZvY", "text": "書き出した概要欄(概要欄.txt・全量)"}, {"id": "index_zetfem2o", "text": "BGM と効果音の候補を調べた表(音源_候補.md・全量)"}, {"id": "index_QVHbh5dK", "text": "参照した情報"}]}
\ No newline at end of file
diff --git a/src/data/records/making-explainer-video-with-ai.json b/src/data/records/making-explainer-video-with-ai.json
index 2d08b5a..ce3bc3b 100644
--- a/src/data/records/making-explainer-video-with-ai.json
+++ b/src/data/records/making-explainer-video-with-ai.json
@@ -1 +1 @@
-{"day": "LOG330", "series": "ロボティクスデザイン", "published": "2026-10-11T13:46:46.000Z", "title": "解説動画をAIと作る ― 型の分析から敵対的検証まで、試作1本で決めたこと", "slug": "making-explainer-video-with-ai", "cover": null, "thumb": "/thumbs/r-making-explainer-video-with-ai.html", "lang": "JP", "description": "YouTube解説動画の試作1本を、参考動画の型の分析、台本、音声合成と図解の自動組み立て、codexの敵対的検証、デザイン修正まで進めた記録。決めたルールを量産のベースとして残した。", "updated": "2026-10-12T00:09:46.000Z", "body": "<!--TOC--><p></p><p>Pixels to Motors の YouTube で解説動画を出すために、Claude と対話しながら試作を1本作りました。題材は「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」で、尺は13分50秒です。</p><p>進め方は次の順番でした。参考にしたい解説動画3本の文字起こしから、構成・話し方・補足説明の型を取り出す。型を台本テンプレートにまとめ、テンプレートに沿って台本を書く。台本を、読み上げ音声・図解・字幕・実機の映像に分けてコードで組み立て、MP4 に書き出す。できた台本を codex(OpenAI のコーディング AI)に渡して、論破するつもりで検証してもらう。指摘とデザインの修正を重ねる。最後に、量産するときのベースとしてルール文書とスクリプト一式を残す。</p><p>本記事は、その過程で決めたことと、決めた理由の記録です。間違えたところ(根拠のない数字を書いた、30cm の例が誤解を招く、など)も消さずに残しています。制作スクリプトの全文、実行ログ、codex の検証結果の全文は記事の後半に載せています。</p><div class=\"video\"><video controls playsinline preload=\"metadata\" src=\"/media/s-1920x1080_b273c60b-57bf-4356-b2b2-1b5a4023565f.mp4\"></video></div><p></p><h2 id=\"index_D_tmh6uh\">参考にした3本の解説動画は、冒頭1分半で問い・結論・目次を全部出していた</h2><p>参考にしたのは、時事ニュースを統計と自分の体験で検証する形式の解説動画3本です(YouTube「海外Yパパラジオ」の、会社への侵入と暗数の回、AI とサイバー攻撃の回、溺れている人を AI に見つけさせる実験の回)。動画のページは Claude からは開けなかったため、文字起こしを貼って分析しました。</p><p>3本とも、骨格が同じでした。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>時間の目安</p></th><th colspan=\"1\" rowspan=\"1\"><p>中身</p></th><th colspan=\"1\" rowspan=\"1\"><p>実例</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>0:00〜0:45</p></td><td colspan=\"1\" rowspan=\"1\"><p>挨拶より先に、時事ネタか視聴者のコメントから入る</p></td><td colspan=\"1\" rowspan=\"1\"><p>「最近漏洩多くないですか?」というコメント</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>〜0:50</p></td><td colspan=\"1\" rowspan=\"1\"><p>決まったジングルと自己紹介</p></td><td colspan=\"1\" rowspan=\"1\"><p>住んでいる場所から世界を見ている、と名乗る</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>〜1:10</p></td><td colspan=\"1\" rowspan=\"1\"><p>その回の問いを疑問文1つで言う</p></td><td colspan=\"1\" rowspan=\"1\"><p>「会社への侵入は本当に急に増えたのか」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>〜1:30</p></td><td colspan=\"1\" rowspan=\"1\"><p>結論か鍵になる言葉を先に出し、自分の立場を言う</p></td><td colspan=\"1\" rowspan=\"1\"><p>「変わったのは手口じゃなく攻撃の値段」「増えていないと言いたいわけじゃない」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>〜1:50</p></td><td colspan=\"1\" rowspan=\"1\"><p>「大きく○つあります」で目次を言う</p></td><td colspan=\"1\" rowspan=\"1\"><p>3〜5項目</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>本編</p></td><td colspan=\"1\" rowspan=\"1\"><p>「1つ目、○○です」で区切る。各節の最後の問いが次の節の題になる</p></td><td colspan=\"1\" rowspan=\"1\"><p>「じゃあ一斉に調べ始めたら何が出てくるのか」→ 次の節</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>終盤</p></td><td colspan=\"1\" rowspan=\"1\"><p>「ここからが僕の一番言いたいこと」と口で言ってから意見を話す</p></td><td colspan=\"1\" rowspan=\"1\"><p>事実と意見の境目を視聴者に示す</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>締め</p></td><td colspan=\"1\" rowspan=\"1\"><p>冒頭の問いに答え、次にニュースを見たときに考える問いを1つ渡す</p></td><td colspan=\"1\" rowspan=\"1\"><p>「起きた数か、見つかった数か、一度だけ考えてみて」</p></td></tr></tbody></table></div><p>冒頭の1分半で「何の話か・何に答えるか・どの立場で話すか・何章あるか」がそろいます。ほかに共通していたのは、視聴者の思い込みを1つ置いてデータで崩し、崩したあとに逆方向の早合点も止めること。数字は必ず比較で言い直すこと(「15倍ですよ」)。専門用語は使った直後に1文で説明すること。限界を自分から言うこと(「動画2本だけなので、性能の評価というより最初の手触り」)。</p><p>溺れている人の回だけは型が少し違い、道具の説明 → なぜ難しいか → 試した結果 → 失敗の深掘り → 見せ方を変えて再実験 → 見立て、の順でした。山場が「失敗が見つかる場面」にある実験回です。</p><p></p><h2 id=\"index_0LUCW9SF\">台本テンプレートは「解説回」と「実験回」の2種類に分けた</h2><p>型を、パートごとの秒数とセリフの穴埋め(例「先に結論を言っちゃうと、変わったのは{A}じゃなくて{B}なんですよ」)にして、解説回と実験回の2種類のテンプレートにしました。収録前に確認するチェックリスト(専門用語は初出で1文の説明があるか、数字に出典の名前が付いているか、など)も付けています。</p><p>試作の題材は「フィジカルAIの現在地」で、問いの形にして「ロボットにもChatGPTの瞬間はもう来たのか」としました。解説回のテンプレートに当てはめ、外部の数字(人型ロボットの出荷台数、1X の家庭用ロボット NEO、ゴールドバーグさんの「10万年のデータの壁」、Generalist の学習データ量)は Web で一次情報を確認して入れました。</p><p></p><h2 id=\"index_bTgs9pio\">台本に入れた自分の数字は、台帳と公開記事で裏を取った</h2><p>台本の節2は、自宅の机でロボットアーム(mechArm 270-Pi)を動かしてきた体験談です。ここに入れた数字は、Mac のフォルダと外付け SSD(KIOXIA)をつないでもらい、記録で確かめました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>台本の数字</p></th><th colspan=\"1\" rowspan=\"1\"><p>確かめた記録</p></th><th colspan=\"1\" rowspan=\"1\"><p>結果</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>掴んで置く作業の試行回数</p></td><td colspan=\"1\" rowspan=\"1\"><p>台帳 pick_place_ledger_v3.csv</p></td><td colspan=\"1\" rowspan=\"1\"><p>657行。テスト1行を除いて656回(2026年6月18日〜7月20日)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>爪先の位置の対応表の点数</p></td><td colspan=\"1\" rowspan=\"1\"><p>公開記事 LOG283</p></td><td colspan=\"1\" rowspan=\"1\"><p>約170万点</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>計算ツールとデジタルツインの爪先の食い違い</p></td><td colspan=\"1\" rowspan=\"1\"><p>arm_kinematics.md、公開記事 LOG222</p></td><td colspan=\"1\" rowspan=\"1\"><p>同じ関節角で300mm以上</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>黙って捨てられる指令</p></td><td colspan=\"1\" rowspan=\"1\"><p>公開記事 LOG257</p></td><td colspan=\"1\" rowspan=\"1\"><p>j1(肩)が約90.7度を超えると send_angles がエラーなしで無視される</p></td></tr></tbody></table></div><p>1つ、間違いがありました。試行回数を最初「600回以上」と書いていましたが、これは記録を見ずに書いた数字でした。記事を検索した段階では LOG255 の「100試行ぶん」しか見つからず、いったん「100回以上」に直しました。そのあと台帳を数えたら656回あり、「650回以上」に直しています。結果として最初の数字は大きく外れていませんでしたが、根拠なしに書いた数字だったことに変わりはありません。台帳の集計結果は、後半の実行ログに全量で載せています。</p><p></p><h2 id=\"index_vjneh5nj\">動画は、Open JTalk・Playwright・ffmpeg で1文ずつ組み立てた</h2><p>台本から動画までを、クラウドの作業環境でコードにして組み立てました。</p><div class=\"embed\"><iframe src=\"/embeds/making-explainer-video-with-ai-vpZFxoYF.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p></p><ol><li><p>narration.py に台本を書く。1文ごとに「場面の名前・字幕の文・後ろの間(秒)」の組にする</p></li><li><p>tts2.py で、1文ずつ読み上げ音声を作る</p></li><li><p>scenes.py に、場面ごとの図解を HTML で書く</p></li><li><p>render.py で、図解・テロップ・字幕を Playwright(ブラウザを自動で動かす道具)で画像にする</p></li><li><p>assemble.py で、1文ごとに「図解か実機映像」+「字幕」を ffmpeg で数秒の映像にし、全部つないで音声と合わせる。字幕ファイル(srt)も同時に出す</p></li></ol><p>1文を1つの映像のかたまりにしたのは、あとで1文だけ直したときに、その文の映像だけ作り直せばよいからです。実際、自己紹介の1文を差し替えたときは、作り直したのは3つのかたまりだけでした。</p><p>読み上げ音声には、Open JTalk(日本語の音声合成エンジン)の男声を使いました。より自然な音声エンジン(VOICEVOX など)は、作業環境から配布元の GitHub にアクセスできず、取り込めませんでした。Open JTalk は Ubuntu のパッケージで入れられたので、仮の声として使っています。抑揚は機械的で、公開するなら本人の声で録り直す前提です。</p><p>実機の映像は、Mac の動画フォルダにあった5本から、赤いブロックを掴んで箱に置く実機の映像(LOG124)、アーム単体の動き(LOG123)、Unity のデジタルツインの映像2本を選びました。LOG123 と LOG124 の右下には、以前に使った編集ツールの透かし(clideo.com)が入っていたため、ffmpeg の delogo でぼかし、字幕の後ろに敷く黒い帯の下に隠しています。</p><p></p><h2 id=\"index_YxOgXNy5\">Open JTalk は、ひと息が長い文の最後を1音0.47秒に引き伸ばしていた</h2><p>最初に書き出した動画(13分33秒)を見てもらったところ、次の指摘がありました。</p><blockquote><p>動画の中で音声が間延びしたところが何箇所か存在します。 それだけ修正を行うことはできますか?</p></blockquote><p>Open JTalk は、音声と一緒に「どの音を何秒出したか」の記録を出せます。これを全153文について調べると、6文で、文の後半の音が1つあたり約0.47秒になっていました。普段は0.1秒前後なので、3〜4倍に引き伸ばされています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>文</p></th><th colspan=\"1\" rowspan=\"1\"><p>引き伸ばされていた部分</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>現実の世界で物に触れて動かす技術のことなんですよ。</p></td><td colspan=\"1\" rowspan=\"1\"><p>「…なんですよ」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ChatGPTのようなAIの学習に使われたインターネットの文章を、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「…使われた」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ChatGPTは10万年分の教科書で育ったけれど、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「育ったけれど」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ここで、じゃあロボットにChatGPTの瞬間は当分来ない、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「当分来ない」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1Xが家の中で遠隔操作しながらデータを集めているのも、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「いるのも」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>出荷されたロボットの多くがデータ集めに回っているのも、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「のも」</p></td></tr></tbody></table></div><p>共通していたのは、読点なしでひと息に読む区間が長いことでした。ひと息の区間が長いと、その区間の最後の言葉が引き伸ばされます。</p><p>直し方は、読み上げ用の原稿だけに読点を足して区切ることです。字幕の文は変えません。最初は読点の位置で自動的に区切りましたが、「遠隔操作しなが/ら」のように単語の途中で切れた文があり、区切る位置を手で指定し直しました。その後、台本を書き直したときに、作った音声をすべて検査し、1音が0.3秒を超える文を見つけたら区切って作り直す処理を tts2.py に入れました。区切っても直らない文は、narration.py の OVERRIDE に「読み上げ用の読点の位置」を書いて直しています。</p><p></p><h2 id=\"index_0fVKV772\">codex の敵対的検証で、台本の結論は「成立していない」と判定された</h2><p>台本を公開前に codex に渡し、「この台本を論破するつもりで」検証してもらいました。依頼ファイルには、台本の全文に文番号(S001〜S152)を振り、使った出典の表と、検証してほしい観点(事実と数字・論理・抜けている反論・言い方)を付けています。依頼ファイルの全文と、返ってきた検証結果の全文は、後半に載せています。</p><p>判定は「直してから公開」、結論の評価は「現状では成立していない」でした。重大とされたのは次の4点です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>codex の指摘</p></th><th colspan=\"1\" rowspan=\"1\"><p>中身</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>「ChatGPTの瞬間」が未定義</p></td><td colspan=\"1\" rowspan=\"1\"><p>写真に質問して答えが返った体験1つから、「見て判断する部分には来た」と言い切っている</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>異種データの「1700分の1」比較</p></td><td colspan=\"1\" rowspan=\"1\"><p>人が文章を読む時間(10万年)と、作業を記録した時間(57年)は、意味も単位も違う</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>実機の問題を「データ不足」の例にしている</p></td><td colspan=\"1\" rowspan=\"1\"><p>計算ツールとデジタルツインの食い違いや、指令が無視される問題は、学習データの不足とは別の話</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Generalist の最新発表の抜け</p></td><td colspan=\"1\" rowspan=\"1\"><p>4月の GEN-1 を最新として話していたが、8月に GEN-1.5 が出ていた</p></td></tr></tbody></table></div><p>結論そのものについて、codex は次の形なら条件つきで成立すると書いていました。</p><blockquote><p>僕の机では、画像に質問する入口はずっと簡単になりました。一方、物を確実に動かすには、機体や環境とのすり合わせがまだ必要でした。その負担を減らす研究は急速に進んでいます。でも、いろんな家庭で安全に家事を任せられる転換点まで来たかは、まだ判断できません。</p></blockquote><p>台本の結論は、この形に差し替えました。</p><p></p><h2 id=\"index_t6LAP1jb\">codex が新しく挙げた事実は、一次情報で確かめてから採用した</h2><p>codex の指摘には、台本になかった新しい事実が含まれていました。採用する前に、それぞれの一次情報を開いて確かめています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>codex が挙げた事実</p></th><th colspan=\"1\" rowspan=\"1\"><p>確かめた一次情報</p></th><th colspan=\"1\" rowspan=\"1\"><p>確かめた内容</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Generalist が8月に GEN-1.5 を発表</p></td><td colspan=\"1\" rowspan=\"1\"><p>Generalist 公式ブログ(2026年8月19日)</p></td><td colspan=\"1\" rowspan=\"1\"><p>3〜12秒の実演1回から新しい作業をこなす。10種類の作業で平均成功率59%。本人たちも「作業は簡単で短く、成功率は控えめ」と書いている</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>SAG の分類は「工場や店舗向け」ではない</p></td><td colspan=\"1\" rowspan=\"1\"><p>Smart Analytics Global の発表</p></td><td colspan=\"1\" rowspan=\"1\"><p>原文は「Industrial and commercial applications」(産業・商業用途)。出荷の総数も約19,100台で、Counterpoint の22,000台超とは違う</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1X NEO の月499ドルのプランは配送時期が別</p></td><td colspan=\"1\" rowspan=\"1\"><p>1X の注文ページ</p></td><td colspan=\"1\" rowspan=\"1\"><p>2万ドルの早期購入枠は「US Deliveries start 2026」。月499ドルは「Standard Delivery」で日付の記載なし。最初は基本的な自律機能で、知らない作業は予約した時間に担当者が遠隔で支援する</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ゴールドバーグさんの10万年は「文章だけ」ではない</p></td><td colspan=\"1\" rowspan=\"1\"><p>Science Robotics の論説(2025年8月)</p></td><td colspan=\"1\" rowspan=\"1\"><p>現代の画像と言葉を扱う AI(VLM)の学習資料を、人が読む・見る時間に換算した概算</p></td></tr></tbody></table></div><p>確認できた4点はすべて台本に反映しました。台本の書き直しでは、問いの中の言葉(ChatGPTの瞬間)を冒頭で定義し、話の範囲(自動運転は扱わず、アームと人型ロボットが物を扱う話に絞る)を言い、自分の机の上の話は「僕の機体とソフトの組み合わせでは」と条件を付けて話す形にしています。</p><p></p><h2 id=\"index_YhZ9JBp8\">根拠の弱い数字は足さずに削った</h2><p>codex の指摘を反映する前に、次の注意がありました。</p><blockquote><p>チェックして修正点を検討して反映したいです(過剰なデータは特に問題になりやすいので注意が必要です)</p></blockquote><p>そこで、指摘に答えるために数字を足すのではなく、論旨に必要でないものは削る方針にしました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>削った数字</p></th><th colspan=\"1\" rowspan=\"1\"><p>削った理由</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1700分の1、約57年</p></td><td colspan=\"1\" rowspan=\"1\"><p>意味と単位の違う数字の割り算で、比較として成り立たない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>成功率99%超、ロボットのデータ約1時間</p></td><td colspan=\"1\" rowspan=\"1\"><p>会社の自己申告で、評価の条件を説明しないと誤解される</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>爪先の対応表の約170万点</p></td><td colspan=\"1\" rowspan=\"1\"><p>シミュレーターの中で作った表なのに、実機で170万回測ったように聞こえる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>AGIBOT 44%・Unitree 31% の内訳</p></td><td colspan=\"1\" rowspan=\"1\"><p>なくても話が変わらない</p></td></tr></tbody></table></div><p>残した数字には「推計では」「会社の発表では」を口で付けています。調査会社2社で用途の数字が逆向きに見える件は、「総数も分類も違うので、単純な裏表としては比べられません」と言う形にしました。</p><p></p><h2 id=\"index_gc6toY6y\">読み上げは「爪先」を「つまさき」と読んでいた</h2><blockquote><p>あと「爪先」をつまさきと言っていますが、アームの場合は「つめさき」の方の読み方が適切かと思います(つまさきだと、足の指先に思ってしまう)</p></blockquote><p>Open JTalk の辞書では「爪先」はツマサキでした。読み上げ用の読み替え表に「爪先 → つめさき」を足しています。同じ確認で、「90度ちょっとを超える値を送ると」の「値」がネと読まれていたことも見つけ、「値を送る → あたいを送る」を足しました。ほかに直した読みは「4分の3」(ヨンプンノサン → よんぶんのさん)、「1700分の1」(センナナヒャップンノイチ → せんななひゃくぶんのいち)、英語の名前(ChatGPT、Counterpoint、ikpy など)です。新しい固有名や数字を入れたら、エンジンの解析結果のカタカナを出して目で確認する手順にしました。</p><p></p><h2 id=\"index_HCnc4FWz\">カード上の太線と、角の立った字幕の縁取りをやめた</h2><blockquote><p>それとデザイン面で直したいのがカードの上に太いラインがあるのはいわゆるAIっぽいこなしなので違う表現にしたいです。</p></blockquote><p>最初の図解は、2つのものを対比するとき、白いカードの上辺に青とオレンジの太い線を引いていました。これをやめ、2列の間を細い縦線(2px)で区切り、色は文字の色だけで見分ける形にしました。同じ理由で、名前テロップの左端の太い線も外しています。</p><p></p><figure><img src=\"/media/s-1944x604_v-frms_webp_1ae69cf2-9b0a-420a-a0b8-a54b8fd6b7d8.png\" alt=\"\"><figcaption></figcaption></figure><p></p><blockquote><p>さらにフォントですが、フォントの黒縁が角が立っていているところが散見されます。 角を落とした黒縁に変更してください。</p></blockquote><p>字幕は CSS の文字の縁取り(-webkit-text-stroke)で作っていました。この方式は、線の角を丸める指定ができず、「宅」や「机」の角に尖った黒い角が出ます。字幕を SVG の文字に作り替え、線の角を丸くする指定(stroke-linejoin: round)を入れました。</p><p></p><figure><img src=\"/media/s-1100x610_v-fs_webp_3b59dae8-f9d7-4d46-ab1e-9cf7a3b11305.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_mlpaKj8r\">自己紹介から「秋田」「29年」「毎日」を外し、ロゴを差し替えた</h2><blockquote><p>動画の自己紹介のところで、29年秋田で、というところは削除で。 本業のデザイナーをしながらロボティクスの研究、開発を行っている、などの紹介に修正してください。</p></blockquote><blockquote><p>その記録を毎日公開、だと毎日ロボットを触っているかのような説明なので、「毎日」は削除した方がいい。</p></blockquote><p>自己紹介の文は「僕は本業のデザイナーをしながら、ロボティクスの研究と開発をしています。自宅の机の上でロボットアームを動かして、その記録を公開しています。」に固定しました。動画の中だけでなく、台本のドキュメントと codex への依頼ファイルにも同じ文言が残っていたので、そちらも直しています。</p><p>画面右上とテロップの「PIXELS TO MOTORS」の文字は、Pixels to Motors のロゴ画像に差し替えました。もらったロゴ画像(灰色の地に黒)から、地の明るさとの差で透明度を計算して切り抜き、文字の色をスライドの黒にそろえています。</p><p></p><figure><img src=\"/media/s-1920x1080_v-frms_webp_b7837ab7-c1c0-4d01-943b-1d3aa04345db.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_9szChvZM\">背景と文字の色を、記事サムネイルの #F5F5F0 と #0D0D0D に合わせた</h2><blockquote><p>動画の背景色やテキスト色を記事のサムネイルと同じ色に変更してください(差し色で使っているオレンジや水色は少し濃くするくらいでカラーを使うのはOKです) 色だけの話です。他は修正しないでください。</p></blockquote><p>記事のサムネイル画像から色を拾い、次の色に置き換えました。レイアウトと文言には手を付けていません。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>用途</p></th><th colspan=\"1\" rowspan=\"1\"><p>変更前</p></th><th colspan=\"1\" rowspan=\"1\"><p>変更後</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>背景</p></td><td colspan=\"1\" rowspan=\"1\"><p>#F4F2ED</p></td><td colspan=\"1\" rowspan=\"1\"><p>#F5F5F0</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>文字</p></td><td colspan=\"1\" rowspan=\"1\"><p>#17191E</p></td><td colspan=\"1\" rowspan=\"1\"><p>#0D0D0D</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>薄い文字</p></td><td colspan=\"1\" rowspan=\"1\"><p>#5F636B</p></td><td colspan=\"1\" rowspan=\"1\"><p>#888888</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>区切り線</p></td><td colspan=\"1\" rowspan=\"1\"><p>#CFCAC0</p></td><td colspan=\"1\" rowspan=\"1\"><p>#D6D6D1</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>オレンジ(強調)</p></td><td colspan=\"1\" rowspan=\"1\"><p>#D9542B</p></td><td colspan=\"1\" rowspan=\"1\"><p>#C4471F</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>青(対比のもう一方)</p></td><td colspan=\"1\" rowspan=\"1\"><p>#1F7FB0</p></td><td colspan=\"1\" rowspan=\"1\"><p>#1A6C98</p></td></tr></tbody></table></div><p>オレンジと青は、色覚の違いがあっても見分けられるかを検証するスクリプト(validate_palette.js)にかけ、新しい背景色の上で全項目を通過することを確かめてから使っています。検証の出力は後半の実行ログにあります。</p><p></p><h2 id=\"index_msVyMuQC\">30cmの食い違いは、アームが外れた距離ではなく、爪先の位置の決め方の違いだった</h2><blockquote><p>30cmも違うって合っている? 30mmとかじゃなくて?なんか例が良くない?</p></blockquote><p>数字そのものは記録どおりでした。公開記事 LOG222 に「同じ関節角を入れても、先端位置が300mm以上違う」とあります。ただ、同じ記事に理由も書かれていました。ikpy(関節の角度から先端の位置を計算する一般的なライブラリ)には、グリッパーの3D形状もデジタルツインの物理も入っていません。一方、デジタルツインで作った表の爪先の位置は、Unity の中でグリッパー先端の3Dモデルが実際に来た場所です。2つは、そもそも爪先の位置の決め方が違っていました。</p><p>mechArm 270 の腕の長さは約27cmです。「30cm食い違う」とだけ聞くと、腕の長さより大きく外れたように受け取れてしまいます。差し替え候補として、LOG222 にある実機のずれ(赤いブロックが6回とも同じ向きに、箱の中心から約5cmずれて落ちた件)も挙げましたが、30cm のまま説明を足す形を選んでもらいました。台本に「これは、アームが30cm外れた、という意味ではありません」「計算ツールには、グリッパーの形も、デジタルツインの物理も入っていなくて、爪先の位置の決め方そのものが違っていたんです」の2文を足し、図にも同じ説明を1行入れています。</p><p></p><figure><img src=\"/media/s-1280x720_v-fms_webp_89ef024b-47c1-4439-9d46-42759eb70eaa.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_Apnn9rBf\">打ち出し・説明・補足の3段階で、画面に強弱をつけた</h2><blockquote><p>全部じゃなくていいのですが、動画内の説明テキストの内容を強弱をつけるようにしたいです。 打ち出しを強くしたい、簡潔にわかりやすく内容を伝えたい、タイトルをして使いたいなどの時はテキストを大きくわかりやすくしたり、わかりやすい図解を入れ込んだりして動画内の内容がうまく伝わるように変化をつけたいです。</p></blockquote><p>参考にした動画の画面を見ると、小さな色付きの前置き(例「韓国の甲状腺がん」)の下に、1〜2行の大きな文(「診断は15倍/亡くなる人は増えず」)を置いていました。章の始まりには「今日の話 → 2つ目 → 見つからない」のように大きなキーワードを置き、割合は点の並びで見せていました。</p><p>これを、背景・文字・差し色はそのままで取り入れ、40ある場面のうち11場面だけ切り替えました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>段階</p></th><th colspan=\"1\" rowspan=\"1\"><p>使った場面</p></th><th colspan=\"1\" rowspan=\"1\"><p>見た目</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>打ち出し</p></td><td colspan=\"1\" rowspan=\"1\"><p>冒頭の問いかけ、結論、「すり合わせの仕事」、データの壁、比べ方の注意、最後の答え(6場面)</p></td><td colspan=\"1\" rowspan=\"1\"><p>色付きの前置き(44px)+ 1〜2行の大きな文(110〜150px)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>章の扉</p></td><td colspan=\"1\" rowspan=\"1\"><p>4つの章の始まり</p></td><td colspan=\"1\" rowspan=\"1\"><p>「今日の話」→「1つ目」→ 大きなキーワード(200px・オレンジ)→ 章の問い</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>点の並び</p></td><td colspan=\"1\" rowspan=\"1\"><p>中国メーカーの割合</p></td><td colspan=\"1\" rowspan=\"1\"><p>100個の点のうち97個に色を付ける</p></td></tr></tbody></table></div><p>打ち出しの文は、字幕をそのまま大きくするのではなく、その場面で一番残したい一言に縮めています。たとえば字幕が「この2つは、AIの学習データが足りない、という話とは別です。」の場面は、「データ不足じゃない/すり合わせの仕事」にしました。</p><p></p><figure><img src=\"/media/s-1944x604_v-frms_webp_cf142cf2-c7a5-4f0f-88d3-ae65e0605315.png\" alt=\"\"><figcaption></figcaption></figure><p></p><figure><img src=\"/media/s-1944x604_v-frms_webp_077bbf3b-9bcf-4932-bd31-a613c2b4d0eb.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_sOZrkg72\">動画ファイルは、iCloud ではなく KIOXIA の videos/ に置いた</h2><blockquote><p>動画はデータが重くなるのでSSDの容量を圧迫するので別のフォルダに保管するようにしてください。</p></blockquote><p>動画ファイル(mp4)は、外付け SSD の KIOXIA に <code>r2r2r-data/videos/{動画名}/</code> を作って置き、iCloud からは消しました。字幕の srt と codex への依頼ファイルは軽いので iCloud に残しています。KIOXIA には、YouTube に上げる高画質の作業用(約47MB)と、確認用の圧縮版(約30MB)の2本を置いています。</p><p>ここで1つ問題が起きました。作業環境から Mac へ一度に送れるファイルは30MBまでなので、作業用の動画を12MBずつ4つに分けて送り、Mac 側でつなぎ直しました。ところが、つないだファイルのチェック値(md5)が元と一致しません。調べると、動画の先頭部分を含む1つ目の断片だけが、元より5,875バイト大きくなり、中身も化けていました。2〜4つ目の断片は元と一致していました。1つ目の断片だけを gzip で圧縮した形で送り直し、Mac 側で展開してからつなぐと、チェック値が元と一致しました。転送の途中で、動画の先頭部分だけが何らかの変換を受けたと考えていますが、原因までは確かめていません。</p><p></p><h2 id=\"index_52jeog_s\">量産のベースとして video_rules.md と制作スクリプト一式を残した</h2><p>ここまでに決めたことを、動画制作ルール video_rules.md にまとめました。記事の article_rules.md と同じく、動画を作るときに最初に読む正本です。プロジェクトの資料と、iCloud の <code>動画/_制作ベース/</code> の両方に置き、同じフォルダに制作スクリプト一式とロゴの透過 PNG を入れています。</p><p>video_rules.md は8つの節でできています。動画の構成(解説回の型と時間配分)、台本のルール(codex の検証で直した点から作った11項目)、ナレーション(読み替え表と間延びの検査)、画面デザイン(色・文字の大きさ・図の型・強弱の3段階・やらない表現)、字幕、実機の映像とテロップ、書き出しと保存、まだ決まっていない項目、です。全文は後半に載せています。</p><p>MASTER(プロジェクト全体の方針文書)の保存先テーブルと、KIOXIA の README.md のフォルダ構成には、videos/ と _制作ベース の行がまだありません。この2か所の追記は、本人が行う予定です。</p><p></p><h2 id=\"index_3J7_uzPW\">まだ決まっていない8項目は、未決のまま残した</h2><p>量産の前に決めることが残っています。video_rules.md の8節に、いまの状態と決めることを並べています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>いまの状態</p></th><th colspan=\"1\" rowspan=\"1\"><p>決めること</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ナレーションの声</p></td><td colspan=\"1\" rowspan=\"1\"><p>Open JTalk の仮音声。抑揚が機械的</p></td><td colspan=\"1\" rowspan=\"1\"><p>本人の声で録るか、品質の高い音声合成にするか。srt に1文ごとの時間が入っているので、それに合わせて録れば映像はそのまま使える</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>図解の動き</p></td><td colspan=\"1\" rowspan=\"1\"><p>すべて静止画</p></td><td colspan=\"1\" rowspan=\"1\"><p>数字のカウントアップや矢印が伸びるなどの動きを入れるか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>背景の映像</p></td><td colspan=\"1\" rowspan=\"1\"><p>実機映像は自己紹介・節2・エンディングだけ</p></td><td colspan=\"1\" rowspan=\"1\"><p>解説中も実機映像を薄く敷くか、図解だけにするか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM・効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>なし</p></td><td colspan=\"1\" rowspan=\"1\"><p>使うか。使うなら YouTube で収益化できる音源から選ぶ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ジングル</p></td><td colspan=\"1\" rowspan=\"1\"><p>文言だけ</p></td><td colspan=\"1\" rowspan=\"1\"><p>音とロゴの動きを付けるか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>サムネイル</p></td><td colspan=\"1\" rowspan=\"1\"><p>まだない</p></td><td colspan=\"1\" rowspan=\"1\"><p>記事サムネイルと同じ配色・線画のトーンで作るか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>番組名</p></td><td colspan=\"1\" rowspan=\"1\"><p>未定</p></td><td colspan=\"1\" rowspan=\"1\"><p>決める</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>概要欄のチャプター</p></td><td colspan=\"1\" rowspan=\"1\"><p>まだない</p></td><td colspan=\"1\" rowspan=\"1\"><p>srt の節の開始時刻から作れる</p></td></tr></tbody></table></div><p></p><h2 id=\"index_sWhPqJ9y\">制作スクリプトの全文</h2><p>作業環境(Ubuntu、Python 3.13、ffmpeg 6.1.1、Playwright の Chromium、Noto Sans CJK JP)で動かした最終版です。Open JTalk は <code>apt-get install open-jtalk open-jtalk-mecab-naist-jdic hts-voice-nitech-jp-atr503-m001</code> で入れています。実機の映像は <code>src/c1.mp4</code>(LOG123)、<code>src/c2.mp4</code>(LOG124)、<code>src/c3.mov</code>(デジタルツインの並列スキャン)、<code>src/c4.mov</code>(デジタルツイン)として置き、ロゴは <code>logo_dark.png</code> として同じフォルダに置いて動かします。</p><h3 id=\"index_ZLxuvA9o\">narration.py(台本・読み替え表・読み上げ用の読点)</h3><pre><code class=\"language-python\"># -*- coding: utf-8 -*-\n# 台本「ロボットにもChatGPTの瞬間はもう来たのか」読み上げ原稿(codex 検証反映版)\n# 各要素: (scene_id, 字幕テキスト, 後ろの間[秒]) 間を省略すると 0.35 秒\nSEGS = [\n # 0 フック\n (\"hook_ship\", \"調査会社Counterpointの推計では、今年の上半期に出荷された人型ロボットは、\"),\n (\"hook_ship\", \"世界で2万2000台を超えました。前の年の同じ時期の、およそ4倍です。\", 0.6),\n (\"hook_forecast\", \"年末までには、5万台を超えると予想されています。\"),\n (\"hook_forecast\", \"家事をする人型ロボットの予約販売も、去年の秋から始まっています。\", 0.6),\n (\"hook_question\", \"こういうニュースを見ると、ChatGPTが出たときみたいに、\"),\n (\"hook_question\", \"ロボットも一気に賢くなって、家に来るのはもうすぐ、って感じませんか。\", 0.9),\n # 1 自己紹介\n (\"title\", \"Pixels to Motors。こんにちは、エイイチです。\"),\n (\"title\", \"僕は本業のデザイナーをしながら、ロボティクスの研究と開発をしています。\"),\n (\"title\", \"自宅の机の上でロボットアームを動かして、その記録を公開しています。\"),\n (\"title\", \"作る側の机の上から見える、フィジカルAIの話をします。\", 0.8),\n # 2 今日の問い\n (\"question\", \"今日の問いは、ロボットにもChatGPTのような瞬間は、もう来たのか、です。\"),\n (\"question\", \"ここでいうChatGPTの瞬間は、専門家じゃなくても、\"),\n (\"question\", \"いろんな仕事にすぐ使えるようになる転換点、という意味です。\", 0.7),\n (\"define\", \"フィジカルAIっていうのは、現実の世界を認識して、行動するAIのことです。\"),\n (\"define\", \"自動運転もここに入りますが、\"),\n (\"define\", \"今日は主に、アームや人型ロボットが物を扱う話に絞ります。\", 0.6),\n # 3 結論と立場\n (\"answer\", \"先に結論を言っちゃうと、写真を見せて判断させる入口は、ぐっと簡単になりました。\"),\n (\"answer\", \"でも、物を確実に動かすところは、まだ手間がかかるんですよ。\"),\n (\"answer\", \"その手間は大きく2つあります。\"),\n (\"answer\", \"動作と結びついたデータが集めにくいこと。そして、機体や環境とのすり合わせです。\", 0.6),\n (\"stance\", \"先に言っておくと、フィジカルAIはまだ使えない、と言いたいわけじゃないんです。\"),\n (\"stance\", \"どこまで来ていて、どこで手間がかかっているのかを、分けて見たいんです。\", 0.6),\n # 4 目次\n (\"toc\", \"大きく3つあります。\"),\n (\"toc\", \"出荷が4倍になったロボットは、何をしているのか。\"),\n (\"toc\", \"僕の机の上のロボットアームで、何が簡単で、何が難しかったのか。\"),\n (\"toc\", \"そして、データの壁はどれくらい高くて、誰がどう越えようとしているのか。\"),\n (\"toc\", \"最後に僕の見立てを話します。\", 0.9),\n # 5 節1\n (\"c1\", \"1つ目、出荷が4倍になったロボットは、何をしているのか、です。\", 0.7),\n (\"use\", \"Counterpointの推計では、出荷された人型ロボットの6割以上が、\"),\n (\"use\", \"展示やイベントで動きを見せる用途と、研究やデータ生産の用途なんですよ。\", 0.6),\n (\"datacollect\", \"データ生産っていうのは、ロボットが学ぶための動きの記録を作ることです。\"),\n (\"datacollect\", \"人が遠隔で動かして記録するやり方も、自律で動いた記録を残すやり方もあります。\"),\n (\"datacollect\", \"ただ、研究向けの台数が全部データ集めに使われる、という意味ではありません。\"),\n (\"datacollect\", \"それでも、用途の分類に「データ生産」という項目があること自体が、\"),\n (\"datacollect\", \"今の段階をよく表していると思います。\", 0.7),\n (\"conflict\", \"一方で、別の調査会社のSmart Analytics Globalは、\"),\n (\"conflict\", \"同じ半年の出荷を約1万9100台と推計していて、\"),\n (\"conflict\", \"産業・商業用途が7割を超えた、としています。\"),\n (\"conflict\", \"総数も分類も違うので、この2つは、単純な裏表としては比べられません。\"),\n (\"conflict\", \"それに、出荷台数は、そのまま自律的に働いている台数でもないんです。\", 0.7),\n (\"china\", \"もう1つ、SAGの推計では、出荷の97%以上が中国のメーカーです。\", 0.7),\n (\"neo\", \"家のほうも見てみます。\"),\n (\"neo\", \"アメリカの1Xという会社は、家事をする人型ロボット、NEOの予約を、去年の10月に始めました。\"),\n (\"neo\", \"2万ドルの早期購入枠は、今年配送の予定で、月499ドルのプランは、その後です。\"),\n (\"neo\", \"最初は基本的な自律機能だけで、知らない作業は、\"),\n (\"neo\", \"予約した時間に、1Xの担当者が遠隔で手伝う仕組みなんですよ。\"),\n (\"neo\", \"10月11日の時点で、一般のお客さんへの配送開始を示す公式発表は、\"),\n (\"neo\", \"僕が確認した範囲では、見つかっていません。\", 0.7),\n (\"neo_point\", \"これ、ちょっと考えさせられませんか。\"),\n (\"neo_point\", \"売っているのはロボットだけじゃなくて、遠隔で手伝う人と、\"),\n (\"neo_point\", \"そこで動きを覚えていく仕組みごとなんです。\", 0.7),\n (\"bridge1\", \"工場でも家でも、使いながらデータを集める動きが出てきているんですよ。\"),\n (\"bridge1\", \"じゃあ、物を動かすのは、何がそんなに大変なのか。\"),\n (\"bridge1\", \"まず、僕の机の上の話をさせてください。\", 0.9),\n # 6 節2\n (\"c2\", \"2つ目、僕の机の上のロボットアームで、何が簡単で、何が難しかったのか、です。\", 0.7),\n (\"v_real\", \"ここは僕自身の話から始めさせてください。\"),\n (\"v_real\", \"僕は自宅の机で、mechArm 270という小さなロボットアームを動かしています。\"),\n (\"v_real\", \"赤いブロックを掴んで、箱に置く。\"),\n (\"trials\", \"それだけの作業を、失敗も含めて650回以上試して、全部台帳に残しています。\", 0.7),\n (\"easy\", \"まず、簡単だったほうから話します。掴めたかどうかの判定です。\"),\n (\"easy\", \"iPhoneで撮った写真をAIに見せて、「ブロックを掴めていますか」と聞くと、\"),\n (\"easy\", \"はい、いいえで答えが返ってくる。この入口は、驚くほど簡単でした。\"),\n (\"easy\", \"ただ、答えが返ることと、毎回正しいことは別です。\"),\n (\"easy\", \"どこまで任せられるかは、失敗例も含めて確かめる必要があります。\", 0.7),\n (\"hard\", \"難しかったのは、動かすほうでした。\", 0.6),\n (\"gap\", \"ロボットアームの爪先がどこに来るかは、普通は関節の角度から計算で出します。\"),\n (\"gap\", \"ところが、ikpyという計算ツールで出した爪先の位置と、\"),\n (\"v_twin\", \"パソコンの中に作ったアームの分身、デジタルツインで動かした位置が、\"),\n (\"gap\", \"僕の設定では、同じ関節の角度なのに、30cm以上食い違ったんです。\"),\n (\"gap\", \"これは、アームが30cm外れた、という意味ではありません。\"),\n (\"gap\", \"計算ツールには、グリッパーの形も、デジタルツインの物理も入っていなくて、爪先の位置の決め方そのものが違っていたんです。\", 0.7),\n (\"v_scan\", \"だから僕は、分身の中でアームを動かして、\"),\n (\"v_scan\", \"関節の角度と爪先の位置の対応表を作りました。\"),\n (\"v_scan\", \"これは計算の表で、実機で全部の点を測ったわけではありません。\", 0.7),\n (\"silent\", \"もう1つ困ったのが、黙る失敗です。\"),\n (\"silent\", \"僕の機体とソフトの組み合わせでは、肩の関節に、90度ちょっとを超える値を送ると、\"),\n (\"silent\", \"エラーが出ないのに、アームが動かないことがありました。\"),\n (\"silent\", \"ログを1行ずつ見て、やっと気づきました。\", 0.7),\n (\"point2\", \"でも、ポイントはここなんです。\"),\n (\"point2\", \"この2つは、AIの学習データが足りない、という話とは別です。\"),\n (\"point2\", \"モデルや制御が、僕の機体と噛み合っているかを確かめる、すり合わせの仕事でした。\"),\n (\"point2\", \"ほかの環境で学んだことを持ってくる研究も進んでいます。\"),\n (\"point2\", \"それでも、僕の構成でちゃんと動くかは、ここで確かめるしかなかったんです。\", 0.6),\n (\"point2\", \"じゃあ、もう1つの手間、データのほうは、業界全体でどうなっているのか。\", 0.9),\n # 7 節3\n (\"c3\", \"3つ目、データの壁はどれくらい高くて、誰がどう越えようとしているのか、です。\", 0.7),\n (\"years\", \"カリフォルニア大学バークレー校のロボット研究者、\"),\n (\"years\", \"ケン・ゴールドバーグさんが、去年、こんな概算を紹介しています。\"),\n (\"years\", \"画像と言葉を扱うAIの学習資料を、\"),\n (\"years\", \"人が読んだり見たりする時間に換算すると、約10万年規模になる。\", 0.7),\n (\"wall\", \"それに比べて、ロボットが手を動かすための、動作と結びついた経験は、集めにくい。\"),\n (\"wall\", \"ゴールドバーグさんは、これを「10万年のデータの壁」と呼んでいます。\"),\n (\"wall\", \"ただ、ロボットにも同じ量が必要だと分かったわけではありません。\"),\n (\"wall\", \"集めにくい、という問題提起なんです。\", 0.6),\n (\"wall\", \"ここで、じゃあロボットにChatGPTの瞬間は当分来ない、\"),\n (\"wall\", \"と言いたくなるじゃないですか。でも、ちょっと待ってほしいんです。\", 0.7),\n (\"gen\", \"アメリカのGeneralistという会社は、人が手で作業する様子を、\"),\n (\"gen\", \"安い装着型の機材で記録して集めています。\"),\n (\"gen\", \"会社の発表では、去年の11月に27万時間、今年の4月に50万時間を超えました。\", 0.6),\n (\"gen2\", \"さらに8月には、簡単で短い作業なら、\"),\n (\"gen2\", \"数秒の実演を見せるだけで、新しい作業をこなす例も発表しています。\"),\n (\"gen2\", \"ただ、本人たちも、作業は簡単で短く、成功率はまだ控えめだと書いています。\"),\n (\"gen2\", \"それでも、「動かすには毎回大量の専用データが必要」とは、言い切れなくなってきました。\", 0.7),\n (\"compare\", \"ここで1つ、注意があります。\"),\n (\"compare\", \"作業を記録した時間と、10万年という数字は、そのまま比べられません。\"),\n (\"compare\", \"何倍足りないかより、いろんな場面の経験を、どれだけ広く集められるかが問題なんです。\", 0.7),\n (\"dirt\", \"つまり、壁はまだ高い。でも、壁の前でみんなが土を積み始めている。\"),\n (\"dirt\", \"1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、\"),\n (\"dirt\", \"Generalistが人の手作業を記録しているのも、同じ壁に向かっている動きなんです。\", 0.7),\n (\"notall\", \"ただ、ここで「なんだ、まだ全然ダメなのか」で\"),\n (\"notall\", \"片付けてしまうのも違うんです。\"),\n (\"notall\", \"写真を見せて判断させる入口は、僕の机の上でも、もう普通に使えています。\", 0.6),\n (\"notall\", \"じゃあ、この2つが混ざっている今、ロボットのニュースの何を見ればいいのか。\", 0.9),\n # 8 見立て\n (\"c4\", \"ここからが、僕の一番言いたいことです。\", 0.7),\n (\"q\", \"ロボットのニュースを見たときの問いが、2つに変わるんですよ。\", 0.6),\n (\"q1\", \"1つは、そのロボットは自律で動いているのか、人が遠隔で動かしているのか、です。\"),\n (\"q1\", \"デモ動画を見るときは、自律か遠隔操作か、\"),\n (\"q1\", \"再生速度や、人が手を出した条件が書かれているかを見ます。\", 0.7),\n (\"q2\", \"もう1つは、その仕事は「見て判断する」で済むのか、\"),\n (\"q2\", \"「触れて動かす」精度が要るのか、です。\"),\n (\"q2\", \"棚の商品が足りているかを見るのは、前者です。\"),\n (\"q2\", \"柔らかい物を潰さずに掴むのは、後者です。\"),\n (\"q2\", \"前者は、手軽に試せるところまで来ています。ただ、どこまで任せられるかは、現場で確かめる必要がある。\"),\n (\"q2\", \"後者は、その現場の機体と環境に合わせて、すり合わせる仕事が、まだ残っています。\", 0.7),\n (\"gofe\", \"ゴールドバーグさんは、昔ながらのエンジニアリングと学習を組み合わせて、\"),\n (\"gofe\", \"現場で役に立つ仕事をしながら、データを集めていくのが近道だ、と言っています。\"),\n (\"gofe\", \"僕の机の上も、AIと従来の制御を組み合わせる点では通じています。\"),\n (\"gofe\", \"ただ、現場で働きながらデータが増える仕組みとは、まだ規模も段階も違います。\", 0.7),\n (\"motion\", \"で、作る側として、もう1つ言わせてください。\"),\n (\"motion\", \"データの話と並行して、もう1つ要るのが「どう動くか」の設計です。\"),\n (\"motion\", \"例えばロボットが人に物を渡すとき、\"),\n (\"motion\", \"何秒待って手を開けば、人は安心して受け取れるのか。\"),\n (\"motion\", \"これは、成功か失敗かの判定だけでは出てこない。\"),\n (\"motion\", \"人が安心できる動きを設計する仕事も、同時に必要なんだと、僕は見ています。\", 0.9),\n # 9 回答\n (\"answer2\", \"じゃあ、冒頭の問いに答えますね。\", 0.4),\n (\"answer2\", \"僕の机では、写真を見せて判断させる入口は、ずっと簡単になりました。\"),\n (\"answer2\", \"一方、物を確実に動かすには、機体や環境とのすり合わせが、まだ必要でした。\"),\n (\"answer2\", \"その負担を減らす研究は、急速に進んでいます。\"),\n (\"answer2\", \"でも、いろんな家で、安全に家事を任せられる転換点まで来たかは、まだ判断できません。\", 0.7),\n (\"action\", \"ロボットのデモ動画を見たら、\"),\n (\"action\", \"「これは自律なのか、何回やって、何回うまくいったのか」を、\"),\n (\"action\", \"一度だけ考えてみてください。\"),\n (\"action\", \"それだけで、同じ動画の見え方が、かなり変わると思います。\", 0.9),\n # 10 アウトロ\n (\"outro\", \"今日はここまでです。\"),\n (\"outro\", \"この動画が気に入ったら、高評価とチャンネル登録をお願いします。\"),\n (\"outro\", \"コメントは全部読ませてもらっています。それではまた。\", 1.2),\n (\"sources\", \"\", 6.0),\n]\n\n# TTS 用の読み(字幕は元の表記のまま)\nREAD = [\n (\"Pixels to Motors\", \"ピクセルズ トゥー モーターズ\"),\n (\"Smart Analytics Global\", \"スマート アナリティクス グローバル\"),\n (\"Counterpoint\", \"カウンターポイント\"),\n (\"ChatGPT\", \"チャットジーピーティー\"),\n (\"Generalist\", \"ジェネラリスト\"),\n (\"mechArm 270\", \"メックアーム ニーナナゼロ\"),\n (\"iPhone\", \"アイフォーン\"),\n (\"ikpy\", \"アイケーパイ\"),\n (\"NEO\", \"ネオ\"),\n (\"1X\", \"ワンエックス\"),\n (\"SAG\", \"エスエージー\"),\n (\"AI\", \"エーアイ\"),\n (\"30cm\", \"さんじゅっセンチ\"),\n (\"爪先\", \"つめさき\"),\n (\"1万9100台\", \"いちまんきゅうせんひゃくだい\"),\n (\"値を送る\", \"あたいを送る\"),\n (\"「\", \"\"), (\"」\", \"\"),\n]\n\n# 間延び対策:ひと息が長い文だけ、読み上げ用に読点を足す(字幕は変えない)\nOVERRIDE = {\n \"同じ半年の出荷を約1万9100台と推計していて、\": \"同じ半年の出荷を、約1万9100台と、推計していて、\",\n \"工場でも家でも、使いながらデータを集める動きが出てきているんですよ。\": \"工場でも家でも、使いながら、データを集める動きが、出てきているんですよ。\",\n \"ほかの環境で学んだことを持ってくる研究も進んでいます。\": \"ほかの環境で学んだことを、持ってくる研究も、進んでいます。\",\n \"ここで、じゃあロボットにChatGPTの瞬間は当分来ない、\": \"ここで、じゃあ、ロボットに、ChatGPTの瞬間は、当分来ない、\",\n \"1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、\": \"1Xが、家の中で、遠隔で手伝いながら、動きを覚えさせているのも、\",\n}\n\ndef reading(t):\n t = OVERRIDE.get(t, t)\n for a, b in READ:\n t = t.replace(a, b)\n return t</code></pre><h3 id=\"index_ShUg2Clt\">tts2.py(読み上げ音声の生成と間延びの検査)</h3><pre><code class=\"language-python\">import subprocess, json, sys, re, wave\nimport numpy as np\nsys.path.insert(0,'.')\nfrom narration import SEGS, reading\nVOICE='/usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice'\nDIC='/var/lib/mecab/dic/open-jtalk/naist-jdic'\nSR=48000\ndef synth(text,base):\n w=base+'.wav'; l=base+'.log'\n subprocess.run(['open_jtalk','-m',VOICE,'-x',DIC,'-r','1.08','-fm','-1','-a','0.55','-jf','1.0','-ow',w,'-ot',l],input=text.encode(),check=True)\n log=open(l,encoding='utf-8',errors='ignore').read()\n ph=[]\n for x in log.split('[Output label]')[1].strip().splitlines():\n m=re.match(r'(\\d+) (\\d+) \\S+?\\^\\S+?-(\\S+?)\\+',x)\n if m: ph.append((int(m.group(1))/1e7,int(m.group(2))/1e7,m.group(3)))\n with wave.open(w) as f: a=np.frombuffer(f.readframes(f.getnframes()),dtype=np.int16)\n inner=[(e-s) for s,e,p in ph[1:-1] if p not in('pau','sil')]\n pron=''.join(x.split(',')[9] for x in log.split('[Text analysis result]')[1].split('[Output label]')[0].splitlines() if x.count(',')>9)\n return a,ph,max(inner) if inner else 0,pron\nout=[]\nfor i,s in enumerate(SEGS):\n scene,text=s[0],s[1]; pause=s[2] if len(s)>2 else 0.35\n d=0.0; note=''\n if text:\n r=reading(text)\n a,ph,worst,pron=synth(r,f'tts/{i:03d}')\n if worst>0.3:\n parts=[p for p in re.split(r'(?<=[、。])',r) if p]\n audio=[]; worst2=0\n for n,p in enumerate(parts):\n b,ph2,w2,_=synth(p,f'tts/{i:03d}_p{n}'); worst2=max(worst2,w2)\n st=int(ph2[0][1]*SR) if n>0 else 0\n en=int(ph2[-1][0]*SR) if n<len(parts)-1 else len(b)\n audio.append(b[st:en])\n if n<len(parts)-1: audio.append(np.zeros(int(0.12*SR),dtype=np.int16))\n a=np.concatenate(audio)\n with wave.open(f'tts/{i:03d}.wav','wb') as f:\n f.setnchannels(1); f.setsampwidth(2); f.setframerate(SR); f.writeframes(a.tobytes())\n note=f'STRETCH {worst:.2f}s -> split {len(parts)} parts, max {worst2:.2f}s'\n print(i,text[:30],note)\n d=len(a)/SR\n if re.search(r'[0-9]|爪先',text): print('READ',i,text[:24],'→',pron[:60])\n out.append(dict(i=i,scene=scene,text=text,dur=d,pause=pause,note=note))\njson.dump(out,open('tts/index.json','w'),ensure_ascii=False,indent=0)\ntot=sum(o['dur']+o['pause'] for o in out)\nprint('segments',len(out),'total %d:%02d'%(tot//60,tot%60))</code></pre><h3 id=\"index_T1WL0zVD\">scenes.py(場面ごとの図解・テロップ・実機映像の指定)</h3><pre><code class=\"language-python\"># -*- coding: utf-8 -*-\nimport base64 as _b64, os as _os\nLOGO='data:image/png;base64,'+_b64.b64encode(open(_os.path.join(_os.path.dirname(_os.path.abspath(__file__)),'logo_dark.png'),'rb').read()).decode()\n# 1920x1080 の各シーン(字幕は別レイヤー)。下端 230px は字幕用に空ける。\n# v2: カード上の太線をやめ、左右は細い縦線で区切る。色は文字色だけで区別する。\nBLUE = \"#1A6C98\" # 見て判断する\nRED = \"#C4471F\" # 触れて動かす/強調\nINK = \"#0D0D0D\"\nSUB = \"#888888\"\nBG = \"#F5F5F0\"\nRULE = \"#D6D6D1\"\n\nCSS = f\"\"\"\n*{{margin:0;padding:0;box-sizing:border-box}}\nhtml,body{{width:1920px;height:1080px;overflow:hidden}}\nbody{{background:{BG};color:{INK};font-family:'Noto Sans CJK JP',sans-serif;}}\n.wrap{{position:absolute;left:140px;right:140px;top:96px;bottom:250px;display:flex;flex-direction:column;justify-content:center}}\n.tag{{position:absolute;left:140px;top:52px;font-size:28px;font-weight:700;color:{SUB};letter-spacing:.04em}}\n.tag b{{color:{RED};margin-right:14px}}\n.brand{{position:absolute;right:140px;top:46px;height:34px}}\n.kicker{{font-size:40px;font-weight:700;color:{SUB};margin-bottom:22px}}\n.h1{{font-size:84px;font-weight:900;line-height:1.25}}\n.h2{{font-size:64px;font-weight:900;line-height:1.3}}\n.h3{{font-size:48px;font-weight:700;line-height:1.4}}\n.p{{font-size:40px;font-weight:500;line-height:1.55;color:{INK}}}\n.muted{{color:{SUB}}}\n.red{{color:{RED}}} .blue{{color:{BLUE}}}\n.hero{{font-size:220px;font-weight:900;line-height:1;letter-spacing:-.02em}}\n.hero small{{font-size:90px;font-weight:900;margin-left:10px}}\n.src{{position:absolute;left:140px;bottom:236px;font-size:24px;color:{SUB}}}\n.row{{display:flex;align-items:stretch}}\n.col{{flex:1;padding:10px 56px 10px 0}}\n.col + .col{{border-left:2px solid {RULE};padding-left:56px}}\n.col .lab{{font-size:34px;font-weight:700;color:{SUB};margin-bottom:14px}}\n.col .big{{font-size:66px;font-weight:900;line-height:1.22}}\n.col .note{{font-size:34px;line-height:1.5;margin-top:16px}}\n.list{{list-style:none}}\n.list li{{font-size:46px;font-weight:700;line-height:1.35;padding:22px 0;border-bottom:2px solid {RULE};display:flex;gap:28px;align-items:baseline}}\n.list li .n{{font-size:44px;font-weight:900;color:{RED};min-width:60px}}\n.list li.dim{{color:{SUB}}}\n.chap{{font-size:200px;font-weight:900;color:{RED};line-height:1}}\n.pill{{align-self:flex-start;display:inline-block;font-size:30px;font-weight:700;padding:8px 22px;border-radius:999px;background:{INK};color:#fff;margin-bottom:26px}}\n.flow{{display:flex;align-items:center;gap:30px}}\n.box{{background:#fff;border-radius:18px;padding:34px 40px;font-size:42px;font-weight:700;text-align:center;flex:1}}\n.arrow{{font-size:64px;color:{SUB};font-weight:900}}\n\"\"\"\n\ndef page(body, tag=None, brand=True):\n t = f'<div class=\"tag\">{tag}</div>' if tag else \"\"\n b = f'<img class=\"brand\" src=\"{LOGO}\">' if brand else \"\"\n return f'<!doctype html><html><head><meta charset=\"utf-8\"><style>{CSS}</style></head><body>{t}{b}{body}</body></html>'\n\nT1 = \"<b>1</b>出荷されたロボットは何をしているのか\"\nT2 = \"<b>2</b>机の上のロボットアームで、簡単だったこと・難しかったこと\"\nT3 = \"<b>3</b>データの壁の高さと、越えようとしている会社\"\nT4 = \"<b>4</b>僕の見立て\"\n\ndef hbar(parts, height=96):\n segs = \"\"\n for lab, pct, col, tc in parts:\n segs += (f'<div style=\"flex:{pct};background:{col};height:{height}px;border-radius:6px;'\n f'display:flex;align-items:center;padding-left:24px;color:{tc};font-size:34px;font-weight:700;white-space:nowrap;overflow:hidden\">{lab}</div>')\n return f'<div style=\"display:flex;gap:4px;width:100%\">{segs}</div>'\n\ndef two(lab1, big1, lab2, big2, c1=\"blue\", c2=\"red\", size=60, note1=\"\", note2=\"\"):\n n1 = f'<div class=\"note\">{note1}</div>' if note1 else \"\"\n n2 = f'<div class=\"note\">{note2}</div>' if note2 else \"\"\n return (f'<div class=\"row\"><div class=\"col\"><div class=\"lab\">{lab1}</div>{n1}<div class=\"big {c1}\" style=\"font-size:{size}px;margin-top:8px\">{big1}</div></div>'\n f'<div class=\"col\"><div class=\"lab\">{lab2}</div>{n2}<div class=\"big {c2}\" style=\"font-size:{size}px;margin-top:8px\">{big2}</div></div></div>')\n\ndef chapter(n, title):\n return page(f'<div class=\"wrap\"><div class=\"chap\">{n}</div><div class=\"h1\" style=\"margin-top:30px\">{title}</div></div>')\n\n\n# --- 強弱の型(2026-10-11 追加)---\n# 打ち出し: 小さい前置き(色)+ 1〜2行の大きな文。1行は10字前後まで\ndef punch(kicker, lines, sub=\"\", kc=\"red\", size=150, tag=None):\n body = \"<br>\".join(lines)\n sb = f'<div class=\"h3 muted\" style=\"margin-top:40px;font-size:44px\">{sub}</div>' if sub else \"\"\n return page(f\"\"\"<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div class=\"{kc}\" style=\"font-size:44px;font-weight:900;letter-spacing:.06em;margin-bottom:28px\">{kicker}</div>\n <div style=\"font-size:{size}px;font-weight:900;line-height:1.22;letter-spacing:-.01em\">{body}</div>{sb}\n</div>\"\"\", tag=tag)\n\n# 章の扉: 「今日の話」→「1つ目」→ 大きなキーワード → 章の問い\ndef chapter2(no, keyword, sub):\n return page(f\"\"\"<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div class=\"muted\" style=\"font-size:40px;font-weight:700;letter-spacing:.1em\">今日の話</div>\n <div style=\"font-size:72px;font-weight:900;margin-top:26px\">{no}</div>\n <div class=\"red\" style=\"font-size:200px;font-weight:900;line-height:1.1;margin-top:6px\">{keyword}</div>\n <div style=\"font-size:48px;font-weight:700;margin-top:26px\">{sub}</div>\n</div>\"\"\")\n\n# 点の並び: 100個の点のうち n 個に色を付ける(割合を数で見せる)\ndef dots(n, kicker, label, big, note=\"\", src=\"\", tag=None):\n cells = \"\".join(f'<span style=\"width:38px;height:38px;border-radius:50%;background:{RED if i < n else \"#D3D3CE\"}\"></span>' for i in range(100))\n return page(f\"\"\"<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div style=\"font-size:48px;font-weight:900;margin-bottom:34px\">{kicker}</div>\n <div style=\"display:flex;align-items:center;gap:56px\">\n <div style=\"display:grid;grid-template-columns:repeat(25,38px);gap:10px\">{cells}</div>\n <div class=\"red\" style=\"font-size:190px;font-weight:900;line-height:1\">{big}</div>\n </div>\n <div style=\"font-size:48px;font-weight:700;margin-top:34px\">{label}</div>\n <div class=\"muted\" style=\"font-size:32px;margin-top:10px\">{note}</div>\n</div>{f'<div class=\"src\">{src}</div>' if src else ''}\"\"\", tag=tag)\n\nS = {}\n\nS[\"hook_ship\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期 人型ロボットの世界出荷(推計)</div>\n <div class=\"hero\">22,000<small>台超</small></div>\n <div class=\"h2\" style=\"margin-top:36px\">前年同期の <span class=\"red\">約4倍</span></div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)</div>\"\"\")\n\nS[\"hook_forecast\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年通年の予想</div>\n <div class=\"hero\">50,000<small>台超</small></div>\n <div class=\"h3\" style=\"margin-top:44px\">家事をする人型ロボットの予約販売も、2025年秋から</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)/1X</div>\"\"\")\n\nS[\"hook_question\"] = page(\"\"\"\n<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div class=\"h1\">ロボットも、ChatGPTのときみたいに<br><span class=\"red\">一気に賢くなる?</span></div>\n</div>\"\"\")\n\nS[\"question\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">今日の問い</div>\n <div class=\"h1\">ロボットにも<span class=\"red\">ChatGPTの瞬間</span>は<br>もう来たのか</div>\n <div class=\"h3 muted\" style=\"margin-top:44px\">ChatGPTの瞬間=専門家でなくても、<br>いろんな仕事にすぐ使えるようになる転換点</div>\n</div>\"\"\")\n\nS[\"define\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">フィジカルAI とは</div>\n <div class=\"h2\">現実の世界を認識して、<span class=\"red\">行動する</span>AI</div>\n <div class=\"row\" style=\"margin-top:56px;gap:30px\">\n <div class=\"box\" style=\"border:4px solid #0D0D0D\">ロボットアーム</div>\n <div class=\"box\" style=\"border:4px solid #0D0D0D\">人型ロボット</div>\n <div class=\"box muted\" style=\"background:transparent;border:2px dashed #D6D6D1\">自動運転<br><span style=\"font-size:30px\">今日は扱わない</span></div>\n </div>\n</div>\"\"\")\n\nS[\"answer\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">先に結論</div>\n {two(\"写真を見せて判断させる入口\", \"ぐっと簡単になった\", \"物を確実に動かすところ\", \"まだ手間がかかる\")}\n <div class=\"h3\" style=\"margin-top:56px\">手間は2つ<br>① <span class=\"red\">動作と結びついたデータ</span>が集めにくい<br>② <span class=\"red\">機体や環境とのすり合わせ</span></div>\n</div>\"\"\")\n\nS[\"stance\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2 muted\" style=\"text-decoration:line-through;text-decoration-thickness:6px\">フィジカルAIはまだ使えない</div>\n <div class=\"h1\" style=\"margin-top:48px\">どこまで来ていて、<br>どこで<span class=\"red\">手間がかかっているのか</span>を、<br>分けて見る</div>\n</div>\"\"\")\n\nS[\"toc\"] = page(\"\"\"\n<div class=\"wrap\">\n <ul class=\"list\">\n <li><span class=\"n\">1</span>出荷が4倍になったロボットは、何をしているのか</li>\n <li><span class=\"n\">2</span>机の上のロボットアームで、何が簡単で何が難しかったか</li>\n <li><span class=\"n\">3</span>データの壁はどれくらい高く、誰がどう越えようとしているか</li>\n <li class=\"dim\"><span class=\"n\">+</span>僕の見立て</li>\n </ul>\n</div>\"\"\")\n\nS[\"c1\"] = chapter(\"1\", \"出荷が4倍になったロボットは、<br>何をしているのか\")\nS[\"c2\"] = chapter(\"2\", \"机の上のロボットアームで、<br>何が簡単で、何が難しかったのか\")\nS[\"c3\"] = chapter(\"3\", \"データの壁はどれくらい高くて、<br>誰がどう越えようとしているのか\")\nS[\"c4\"] = chapter(\"4\", \"僕の見立て\")\n\nS[\"use\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期の人型ロボット出荷の用途(Counterpointの推計)</div>\n {hbar([(\"展示・イベント + 研究・データ生産 6割以上\", 62, RED, \"#fff\"), (\"その他\", 38, \"#D3D3CE\", INK)])}\n <div class=\"h3\" style=\"margin-top:44px\">出荷の <span class=\"red\">6割以上</span> が、<br>動きを見せる用途と、研究・データ生産の用途</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)。帯の長さは「6割以上」を表す目安</div>\"\"\", tag=T1)\n\nS[\"datacollect\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">データ生産 = ロボットが学ぶための、動きの記録を作ること</div>\n <div class=\"row\" style=\"gap:30px\">\n <div class=\"box\">人が遠隔で動かして<br>記録する</div>\n <div class=\"box\">自律で動いた記録を<br>残す</div>\n </div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">※研究向けの台数が、全部データ集めに使われるわけではない</div>\n <div class=\"h3\" style=\"margin-top:20px\">分類に「データ生産」がある = 今の段階を表している</div>\n</div>\"\"\", tag=T1)\n\nS[\"conflict\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">同じ2026年上半期の推計</div>\n {two(\"Counterpoint\", \"見せる用途+研究・データ生産<br>6割以上\", \"Smart Analytics Global(SAG)\", \"産業・商業用途<br>7割超\", c1=\"\", c2=\"\", size=52, note1=\"出荷 22,000台超\", note2=\"出荷 約19,100台\")}\n <div class=\"h3\" style=\"margin-top:46px\">総数も分類も違う → <span class=\"red\">単純な裏表としては比べられない</span></div>\n <div class=\"h3 muted\" style=\"margin-top:10px\">出荷台数 ≠ 自律的に働いている台数</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)/Smart Analytics Global(2026年8月)</div>\"\"\", tag=T1)\n\nS[\"china\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期の人型ロボット出荷のうち、中国メーカーの割合(SAGの推計)</div>\n <div class=\"hero\">97<small>%以上</small></div>\n</div>\n<div class=\"src\">出典:Smart Analytics Global(2026年8月)</div>\"\"\", tag=T1)\n\nS[\"neo\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">1X「NEO」 家事をする人型ロボット(2025年10月 予約開始)</div>\n {two(\"値段と配送\", \"2万ドル:2026年配送予定<br>月499ドル:その後\", \"動き方\", \"最初は基本的な自律機能<br>知らない作業は<br>担当者が遠隔で手伝う\", c1=\"\", c2=\"\", size=46)}\n <div class=\"h3\" style=\"margin-top:40px\">2026年10月11日時点:一般顧客への配送開始の公式発表は<span class=\"red\">確認できず</span></div>\n</div>\n<div class=\"src\">出典:1X 公式の発表・注文ページ(2026年10月11日確認)</div>\"\"\", tag=T1)\n\nS[\"neo_point\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">売っているのは</div>\n <div class=\"flow\">\n <div class=\"box\">ロボット</div><div class=\"arrow\">+</div>\n <div class=\"box\">遠隔で<br>手伝う人</div><div class=\"arrow\">+</div>\n <div class=\"box\" style=\"border:4px solid {RED}\">動きを<br>覚えていく仕組み</div>\n </div>\n</div>\"\"\", tag=T1)\n\nS[\"bridge1\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">工場でも家でも、<br><span class=\"red\">使いながらデータを集める</span>動き</div>\n <div class=\"h2 muted\" style=\"margin-top:56px\">では、物を動かすのは<br>何がそんなに大変なのか</div>\n</div>\"\"\", tag=T1)\n\nS[\"trials\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">赤いブロックを掴んで、箱に置く</div>\n <div class=\"hero\">650<small>回以上</small></div>\n <div class=\"h3\" style=\"margin-top:36px\">失敗も含めた試行回数。全部台帳に記録</div>\n</div>\n<div class=\"src\">PICK→PLACE の通し試行(台帳 pick_place_ledger_v3.csv、2026年6月18日〜7月20日で656回)</div>\"\"\", tag=T2)\n\nS[\"easy\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{BLUE}\">簡単だった:掴めたかの判定</div>\n <div class=\"flow\">\n <div class=\"box\">iPhoneで<br>撮った写真</div><div class=\"arrow\">→</div>\n <div class=\"box\" style=\"flex:1.5\">AIに聞く<br><span style=\"font-size:32px;white-space:nowrap\" class=\"muted\">「ブロックを掴めていますか」</span></div><div class=\"arrow\">→</div>\n <div class=\"box\" style=\"border:4px solid {BLUE}\"><span class=\"blue\">はい</span>/いいえ</div>\n </div>\n <div class=\"h3\" style=\"margin-top:48px\">ただし、<span class=\"red\">答えが返る ≠ 毎回正しい</span></div>\n</div>\"\"\", tag=T2)\n\nS[\"hard\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">難しかった</div>\n <div class=\"h1\">動かすほう</div>\n</div>\"\"\", tag=T2)\n\nS[\"gap\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">僕の設定で、同じ関節の角度を入れたときの爪先の位置</div>\n <div style=\"position:relative;height:400px;margin-top:10px\">\n <div style=\"position:absolute;left:60px;top:250px;right:60px;height:4px;background:{RULE}\"></div>\n <div style=\"position:absolute;left:150px;top:218px;width:68px;height:68px;border-radius:50%;background:#fff;border:8px solid {SUB}\"></div>\n <div style=\"position:absolute;left:4px;top:310px;width:440px;text-align:center;font-size:36px;font-weight:700;color:{SUB}\">計算ツール(ikpy)</div>\n <div style=\"position:absolute;right:150px;top:218px;width:68px;height:68px;border-radius:50%;background:{RED}\"></div>\n <div style=\"position:absolute;right:4px;top:310px;width:440px;text-align:center;font-size:36px;font-weight:700;color:{RED}\">デジタルツイン</div>\n <div style=\"position:absolute;left:184px;right:184px;top:140px;height:4px;background:{INK}\"></div>\n <div style=\"position:absolute;left:182px;top:120px;width:4px;height:44px;background:{INK}\"></div>\n <div style=\"position:absolute;right:182px;top:120px;width:4px;height:44px;background:{INK}\"></div>\n <div style=\"position:absolute;left:0;right:0;top:20px;text-align:center;font-size:72px;font-weight:900\"><span class=\"red\">30cm以上</span> 食い違う</div>\n </div>\n</div>\n<div class=\"h3\" style=\"position:absolute;left:140px;bottom:290px;font-size:36px\">アームが外れた距離ではない。<span class=\"red\">爪先の位置の決め方</span>が違っていた<span class=\"muted\">(計算ツールにはグリッパーの形がない)</span></div>\n<div class=\"src\">図は模式図</div>\"\"\", tag=T2)\n\nS[\"silent\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">黙る失敗</div>\n <div class=\"flow\">\n <div class=\"box\">肩の関節に<br><span class=\"red\">90度ちょっと</span>を超える値</div><div class=\"arrow\">→</div>\n <div class=\"box\">エラーは出ない<br>アームも動かない</div>\n </div>\n <div class=\"h3 muted\" style=\"margin-top:44px\">僕の機体とソフトの組み合わせで起きたこと</div>\n</div>\"\"\", tag=T2)\n\nS[\"point2\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">この2つは</div>\n <div class=\"h2 muted\" style=\"text-decoration:line-through;text-decoration-thickness:5px\">AIの学習データが足りない</div>\n <div class=\"h1\" style=\"margin-top:30px\"><span class=\"red\">すり合わせ</span>の仕事</div>\n <div class=\"h3\" style=\"margin-top:20px\">モデルや制御が、自分の機体と噛み合っているかを確かめる</div>\n</div>\"\"\", tag=T2)\n\nS[\"years\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">画像と言葉を扱うAIの学習資料を、人が読む・見る時間に換算すると</div>\n <div class=\"hero\">約10<small>万年規模</small></div>\n</div>\n<div class=\"src\">出典:Ken Goldberg, Science Robotics(2025年8月, Editorial)</div>\"\"\", tag=T3)\n\nS[\"wall\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"h1\">10万年の<span class=\"red\">データの壁</span></div>\n <div style=\"margin-top:50px\">{two(\"画像と言葉\", \"大量にある\", \"動作と結びついた経験\", \"集めにくい\", size=58)}</div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">※ロボットにも同じ量が必要だと分かったわけではない</div>\n</div>\n<div class=\"src\">出典:Ken Goldberg, Science Robotics(2025年8月)</div>\"\"\", tag=T3)\n\nS[\"gen\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">Generalist が集めた、人の手作業の記録(会社の発表)</div>\n <div style=\"display:flex;flex-direction:column;gap:30px;margin-top:10px\">\n <div style=\"display:flex;align-items:center;gap:30px\"><div style=\"width:260px;font-size:36px;font-weight:700;color:{SUB}\">2025年11月</div>\n <div style=\"width:{int(1060*27/50)}px;height:110px;background:#D3D3CE;border-radius:0 6px 6px 0\"></div><div style=\"font-size:52px;font-weight:900;white-space:nowrap\">27万時間</div></div>\n <div style=\"display:flex;align-items:center;gap:30px\"><div style=\"width:260px;font-size:36px;font-weight:700;color:{SUB}\">2026年4月</div>\n <div style=\"width:1060px;height:110px;background:{RED};border-radius:0 6px 6px 0\"></div><div style=\"font-size:52px;font-weight:900;white-space:nowrap\">50万時間超</div></div>\n </div>\n <div class=\"h3\" style=\"margin-top:44px\">安い装着型の機材で記録</div>\n</div>\n<div class=\"src\">出典:Generalist(GEN-0 2025年11月/GEN-1 2026年4月)</div>\"\"\", tag=T3)\n\nS[\"gen2\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">Generalist GEN-1.5(2026年8月)</div>\n <div class=\"h2\">簡単で短い作業なら、<br><span class=\"red\">数秒の実演</span>で新しい作業をこなす例</div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">本人たちも:作業は簡単で短く、成功率はまだ控えめ</div>\n</div>\n<div class=\"src\">出典:Generalist「GEN-1.5」(2026年8月19日)</div>\"\"\", tag=T3)\n\nS[\"compare\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">作業を記録した時間と、10万年は<br><span class=\"red\">そのまま比べられない</span></div>\n <div class=\"h3\" style=\"margin-top:50px\"><span class=\"muted\">何倍足りないか</span> より <span class=\"red\">どれだけ広く集められるか</span></div>\n</div>\"\"\", tag=T3)\n\nS[\"dirt\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">壁はまだ高い。でも、<br><span class=\"red\">壁の前で土を積み始めている</span></div>\n <ul class=\"list\" style=\"margin-top:30px\">\n <li style=\"font-size:40px\"><span class=\"n\">・</span>1X:家の中で遠隔で手伝いながら、動きを覚えさせる</li>\n <li style=\"font-size:40px\"><span class=\"n\">・</span>Generalist:人の手作業を装着型の機材で記録する</li>\n </ul>\n</div>\"\"\", tag=T3)\n\nS[\"notall\"] = page(f\"\"\"\n<div class=\"wrap\">\n {two(\"写真を見せて判断させる入口\", \"もう普通に使える\", \"物を確実に動かす\", \"すり合わせとデータが<br>まだ要る\", size=56)}\n <div class=\"h3\" style=\"margin-top:48px\">「まだ全然ダメ」でも「もう来た」でもない</div>\n</div>\"\"\", tag=T3)\n\nS[\"q\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h1\">ロボットのニュースを見る<br><span class=\"red\">問いが2つ</span>に変わる</div>\n</div>\"\"\", tag=T4)\n\nS[\"q1\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">問い 1</div>\n <div class=\"h1\">自律で動いているのか、<br>人が遠隔で動かしているのか</div>\n <div class=\"h3 muted\" style=\"margin-top:36px\">デモ動画で見るところ:自律か遠隔か/再生速度/人が手を出した条件</div>\n</div>\"\"\", tag=T4)\n\nS[\"q2\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">問い 2</div>\n <div class=\"h2\" style=\"margin-bottom:40px\">その仕事には、どちらが要るのか</div>\n {two(\"見て判断する\", \"手軽に試せる<br><span style='font-size:34px;color:#888888'>任せる範囲は現場で確かめる</span>\", \"触れて動かす精度\", \"現場の機体と環境に<br>すり合わせる仕事が残る\", size=48, note1=\"例:棚の商品が足りているか見る\", note2=\"例:柔らかい物を潰さずに掴む\")}\n</div>\"\"\", tag=T4)\n\nS[\"gofe\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">ゴールドバーグさんの近道</div>\n <div class=\"flow\">\n <div class=\"box\">昔ながらの工学<br>+学習</div><div class=\"arrow\">→</div>\n <div class=\"box\">現場で役に立つ<br>仕事をする</div><div class=\"arrow\">→</div>\n <div class=\"box\">働きながら<br>データが増える</div>\n </div>\n <div class=\"h3\" style=\"margin-top:48px\">僕の机の上:組み合わせる点は通じる。<span class=\"muted\">規模も段階もまだ違う</span></div>\n</div>\"\"\", tag=T4)\n\nS[\"motion\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">データの話と並行して要るもの</div>\n <div class=\"h1\"><span class=\"red\">どう動くか</span>の設計</div>\n <div class=\"h3\" style=\"margin-top:36px\">ロボットが人に物を渡すとき、<br>何秒待って手を開けば、人は安心して受け取れるのか</div>\n <div class=\"h3 muted\" style=\"margin-top:28px\">成功か失敗かの判定だけでは出てこない</div>\n</div>\"\"\", tag=T4)\n\nS[\"answer2\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">冒頭の問いへの答え</div>\n {two(\"写真を見せて判断させる入口\", \"ずっと簡単になった\", \"物を確実に動かす\", \"すり合わせが<br>まだ必要\", size=58)}\n <div class=\"h3\" style=\"margin-top:44px\">負担を減らす研究は急速に進んでいる。<br>家で家事を任せられる転換点かは、<span class=\"red\">まだ判断できない</span></div>\n</div>\"\"\")\n\nS[\"action\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">ロボットのデモ動画を見たら</div>\n <div class=\"h1\">自律なのか、何回やって、<br><span class=\"red\">何回うまくいった</span>のか</div>\n</div>\"\"\")\n\nS[\"sources\"] = page(\"\"\"\n<div class=\"wrap\" style=\"bottom:90px;justify-content:flex-start;top:120px\">\n <div class=\"h2\" style=\"margin-bottom:30px\">出典</div>\n <div class=\"p\" style=\"font-size:29px;line-height:1.75\">\n 人型ロボットの出荷:Counterpoint Research(2026年8月)/Smart Analytics Global(2026年8月)<br>\n 1X NEO:1X 公式の発表・注文ページ(2026年10月11日確認)<br>\n 10万年のデータの壁:Ken Goldberg, Science Robotics(2025年8月, Editorial)<br>\n Generalist:GEN-0(2025年11月)、GEN-1(2026年4月)、GEN-1.5(2026年8月)の公式発表<br>\n ほかの機体から学ぶ研究の例:Gemini Robotics 1.5 技術報告(Google DeepMind)<br>\n 実機の数字:Pixels to Motors の台帳(pick_place_ledger_v3.csv)、公開記事 DAY257<br>\n <span class=\"muted\">ナレーションは仮音声(Open JTalk/HTS voice nitech_jp_atr503_m001)</span>\n </div>\n</div>\"\"\")\n\n\n# --- 強弱をつける場面の上書き(2026-10-11)---\nS[\"hook_question\"] = punch(\"こういうニュースを見ると\", [\"ロボットも\", \"一気に賢くなる?\"])\nS[\"answer\"] = punch(\"先に結論\", ['見て判断する入口は<span class=\"blue\">簡単に</span>', '物を動かすのは<span class=\"red\">まだ手間</span>'], sub=\"手間は2つ:動作と結びついたデータ/機体や環境とのすり合わせ\", size=110)\nS[\"c1\"] = chapter2(\"1つ目\", \"出荷の中身\", \"出荷が4倍になったロボットは、何をしているのか\")\nS[\"c2\"] = chapter2(\"2つ目\", \"机の上の実感\", \"何が簡単で、何が難しかったのか\")\nS[\"c3\"] = chapter2(\"3つ目\", \"データの壁\", \"どれくらい高くて、誰がどう越えようとしているのか\")\nS[\"c4\"] = chapter2(\"最後に\", \"ニュースの見方\", \"僕の見立て\")\nS[\"china\"] = dots(97, \"2026年上半期の人型ロボット出荷のうち\", \"中国メーカー\", \"97%\", note=\"SAGの推計では「97%以上」\", src=\"出典:Smart Analytics Global(2026年8月)\", tag=T1)\nS[\"point2\"] = punch(\"この2つは\", [\"データ不足じゃない\", '<span class=\"red\">すり合わせ</span>の仕事'], sub=\"モデルや制御が、自分の機体と噛み合っているかを確かめる\", tag=T2)\nS[\"wall\"] = punch(\"ゴールドバーグさんの問題提起\", ['10万年の', '<span class=\"red\">データの壁</span>'], sub=\"動作と結びついた経験は集めにくい(同じ量が必要と分かったわけではない)\", kc=\"muted\", tag=T3)\nS[\"compare\"] = punch(\"ひとつ注意\", ['<span class=\"muted\">何倍足りないか</span>より', '<span class=\"red\">どれだけ広く</span>集めるか'], sub=\"作業を記録した時間と10万年は、そのまま比べられない\", size=110, tag=T3)\nS[\"answer2\"] = punch(\"冒頭の問いへの答え\", ['判断する入口は<span class=\"blue\">来た</span>', '家事を任せるのは<span class=\"red\">まだ</span>'], sub=\"負担を減らす研究は急速に進んでいる\", size=110)\n\n# 動画シーンに重ねるオーバーレイ(透過)\nOVERLAY_CSS = f\"\"\"\n*{{margin:0;padding:0;box-sizing:border-box}}\nhtml,body{{width:1920px;height:1080px;background:transparent;font-family:'Noto Sans CJK JP',sans-serif}}\n.lt{{position:absolute;left:96px;top:80px;background:rgba(245,245,240,.94);padding:28px 40px;border-radius:14px}}\n.lt .a{{height:30px;display:block;margin-bottom:6px}}\n.lt .b{{font-size:60px;font-weight:900;color:{INK};margin-top:4px}}\n.lt .c{{font-size:30px;font-weight:500;color:{SUB};margin-top:6px}}\n.cap{{position:absolute;left:96px;top:80px;background:rgba(13,13,13,.78);color:#fff;font-size:34px;font-weight:700;padding:14px 26px;border-radius:10px}}\n\"\"\"\ndef overlay(body):\n return f'<!doctype html><html><head><meta charset=\"utf-8\"><style>{OVERLAY_CSS}</style></head><body>{body}</body></html>'\n\nO = {\n \"title\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">エイイチ</div><div class=\"c\">デザイナー/ロボティクスの研究・開発</div></div>'),\n \"v_real\": overlay('<div class=\"cap\">実機:mechArm 270-Pi 赤いブロックを掴んで箱に置く</div>'),\n \"v_twin\": overlay('<div class=\"cap\">パソコンの中のアームの分身(Unity のデジタルツイン)</div>'),\n \"v_scan\": overlay('<div class=\"cap\">デジタルツインの中で動かして、関節の角度と爪先の位置の対応表を作る</div>'),\n \"outro\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">高評価・チャンネル登録</div><div class=\"c\">コメントは全部読んでいます</div></div>'),\n}\n\n# 動画シーン: (ソースファイル, 開始秒, 透かし消しの有無)\nVIDEO = {\n \"title\": (\"src/c1.mp4\", 0.0, True),\n \"v_real\": (\"src/c2.mp4\", 22.0, True),\n \"v_twin\": (\"src/c4.mov\", 0.0, False),\n \"v_scan\": (\"src/c3.mov\", 0.0, False),\n \"outro\": (\"src/c1.mp4\", 8.0, True),\n}</code></pre><h3 id=\"index_Na0vVVb3\">render.py(図解・テロップ・字幕を画像にする)</h3><pre><code class=\"language-python\">import json, sys, os, html\nsys.path.insert(0,'.')\nfrom scenes import S, O\nfrom playwright.sync_api import sync_playwright\nidx=json.load(open('tts/index.json'))\nos.makedirs('png/scene',exist_ok=True); os.makedirs('png/ovl',exist_ok=True); os.makedirs('png/sub',exist_ok=True)\nSUBCSS_OLD=\"\"\"*{margin:0;padding:0}html,body{width:1920px;height:1080px;background:transparent;font-family:'Noto Sans CJK JP',sans-serif}\n.s{position:absolute;left:140px;right:140px;bottom:56px;text-align:center;font-size:52px;font-weight:800;line-height:1.38;color:#fff;\n-webkit-text-stroke:9px #0D0D0D;paint-order:stroke fill;word-break:normal;line-break:strict}\"\"\"\nSUBCSS=\"\"\"*{margin:0;padding:0}html,body{width:1920px;height:1080px;background:transparent}\nsvg{position:absolute;left:0;top:0}\n.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}\"\"\"\nonly=sys.argv[1:]\ndef split2(t):\n if len(t)<=29: return html.escape(t)\n mid=len(t)/2; best=None\n for i,ch in enumerate(t[:-1]):\n if ch in '、。': \n if best is None or abs(i+1-mid)<abs(best-mid): best=i+1\n if best is None or max(best,len(t)-best)>31:\n # 読点で割れない場合は真ん中付近の助詞の後ろ\n for d in range(0,12):\n for i in (int(mid)+d,int(mid)-d):\n if 0<i<len(t) and t[i-1] in 'はがをにでとものへてや' and t[i] not in '、。」':\n return html.escape(t[:i])+'<br>'+html.escape(t[i:])\n return html.escape(t)\n return html.escape(t[:best])+'<br>'+html.escape(t[best:])\nwith sync_playwright() as p:\n b=p.chromium.launch(); pg=b.new_page(viewport={'width':1920,'height':1080})\n for k,h in S.items():\n if only and k not in only: continue\n pg.set_content(h); pg.wait_for_timeout(50); pg.screenshot(path=f'png/scene/{k}.png')\n for k,h in O.items():\n if only and k not in only: continue\n pg.set_content(h); pg.screenshot(path=f'png/ovl/{k}.png',omit_background=True)\n if not only:\n for o in idx:\n lines=split2(o['text']).split('<br>')\n fs=52\n svg_lines=''.join(f'<text x=\"960\" text-anchor=\"middle\" class=\"t\">{ln}</text>' for ln in lines)\n pg.set_content(f\"\"\"<!doctype html><html><head><meta charset=\"utf-8\"><style>{SUBCSS}</style></head><body>\n<svg id=\"s\" width=\"1920\" height=\"1080\" viewBox=\"0 0 1920 1080\">{svg_lines}</svg></body></html>\"\"\")\n pg.evaluate(\"\"\"() => {\n const ts=[...document.querySelectorAll('text.t')]; const lh=72; const n=ts.length;\n let fs=52; for (const t of ts){ const w=t.getComputedTextLength(); if (w>1640) fs=Math.min(fs, Math.floor(52*1640/w)); }\n ts.forEach((t,k)=>{ t.style.fontSize=fs+'px'; t.setAttribute('y', 1080-62-(n-1-k)*lh); });\n }\"\"\")\n pg.screenshot(path=f\"png/sub/{o['i']:03d}.png\",omit_background=True)\n b.close()\nmissing=[o['scene'] for o in idx if o['scene'] not in S and o['scene'] not in O]\nprint('missing scenes',sorted(set(missing)))</code></pre><h3 id=\"index_pTtPmPEL\">assemble.py(1文ずつ映像を作り、つないで音声と合わせる)</h3><pre><code class=\"language-python\">import json, subprocess, os, sys, wave\nimport numpy as np\nfrom concurrent.futures import ThreadPoolExecutor\nfrom PIL import Image\nsys.path.insert(0,'.')\nfrom scenes import VIDEO\nFPS=30; SR=48000\nidx=json.load(open('tts/index.json'))\nos.makedirs('chunks',exist_ok=True); os.makedirs('comp',exist_ok=True)\n# 尺(フレーム単位)\nfor o in idx:\n o['frames']=max(1,round((o['dur']+o['pause'])*FPS))\n# 動画シーンのオフセット(同じシーンが続く間は続きから)\nprev=None; off=0.0\nfor o in idx:\n sc=o['scene']\n if sc in VIDEO:\n if sc!=prev: off=VIDEO[sc][1]\n o['voff']=off; off+=o['frames']/FPS\n prev=sc\nENC=['-c:v','libx264','-preset','veryfast','-crf','20','-pix_fmt','yuv420p','-r',str(FPS),'-an']\ndef job(o):\n out=f\"chunks/{o['i']:03d}.mp4\"\n if os.path.exists(out) and '--force' not in sys.argv: return\n sub=f\"png/sub/{o['i']:03d}.png\"; sc=o['scene']; n=str(o['frames'])\n if sc in VIDEO:\n src,_,wm=VIDEO[sc]\n dl='delogo=x=2740:y=1840:w=1010:h=220,' if wm else ''\n fc=(f\"[0:v]{dl}scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080,fps={FPS},setsar=1,\"\n f\"drawbox=x=0:y=850:w=iw:h=230:color=black@0.5:t=fill[b];[b][1:v]overlay[c];[c][2:v]overlay[v]\")\n cmd=['ffmpeg','-v','error','-y','-ss',f\"{o['voff']:.3f}\",'-stream_loop','-1','-i',src,\n '-loop','1','-i',f'png/ovl/{sc}.png','-loop','1','-i',sub,'-filter_complex',fc,'-map','[v]','-frames:v',n]+ENC+[out]\n else:\n comp=f\"comp/{o['i']:03d}.png\"\n im=Image.open(f'png/scene/{sc}.png').convert('RGBA'); im.alpha_composite(Image.open(sub).convert('RGBA')); im.convert('RGB').save(comp)\n cmd=['ffmpeg','-v','error','-y','-loop','1','-framerate',str(FPS),'-i',comp,'-frames:v',n,'-tune','stillimage']+ENC+[out]\n subprocess.run(cmd,check=True)\nwith ThreadPoolExecutor(2) as ex: list(ex.map(job,idx))\n# 音声:各文を尺ちょうどに合わせて連結\nparts=[]\nfor o in idx:\n need=o['frames']*SR//FPS\n if o['text']:\n with wave.open(f\"tts/{o['i']:03d}.wav\") as w:\n a=np.frombuffer(w.readframes(w.getnframes()),dtype=np.int16)\n else: a=np.zeros(0,dtype=np.int16)\n a=a[:need]; parts.append(np.concatenate([a,np.zeros(need-len(a),dtype=np.int16)]))\nwith wave.open('narration.wav','wb') as w:\n w.setnchannels(1); w.setsampwidth(2); w.setframerate(SR); w.writeframes(np.concatenate(parts).tobytes())\nopen('chunks/list.txt','w').write(''.join(f\"file '{o['i']:03d}.mp4'\\n\" for o in idx))\n# 字幕 SRT\ndef ts(f):\n ms=round(f/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}\"\nt=0; srt=[]; k=1\nfor o in idx:\n if o['text']:\n srt.append(f\"{k}\\n{ts(t)} --> {ts(t+round(o['dur']*FPS))}\\n{o['text']}\\n\"); k+=1\n t+=o['frames']\nopen('out.srt','w',encoding='utf-8').write('\\n'.join(srt))\njson.dump(idx,open('timeline.json','w'),ensure_ascii=False)\nsubprocess.run(['ffmpeg','-v','error','-y','-f','concat','-safe','0','-i','chunks/list.txt','-i','narration.wav',\n '-map','0:v','-map','1:a','-c:v','copy','-af','loudnorm=I=-16:TP=-1.5:LRA=11','-c:a','aac','-b:a','192k','-ar','48000','-movflags','+faststart','out.mp4'],check=True)\nprint('total frames',t,'=',t/FPS,'s')</code></pre><h3 id=\"index_vy52HFp6\">analyze.py(最初の版で、間延びした文を見つけたスクリプト)</h3><pre><code class=\"language-python\">import json,re,subprocess\nidx=json.load(open('tts/index.json'))\nrows=[]\nfor o in idx:\n if not o['text']: continue\n log=open('tts/%03d.log'%o['i'],encoding='utf-8',errors='ignore').read()\n lab=log.split('[Output label]')[1].strip().splitlines()\n ph=[]\n for l in lab:\n m=re.match(r'(\\d+) (\\d+) \\S+?\\^\\S+?-(\\S+?)\\+',l)\n if m: ph.append((int(m.group(1))/1e7,int(m.group(2))/1e7,m.group(3)))\n scale=o['dur']/ph[-1][1]\n lead=ph[0][1]-ph[0][0]; tail=ph[-1][1]-ph[-1][0]\n longv=[(p,round((e-s)*scale,2)) for s,e,p in ph[1:-1] if p not in('pau','sil') and (e-s)*scale>0.22]\n pau=[round((e-s)*scale,2) for s,e,p in ph if p=='pau' and (e-s)*scale>0.35]\n gap=round((tail)*scale+o['pause'],2)\n rows.append((o['i'],o['text'][:30],round(lead*scale,2),round(tail*scale,2),o['pause'],longv,pau))\nprint('scale ex',scale)\nfor r in rows:\n if r[5] or r[6] or r[3]+r[4]+0.25>1.3:\n print(r)</code></pre><p></p><h2 id=\"index_c3VMNBPy\">実行ログ</h2><h3 id=\"index_U2U1WMpc\">台帳の試行回数を数えた結果</h3><pre><code>$ cd $HOME/mnt/runs; for f in pick_place_ledger_v3.csv handover_ledger_v1.csv; do echo \"== $f\"; python3 - \"$f\" <<'EOF'\nimport csv,sys,collections\np=sys.argv[1]\ntry:\n rows=list(csv.DictReader(open(p,encoding='utf-8-sig')))\nexcept Exception as e:\n print(\"ERR\",e); sys.exit()\nprint(\"rows\",len(rows))\ncols=rows[0].keys() if rows else []\nfor c in [\"place_verdict\",\"pick\",\"pick_stage\",\"status\",\"run_status\",\"verdict\"]:\n if c in cols:\n print(c, collections.Counter(r[c] for r in rows).most_common(8))\nfor c in [\"run_stem\",\"timestamp\"]:\n if c in cols and rows:\n print(c, rows[0][c], \"->\", rows[-1][c])\nEOF\ndone\n== pick_place_ledger_v3.csv\nrows 657\nplace_verdict [('', 259), ('yes', 217), ('no', 127), ('unknown', 54)]\nrun_stem run_0042_test -> run_0001_20260720_213323\ntimestamp 2026-06-18T00:00:00 -> 2026-07-20T21:35:11.608053\n== handover_ledger_v1.csv\nrows 8\nrun_stem run_handover_20260720_210544_315479_001 -> run_handover_20260720_210618_993522_008\ntimestamp 2026-07-20T21:05:45.451732+09:00 -> 2026-07-20T21:06:20.009752+09:00\n\n$ cd $HOME/mnt/runs && python3 - <<'EOF'\nimport csv,collections\nrows=list(csv.DictReader(open('pick_place_ledger_v3.csv',encoding='utf-8-sig')))\nts=sorted(r['timestamp'] for r in rows if r.get('timestamp'))\nprint(\"min\",ts[0],\"max\",ts[-1])\ntest=[r for r in rows if any(k in r['run_stem'].lower() for k in ('test','dry','sim','mock'))]\nprint(\"test-like\",len(test))\nreal=[r for r in rows if r not in test]\nprint(\"real\",len(real), collections.Counter(r['place_verdict'] for r in real))\nprint([c for c in rows[0].keys()][:12])\nEOF\nls run_pick_place | wc -l\nmin 2026-06-18T00:00:00 max 2026-07-20T21:35:11.608053\ntest-like 1\nreal 656 Counter({'': 259, 'yes': 216, 'no': 127, 'unknown': 54})\n['timestamp', 'run_stem', 'trial_no', 'pick_verdict', 'pick_reason', 'pick_confidence', 'place_verdict', 'place_reason', 'place_confidence', 'block_pick_x', 'block_pick_z', 'block_place_x']\n758</code></pre><h3 id=\"index_a4QOCEhV\">間延びした文の検出結果(最初の版)</h3><pre><code>$ python3 analyze.py | python3 -c \"\nimport sys,ast\nfor line in sys.stdin:\n if line.startswith('('):\n r=ast.literal_eval(line)\n if r[5]: print(r[0], r[1], r[5][:8])\n\"\n13 現実の世界で物に触れて動かす技術のことなんですよ。 [('N', 0.47), ('d', 0.47), ('e', 0.47), ('s', 0.47), ('U', 0.41), ('y', 0.46), ('o', 0.47)]\n88 ChatGPTのようなAIの学習に使われたインターネットの文 [('ts', 0.48), ('U', 0.49), ('k', 0.46), ('a', 0.48), ('w', 0.49), ('a', 0.49), ('r', 0.48), ('e', 0.49)]\n92 ChatGPTは10万年分の教科書で育ったけれど、 [('s', 0.48), ('o', 0.44), ('d', 0.48), ('a', 0.48), ('cl', 0.48), ('t', 0.48), ('a', 0.48), ('k', 0.48)]\n94 ここで、じゃあロボットにChatGPTの瞬間は当分来ない、 [('t', 0.47), ('o', 0.48), ('o', 0.47), ('b', 0.48), ('u', 0.47), ('N', 0.45), ('k', 0.47), ('o', 0.48)]\n109 1Xが家の中で遠隔操作しながらデータを集めているのも、 [('i', 0.47), ('r', 0.42), ('u', 0.47), ('n', 0.46), ('o', 0.47), ('m', 0.47), ('o', 0.47)]\n110 出荷されたロボットの多くがデータ集めに回っているのも、 [('n', 0.45), ('o', 0.45), ('m', 0.45), ('o', 0.45)]</code></pre><h3 id=\"index_H1VyBuua\">6文を区切って作り直した結果</h3><pre><code>$ python3 fixtts.py 13 88 92 94 109 110\n13 ['現実の世界で物に、', '触れて動かす技術のことなんですよ。'] max phoneme 0.11s dur 7.25 -> 4.62\n88 ['チャットジーピーティーのようなエーアイの、', '学習に使われたインターネットの文章を、'] max phoneme 0.14s dur 17.55 -> 5.45\n92 ['チャットジーピーティーは、', '10万年分の教科書で育ったけれど、'] max phoneme 0.14s dur 9.71 -> 4.49\n94 ['ここで、', 'じゃあロボットにチャットジーピーティーの、', '瞬間は当分来ない、'] max phoneme 0.13s dur 9.59 -> 5.01\n109 ['ワンエックスが家の中で遠隔操作しなが、', 'らデータを集めているのも、'] max phoneme 0.17s dur 7.80 -> 5.02\n110 ['出荷されたロボットの多くが、', 'データ集めに回っているのも、'] max phoneme 0.15s dur 5.72 -> 4.38\n\n(109 が「しなが/ら」で切れたため、区切る位置を手で指定して作り直した)\n$ python3 fixtts.py 13 109\n13 ['現実の世界で、', '物に触れて動かす技術のことなんですよ。'] max phoneme 0.11s dur 4.62 -> 4.59\n109 ['ワンエックスが、', '家の中で遠隔操作しながら、', 'データを集めているのも、'] max phoneme 0.17s dur 5.02 -> 5.29</code></pre><h3 id=\"index_qk7nYpvW\">台本の書き直し後に、全文を検査して作り直した結果</h3><pre><code>$ python3 tts2.py (台本の書き直し後の1回目。読点の追加なし)\nREAD 1 世界で2万2000台を超えました。前の年の同じ時 → セカイデニマンニセンダイヲコエマシ’タ、マエノトシノオナジジキノ、オヨソヨンバイデス’、\nREAD 2 年末までには、5万台を超えると予想されています。 → ネンマツマデニワ、ゴマンダイヲコエルトヨソーサレテイマス’、\nREAD 7 僕は秋田で29年デザイナーをやりながら、 → ボクワアキ’タデニジューキューネンデザイナーヲヤリナガラ、\nREAD 18 その手間は大きく2つあります。 → ソノテマワオーキ’クフ’タツアリマス’、\nREAD 22 大きく3つあります。 → オーキ’クミッツアリマス’、\nREAD 23 出荷が4倍になったロボットは、何をしているのか。 → シュッカガヨンバイニナッタロボットワ、ナニヲシ’テイルノカ、\nREAD 27 1つ目、出荷が4倍になったロボットは、何をしてい → ヒ’トツメ、シュッカガヨンバイニナッタロボットワ、ナニヲシ’テイルノカ、デス’、\nREAD 28 Counterpointの推計では、出荷された人 → カウンターポイントノスイケーデワ、シュッカサレタヒ’トガタロボットノロクワリイジョーガ、\n36 同じ半年の出荷を約1万9100台と推計していて、 STRETCH 0.47s -> split 1 parts, max 0.47s\nREAD 36 同じ半年の出荷を約1万9100台と推計していて、 → オナジハントシノシュッカヲヤクイチマンキューセンヒャクダイトスイケーシ’テイテ、\nREAD 37 産業・商業用途が7割を超えた、としています。 → サンギョー、ショーギョーヨートガナナワリヲコエタ、トシ’テイマス’、\nREAD 38 総数も分類も違うので、この2つは、単純な裏表とし → ソースーモブンルイモチガウノデ、コノフ’タツワ、タンジュンナウラオモテトシ’テワクラベラレマセン、\nREAD 40 もう1つ、SAGの推計では、出荷の97%以上が中 → モーヒ’トツ、エスエージーノスイケーデワ、シュッカノキュージューナナパーセントイジョーガチューゴクノメーカーデス’、\nREAD 42 アメリカの1Xという会社は、家事をする人型ロボッ → アメリカノワンエック’ストイウカイシャワ、カジヲスルヒ’トガタロボット、ネオノヨヤクヲ、キョネンノジューガツニハジメマシ\nREAD 43 2万ドルの早期購入枠は、今年配送の予定で、月49 → ニマンドルノソーキ’コーニューワクワ、コトシ’ハイソーノヨテーデ、ツ’キヨンヒャク’キュージューキュードルノプランワ、ソ\nREAD 45 予約した時間に、1Xの担当者が遠隔で手伝う仕組み → ヨヤクシ’タジカンニ、ワンエック’スノタントーシャガエンカクデテツダウシ’クミナンデス’ヨ、\nREAD 46 10月11日の時点で、一般のお客さんへの配送開始 → ジューガツジューイチニチノジテンデ、イッパンノオキャク’サンエノハイソーカイシヲシメス’コーシ’キハッピョーワ、\n51 工場でも家でも、使いながらデータを集める動きが出てきているん STRETCH 0.47s -> split 2 parts, max 0.48s\nREAD 54 2つ目、僕の机の上のロボットアームで、何が簡単で → フ’タツメ、ボクノツ’クエノウエノロボットアームデ、ナニガカンタンデ、ナニガムズカシカッタノカ、デス’、\nREAD 56 僕は自宅の机で、mechArm 270という小さ → ボクワジタクノツ’クエデ、メックアーム、ニーナナゼロトイウチーサナロボットアームヲウゴカシ’テイマス’、\nREAD 58 それだけの作業を、失敗も含めて650回以上試して → ソレダケノサギョーヲ、シッパイモフ’クメテロッピャクゴジュッカイイジョータメシテ、ゼンブダイチョーニノコシ’テイマス’、\nREAD 65 ロボットアームの爪先がどこに来るかは、普通は関節 → ロボットアームノツメサキガドコニクルカワ、フ’ツーワカンセツノカクドカラケーサンデダシマス’、\nREAD 66 ところが、ikpyという計算ツールで出した爪先の → トコロガ、アイケーパイトイウケーサンツールデダシ’タツメサキノイチ’ト、\nREAD 68 僕の設定では、同じ関節の角度なのに、30cm以上 → ボクノセッテーデワ、オナジカンセツノカクドナノニ、サンジュッセンチイジョークイチガッタンデス’、\nREAD 70 関節の角度と爪先の位置の対応表を作りました。 → カンセツノカクドトツメサキノイチノタイオーヒョーヲツ’クリマシ’タ、\nREAD 72 もう1つ困ったのが、黙る失敗です。 → モーヒ’トツ’コマッタノガ、ダマルシッパイデス’、\nREAD 73 僕の機体とソフトの組み合わせでは、肩の関節に、9 → ボクノキ’タイトソフ’トノクミアワセデワ、カタノカンセツニ、キュージュードチョットヲコエルネヲオクルト、\nREAD 75 ログを1行ずつ見て、やっと気づきました。 → ログヲイチギョーズツミテ、ヤットキズキマシ’タ、\nREAD 77 この2つは、AIの学習データが足りない、という話 → コノフ’タツワ、エーアイノガク’シューデータガタリナイ、トイウハナシトワベツデス’、\n79 ほかの環境で学んだことを持ってくる研究も進んでいます。 STRETCH 0.48s -> split 1 parts, max 0.48s\nREAD 81 じゃあ、もう1つの手間、データのほうは、業界全体 → ジャー、モーヒ’トツノテマ、データノホーワ、ギョーカイゼンタイデドーナッテイルノカ、\nREAD 82 3つ目、データの壁はどれくらい高くて、誰がどう越 → ミッツメ、データノカベワドレクライタカク’テ、ダレガドーコエヨートシ’テイルノカ、デス’、\nREAD 86 人が読んだり見たりする時間に換算すると、約10万 → ヒ’トガヨンダリミタリスルジカンニカンサンスルト、ヤクジューマンネンキボニナル、\nREAD 88 ゴールドバーグさんは、これを「10万年のデータの → ゴールドバーグサンワ、コレヲジューマンネンノデータノカベトヨンデイマス’、\n91 ここで、じゃあロボットにChatGPTの瞬間は当分来ない、 STRETCH 0.48s -> split 2 parts, max 0.48s\nREAD 95 会社の発表では、去年の11月に27万時間、今年の → カイシャノハッピョーデワ、キョネンノジューイチガツニニジューナナマンジカン、コトシノシガツニゴジューマンジカンヲコエマシ\nREAD 96 さらに8月には、簡単で短い作業なら、 → サラニハチガツニワ、カンタンデミジカイサギョーナラ、\nREAD 100 ここで1つ、注意があります。 → ココデヒ’トツ、チューイガアリマス’、\nREAD 101 作業を記録した時間と、10万年という数字は、その → サギョーヲキロクシ’タジカント、ジューマンネントイウスージワ、ソノママクラベラレマセン、\n104 1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、 STRETCH 0.47s -> split 1 parts, max 0.47s\nREAD 104 1Xが家の中で遠隔で手伝いながら動きを覚えさせて → ワンエック’スガイエノナカデエンカクデテツダイナガラウゴキヲオボエサセテイルノモ、\nREAD 109 じゃあ、この2つが混ざっている今、ロボットのニュ → ジャー、コノフ’タツガマザッテイルイマ、ロボットノニュースノナニヲミレバイイノカ、\nREAD 111 ロボットのニュースを見たときの問いが、2つに変わ → ロボットノニュースヲミタトキノトイガ、フ’タツニカワルンデス’ヨ、\nREAD 112 1つは、そのロボットは自律で動いているのか、人が → ヒ’トツワ、ソノロボットワジリツデウゴイテイルノカ、ヒ’トガエンカクデウゴカシ’テイルノカ、デス’、\nREAD 115 もう1つは、その仕事は「見て判断する」で済むのか → モーヒ’トツワ、ソノシゴトワミテハンダンスルデスムノカ、\nREAD 125 で、作る側として、もう1つ言わせてください。 → デ、ツ’クルガワトシ’テ、モーヒ’トツイワセテクダサイ、\nREAD 126 データの話と並行して、もう1つ要るのが「どう動く → データノハナシトヘーコーシ’テ、モーヒ’トツイルノガドーウゴク’カノセッケーデス’、\nsegments 144 total 13:45\n\n(どの音で伸びているかを調べた結果。* は0.3秒を超えた音)\ntts/036.log sil* o n a j i h a N t o sh i n o sh u cl k a o y a k u i ch i m a N ky u u s e N hy a k u d a i t o s u i k e e sh* I* t* e* i* t* e* sil*\ntts/051_p1.log sil ts U k a i n a g a r a d e e t a o a ts u m e r u u g o k i g a d e t e k i t e i r u N* d* e* s* U* y* o* sil*\ntts/079.log sil h o k a n o k a N ky o o d e m a n a N d a k o t o o m o cl t e k u r u k e N ky u u m o s* u* s* u* N* d* e* i* m* a* s* U* sil*\ntts/091_p1.log sil* j a a r o b o cl t o n i ch a cl t o j i i p i i t i i n o sh u N k a N w a t* o* o* b* u* N* k* o* n* a* i* sil*\ntts/104.log sil* w a N e cl k U s u g a i e n o n a k a d e e N k a k u d e t e ts u d a i n a g a r a u g o k i o o b o e s a s e t e i* r* u* n* o* m* o* sil*\n\n(5文の OVERRIDE に読点を足し、「値を送る → あたいを送る」を読み替え表に足して作り直した)\n$ python3 tts2.py | grep -v ^READ\nsegments 144 total 13:32\n[] ← 間延びのあった文の一覧(0件)\nボクノキ’タイトソフ’トノクミアワセデワ、カタノカンセツニ、キュージュードチョットヲコエルアタイヲオクルト、\nオナジハントシノシュッカヲ、ヤクイチマンキューセンヒャクダイト、スイケーシ’テイテ、\nロボットアームノツメサキガドコニクルカワ、フ’ツーワカンセツノカクドカラケーサンデダシマス’、</code></pre><h3 id=\"index_sWdoCk8D\">色の検証(validate_palette.js)</h3><pre><code>$ node scripts/validate_palette.js \"#2A7A9B,#D9542B\" --mode light --surface \"#F4F2ED\"\nPalette (light, surface #F4F2ED, categorical): 2 slots\n [PASS] Lightness band all 2 inside L 0.43–0.77\n [FAIL] Chroma floor below floor (reads gray): [[\"#2A7A9B\",0.091]]\n [PASS] CVD separation worst adjacent #D9542B↔#2A7A9B ΔE 15.7 (protan) · tritan 32.3\n [PASS] Normal-vision floor worst adjacent #D9542B↔#2A7A9B ΔE 27.5 (normal)\n [PASS] Contrast vs surface all 2 >= 3:1\n\n → FAILED — fix the marked checks (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)\n scope: categorical palettes only. For a lone status/text color check WCAG text contrast; for a sequential ramp, lightness monotonicity.\n\n$ node scripts/validate_palette.js \"#1F7FB0,#D9542B\" --mode light --surface \"#F4F2ED\"\n [PASS] Lightness band all 2 inside L 0.43–0.77\n [PASS] Chroma floor all 2 >= 0.1\n [PASS] CVD separation worst adjacent #D9542B↔#1F7FB0 ΔE 18.7 (protan) · tritan 32.3\n [PASS] Normal-vision floor worst adjacent #D9542B↔#1F7FB0 ΔE 28.9 (normal)\n [PASS] Contrast vs surface all 2 >= 3:1\n → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)\n\n(記事サムネイルの配色に合わせたあと)\n$ node scripts/validate_palette.js \"#1A6C98,#C4471F\" --mode light --surface \"#F5F5F0\"\n [PASS] Lightness band all 2 inside L 0.43–0.77\n [PASS] Chroma floor all 2 >= 0.1\n [PASS] CVD separation worst adjacent #C4471F↔#1A6C98 ΔE 17.2 (protan) · tritan 30.4\n [PASS] Normal-vision floor worst adjacent #C4471F↔#1A6C98 ΔE 27.2 (normal)\n [PASS] Contrast vs surface all 2 >= 3:1\n → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)</code></pre><h3 id=\"index_YpOeZt_F\">書き出しの結果</h3><pre><code>(試作1本目・153文)\ntotal frames 24385 = 812.8333333333334 s\nstream|codec_name=h264|width=1920|height=1080\nstream|codec_name=aac\nformat|duration=812.900000\n\n(間延びした6文を直したあと)\ntotal frames 23532 = 784.4 s\nvideo,784.400000\naudio,784.400000\n\n(codex の指摘・デザイン・読み・自己紹介を反映したあと・144文)\ntotal frames 24448 = 814.9333333333333 s\nvideo,814.933333\naudio,815.000000\n\n(記事サムネイルの配色・ロゴ・「毎日」の削除を反映したあと)\ntotal frames 24434 = 814.4666666666667 s\n\n(30cm の説明2文を足したあと・146文)\ntotal frames 24891 = 829.7 s\n\n(打ち出し・章の扉・点の並びを入れたあと)\ntotal frames 24891 = 829.7 s\n-rw-r--r-- 1 root root 47663796 Oct 11 22:01 /tmp/claude-0/-home-claude/9041d877-fbc0-57ad-97c5-f63dfd4877c5/scratchpad/v/out.mp4\n</code></pre><h3 id=\"index_ImJl488D\">KIOXIA への転送で、1つ目の断片だけが化けた記録</h3><pre><code>(作業環境側で12MBずつに分けた断片の md5)\nb3a5e3da6efb449d63049eb14400a1d3 p_00\nbff9ad51719a351b1698a90a11152082 p_01\n32d70b67218b8e9594637b6e06dadc7b p_02\nc41f4eedb61bdd951e2484d85e6f6691 p_03\n元の作業用 mp4: a54a7d2fa84886ceab3237618ffd5838\n\n(Mac 側に届いた断片の md5。p_00 だけ一致しない)\nda313675885a59bd4988dbe2ce6270ee p_00\nbff9ad51719a351b1698a90a11152082 p_01\n32d70b67218b8e9594637b6e06dadc7b p_02\nc41f4eedb61bdd951e2484d85e6f6691 p_03\n-rwx------ 12588787 Oct 11 12:44 p_00 ← 元は 12582912 バイト(5,875 バイト多い)\n-rwx------ 12582912 Oct 11 12:44 p_01\n-rwx------ 12582912 Oct 11 12:45 p_02\n-rwx------ 9186276 Oct 11 12:45 p_03\nつないだ mp4: e5e65bee0bd4cf47694b33163d54adc3 ← 元と一致しない\n\n(p_00 だけ gzip で包んで送り直し、Mac 側で展開してからつないだ)\n12582912 b3a5e3da6efb449d63049eb14400a1d3 ← 展開した p_00。元と一致\na54a7d2fa84886ceab3237618ffd5838 ← つないだ作業用 mp4。元と一致\n\n(最終版でも同じ手順で送った)\n元の作業用 mp4: 2dd0e053c2da238bbd073bb7bf8b12ad\nつないだ作業用 mp4: 2dd0e053c2da238bbd073bb7bf8b12ad</code></pre><p></p><h2 id=\"index_o0bVwuXE\">codex への検証依頼の全文</h2><p>台本の全文は、検証にかけた時点の版です(このあと書き直しています)。</p><pre><code class=\"language-markdown\"># 敵対的検証依頼:YouTube台本「ロボットにもChatGPTの瞬間はもう来たのか」\n\n## あなたへの依頼\n\nこれから公開する解説動画(約13分)の台本を、**この台本を論破するつもりで**検証してください。褒める必要はありません。見落としを1つでも多く見つけることが目的です。\n\n- 台本の書き手:Pixels to Motors(エイイチ)。秋田でロボットアーム(mechArm 270-Pi)を自宅で動かし、記録を公開している個人。29年のデザイナー経験。\n- 視聴者:フィジカルAIに関心はあるが専門家ではない日本語話者\n- 台本の結論:「見て判断する」ところは安くなった(ChatGPTの瞬間がもう来た)。「触れて動かす」ところはまだ高い(データの壁があり、まだ来ていない)\n- 検証日:2026年10月11日。Webを参照できる場合は一次情報まで遡って確認してください。参照できない場合は「未確認」と明記し、推測で OK を出さないでください\n\n## 検証してほしい観点\n\n### A. 事実と数字\nA1. 各数字が出典に実際に書かれているか。出典の数字を取り違えていないか(期間・対象・単位)\nA2. 出典自体の信頼性。調査会社のプレスリリース、企業の自社発表、二次報道の区別がついているか\nA3. 2026年10月11日時点で古くなっている情報がないか(特に 1X NEO の出荷状況、Generalist の最新発表、出荷台数)\nA4. 本人の実機の数字(S061 の650回以上、S068〜S071 の30cm以上、S074 の170万点、S076〜S077 の90度ちょっと)が、話の文脈で誤解を生まない言い方になっているか\n\n### B. 論理\nB1. 結論(S143〜S144)「見て判断する部分にはもう来ている」は、台本の中で示した根拠(本人の判定の体験談1つ)だけで言い切れるか。反例はないか\nB2. 「データの壁」の説明(S105〜S108:10万年 vs 57年、約1700分の1)は比較として成立しているか。台本の断り書き(単位をそろえた比較ではない)で十分か\nB3. (S028〜S039)「出荷ロボットの6割以上が見せる用途・研究・データ集め」と「SAG では工場・店舗向けが7割超」を並べた扱いは公正か。どちらかに寄せた印象操作になっていないか\nB4. 本人の机の上の体験(ikpy のずれ、黙る失敗)から、業界全体の「触れて動かすはまだ高い」へ一般化してよいか。飛躍がないか\nB5. 「人型ロボット」の話と「ロボットアーム」の話、「フィジカルAI」全体(自動運転を含む)の話が混ざって、範囲がぶれていないか\n\n### C. 抜けている反論・視点\nC1. 台本の主張に対して、専門家なら真っ先に出す反論は何か(例:シミュレーション合成データ、世界モデル、VLA の汎化など)\nC2. 触れずに済ませているが、視聴者の判断を変えうる事実はないか\n\n### D. 言い方\nD1. 断定が強すぎる文、逆に曖昧すぎて意味のない文\nD2. 誤解を生む用語の使い方(例:「フィジカルAI」「データ集め」「ChatGPTの瞬間」の定義)\nD3. 特定の企業・人物について、不公平・不正確な描写\n\n## 回答の書式\n\n指摘ごとに次の形で書いてください。重大なものから順に並べてください。\n\n```\n[❌ NG / ⚠️ 要注意] 文番号(例:S043)\n問題:何が問題か(1〜2文)\n根拠:確認した出典(URL)または推論の筋道\n修正案:差し替える文(台本の話し言葉の調子に合わせて)\n```\n\n最後に次の3点をまとめてください。\n1. このまま公開してよいか(はい/直してから/大きく作り直すべき)\n2. 公開前に必ず直すべき指摘の文番号\n3. 台本の結論そのものへの評価(成立している/条件つきで成立/成立していない)と、その理由\n\n---\n\n## 台本が使った出典\n\n| 主張 | 文番号 | 出典 |\n|---|---|---|\n| 2026年上半期の人型ロボット出荷2万2000台超・前年同期の約4倍・通年5万台超の予想・用途の6割以上が見せる用途と研究・データ集め | S001〜S005, S028〜S029 | Counterpoint Research(2026年8月)https://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026 ※台本作成時はページ本文を読み込めず、検索結果の抜粋から数字を取った |\n| 同期間:中国メーカー97%超・AGIBOT 44%・Unitree 31%・工場や店舗向け70%超 | S034〜S035, S041〜S042 | Smart Analytics Global の調査を紹介した TechRepublic(2026年8月11日)https://techrepublic.com/article/news-humanoid-robot-shipments-chinese-vendors |\n| 1X NEO:2025年10月に予約開始・2万ドルまたは月499ドル・難しい作業は担当者が遠隔操作・2026年10月初めの時点で顧客への出荷の発表なし | S044〜S049 | 1X 予約ページ/botinfo.ai(2026年10月5日確認)https://botinfo.ai/articles/1x-neo-home-robot / AI2Work https://ai2.work/blog/1x-neo-ships-into-homes-with-a-human-teleoperator-still-inside |\n| インターネットの文章を人が読むと約10万年(10万年のデータの壁)、狭い仕事を確実にこなすロボットを現場に置いて働きながらデータを集めるのが近道 | S087〜S096, S130〜S132 | Ken Goldberg, Science Robotics(2025年8月)https://news.berkeley.edu/?p=134910 |\n| Generalist:27万時間(2025年11月)→50万時間超(2026年4月)・新しいロボットや作業に合わせるデータは約1時間の場合も・選んだ簡単な作業で成功率99%超 | S097〜S104 | Generalist の発表を紹介した記事 https://yespress.io/generalist / https://www.humanoidsdaily.com/news/generalist-ai-unveils-gen-0-claims-scaling-laws-for-robotics-backed-by-270-000-hours-of-real-world-data |\n| 本人の実機:PICK→PLACE の通し試行656回(2026年6月18日〜7月20日)、FKテーブル約170万点、ikpy とデジタルツインの爪先が同じ関節角で300mm以上ずれる、j1 が約90.7度を超えると send_angles がエラーなしで無視される | S061, S068〜S071, S074, S076〜S077 | 本人の台帳 pick_place_ledger_v3.csv、公開記事 DAY257・DAY283、設計メモ arm_kinematics.md(非公開資料を含むため、ここは記述の妥当性だけ見てください) |\n\n## 台本全文(動画で読み上げている版)\n\n\n### 0 フック\n\nS001 今年の1月から6月までに、世界で出荷された人型ロボットは、2万2000台を超えました。\n\n### 0 フック\n\nS002 前の年の同じ時期の、およそ4倍です。\n\n### 0 フック\n\nS003 調査会社のCounterpointが、8月に出した数字です。\nS004 年末までには、5万台を超えると予想されています。\nS005 家事をする人型ロボットの予約販売も、去年の秋から始まっています。\nS006 こういうニュースを見ると、ChatGPTが出たときみたいに、\nS007 ロボットも一気に賢くなって、家に来るのはもうすぐ、って感じませんか。\n\n### 1 自己紹介\n\nS008 Pixels to Motors。こんにちは、エイイチです。\n\n### 1 自己紹介\n\nS009 僕は秋田で29年デザイナーをやりながら、\n\n### 1 自己紹介\n\nS010 自宅の机の上でロボットアームを動かして、その記録を毎日公開しています。\n\n### 1 自己紹介\n\nS011 作る側の机の上から見える、フィジカルAIの話をします。\n\n### 2 今日の問い\n\nS012 今日の問いは、ロボットにもChatGPTのような瞬間は、もう来たのか、です。\nS013 フィジカルAIっていうのは、AIに体を持たせて、\nS014 現実の世界で物に触れて動かす技術のことなんですよ。\nS015 ロボットアームも、人型ロボットも、自動運転もここに入ります。\n\n### 3 結論と立場\n\nS016 先に結論を言っちゃうと、安くなったのは「見て判断する」ところで、\n\n### 3 結論と立場\n\nS017 「触れて動かす」ところは、まだ高いんですよ。\n\n### 3 結論と立場\n\nS018 鍵になるのが、「データの壁」です。\nS019 先に言っておくと、フィジカルAIはまだ使えない、と言いたいわけじゃないんです。\nS020 どこまで来ていて、どこで止まっているのかを、分けて見たいんです。\n\n### 4 目次\n\nS021 大きく3つあります。\n\n### 4 目次\n\nS022 出荷が4倍になったロボットは、何をしているのか。\n\n### 4 目次\n\nS023 僕の机の上のロボットアームで、何が簡単で、何が難しかったのか。\n\n### 4 目次\n\nS024 そして、データの壁はどれくらい高くて、誰がどう越えようとしているのか。\n\n### 4 目次\n\nS025 最後に僕の見立てを話します。\n\n### 5 節1:出荷されたロボットは何をしているのか\n\nS026 1つ目、出荷が4倍になったロボットは、何をしているのか、です。\nS027 まず数字の中身を見ます。\nS028 Counterpointによると、出荷された人型ロボットのうち6割以上は、\nS029 展示やイベントで動きを見せる用途と、研究やデータ集めの用途なんですよ。\nS030 データ集めっていうのは、人がロボットを遠隔で動かして、その動きを記録して、\nS031 AIが学ぶための教材を作る仕事のことです。\nS032 つまり、ロボットの大きな買い手の1つは、ロボットに教えるためのロボットなんです。\nS033 ただ、ここで1つ断っておきます。\nS034 別の調査会社のSmart Analytics Globalは、同じ半年について、\nS035 工場や店舗向けが7割を超えた、と言っているんですよ。\nS036 同じ期間なのに、ほぼ逆の数字です。\nS037 用途の分け方が、会社によって違うんだと思います。\nS038 だから「全部見せ物だ」とも、「もう工場で働いている」とも言い切れない。\nS039 そこは正直に言っておきます。\nS040 はっきりしているのは、どこが作っているかです。\nS041 SAGの数字だと、出荷の97%以上が中国のメーカーです。\nS042 上位のAGIBOTとUnitreeの2社だけで、およそ4分の3なんですよ。\nS043 家のほうも見てみます。\nS044 アメリカの1Xという会社は、家事をする人型ロボット、NEOの予約を、去年の10月に始めました。\nS045 値段は2万ドル。月499ドルのプランもあります。\nS046 ただ、難しい家事は、1Xの担当者がVRゴーグルをつけて、\nS047 ロボットのカメラ越しに家の中を見ながら、遠隔で動かす仕組みなんですよ。\nS048 今年中にアメリカで届け始める予定ですが、\nS049 10月の初めの時点で、お客さんの家に届いたという発表は、まだありません。\nS050 これ、ちょっと考えさせられませんか。\nS051 売っているのはロボットだけじゃなくて、遠隔で動かす人と、\nS052 そこで集まるデータの仕組みごとなんです。\nS053 で、ここが大事なんですけど、工場の話にも家の話にも、\nS054 データを集めるという、同じ目的が出てくるんですよ。\nS055 じゃあ、なぜそこまでしてデータを集めないといけないのか。\nS056 その前に、僕の机の上の話をさせてください。\n\n### 6 節2:机の上のロボットアームで、簡単だったこと・難しかったこと\n\nS057 2つ目、僕の机の上のロボットアームで、何が簡単で、何が難しかったのか、です。\nS058 ここは僕自身の話から始めさせてください。\nS059 僕は自宅の机で、mechArm 270という小さなロボットアームを動かしています。\nS060 赤いブロックを掴んで、箱に置く。\nS061 それだけの作業を650回以上試して、全部の結果を台帳に残しているんですよ。\nS062 まず、簡単だったほうから話します。掴めたかどうかの判定です。\nS063 iPhoneで撮った写真をAIに見せて、「ブロックを掴めていますか」と聞くと、\nS064 はい、いいえで答えが返ってくる。\nS065 少し前なら、ここを作るだけで大仕事だったはずなんですよ。\nS066 難しかったのは、動かすほうでした。\nS067 ロボットアームの爪の先がどこに来るかは、普通は関節の角度から計算で出します。\nS068 ところが、ikpyという、よく使われる計算ツールで出した爪先の位置と、\nS069 パソコンの中に作ったアームの分身で、実際に動かした位置が、\nS070 同じ関節の角度なのに、30cm以上ずれたんです。\nS071 30cmですよ。机の上のブロックなら、端から端まで外れます。\nS072 だから僕は、その分身、デジタルツインの中でアームを実際に動かして、\nS073 爪先がどこに来るかを、1点ずつ測って表にしました。\nS074 その表は今、170万点ほどあります。\nS075 もう1つ困ったのが、黙る失敗です。\nS076 肩の関節に、90度ちょっとを超える角度を送ると、\nS077 エラーも出さずに、指令が黙って捨てられるんですよ。\nS078 アームは動かない。でも、何も言ってこない。\nS079 ログを1行ずつ見て、やっと気づきました。\nS080 でも、ポイントはここなんです。\nS081 見て判断するところは、AIに頼めばすぐ手に入りました。\nS082 でも、爪先がどこに来るか、指令がちゃんと届いたかは、\nS083 自分の机の上で、自分のアームで測るしかなかった。\nS084 インターネットのどこにも、僕のアームの、僕の机の上のデータはないんですよ。\nS085 じゃあ、この「どこにもないデータ」は、世界全体ではどれくらい足りないのか。\n\n### 7 節3:データの壁の高さと、越えようとしている会社\n\nS086 3つ目、データの壁はどれくらい高くて、誰がどう越えようとしているのか、です。\nS087 カリフォルニア大学バークレー校のロボット研究者、\nS088 ケン・ゴールドバーグさんが、去年、面白い計算をしています。\nS089 ChatGPTのようなAIの学習に使われたインターネットの文章を、\nS090 人が全部読むと何年かかるか。答えは、約10万年なんですよ。\nS091 ロボットが手を動かすためのデータは、その足元にも及ばない。\nS092 ゴールドバーグさんは、これを「10万年のデータの壁」と呼んでいます。\nS093 ChatGPTは10万年分の教科書で育ったけれど、\nS094 ロボットには、その教科書がまだない、ということなんです。\nS095 ここで、じゃあロボットにChatGPTの瞬間は当分来ない、\nS096 と言いたくなるじゃないですか。でも、ちょっと待ってほしいんです。\nS097 アメリカのGeneralistという会社は、人が手で作業する様子を、\nS098 安い装着型の機材で記録して集めています。\nS099 去年の11月の時点で27万時間、今年の4月には50万時間を超えました。\nS100 半年で2倍近くです。\nS101 しかもこの会社によると、新しいロボットや新しい作業に合わせるのに、\nS102 そのロボット自身のデータは、1時間ほどで済む場合があって、\nS103 選んだいくつかの簡単な作業では、成功率が99%を超えたそうです。\nS104 会社自身の発表なので、そこは差し引いて聞いてください。\nS105 50万時間を年に直すと、約57年です。\nS106 10万年と比べると、まだ1700分の1くらい。\nS107 ただ、これは人が文章を読む時間と、作業を記録した時間なので、\nS108 単位をそろえた比較ではありません。目安として聞いてください。\nS109 つまり、壁はまだ高い。でも、壁の前でみんなが土を積み始めている。\nS110 1Xが家の中で遠隔操作しながらデータを集めているのも、\nS111 出荷されたロボットの多くがデータ集めに回っているのも、\nS112 同じ壁に向かっている動きなんです。\nS113 ただ、ここで「なんだ、まだ全然ダメなのか」で\nS114 片付けてしまうのも違うんです。\nS115 見て判断するほうには、もう10万年分の教科書がある。\nS116 そこは僕の机の上でも、もう普通に使えています。\nS117 じゃあ、この2つが混ざっている今、ロボットのニュースの何を見ればいいのか。\n\n### 8 見立て\n\nS118 ここからが、僕の一番言いたいことです。\nS119 データの壁を知っていると、\nS120 ロボットのニュースを見たときの問いが、2つに変わるんですよ。\nS121 1つは、そのロボットは誰が動かしていて、データは誰が集めているのか、です。\nS122 人が遠隔で動かしているのか、ロボットが自分で動いているのか。\nS123 デモ動画では、ここがほとんど言われないんですよね。\nS124 もう1つは、その仕事は「見て判断する」で済むのか、\nS125 「触れて動かす」精度が要るのか、です。\nS126 棚の商品が足りているかを見るのは、前者です。\nS127 柔らかい物を潰さずに掴むのは、後者です。\nS128 前者なら、もう安く使える。\nS129 後者は、その現場で測って固める仕事が、まだ残っている。\nS130 ゴールドバーグさんも、昔ながらのエンジニアリングで、\nS131 狭い仕事を確実にこなすロボットを現場に置いて、\nS132 働きながらデータを集めるのが近道だ、と言っています。\nS133 僕の机の上でやっていることも、まさにそれなんですよ。\nS134 判断はAIに任せる。動きは、自分のアームで測って固める。\nS135 で、作る側として、もう1つ言わせてください。\nS136 データの壁を越えたあとも、最後に残るのは「どう動くか」なんです。\nS137 例えばロボットが人に物を渡すとき、\nS138 何秒待って手を開けば、人は安心して受け取れるのか。\nS139 これは、成功か失敗かの判定だけでは出てこない。\nS140 動きの質そのものを設計する仕事が、\nS141 壁の向こうにも残っていると、僕は見ています。\n\n### 9 冒頭の問いへの回答\n\nS142 じゃあ、冒頭の問いに答えますね。\n\n### 9 冒頭の問いへの回答\n\nS143 ロボットにChatGPTの瞬間は、見て判断する部分には、もう来ています。\n\n### 9 冒頭の問いへの回答\n\nS144 でも、現実に触れて動かす部分には、まだ来ていません。\n\n### 9 冒頭の問いへの回答\n\nS145 その差を埋めるデータを、今、世界中で必死に集めている最中です。\nS146 ロボットのデモ動画を見たら、\nS147 「これは誰が動かしていて、何回やって、何回うまくいったのか」を、\nS148 一度だけ考えてみてください。\nS149 それだけで、同じ動画の見え方が、かなり変わると思います。\n\n### 10 アウトロ\n\nS150 今日はここまでです。\n\n### 10 アウトロ\n\nS151 この動画が気に入ったら、高評価とチャンネル登録をお願いします。\n\n### 10 アウトロ\n\nS152 コメントは全部読ませてもらっています。それではまた。</code></pre><p></p><h2 id=\"index_tELWWv5H\">codex から返ってきた検証結果の全文</h2><pre><code>判定は「直してから」です。現在の結論は、台本の根拠だけでは成立していません。 特に、①「ChatGPTの瞬間」の定義、②異種データの1700分の1比較、③実機の設定・制御問題を学習データ不足へつなげる論理、④Generalistの最新発表の欠落が重大です。\n2026年10月11日時点でWeb確認しました。以下では、一次資料の本文確認、検索抜粋だけの確認、推論、未確認を区別します。本人の台帳・設計メモは依頼どおり検証対象外とし、数字の伝え方だけを評価します。\n[❌ NG] S016〜S018、S143〜S145\n問題:「ChatGPTの瞬間」が未定義です。写真への回答を一度得られたことから、視覚判断全体の転換を宣言し、さらに物理操作全体には転換がないと断定するのは、両方向に飛躍しています。\n根拠:使いやすさ、汎化、信頼性、普及、費用は別の評価軸です。例えば[Gemini Roboticsの公式資料](https://deepmind.google/en/models/gemini-robotics/gemini-robotics/)は、視覚と言語から動作を出すモデルを示していますが、提供はPrivate previewです。「能力がある」と「誰でも利用できる」も分ける必要があります。\n修正案:「ここではChatGPTの瞬間を、専門家でなくても、いろんな仕事にすぐ使えるようになる転換点、と呼びます。僕の机では、画像に質問する入口はぐっと簡単になりました。でも、いろんな家で安全に家事を任せられる転換点まで来たかは、まだ判断できません。」\n[❌ NG] S105〜S109\n問題:57年への換算は正しいですが、「1700分の1」はデータの不足率ではありません。「目安」と断っても、比較対象の意味が違うため、壁の高さを数量化したことにはなりません。\n根拠:500,000÷24÷365≒57.1、100,000÷57.1≒1752。前者は作業記録の総時間、後者は異種の学習資料を人間が読む・見る時間への換算です。情報密度、重複、センサー、タスクの多様性、必要性能がそろっていません。また一社のデータ量は、世界全体の総量でもありません。\n修正案:「50万時間は、連続した時間に直すと約57年です。ただ、文章や画像を見る時間と、作業を記録した時間は、そのまま比べられません。何倍足りないかではなく、必要な経験をどれだけ広く集められるかが問題なんです。」\n[❌ NG] S068〜S074、S082〜S085、S133〜S134\n問題:計算モデル間の30cmの不一致と指令の不受理は、それだけではAIの学習データ不足を示しません。モデルの整合性や制御インターフェースの問題を、業界の「データの壁」の実例として使っています。\n根拠:同じ角度の比較には、座標系、リンク寸法、関節軸、角度の符号・原点、工具先端の定義を一致させる必要があります。どちらが正しいかは実機との比較が必要です。原因は非公開資料のため未確認です。\n修正案:「僕の設定では、計算モデルとシミュレーターの位置が30cm以上食い違いました。これはAIの学習データ不足とは別に、モデルや制御の整合性を確かめる仕事が必要だった、という話です。」\n[❌ NG] S099〜S104、S109、S129、S144\n問題:4月のGEN-1を最新状況のように紹介していますが、8月のGEN-1.5が抜けています。新しい作業への適応負担を論じる動画では、視聴者の判断を変える重要な欠落です。\n根拠:[Generalistの8月19日公式発表](https://generalistai.com/blog/gen-1.5)は、短い実演を使った新タスクへの適応を報告しています。ただし同社自身の評価で、対象は簡単で短い作業です。家庭全体への汎化を証明したわけではありません。\n修正案:「さらに8月にはGEN-1.5を発表して、簡単で短い作業なら、数秒の実演から適応できる例も報告しています。会社発表なので評価条件は見る必要がありますが、『動かすには毎回大量の専用データが必要』とも言い切れなくなっています。」\n[❌ NG] S034〜S038\n問題:SAGの「industrial and commercial」を「工場や店舗向け」と狭めています。また用途と導入先は重なり得るため、「ほぼ逆の数字」「分類の違いが原因」とする説明は未立証です。\n根拠:[SAGの一次発表](https://smartanalyticsglobal.com/global-humanoid-robot-shipments-2026-agibot-unitree/)の表現は産業・商業用途です。同社の総出荷推計も約19,100台で、Counterpointの22,000台超とは異なります。両調査の分類対応表や集計方法は、今回確認できていません。商業施設での展示と研究用途が排他的とは限りません。\n修正案:「一方、SAGは約1万9100台と推計して、産業・商業用途が7割超としています。ただ、総数も分類も違うので、この2つは単純な裏表として比べられません。どれだけ自律的に実務をこなしたかは、この数字だけでは分からないんです。」\n[❌ NG] S089〜S094、S115〜S116\n問題:Goldbergの10万年は「文章だけ」ではありません。さらに、その比喩を「見て判断する側には十分な教科書がある」という性能保証に変えています。\n根拠:[Science Roboticsの原文](https://autolab.berkeley.edu/assets/publications/media/GOFE-Can-Close-the-100000-Year-Robot-Data-Gap-Science-Robotics-Aug-2025-scirobotics.aea7390.pdf)は、現代のVLMの学習に使う文章と画像を読む・見る時間への概算です。これはEditorialであり、一般ロボットに必要なデータ量の実証値ではありません。\n修正案:「ゴールドバーグさんは、画像と言葉を扱うAIの学習資料を、人が読む・見る時間に換算すると、約10万年規模だと紹介しています。ロボットにも同じ量が必要だと分かったわけではありませんが、動作指令と結びついた経験は集めにくい、という問題提起です。」\n[❌ NG] S083〜S084、S129、S145\n問題:「自分の机で測るしかない」は、他環境・他機体からの転移を否定する表現です。また、データだけが判断と動作の差を埋めるように聞こえます。\n根拠:[Gemini Robotics 1.5技術報告](https://storage.googleapis.com/deepmind-media/gemini-robotics/Gemini-Robotics-1-5-Tech-Report.pdf)は機体をまたいだ技能転移を報告しています。専門家なら、VLAの汎化、シミュレーション、世界モデル、モデルに基づく制御をまず反論として出します。[NVIDIAの公式説明](https://developer.nvidia.com/isaac/gr00t?ncid=so-yout-261862-vt48)も実データと合成データの併用を示しています。ただし、これらが現場検証を不要にしたとは言えません。\n修正案:「ほかの環境で学んだことを持ってくる研究も進んでいます。それでも、僕の構成でちゃんと動くかは確認が必要でした。差を埋めるのはデータだけでなく、モデル、制御、機械そのものの改善でもあります。」\n[⚠️ 要注意] S013〜S015、S017、S144\n問題:「物に触れて動かす」で自動運転まで定義し、その後はほぼ物体操作と家庭用人型ロボットの話になります。広い領域を否定する結論になっています。\n根拠:台本内の対象の不一致です。Goldbergの原文自身も、自動運転や荷物仕分けを既に実用化された例として挙げています。限定用途の実用化と、汎用家庭ロボットの普及は別問題です。\n修正案:「フィジカルAIは、現実世界を認識して行動するAIのことです。自動運転も入りますが、今日は主に、アームや人型ロボットが物を扱う話、とくに家庭のような変化の多い場所に絞ります。」\n[⚠️ 要注意] S001〜S004、S028〜S029、S040〜S042\n問題:数字の記載は確認できても、市場の確定実績として扱うには調査方法の確認が不足しています。「はっきりしている」も強すぎます。\n根拠:Counterpointの公式ページ由来の検索抜粋では、22,000台超、前年比約300%増、通年50,000台超の予測、用途の合計60%超を確認しました。直接開いた本文は取得できず、集計定義は未確認です。SAGの本文では中国メーカー97%超、44%+31%を確認しました。いずれも調査会社の推計で、出荷は自律稼働や顧客検収を意味しません。[Counterpoint](https://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026)、[SAG](https://smartanalyticsglobal.com/global-humanoid-robot-shipments-2026-agibot-unitree/)\n修正案:「Counterpointの推計では、今年上半期の出荷は2万2000台超、前年同期のおよそ4倍です。ただ、出荷台数は、そのまま自律的に働いている台数ではありません。中国メーカーへの集中も、SAGの推計では非常に大きいです。」\n[⚠️ 要注意] S030〜S032、S053〜S054、S110〜S112\n問題:「データ集め」を遠隔操作だけに限定し、研究用途までデータ生産へ読み替えています。さらに工場と家庭の共通の「目的」として断定しています。\n根拠:研究には制御・機構・移動などの検証も含まれ、全てが学習データ収集とは限りません。作業中の自律ログや人の実演も収集方法です。「研究・データ生産」という合算から、それぞれの比率や購入目的は分かりません。\n修正案:「データ集めには、人が遠隔で動かして記録する方法も、自律作業の記録を残す方法もあります。ただ、研究向けの台数が全部データ集めに使われる、という意味ではありません。」\n[⚠️ 要注意] S044〜S049、S051〜S052\n問題:価格プランの提供時期を省き、遠隔支援が常時の仕組みのように聞こえます。顧客出荷については、発表を見つけられないことと、出荷していないことを区別すべきです。\n根拠:[1Xの発売発表](https://www.1x.tech/discover/neo-home-robot)は2025年10月28日、20,000ドルのEarly Accessは2026年優先配送、499ドルの月額プランは後日配送です。[現行注文ページ](https://www.1x.tech/order)では基本的自律性と予約した時間のExpert Modeを説明しています。VRゴーグルの具体的運用は今回の公式本文では未確認。10月11日までの一般顧客への配送開始を示す公式発表も今回の検索では確認できませんでした。\n修正案:「2万ドルの早期購入枠は2026年配送予定で、月499ドルのプランは後日です。初期の自律機能に加えて、知らない作業では予約した時間に担当者が遠隔で支援します。10月11日時点で、一般顧客への配送開始を示す公式発表は、僕が確認した範囲では見つかっていません。」\n[⚠️ 要注意] S063〜S065、S081、S126〜S129\n問題:回答が返ることと、正しく判定できることを混同しています。「前者ならもう安く使える」は、精度や運用条件を無視しています。\n根拠:判定を評価するには、実際の保持状態との照合、失敗画像、遮蔽、誤判定率などが必要です。棚の欠品検知にも死角や商品識別、照明、誤報対応があります。台本にはモデル名、正答率、費用がなく、「安い」の実証もありません。\n修正案:「写真を見せて質問する入口は、驚くほど簡単でした。ただ、答えが返ることと、毎回正しいことは別です。僕の条件でどこまで任せられるかは、失敗例も含めて確かめる必要があります。」\n[⚠️ 要注意] S061\n問題:650回以上が成功した回数、あるいは同一条件での評価回数に聞こえます。提示された情報は、期間中のPICK→PLACE通し試行656回です。\n根拠:ユーザー提示の台帳説明に基づく指摘です。試行間の構成変更や中断の扱いは未確認で、成功率やモデル性能は推定できません。\n修正案:「6月18日から7月20日までに、掴んで置く一連の試行を656回記録しました。これは成功回数ではなく、失敗も含めた試行回数です。」\n[⚠️ 要注意] S069〜S074\n問題:「実際に動かした」が実機測定に聞こえ、170万点が現実の経験データとして受け取られます。提示された説明ではシミュレーター内のFK表です。\n根拠:仮想モデルの大量サンプリングは、そのモデルの写像を細かくしたものです。点数が多くても、モデルと実機の一致や接触精度を証明しません。\n修正案:「シミュレーターの中で関節角と爪先位置の対応を計算して、約170万点の表にしました。実機で170万回測ったわけではなく、現物との一致は別に確認する必要があります。」\n[⚠️ 要注意] S076〜S079\n問題:90度ちょっとという閾値を機種全体の仕様として語っています。また「指令が捨てられる」は、通信を観測していなければ原因の断定です。\n根拠:提示情報は本人の構成でj1が約90.7度を超えたときの観察です。ファームウェア、SDK、他関節の値、検証箇所は未確認です。「肩」だけでは関節も特定しにくくなります。\n修正案:「僕の機体とソフトの組み合わせでは、j1に約90.7度を超える値を送ると、例外が出ないのに動かないことがありました。機種全体の仕様か、どの段階で止まったのかは、まだ切り分けが必要です。」\n[⚠️ 要注意] S099〜S104\n問題:期間が「半年」と丸められ、99%超が評価全体の平均のようにも聞こえます。1時間は追加のロボットデータであって、全学習・導入に必要な時間ではありません。\n根拠:[GEN-0公式発表](https://generalistai.com/blog/gen-0)は2025年11月4日、[GEN-1公式発表](https://generalistai.com/blog/gen-1)は2026年4月2日で約5か月です。27万時間超と50万時間超、平均99%と一部タスク99%超を区別する必要があります。自己申告の試験結果で、現場導入の総費用は示していません。\n修正案:「会社発表では、約5か月で27万時間超から50万時間超に増えました。事前学習に加えて、示した試験では約1時間のロボットデータを使い、選んだ作業群で平均99%の成功率を報告しています。導入全体が1時間で終わる、という意味ではありません。」\n[⚠️ 要注意] S130〜S133\n問題:Goldbergの提案を、個人の机上試行と「まさに同じ」としています。提案の肝である、実務で価値を生みながらデータを蓄積する点が抜けます。\n根拠:原文の提案は、従来の工学と学習を組み合わせ、商業環境で有用な仕事をするロボットから継続的にデータを集めるものです。家庭の開発試行との類似はありますが、稼働規模と実用性は異なります。前掲のScience Robotics原文。\n修正案:「僕の机の上も、AIと従来の制御を組み合わせる点では通じています。ただ、現場で役に立つ仕事をしながらデータが増える仕組みとは、まだ規模も段階も違います。」\n[⚠️ 要注意] S123、S136〜S141\n問題:「デモではほとんど言われない」は調査なしの一般化です。また人への物の渡し方を、データの壁を越えた後の課題に分離するのも不正確です。\n根拠:Generalistの公開動画には自律動作・再生速度の表示があります。人との動作の調整も、知覚、制御、学習、安全性を含む現在の研究対象です。[DeepMindの人とロボットの相互作用・安全性説明](https://deepmind.google/models/gemini-robotics/responsibly-advancing-ai-and-robotics/)もその一例です。\n修正案:「デモを見るときは、自律か遠隔操作か、速度や介入の条件が書かれているかを見ます。そしてデータ集めと並行して、人が安心して受け取れる動きを設計する仕事も必要なんです。」\n\n1. このまま公開してよいか:直してから。 体験談の章は残せますが、結論とデータの壁への接続は書き直す必要があります。\n2. 公開前に必ず直すべき文番号: S016〜S018、S034〜S038、S068〜S074、S082〜S085、S089〜S094、S099〜S109、S115〜S116、S128〜S129、S143〜S145。\n3. 結論そのものへの評価:現状では成立していない。 「見て判断する全体には来た/触れて動かす全体には来ていない」という二分は広すぎ、証拠も非対称です。次の結論なら条件つきで成立します。\n\n「僕の机では、画像に質問する入口はずっと簡単になりました。一方、物を確実に動かすには、機体や環境とのすり合わせがまだ必要でした。その負担を減らす研究は急速に進んでいます。でも、いろんな家庭で安全に家事を任せられる転換点まで来たかは、まだ判断できません。」</code></pre><p></p><h2 id=\"index_9Y98_SCr\">video_rules.md の全文</h2><pre><code class=\"language-markdown\"># video_rules.md\n\n## 動画制作ルール(唯一の正)\n\n最終更新: 2026-10-11\n状態: 試作1本目「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」を作りながら決めたことをまとめた初版。まだ決まっていない項目は8節にまとめている。\n\nYouTube 用の解説動画を作るときは、まずこのファイルを読む。台本の文体は language_rules.md、記事は article_rules.md が正。動画の構成・数字の扱い・画面・音声・書き出しは本ファイルが正。\n\n---\n\n## 1. 動画の構成(解説回の型)\n\n参考にした3本の解説動画(時事ニュースをデータと体験で検証する形式)から取った型。尺は13〜14分。\n\n| # | パート | 時間の目安 | やること |\n|---|---|---|---|\n| 0 | フック | 0:00〜0:45 | 挨拶より先に、視聴者が最近見たニュースや数字から入る。世間の感覚を代弁する(「〜って感じませんか」) |\n| 1 | 自己紹介 | 〜0:55 | ロゴのテロップを出し、毎回同じ文言で名乗る(2節の文言) |\n| 2 | 今日の問い | 〜1:10 | 問いを疑問文1つで言い、問いの中の言葉をその場で定義する |\n| 3 | 結論と立場 | 〜1:30 | 先に結論を言う。誤解されやすい方向を先に否定する(「〜と言いたいわけじゃないんです」) |\n| 4 | 目次 | 〜1:50 | 「大きく3つあります」と数を言ってから並べる |\n| 5〜7 | 本編(3節) | 〜10:00 | 「1つ目、〜です」で始め、各節の最後の1文を次の節の問いにする |\n| 8 | 見立て | 〜12:30 | 「ここからが僕の一番言いたいことです」と言ってから意見を話す |\n| 9 | 問いへの回答 | 〜13:20 | 冒頭の問いに3〜4文で答え、視聴者が次にニュースを見たときに考える問いを1つ渡す |\n| 10 | アウトロ | 〜13:40 | 毎回同じ文言 |\n\nパートごとのセリフの型(穴埋め式)と、実験回の型は Docs「動画台本テンプレート|台本例:フィジカルAIの現在地」にある。\n\n---\n\n## 2. 台本のルール\n\n公開前に codex に敵対的検証(台本を論破するつもりで見落としを探してもらう)をかけたところ「このまま公開は不可」と判定された。以下はそのとき直した点から作ったルール。\n\n1. **問いの中の言葉は、冒頭で定義する。** 例: 「ChatGPTの瞬間」を定義しないまま「来た/来ていない」と言い切っていた → 「専門家でなくても、いろんな仕事にすぐ使えるようになる転換点」と冒頭で言う形に直した。\n2. **話の範囲を言う。** 例: フィジカルAIの定義に自動運転を含めながら、アームの話だけで結論を出していた → 「自動運転も入りますが、今日はアームや人型ロボットが物を扱う話に絞ります」と言う。\n3. **数字は、出典で裏が取れて、論旨に必要なものだけ使う。** 増やすほど突っ込まれる場所が増える。試作1本目で削った数字: 1700分の1(単位の違う比較)、成功率99%・約1時間(会社の自己申告で条件の説明が要る)、170万点(シミュレーターの表なのに実機の測定と誤解される)、メーカー別の内訳(なくても話が変わらない)。\n4. **推計・会社自身の発表は、そう口で言う。** 「Counterpointの推計では」「会社の発表では」。調査会社同士で数字が違うときは、どちらかに寄せず「総数も分類も違うので、単純には比べられない」と言う。\n5. **意味や単位の違う数字を並べて比べない。** 例: 「人が文章を読む時間 10万年」と「作業を記録した時間 57年」を割り算していた → 比較ごと削った。\n6. **自分の実機の話は、自分の条件の話として言う。** 「僕の機体とソフトの組み合わせでは」を付ける。机の上で起きたことを業界全体の結論に広げない。例: 計算ツールとデジタルツインの爪先の位置が30cm以上食い違った話を「データ不足」の例にしていた → 「モデルや制御が自分の機体と噛み合っているかを確かめる、すり合わせの仕事」として話す形に直した。\n7. **自分の数字は、何の数かを添える。** 例: 「650回以上」→「失敗も含めた試行回数」。\n8. **最新の発表は、公開の直前に確認する。** 例: 4月の発表を最新として話していたが、8月に次の発表(GEN-1.5)が出ていた。\n9. **「見つからない」と「していない」を分ける。** 例: 「出荷していない」ではなく「配送開始を示す公式発表は、僕が確認した範囲では見つかっていません」。\n10. **自分の活動を盛らない。** 自己紹介の文言は「僕は本業のデザイナーをしながら、ロボティクスの研究と開発をしています。自宅の机の上でロボットアームを動かして、その記録を公開しています。」に固定する。「秋田」「29年」「毎日」は入れない。\n11. **公開前に codex で敵対的検証をかける。** 依頼ファイルの型は、試作1本目の動画フォルダにある `codex_敵対的検証依頼_*.md`。台本全文に文番号(S001〜)を振り、出典の表と検証の観点(事実・論理・抜けている反論・言い方)を付けて渡す。返ってきた指摘は、一次情報で確かめてから採用する。\n\n---\n\n## 3. ナレーション(現状は仮音声)\n\nいまは仮の合成音声で作っている。本番の声は未決(8節)。\n\n- **エンジン:** Open JTalk、声は nitech_jp_atr503_m001(男声)。設定は 速さ 1.08、音の高さ -1、声質 0.55\n- **読み替え表:** 英語の名前はカタカナで読ませる(ChatGPT→チャットジーピーティー、ikpy→アイケーパイ など)。誤読した語も表に足す。これまでの例: 爪先→つめさき(「つまさき」だと足の指先に聞こえる)、値→あたい、4分の3→よんぶんのさん。新しい固有名や数字を入れたら、エンジンの解析結果(カタカナの読み)を出して目で確認する\n- **間延びの検査:** ひと息で読む区間が長いと、その区間の最後の言葉が1音0.47秒ほどに引き伸ばされる(普段は0.1秒前後)。作った音声すべてで、1音0.3秒を超える箇所を検出する。見つかった文は、読み上げ用の原稿だけに読点を足して区切る。字幕の文は変えない\n- **文のあとの間:** 通常 0.35秒、話題の区切り 0.6〜0.7秒、節の終わり 0.9秒\n\n---\n\n## 4. 画面デザイン\n\n### 4-1. 画面の枠\n\n- 1920×1080、30fps\n- 余白は左右140px。下の250pxは字幕のために空ける\n- 左上に章のタグ(章番号はオレンジ、見出しは薄い文字)、右上にロゴ(高さ34px)\n\n### 4-2. 色(記事のサムネイルと同じ色)\n\n| 用途 | 色 |\n|---|---|\n| 背景 | #F5F5F0 |\n| 文字 | #0D0D0D |\n| 薄い文字(ラベル・出典・補足) | #888888 |\n| 区切り線 | #D6D6D1 |\n| 横帯・棒の「その他」 | #D3D3CE |\n| 箱(フロー図)の背景 | #FFFFFF |\n| オレンジ(強調・主題) | #C4471F |\n| 青(対比のもう一方) | #1A6C98 |\n\n- オレンジは、1画面の中で一番伝えたい語だけに使う\n- 青とオレンジを並べるのは、2つのものを対比するときだけ(試作1本目では「見て判断する=青」「触れて動かす=オレンジ」)\n- 新しい色を足すときは、色覚の違いがあっても見分けられるかを検証してから使う(dataviz スキルの validate_palette.js。背景色 #F5F5F0 を指定する)\n\n### 4-3. 文字\n\nフォントは Noto Sans CJK JP。\n\n| 用途 | 大きさ | 太さ |\n|---|---|---|\n| 大きい数字(ヒーロー数字) | 220px(単位は90px) | 900 |\n| 章番号 | 200px(オレンジ) | 900 |\n| 見出し 大/中/小 | 84px/64px/48px | 900/900/700 |\n| 見出しの上の小見出し | 40px(薄い文字) | 700 |\n| 2列比較のラベル | 34px(薄い文字) | 700 |\n| 出典 | 24px(薄い文字) | 400 |\n\n### 4-4. 図の型\n\n内容の形に合わせて、次の型から選ぶ。\n\n| 内容 | 型 |\n|---|---|\n| 伝えたい数字が1つ | 大きい数字+上に何の数か+下に比較(「前年同期の約4倍」) |\n| 2つのものの対比 | 2列。間を細い縦線(2px)で区切り、色は文字の色だけで分ける |\n| 手順・流れ | 白い箱を矢印でつなぐ |\n| 全体のうちの割合 | 横帯(ラベルを帯の中に書く) |\n| 時期ごとの量 | 横棒(長さは実際の比率) |\n| 章の始まり | 大きい章番号+章の問い |\n| 視聴者への問い | 黒い丸ラベル(「問い 1」)+大きい見出し |\n| 並ぶ項目 | 番号付きの行を細い線で区切る |\n\n### 4-4b. 強弱のつけ方\n\n全部の場面を同じ強さにしない。1本の中で、次の3段階を使い分ける。打ち出しは1本に6〜10場面ほど、残りは説明の図にする。\n\n| 段階 | 使う場面 | 見た目 |\n|---|---|---|\n| 打ち出し | 結論、問いかけ、言い切りたい一言、章の始まり | 小さい前置き(色・44px)の下に、1〜2行の大きな文(110〜150px)。1行は10字前後まで。補足は44pxの薄い文字で1行だけ |\n| 説明 | 数字・比較・手順を見せる場面 | 4-4の図の型(見出し64〜84px) |\n| 補足 | 出典・断り書き | 24〜34pxの薄い文字 |\n\n打ち出しの型(試作1本目で使ったもの):\n\n- **打ち出し文:** 前置き「先に結論」→「見て判断する入口は簡単に/物を動かすのはまだ手間」。対比の語だけ青とオレンジにする\n- **章の扉:** 「今日の話」→「1つ目」→ 大きなキーワード(200px・オレンジ。例「データの壁」)→ 章の問いを1行\n- **点の並び:** 割合は、100個の点のうち該当する数だけ色を付けて見せる(例: 中国メーカー 97%)。「以上」「推計」は下の注に書く\n\n打ち出しにする文の選び方: 字幕をそのまま大きくするのではなく、その場面で一番残したい一言に縮める。字幕と同じ文を大きく出さない。\n\n### 4-5. やらない表現\n\n- カードの上や左に太い色の線を引く(よくある AI 生成スライドの見た目になる)\n- 影、グラデーション\n- 1画面にオレンジと青以外の色を足す\n- 意味や単位の違う数字を同じ棒や帯に並べる\n\n### 4-6. 出典\n\n数字を出す画面には、左下(字幕の上)に出典を24pxの薄い文字で必ず入れる。推計なら「推計」、会社の自己申告なら「会社の発表」と書く。模式図には「図は模式図」と入れる。\n\n---\n\n## 5. 字幕\n\n- 白文字 52px・太さ800。縁取りは #0D0D0D・11px で、角を丸くする(SVG の stroke-linejoin: round。CSS の文字の縁取りは角が尖るので使わない)\n- 読み上げの1文=字幕1枚\n- 2行まで、1行31字以内。改行は読点の位置で入れる。読点がなければ助詞の後ろで割る\n- 位置は画面の下から62px\n\n---\n\n## 6. 実機の映像とテロップ\n\n- 映像は 1920×1080 に拡大して切り抜く。字幕の後ろに黒50%の帯(高さ230px)を敷く\n- 何の映像かを左上に出す(黒78%の地に白文字34px。例「実機:mechArm 270-Pi 赤いブロックを掴んで箱に置く」)\n- 名前のテロップ: 薄い地に、ロゴ(高さ30px)・名前60px・肩書き30px(「デザイナー/ロボティクスの研究・開発」)。左の太い線は付けない\n- 編集ツールの透かし(例: clideo.com)が入った素材は、ぼかしたうえで字幕帯の下に隠す。これから撮る素材は透かしなしで書き出しておく\n- iCloud にしかない素材を取り込むと、Mac にダウンロードされる\n\n---\n\n## 7. 書き出しと保存\n\n- **作業用(YouTube に上げる方):** H.264(crf 20)、音声 AAC 192kbps、音量を -16 LUFS にそろえる\n- **確認用(チャットで送る方):** crf 29、音声 112kbps。30MB以下に収める(チャットで送れる上限)\n- **保存先:** 動画ファイル(mp4)は重く、iCloud に置くと Mac の内蔵 SSD を圧迫するので、外付け SSD(KIOXIA)に置く。軽いファイルは iCloud に置く\n - 動画ファイル(作業用・確認用の mp4): `/Volumes/KIOXIA/r2r2r-data/videos/{動画名}/`\n - 字幕の srt・codex 依頼の md: iCloud `デジタルツイン学習R2R2R/動画/{動画名}/`\n - 制作ベース: iCloud `デジタルツイン学習R2R2R/動画/_制作ベース/`(このファイルの写し、スクリプト一式、ロゴの透過 PNG)\n - 素材の実機映像も、新しく撮るものは KIOXIA に置く。iCloud にしかない素材を取り込むと Mac にダウンロードされて内蔵 SSD を使う\n - MASTER の保存先テーブルと、KIOXIA の README.md のフォルダ構成には、まだ videos/ の行がない。追記はエイイチが行う\n- **作る手順(_制作ベース のスクリプト):**\n 1. `narration.py` に台本を書く(1文ごとに、場面の名前・字幕の文・後ろの間)。読み替え表もここ\n 2. `tts2.py` で音声を作る(間延びの検査と、区切っての作り直しまで自動)\n 3. `scenes.py` に場面ごとの図を書く(4節の型と色)。実機映像は `src/` に置き、`VIDEO` に場面名と開始秒を書く\n 4. `render.py` で図・テロップ・字幕を画像にする\n 5. `assemble.py` で1文ずつ映像を作ってつなぎ、音声と合わせる(字幕の srt も出る)\n 6. 確認用に圧縮する\n\n---\n\n## 8. 未決・改良予定\n\n| 項目 | いまの状態 | 決めること |\n|---|---|---|\n| ナレーションの声 | Open JTalk の仮音声。抑揚が機械的 | 本人の声で録るか、品質の高い音声合成(VOICEVOX など。利用規約とクレジット表記の条件を確認)にするか。srt に1文ごとの時間が入っているので、それに合わせて録れば映像はそのまま使える |\n| 図解の動き | すべて静止画 | 数字のカウントアップ、矢印が伸びる、棒が伸びるなどの動きを入れるか。入れるなら、どの図の型に入れるか |\n| 背景の映像 | 実機映像は自己紹介・節2・エンディングだけ。ほかは無地の背景 | 解説中も実機映像を薄く敷くか、図解だけにするか。使う素材を撮りためるルール |\n| BGM・効果音 | なし | 使うか。使うなら、YouTube で収益化できてクレジット表記の条件を満たす音源から選ぶ。ナレーションが聞き取れる音量に下げる |\n| ジングル | 文言だけ(「Pixels to Motors。こんにちは、エイイチです。」) | 音とロゴの動きを付けるか |\n| サムネイル | まだない | 記事のサムネイルと同じ配色・線画のトーンで作るか |\n| 番組名 | 未定 | 決める |\n| 概要欄のチャプター | まだない | srt の節の開始時刻から作れる |\n\n---\n\n## 更新履歴\n\n- 2026-10-11: 4-4b「強弱のつけ方」を追加(打ち出し・説明・補足の3段階、打ち出し文・章の扉・点の並びの型)。\n- 2026-10-11: 動画ファイルの保存先を iCloud から KIOXIA の videos/ に変更(内蔵 SSD の圧迫を避けるため)。\n- 2026-10-11: 初版。試作1本目の制作で決めた構成・台本・ナレーション・画面・字幕・映像・書き出しのルールをまとめた。台本ルールは codex の敵対的検証で直した点から作った。色は記事のサムネイルに合わせた。</code></pre><h2 id=\"index_lmKEOQqP\">参照した情報</h2><ul><li><p><a href=\"https://www.youtube.com/watch?v=cDEex1_8cOo\" data-has-link=\"true\">YouTube:参考にした解説動画 1</a></p></li><li><p><a href=\"https://www.youtube.com/watch?v=bmmF1tUwnt8\" data-has-link=\"true\">YouTube:参考にした解説動画 2</a></p></li><li><p><a href=\"https://www.youtube.com/watch?v=LI-VAGxwvno\" data-has-link=\"true\">YouTube:参考にした解説動画 3</a></p></li><li><p><a href=\"https://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026\" data-has-link=\"true\">Counterpoint Research:Global Humanoid Robot Shipments Soar Nearly 300% YoY in H1 2026</a></p></li><li><p><a href=\"https://smartanalyticsglobal.com/global-humanoid-robot-shipments-2026-agibot-unitree/\" data-has-link=\"true\">Smart Analytics Global:2026年上半期の人型ロボット出荷</a></p></li><li><p><a href=\"https://www.1x.tech/order\" data-has-link=\"true\">1X:NEO 注文ページ</a></p></li><li><p><a href=\"https://autolab.berkeley.edu/assets/publications/media/GOFE-Can-Close-the-100000-Year-Robot-Data-Gap-Science-Robotics-Aug-2025-scirobotics.aea7390.pdf\" data-has-link=\"true\">Ken Goldberg, Science Robotics(2025年8月):Good Old-Fashioned Engineering Can Close the 100,000-Year \"Data Gap\" in Robotics</a></p></li><li><p><a href=\"https://generalistai.com/blog/gen-1.5\" data-has-link=\"true\">Generalist:GEN-1.5(2026年8月19日)</a></p></li></ul><p></p>", "toc": [{"id": "index_D_tmh6uh", "text": "参考にした3本の解説動画は、冒頭1分半で問い・結論・目次を全部出していた"}, {"id": "index_0LUCW9SF", "text": "台本テンプレートは「解説回」と「実験回」の2種類に分けた"}, {"id": "index_bTgs9pio", "text": "台本に入れた自分の数字は、台帳と公開記事で裏を取った"}, {"id": "index_vjneh5nj", "text": "動画は、Open JTalk・Playwright・ffmpeg で1文ずつ組み立てた"}, {"id": "index_YxOgXNy5", "text": "Open JTalk は、ひと息が長い文の最後を1音0.47秒に引き伸ばしていた"}, {"id": "index_0fVKV772", "text": "codex の敵対的検証で、台本の結論は「成立していない」と判定された"}, {"id": "index_t6LAP1jb", "text": "codex が新しく挙げた事実は、一次情報で確かめてから採用した"}, {"id": "index_YhZ9JBp8", "text": "根拠の弱い数字は足さずに削った"}, {"id": "index_gc6toY6y", "text": "読み上げは「爪先」を「つまさき」と読んでいた"}, {"id": "index_HCnc4FWz", "text": "カード上の太線と、角の立った字幕の縁取りをやめた"}, {"id": "index_mlpaKj8r", "text": "自己紹介から「秋田」「29年」「毎日」を外し、ロゴを差し替えた"}, {"id": "index_9szChvZM", "text": "背景と文字の色を、記事サムネイルの #F5F5F0 と #0D0D0D に合わせた"}, {"id": "index_msVyMuQC", "text": "30cmの食い違いは、アームが外れた距離ではなく、爪先の位置の決め方の違いだった"}, {"id": "index_Apnn9rBf", "text": "打ち出し・説明・補足の3段階で、画面に強弱をつけた"}, {"id": "index_sOZrkg72", "text": "動画ファイルは、iCloud ではなく KIOXIA の videos/ に置いた"}, {"id": "index_52jeog_s", "text": "量産のベースとして video_rules.md と制作スクリプト一式を残した"}, {"id": "index_3J7_uzPW", "text": "まだ決まっていない8項目は、未決のまま残した"}, {"id": "index_sWhPqJ9y", "text": "制作スクリプトの全文"}, {"id": "index_c3VMNBPy", "text": "実行ログ"}, {"id": "index_o0bVwuXE", "text": "codex への検証依頼の全文"}, {"id": "index_tELWWv5H", "text": "codex から返ってきた検証結果の全文"}, {"id": "index_9Y98_SCr", "text": "video_rules.md の全文"}, {"id": "index_lmKEOQqP", "text": "参照した情報"}]}
\ No newline at end of file
+{"day": "LOG330", "series": "ひとり研究所の裏側", "published": "2026-10-11T13:46:46.000Z", "title": "解説動画をAIと作る ― 型の分析から敵対的検証まで、試作1本で決めたこと", "slug": "making-explainer-video-with-ai", "cover": null, "thumb": "/thumbs/r-making-explainer-video-with-ai.html", "lang": "JP", "description": "YouTube解説動画の試作1本を、参考動画の型の分析、台本、音声合成と図解の自動組み立て、codexの敵対的検証、デザイン修正まで進めた記録。決めたルールを量産のベースとして残した。", "updated": "2026-10-12T00:09:46.000Z", "body": "<!--TOC--><p></p><p>Pixels to Motors の YouTube で解説動画を出すために、Claude と対話しながら試作を1本作りました。題材は「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」で、尺は13分50秒です。</p><p>進め方は次の順番でした。参考にしたい解説動画3本の文字起こしから、構成・話し方・補足説明の型を取り出す。型を台本テンプレートにまとめ、テンプレートに沿って台本を書く。台本を、読み上げ音声・図解・字幕・実機の映像に分けてコードで組み立て、MP4 に書き出す。できた台本を codex(OpenAI のコーディング AI)に渡して、論破するつもりで検証してもらう。指摘とデザインの修正を重ねる。最後に、量産するときのベースとしてルール文書とスクリプト一式を残す。</p><p>本記事は、その過程で決めたことと、決めた理由の記録です。間違えたところ(根拠のない数字を書いた、30cm の例が誤解を招く、など)も消さずに残しています。制作スクリプトの全文、実行ログ、codex の検証結果の全文は記事の後半に載せています。</p><div class=\"video\"><video controls playsinline preload=\"metadata\" src=\"/media/s-1920x1080_b273c60b-57bf-4356-b2b2-1b5a4023565f.mp4\"></video></div><p></p><h2 id=\"index_D_tmh6uh\">参考にした3本の解説動画は、冒頭1分半で問い・結論・目次を全部出していた</h2><p>参考にしたのは、時事ニュースを統計と自分の体験で検証する形式の解説動画3本です(YouTube「海外Yパパラジオ」の、会社への侵入と暗数の回、AI とサイバー攻撃の回、溺れている人を AI に見つけさせる実験の回)。動画のページは Claude からは開けなかったため、文字起こしを貼って分析しました。</p><p>3本とも、骨格が同じでした。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>時間の目安</p></th><th colspan=\"1\" rowspan=\"1\"><p>中身</p></th><th colspan=\"1\" rowspan=\"1\"><p>実例</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>0:00〜0:45</p></td><td colspan=\"1\" rowspan=\"1\"><p>挨拶より先に、時事ネタか視聴者のコメントから入る</p></td><td colspan=\"1\" rowspan=\"1\"><p>「最近漏洩多くないですか?」というコメント</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>〜0:50</p></td><td colspan=\"1\" rowspan=\"1\"><p>決まったジングルと自己紹介</p></td><td colspan=\"1\" rowspan=\"1\"><p>住んでいる場所から世界を見ている、と名乗る</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>〜1:10</p></td><td colspan=\"1\" rowspan=\"1\"><p>その回の問いを疑問文1つで言う</p></td><td colspan=\"1\" rowspan=\"1\"><p>「会社への侵入は本当に急に増えたのか」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>〜1:30</p></td><td colspan=\"1\" rowspan=\"1\"><p>結論か鍵になる言葉を先に出し、自分の立場を言う</p></td><td colspan=\"1\" rowspan=\"1\"><p>「変わったのは手口じゃなく攻撃の値段」「増えていないと言いたいわけじゃない」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>〜1:50</p></td><td colspan=\"1\" rowspan=\"1\"><p>「大きく○つあります」で目次を言う</p></td><td colspan=\"1\" rowspan=\"1\"><p>3〜5項目</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>本編</p></td><td colspan=\"1\" rowspan=\"1\"><p>「1つ目、○○です」で区切る。各節の最後の問いが次の節の題になる</p></td><td colspan=\"1\" rowspan=\"1\"><p>「じゃあ一斉に調べ始めたら何が出てくるのか」→ 次の節</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>終盤</p></td><td colspan=\"1\" rowspan=\"1\"><p>「ここからが僕の一番言いたいこと」と口で言ってから意見を話す</p></td><td colspan=\"1\" rowspan=\"1\"><p>事実と意見の境目を視聴者に示す</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>締め</p></td><td colspan=\"1\" rowspan=\"1\"><p>冒頭の問いに答え、次にニュースを見たときに考える問いを1つ渡す</p></td><td colspan=\"1\" rowspan=\"1\"><p>「起きた数か、見つかった数か、一度だけ考えてみて」</p></td></tr></tbody></table></div><p>冒頭の1分半で「何の話か・何に答えるか・どの立場で話すか・何章あるか」がそろいます。ほかに共通していたのは、視聴者の思い込みを1つ置いてデータで崩し、崩したあとに逆方向の早合点も止めること。数字は必ず比較で言い直すこと(「15倍ですよ」)。専門用語は使った直後に1文で説明すること。限界を自分から言うこと(「動画2本だけなので、性能の評価というより最初の手触り」)。</p><p>溺れている人の回だけは型が少し違い、道具の説明 → なぜ難しいか → 試した結果 → 失敗の深掘り → 見せ方を変えて再実験 → 見立て、の順でした。山場が「失敗が見つかる場面」にある実験回です。</p><p></p><h2 id=\"index_0LUCW9SF\">台本テンプレートは「解説回」と「実験回」の2種類に分けた</h2><p>型を、パートごとの秒数とセリフの穴埋め(例「先に結論を言っちゃうと、変わったのは{A}じゃなくて{B}なんですよ」)にして、解説回と実験回の2種類のテンプレートにしました。収録前に確認するチェックリスト(専門用語は初出で1文の説明があるか、数字に出典の名前が付いているか、など)も付けています。</p><p>試作の題材は「フィジカルAIの現在地」で、問いの形にして「ロボットにもChatGPTの瞬間はもう来たのか」としました。解説回のテンプレートに当てはめ、外部の数字(人型ロボットの出荷台数、1X の家庭用ロボット NEO、ゴールドバーグさんの「10万年のデータの壁」、Generalist の学習データ量)は Web で一次情報を確認して入れました。</p><p></p><h2 id=\"index_bTgs9pio\">台本に入れた自分の数字は、台帳と公開記事で裏を取った</h2><p>台本の節2は、自宅の机でロボットアーム(mechArm 270-Pi)を動かしてきた体験談です。ここに入れた数字は、Mac のフォルダと外付け SSD(KIOXIA)をつないでもらい、記録で確かめました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>台本の数字</p></th><th colspan=\"1\" rowspan=\"1\"><p>確かめた記録</p></th><th colspan=\"1\" rowspan=\"1\"><p>結果</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>掴んで置く作業の試行回数</p></td><td colspan=\"1\" rowspan=\"1\"><p>台帳 pick_place_ledger_v3.csv</p></td><td colspan=\"1\" rowspan=\"1\"><p>657行。テスト1行を除いて656回(2026年6月18日〜7月20日)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>爪先の位置の対応表の点数</p></td><td colspan=\"1\" rowspan=\"1\"><p>公開記事 LOG283</p></td><td colspan=\"1\" rowspan=\"1\"><p>約170万点</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>計算ツールとデジタルツインの爪先の食い違い</p></td><td colspan=\"1\" rowspan=\"1\"><p>arm_kinematics.md、公開記事 LOG222</p></td><td colspan=\"1\" rowspan=\"1\"><p>同じ関節角で300mm以上</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>黙って捨てられる指令</p></td><td colspan=\"1\" rowspan=\"1\"><p>公開記事 LOG257</p></td><td colspan=\"1\" rowspan=\"1\"><p>j1(肩)が約90.7度を超えると send_angles がエラーなしで無視される</p></td></tr></tbody></table></div><p>1つ、間違いがありました。試行回数を最初「600回以上」と書いていましたが、これは記録を見ずに書いた数字でした。記事を検索した段階では LOG255 の「100試行ぶん」しか見つからず、いったん「100回以上」に直しました。そのあと台帳を数えたら656回あり、「650回以上」に直しています。結果として最初の数字は大きく外れていませんでしたが、根拠なしに書いた数字だったことに変わりはありません。台帳の集計結果は、後半の実行ログに全量で載せています。</p><p></p><h2 id=\"index_vjneh5nj\">動画は、Open JTalk・Playwright・ffmpeg で1文ずつ組み立てた</h2><p>台本から動画までを、クラウドの作業環境でコードにして組み立てました。</p><div class=\"embed\"><iframe src=\"/embeds/making-explainer-video-with-ai-vpZFxoYF.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p></p><ol><li><p>narration.py に台本を書く。1文ごとに「場面の名前・字幕の文・後ろの間(秒)」の組にする</p></li><li><p>tts2.py で、1文ずつ読み上げ音声を作る</p></li><li><p>scenes.py に、場面ごとの図解を HTML で書く</p></li><li><p>render.py で、図解・テロップ・字幕を Playwright(ブラウザを自動で動かす道具)で画像にする</p></li><li><p>assemble.py で、1文ごとに「図解か実機映像」+「字幕」を ffmpeg で数秒の映像にし、全部つないで音声と合わせる。字幕ファイル(srt)も同時に出す</p></li></ol><p>1文を1つの映像のかたまりにしたのは、あとで1文だけ直したときに、その文の映像だけ作り直せばよいからです。実際、自己紹介の1文を差し替えたときは、作り直したのは3つのかたまりだけでした。</p><p>読み上げ音声には、Open JTalk(日本語の音声合成エンジン)の男声を使いました。より自然な音声エンジン(VOICEVOX など)は、作業環境から配布元の GitHub にアクセスできず、取り込めませんでした。Open JTalk は Ubuntu のパッケージで入れられたので、仮の声として使っています。抑揚は機械的で、公開するなら本人の声で録り直す前提です。</p><p>実機の映像は、Mac の動画フォルダにあった5本から、赤いブロックを掴んで箱に置く実機の映像(LOG124)、アーム単体の動き(LOG123)、Unity のデジタルツインの映像2本を選びました。LOG123 と LOG124 の右下には、以前に使った編集ツールの透かし(clideo.com)が入っていたため、ffmpeg の delogo でぼかし、字幕の後ろに敷く黒い帯の下に隠しています。</p><p></p><h2 id=\"index_YxOgXNy5\">Open JTalk は、ひと息が長い文の最後を1音0.47秒に引き伸ばしていた</h2><p>最初に書き出した動画(13分33秒)を見てもらったところ、次の指摘がありました。</p><blockquote><p>動画の中で音声が間延びしたところが何箇所か存在します。 それだけ修正を行うことはできますか?</p></blockquote><p>Open JTalk は、音声と一緒に「どの音を何秒出したか」の記録を出せます。これを全153文について調べると、6文で、文の後半の音が1つあたり約0.47秒になっていました。普段は0.1秒前後なので、3〜4倍に引き伸ばされています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>文</p></th><th colspan=\"1\" rowspan=\"1\"><p>引き伸ばされていた部分</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>現実の世界で物に触れて動かす技術のことなんですよ。</p></td><td colspan=\"1\" rowspan=\"1\"><p>「…なんですよ」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ChatGPTのようなAIの学習に使われたインターネットの文章を、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「…使われた」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ChatGPTは10万年分の教科書で育ったけれど、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「育ったけれど」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ここで、じゃあロボットにChatGPTの瞬間は当分来ない、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「当分来ない」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1Xが家の中で遠隔操作しながらデータを集めているのも、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「いるのも」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>出荷されたロボットの多くがデータ集めに回っているのも、</p></td><td colspan=\"1\" rowspan=\"1\"><p>「のも」</p></td></tr></tbody></table></div><p>共通していたのは、読点なしでひと息に読む区間が長いことでした。ひと息の区間が長いと、その区間の最後の言葉が引き伸ばされます。</p><p>直し方は、読み上げ用の原稿だけに読点を足して区切ることです。字幕の文は変えません。最初は読点の位置で自動的に区切りましたが、「遠隔操作しなが/ら」のように単語の途中で切れた文があり、区切る位置を手で指定し直しました。その後、台本を書き直したときに、作った音声をすべて検査し、1音が0.3秒を超える文を見つけたら区切って作り直す処理を tts2.py に入れました。区切っても直らない文は、narration.py の OVERRIDE に「読み上げ用の読点の位置」を書いて直しています。</p><p></p><h2 id=\"index_0fVKV772\">codex の敵対的検証で、台本の結論は「成立していない」と判定された</h2><p>台本を公開前に codex に渡し、「この台本を論破するつもりで」検証してもらいました。依頼ファイルには、台本の全文に文番号(S001〜S152)を振り、使った出典の表と、検証してほしい観点(事実と数字・論理・抜けている反論・言い方)を付けています。依頼ファイルの全文と、返ってきた検証結果の全文は、後半に載せています。</p><p>判定は「直してから公開」、結論の評価は「現状では成立していない」でした。重大とされたのは次の4点です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>codex の指摘</p></th><th colspan=\"1\" rowspan=\"1\"><p>中身</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>「ChatGPTの瞬間」が未定義</p></td><td colspan=\"1\" rowspan=\"1\"><p>写真に質問して答えが返った体験1つから、「見て判断する部分には来た」と言い切っている</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>異種データの「1700分の1」比較</p></td><td colspan=\"1\" rowspan=\"1\"><p>人が文章を読む時間(10万年)と、作業を記録した時間(57年)は、意味も単位も違う</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>実機の問題を「データ不足」の例にしている</p></td><td colspan=\"1\" rowspan=\"1\"><p>計算ツールとデジタルツインの食い違いや、指令が無視される問題は、学習データの不足とは別の話</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Generalist の最新発表の抜け</p></td><td colspan=\"1\" rowspan=\"1\"><p>4月の GEN-1 を最新として話していたが、8月に GEN-1.5 が出ていた</p></td></tr></tbody></table></div><p>結論そのものについて、codex は次の形なら条件つきで成立すると書いていました。</p><blockquote><p>僕の机では、画像に質問する入口はずっと簡単になりました。一方、物を確実に動かすには、機体や環境とのすり合わせがまだ必要でした。その負担を減らす研究は急速に進んでいます。でも、いろんな家庭で安全に家事を任せられる転換点まで来たかは、まだ判断できません。</p></blockquote><p>台本の結論は、この形に差し替えました。</p><p></p><h2 id=\"index_t6LAP1jb\">codex が新しく挙げた事実は、一次情報で確かめてから採用した</h2><p>codex の指摘には、台本になかった新しい事実が含まれていました。採用する前に、それぞれの一次情報を開いて確かめています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>codex が挙げた事実</p></th><th colspan=\"1\" rowspan=\"1\"><p>確かめた一次情報</p></th><th colspan=\"1\" rowspan=\"1\"><p>確かめた内容</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Generalist が8月に GEN-1.5 を発表</p></td><td colspan=\"1\" rowspan=\"1\"><p>Generalist 公式ブログ(2026年8月19日)</p></td><td colspan=\"1\" rowspan=\"1\"><p>3〜12秒の実演1回から新しい作業をこなす。10種類の作業で平均成功率59%。本人たちも「作業は簡単で短く、成功率は控えめ」と書いている</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>SAG の分類は「工場や店舗向け」ではない</p></td><td colspan=\"1\" rowspan=\"1\"><p>Smart Analytics Global の発表</p></td><td colspan=\"1\" rowspan=\"1\"><p>原文は「Industrial and commercial applications」(産業・商業用途)。出荷の総数も約19,100台で、Counterpoint の22,000台超とは違う</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1X NEO の月499ドルのプランは配送時期が別</p></td><td colspan=\"1\" rowspan=\"1\"><p>1X の注文ページ</p></td><td colspan=\"1\" rowspan=\"1\"><p>2万ドルの早期購入枠は「US Deliveries start 2026」。月499ドルは「Standard Delivery」で日付の記載なし。最初は基本的な自律機能で、知らない作業は予約した時間に担当者が遠隔で支援する</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ゴールドバーグさんの10万年は「文章だけ」ではない</p></td><td colspan=\"1\" rowspan=\"1\"><p>Science Robotics の論説(2025年8月)</p></td><td colspan=\"1\" rowspan=\"1\"><p>現代の画像と言葉を扱う AI(VLM)の学習資料を、人が読む・見る時間に換算した概算</p></td></tr></tbody></table></div><p>確認できた4点はすべて台本に反映しました。台本の書き直しでは、問いの中の言葉(ChatGPTの瞬間)を冒頭で定義し、話の範囲(自動運転は扱わず、アームと人型ロボットが物を扱う話に絞る)を言い、自分の机の上の話は「僕の機体とソフトの組み合わせでは」と条件を付けて話す形にしています。</p><p></p><h2 id=\"index_YhZ9JBp8\">根拠の弱い数字は足さずに削った</h2><p>codex の指摘を反映する前に、次の注意がありました。</p><blockquote><p>チェックして修正点を検討して反映したいです(過剰なデータは特に問題になりやすいので注意が必要です)</p></blockquote><p>そこで、指摘に答えるために数字を足すのではなく、論旨に必要でないものは削る方針にしました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>削った数字</p></th><th colspan=\"1\" rowspan=\"1\"><p>削った理由</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1700分の1、約57年</p></td><td colspan=\"1\" rowspan=\"1\"><p>意味と単位の違う数字の割り算で、比較として成り立たない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>成功率99%超、ロボットのデータ約1時間</p></td><td colspan=\"1\" rowspan=\"1\"><p>会社の自己申告で、評価の条件を説明しないと誤解される</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>爪先の対応表の約170万点</p></td><td colspan=\"1\" rowspan=\"1\"><p>シミュレーターの中で作った表なのに、実機で170万回測ったように聞こえる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>AGIBOT 44%・Unitree 31% の内訳</p></td><td colspan=\"1\" rowspan=\"1\"><p>なくても話が変わらない</p></td></tr></tbody></table></div><p>残した数字には「推計では」「会社の発表では」を口で付けています。調査会社2社で用途の数字が逆向きに見える件は、「総数も分類も違うので、単純な裏表としては比べられません」と言う形にしました。</p><p></p><h2 id=\"index_gc6toY6y\">読み上げは「爪先」を「つまさき」と読んでいた</h2><blockquote><p>あと「爪先」をつまさきと言っていますが、アームの場合は「つめさき」の方の読み方が適切かと思います(つまさきだと、足の指先に思ってしまう)</p></blockquote><p>Open JTalk の辞書では「爪先」はツマサキでした。読み上げ用の読み替え表に「爪先 → つめさき」を足しています。同じ確認で、「90度ちょっとを超える値を送ると」の「値」がネと読まれていたことも見つけ、「値を送る → あたいを送る」を足しました。ほかに直した読みは「4分の3」(ヨンプンノサン → よんぶんのさん)、「1700分の1」(センナナヒャップンノイチ → せんななひゃくぶんのいち)、英語の名前(ChatGPT、Counterpoint、ikpy など)です。新しい固有名や数字を入れたら、エンジンの解析結果のカタカナを出して目で確認する手順にしました。</p><p></p><h2 id=\"index_HCnc4FWz\">カード上の太線と、角の立った字幕の縁取りをやめた</h2><blockquote><p>それとデザイン面で直したいのがカードの上に太いラインがあるのはいわゆるAIっぽいこなしなので違う表現にしたいです。</p></blockquote><p>最初の図解は、2つのものを対比するとき、白いカードの上辺に青とオレンジの太い線を引いていました。これをやめ、2列の間を細い縦線(2px)で区切り、色は文字の色だけで見分ける形にしました。同じ理由で、名前テロップの左端の太い線も外しています。</p><p></p><figure><img src=\"/media/s-1944x604_v-frms_webp_1ae69cf2-9b0a-420a-a0b8-a54b8fd6b7d8.png\" alt=\"\"><figcaption></figcaption></figure><p></p><blockquote><p>さらにフォントですが、フォントの黒縁が角が立っていているところが散見されます。 角を落とした黒縁に変更してください。</p></blockquote><p>字幕は CSS の文字の縁取り(-webkit-text-stroke)で作っていました。この方式は、線の角を丸める指定ができず、「宅」や「机」の角に尖った黒い角が出ます。字幕を SVG の文字に作り替え、線の角を丸くする指定(stroke-linejoin: round)を入れました。</p><p></p><figure><img src=\"/media/s-1100x610_v-fs_webp_3b59dae8-f9d7-4d46-ab1e-9cf7a3b11305.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_mlpaKj8r\">自己紹介から「秋田」「29年」「毎日」を外し、ロゴを差し替えた</h2><blockquote><p>動画の自己紹介のところで、29年秋田で、というところは削除で。 本業のデザイナーをしながらロボティクスの研究、開発を行っている、などの紹介に修正してください。</p></blockquote><blockquote><p>その記録を毎日公開、だと毎日ロボットを触っているかのような説明なので、「毎日」は削除した方がいい。</p></blockquote><p>自己紹介の文は「僕は本業のデザイナーをしながら、ロボティクスの研究と開発をしています。自宅の机の上でロボットアームを動かして、その記録を公開しています。」に固定しました。動画の中だけでなく、台本のドキュメントと codex への依頼ファイルにも同じ文言が残っていたので、そちらも直しています。</p><p>画面右上とテロップの「PIXELS TO MOTORS」の文字は、Pixels to Motors のロゴ画像に差し替えました。もらったロゴ画像(灰色の地に黒)から、地の明るさとの差で透明度を計算して切り抜き、文字の色をスライドの黒にそろえています。</p><p></p><figure><img src=\"/media/s-1920x1080_v-frms_webp_b7837ab7-c1c0-4d01-943b-1d3aa04345db.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_9szChvZM\">背景と文字の色を、記事サムネイルの #F5F5F0 と #0D0D0D に合わせた</h2><blockquote><p>動画の背景色やテキスト色を記事のサムネイルと同じ色に変更してください(差し色で使っているオレンジや水色は少し濃くするくらいでカラーを使うのはOKです) 色だけの話です。他は修正しないでください。</p></blockquote><p>記事のサムネイル画像から色を拾い、次の色に置き換えました。レイアウトと文言には手を付けていません。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>用途</p></th><th colspan=\"1\" rowspan=\"1\"><p>変更前</p></th><th colspan=\"1\" rowspan=\"1\"><p>変更後</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>背景</p></td><td colspan=\"1\" rowspan=\"1\"><p>#F4F2ED</p></td><td colspan=\"1\" rowspan=\"1\"><p>#F5F5F0</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>文字</p></td><td colspan=\"1\" rowspan=\"1\"><p>#17191E</p></td><td colspan=\"1\" rowspan=\"1\"><p>#0D0D0D</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>薄い文字</p></td><td colspan=\"1\" rowspan=\"1\"><p>#5F636B</p></td><td colspan=\"1\" rowspan=\"1\"><p>#888888</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>区切り線</p></td><td colspan=\"1\" rowspan=\"1\"><p>#CFCAC0</p></td><td colspan=\"1\" rowspan=\"1\"><p>#D6D6D1</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>オレンジ(強調)</p></td><td colspan=\"1\" rowspan=\"1\"><p>#D9542B</p></td><td colspan=\"1\" rowspan=\"1\"><p>#C4471F</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>青(対比のもう一方)</p></td><td colspan=\"1\" rowspan=\"1\"><p>#1F7FB0</p></td><td colspan=\"1\" rowspan=\"1\"><p>#1A6C98</p></td></tr></tbody></table></div><p>オレンジと青は、色覚の違いがあっても見分けられるかを検証するスクリプト(validate_palette.js)にかけ、新しい背景色の上で全項目を通過することを確かめてから使っています。検証の出力は後半の実行ログにあります。</p><p></p><h2 id=\"index_msVyMuQC\">30cmの食い違いは、アームが外れた距離ではなく、爪先の位置の決め方の違いだった</h2><blockquote><p>30cmも違うって合っている? 30mmとかじゃなくて?なんか例が良くない?</p></blockquote><p>数字そのものは記録どおりでした。公開記事 LOG222 に「同じ関節角を入れても、先端位置が300mm以上違う」とあります。ただ、同じ記事に理由も書かれていました。ikpy(関節の角度から先端の位置を計算する一般的なライブラリ)には、グリッパーの3D形状もデジタルツインの物理も入っていません。一方、デジタルツインで作った表の爪先の位置は、Unity の中でグリッパー先端の3Dモデルが実際に来た場所です。2つは、そもそも爪先の位置の決め方が違っていました。</p><p>mechArm 270 の腕の長さは約27cmです。「30cm食い違う」とだけ聞くと、腕の長さより大きく外れたように受け取れてしまいます。差し替え候補として、LOG222 にある実機のずれ(赤いブロックが6回とも同じ向きに、箱の中心から約5cmずれて落ちた件)も挙げましたが、30cm のまま説明を足す形を選んでもらいました。台本に「これは、アームが30cm外れた、という意味ではありません」「計算ツールには、グリッパーの形も、デジタルツインの物理も入っていなくて、爪先の位置の決め方そのものが違っていたんです」の2文を足し、図にも同じ説明を1行入れています。</p><p></p><figure><img src=\"/media/s-1280x720_v-fms_webp_89ef024b-47c1-4439-9d46-42759eb70eaa.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_Apnn9rBf\">打ち出し・説明・補足の3段階で、画面に強弱をつけた</h2><blockquote><p>全部じゃなくていいのですが、動画内の説明テキストの内容を強弱をつけるようにしたいです。 打ち出しを強くしたい、簡潔にわかりやすく内容を伝えたい、タイトルをして使いたいなどの時はテキストを大きくわかりやすくしたり、わかりやすい図解を入れ込んだりして動画内の内容がうまく伝わるように変化をつけたいです。</p></blockquote><p>参考にした動画の画面を見ると、小さな色付きの前置き(例「韓国の甲状腺がん」)の下に、1〜2行の大きな文(「診断は15倍/亡くなる人は増えず」)を置いていました。章の始まりには「今日の話 → 2つ目 → 見つからない」のように大きなキーワードを置き、割合は点の並びで見せていました。</p><p>これを、背景・文字・差し色はそのままで取り入れ、40ある場面のうち11場面だけ切り替えました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>段階</p></th><th colspan=\"1\" rowspan=\"1\"><p>使った場面</p></th><th colspan=\"1\" rowspan=\"1\"><p>見た目</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>打ち出し</p></td><td colspan=\"1\" rowspan=\"1\"><p>冒頭の問いかけ、結論、「すり合わせの仕事」、データの壁、比べ方の注意、最後の答え(6場面)</p></td><td colspan=\"1\" rowspan=\"1\"><p>色付きの前置き(44px)+ 1〜2行の大きな文(110〜150px)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>章の扉</p></td><td colspan=\"1\" rowspan=\"1\"><p>4つの章の始まり</p></td><td colspan=\"1\" rowspan=\"1\"><p>「今日の話」→「1つ目」→ 大きなキーワード(200px・オレンジ)→ 章の問い</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>点の並び</p></td><td colspan=\"1\" rowspan=\"1\"><p>中国メーカーの割合</p></td><td colspan=\"1\" rowspan=\"1\"><p>100個の点のうち97個に色を付ける</p></td></tr></tbody></table></div><p>打ち出しの文は、字幕をそのまま大きくするのではなく、その場面で一番残したい一言に縮めています。たとえば字幕が「この2つは、AIの学習データが足りない、という話とは別です。」の場面は、「データ不足じゃない/すり合わせの仕事」にしました。</p><p></p><figure><img src=\"/media/s-1944x604_v-frms_webp_cf142cf2-c7a5-4f0f-88d3-ae65e0605315.png\" alt=\"\"><figcaption></figcaption></figure><p></p><figure><img src=\"/media/s-1944x604_v-frms_webp_077bbf3b-9bcf-4932-bd31-a613c2b4d0eb.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index_sOZrkg72\">動画ファイルは、iCloud ではなく KIOXIA の videos/ に置いた</h2><blockquote><p>動画はデータが重くなるのでSSDの容量を圧迫するので別のフォルダに保管するようにしてください。</p></blockquote><p>動画ファイル(mp4)は、外付け SSD の KIOXIA に <code>r2r2r-data/videos/{動画名}/</code> を作って置き、iCloud からは消しました。字幕の srt と codex への依頼ファイルは軽いので iCloud に残しています。KIOXIA には、YouTube に上げる高画質の作業用(約47MB)と、確認用の圧縮版(約30MB)の2本を置いています。</p><p>ここで1つ問題が起きました。作業環境から Mac へ一度に送れるファイルは30MBまでなので、作業用の動画を12MBずつ4つに分けて送り、Mac 側でつなぎ直しました。ところが、つないだファイルのチェック値(md5)が元と一致しません。調べると、動画の先頭部分を含む1つ目の断片だけが、元より5,875バイト大きくなり、中身も化けていました。2〜4つ目の断片は元と一致していました。1つ目の断片だけを gzip で圧縮した形で送り直し、Mac 側で展開してからつなぐと、チェック値が元と一致しました。転送の途中で、動画の先頭部分だけが何らかの変換を受けたと考えていますが、原因までは確かめていません。</p><p></p><h2 id=\"index_52jeog_s\">量産のベースとして video_rules.md と制作スクリプト一式を残した</h2><p>ここまでに決めたことを、動画制作ルール video_rules.md にまとめました。記事の article_rules.md と同じく、動画を作るときに最初に読む正本です。プロジェクトの資料と、iCloud の <code>動画/_制作ベース/</code> の両方に置き、同じフォルダに制作スクリプト一式とロゴの透過 PNG を入れています。</p><p>video_rules.md は8つの節でできています。動画の構成(解説回の型と時間配分)、台本のルール(codex の検証で直した点から作った11項目)、ナレーション(読み替え表と間延びの検査)、画面デザイン(色・文字の大きさ・図の型・強弱の3段階・やらない表現)、字幕、実機の映像とテロップ、書き出しと保存、まだ決まっていない項目、です。全文は後半に載せています。</p><p>MASTER(プロジェクト全体の方針文書)の保存先テーブルと、KIOXIA の README.md のフォルダ構成には、videos/ と _制作ベース の行がまだありません。この2か所の追記は、本人が行う予定です。</p><p></p><h2 id=\"index_3J7_uzPW\">まだ決まっていない8項目は、未決のまま残した</h2><p>量産の前に決めることが残っています。video_rules.md の8節に、いまの状態と決めることを並べています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>いまの状態</p></th><th colspan=\"1\" rowspan=\"1\"><p>決めること</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ナレーションの声</p></td><td colspan=\"1\" rowspan=\"1\"><p>Open JTalk の仮音声。抑揚が機械的</p></td><td colspan=\"1\" rowspan=\"1\"><p>本人の声で録るか、品質の高い音声合成にするか。srt に1文ごとの時間が入っているので、それに合わせて録れば映像はそのまま使える</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>図解の動き</p></td><td colspan=\"1\" rowspan=\"1\"><p>すべて静止画</p></td><td colspan=\"1\" rowspan=\"1\"><p>数字のカウントアップや矢印が伸びるなどの動きを入れるか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>背景の映像</p></td><td colspan=\"1\" rowspan=\"1\"><p>実機映像は自己紹介・節2・エンディングだけ</p></td><td colspan=\"1\" rowspan=\"1\"><p>解説中も実機映像を薄く敷くか、図解だけにするか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>BGM・効果音</p></td><td colspan=\"1\" rowspan=\"1\"><p>なし</p></td><td colspan=\"1\" rowspan=\"1\"><p>使うか。使うなら YouTube で収益化できる音源から選ぶ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ジングル</p></td><td colspan=\"1\" rowspan=\"1\"><p>文言だけ</p></td><td colspan=\"1\" rowspan=\"1\"><p>音とロゴの動きを付けるか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>サムネイル</p></td><td colspan=\"1\" rowspan=\"1\"><p>まだない</p></td><td colspan=\"1\" rowspan=\"1\"><p>記事サムネイルと同じ配色・線画のトーンで作るか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>番組名</p></td><td colspan=\"1\" rowspan=\"1\"><p>未定</p></td><td colspan=\"1\" rowspan=\"1\"><p>決める</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>概要欄のチャプター</p></td><td colspan=\"1\" rowspan=\"1\"><p>まだない</p></td><td colspan=\"1\" rowspan=\"1\"><p>srt の節の開始時刻から作れる</p></td></tr></tbody></table></div><p></p><h2 id=\"index_sWhPqJ9y\">制作スクリプトの全文</h2><p>作業環境(Ubuntu、Python 3.13、ffmpeg 6.1.1、Playwright の Chromium、Noto Sans CJK JP)で動かした最終版です。Open JTalk は <code>apt-get install open-jtalk open-jtalk-mecab-naist-jdic hts-voice-nitech-jp-atr503-m001</code> で入れています。実機の映像は <code>src/c1.mp4</code>(LOG123)、<code>src/c2.mp4</code>(LOG124)、<code>src/c3.mov</code>(デジタルツインの並列スキャン)、<code>src/c4.mov</code>(デジタルツイン)として置き、ロゴは <code>logo_dark.png</code> として同じフォルダに置いて動かします。</p><h3 id=\"index_ZLxuvA9o\">narration.py(台本・読み替え表・読み上げ用の読点)</h3><pre><code class=\"language-python\"># -*- coding: utf-8 -*-\n# 台本「ロボットにもChatGPTの瞬間はもう来たのか」読み上げ原稿(codex 検証反映版)\n# 各要素: (scene_id, 字幕テキスト, 後ろの間[秒]) 間を省略すると 0.35 秒\nSEGS = [\n # 0 フック\n (\"hook_ship\", \"調査会社Counterpointの推計では、今年の上半期に出荷された人型ロボットは、\"),\n (\"hook_ship\", \"世界で2万2000台を超えました。前の年の同じ時期の、およそ4倍です。\", 0.6),\n (\"hook_forecast\", \"年末までには、5万台を超えると予想されています。\"),\n (\"hook_forecast\", \"家事をする人型ロボットの予約販売も、去年の秋から始まっています。\", 0.6),\n (\"hook_question\", \"こういうニュースを見ると、ChatGPTが出たときみたいに、\"),\n (\"hook_question\", \"ロボットも一気に賢くなって、家に来るのはもうすぐ、って感じませんか。\", 0.9),\n # 1 自己紹介\n (\"title\", \"Pixels to Motors。こんにちは、エイイチです。\"),\n (\"title\", \"僕は本業のデザイナーをしながら、ロボティクスの研究と開発をしています。\"),\n (\"title\", \"自宅の机の上でロボットアームを動かして、その記録を公開しています。\"),\n (\"title\", \"作る側の机の上から見える、フィジカルAIの話をします。\", 0.8),\n # 2 今日の問い\n (\"question\", \"今日の問いは、ロボットにもChatGPTのような瞬間は、もう来たのか、です。\"),\n (\"question\", \"ここでいうChatGPTの瞬間は、専門家じゃなくても、\"),\n (\"question\", \"いろんな仕事にすぐ使えるようになる転換点、という意味です。\", 0.7),\n (\"define\", \"フィジカルAIっていうのは、現実の世界を認識して、行動するAIのことです。\"),\n (\"define\", \"自動運転もここに入りますが、\"),\n (\"define\", \"今日は主に、アームや人型ロボットが物を扱う話に絞ります。\", 0.6),\n # 3 結論と立場\n (\"answer\", \"先に結論を言っちゃうと、写真を見せて判断させる入口は、ぐっと簡単になりました。\"),\n (\"answer\", \"でも、物を確実に動かすところは、まだ手間がかかるんですよ。\"),\n (\"answer\", \"その手間は大きく2つあります。\"),\n (\"answer\", \"動作と結びついたデータが集めにくいこと。そして、機体や環境とのすり合わせです。\", 0.6),\n (\"stance\", \"先に言っておくと、フィジカルAIはまだ使えない、と言いたいわけじゃないんです。\"),\n (\"stance\", \"どこまで来ていて、どこで手間がかかっているのかを、分けて見たいんです。\", 0.6),\n # 4 目次\n (\"toc\", \"大きく3つあります。\"),\n (\"toc\", \"出荷が4倍になったロボットは、何をしているのか。\"),\n (\"toc\", \"僕の机の上のロボットアームで、何が簡単で、何が難しかったのか。\"),\n (\"toc\", \"そして、データの壁はどれくらい高くて、誰がどう越えようとしているのか。\"),\n (\"toc\", \"最後に僕の見立てを話します。\", 0.9),\n # 5 節1\n (\"c1\", \"1つ目、出荷が4倍になったロボットは、何をしているのか、です。\", 0.7),\n (\"use\", \"Counterpointの推計では、出荷された人型ロボットの6割以上が、\"),\n (\"use\", \"展示やイベントで動きを見せる用途と、研究やデータ生産の用途なんですよ。\", 0.6),\n (\"datacollect\", \"データ生産っていうのは、ロボットが学ぶための動きの記録を作ることです。\"),\n (\"datacollect\", \"人が遠隔で動かして記録するやり方も、自律で動いた記録を残すやり方もあります。\"),\n (\"datacollect\", \"ただ、研究向けの台数が全部データ集めに使われる、という意味ではありません。\"),\n (\"datacollect\", \"それでも、用途の分類に「データ生産」という項目があること自体が、\"),\n (\"datacollect\", \"今の段階をよく表していると思います。\", 0.7),\n (\"conflict\", \"一方で、別の調査会社のSmart Analytics Globalは、\"),\n (\"conflict\", \"同じ半年の出荷を約1万9100台と推計していて、\"),\n (\"conflict\", \"産業・商業用途が7割を超えた、としています。\"),\n (\"conflict\", \"総数も分類も違うので、この2つは、単純な裏表としては比べられません。\"),\n (\"conflict\", \"それに、出荷台数は、そのまま自律的に働いている台数でもないんです。\", 0.7),\n (\"china\", \"もう1つ、SAGの推計では、出荷の97%以上が中国のメーカーです。\", 0.7),\n (\"neo\", \"家のほうも見てみます。\"),\n (\"neo\", \"アメリカの1Xという会社は、家事をする人型ロボット、NEOの予約を、去年の10月に始めました。\"),\n (\"neo\", \"2万ドルの早期購入枠は、今年配送の予定で、月499ドルのプランは、その後です。\"),\n (\"neo\", \"最初は基本的な自律機能だけで、知らない作業は、\"),\n (\"neo\", \"予約した時間に、1Xの担当者が遠隔で手伝う仕組みなんですよ。\"),\n (\"neo\", \"10月11日の時点で、一般のお客さんへの配送開始を示す公式発表は、\"),\n (\"neo\", \"僕が確認した範囲では、見つかっていません。\", 0.7),\n (\"neo_point\", \"これ、ちょっと考えさせられませんか。\"),\n (\"neo_point\", \"売っているのはロボットだけじゃなくて、遠隔で手伝う人と、\"),\n (\"neo_point\", \"そこで動きを覚えていく仕組みごとなんです。\", 0.7),\n (\"bridge1\", \"工場でも家でも、使いながらデータを集める動きが出てきているんですよ。\"),\n (\"bridge1\", \"じゃあ、物を動かすのは、何がそんなに大変なのか。\"),\n (\"bridge1\", \"まず、僕の机の上の話をさせてください。\", 0.9),\n # 6 節2\n (\"c2\", \"2つ目、僕の机の上のロボットアームで、何が簡単で、何が難しかったのか、です。\", 0.7),\n (\"v_real\", \"ここは僕自身の話から始めさせてください。\"),\n (\"v_real\", \"僕は自宅の机で、mechArm 270という小さなロボットアームを動かしています。\"),\n (\"v_real\", \"赤いブロックを掴んで、箱に置く。\"),\n (\"trials\", \"それだけの作業を、失敗も含めて650回以上試して、全部台帳に残しています。\", 0.7),\n (\"easy\", \"まず、簡単だったほうから話します。掴めたかどうかの判定です。\"),\n (\"easy\", \"iPhoneで撮った写真をAIに見せて、「ブロックを掴めていますか」と聞くと、\"),\n (\"easy\", \"はい、いいえで答えが返ってくる。この入口は、驚くほど簡単でした。\"),\n (\"easy\", \"ただ、答えが返ることと、毎回正しいことは別です。\"),\n (\"easy\", \"どこまで任せられるかは、失敗例も含めて確かめる必要があります。\", 0.7),\n (\"hard\", \"難しかったのは、動かすほうでした。\", 0.6),\n (\"gap\", \"ロボットアームの爪先がどこに来るかは、普通は関節の角度から計算で出します。\"),\n (\"gap\", \"ところが、ikpyという計算ツールで出した爪先の位置と、\"),\n (\"v_twin\", \"パソコンの中に作ったアームの分身、デジタルツインで動かした位置が、\"),\n (\"gap\", \"僕の設定では、同じ関節の角度なのに、30cm以上食い違ったんです。\"),\n (\"gap\", \"これは、アームが30cm外れた、という意味ではありません。\"),\n (\"gap\", \"計算ツールには、グリッパーの形も、デジタルツインの物理も入っていなくて、爪先の位置の決め方そのものが違っていたんです。\", 0.7),\n (\"v_scan\", \"だから僕は、分身の中でアームを動かして、\"),\n (\"v_scan\", \"関節の角度と爪先の位置の対応表を作りました。\"),\n (\"v_scan\", \"これは計算の表で、実機で全部の点を測ったわけではありません。\", 0.7),\n (\"silent\", \"もう1つ困ったのが、黙る失敗です。\"),\n (\"silent\", \"僕の機体とソフトの組み合わせでは、肩の関節に、90度ちょっとを超える値を送ると、\"),\n (\"silent\", \"エラーが出ないのに、アームが動かないことがありました。\"),\n (\"silent\", \"ログを1行ずつ見て、やっと気づきました。\", 0.7),\n (\"point2\", \"でも、ポイントはここなんです。\"),\n (\"point2\", \"この2つは、AIの学習データが足りない、という話とは別です。\"),\n (\"point2\", \"モデルや制御が、僕の機体と噛み合っているかを確かめる、すり合わせの仕事でした。\"),\n (\"point2\", \"ほかの環境で学んだことを持ってくる研究も進んでいます。\"),\n (\"point2\", \"それでも、僕の構成でちゃんと動くかは、ここで確かめるしかなかったんです。\", 0.6),\n (\"point2\", \"じゃあ、もう1つの手間、データのほうは、業界全体でどうなっているのか。\", 0.9),\n # 7 節3\n (\"c3\", \"3つ目、データの壁はどれくらい高くて、誰がどう越えようとしているのか、です。\", 0.7),\n (\"years\", \"カリフォルニア大学バークレー校のロボット研究者、\"),\n (\"years\", \"ケン・ゴールドバーグさんが、去年、こんな概算を紹介しています。\"),\n (\"years\", \"画像と言葉を扱うAIの学習資料を、\"),\n (\"years\", \"人が読んだり見たりする時間に換算すると、約10万年規模になる。\", 0.7),\n (\"wall\", \"それに比べて、ロボットが手を動かすための、動作と結びついた経験は、集めにくい。\"),\n (\"wall\", \"ゴールドバーグさんは、これを「10万年のデータの壁」と呼んでいます。\"),\n (\"wall\", \"ただ、ロボットにも同じ量が必要だと分かったわけではありません。\"),\n (\"wall\", \"集めにくい、という問題提起なんです。\", 0.6),\n (\"wall\", \"ここで、じゃあロボットにChatGPTの瞬間は当分来ない、\"),\n (\"wall\", \"と言いたくなるじゃないですか。でも、ちょっと待ってほしいんです。\", 0.7),\n (\"gen\", \"アメリカのGeneralistという会社は、人が手で作業する様子を、\"),\n (\"gen\", \"安い装着型の機材で記録して集めています。\"),\n (\"gen\", \"会社の発表では、去年の11月に27万時間、今年の4月に50万時間を超えました。\", 0.6),\n (\"gen2\", \"さらに8月には、簡単で短い作業なら、\"),\n (\"gen2\", \"数秒の実演を見せるだけで、新しい作業をこなす例も発表しています。\"),\n (\"gen2\", \"ただ、本人たちも、作業は簡単で短く、成功率はまだ控えめだと書いています。\"),\n (\"gen2\", \"それでも、「動かすには毎回大量の専用データが必要」とは、言い切れなくなってきました。\", 0.7),\n (\"compare\", \"ここで1つ、注意があります。\"),\n (\"compare\", \"作業を記録した時間と、10万年という数字は、そのまま比べられません。\"),\n (\"compare\", \"何倍足りないかより、いろんな場面の経験を、どれだけ広く集められるかが問題なんです。\", 0.7),\n (\"dirt\", \"つまり、壁はまだ高い。でも、壁の前でみんなが土を積み始めている。\"),\n (\"dirt\", \"1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、\"),\n (\"dirt\", \"Generalistが人の手作業を記録しているのも、同じ壁に向かっている動きなんです。\", 0.7),\n (\"notall\", \"ただ、ここで「なんだ、まだ全然ダメなのか」で\"),\n (\"notall\", \"片付けてしまうのも違うんです。\"),\n (\"notall\", \"写真を見せて判断させる入口は、僕の机の上でも、もう普通に使えています。\", 0.6),\n (\"notall\", \"じゃあ、この2つが混ざっている今、ロボットのニュースの何を見ればいいのか。\", 0.9),\n # 8 見立て\n (\"c4\", \"ここからが、僕の一番言いたいことです。\", 0.7),\n (\"q\", \"ロボットのニュースを見たときの問いが、2つに変わるんですよ。\", 0.6),\n (\"q1\", \"1つは、そのロボットは自律で動いているのか、人が遠隔で動かしているのか、です。\"),\n (\"q1\", \"デモ動画を見るときは、自律か遠隔操作か、\"),\n (\"q1\", \"再生速度や、人が手を出した条件が書かれているかを見ます。\", 0.7),\n (\"q2\", \"もう1つは、その仕事は「見て判断する」で済むのか、\"),\n (\"q2\", \"「触れて動かす」精度が要るのか、です。\"),\n (\"q2\", \"棚の商品が足りているかを見るのは、前者です。\"),\n (\"q2\", \"柔らかい物を潰さずに掴むのは、後者です。\"),\n (\"q2\", \"前者は、手軽に試せるところまで来ています。ただ、どこまで任せられるかは、現場で確かめる必要がある。\"),\n (\"q2\", \"後者は、その現場の機体と環境に合わせて、すり合わせる仕事が、まだ残っています。\", 0.7),\n (\"gofe\", \"ゴールドバーグさんは、昔ながらのエンジニアリングと学習を組み合わせて、\"),\n (\"gofe\", \"現場で役に立つ仕事をしながら、データを集めていくのが近道だ、と言っています。\"),\n (\"gofe\", \"僕の机の上も、AIと従来の制御を組み合わせる点では通じています。\"),\n (\"gofe\", \"ただ、現場で働きながらデータが増える仕組みとは、まだ規模も段階も違います。\", 0.7),\n (\"motion\", \"で、作る側として、もう1つ言わせてください。\"),\n (\"motion\", \"データの話と並行して、もう1つ要るのが「どう動くか」の設計です。\"),\n (\"motion\", \"例えばロボットが人に物を渡すとき、\"),\n (\"motion\", \"何秒待って手を開けば、人は安心して受け取れるのか。\"),\n (\"motion\", \"これは、成功か失敗かの判定だけでは出てこない。\"),\n (\"motion\", \"人が安心できる動きを設計する仕事も、同時に必要なんだと、僕は見ています。\", 0.9),\n # 9 回答\n (\"answer2\", \"じゃあ、冒頭の問いに答えますね。\", 0.4),\n (\"answer2\", \"僕の机では、写真を見せて判断させる入口は、ずっと簡単になりました。\"),\n (\"answer2\", \"一方、物を確実に動かすには、機体や環境とのすり合わせが、まだ必要でした。\"),\n (\"answer2\", \"その負担を減らす研究は、急速に進んでいます。\"),\n (\"answer2\", \"でも、いろんな家で、安全に家事を任せられる転換点まで来たかは、まだ判断できません。\", 0.7),\n (\"action\", \"ロボットのデモ動画を見たら、\"),\n (\"action\", \"「これは自律なのか、何回やって、何回うまくいったのか」を、\"),\n (\"action\", \"一度だけ考えてみてください。\"),\n (\"action\", \"それだけで、同じ動画の見え方が、かなり変わると思います。\", 0.9),\n # 10 アウトロ\n (\"outro\", \"今日はここまでです。\"),\n (\"outro\", \"この動画が気に入ったら、高評価とチャンネル登録をお願いします。\"),\n (\"outro\", \"コメントは全部読ませてもらっています。それではまた。\", 1.2),\n (\"sources\", \"\", 6.0),\n]\n\n# TTS 用の読み(字幕は元の表記のまま)\nREAD = [\n (\"Pixels to Motors\", \"ピクセルズ トゥー モーターズ\"),\n (\"Smart Analytics Global\", \"スマート アナリティクス グローバル\"),\n (\"Counterpoint\", \"カウンターポイント\"),\n (\"ChatGPT\", \"チャットジーピーティー\"),\n (\"Generalist\", \"ジェネラリスト\"),\n (\"mechArm 270\", \"メックアーム ニーナナゼロ\"),\n (\"iPhone\", \"アイフォーン\"),\n (\"ikpy\", \"アイケーパイ\"),\n (\"NEO\", \"ネオ\"),\n (\"1X\", \"ワンエックス\"),\n (\"SAG\", \"エスエージー\"),\n (\"AI\", \"エーアイ\"),\n (\"30cm\", \"さんじゅっセンチ\"),\n (\"爪先\", \"つめさき\"),\n (\"1万9100台\", \"いちまんきゅうせんひゃくだい\"),\n (\"値を送る\", \"あたいを送る\"),\n (\"「\", \"\"), (\"」\", \"\"),\n]\n\n# 間延び対策:ひと息が長い文だけ、読み上げ用に読点を足す(字幕は変えない)\nOVERRIDE = {\n \"同じ半年の出荷を約1万9100台と推計していて、\": \"同じ半年の出荷を、約1万9100台と、推計していて、\",\n \"工場でも家でも、使いながらデータを集める動きが出てきているんですよ。\": \"工場でも家でも、使いながら、データを集める動きが、出てきているんですよ。\",\n \"ほかの環境で学んだことを持ってくる研究も進んでいます。\": \"ほかの環境で学んだことを、持ってくる研究も、進んでいます。\",\n \"ここで、じゃあロボットにChatGPTの瞬間は当分来ない、\": \"ここで、じゃあ、ロボットに、ChatGPTの瞬間は、当分来ない、\",\n \"1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、\": \"1Xが、家の中で、遠隔で手伝いながら、動きを覚えさせているのも、\",\n}\n\ndef reading(t):\n t = OVERRIDE.get(t, t)\n for a, b in READ:\n t = t.replace(a, b)\n return t</code></pre><h3 id=\"index_ShUg2Clt\">tts2.py(読み上げ音声の生成と間延びの検査)</h3><pre><code class=\"language-python\">import subprocess, json, sys, re, wave\nimport numpy as np\nsys.path.insert(0,'.')\nfrom narration import SEGS, reading\nVOICE='/usr/share/hts-voice/nitech-jp-atr503-m001/nitech_jp_atr503_m001.htsvoice'\nDIC='/var/lib/mecab/dic/open-jtalk/naist-jdic'\nSR=48000\ndef synth(text,base):\n w=base+'.wav'; l=base+'.log'\n subprocess.run(['open_jtalk','-m',VOICE,'-x',DIC,'-r','1.08','-fm','-1','-a','0.55','-jf','1.0','-ow',w,'-ot',l],input=text.encode(),check=True)\n log=open(l,encoding='utf-8',errors='ignore').read()\n ph=[]\n for x in log.split('[Output label]')[1].strip().splitlines():\n m=re.match(r'(\\d+) (\\d+) \\S+?\\^\\S+?-(\\S+?)\\+',x)\n if m: ph.append((int(m.group(1))/1e7,int(m.group(2))/1e7,m.group(3)))\n with wave.open(w) as f: a=np.frombuffer(f.readframes(f.getnframes()),dtype=np.int16)\n inner=[(e-s) for s,e,p in ph[1:-1] if p not in('pau','sil')]\n pron=''.join(x.split(',')[9] for x in log.split('[Text analysis result]')[1].split('[Output label]')[0].splitlines() if x.count(',')>9)\n return a,ph,max(inner) if inner else 0,pron\nout=[]\nfor i,s in enumerate(SEGS):\n scene,text=s[0],s[1]; pause=s[2] if len(s)>2 else 0.35\n d=0.0; note=''\n if text:\n r=reading(text)\n a,ph,worst,pron=synth(r,f'tts/{i:03d}')\n if worst>0.3:\n parts=[p for p in re.split(r'(?<=[、。])',r) if p]\n audio=[]; worst2=0\n for n,p in enumerate(parts):\n b,ph2,w2,_=synth(p,f'tts/{i:03d}_p{n}'); worst2=max(worst2,w2)\n st=int(ph2[0][1]*SR) if n>0 else 0\n en=int(ph2[-1][0]*SR) if n<len(parts)-1 else len(b)\n audio.append(b[st:en])\n if n<len(parts)-1: audio.append(np.zeros(int(0.12*SR),dtype=np.int16))\n a=np.concatenate(audio)\n with wave.open(f'tts/{i:03d}.wav','wb') as f:\n f.setnchannels(1); f.setsampwidth(2); f.setframerate(SR); f.writeframes(a.tobytes())\n note=f'STRETCH {worst:.2f}s -> split {len(parts)} parts, max {worst2:.2f}s'\n print(i,text[:30],note)\n d=len(a)/SR\n if re.search(r'[0-9]|爪先',text): print('READ',i,text[:24],'→',pron[:60])\n out.append(dict(i=i,scene=scene,text=text,dur=d,pause=pause,note=note))\njson.dump(out,open('tts/index.json','w'),ensure_ascii=False,indent=0)\ntot=sum(o['dur']+o['pause'] for o in out)\nprint('segments',len(out),'total %d:%02d'%(tot//60,tot%60))</code></pre><h3 id=\"index_T1WL0zVD\">scenes.py(場面ごとの図解・テロップ・実機映像の指定)</h3><pre><code class=\"language-python\"># -*- coding: utf-8 -*-\nimport base64 as _b64, os as _os\nLOGO='data:image/png;base64,'+_b64.b64encode(open(_os.path.join(_os.path.dirname(_os.path.abspath(__file__)),'logo_dark.png'),'rb').read()).decode()\n# 1920x1080 の各シーン(字幕は別レイヤー)。下端 230px は字幕用に空ける。\n# v2: カード上の太線をやめ、左右は細い縦線で区切る。色は文字色だけで区別する。\nBLUE = \"#1A6C98\" # 見て判断する\nRED = \"#C4471F\" # 触れて動かす/強調\nINK = \"#0D0D0D\"\nSUB = \"#888888\"\nBG = \"#F5F5F0\"\nRULE = \"#D6D6D1\"\n\nCSS = f\"\"\"\n*{{margin:0;padding:0;box-sizing:border-box}}\nhtml,body{{width:1920px;height:1080px;overflow:hidden}}\nbody{{background:{BG};color:{INK};font-family:'Noto Sans CJK JP',sans-serif;}}\n.wrap{{position:absolute;left:140px;right:140px;top:96px;bottom:250px;display:flex;flex-direction:column;justify-content:center}}\n.tag{{position:absolute;left:140px;top:52px;font-size:28px;font-weight:700;color:{SUB};letter-spacing:.04em}}\n.tag b{{color:{RED};margin-right:14px}}\n.brand{{position:absolute;right:140px;top:46px;height:34px}}\n.kicker{{font-size:40px;font-weight:700;color:{SUB};margin-bottom:22px}}\n.h1{{font-size:84px;font-weight:900;line-height:1.25}}\n.h2{{font-size:64px;font-weight:900;line-height:1.3}}\n.h3{{font-size:48px;font-weight:700;line-height:1.4}}\n.p{{font-size:40px;font-weight:500;line-height:1.55;color:{INK}}}\n.muted{{color:{SUB}}}\n.red{{color:{RED}}} .blue{{color:{BLUE}}}\n.hero{{font-size:220px;font-weight:900;line-height:1;letter-spacing:-.02em}}\n.hero small{{font-size:90px;font-weight:900;margin-left:10px}}\n.src{{position:absolute;left:140px;bottom:236px;font-size:24px;color:{SUB}}}\n.row{{display:flex;align-items:stretch}}\n.col{{flex:1;padding:10px 56px 10px 0}}\n.col + .col{{border-left:2px solid {RULE};padding-left:56px}}\n.col .lab{{font-size:34px;font-weight:700;color:{SUB};margin-bottom:14px}}\n.col .big{{font-size:66px;font-weight:900;line-height:1.22}}\n.col .note{{font-size:34px;line-height:1.5;margin-top:16px}}\n.list{{list-style:none}}\n.list li{{font-size:46px;font-weight:700;line-height:1.35;padding:22px 0;border-bottom:2px solid {RULE};display:flex;gap:28px;align-items:baseline}}\n.list li .n{{font-size:44px;font-weight:900;color:{RED};min-width:60px}}\n.list li.dim{{color:{SUB}}}\n.chap{{font-size:200px;font-weight:900;color:{RED};line-height:1}}\n.pill{{align-self:flex-start;display:inline-block;font-size:30px;font-weight:700;padding:8px 22px;border-radius:999px;background:{INK};color:#fff;margin-bottom:26px}}\n.flow{{display:flex;align-items:center;gap:30px}}\n.box{{background:#fff;border-radius:18px;padding:34px 40px;font-size:42px;font-weight:700;text-align:center;flex:1}}\n.arrow{{font-size:64px;color:{SUB};font-weight:900}}\n\"\"\"\n\ndef page(body, tag=None, brand=True):\n t = f'<div class=\"tag\">{tag}</div>' if tag else \"\"\n b = f'<img class=\"brand\" src=\"{LOGO}\">' if brand else \"\"\n return f'<!doctype html><html><head><meta charset=\"utf-8\"><style>{CSS}</style></head><body>{t}{b}{body}</body></html>'\n\nT1 = \"<b>1</b>出荷されたロボットは何をしているのか\"\nT2 = \"<b>2</b>机の上のロボットアームで、簡単だったこと・難しかったこと\"\nT3 = \"<b>3</b>データの壁の高さと、越えようとしている会社\"\nT4 = \"<b>4</b>僕の見立て\"\n\ndef hbar(parts, height=96):\n segs = \"\"\n for lab, pct, col, tc in parts:\n segs += (f'<div style=\"flex:{pct};background:{col};height:{height}px;border-radius:6px;'\n f'display:flex;align-items:center;padding-left:24px;color:{tc};font-size:34px;font-weight:700;white-space:nowrap;overflow:hidden\">{lab}</div>')\n return f'<div style=\"display:flex;gap:4px;width:100%\">{segs}</div>'\n\ndef two(lab1, big1, lab2, big2, c1=\"blue\", c2=\"red\", size=60, note1=\"\", note2=\"\"):\n n1 = f'<div class=\"note\">{note1}</div>' if note1 else \"\"\n n2 = f'<div class=\"note\">{note2}</div>' if note2 else \"\"\n return (f'<div class=\"row\"><div class=\"col\"><div class=\"lab\">{lab1}</div>{n1}<div class=\"big {c1}\" style=\"font-size:{size}px;margin-top:8px\">{big1}</div></div>'\n f'<div class=\"col\"><div class=\"lab\">{lab2}</div>{n2}<div class=\"big {c2}\" style=\"font-size:{size}px;margin-top:8px\">{big2}</div></div></div>')\n\ndef chapter(n, title):\n return page(f'<div class=\"wrap\"><div class=\"chap\">{n}</div><div class=\"h1\" style=\"margin-top:30px\">{title}</div></div>')\n\n\n# --- 強弱の型(2026-10-11 追加)---\n# 打ち出し: 小さい前置き(色)+ 1〜2行の大きな文。1行は10字前後まで\ndef punch(kicker, lines, sub=\"\", kc=\"red\", size=150, tag=None):\n body = \"<br>\".join(lines)\n sb = f'<div class=\"h3 muted\" style=\"margin-top:40px;font-size:44px\">{sub}</div>' if sub else \"\"\n return page(f\"\"\"<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div class=\"{kc}\" style=\"font-size:44px;font-weight:900;letter-spacing:.06em;margin-bottom:28px\">{kicker}</div>\n <div style=\"font-size:{size}px;font-weight:900;line-height:1.22;letter-spacing:-.01em\">{body}</div>{sb}\n</div>\"\"\", tag=tag)\n\n# 章の扉: 「今日の話」→「1つ目」→ 大きなキーワード → 章の問い\ndef chapter2(no, keyword, sub):\n return page(f\"\"\"<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div class=\"muted\" style=\"font-size:40px;font-weight:700;letter-spacing:.1em\">今日の話</div>\n <div style=\"font-size:72px;font-weight:900;margin-top:26px\">{no}</div>\n <div class=\"red\" style=\"font-size:200px;font-weight:900;line-height:1.1;margin-top:6px\">{keyword}</div>\n <div style=\"font-size:48px;font-weight:700;margin-top:26px\">{sub}</div>\n</div>\"\"\")\n\n# 点の並び: 100個の点のうち n 個に色を付ける(割合を数で見せる)\ndef dots(n, kicker, label, big, note=\"\", src=\"\", tag=None):\n cells = \"\".join(f'<span style=\"width:38px;height:38px;border-radius:50%;background:{RED if i < n else \"#D3D3CE\"}\"></span>' for i in range(100))\n return page(f\"\"\"<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div style=\"font-size:48px;font-weight:900;margin-bottom:34px\">{kicker}</div>\n <div style=\"display:flex;align-items:center;gap:56px\">\n <div style=\"display:grid;grid-template-columns:repeat(25,38px);gap:10px\">{cells}</div>\n <div class=\"red\" style=\"font-size:190px;font-weight:900;line-height:1\">{big}</div>\n </div>\n <div style=\"font-size:48px;font-weight:700;margin-top:34px\">{label}</div>\n <div class=\"muted\" style=\"font-size:32px;margin-top:10px\">{note}</div>\n</div>{f'<div class=\"src\">{src}</div>' if src else ''}\"\"\", tag=tag)\n\nS = {}\n\nS[\"hook_ship\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期 人型ロボットの世界出荷(推計)</div>\n <div class=\"hero\">22,000<small>台超</small></div>\n <div class=\"h2\" style=\"margin-top:36px\">前年同期の <span class=\"red\">約4倍</span></div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)</div>\"\"\")\n\nS[\"hook_forecast\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年通年の予想</div>\n <div class=\"hero\">50,000<small>台超</small></div>\n <div class=\"h3\" style=\"margin-top:44px\">家事をする人型ロボットの予約販売も、2025年秋から</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)/1X</div>\"\"\")\n\nS[\"hook_question\"] = page(\"\"\"\n<div class=\"wrap\" style=\"align-items:center;text-align:center\">\n <div class=\"h1\">ロボットも、ChatGPTのときみたいに<br><span class=\"red\">一気に賢くなる?</span></div>\n</div>\"\"\")\n\nS[\"question\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">今日の問い</div>\n <div class=\"h1\">ロボットにも<span class=\"red\">ChatGPTの瞬間</span>は<br>もう来たのか</div>\n <div class=\"h3 muted\" style=\"margin-top:44px\">ChatGPTの瞬間=専門家でなくても、<br>いろんな仕事にすぐ使えるようになる転換点</div>\n</div>\"\"\")\n\nS[\"define\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">フィジカルAI とは</div>\n <div class=\"h2\">現実の世界を認識して、<span class=\"red\">行動する</span>AI</div>\n <div class=\"row\" style=\"margin-top:56px;gap:30px\">\n <div class=\"box\" style=\"border:4px solid #0D0D0D\">ロボットアーム</div>\n <div class=\"box\" style=\"border:4px solid #0D0D0D\">人型ロボット</div>\n <div class=\"box muted\" style=\"background:transparent;border:2px dashed #D6D6D1\">自動運転<br><span style=\"font-size:30px\">今日は扱わない</span></div>\n </div>\n</div>\"\"\")\n\nS[\"answer\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">先に結論</div>\n {two(\"写真を見せて判断させる入口\", \"ぐっと簡単になった\", \"物を確実に動かすところ\", \"まだ手間がかかる\")}\n <div class=\"h3\" style=\"margin-top:56px\">手間は2つ<br>① <span class=\"red\">動作と結びついたデータ</span>が集めにくい<br>② <span class=\"red\">機体や環境とのすり合わせ</span></div>\n</div>\"\"\")\n\nS[\"stance\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2 muted\" style=\"text-decoration:line-through;text-decoration-thickness:6px\">フィジカルAIはまだ使えない</div>\n <div class=\"h1\" style=\"margin-top:48px\">どこまで来ていて、<br>どこで<span class=\"red\">手間がかかっているのか</span>を、<br>分けて見る</div>\n</div>\"\"\")\n\nS[\"toc\"] = page(\"\"\"\n<div class=\"wrap\">\n <ul class=\"list\">\n <li><span class=\"n\">1</span>出荷が4倍になったロボットは、何をしているのか</li>\n <li><span class=\"n\">2</span>机の上のロボットアームで、何が簡単で何が難しかったか</li>\n <li><span class=\"n\">3</span>データの壁はどれくらい高く、誰がどう越えようとしているか</li>\n <li class=\"dim\"><span class=\"n\">+</span>僕の見立て</li>\n </ul>\n</div>\"\"\")\n\nS[\"c1\"] = chapter(\"1\", \"出荷が4倍になったロボットは、<br>何をしているのか\")\nS[\"c2\"] = chapter(\"2\", \"机の上のロボットアームで、<br>何が簡単で、何が難しかったのか\")\nS[\"c3\"] = chapter(\"3\", \"データの壁はどれくらい高くて、<br>誰がどう越えようとしているのか\")\nS[\"c4\"] = chapter(\"4\", \"僕の見立て\")\n\nS[\"use\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期の人型ロボット出荷の用途(Counterpointの推計)</div>\n {hbar([(\"展示・イベント + 研究・データ生産 6割以上\", 62, RED, \"#fff\"), (\"その他\", 38, \"#D3D3CE\", INK)])}\n <div class=\"h3\" style=\"margin-top:44px\">出荷の <span class=\"red\">6割以上</span> が、<br>動きを見せる用途と、研究・データ生産の用途</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)。帯の長さは「6割以上」を表す目安</div>\"\"\", tag=T1)\n\nS[\"datacollect\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">データ生産 = ロボットが学ぶための、動きの記録を作ること</div>\n <div class=\"row\" style=\"gap:30px\">\n <div class=\"box\">人が遠隔で動かして<br>記録する</div>\n <div class=\"box\">自律で動いた記録を<br>残す</div>\n </div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">※研究向けの台数が、全部データ集めに使われるわけではない</div>\n <div class=\"h3\" style=\"margin-top:20px\">分類に「データ生産」がある = 今の段階を表している</div>\n</div>\"\"\", tag=T1)\n\nS[\"conflict\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">同じ2026年上半期の推計</div>\n {two(\"Counterpoint\", \"見せる用途+研究・データ生産<br>6割以上\", \"Smart Analytics Global(SAG)\", \"産業・商業用途<br>7割超\", c1=\"\", c2=\"\", size=52, note1=\"出荷 22,000台超\", note2=\"出荷 約19,100台\")}\n <div class=\"h3\" style=\"margin-top:46px\">総数も分類も違う → <span class=\"red\">単純な裏表としては比べられない</span></div>\n <div class=\"h3 muted\" style=\"margin-top:10px\">出荷台数 ≠ 自律的に働いている台数</div>\n</div>\n<div class=\"src\">出典:Counterpoint Research(2026年8月)/Smart Analytics Global(2026年8月)</div>\"\"\", tag=T1)\n\nS[\"china\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">2026年上半期の人型ロボット出荷のうち、中国メーカーの割合(SAGの推計)</div>\n <div class=\"hero\">97<small>%以上</small></div>\n</div>\n<div class=\"src\">出典:Smart Analytics Global(2026年8月)</div>\"\"\", tag=T1)\n\nS[\"neo\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">1X「NEO」 家事をする人型ロボット(2025年10月 予約開始)</div>\n {two(\"値段と配送\", \"2万ドル:2026年配送予定<br>月499ドル:その後\", \"動き方\", \"最初は基本的な自律機能<br>知らない作業は<br>担当者が遠隔で手伝う\", c1=\"\", c2=\"\", size=46)}\n <div class=\"h3\" style=\"margin-top:40px\">2026年10月11日時点:一般顧客への配送開始の公式発表は<span class=\"red\">確認できず</span></div>\n</div>\n<div class=\"src\">出典:1X 公式の発表・注文ページ(2026年10月11日確認)</div>\"\"\", tag=T1)\n\nS[\"neo_point\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">売っているのは</div>\n <div class=\"flow\">\n <div class=\"box\">ロボット</div><div class=\"arrow\">+</div>\n <div class=\"box\">遠隔で<br>手伝う人</div><div class=\"arrow\">+</div>\n <div class=\"box\" style=\"border:4px solid {RED}\">動きを<br>覚えていく仕組み</div>\n </div>\n</div>\"\"\", tag=T1)\n\nS[\"bridge1\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">工場でも家でも、<br><span class=\"red\">使いながらデータを集める</span>動き</div>\n <div class=\"h2 muted\" style=\"margin-top:56px\">では、物を動かすのは<br>何がそんなに大変なのか</div>\n</div>\"\"\", tag=T1)\n\nS[\"trials\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">赤いブロックを掴んで、箱に置く</div>\n <div class=\"hero\">650<small>回以上</small></div>\n <div class=\"h3\" style=\"margin-top:36px\">失敗も含めた試行回数。全部台帳に記録</div>\n</div>\n<div class=\"src\">PICK→PLACE の通し試行(台帳 pick_place_ledger_v3.csv、2026年6月18日〜7月20日で656回)</div>\"\"\", tag=T2)\n\nS[\"easy\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{BLUE}\">簡単だった:掴めたかの判定</div>\n <div class=\"flow\">\n <div class=\"box\">iPhoneで<br>撮った写真</div><div class=\"arrow\">→</div>\n <div class=\"box\" style=\"flex:1.5\">AIに聞く<br><span style=\"font-size:32px;white-space:nowrap\" class=\"muted\">「ブロックを掴めていますか」</span></div><div class=\"arrow\">→</div>\n <div class=\"box\" style=\"border:4px solid {BLUE}\"><span class=\"blue\">はい</span>/いいえ</div>\n </div>\n <div class=\"h3\" style=\"margin-top:48px\">ただし、<span class=\"red\">答えが返る ≠ 毎回正しい</span></div>\n</div>\"\"\", tag=T2)\n\nS[\"hard\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">難しかった</div>\n <div class=\"h1\">動かすほう</div>\n</div>\"\"\", tag=T2)\n\nS[\"gap\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">僕の設定で、同じ関節の角度を入れたときの爪先の位置</div>\n <div style=\"position:relative;height:400px;margin-top:10px\">\n <div style=\"position:absolute;left:60px;top:250px;right:60px;height:4px;background:{RULE}\"></div>\n <div style=\"position:absolute;left:150px;top:218px;width:68px;height:68px;border-radius:50%;background:#fff;border:8px solid {SUB}\"></div>\n <div style=\"position:absolute;left:4px;top:310px;width:440px;text-align:center;font-size:36px;font-weight:700;color:{SUB}\">計算ツール(ikpy)</div>\n <div style=\"position:absolute;right:150px;top:218px;width:68px;height:68px;border-radius:50%;background:{RED}\"></div>\n <div style=\"position:absolute;right:4px;top:310px;width:440px;text-align:center;font-size:36px;font-weight:700;color:{RED}\">デジタルツイン</div>\n <div style=\"position:absolute;left:184px;right:184px;top:140px;height:4px;background:{INK}\"></div>\n <div style=\"position:absolute;left:182px;top:120px;width:4px;height:44px;background:{INK}\"></div>\n <div style=\"position:absolute;right:182px;top:120px;width:4px;height:44px;background:{INK}\"></div>\n <div style=\"position:absolute;left:0;right:0;top:20px;text-align:center;font-size:72px;font-weight:900\"><span class=\"red\">30cm以上</span> 食い違う</div>\n </div>\n</div>\n<div class=\"h3\" style=\"position:absolute;left:140px;bottom:290px;font-size:36px\">アームが外れた距離ではない。<span class=\"red\">爪先の位置の決め方</span>が違っていた<span class=\"muted\">(計算ツールにはグリッパーの形がない)</span></div>\n<div class=\"src\">図は模式図</div>\"\"\", tag=T2)\n\nS[\"silent\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">黙る失敗</div>\n <div class=\"flow\">\n <div class=\"box\">肩の関節に<br><span class=\"red\">90度ちょっと</span>を超える値</div><div class=\"arrow\">→</div>\n <div class=\"box\">エラーは出ない<br>アームも動かない</div>\n </div>\n <div class=\"h3 muted\" style=\"margin-top:44px\">僕の機体とソフトの組み合わせで起きたこと</div>\n</div>\"\"\", tag=T2)\n\nS[\"point2\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">この2つは</div>\n <div class=\"h2 muted\" style=\"text-decoration:line-through;text-decoration-thickness:5px\">AIの学習データが足りない</div>\n <div class=\"h1\" style=\"margin-top:30px\"><span class=\"red\">すり合わせ</span>の仕事</div>\n <div class=\"h3\" style=\"margin-top:20px\">モデルや制御が、自分の機体と噛み合っているかを確かめる</div>\n</div>\"\"\", tag=T2)\n\nS[\"years\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">画像と言葉を扱うAIの学習資料を、人が読む・見る時間に換算すると</div>\n <div class=\"hero\">約10<small>万年規模</small></div>\n</div>\n<div class=\"src\">出典:Ken Goldberg, Science Robotics(2025年8月, Editorial)</div>\"\"\", tag=T3)\n\nS[\"wall\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"h1\">10万年の<span class=\"red\">データの壁</span></div>\n <div style=\"margin-top:50px\">{two(\"画像と言葉\", \"大量にある\", \"動作と結びついた経験\", \"集めにくい\", size=58)}</div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">※ロボットにも同じ量が必要だと分かったわけではない</div>\n</div>\n<div class=\"src\">出典:Ken Goldberg, Science Robotics(2025年8月)</div>\"\"\", tag=T3)\n\nS[\"gen\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">Generalist が集めた、人の手作業の記録(会社の発表)</div>\n <div style=\"display:flex;flex-direction:column;gap:30px;margin-top:10px\">\n <div style=\"display:flex;align-items:center;gap:30px\"><div style=\"width:260px;font-size:36px;font-weight:700;color:{SUB}\">2025年11月</div>\n <div style=\"width:{int(1060*27/50)}px;height:110px;background:#D3D3CE;border-radius:0 6px 6px 0\"></div><div style=\"font-size:52px;font-weight:900;white-space:nowrap\">27万時間</div></div>\n <div style=\"display:flex;align-items:center;gap:30px\"><div style=\"width:260px;font-size:36px;font-weight:700;color:{SUB}\">2026年4月</div>\n <div style=\"width:1060px;height:110px;background:{RED};border-radius:0 6px 6px 0\"></div><div style=\"font-size:52px;font-weight:900;white-space:nowrap\">50万時間超</div></div>\n </div>\n <div class=\"h3\" style=\"margin-top:44px\">安い装着型の機材で記録</div>\n</div>\n<div class=\"src\">出典:Generalist(GEN-0 2025年11月/GEN-1 2026年4月)</div>\"\"\", tag=T3)\n\nS[\"gen2\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">Generalist GEN-1.5(2026年8月)</div>\n <div class=\"h2\">簡単で短い作業なら、<br><span class=\"red\">数秒の実演</span>で新しい作業をこなす例</div>\n <div class=\"h3 muted\" style=\"margin-top:40px\">本人たちも:作業は簡単で短く、成功率はまだ控えめ</div>\n</div>\n<div class=\"src\">出典:Generalist「GEN-1.5」(2026年8月19日)</div>\"\"\", tag=T3)\n\nS[\"compare\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">作業を記録した時間と、10万年は<br><span class=\"red\">そのまま比べられない</span></div>\n <div class=\"h3\" style=\"margin-top:50px\"><span class=\"muted\">何倍足りないか</span> より <span class=\"red\">どれだけ広く集められるか</span></div>\n</div>\"\"\", tag=T3)\n\nS[\"dirt\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h2\">壁はまだ高い。でも、<br><span class=\"red\">壁の前で土を積み始めている</span></div>\n <ul class=\"list\" style=\"margin-top:30px\">\n <li style=\"font-size:40px\"><span class=\"n\">・</span>1X:家の中で遠隔で手伝いながら、動きを覚えさせる</li>\n <li style=\"font-size:40px\"><span class=\"n\">・</span>Generalist:人の手作業を装着型の機材で記録する</li>\n </ul>\n</div>\"\"\", tag=T3)\n\nS[\"notall\"] = page(f\"\"\"\n<div class=\"wrap\">\n {two(\"写真を見せて判断させる入口\", \"もう普通に使える\", \"物を確実に動かす\", \"すり合わせとデータが<br>まだ要る\", size=56)}\n <div class=\"h3\" style=\"margin-top:48px\">「まだ全然ダメ」でも「もう来た」でもない</div>\n</div>\"\"\", tag=T3)\n\nS[\"q\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"h1\">ロボットのニュースを見る<br><span class=\"red\">問いが2つ</span>に変わる</div>\n</div>\"\"\", tag=T4)\n\nS[\"q1\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">問い 1</div>\n <div class=\"h1\">自律で動いているのか、<br>人が遠隔で動かしているのか</div>\n <div class=\"h3 muted\" style=\"margin-top:36px\">デモ動画で見るところ:自律か遠隔か/再生速度/人が手を出した条件</div>\n</div>\"\"\", tag=T4)\n\nS[\"q2\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\" style=\"background:{RED}\">問い 2</div>\n <div class=\"h2\" style=\"margin-bottom:40px\">その仕事には、どちらが要るのか</div>\n {two(\"見て判断する\", \"手軽に試せる<br><span style='font-size:34px;color:#888888'>任せる範囲は現場で確かめる</span>\", \"触れて動かす精度\", \"現場の機体と環境に<br>すり合わせる仕事が残る\", size=48, note1=\"例:棚の商品が足りているか見る\", note2=\"例:柔らかい物を潰さずに掴む\")}\n</div>\"\"\", tag=T4)\n\nS[\"gofe\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">ゴールドバーグさんの近道</div>\n <div class=\"flow\">\n <div class=\"box\">昔ながらの工学<br>+学習</div><div class=\"arrow\">→</div>\n <div class=\"box\">現場で役に立つ<br>仕事をする</div><div class=\"arrow\">→</div>\n <div class=\"box\">働きながら<br>データが増える</div>\n </div>\n <div class=\"h3\" style=\"margin-top:48px\">僕の机の上:組み合わせる点は通じる。<span class=\"muted\">規模も段階もまだ違う</span></div>\n</div>\"\"\", tag=T4)\n\nS[\"motion\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">データの話と並行して要るもの</div>\n <div class=\"h1\"><span class=\"red\">どう動くか</span>の設計</div>\n <div class=\"h3\" style=\"margin-top:36px\">ロボットが人に物を渡すとき、<br>何秒待って手を開けば、人は安心して受け取れるのか</div>\n <div class=\"h3 muted\" style=\"margin-top:28px\">成功か失敗かの判定だけでは出てこない</div>\n</div>\"\"\", tag=T4)\n\nS[\"answer2\"] = page(f\"\"\"\n<div class=\"wrap\">\n <div class=\"pill\">冒頭の問いへの答え</div>\n {two(\"写真を見せて判断させる入口\", \"ずっと簡単になった\", \"物を確実に動かす\", \"すり合わせが<br>まだ必要\", size=58)}\n <div class=\"h3\" style=\"margin-top:44px\">負担を減らす研究は急速に進んでいる。<br>家で家事を任せられる転換点かは、<span class=\"red\">まだ判断できない</span></div>\n</div>\"\"\")\n\nS[\"action\"] = page(\"\"\"\n<div class=\"wrap\">\n <div class=\"kicker\">ロボットのデモ動画を見たら</div>\n <div class=\"h1\">自律なのか、何回やって、<br><span class=\"red\">何回うまくいった</span>のか</div>\n</div>\"\"\")\n\nS[\"sources\"] = page(\"\"\"\n<div class=\"wrap\" style=\"bottom:90px;justify-content:flex-start;top:120px\">\n <div class=\"h2\" style=\"margin-bottom:30px\">出典</div>\n <div class=\"p\" style=\"font-size:29px;line-height:1.75\">\n 人型ロボットの出荷:Counterpoint Research(2026年8月)/Smart Analytics Global(2026年8月)<br>\n 1X NEO:1X 公式の発表・注文ページ(2026年10月11日確認)<br>\n 10万年のデータの壁:Ken Goldberg, Science Robotics(2025年8月, Editorial)<br>\n Generalist:GEN-0(2025年11月)、GEN-1(2026年4月)、GEN-1.5(2026年8月)の公式発表<br>\n ほかの機体から学ぶ研究の例:Gemini Robotics 1.5 技術報告(Google DeepMind)<br>\n 実機の数字:Pixels to Motors の台帳(pick_place_ledger_v3.csv)、公開記事 DAY257<br>\n <span class=\"muted\">ナレーションは仮音声(Open JTalk/HTS voice nitech_jp_atr503_m001)</span>\n </div>\n</div>\"\"\")\n\n\n# --- 強弱をつける場面の上書き(2026-10-11)---\nS[\"hook_question\"] = punch(\"こういうニュースを見ると\", [\"ロボットも\", \"一気に賢くなる?\"])\nS[\"answer\"] = punch(\"先に結論\", ['見て判断する入口は<span class=\"blue\">簡単に</span>', '物を動かすのは<span class=\"red\">まだ手間</span>'], sub=\"手間は2つ:動作と結びついたデータ/機体や環境とのすり合わせ\", size=110)\nS[\"c1\"] = chapter2(\"1つ目\", \"出荷の中身\", \"出荷が4倍になったロボットは、何をしているのか\")\nS[\"c2\"] = chapter2(\"2つ目\", \"机の上の実感\", \"何が簡単で、何が難しかったのか\")\nS[\"c3\"] = chapter2(\"3つ目\", \"データの壁\", \"どれくらい高くて、誰がどう越えようとしているのか\")\nS[\"c4\"] = chapter2(\"最後に\", \"ニュースの見方\", \"僕の見立て\")\nS[\"china\"] = dots(97, \"2026年上半期の人型ロボット出荷のうち\", \"中国メーカー\", \"97%\", note=\"SAGの推計では「97%以上」\", src=\"出典:Smart Analytics Global(2026年8月)\", tag=T1)\nS[\"point2\"] = punch(\"この2つは\", [\"データ不足じゃない\", '<span class=\"red\">すり合わせ</span>の仕事'], sub=\"モデルや制御が、自分の機体と噛み合っているかを確かめる\", tag=T2)\nS[\"wall\"] = punch(\"ゴールドバーグさんの問題提起\", ['10万年の', '<span class=\"red\">データの壁</span>'], sub=\"動作と結びついた経験は集めにくい(同じ量が必要と分かったわけではない)\", kc=\"muted\", tag=T3)\nS[\"compare\"] = punch(\"ひとつ注意\", ['<span class=\"muted\">何倍足りないか</span>より', '<span class=\"red\">どれだけ広く</span>集めるか'], sub=\"作業を記録した時間と10万年は、そのまま比べられない\", size=110, tag=T3)\nS[\"answer2\"] = punch(\"冒頭の問いへの答え\", ['判断する入口は<span class=\"blue\">来た</span>', '家事を任せるのは<span class=\"red\">まだ</span>'], sub=\"負担を減らす研究は急速に進んでいる\", size=110)\n\n# 動画シーンに重ねるオーバーレイ(透過)\nOVERLAY_CSS = f\"\"\"\n*{{margin:0;padding:0;box-sizing:border-box}}\nhtml,body{{width:1920px;height:1080px;background:transparent;font-family:'Noto Sans CJK JP',sans-serif}}\n.lt{{position:absolute;left:96px;top:80px;background:rgba(245,245,240,.94);padding:28px 40px;border-radius:14px}}\n.lt .a{{height:30px;display:block;margin-bottom:6px}}\n.lt .b{{font-size:60px;font-weight:900;color:{INK};margin-top:4px}}\n.lt .c{{font-size:30px;font-weight:500;color:{SUB};margin-top:6px}}\n.cap{{position:absolute;left:96px;top:80px;background:rgba(13,13,13,.78);color:#fff;font-size:34px;font-weight:700;padding:14px 26px;border-radius:10px}}\n\"\"\"\ndef overlay(body):\n return f'<!doctype html><html><head><meta charset=\"utf-8\"><style>{OVERLAY_CSS}</style></head><body>{body}</body></html>'\n\nO = {\n \"title\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">エイイチ</div><div class=\"c\">デザイナー/ロボティクスの研究・開発</div></div>'),\n \"v_real\": overlay('<div class=\"cap\">実機:mechArm 270-Pi 赤いブロックを掴んで箱に置く</div>'),\n \"v_twin\": overlay('<div class=\"cap\">パソコンの中のアームの分身(Unity のデジタルツイン)</div>'),\n \"v_scan\": overlay('<div class=\"cap\">デジタルツインの中で動かして、関節の角度と爪先の位置の対応表を作る</div>'),\n \"outro\": overlay('<div class=\"lt\"><img class=\"a\" src=\"'+LOGO+'\"><div class=\"b\">高評価・チャンネル登録</div><div class=\"c\">コメントは全部読んでいます</div></div>'),\n}\n\n# 動画シーン: (ソースファイル, 開始秒, 透かし消しの有無)\nVIDEO = {\n \"title\": (\"src/c1.mp4\", 0.0, True),\n \"v_real\": (\"src/c2.mp4\", 22.0, True),\n \"v_twin\": (\"src/c4.mov\", 0.0, False),\n \"v_scan\": (\"src/c3.mov\", 0.0, False),\n \"outro\": (\"src/c1.mp4\", 8.0, True),\n}</code></pre><h3 id=\"index_Na0vVVb3\">render.py(図解・テロップ・字幕を画像にする)</h3><pre><code class=\"language-python\">import json, sys, os, html\nsys.path.insert(0,'.')\nfrom scenes import S, O\nfrom playwright.sync_api import sync_playwright\nidx=json.load(open('tts/index.json'))\nos.makedirs('png/scene',exist_ok=True); os.makedirs('png/ovl',exist_ok=True); os.makedirs('png/sub',exist_ok=True)\nSUBCSS_OLD=\"\"\"*{margin:0;padding:0}html,body{width:1920px;height:1080px;background:transparent;font-family:'Noto Sans CJK JP',sans-serif}\n.s{position:absolute;left:140px;right:140px;bottom:56px;text-align:center;font-size:52px;font-weight:800;line-height:1.38;color:#fff;\n-webkit-text-stroke:9px #0D0D0D;paint-order:stroke fill;word-break:normal;line-break:strict}\"\"\"\nSUBCSS=\"\"\"*{margin:0;padding:0}html,body{width:1920px;height:1080px;background:transparent}\nsvg{position:absolute;left:0;top:0}\n.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}\"\"\"\nonly=sys.argv[1:]\ndef split2(t):\n if len(t)<=29: return html.escape(t)\n mid=len(t)/2; best=None\n for i,ch in enumerate(t[:-1]):\n if ch in '、。': \n if best is None or abs(i+1-mid)<abs(best-mid): best=i+1\n if best is None or max(best,len(t)-best)>31:\n # 読点で割れない場合は真ん中付近の助詞の後ろ\n for d in range(0,12):\n for i in (int(mid)+d,int(mid)-d):\n if 0<i<len(t) and t[i-1] in 'はがをにでとものへてや' and t[i] not in '、。」':\n return html.escape(t[:i])+'<br>'+html.escape(t[i:])\n return html.escape(t)\n return html.escape(t[:best])+'<br>'+html.escape(t[best:])\nwith sync_playwright() as p:\n b=p.chromium.launch(); pg=b.new_page(viewport={'width':1920,'height':1080})\n for k,h in S.items():\n if only and k not in only: continue\n pg.set_content(h); pg.wait_for_timeout(50); pg.screenshot(path=f'png/scene/{k}.png')\n for k,h in O.items():\n if only and k not in only: continue\n pg.set_content(h); pg.screenshot(path=f'png/ovl/{k}.png',omit_background=True)\n if not only:\n for o in idx:\n lines=split2(o['text']).split('<br>')\n fs=52\n svg_lines=''.join(f'<text x=\"960\" text-anchor=\"middle\" class=\"t\">{ln}</text>' for ln in lines)\n pg.set_content(f\"\"\"<!doctype html><html><head><meta charset=\"utf-8\"><style>{SUBCSS}</style></head><body>\n<svg id=\"s\" width=\"1920\" height=\"1080\" viewBox=\"0 0 1920 1080\">{svg_lines}</svg></body></html>\"\"\")\n pg.evaluate(\"\"\"() => {\n const ts=[...document.querySelectorAll('text.t')]; const lh=72; const n=ts.length;\n let fs=52; for (const t of ts){ const w=t.getComputedTextLength(); if (w>1640) fs=Math.min(fs, Math.floor(52*1640/w)); }\n ts.forEach((t,k)=>{ t.style.fontSize=fs+'px'; t.setAttribute('y', 1080-62-(n-1-k)*lh); });\n }\"\"\")\n pg.screenshot(path=f\"png/sub/{o['i']:03d}.png\",omit_background=True)\n b.close()\nmissing=[o['scene'] for o in idx if o['scene'] not in S and o['scene'] not in O]\nprint('missing scenes',sorted(set(missing)))</code></pre><h3 id=\"index_pTtPmPEL\">assemble.py(1文ずつ映像を作り、つないで音声と合わせる)</h3><pre><code class=\"language-python\">import json, subprocess, os, sys, wave\nimport numpy as np\nfrom concurrent.futures import ThreadPoolExecutor\nfrom PIL import Image\nsys.path.insert(0,'.')\nfrom scenes import VIDEO\nFPS=30; SR=48000\nidx=json.load(open('tts/index.json'))\nos.makedirs('chunks',exist_ok=True); os.makedirs('comp',exist_ok=True)\n# 尺(フレーム単位)\nfor o in idx:\n o['frames']=max(1,round((o['dur']+o['pause'])*FPS))\n# 動画シーンのオフセット(同じシーンが続く間は続きから)\nprev=None; off=0.0\nfor o in idx:\n sc=o['scene']\n if sc in VIDEO:\n if sc!=prev: off=VIDEO[sc][1]\n o['voff']=off; off+=o['frames']/FPS\n prev=sc\nENC=['-c:v','libx264','-preset','veryfast','-crf','20','-pix_fmt','yuv420p','-r',str(FPS),'-an']\ndef job(o):\n out=f\"chunks/{o['i']:03d}.mp4\"\n if os.path.exists(out) and '--force' not in sys.argv: return\n sub=f\"png/sub/{o['i']:03d}.png\"; sc=o['scene']; n=str(o['frames'])\n if sc in VIDEO:\n src,_,wm=VIDEO[sc]\n dl='delogo=x=2740:y=1840:w=1010:h=220,' if wm else ''\n fc=(f\"[0:v]{dl}scale=1920:1080:force_original_aspect_ratio=increase,crop=1920:1080,fps={FPS},setsar=1,\"\n f\"drawbox=x=0:y=850:w=iw:h=230:color=black@0.5:t=fill[b];[b][1:v]overlay[c];[c][2:v]overlay[v]\")\n cmd=['ffmpeg','-v','error','-y','-ss',f\"{o['voff']:.3f}\",'-stream_loop','-1','-i',src,\n '-loop','1','-i',f'png/ovl/{sc}.png','-loop','1','-i',sub,'-filter_complex',fc,'-map','[v]','-frames:v',n]+ENC+[out]\n else:\n comp=f\"comp/{o['i']:03d}.png\"\n im=Image.open(f'png/scene/{sc}.png').convert('RGBA'); im.alpha_composite(Image.open(sub).convert('RGBA')); im.convert('RGB').save(comp)\n cmd=['ffmpeg','-v','error','-y','-loop','1','-framerate',str(FPS),'-i',comp,'-frames:v',n,'-tune','stillimage']+ENC+[out]\n subprocess.run(cmd,check=True)\nwith ThreadPoolExecutor(2) as ex: list(ex.map(job,idx))\n# 音声:各文を尺ちょうどに合わせて連結\nparts=[]\nfor o in idx:\n need=o['frames']*SR//FPS\n if o['text']:\n with wave.open(f\"tts/{o['i']:03d}.wav\") as w:\n a=np.frombuffer(w.readframes(w.getnframes()),dtype=np.int16)\n else: a=np.zeros(0,dtype=np.int16)\n a=a[:need]; parts.append(np.concatenate([a,np.zeros(need-len(a),dtype=np.int16)]))\nwith wave.open('narration.wav','wb') as w:\n w.setnchannels(1); w.setsampwidth(2); w.setframerate(SR); w.writeframes(np.concatenate(parts).tobytes())\nopen('chunks/list.txt','w').write(''.join(f\"file '{o['i']:03d}.mp4'\\n\" for o in idx))\n# 字幕 SRT\ndef ts(f):\n ms=round(f/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}\"\nt=0; srt=[]; k=1\nfor o in idx:\n if o['text']:\n srt.append(f\"{k}\\n{ts(t)} --> {ts(t+round(o['dur']*FPS))}\\n{o['text']}\\n\"); k+=1\n t+=o['frames']\nopen('out.srt','w',encoding='utf-8').write('\\n'.join(srt))\njson.dump(idx,open('timeline.json','w'),ensure_ascii=False)\nsubprocess.run(['ffmpeg','-v','error','-y','-f','concat','-safe','0','-i','chunks/list.txt','-i','narration.wav',\n '-map','0:v','-map','1:a','-c:v','copy','-af','loudnorm=I=-16:TP=-1.5:LRA=11','-c:a','aac','-b:a','192k','-ar','48000','-movflags','+faststart','out.mp4'],check=True)\nprint('total frames',t,'=',t/FPS,'s')</code></pre><h3 id=\"index_vy52HFp6\">analyze.py(最初の版で、間延びした文を見つけたスクリプト)</h3><pre><code class=\"language-python\">import json,re,subprocess\nidx=json.load(open('tts/index.json'))\nrows=[]\nfor o in idx:\n if not o['text']: continue\n log=open('tts/%03d.log'%o['i'],encoding='utf-8',errors='ignore').read()\n lab=log.split('[Output label]')[1].strip().splitlines()\n ph=[]\n for l in lab:\n m=re.match(r'(\\d+) (\\d+) \\S+?\\^\\S+?-(\\S+?)\\+',l)\n if m: ph.append((int(m.group(1))/1e7,int(m.group(2))/1e7,m.group(3)))\n scale=o['dur']/ph[-1][1]\n lead=ph[0][1]-ph[0][0]; tail=ph[-1][1]-ph[-1][0]\n longv=[(p,round((e-s)*scale,2)) for s,e,p in ph[1:-1] if p not in('pau','sil') and (e-s)*scale>0.22]\n pau=[round((e-s)*scale,2) for s,e,p in ph if p=='pau' and (e-s)*scale>0.35]\n gap=round((tail)*scale+o['pause'],2)\n rows.append((o['i'],o['text'][:30],round(lead*scale,2),round(tail*scale,2),o['pause'],longv,pau))\nprint('scale ex',scale)\nfor r in rows:\n if r[5] or r[6] or r[3]+r[4]+0.25>1.3:\n print(r)</code></pre><p></p><h2 id=\"index_c3VMNBPy\">実行ログ</h2><h3 id=\"index_U2U1WMpc\">台帳の試行回数を数えた結果</h3><pre><code>$ cd $HOME/mnt/runs; for f in pick_place_ledger_v3.csv handover_ledger_v1.csv; do echo \"== $f\"; python3 - \"$f\" <<'EOF'\nimport csv,sys,collections\np=sys.argv[1]\ntry:\n rows=list(csv.DictReader(open(p,encoding='utf-8-sig')))\nexcept Exception as e:\n print(\"ERR\",e); sys.exit()\nprint(\"rows\",len(rows))\ncols=rows[0].keys() if rows else []\nfor c in [\"place_verdict\",\"pick\",\"pick_stage\",\"status\",\"run_status\",\"verdict\"]:\n if c in cols:\n print(c, collections.Counter(r[c] for r in rows).most_common(8))\nfor c in [\"run_stem\",\"timestamp\"]:\n if c in cols and rows:\n print(c, rows[0][c], \"->\", rows[-1][c])\nEOF\ndone\n== pick_place_ledger_v3.csv\nrows 657\nplace_verdict [('', 259), ('yes', 217), ('no', 127), ('unknown', 54)]\nrun_stem run_0042_test -> run_0001_20260720_213323\ntimestamp 2026-06-18T00:00:00 -> 2026-07-20T21:35:11.608053\n== handover_ledger_v1.csv\nrows 8\nrun_stem run_handover_20260720_210544_315479_001 -> run_handover_20260720_210618_993522_008\ntimestamp 2026-07-20T21:05:45.451732+09:00 -> 2026-07-20T21:06:20.009752+09:00\n\n$ cd $HOME/mnt/runs && python3 - <<'EOF'\nimport csv,collections\nrows=list(csv.DictReader(open('pick_place_ledger_v3.csv',encoding='utf-8-sig')))\nts=sorted(r['timestamp'] for r in rows if r.get('timestamp'))\nprint(\"min\",ts[0],\"max\",ts[-1])\ntest=[r for r in rows if any(k in r['run_stem'].lower() for k in ('test','dry','sim','mock'))]\nprint(\"test-like\",len(test))\nreal=[r for r in rows if r not in test]\nprint(\"real\",len(real), collections.Counter(r['place_verdict'] for r in real))\nprint([c for c in rows[0].keys()][:12])\nEOF\nls run_pick_place | wc -l\nmin 2026-06-18T00:00:00 max 2026-07-20T21:35:11.608053\ntest-like 1\nreal 656 Counter({'': 259, 'yes': 216, 'no': 127, 'unknown': 54})\n['timestamp', 'run_stem', 'trial_no', 'pick_verdict', 'pick_reason', 'pick_confidence', 'place_verdict', 'place_reason', 'place_confidence', 'block_pick_x', 'block_pick_z', 'block_place_x']\n758</code></pre><h3 id=\"index_a4QOCEhV\">間延びした文の検出結果(最初の版)</h3><pre><code>$ python3 analyze.py | python3 -c \"\nimport sys,ast\nfor line in sys.stdin:\n if line.startswith('('):\n r=ast.literal_eval(line)\n if r[5]: print(r[0], r[1], r[5][:8])\n\"\n13 現実の世界で物に触れて動かす技術のことなんですよ。 [('N', 0.47), ('d', 0.47), ('e', 0.47), ('s', 0.47), ('U', 0.41), ('y', 0.46), ('o', 0.47)]\n88 ChatGPTのようなAIの学習に使われたインターネットの文 [('ts', 0.48), ('U', 0.49), ('k', 0.46), ('a', 0.48), ('w', 0.49), ('a', 0.49), ('r', 0.48), ('e', 0.49)]\n92 ChatGPTは10万年分の教科書で育ったけれど、 [('s', 0.48), ('o', 0.44), ('d', 0.48), ('a', 0.48), ('cl', 0.48), ('t', 0.48), ('a', 0.48), ('k', 0.48)]\n94 ここで、じゃあロボットにChatGPTの瞬間は当分来ない、 [('t', 0.47), ('o', 0.48), ('o', 0.47), ('b', 0.48), ('u', 0.47), ('N', 0.45), ('k', 0.47), ('o', 0.48)]\n109 1Xが家の中で遠隔操作しながらデータを集めているのも、 [('i', 0.47), ('r', 0.42), ('u', 0.47), ('n', 0.46), ('o', 0.47), ('m', 0.47), ('o', 0.47)]\n110 出荷されたロボットの多くがデータ集めに回っているのも、 [('n', 0.45), ('o', 0.45), ('m', 0.45), ('o', 0.45)]</code></pre><h3 id=\"index_H1VyBuua\">6文を区切って作り直した結果</h3><pre><code>$ python3 fixtts.py 13 88 92 94 109 110\n13 ['現実の世界で物に、', '触れて動かす技術のことなんですよ。'] max phoneme 0.11s dur 7.25 -> 4.62\n88 ['チャットジーピーティーのようなエーアイの、', '学習に使われたインターネットの文章を、'] max phoneme 0.14s dur 17.55 -> 5.45\n92 ['チャットジーピーティーは、', '10万年分の教科書で育ったけれど、'] max phoneme 0.14s dur 9.71 -> 4.49\n94 ['ここで、', 'じゃあロボットにチャットジーピーティーの、', '瞬間は当分来ない、'] max phoneme 0.13s dur 9.59 -> 5.01\n109 ['ワンエックスが家の中で遠隔操作しなが、', 'らデータを集めているのも、'] max phoneme 0.17s dur 7.80 -> 5.02\n110 ['出荷されたロボットの多くが、', 'データ集めに回っているのも、'] max phoneme 0.15s dur 5.72 -> 4.38\n\n(109 が「しなが/ら」で切れたため、区切る位置を手で指定して作り直した)\n$ python3 fixtts.py 13 109\n13 ['現実の世界で、', '物に触れて動かす技術のことなんですよ。'] max phoneme 0.11s dur 4.62 -> 4.59\n109 ['ワンエックスが、', '家の中で遠隔操作しながら、', 'データを集めているのも、'] max phoneme 0.17s dur 5.02 -> 5.29</code></pre><h3 id=\"index_qk7nYpvW\">台本の書き直し後に、全文を検査して作り直した結果</h3><pre><code>$ python3 tts2.py (台本の書き直し後の1回目。読点の追加なし)\nREAD 1 世界で2万2000台を超えました。前の年の同じ時 → セカイデニマンニセンダイヲコエマシ’タ、マエノトシノオナジジキノ、オヨソヨンバイデス’、\nREAD 2 年末までには、5万台を超えると予想されています。 → ネンマツマデニワ、ゴマンダイヲコエルトヨソーサレテイマス’、\nREAD 7 僕は秋田で29年デザイナーをやりながら、 → ボクワアキ’タデニジューキューネンデザイナーヲヤリナガラ、\nREAD 18 その手間は大きく2つあります。 → ソノテマワオーキ’クフ’タツアリマス’、\nREAD 22 大きく3つあります。 → オーキ’クミッツアリマス’、\nREAD 23 出荷が4倍になったロボットは、何をしているのか。 → シュッカガヨンバイニナッタロボットワ、ナニヲシ’テイルノカ、\nREAD 27 1つ目、出荷が4倍になったロボットは、何をしてい → ヒ’トツメ、シュッカガヨンバイニナッタロボットワ、ナニヲシ’テイルノカ、デス’、\nREAD 28 Counterpointの推計では、出荷された人 → カウンターポイントノスイケーデワ、シュッカサレタヒ’トガタロボットノロクワリイジョーガ、\n36 同じ半年の出荷を約1万9100台と推計していて、 STRETCH 0.47s -> split 1 parts, max 0.47s\nREAD 36 同じ半年の出荷を約1万9100台と推計していて、 → オナジハントシノシュッカヲヤクイチマンキューセンヒャクダイトスイケーシ’テイテ、\nREAD 37 産業・商業用途が7割を超えた、としています。 → サンギョー、ショーギョーヨートガナナワリヲコエタ、トシ’テイマス’、\nREAD 38 総数も分類も違うので、この2つは、単純な裏表とし → ソースーモブンルイモチガウノデ、コノフ’タツワ、タンジュンナウラオモテトシ’テワクラベラレマセン、\nREAD 40 もう1つ、SAGの推計では、出荷の97%以上が中 → モーヒ’トツ、エスエージーノスイケーデワ、シュッカノキュージューナナパーセントイジョーガチューゴクノメーカーデス’、\nREAD 42 アメリカの1Xという会社は、家事をする人型ロボッ → アメリカノワンエック’ストイウカイシャワ、カジヲスルヒ’トガタロボット、ネオノヨヤクヲ、キョネンノジューガツニハジメマシ\nREAD 43 2万ドルの早期購入枠は、今年配送の予定で、月49 → ニマンドルノソーキ’コーニューワクワ、コトシ’ハイソーノヨテーデ、ツ’キヨンヒャク’キュージューキュードルノプランワ、ソ\nREAD 45 予約した時間に、1Xの担当者が遠隔で手伝う仕組み → ヨヤクシ’タジカンニ、ワンエック’スノタントーシャガエンカクデテツダウシ’クミナンデス’ヨ、\nREAD 46 10月11日の時点で、一般のお客さんへの配送開始 → ジューガツジューイチニチノジテンデ、イッパンノオキャク’サンエノハイソーカイシヲシメス’コーシ’キハッピョーワ、\n51 工場でも家でも、使いながらデータを集める動きが出てきているん STRETCH 0.47s -> split 2 parts, max 0.48s\nREAD 54 2つ目、僕の机の上のロボットアームで、何が簡単で → フ’タツメ、ボクノツ’クエノウエノロボットアームデ、ナニガカンタンデ、ナニガムズカシカッタノカ、デス’、\nREAD 56 僕は自宅の机で、mechArm 270という小さ → ボクワジタクノツ’クエデ、メックアーム、ニーナナゼロトイウチーサナロボットアームヲウゴカシ’テイマス’、\nREAD 58 それだけの作業を、失敗も含めて650回以上試して → ソレダケノサギョーヲ、シッパイモフ’クメテロッピャクゴジュッカイイジョータメシテ、ゼンブダイチョーニノコシ’テイマス’、\nREAD 65 ロボットアームの爪先がどこに来るかは、普通は関節 → ロボットアームノツメサキガドコニクルカワ、フ’ツーワカンセツノカクドカラケーサンデダシマス’、\nREAD 66 ところが、ikpyという計算ツールで出した爪先の → トコロガ、アイケーパイトイウケーサンツールデダシ’タツメサキノイチ’ト、\nREAD 68 僕の設定では、同じ関節の角度なのに、30cm以上 → ボクノセッテーデワ、オナジカンセツノカクドナノニ、サンジュッセンチイジョークイチガッタンデス’、\nREAD 70 関節の角度と爪先の位置の対応表を作りました。 → カンセツノカクドトツメサキノイチノタイオーヒョーヲツ’クリマシ’タ、\nREAD 72 もう1つ困ったのが、黙る失敗です。 → モーヒ’トツ’コマッタノガ、ダマルシッパイデス’、\nREAD 73 僕の機体とソフトの組み合わせでは、肩の関節に、9 → ボクノキ’タイトソフ’トノクミアワセデワ、カタノカンセツニ、キュージュードチョットヲコエルネヲオクルト、\nREAD 75 ログを1行ずつ見て、やっと気づきました。 → ログヲイチギョーズツミテ、ヤットキズキマシ’タ、\nREAD 77 この2つは、AIの学習データが足りない、という話 → コノフ’タツワ、エーアイノガク’シューデータガタリナイ、トイウハナシトワベツデス’、\n79 ほかの環境で学んだことを持ってくる研究も進んでいます。 STRETCH 0.48s -> split 1 parts, max 0.48s\nREAD 81 じゃあ、もう1つの手間、データのほうは、業界全体 → ジャー、モーヒ’トツノテマ、データノホーワ、ギョーカイゼンタイデドーナッテイルノカ、\nREAD 82 3つ目、データの壁はどれくらい高くて、誰がどう越 → ミッツメ、データノカベワドレクライタカク’テ、ダレガドーコエヨートシ’テイルノカ、デス’、\nREAD 86 人が読んだり見たりする時間に換算すると、約10万 → ヒ’トガヨンダリミタリスルジカンニカンサンスルト、ヤクジューマンネンキボニナル、\nREAD 88 ゴールドバーグさんは、これを「10万年のデータの → ゴールドバーグサンワ、コレヲジューマンネンノデータノカベトヨンデイマス’、\n91 ここで、じゃあロボットにChatGPTの瞬間は当分来ない、 STRETCH 0.48s -> split 2 parts, max 0.48s\nREAD 95 会社の発表では、去年の11月に27万時間、今年の → カイシャノハッピョーデワ、キョネンノジューイチガツニニジューナナマンジカン、コトシノシガツニゴジューマンジカンヲコエマシ\nREAD 96 さらに8月には、簡単で短い作業なら、 → サラニハチガツニワ、カンタンデミジカイサギョーナラ、\nREAD 100 ここで1つ、注意があります。 → ココデヒ’トツ、チューイガアリマス’、\nREAD 101 作業を記録した時間と、10万年という数字は、その → サギョーヲキロクシ’タジカント、ジューマンネントイウスージワ、ソノママクラベラレマセン、\n104 1Xが家の中で遠隔で手伝いながら動きを覚えさせているのも、 STRETCH 0.47s -> split 1 parts, max 0.47s\nREAD 104 1Xが家の中で遠隔で手伝いながら動きを覚えさせて → ワンエック’スガイエノナカデエンカクデテツダイナガラウゴキヲオボエサセテイルノモ、\nREAD 109 じゃあ、この2つが混ざっている今、ロボットのニュ → ジャー、コノフ’タツガマザッテイルイマ、ロボットノニュースノナニヲミレバイイノカ、\nREAD 111 ロボットのニュースを見たときの問いが、2つに変わ → ロボットノニュースヲミタトキノトイガ、フ’タツニカワルンデス’ヨ、\nREAD 112 1つは、そのロボットは自律で動いているのか、人が → ヒ’トツワ、ソノロボットワジリツデウゴイテイルノカ、ヒ’トガエンカクデウゴカシ’テイルノカ、デス’、\nREAD 115 もう1つは、その仕事は「見て判断する」で済むのか → モーヒ’トツワ、ソノシゴトワミテハンダンスルデスムノカ、\nREAD 125 で、作る側として、もう1つ言わせてください。 → デ、ツ’クルガワトシ’テ、モーヒ’トツイワセテクダサイ、\nREAD 126 データの話と並行して、もう1つ要るのが「どう動く → データノハナシトヘーコーシ’テ、モーヒ’トツイルノガドーウゴク’カノセッケーデス’、\nsegments 144 total 13:45\n\n(どの音で伸びているかを調べた結果。* は0.3秒を超えた音)\ntts/036.log sil* o n a j i h a N t o sh i n o sh u cl k a o y a k u i ch i m a N ky u u s e N hy a k u d a i t o s u i k e e sh* I* t* e* i* t* e* sil*\ntts/051_p1.log sil ts U k a i n a g a r a d e e t a o a ts u m e r u u g o k i g a d e t e k i t e i r u N* d* e* s* U* y* o* sil*\ntts/079.log sil h o k a n o k a N ky o o d e m a n a N d a k o t o o m o cl t e k u r u k e N ky u u m o s* u* s* u* N* d* e* i* m* a* s* U* sil*\ntts/091_p1.log sil* j a a r o b o cl t o n i ch a cl t o j i i p i i t i i n o sh u N k a N w a t* o* o* b* u* N* k* o* n* a* i* sil*\ntts/104.log sil* w a N e cl k U s u g a i e n o n a k a d e e N k a k u d e t e ts u d a i n a g a r a u g o k i o o b o e s a s e t e i* r* u* n* o* m* o* sil*\n\n(5文の OVERRIDE に読点を足し、「値を送る → あたいを送る」を読み替え表に足して作り直した)\n$ python3 tts2.py | grep -v ^READ\nsegments 144 total 13:32\n[] ← 間延びのあった文の一覧(0件)\nボクノキ’タイトソフ’トノクミアワセデワ、カタノカンセツニ、キュージュードチョットヲコエルアタイヲオクルト、\nオナジハントシノシュッカヲ、ヤクイチマンキューセンヒャクダイト、スイケーシ’テイテ、\nロボットアームノツメサキガドコニクルカワ、フ’ツーワカンセツノカクドカラケーサンデダシマス’、</code></pre><h3 id=\"index_sWdoCk8D\">色の検証(validate_palette.js)</h3><pre><code>$ node scripts/validate_palette.js \"#2A7A9B,#D9542B\" --mode light --surface \"#F4F2ED\"\nPalette (light, surface #F4F2ED, categorical): 2 slots\n [PASS] Lightness band all 2 inside L 0.43–0.77\n [FAIL] Chroma floor below floor (reads gray): [[\"#2A7A9B\",0.091]]\n [PASS] CVD separation worst adjacent #D9542B↔#2A7A9B ΔE 15.7 (protan) · tritan 32.3\n [PASS] Normal-vision floor worst adjacent #D9542B↔#2A7A9B ΔE 27.5 (normal)\n [PASS] Contrast vs surface all 2 >= 3:1\n\n → FAILED — fix the marked checks (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)\n scope: categorical palettes only. For a lone status/text color check WCAG text contrast; for a sequential ramp, lightness monotonicity.\n\n$ node scripts/validate_palette.js \"#1F7FB0,#D9542B\" --mode light --surface \"#F4F2ED\"\n [PASS] Lightness band all 2 inside L 0.43–0.77\n [PASS] Chroma floor all 2 >= 0.1\n [PASS] CVD separation worst adjacent #D9542B↔#1F7FB0 ΔE 18.7 (protan) · tritan 32.3\n [PASS] Normal-vision floor worst adjacent #D9542B↔#1F7FB0 ΔE 28.9 (normal)\n [PASS] Contrast vs surface all 2 >= 3:1\n → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)\n\n(記事サムネイルの配色に合わせたあと)\n$ node scripts/validate_palette.js \"#1A6C98,#C4471F\" --mode light --surface \"#F5F5F0\"\n [PASS] Lightness band all 2 inside L 0.43–0.77\n [PASS] Chroma floor all 2 >= 0.1\n [PASS] CVD separation worst adjacent #C4471F↔#1A6C98 ΔE 17.2 (protan) · tritan 30.4\n [PASS] Normal-vision floor worst adjacent #C4471F↔#1A6C98 ΔE 27.2 (normal)\n [PASS] Contrast vs surface all 2 >= 3:1\n → ALL CHECKS PASS (CVD in the 6–8 floor band is legal ONLY with secondary encoding: direct labels, gaps, or texture)</code></pre><h3 id=\"index_YpOeZt_F\">書き出しの結果</h3><pre><code>(試作1本目・153文)\ntotal frames 24385 = 812.8333333333334 s\nstream|codec_name=h264|width=1920|height=1080\nstream|codec_name=aac\nformat|duration=812.900000\n\n(間延びした6文を直したあと)\ntotal frames 23532 = 784.4 s\nvideo,784.400000\naudio,784.400000\n\n(codex の指摘・デザイン・読み・自己紹介を反映したあと・144文)\ntotal frames 24448 = 814.9333333333333 s\nvideo,814.933333\naudio,815.000000\n\n(記事サムネイルの配色・ロゴ・「毎日」の削除を反映したあと)\ntotal frames 24434 = 814.4666666666667 s\n\n(30cm の説明2文を足したあと・146文)\ntotal frames 24891 = 829.7 s\n\n(打ち出し・章の扉・点の並びを入れたあと)\ntotal frames 24891 = 829.7 s\n-rw-r--r-- 1 root root 47663796 Oct 11 22:01 /tmp/claude-0/-home-claude/9041d877-fbc0-57ad-97c5-f63dfd4877c5/scratchpad/v/out.mp4\n</code></pre><h3 id=\"index_ImJl488D\">KIOXIA への転送で、1つ目の断片だけが化けた記録</h3><pre><code>(作業環境側で12MBずつに分けた断片の md5)\nb3a5e3da6efb449d63049eb14400a1d3 p_00\nbff9ad51719a351b1698a90a11152082 p_01\n32d70b67218b8e9594637b6e06dadc7b p_02\nc41f4eedb61bdd951e2484d85e6f6691 p_03\n元の作業用 mp4: a54a7d2fa84886ceab3237618ffd5838\n\n(Mac 側に届いた断片の md5。p_00 だけ一致しない)\nda313675885a59bd4988dbe2ce6270ee p_00\nbff9ad51719a351b1698a90a11152082 p_01\n32d70b67218b8e9594637b6e06dadc7b p_02\nc41f4eedb61bdd951e2484d85e6f6691 p_03\n-rwx------ 12588787 Oct 11 12:44 p_00 ← 元は 12582912 バイト(5,875 バイト多い)\n-rwx------ 12582912 Oct 11 12:44 p_01\n-rwx------ 12582912 Oct 11 12:45 p_02\n-rwx------ 9186276 Oct 11 12:45 p_03\nつないだ mp4: e5e65bee0bd4cf47694b33163d54adc3 ← 元と一致しない\n\n(p_00 だけ gzip で包んで送り直し、Mac 側で展開してからつないだ)\n12582912 b3a5e3da6efb449d63049eb14400a1d3 ← 展開した p_00。元と一致\na54a7d2fa84886ceab3237618ffd5838 ← つないだ作業用 mp4。元と一致\n\n(最終版でも同じ手順で送った)\n元の作業用 mp4: 2dd0e053c2da238bbd073bb7bf8b12ad\nつないだ作業用 mp4: 2dd0e053c2da238bbd073bb7bf8b12ad</code></pre><p></p><h2 id=\"index_o0bVwuXE\">codex への検証依頼の全文</h2><p>台本の全文は、検証にかけた時点の版です(このあと書き直しています)。</p><pre><code class=\"language-markdown\"># 敵対的検証依頼:YouTube台本「ロボットにもChatGPTの瞬間はもう来たのか」\n\n## あなたへの依頼\n\nこれから公開する解説動画(約13分)の台本を、**この台本を論破するつもりで**検証してください。褒める必要はありません。見落としを1つでも多く見つけることが目的です。\n\n- 台本の書き手:Pixels to Motors(エイイチ)。秋田でロボットアーム(mechArm 270-Pi)を自宅で動かし、記録を公開している個人。29年のデザイナー経験。\n- 視聴者:フィジカルAIに関心はあるが専門家ではない日本語話者\n- 台本の結論:「見て判断する」ところは安くなった(ChatGPTの瞬間がもう来た)。「触れて動かす」ところはまだ高い(データの壁があり、まだ来ていない)\n- 検証日:2026年10月11日。Webを参照できる場合は一次情報まで遡って確認してください。参照できない場合は「未確認」と明記し、推測で OK を出さないでください\n\n## 検証してほしい観点\n\n### A. 事実と数字\nA1. 各数字が出典に実際に書かれているか。出典の数字を取り違えていないか(期間・対象・単位)\nA2. 出典自体の信頼性。調査会社のプレスリリース、企業の自社発表、二次報道の区別がついているか\nA3. 2026年10月11日時点で古くなっている情報がないか(特に 1X NEO の出荷状況、Generalist の最新発表、出荷台数)\nA4. 本人の実機の数字(S061 の650回以上、S068〜S071 の30cm以上、S074 の170万点、S076〜S077 の90度ちょっと)が、話の文脈で誤解を生まない言い方になっているか\n\n### B. 論理\nB1. 結論(S143〜S144)「見て判断する部分にはもう来ている」は、台本の中で示した根拠(本人の判定の体験談1つ)だけで言い切れるか。反例はないか\nB2. 「データの壁」の説明(S105〜S108:10万年 vs 57年、約1700分の1)は比較として成立しているか。台本の断り書き(単位をそろえた比較ではない)で十分か\nB3. (S028〜S039)「出荷ロボットの6割以上が見せる用途・研究・データ集め」と「SAG では工場・店舗向けが7割超」を並べた扱いは公正か。どちらかに寄せた印象操作になっていないか\nB4. 本人の机の上の体験(ikpy のずれ、黙る失敗)から、業界全体の「触れて動かすはまだ高い」へ一般化してよいか。飛躍がないか\nB5. 「人型ロボット」の話と「ロボットアーム」の話、「フィジカルAI」全体(自動運転を含む)の話が混ざって、範囲がぶれていないか\n\n### C. 抜けている反論・視点\nC1. 台本の主張に対して、専門家なら真っ先に出す反論は何か(例:シミュレーション合成データ、世界モデル、VLA の汎化など)\nC2. 触れずに済ませているが、視聴者の判断を変えうる事実はないか\n\n### D. 言い方\nD1. 断定が強すぎる文、逆に曖昧すぎて意味のない文\nD2. 誤解を生む用語の使い方(例:「フィジカルAI」「データ集め」「ChatGPTの瞬間」の定義)\nD3. 特定の企業・人物について、不公平・不正確な描写\n\n## 回答の書式\n\n指摘ごとに次の形で書いてください。重大なものから順に並べてください。\n\n```\n[❌ NG / ⚠️ 要注意] 文番号(例:S043)\n問題:何が問題か(1〜2文)\n根拠:確認した出典(URL)または推論の筋道\n修正案:差し替える文(台本の話し言葉の調子に合わせて)\n```\n\n最後に次の3点をまとめてください。\n1. このまま公開してよいか(はい/直してから/大きく作り直すべき)\n2. 公開前に必ず直すべき指摘の文番号\n3. 台本の結論そのものへの評価(成立している/条件つきで成立/成立していない)と、その理由\n\n---\n\n## 台本が使った出典\n\n| 主張 | 文番号 | 出典 |\n|---|---|---|\n| 2026年上半期の人型ロボット出荷2万2000台超・前年同期の約4倍・通年5万台超の予想・用途の6割以上が見せる用途と研究・データ集め | S001〜S005, S028〜S029 | Counterpoint Research(2026年8月)https://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026 ※台本作成時はページ本文を読み込めず、検索結果の抜粋から数字を取った |\n| 同期間:中国メーカー97%超・AGIBOT 44%・Unitree 31%・工場や店舗向け70%超 | S034〜S035, S041〜S042 | Smart Analytics Global の調査を紹介した TechRepublic(2026年8月11日)https://techrepublic.com/article/news-humanoid-robot-shipments-chinese-vendors |\n| 1X NEO:2025年10月に予約開始・2万ドルまたは月499ドル・難しい作業は担当者が遠隔操作・2026年10月初めの時点で顧客への出荷の発表なし | S044〜S049 | 1X 予約ページ/botinfo.ai(2026年10月5日確認)https://botinfo.ai/articles/1x-neo-home-robot / AI2Work https://ai2.work/blog/1x-neo-ships-into-homes-with-a-human-teleoperator-still-inside |\n| インターネットの文章を人が読むと約10万年(10万年のデータの壁)、狭い仕事を確実にこなすロボットを現場に置いて働きながらデータを集めるのが近道 | S087〜S096, S130〜S132 | Ken Goldberg, Science Robotics(2025年8月)https://news.berkeley.edu/?p=134910 |\n| Generalist:27万時間(2025年11月)→50万時間超(2026年4月)・新しいロボットや作業に合わせるデータは約1時間の場合も・選んだ簡単な作業で成功率99%超 | S097〜S104 | Generalist の発表を紹介した記事 https://yespress.io/generalist / https://www.humanoidsdaily.com/news/generalist-ai-unveils-gen-0-claims-scaling-laws-for-robotics-backed-by-270-000-hours-of-real-world-data |\n| 本人の実機:PICK→PLACE の通し試行656回(2026年6月18日〜7月20日)、FKテーブル約170万点、ikpy とデジタルツインの爪先が同じ関節角で300mm以上ずれる、j1 が約90.7度を超えると send_angles がエラーなしで無視される | S061, S068〜S071, S074, S076〜S077 | 本人の台帳 pick_place_ledger_v3.csv、公開記事 DAY257・DAY283、設計メモ arm_kinematics.md(非公開資料を含むため、ここは記述の妥当性だけ見てください) |\n\n## 台本全文(動画で読み上げている版)\n\n\n### 0 フック\n\nS001 今年の1月から6月までに、世界で出荷された人型ロボットは、2万2000台を超えました。\n\n### 0 フック\n\nS002 前の年の同じ時期の、およそ4倍です。\n\n### 0 フック\n\nS003 調査会社のCounterpointが、8月に出した数字です。\nS004 年末までには、5万台を超えると予想されています。\nS005 家事をする人型ロボットの予約販売も、去年の秋から始まっています。\nS006 こういうニュースを見ると、ChatGPTが出たときみたいに、\nS007 ロボットも一気に賢くなって、家に来るのはもうすぐ、って感じませんか。\n\n### 1 自己紹介\n\nS008 Pixels to Motors。こんにちは、エイイチです。\n\n### 1 自己紹介\n\nS009 僕は秋田で29年デザイナーをやりながら、\n\n### 1 自己紹介\n\nS010 自宅の机の上でロボットアームを動かして、その記録を毎日公開しています。\n\n### 1 自己紹介\n\nS011 作る側の机の上から見える、フィジカルAIの話をします。\n\n### 2 今日の問い\n\nS012 今日の問いは、ロボットにもChatGPTのような瞬間は、もう来たのか、です。\nS013 フィジカルAIっていうのは、AIに体を持たせて、\nS014 現実の世界で物に触れて動かす技術のことなんですよ。\nS015 ロボットアームも、人型ロボットも、自動運転もここに入ります。\n\n### 3 結論と立場\n\nS016 先に結論を言っちゃうと、安くなったのは「見て判断する」ところで、\n\n### 3 結論と立場\n\nS017 「触れて動かす」ところは、まだ高いんですよ。\n\n### 3 結論と立場\n\nS018 鍵になるのが、「データの壁」です。\nS019 先に言っておくと、フィジカルAIはまだ使えない、と言いたいわけじゃないんです。\nS020 どこまで来ていて、どこで止まっているのかを、分けて見たいんです。\n\n### 4 目次\n\nS021 大きく3つあります。\n\n### 4 目次\n\nS022 出荷が4倍になったロボットは、何をしているのか。\n\n### 4 目次\n\nS023 僕の机の上のロボットアームで、何が簡単で、何が難しかったのか。\n\n### 4 目次\n\nS024 そして、データの壁はどれくらい高くて、誰がどう越えようとしているのか。\n\n### 4 目次\n\nS025 最後に僕の見立てを話します。\n\n### 5 節1:出荷されたロボットは何をしているのか\n\nS026 1つ目、出荷が4倍になったロボットは、何をしているのか、です。\nS027 まず数字の中身を見ます。\nS028 Counterpointによると、出荷された人型ロボットのうち6割以上は、\nS029 展示やイベントで動きを見せる用途と、研究やデータ集めの用途なんですよ。\nS030 データ集めっていうのは、人がロボットを遠隔で動かして、その動きを記録して、\nS031 AIが学ぶための教材を作る仕事のことです。\nS032 つまり、ロボットの大きな買い手の1つは、ロボットに教えるためのロボットなんです。\nS033 ただ、ここで1つ断っておきます。\nS034 別の調査会社のSmart Analytics Globalは、同じ半年について、\nS035 工場や店舗向けが7割を超えた、と言っているんですよ。\nS036 同じ期間なのに、ほぼ逆の数字です。\nS037 用途の分け方が、会社によって違うんだと思います。\nS038 だから「全部見せ物だ」とも、「もう工場で働いている」とも言い切れない。\nS039 そこは正直に言っておきます。\nS040 はっきりしているのは、どこが作っているかです。\nS041 SAGの数字だと、出荷の97%以上が中国のメーカーです。\nS042 上位のAGIBOTとUnitreeの2社だけで、およそ4分の3なんですよ。\nS043 家のほうも見てみます。\nS044 アメリカの1Xという会社は、家事をする人型ロボット、NEOの予約を、去年の10月に始めました。\nS045 値段は2万ドル。月499ドルのプランもあります。\nS046 ただ、難しい家事は、1Xの担当者がVRゴーグルをつけて、\nS047 ロボットのカメラ越しに家の中を見ながら、遠隔で動かす仕組みなんですよ。\nS048 今年中にアメリカで届け始める予定ですが、\nS049 10月の初めの時点で、お客さんの家に届いたという発表は、まだありません。\nS050 これ、ちょっと考えさせられませんか。\nS051 売っているのはロボットだけじゃなくて、遠隔で動かす人と、\nS052 そこで集まるデータの仕組みごとなんです。\nS053 で、ここが大事なんですけど、工場の話にも家の話にも、\nS054 データを集めるという、同じ目的が出てくるんですよ。\nS055 じゃあ、なぜそこまでしてデータを集めないといけないのか。\nS056 その前に、僕の机の上の話をさせてください。\n\n### 6 節2:机の上のロボットアームで、簡単だったこと・難しかったこと\n\nS057 2つ目、僕の机の上のロボットアームで、何が簡単で、何が難しかったのか、です。\nS058 ここは僕自身の話から始めさせてください。\nS059 僕は自宅の机で、mechArm 270という小さなロボットアームを動かしています。\nS060 赤いブロックを掴んで、箱に置く。\nS061 それだけの作業を650回以上試して、全部の結果を台帳に残しているんですよ。\nS062 まず、簡単だったほうから話します。掴めたかどうかの判定です。\nS063 iPhoneで撮った写真をAIに見せて、「ブロックを掴めていますか」と聞くと、\nS064 はい、いいえで答えが返ってくる。\nS065 少し前なら、ここを作るだけで大仕事だったはずなんですよ。\nS066 難しかったのは、動かすほうでした。\nS067 ロボットアームの爪の先がどこに来るかは、普通は関節の角度から計算で出します。\nS068 ところが、ikpyという、よく使われる計算ツールで出した爪先の位置と、\nS069 パソコンの中に作ったアームの分身で、実際に動かした位置が、\nS070 同じ関節の角度なのに、30cm以上ずれたんです。\nS071 30cmですよ。机の上のブロックなら、端から端まで外れます。\nS072 だから僕は、その分身、デジタルツインの中でアームを実際に動かして、\nS073 爪先がどこに来るかを、1点ずつ測って表にしました。\nS074 その表は今、170万点ほどあります。\nS075 もう1つ困ったのが、黙る失敗です。\nS076 肩の関節に、90度ちょっとを超える角度を送ると、\nS077 エラーも出さずに、指令が黙って捨てられるんですよ。\nS078 アームは動かない。でも、何も言ってこない。\nS079 ログを1行ずつ見て、やっと気づきました。\nS080 でも、ポイントはここなんです。\nS081 見て判断するところは、AIに頼めばすぐ手に入りました。\nS082 でも、爪先がどこに来るか、指令がちゃんと届いたかは、\nS083 自分の机の上で、自分のアームで測るしかなかった。\nS084 インターネットのどこにも、僕のアームの、僕の机の上のデータはないんですよ。\nS085 じゃあ、この「どこにもないデータ」は、世界全体ではどれくらい足りないのか。\n\n### 7 節3:データの壁の高さと、越えようとしている会社\n\nS086 3つ目、データの壁はどれくらい高くて、誰がどう越えようとしているのか、です。\nS087 カリフォルニア大学バークレー校のロボット研究者、\nS088 ケン・ゴールドバーグさんが、去年、面白い計算をしています。\nS089 ChatGPTのようなAIの学習に使われたインターネットの文章を、\nS090 人が全部読むと何年かかるか。答えは、約10万年なんですよ。\nS091 ロボットが手を動かすためのデータは、その足元にも及ばない。\nS092 ゴールドバーグさんは、これを「10万年のデータの壁」と呼んでいます。\nS093 ChatGPTは10万年分の教科書で育ったけれど、\nS094 ロボットには、その教科書がまだない、ということなんです。\nS095 ここで、じゃあロボットにChatGPTの瞬間は当分来ない、\nS096 と言いたくなるじゃないですか。でも、ちょっと待ってほしいんです。\nS097 アメリカのGeneralistという会社は、人が手で作業する様子を、\nS098 安い装着型の機材で記録して集めています。\nS099 去年の11月の時点で27万時間、今年の4月には50万時間を超えました。\nS100 半年で2倍近くです。\nS101 しかもこの会社によると、新しいロボットや新しい作業に合わせるのに、\nS102 そのロボット自身のデータは、1時間ほどで済む場合があって、\nS103 選んだいくつかの簡単な作業では、成功率が99%を超えたそうです。\nS104 会社自身の発表なので、そこは差し引いて聞いてください。\nS105 50万時間を年に直すと、約57年です。\nS106 10万年と比べると、まだ1700分の1くらい。\nS107 ただ、これは人が文章を読む時間と、作業を記録した時間なので、\nS108 単位をそろえた比較ではありません。目安として聞いてください。\nS109 つまり、壁はまだ高い。でも、壁の前でみんなが土を積み始めている。\nS110 1Xが家の中で遠隔操作しながらデータを集めているのも、\nS111 出荷されたロボットの多くがデータ集めに回っているのも、\nS112 同じ壁に向かっている動きなんです。\nS113 ただ、ここで「なんだ、まだ全然ダメなのか」で\nS114 片付けてしまうのも違うんです。\nS115 見て判断するほうには、もう10万年分の教科書がある。\nS116 そこは僕の机の上でも、もう普通に使えています。\nS117 じゃあ、この2つが混ざっている今、ロボットのニュースの何を見ればいいのか。\n\n### 8 見立て\n\nS118 ここからが、僕の一番言いたいことです。\nS119 データの壁を知っていると、\nS120 ロボットのニュースを見たときの問いが、2つに変わるんですよ。\nS121 1つは、そのロボットは誰が動かしていて、データは誰が集めているのか、です。\nS122 人が遠隔で動かしているのか、ロボットが自分で動いているのか。\nS123 デモ動画では、ここがほとんど言われないんですよね。\nS124 もう1つは、その仕事は「見て判断する」で済むのか、\nS125 「触れて動かす」精度が要るのか、です。\nS126 棚の商品が足りているかを見るのは、前者です。\nS127 柔らかい物を潰さずに掴むのは、後者です。\nS128 前者なら、もう安く使える。\nS129 後者は、その現場で測って固める仕事が、まだ残っている。\nS130 ゴールドバーグさんも、昔ながらのエンジニアリングで、\nS131 狭い仕事を確実にこなすロボットを現場に置いて、\nS132 働きながらデータを集めるのが近道だ、と言っています。\nS133 僕の机の上でやっていることも、まさにそれなんですよ。\nS134 判断はAIに任せる。動きは、自分のアームで測って固める。\nS135 で、作る側として、もう1つ言わせてください。\nS136 データの壁を越えたあとも、最後に残るのは「どう動くか」なんです。\nS137 例えばロボットが人に物を渡すとき、\nS138 何秒待って手を開けば、人は安心して受け取れるのか。\nS139 これは、成功か失敗かの判定だけでは出てこない。\nS140 動きの質そのものを設計する仕事が、\nS141 壁の向こうにも残っていると、僕は見ています。\n\n### 9 冒頭の問いへの回答\n\nS142 じゃあ、冒頭の問いに答えますね。\n\n### 9 冒頭の問いへの回答\n\nS143 ロボットにChatGPTの瞬間は、見て判断する部分には、もう来ています。\n\n### 9 冒頭の問いへの回答\n\nS144 でも、現実に触れて動かす部分には、まだ来ていません。\n\n### 9 冒頭の問いへの回答\n\nS145 その差を埋めるデータを、今、世界中で必死に集めている最中です。\nS146 ロボットのデモ動画を見たら、\nS147 「これは誰が動かしていて、何回やって、何回うまくいったのか」を、\nS148 一度だけ考えてみてください。\nS149 それだけで、同じ動画の見え方が、かなり変わると思います。\n\n### 10 アウトロ\n\nS150 今日はここまでです。\n\n### 10 アウトロ\n\nS151 この動画が気に入ったら、高評価とチャンネル登録をお願いします。\n\n### 10 アウトロ\n\nS152 コメントは全部読ませてもらっています。それではまた。</code></pre><p></p><h2 id=\"index_tELWWv5H\">codex から返ってきた検証結果の全文</h2><pre><code>判定は「直してから」です。現在の結論は、台本の根拠だけでは成立していません。 特に、①「ChatGPTの瞬間」の定義、②異種データの1700分の1比較、③実機の設定・制御問題を学習データ不足へつなげる論理、④Generalistの最新発表の欠落が重大です。\n2026年10月11日時点でWeb確認しました。以下では、一次資料の本文確認、検索抜粋だけの確認、推論、未確認を区別します。本人の台帳・設計メモは依頼どおり検証対象外とし、数字の伝え方だけを評価します。\n[❌ NG] S016〜S018、S143〜S145\n問題:「ChatGPTの瞬間」が未定義です。写真への回答を一度得られたことから、視覚判断全体の転換を宣言し、さらに物理操作全体には転換がないと断定するのは、両方向に飛躍しています。\n根拠:使いやすさ、汎化、信頼性、普及、費用は別の評価軸です。例えば[Gemini Roboticsの公式資料](https://deepmind.google/en/models/gemini-robotics/gemini-robotics/)は、視覚と言語から動作を出すモデルを示していますが、提供はPrivate previewです。「能力がある」と「誰でも利用できる」も分ける必要があります。\n修正案:「ここではChatGPTの瞬間を、専門家でなくても、いろんな仕事にすぐ使えるようになる転換点、と呼びます。僕の机では、画像に質問する入口はぐっと簡単になりました。でも、いろんな家で安全に家事を任せられる転換点まで来たかは、まだ判断できません。」\n[❌ NG] S105〜S109\n問題:57年への換算は正しいですが、「1700分の1」はデータの不足率ではありません。「目安」と断っても、比較対象の意味が違うため、壁の高さを数量化したことにはなりません。\n根拠:500,000÷24÷365≒57.1、100,000÷57.1≒1752。前者は作業記録の総時間、後者は異種の学習資料を人間が読む・見る時間への換算です。情報密度、重複、センサー、タスクの多様性、必要性能がそろっていません。また一社のデータ量は、世界全体の総量でもありません。\n修正案:「50万時間は、連続した時間に直すと約57年です。ただ、文章や画像を見る時間と、作業を記録した時間は、そのまま比べられません。何倍足りないかではなく、必要な経験をどれだけ広く集められるかが問題なんです。」\n[❌ NG] S068〜S074、S082〜S085、S133〜S134\n問題:計算モデル間の30cmの不一致と指令の不受理は、それだけではAIの学習データ不足を示しません。モデルの整合性や制御インターフェースの問題を、業界の「データの壁」の実例として使っています。\n根拠:同じ角度の比較には、座標系、リンク寸法、関節軸、角度の符号・原点、工具先端の定義を一致させる必要があります。どちらが正しいかは実機との比較が必要です。原因は非公開資料のため未確認です。\n修正案:「僕の設定では、計算モデルとシミュレーターの位置が30cm以上食い違いました。これはAIの学習データ不足とは別に、モデルや制御の整合性を確かめる仕事が必要だった、という話です。」\n[❌ NG] S099〜S104、S109、S129、S144\n問題:4月のGEN-1を最新状況のように紹介していますが、8月のGEN-1.5が抜けています。新しい作業への適応負担を論じる動画では、視聴者の判断を変える重要な欠落です。\n根拠:[Generalistの8月19日公式発表](https://generalistai.com/blog/gen-1.5)は、短い実演を使った新タスクへの適応を報告しています。ただし同社自身の評価で、対象は簡単で短い作業です。家庭全体への汎化を証明したわけではありません。\n修正案:「さらに8月にはGEN-1.5を発表して、簡単で短い作業なら、数秒の実演から適応できる例も報告しています。会社発表なので評価条件は見る必要がありますが、『動かすには毎回大量の専用データが必要』とも言い切れなくなっています。」\n[❌ NG] S034〜S038\n問題:SAGの「industrial and commercial」を「工場や店舗向け」と狭めています。また用途と導入先は重なり得るため、「ほぼ逆の数字」「分類の違いが原因」とする説明は未立証です。\n根拠:[SAGの一次発表](https://smartanalyticsglobal.com/global-humanoid-robot-shipments-2026-agibot-unitree/)の表現は産業・商業用途です。同社の総出荷推計も約19,100台で、Counterpointの22,000台超とは異なります。両調査の分類対応表や集計方法は、今回確認できていません。商業施設での展示と研究用途が排他的とは限りません。\n修正案:「一方、SAGは約1万9100台と推計して、産業・商業用途が7割超としています。ただ、総数も分類も違うので、この2つは単純な裏表として比べられません。どれだけ自律的に実務をこなしたかは、この数字だけでは分からないんです。」\n[❌ NG] S089〜S094、S115〜S116\n問題:Goldbergの10万年は「文章だけ」ではありません。さらに、その比喩を「見て判断する側には十分な教科書がある」という性能保証に変えています。\n根拠:[Science Roboticsの原文](https://autolab.berkeley.edu/assets/publications/media/GOFE-Can-Close-the-100000-Year-Robot-Data-Gap-Science-Robotics-Aug-2025-scirobotics.aea7390.pdf)は、現代のVLMの学習に使う文章と画像を読む・見る時間への概算です。これはEditorialであり、一般ロボットに必要なデータ量の実証値ではありません。\n修正案:「ゴールドバーグさんは、画像と言葉を扱うAIの学習資料を、人が読む・見る時間に換算すると、約10万年規模だと紹介しています。ロボットにも同じ量が必要だと分かったわけではありませんが、動作指令と結びついた経験は集めにくい、という問題提起です。」\n[❌ NG] S083〜S084、S129、S145\n問題:「自分の机で測るしかない」は、他環境・他機体からの転移を否定する表現です。また、データだけが判断と動作の差を埋めるように聞こえます。\n根拠:[Gemini Robotics 1.5技術報告](https://storage.googleapis.com/deepmind-media/gemini-robotics/Gemini-Robotics-1-5-Tech-Report.pdf)は機体をまたいだ技能転移を報告しています。専門家なら、VLAの汎化、シミュレーション、世界モデル、モデルに基づく制御をまず反論として出します。[NVIDIAの公式説明](https://developer.nvidia.com/isaac/gr00t?ncid=so-yout-261862-vt48)も実データと合成データの併用を示しています。ただし、これらが現場検証を不要にしたとは言えません。\n修正案:「ほかの環境で学んだことを持ってくる研究も進んでいます。それでも、僕の構成でちゃんと動くかは確認が必要でした。差を埋めるのはデータだけでなく、モデル、制御、機械そのものの改善でもあります。」\n[⚠️ 要注意] S013〜S015、S017、S144\n問題:「物に触れて動かす」で自動運転まで定義し、その後はほぼ物体操作と家庭用人型ロボットの話になります。広い領域を否定する結論になっています。\n根拠:台本内の対象の不一致です。Goldbergの原文自身も、自動運転や荷物仕分けを既に実用化された例として挙げています。限定用途の実用化と、汎用家庭ロボットの普及は別問題です。\n修正案:「フィジカルAIは、現実世界を認識して行動するAIのことです。自動運転も入りますが、今日は主に、アームや人型ロボットが物を扱う話、とくに家庭のような変化の多い場所に絞ります。」\n[⚠️ 要注意] S001〜S004、S028〜S029、S040〜S042\n問題:数字の記載は確認できても、市場の確定実績として扱うには調査方法の確認が不足しています。「はっきりしている」も強すぎます。\n根拠:Counterpointの公式ページ由来の検索抜粋では、22,000台超、前年比約300%増、通年50,000台超の予測、用途の合計60%超を確認しました。直接開いた本文は取得できず、集計定義は未確認です。SAGの本文では中国メーカー97%超、44%+31%を確認しました。いずれも調査会社の推計で、出荷は自律稼働や顧客検収を意味しません。[Counterpoint](https://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026)、[SAG](https://smartanalyticsglobal.com/global-humanoid-robot-shipments-2026-agibot-unitree/)\n修正案:「Counterpointの推計では、今年上半期の出荷は2万2000台超、前年同期のおよそ4倍です。ただ、出荷台数は、そのまま自律的に働いている台数ではありません。中国メーカーへの集中も、SAGの推計では非常に大きいです。」\n[⚠️ 要注意] S030〜S032、S053〜S054、S110〜S112\n問題:「データ集め」を遠隔操作だけに限定し、研究用途までデータ生産へ読み替えています。さらに工場と家庭の共通の「目的」として断定しています。\n根拠:研究には制御・機構・移動などの検証も含まれ、全てが学習データ収集とは限りません。作業中の自律ログや人の実演も収集方法です。「研究・データ生産」という合算から、それぞれの比率や購入目的は分かりません。\n修正案:「データ集めには、人が遠隔で動かして記録する方法も、自律作業の記録を残す方法もあります。ただ、研究向けの台数が全部データ集めに使われる、という意味ではありません。」\n[⚠️ 要注意] S044〜S049、S051〜S052\n問題:価格プランの提供時期を省き、遠隔支援が常時の仕組みのように聞こえます。顧客出荷については、発表を見つけられないことと、出荷していないことを区別すべきです。\n根拠:[1Xの発売発表](https://www.1x.tech/discover/neo-home-robot)は2025年10月28日、20,000ドルのEarly Accessは2026年優先配送、499ドルの月額プランは後日配送です。[現行注文ページ](https://www.1x.tech/order)では基本的自律性と予約した時間のExpert Modeを説明しています。VRゴーグルの具体的運用は今回の公式本文では未確認。10月11日までの一般顧客への配送開始を示す公式発表も今回の検索では確認できませんでした。\n修正案:「2万ドルの早期購入枠は2026年配送予定で、月499ドルのプランは後日です。初期の自律機能に加えて、知らない作業では予約した時間に担当者が遠隔で支援します。10月11日時点で、一般顧客への配送開始を示す公式発表は、僕が確認した範囲では見つかっていません。」\n[⚠️ 要注意] S063〜S065、S081、S126〜S129\n問題:回答が返ることと、正しく判定できることを混同しています。「前者ならもう安く使える」は、精度や運用条件を無視しています。\n根拠:判定を評価するには、実際の保持状態との照合、失敗画像、遮蔽、誤判定率などが必要です。棚の欠品検知にも死角や商品識別、照明、誤報対応があります。台本にはモデル名、正答率、費用がなく、「安い」の実証もありません。\n修正案:「写真を見せて質問する入口は、驚くほど簡単でした。ただ、答えが返ることと、毎回正しいことは別です。僕の条件でどこまで任せられるかは、失敗例も含めて確かめる必要があります。」\n[⚠️ 要注意] S061\n問題:650回以上が成功した回数、あるいは同一条件での評価回数に聞こえます。提示された情報は、期間中のPICK→PLACE通し試行656回です。\n根拠:ユーザー提示の台帳説明に基づく指摘です。試行間の構成変更や中断の扱いは未確認で、成功率やモデル性能は推定できません。\n修正案:「6月18日から7月20日までに、掴んで置く一連の試行を656回記録しました。これは成功回数ではなく、失敗も含めた試行回数です。」\n[⚠️ 要注意] S069〜S074\n問題:「実際に動かした」が実機測定に聞こえ、170万点が現実の経験データとして受け取られます。提示された説明ではシミュレーター内のFK表です。\n根拠:仮想モデルの大量サンプリングは、そのモデルの写像を細かくしたものです。点数が多くても、モデルと実機の一致や接触精度を証明しません。\n修正案:「シミュレーターの中で関節角と爪先位置の対応を計算して、約170万点の表にしました。実機で170万回測ったわけではなく、現物との一致は別に確認する必要があります。」\n[⚠️ 要注意] S076〜S079\n問題:90度ちょっとという閾値を機種全体の仕様として語っています。また「指令が捨てられる」は、通信を観測していなければ原因の断定です。\n根拠:提示情報は本人の構成でj1が約90.7度を超えたときの観察です。ファームウェア、SDK、他関節の値、検証箇所は未確認です。「肩」だけでは関節も特定しにくくなります。\n修正案:「僕の機体とソフトの組み合わせでは、j1に約90.7度を超える値を送ると、例外が出ないのに動かないことがありました。機種全体の仕様か、どの段階で止まったのかは、まだ切り分けが必要です。」\n[⚠️ 要注意] S099〜S104\n問題:期間が「半年」と丸められ、99%超が評価全体の平均のようにも聞こえます。1時間は追加のロボットデータであって、全学習・導入に必要な時間ではありません。\n根拠:[GEN-0公式発表](https://generalistai.com/blog/gen-0)は2025年11月4日、[GEN-1公式発表](https://generalistai.com/blog/gen-1)は2026年4月2日で約5か月です。27万時間超と50万時間超、平均99%と一部タスク99%超を区別する必要があります。自己申告の試験結果で、現場導入の総費用は示していません。\n修正案:「会社発表では、約5か月で27万時間超から50万時間超に増えました。事前学習に加えて、示した試験では約1時間のロボットデータを使い、選んだ作業群で平均99%の成功率を報告しています。導入全体が1時間で終わる、という意味ではありません。」\n[⚠️ 要注意] S130〜S133\n問題:Goldbergの提案を、個人の机上試行と「まさに同じ」としています。提案の肝である、実務で価値を生みながらデータを蓄積する点が抜けます。\n根拠:原文の提案は、従来の工学と学習を組み合わせ、商業環境で有用な仕事をするロボットから継続的にデータを集めるものです。家庭の開発試行との類似はありますが、稼働規模と実用性は異なります。前掲のScience Robotics原文。\n修正案:「僕の机の上も、AIと従来の制御を組み合わせる点では通じています。ただ、現場で役に立つ仕事をしながらデータが増える仕組みとは、まだ規模も段階も違います。」\n[⚠️ 要注意] S123、S136〜S141\n問題:「デモではほとんど言われない」は調査なしの一般化です。また人への物の渡し方を、データの壁を越えた後の課題に分離するのも不正確です。\n根拠:Generalistの公開動画には自律動作・再生速度の表示があります。人との動作の調整も、知覚、制御、学習、安全性を含む現在の研究対象です。[DeepMindの人とロボットの相互作用・安全性説明](https://deepmind.google/models/gemini-robotics/responsibly-advancing-ai-and-robotics/)もその一例です。\n修正案:「デモを見るときは、自律か遠隔操作か、速度や介入の条件が書かれているかを見ます。そしてデータ集めと並行して、人が安心して受け取れる動きを設計する仕事も必要なんです。」\n\n1. このまま公開してよいか:直してから。 体験談の章は残せますが、結論とデータの壁への接続は書き直す必要があります。\n2. 公開前に必ず直すべき文番号: S016〜S018、S034〜S038、S068〜S074、S082〜S085、S089〜S094、S099〜S109、S115〜S116、S128〜S129、S143〜S145。\n3. 結論そのものへの評価:現状では成立していない。 「見て判断する全体には来た/触れて動かす全体には来ていない」という二分は広すぎ、証拠も非対称です。次の結論なら条件つきで成立します。\n\n「僕の机では、画像に質問する入口はずっと簡単になりました。一方、物を確実に動かすには、機体や環境とのすり合わせがまだ必要でした。その負担を減らす研究は急速に進んでいます。でも、いろんな家庭で安全に家事を任せられる転換点まで来たかは、まだ判断できません。」</code></pre><p></p><h2 id=\"index_9Y98_SCr\">video_rules.md の全文</h2><pre><code class=\"language-markdown\"># video_rules.md\n\n## 動画制作ルール(唯一の正)\n\n最終更新: 2026-10-11\n状態: 試作1本目「ロボットにもChatGPTの瞬間はもう来たのか(フィジカルAIの現在地)」を作りながら決めたことをまとめた初版。まだ決まっていない項目は8節にまとめている。\n\nYouTube 用の解説動画を作るときは、まずこのファイルを読む。台本の文体は language_rules.md、記事は article_rules.md が正。動画の構成・数字の扱い・画面・音声・書き出しは本ファイルが正。\n\n---\n\n## 1. 動画の構成(解説回の型)\n\n参考にした3本の解説動画(時事ニュースをデータと体験で検証する形式)から取った型。尺は13〜14分。\n\n| # | パート | 時間の目安 | やること |\n|---|---|---|---|\n| 0 | フック | 0:00〜0:45 | 挨拶より先に、視聴者が最近見たニュースや数字から入る。世間の感覚を代弁する(「〜って感じませんか」) |\n| 1 | 自己紹介 | 〜0:55 | ロゴのテロップを出し、毎回同じ文言で名乗る(2節の文言) |\n| 2 | 今日の問い | 〜1:10 | 問いを疑問文1つで言い、問いの中の言葉をその場で定義する |\n| 3 | 結論と立場 | 〜1:30 | 先に結論を言う。誤解されやすい方向を先に否定する(「〜と言いたいわけじゃないんです」) |\n| 4 | 目次 | 〜1:50 | 「大きく3つあります」と数を言ってから並べる |\n| 5〜7 | 本編(3節) | 〜10:00 | 「1つ目、〜です」で始め、各節の最後の1文を次の節の問いにする |\n| 8 | 見立て | 〜12:30 | 「ここからが僕の一番言いたいことです」と言ってから意見を話す |\n| 9 | 問いへの回答 | 〜13:20 | 冒頭の問いに3〜4文で答え、視聴者が次にニュースを見たときに考える問いを1つ渡す |\n| 10 | アウトロ | 〜13:40 | 毎回同じ文言 |\n\nパートごとのセリフの型(穴埋め式)と、実験回の型は Docs「動画台本テンプレート|台本例:フィジカルAIの現在地」にある。\n\n---\n\n## 2. 台本のルール\n\n公開前に codex に敵対的検証(台本を論破するつもりで見落としを探してもらう)をかけたところ「このまま公開は不可」と判定された。以下はそのとき直した点から作ったルール。\n\n1. **問いの中の言葉は、冒頭で定義する。** 例: 「ChatGPTの瞬間」を定義しないまま「来た/来ていない」と言い切っていた → 「専門家でなくても、いろんな仕事にすぐ使えるようになる転換点」と冒頭で言う形に直した。\n2. **話の範囲を言う。** 例: フィジカルAIの定義に自動運転を含めながら、アームの話だけで結論を出していた → 「自動運転も入りますが、今日はアームや人型ロボットが物を扱う話に絞ります」と言う。\n3. **数字は、出典で裏が取れて、論旨に必要なものだけ使う。** 増やすほど突っ込まれる場所が増える。試作1本目で削った数字: 1700分の1(単位の違う比較)、成功率99%・約1時間(会社の自己申告で条件の説明が要る)、170万点(シミュレーターの表なのに実機の測定と誤解される)、メーカー別の内訳(なくても話が変わらない)。\n4. **推計・会社自身の発表は、そう口で言う。** 「Counterpointの推計では」「会社の発表では」。調査会社同士で数字が違うときは、どちらかに寄せず「総数も分類も違うので、単純には比べられない」と言う。\n5. **意味や単位の違う数字を並べて比べない。** 例: 「人が文章を読む時間 10万年」と「作業を記録した時間 57年」を割り算していた → 比較ごと削った。\n6. **自分の実機の話は、自分の条件の話として言う。** 「僕の機体とソフトの組み合わせでは」を付ける。机の上で起きたことを業界全体の結論に広げない。例: 計算ツールとデジタルツインの爪先の位置が30cm以上食い違った話を「データ不足」の例にしていた → 「モデルや制御が自分の機体と噛み合っているかを確かめる、すり合わせの仕事」として話す形に直した。\n7. **自分の数字は、何の数かを添える。** 例: 「650回以上」→「失敗も含めた試行回数」。\n8. **最新の発表は、公開の直前に確認する。** 例: 4月の発表を最新として話していたが、8月に次の発表(GEN-1.5)が出ていた。\n9. **「見つからない」と「していない」を分ける。** 例: 「出荷していない」ではなく「配送開始を示す公式発表は、僕が確認した範囲では見つかっていません」。\n10. **自分の活動を盛らない。** 自己紹介の文言は「僕は本業のデザイナーをしながら、ロボティクスの研究と開発をしています。自宅の机の上でロボットアームを動かして、その記録を公開しています。」に固定する。「秋田」「29年」「毎日」は入れない。\n11. **公開前に codex で敵対的検証をかける。** 依頼ファイルの型は、試作1本目の動画フォルダにある `codex_敵対的検証依頼_*.md`。台本全文に文番号(S001〜)を振り、出典の表と検証の観点(事実・論理・抜けている反論・言い方)を付けて渡す。返ってきた指摘は、一次情報で確かめてから採用する。\n\n---\n\n## 3. ナレーション(現状は仮音声)\n\nいまは仮の合成音声で作っている。本番の声は未決(8節)。\n\n- **エンジン:** Open JTalk、声は nitech_jp_atr503_m001(男声)。設定は 速さ 1.08、音の高さ -1、声質 0.55\n- **読み替え表:** 英語の名前はカタカナで読ませる(ChatGPT→チャットジーピーティー、ikpy→アイケーパイ など)。誤読した語も表に足す。これまでの例: 爪先→つめさき(「つまさき」だと足の指先に聞こえる)、値→あたい、4分の3→よんぶんのさん。新しい固有名や数字を入れたら、エンジンの解析結果(カタカナの読み)を出して目で確認する\n- **間延びの検査:** ひと息で読む区間が長いと、その区間の最後の言葉が1音0.47秒ほどに引き伸ばされる(普段は0.1秒前後)。作った音声すべてで、1音0.3秒を超える箇所を検出する。見つかった文は、読み上げ用の原稿だけに読点を足して区切る。字幕の文は変えない\n- **文のあとの間:** 通常 0.35秒、話題の区切り 0.6〜0.7秒、節の終わり 0.9秒\n\n---\n\n## 4. 画面デザイン\n\n### 4-1. 画面の枠\n\n- 1920×1080、30fps\n- 余白は左右140px。下の250pxは字幕のために空ける\n- 左上に章のタグ(章番号はオレンジ、見出しは薄い文字)、右上にロゴ(高さ34px)\n\n### 4-2. 色(記事のサムネイルと同じ色)\n\n| 用途 | 色 |\n|---|---|\n| 背景 | #F5F5F0 |\n| 文字 | #0D0D0D |\n| 薄い文字(ラベル・出典・補足) | #888888 |\n| 区切り線 | #D6D6D1 |\n| 横帯・棒の「その他」 | #D3D3CE |\n| 箱(フロー図)の背景 | #FFFFFF |\n| オレンジ(強調・主題) | #C4471F |\n| 青(対比のもう一方) | #1A6C98 |\n\n- オレンジは、1画面の中で一番伝えたい語だけに使う\n- 青とオレンジを並べるのは、2つのものを対比するときだけ(試作1本目では「見て判断する=青」「触れて動かす=オレンジ」)\n- 新しい色を足すときは、色覚の違いがあっても見分けられるかを検証してから使う(dataviz スキルの validate_palette.js。背景色 #F5F5F0 を指定する)\n\n### 4-3. 文字\n\nフォントは Noto Sans CJK JP。\n\n| 用途 | 大きさ | 太さ |\n|---|---|---|\n| 大きい数字(ヒーロー数字) | 220px(単位は90px) | 900 |\n| 章番号 | 200px(オレンジ) | 900 |\n| 見出し 大/中/小 | 84px/64px/48px | 900/900/700 |\n| 見出しの上の小見出し | 40px(薄い文字) | 700 |\n| 2列比較のラベル | 34px(薄い文字) | 700 |\n| 出典 | 24px(薄い文字) | 400 |\n\n### 4-4. 図の型\n\n内容の形に合わせて、次の型から選ぶ。\n\n| 内容 | 型 |\n|---|---|\n| 伝えたい数字が1つ | 大きい数字+上に何の数か+下に比較(「前年同期の約4倍」) |\n| 2つのものの対比 | 2列。間を細い縦線(2px)で区切り、色は文字の色だけで分ける |\n| 手順・流れ | 白い箱を矢印でつなぐ |\n| 全体のうちの割合 | 横帯(ラベルを帯の中に書く) |\n| 時期ごとの量 | 横棒(長さは実際の比率) |\n| 章の始まり | 大きい章番号+章の問い |\n| 視聴者への問い | 黒い丸ラベル(「問い 1」)+大きい見出し |\n| 並ぶ項目 | 番号付きの行を細い線で区切る |\n\n### 4-4b. 強弱のつけ方\n\n全部の場面を同じ強さにしない。1本の中で、次の3段階を使い分ける。打ち出しは1本に6〜10場面ほど、残りは説明の図にする。\n\n| 段階 | 使う場面 | 見た目 |\n|---|---|---|\n| 打ち出し | 結論、問いかけ、言い切りたい一言、章の始まり | 小さい前置き(色・44px)の下に、1〜2行の大きな文(110〜150px)。1行は10字前後まで。補足は44pxの薄い文字で1行だけ |\n| 説明 | 数字・比較・手順を見せる場面 | 4-4の図の型(見出し64〜84px) |\n| 補足 | 出典・断り書き | 24〜34pxの薄い文字 |\n\n打ち出しの型(試作1本目で使ったもの):\n\n- **打ち出し文:** 前置き「先に結論」→「見て判断する入口は簡単に/物を動かすのはまだ手間」。対比の語だけ青とオレンジにする\n- **章の扉:** 「今日の話」→「1つ目」→ 大きなキーワード(200px・オレンジ。例「データの壁」)→ 章の問いを1行\n- **点の並び:** 割合は、100個の点のうち該当する数だけ色を付けて見せる(例: 中国メーカー 97%)。「以上」「推計」は下の注に書く\n\n打ち出しにする文の選び方: 字幕をそのまま大きくするのではなく、その場面で一番残したい一言に縮める。字幕と同じ文を大きく出さない。\n\n### 4-5. やらない表現\n\n- カードの上や左に太い色の線を引く(よくある AI 生成スライドの見た目になる)\n- 影、グラデーション\n- 1画面にオレンジと青以外の色を足す\n- 意味や単位の違う数字を同じ棒や帯に並べる\n\n### 4-6. 出典\n\n数字を出す画面には、左下(字幕の上)に出典を24pxの薄い文字で必ず入れる。推計なら「推計」、会社の自己申告なら「会社の発表」と書く。模式図には「図は模式図」と入れる。\n\n---\n\n## 5. 字幕\n\n- 白文字 52px・太さ800。縁取りは #0D0D0D・11px で、角を丸くする(SVG の stroke-linejoin: round。CSS の文字の縁取りは角が尖るので使わない)\n- 読み上げの1文=字幕1枚\n- 2行まで、1行31字以内。改行は読点の位置で入れる。読点がなければ助詞の後ろで割る\n- 位置は画面の下から62px\n\n---\n\n## 6. 実機の映像とテロップ\n\n- 映像は 1920×1080 に拡大して切り抜く。字幕の後ろに黒50%の帯(高さ230px)を敷く\n- 何の映像かを左上に出す(黒78%の地に白文字34px。例「実機:mechArm 270-Pi 赤いブロックを掴んで箱に置く」)\n- 名前のテロップ: 薄い地に、ロゴ(高さ30px)・名前60px・肩書き30px(「デザイナー/ロボティクスの研究・開発」)。左の太い線は付けない\n- 編集ツールの透かし(例: clideo.com)が入った素材は、ぼかしたうえで字幕帯の下に隠す。これから撮る素材は透かしなしで書き出しておく\n- iCloud にしかない素材を取り込むと、Mac にダウンロードされる\n\n---\n\n## 7. 書き出しと保存\n\n- **作業用(YouTube に上げる方):** H.264(crf 20)、音声 AAC 192kbps、音量を -16 LUFS にそろえる\n- **確認用(チャットで送る方):** crf 29、音声 112kbps。30MB以下に収める(チャットで送れる上限)\n- **保存先:** 動画ファイル(mp4)は重く、iCloud に置くと Mac の内蔵 SSD を圧迫するので、外付け SSD(KIOXIA)に置く。軽いファイルは iCloud に置く\n - 動画ファイル(作業用・確認用の mp4): `/Volumes/KIOXIA/r2r2r-data/videos/{動画名}/`\n - 字幕の srt・codex 依頼の md: iCloud `デジタルツイン学習R2R2R/動画/{動画名}/`\n - 制作ベース: iCloud `デジタルツイン学習R2R2R/動画/_制作ベース/`(このファイルの写し、スクリプト一式、ロゴの透過 PNG)\n - 素材の実機映像も、新しく撮るものは KIOXIA に置く。iCloud にしかない素材を取り込むと Mac にダウンロードされて内蔵 SSD を使う\n - MASTER の保存先テーブルと、KIOXIA の README.md のフォルダ構成には、まだ videos/ の行がない。追記はエイイチが行う\n- **作る手順(_制作ベース のスクリプト):**\n 1. `narration.py` に台本を書く(1文ごとに、場面の名前・字幕の文・後ろの間)。読み替え表もここ\n 2. `tts2.py` で音声を作る(間延びの検査と、区切っての作り直しまで自動)\n 3. `scenes.py` に場面ごとの図を書く(4節の型と色)。実機映像は `src/` に置き、`VIDEO` に場面名と開始秒を書く\n 4. `render.py` で図・テロップ・字幕を画像にする\n 5. `assemble.py` で1文ずつ映像を作ってつなぎ、音声と合わせる(字幕の srt も出る)\n 6. 確認用に圧縮する\n\n---\n\n## 8. 未決・改良予定\n\n| 項目 | いまの状態 | 決めること |\n|---|---|---|\n| ナレーションの声 | Open JTalk の仮音声。抑揚が機械的 | 本人の声で録るか、品質の高い音声合成(VOICEVOX など。利用規約とクレジット表記の条件を確認)にするか。srt に1文ごとの時間が入っているので、それに合わせて録れば映像はそのまま使える |\n| 図解の動き | すべて静止画 | 数字のカウントアップ、矢印が伸びる、棒が伸びるなどの動きを入れるか。入れるなら、どの図の型に入れるか |\n| 背景の映像 | 実機映像は自己紹介・節2・エンディングだけ。ほかは無地の背景 | 解説中も実機映像を薄く敷くか、図解だけにするか。使う素材を撮りためるルール |\n| BGM・効果音 | なし | 使うか。使うなら、YouTube で収益化できてクレジット表記の条件を満たす音源から選ぶ。ナレーションが聞き取れる音量に下げる |\n| ジングル | 文言だけ(「Pixels to Motors。こんにちは、エイイチです。」) | 音とロゴの動きを付けるか |\n| サムネイル | まだない | 記事のサムネイルと同じ配色・線画のトーンで作るか |\n| 番組名 | 未定 | 決める |\n| 概要欄のチャプター | まだない | srt の節の開始時刻から作れる |\n\n---\n\n## 更新履歴\n\n- 2026-10-11: 4-4b「強弱のつけ方」を追加(打ち出し・説明・補足の3段階、打ち出し文・章の扉・点の並びの型)。\n- 2026-10-11: 動画ファイルの保存先を iCloud から KIOXIA の videos/ に変更(内蔵 SSD の圧迫を避けるため)。\n- 2026-10-11: 初版。試作1本目の制作で決めた構成・台本・ナレーション・画面・字幕・映像・書き出しのルールをまとめた。台本ルールは codex の敵対的検証で直した点から作った。色は記事のサムネイルに合わせた。</code></pre><h2 id=\"index_lmKEOQqP\">参照した情報</h2><ul><li><p><a href=\"https://www.youtube.com/watch?v=cDEex1_8cOo\" data-has-link=\"true\">YouTube:参考にした解説動画 1</a></p></li><li><p><a href=\"https://www.youtube.com/watch?v=bmmF1tUwnt8\" data-has-link=\"true\">YouTube:参考にした解説動画 2</a></p></li><li><p><a href=\"https://www.youtube.com/watch?v=LI-VAGxwvno\" data-has-link=\"true\">YouTube:参考にした解説動画 3</a></p></li><li><p><a href=\"https://counterpointresearch.com/en/insights/global-humanoid-robot-shipments-soar-nearly-300-percent-yoy-in-h1-2026\" data-has-link=\"true\">Counterpoint Research:Global Humanoid Robot Shipments Soar Nearly 300% YoY in H1 2026</a></p></li><li><p><a href=\"https://smartanalyticsglobal.com/global-humanoid-robot-shipments-2026-agibot-unitree/\" data-has-link=\"true\">Smart Analytics Global:2026年上半期の人型ロボット出荷</a></p></li><li><p><a href=\"https://www.1x.tech/order\" data-has-link=\"true\">1X:NEO 注文ページ</a></p></li><li><p><a href=\"https://autolab.berkeley.edu/assets/publications/media/GOFE-Can-Close-the-100000-Year-Robot-Data-Gap-Science-Robotics-Aug-2025-scirobotics.aea7390.pdf\" data-has-link=\"true\">Ken Goldberg, Science Robotics(2025年8月):Good Old-Fashioned Engineering Can Close the 100,000-Year \"Data Gap\" in Robotics</a></p></li><li><p><a href=\"https://generalistai.com/blog/gen-1.5\" data-has-link=\"true\">Generalist:GEN-1.5(2026年8月19日)</a></p></li></ul><p></p>", "toc": [{"id": "index_D_tmh6uh", "text": "参考にした3本の解説動画は、冒頭1分半で問い・結論・目次を全部出していた"}, {"id": "index_0LUCW9SF", "text": "台本テンプレートは「解説回」と「実験回」の2種類に分けた"}, {"id": "index_bTgs9pio", "text": "台本に入れた自分の数字は、台帳と公開記事で裏を取った"}, {"id": "index_vjneh5nj", "text": "動画は、Open JTalk・Playwright・ffmpeg で1文ずつ組み立てた"}, {"id": "index_YxOgXNy5", "text": "Open JTalk は、ひと息が長い文の最後を1音0.47秒に引き伸ばしていた"}, {"id": "index_0fVKV772", "text": "codex の敵対的検証で、台本の結論は「成立していない」と判定された"}, {"id": "index_t6LAP1jb", "text": "codex が新しく挙げた事実は、一次情報で確かめてから採用した"}, {"id": "index_YhZ9JBp8", "text": "根拠の弱い数字は足さずに削った"}, {"id": "index_gc6toY6y", "text": "読み上げは「爪先」を「つまさき」と読んでいた"}, {"id": "index_HCnc4FWz", "text": "カード上の太線と、角の立った字幕の縁取りをやめた"}, {"id": "index_mlpaKj8r", "text": "自己紹介から「秋田」「29年」「毎日」を外し、ロゴを差し替えた"}, {"id": "index_9szChvZM", "text": "背景と文字の色を、記事サムネイルの #F5F5F0 と #0D0D0D に合わせた"}, {"id": "index_msVyMuQC", "text": "30cmの食い違いは、アームが外れた距離ではなく、爪先の位置の決め方の違いだった"}, {"id": "index_Apnn9rBf", "text": "打ち出し・説明・補足の3段階で、画面に強弱をつけた"}, {"id": "index_sOZrkg72", "text": "動画ファイルは、iCloud ではなく KIOXIA の videos/ に置いた"}, {"id": "index_52jeog_s", "text": "量産のベースとして video_rules.md と制作スクリプト一式を残した"}, {"id": "index_3J7_uzPW", "text": "まだ決まっていない8項目は、未決のまま残した"}, {"id": "index_sWhPqJ9y", "text": "制作スクリプトの全文"}, {"id": "index_c3VMNBPy", "text": "実行ログ"}, {"id": "index_o0bVwuXE", "text": "codex への検証依頼の全文"}, {"id": "index_tELWWv5H", "text": "codex から返ってきた検証結果の全文"}, {"id": "index_9Y98_SCr", "text": "video_rules.md の全文"}, {"id": "index_lmKEOQqP", "text": "参照した情報"}]}
\ No newline at end of file
diff --git a/src/data/records/sam3-mirror-reflection-grasp-problem.json b/src/data/records/sam3-mirror-reflection-grasp-problem.json
index 9ca1952..64ae047 100644
--- a/src/data/records/sam3-mirror-reflection-grasp-problem.json
+++ b/src/data/records/sam3-mirror-reflection-grasp-problem.json
@@ -1 +1 @@
-{"day": "LOG236", "series": "ロボティクスデザイン", "published": "2026-06-14T02:26:07.000Z", "title": "ロボットの目は、鏡像も「ある」と見る ― SAM 3で器具を切り出して気づいたこと。", "slug": "sam3-mirror-reflection-grasp-problem", "cover": "/media/s-1920x1080_f24658f9-fd06-4c0b-9c47-a870ad20dea0.webp", "thumb": null, "lang": "JP", "description": "SAM 3で滅菌トレイの手術器具を1本ずつ切り出すと、ステンレスに映った像までマスクされた。認識が返すのは形で、実体の有無は別の感覚が要る——掴むロボットの設計から考える。", "updated": "2026-06-14T02:29:19.000Z", "body": "<!--TOC--><p></p><p>ロボットに物を掴ませる前に、まず「どれを掴むか」を見分けさせないといけない。その入口になるのが画像認識だ。Metaが公開しているSAM 3(テキストやクリックで対象を指定し、マスクを切り出せるセグメンテーションモデル)を触りながら、いま画像認識が実際にどこまでできて、どこでつまずくのかを確かめてみた。イチゴから始めて、手術器具を並べた滅菌キッティングの映像に進む。見えてきたのは「認識できるか」ではなく、「認識が見ているもの」と「ロボットが掴めるもの」のあいだにある、思ったより深い溝だった。</p><p></p><h2 id=\"index_DpymIEvD\">赤いイチゴだけ、と頼んでみる</h2><div class=\"video\"><video controls playsinline preload=\"metadata\" src=\"/media/s-900x674_a35ecfaf-09a5-4028-bb34-ae14382f9801.mp4\"></video></div><blockquote><p>赤く熟したイチゴだけ認識したい</p></blockquote><p>ハウス栽培のイチゴが並んだ映像で、まず素直に頼んでみた。SAM 3はテキストで概念を指定すると、画面内で一致するものを全部マスクしてくれる。<code>red strawberry</code> と入れると、たしかに赤い実が次々に切り出されていく。</p><p><strong>—</strong> ただ、どこからを「赤」とみなすかは、こちらで決められない。</p><p>実際に出てきたマスクを見ると、真っ赤な実だけでなく、7割くらい色づいた中間の実まで拾っていた。人間なら「これはまだ早い」と一目で外すグラデーションの途中を、モデルは内部の基準で勝手に線引きする。その閾値はパラメータとして表に出てこない。<code>ripe</code>(熟した)と足しても、印象は変わるが「ここから収穫」という境界を自分の手で動かせるわけではない。認識は「赤いものの集合」を返してくれる。でも「収穫していい赤」を返してくれるわけではなかった。</p><p></p><h2 id=\"index_sAiVdoyH\">認識と判断を、分ける</h2><blockquote><p>7割くらいのイチゴも認識しているっぽいね</p></blockquote><p>この「拾いすぎ」をどう扱うかで、設計の方針が分かれる。</p><p><strong>—</strong> 認識に熟度の判断まで兼ねさせると、毎回ブレる。全部取って、後段で仕分ける。</p><p>セグメンテーションに「完熟だけ」まで判定させようとすると、モデルの気分次第の境界に振り回される。そうではなく、認識には <code>strawberry</code> で全部の実を取らせてしまう。取りこぼしを最小にすることに振り切る。そのうえで熟度は別の工程に渡し、マスクごとの赤色比率(赤いピクセル数をマスク面積で割る)で閾値を切る。こうすると「どこからが完熟か」を自分の数字で握れる。認識と判断を一つの箱に詰め込まない——このあとも何度も顔を出す考え方だ。</p><p></p><h2 id=\"index_RdIst7Dl\">器具を、1本ずつ切り出す</h2><div class=\"video\"><video controls playsinline preload=\"metadata\" src=\"/media/s-1280x720_0e7779e9-0f3a-45fd-aa67-b76c819faabb.mp4\"></video></div><p>イチゴの次に、毛色の違う素材を試したくなった。病院の滅菌キッティング作業——ステンレスのトレイに手術器具を並べて準備する映像だ。</p><blockquote><p>1本ずつ分けたい</p></blockquote><p>ピック&プレースを前提にするなら、器具がひとかたまりではなく「1本=1オブジェクト」で分かれていてほしい。</p><p><strong>—</strong> 種類ごとにテキストを入れて、繋がったらクリックで確定する。</p><p>イチゴより一段難しい。器具もトレイも同じステンレスで色や明るさの差が小さく、しかもトレイは鏡面で映り込みがある。器具同士も隣接して重なる。手が被るフレームも多い。だから攻め方を素材で変える。金属器具は種類ごとに具体的な英語名で指定し、繋がってしまったらクリックやボックスで1本ずつ確定していく。曖昧な <code>instruments</code> ではなく、こう分ける。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>対象</p></th><th colspan=\"1\" rowspan=\"1\"><p>入れたテキスト</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ピンセット</p></td><td colspan=\"1\" rowspan=\"1\"><p>tweezers</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>剪刀(はさみ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>surgical scissors</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>止血鉗子</p></td><td colspan=\"1\" rowspan=\"1\"><p>hemostat</p></td></tr></tbody></table></div><p>実際にやってみると、鉗子・ピンセット・剪刀が、それぞれ別の色のマスクとして分かれて出てきた。「全部まとめて1個」ではなく、1本ずつ追える状態になる。ここまでは、想像していたより素直に動いた。途中でセッションのタイムアウトや「プロンプトを追加できなかった」というエラーも挟んだが、フレームを選び直して張り直せば動く。デモの作法の範囲だ。</p><p></p><h2 id=\"index_4IVWpGdK\">鏡に映った道具も、認識は「ある」と見る</h2><div class=\"embed\"><iframe src=\"/embeds/sam3-mirror-reflection-grasp-problem-cYEPE4i8.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p>ところが、トレイをよく見ると妙なことが起きていた。</p><blockquote><p>ステンレスのトレイに映るハサミやピンセットまでマスクしてしまう</p></blockquote><p>鏡面のトレイに映り込んだ器具の像——実体のない、ただの反射——にまで、マスクが伸びていた。</p><p><strong>—</strong> 人は無視できる。でも認識は、そこに「もう1本」を見ている。</p><p>私たちは鏡像を一瞬で「これは映り込みだ」と切り捨てる。でもセグメンテーションは、実体と反射を区別しない。器具の「形」がそこにあれば、形として拾う。問題はこれが見た目の粗ではなく、掴む側にとっての実害になることだ。マスクが「ここに器具がある」と言えば、ロボットはそこへ手を伸ばす。映り込みを掴みに行けば、当然なにも掴めない。失敗の原因は、モデルの精度が低いことではない。「物のように見える」と「掴める物である」が、認識の中で同じものとして扱われていることにある。金属の上に金属を置き、鏡面で囲うという、現場ではごく当たり前の環境が、認識にとってはいちばん厄介な舞台になる。</p><p></p><h2 id=\"index_boA6IJ4c\">認識は「形」を返す。実体があるかは、別の感覚が要る</h2><p>ここで効いてくるのが、画像認識の外側の感覚だ。</p><p>認識が教えてくれるのは「どこに、どんな形があるか」までだ。そこに質量があるか、硬いか柔らかいか、握って本当に保持できたか——それは形からはわからない。反射には形はあるが手ごたえがない。触覚なら、伸ばした指が空を切った瞬間に「ここには何もない」と即座に返ってくる。</p><p>認識が苦手なところを、触覚が埋める。</p><ul><li><p>認識:形・位置・種類を提案する。ただし実体の有無までは保証しない</p></li><li><p>触覚:接触の有無・硬さ・保持の成否を確認する</p></li><li><p>視覚が候補を出し、触覚が裏を取る。この二段構えで、はじめて「掴めた」が確定する</p></li></ul><p>これからのロボットにとって、画像認識と触覚は対立する技術ではなく、補い合う感覚になる。視覚が「たぶんここ」を出し、触覚が「本当にあった」を返す。そして、それぞれの精度が上がるほど、組み合わせでできることは足し算ではなく掛け算で増えていく。鏡像に手を伸ばさないロボットは、認識だけを磨いても作れない。逆に言えば、認識と触覚が噛み合った瞬間に、ロボットが任せられる仕事は一気に広がっていく。</p><p><strong>認識が返すのは「形」だ。そこに「実体」があるかどうかは、まだ別の感覚が確かめるしかない。</strong></p>", "toc": [{"id": "index_DpymIEvD", "text": "赤いイチゴだけ、と頼んでみる"}, {"id": "index_sAiVdoyH", "text": "認識と判断を、分ける"}, {"id": "index_RdIst7Dl", "text": "器具を、1本ずつ切り出す"}, {"id": "index_4IVWpGdK", "text": "鏡に映った道具も、認識は「ある」と見る"}, {"id": "index_boA6IJ4c", "text": "認識は「形」を返す。実体があるかは、別の感覚が要る"}]}
\ No newline at end of file
+{"day": "LOG236", "series": "画像認識・物体検出", "published": "2026-06-14T02:26:07.000Z", "title": "ロボットの目は、鏡像も「ある」と見る ― SAM 3で器具を切り出して気づいたこと。", "slug": "sam3-mirror-reflection-grasp-problem", "cover": "/media/s-1920x1080_f24658f9-fd06-4c0b-9c47-a870ad20dea0.webp", "thumb": null, "lang": "JP", "description": "SAM 3で滅菌トレイの手術器具を1本ずつ切り出すと、ステンレスに映った像までマスクされた。認識が返すのは形で、実体の有無は別の感覚が要る——掴むロボットの設計から考える。", "updated": "2026-06-14T02:29:19.000Z", "body": "<!--TOC--><p></p><p>ロボットに物を掴ませる前に、まず「どれを掴むか」を見分けさせないといけない。その入口になるのが画像認識だ。Metaが公開しているSAM 3(テキストやクリックで対象を指定し、マスクを切り出せるセグメンテーションモデル)を触りながら、いま画像認識が実際にどこまでできて、どこでつまずくのかを確かめてみた。イチゴから始めて、手術器具を並べた滅菌キッティングの映像に進む。見えてきたのは「認識できるか」ではなく、「認識が見ているもの」と「ロボットが掴めるもの」のあいだにある、思ったより深い溝だった。</p><p></p><h2 id=\"index_DpymIEvD\">赤いイチゴだけ、と頼んでみる</h2><div class=\"video\"><video controls playsinline preload=\"metadata\" src=\"/media/s-900x674_a35ecfaf-09a5-4028-bb34-ae14382f9801.mp4\"></video></div><blockquote><p>赤く熟したイチゴだけ認識したい</p></blockquote><p>ハウス栽培のイチゴが並んだ映像で、まず素直に頼んでみた。SAM 3はテキストで概念を指定すると、画面内で一致するものを全部マスクしてくれる。<code>red strawberry</code> と入れると、たしかに赤い実が次々に切り出されていく。</p><p><strong>—</strong> ただ、どこからを「赤」とみなすかは、こちらで決められない。</p><p>実際に出てきたマスクを見ると、真っ赤な実だけでなく、7割くらい色づいた中間の実まで拾っていた。人間なら「これはまだ早い」と一目で外すグラデーションの途中を、モデルは内部の基準で勝手に線引きする。その閾値はパラメータとして表に出てこない。<code>ripe</code>(熟した)と足しても、印象は変わるが「ここから収穫」という境界を自分の手で動かせるわけではない。認識は「赤いものの集合」を返してくれる。でも「収穫していい赤」を返してくれるわけではなかった。</p><p></p><h2 id=\"index_sAiVdoyH\">認識と判断を、分ける</h2><blockquote><p>7割くらいのイチゴも認識しているっぽいね</p></blockquote><p>この「拾いすぎ」をどう扱うかで、設計の方針が分かれる。</p><p><strong>—</strong> 認識に熟度の判断まで兼ねさせると、毎回ブレる。全部取って、後段で仕分ける。</p><p>セグメンテーションに「完熟だけ」まで判定させようとすると、モデルの気分次第の境界に振り回される。そうではなく、認識には <code>strawberry</code> で全部の実を取らせてしまう。取りこぼしを最小にすることに振り切る。そのうえで熟度は別の工程に渡し、マスクごとの赤色比率(赤いピクセル数をマスク面積で割る)で閾値を切る。こうすると「どこからが完熟か」を自分の数字で握れる。認識と判断を一つの箱に詰め込まない——このあとも何度も顔を出す考え方だ。</p><p></p><h2 id=\"index_RdIst7Dl\">器具を、1本ずつ切り出す</h2><div class=\"video\"><video controls playsinline preload=\"metadata\" src=\"/media/s-1280x720_0e7779e9-0f3a-45fd-aa67-b76c819faabb.mp4\"></video></div><p>イチゴの次に、毛色の違う素材を試したくなった。病院の滅菌キッティング作業——ステンレスのトレイに手術器具を並べて準備する映像だ。</p><blockquote><p>1本ずつ分けたい</p></blockquote><p>ピック&プレースを前提にするなら、器具がひとかたまりではなく「1本=1オブジェクト」で分かれていてほしい。</p><p><strong>—</strong> 種類ごとにテキストを入れて、繋がったらクリックで確定する。</p><p>イチゴより一段難しい。器具もトレイも同じステンレスで色や明るさの差が小さく、しかもトレイは鏡面で映り込みがある。器具同士も隣接して重なる。手が被るフレームも多い。だから攻め方を素材で変える。金属器具は種類ごとに具体的な英語名で指定し、繋がってしまったらクリックやボックスで1本ずつ確定していく。曖昧な <code>instruments</code> ではなく、こう分ける。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>対象</p></th><th colspan=\"1\" rowspan=\"1\"><p>入れたテキスト</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ピンセット</p></td><td colspan=\"1\" rowspan=\"1\"><p>tweezers</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>剪刀(はさみ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>surgical scissors</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>止血鉗子</p></td><td colspan=\"1\" rowspan=\"1\"><p>hemostat</p></td></tr></tbody></table></div><p>実際にやってみると、鉗子・ピンセット・剪刀が、それぞれ別の色のマスクとして分かれて出てきた。「全部まとめて1個」ではなく、1本ずつ追える状態になる。ここまでは、想像していたより素直に動いた。途中でセッションのタイムアウトや「プロンプトを追加できなかった」というエラーも挟んだが、フレームを選び直して張り直せば動く。デモの作法の範囲だ。</p><p></p><h2 id=\"index_4IVWpGdK\">鏡に映った道具も、認識は「ある」と見る</h2><div class=\"embed\"><iframe src=\"/embeds/sam3-mirror-reflection-grasp-problem-cYEPE4i8.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p>ところが、トレイをよく見ると妙なことが起きていた。</p><blockquote><p>ステンレスのトレイに映るハサミやピンセットまでマスクしてしまう</p></blockquote><p>鏡面のトレイに映り込んだ器具の像——実体のない、ただの反射——にまで、マスクが伸びていた。</p><p><strong>—</strong> 人は無視できる。でも認識は、そこに「もう1本」を見ている。</p><p>私たちは鏡像を一瞬で「これは映り込みだ」と切り捨てる。でもセグメンテーションは、実体と反射を区別しない。器具の「形」がそこにあれば、形として拾う。問題はこれが見た目の粗ではなく、掴む側にとっての実害になることだ。マスクが「ここに器具がある」と言えば、ロボットはそこへ手を伸ばす。映り込みを掴みに行けば、当然なにも掴めない。失敗の原因は、モデルの精度が低いことではない。「物のように見える」と「掴める物である」が、認識の中で同じものとして扱われていることにある。金属の上に金属を置き、鏡面で囲うという、現場ではごく当たり前の環境が、認識にとってはいちばん厄介な舞台になる。</p><p></p><h2 id=\"index_boA6IJ4c\">認識は「形」を返す。実体があるかは、別の感覚が要る</h2><p>ここで効いてくるのが、画像認識の外側の感覚だ。</p><p>認識が教えてくれるのは「どこに、どんな形があるか」までだ。そこに質量があるか、硬いか柔らかいか、握って本当に保持できたか——それは形からはわからない。反射には形はあるが手ごたえがない。触覚なら、伸ばした指が空を切った瞬間に「ここには何もない」と即座に返ってくる。</p><p>認識が苦手なところを、触覚が埋める。</p><ul><li><p>認識:形・位置・種類を提案する。ただし実体の有無までは保証しない</p></li><li><p>触覚:接触の有無・硬さ・保持の成否を確認する</p></li><li><p>視覚が候補を出し、触覚が裏を取る。この二段構えで、はじめて「掴めた」が確定する</p></li></ul><p>これからのロボットにとって、画像認識と触覚は対立する技術ではなく、補い合う感覚になる。視覚が「たぶんここ」を出し、触覚が「本当にあった」を返す。そして、それぞれの精度が上がるほど、組み合わせでできることは足し算ではなく掛け算で増えていく。鏡像に手を伸ばさないロボットは、認識だけを磨いても作れない。逆に言えば、認識と触覚が噛み合った瞬間に、ロボットが任せられる仕事は一気に広がっていく。</p><p><strong>認識が返すのは「形」だ。そこに「実体」があるかどうかは、まだ別の感覚が確かめるしかない。</strong></p>", "toc": [{"id": "index_DpymIEvD", "text": "赤いイチゴだけ、と頼んでみる"}, {"id": "index_sAiVdoyH", "text": "認識と判断を、分ける"}, {"id": "index_RdIst7Dl", "text": "器具を、1本ずつ切り出す"}, {"id": "index_4IVWpGdK", "text": "鏡に映った道具も、認識は「ある」と見る"}, {"id": "index_boA6IJ4c", "text": "認識は「形」を返す。実体があるかは、別の感覚が要る"}]}
\ No newline at end of file
diff --git a/src/data/records/speak-to-relearn-robotics.json b/src/data/records/speak-to-relearn-robotics.json
index 777a3ea..0cd6035 100644
--- a/src/data/records/speak-to-relearn-robotics.json
+++ b/src/data/records/speak-to-relearn-robotics.json
@@ -1 +1 @@
-{"day": "LOG328", "series": "ロボティクスデザイン", "published": "2026-10-11T07:39:41.000Z", "title": "ロボティクスを声に出して覚え直す ― 開発ログから用語164語と判断の型104枚の練習ページを作った", "slug": "speak-to-relearn-robotics", "cover": null, "thumb": "/thumbs/r-speak-to-relearn-robotics.html", "lang": "JP", "description": "現場でよく使うロボティクスの用語164語と、開発ログの実話から作った判断の型104枚を、押している間だけ聞いて答え合わせする練習ページにした。1周182回は平均92%、聞き違いを補正すると95%。", "updated": "2026-10-11T07:40:04.000Z", "body": "<!--TOC--><p></p><p>ロボットアームを動かしながら300日以上、開発ログを書いてきました。読み返すと、その日に分かったことは書いてあるのに、言葉として口から出てこないものが多くあります。順運動学、ホモグラフィ、視覚サーボ。使っているのに、人に説明しようとすると詰まります。</p><p>そこで、ロボティクスの言葉と判断を「声に出して」覚え直すための練習ページを作りました。スマホやパソコンでカードを見ながら、ボタンを押している間だけ話します。指を離すと、話した言葉が文字になり、カードの文とどこが違ったかが赤と青で出ます。</p><p>形は、仕事で使っている台本の読み合わせページをそのまま借りました。会議の前に、台本の文を一文ずつ声に出して、言い慣れていない言い回しを直すためのページです。読むだけより、声に出して文字で確かめる方が身につくことは、そちらで分かっていました。</p><p>中身は3段階です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>段階</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>カードに出るもの</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>声に出すもの</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>採点</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1 単語</p></td><td colspan=\"1\" rowspan=\"1\"><p>用語(例:リンク)</p></td><td colspan=\"1\" rowspan=\"1\"><p>用語と短い意味を一続きで</p></td><td colspan=\"1\" rowspan=\"1\"><p>カードの文と1文字ずつ比べる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2 説明</p></td><td colspan=\"1\" rowspan=\"1\"><p>用語と短い意味</p></td><td colspan=\"1\" rowspan=\"1\"><p>詳しい説明(2〜3文)</p></td><td colspan=\"1\" rowspan=\"1\"><p>同上</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3 判断の型</p></td><td colspan=\"1\" rowspan=\"1\"><p>困る場面の問い</p></td><td colspan=\"1\" rowspan=\"1\"><p>教訓の一言(自分の言葉でよい)</p></td><td colspan=\"1\" rowspan=\"1\"><p>キーワードが言えたか</p></td></tr></tbody></table></div><p>単語は、現場でよく使うロボティクスの用語を164語選びました。判断の型は、開発ログに書いてきた実話から104枚にしました。</p><p>この記事は、そのページを作った過程の記録です。最初の版は、開発ログから抜き出した事実を424枚のカードに並べただけのもので、声に出して覚えるには向いていませんでした。そこから、用語から始める3段階に作り直し、練習の記録を保存して集計する仕組みを足しました。最後に、単語をひととおり(182回)練習した記録を集計して、採点の作りを直すところまで進めました。</p><p>練習ページは <a href=\"https://pixelstomotors.github.io/robotics-review/\" data-has-link=\"true\">https://pixelstomotors.github.io/robotics-review/</a> で公開しています。</p><p></p><h2 id=\"index_rNSeBrBJ\">仕事で使っている台本の読み合わせページを、ロボティクスの復習用に作り変えた</h2><p>元にしたページは、横にスワイプする黒いカードに台本の文が1つずつ出て、その下に「押している間だけ聞く」の大きなボタンがある作りです。ボタンを押して話し、指を離すと、ブラウザの音声認識が話した言葉を文字にし、台本の文と比べて違うところに色を付けます。</p><p>このページの作りは、ロボティクスの言葉の練習にそのまま使えると考えました。台本の文を、ロボティクスの用語や説明に入れ替えればよいからです。データ(カードの中身)は、自分の Mac と iPhone の両方から触れる iCloud Drive に置きます。</p><p>置き場所は、iCloud Drive の <code>PixelstoMotors/ロボティクス復習/</code> です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>ファイル</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>中身</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>robotics_review.json</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>カードの中身(用語・判断の型)と、聞き違いの一覧。これがデータの正</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>_tools/template.html</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>ページの形。データは書き出すときに埋め込む</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>_tools/build_review.py</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>JSON とテンプレートからページ(<code>ロボティクス復習.html</code>)を書き出す</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>_tools/practice_review.py</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>練習記録を集計し、集計 JSON と振り返りを作る</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>練習記録/</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>ページの「結果を保存」で入る記録(1回の保存=1ファイル)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>_archive/実機の知見_旧424項目.json</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>作り直す前のカード(調べるための資料として残す)</p></td></tr></tbody></table></div><p></p><h2 id=\"index_Ef3BykTT\">開発ログ194本と調査メモから、9つのエージェントで復習項目を549件抜き出した</h2><p>カードの元にしたのは、GitHub の記事リポジトリ(<code>PixelstoMotors/r2r2r-content</code>)にある LOG102〜LOG326 の記事194本(英語版を除く)と、論文や調査をまとめたメモ5本、プロジェクトの原則・運動学・動的追従の設計をまとめた文書3本です。</p><p>記事はコードと実機ログを全量で載せているので、合わせて約6.7MBあります。1つの作業で全部を読むのは無理なので、記事を8つの範囲に分け、調査メモと原則の文書で1つ、合わせて9つのエージェント(Claude が別に立ち上げる作業役)に並行して読ませました。</p><p>それぞれに渡した指示は同じです。ロボティクスの知識だけを拾うこと(SNS や記事の書き方の話は拾わない)、記事に書いてある事実だけを書くこと、カテゴリを9つのどれかにすること、項目の種類を「要点」「用語」「問い」のどれかにすること。指示文の全文は記事の最後に載せます。</p><p>9つの範囲から合わせて549件が出ました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>カテゴリ</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>件数</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>実機・サーボ制御</p></td><td colspan=\"1\" rowspan=\"1\"><p>82</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ロボット学習</p></td><td colspan=\"1\" rowspan=\"1\"><p>79</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>画像認識・カメラ</p></td><td colspan=\"1\" rowspan=\"1\"><p>76</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>評価・データ管理</p></td><td colspan=\"1\" rowspan=\"1\"><p>65</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>設計原則・進め方</p></td><td colspan=\"1\" rowspan=\"1\"><p>60</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>運動学・座標系</p></td><td colspan=\"1\" rowspan=\"1\"><p>55</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>視覚サーボ・Pick&Place・受け渡し</p></td><td colspan=\"1\" rowspan=\"1\"><p>52</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>社会実装・現場</p></td><td colspan=\"1\" rowspan=\"1\"><p>47</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>シミュレーション・デジタルツイン</p></td><td colspan=\"1\" rowspan=\"1\"><p>33</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>合計</p></td><td colspan=\"1\" rowspan=\"1\"><p>549</p></td></tr></tbody></table></div><p>範囲を分けて抜き出したので、同じ内容が別の日付で何度も出ます。そこで、カテゴリを3つずつ受け持つエージェントを3つ立てて、重複をまとめ、古い記事と新しい記事で結論が違うものは新しい方に合わせ、カテゴリの中に小見出しを付けさせました。549件が424件になりました。</p><p>食い違いの例を2つ挙げます。肩の関節(j1)の上限は、LOG231 では「90.87〜91.66°の間」と書いていましたが、LOG246 で刻んで測り直し「90.0°までは届き、90.5°以上を含む指令は丸ごと無視される」と分かっています。カードは LOG246 に合わせました。シミュレーションと実機のどちらを基準にするかも、LOG169 の「Unity を正」から LOG250 の「実機が土台で、ツインが合わせる側」に変わっていたので、後者にしました。</p><p></p><h2 id=\"index_nY3kXcq7\">424項目を出典の記事と照合し、21件を直した</h2><p>抜き出した文が正しいかは、抜き出した本人ではなく、別のエージェントに確かめさせました。同じ書き手が確かめると、同じところを見落とすからです。</p><p>まず424件から90件(核心として印を付けた55件と、無作為の35件)を照合させたところ、7件に問題がありました。8%の割合なので、残りの334件も4つに分けて全部照合しました。そちらで14件が見つかり、合わせて21件を直しました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>問題の種類</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>例</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>数値の取り違え</p></td><td colspan=\"1\" rowspan=\"1\"><p>3回連続の成功の誤差「2.3mm」は3回のうちの最良値で、ほかの2回は約4.6mmだった</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>原因の書き過ぎ</p></td><td colspan=\"1\" rowspan=\"1\"><p>Z方向の振動の原因を「符号の反転」だけにしていたが、実際は符号を直しても収束せず、常駐デーモンに替えて初めて 1.7秒・2.9mm に収まった</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>計画を実施済みのように書いていた</p></td><td colspan=\"1\" rowspan=\"1\"><p>置いた結果を iPhone で真上から撮って判定するのは、LOG125 では計画で、まだやっていなかった</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>出典の不足</p></td><td colspan=\"1\" rowspan=\"1\"><p>「SDK の定義は +120°」は LOG253 の記述で、出典に LOG253 が入っていなかった</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>一部の条件の結果を一般化していた</p></td><td colspan=\"1\" rowspan=\"1\"><p>実データと生成データを混ぜて成功率が上がった結果は、マグをコーヒーメーカーに載せる1つの作業でのものだった</p></td></tr></tbody></table></div><p></p><h2 id=\"index_6jUeiDe0\">最初の版は「読むだけ」のページで、核心のカードが多すぎて見づらかった</h2><p>最初に作ったのは、JavaScript を使わない1枚の HTML です。iPhone の「ファイル」アプリで iCloud の HTML を開くとプレビューになり、そこでは JavaScript が動かないので、動かなくても読める作りにしました。カテゴリを開くと、横にスワイプするカード、用語をタップして意味を出すリスト、問いをタップして答えを出すリストが並びます。</p><p>見てもらうと、2つ指摘がありました(引用の〔 〕は、仕事の案件名を伏せて言い換えた箇所です)。</p><blockquote><p>核心のところが〔仕事の台本ページ〕の時より多いので、黄色の薄いレイヤーが上にかかっているのが視認性を悪くしている。</p></blockquote><p>元のページでは、台本の中で「ここで止まって言い切る1文」だけを金色の枠と茶色の背景で目立たせていました。台本では1つの議題に1文ほどなので目立つ意味がありますが、こちらでは核心が全体の13%(55枚)あり、茶色の背景が続くと読みにくくなっていました。核心の印は、黒いカードの左上に小さな金色の「核心」の字を置くだけにしました。</p><blockquote><p>あと〔仕事の台本ページ〕と同じように声に出して確認した方が覚えるので、同じように作り変えてください(細かい注意書きは読まない)</p></blockquote><p>もう1つは、読むだけでなく、元のページと同じように声に出して確かめたいという指摘です。説明の注意書きも読まないので外してほしいとのことでした。</p><p></p><h2 id=\"index_atypUTBf\">声で確認するには https が要るので、GitHub Pages に置いた</h2><p>ブラウザで声を文字にする仕組み(Web Speech API の SpeechRecognition)は、https で開いたページでしか使えません。iCloud の HTML をプレビューで開く形では動きません。元の読み合わせページも、この理由で GitHub Pages に置いていました。</p><p>そこで、新しく公開リポジトリ <code>PixelstoMotors/robotics-review</code> を作り、Settings → Pages で Branch を <code>main</code>、フォルダを <code>/ (root)</code> にして公開しました。リポジトリの作成と Pages の設定は、こちらの作業環境から操作できなかったので、ブラウザで手で行いました。中身(<code>index.html</code>)は、iCloud で書き出したページをそのまま置いています。</p><p>公開ページは URL を知っていれば誰でも見られますが、<code><meta name=\"robots\" content=\"noindex, nofollow\"></code> を入れているので検索には出ません。練習の記録はページの外には送らず、保存するときも自分の iCloud にだけ書きます。</p><p>iPhone では、Safari で公開ページを開き、共有ボタンから「ホーム画面に追加」しておくと、次からはアイコンから開けます。</p><p></p><h2 id=\"index_Lghl9cGq\">押している間だけ聞き、カードの文と1文字ずつ比べる</h2><p>声で確かめる部分は、元のページの作りを踏襲しました。</p><ol><li><p>ボタンを押した瞬間に音声認識を始める(<code>lang = 'ja-JP'</code>、話している途中の文字も出す設定)</p></li><li><p>指を離してから 0.25秒待って止める(言い終わりを取りこぼさないため)</p></li><li><p>話した言葉とカードの文を、句読点と空白を除き、カタカナとひらがな、全角と半角、大文字と小文字を同じとみなしてから、1文字ずつ並べて比べる</p></li><li><p>両方に共通して並ぶ文字が一番多くなる並べ方(最長共通部分列、LCS)を求め、カードにあって話さなかった文字を赤、カードになくて話した文字を青で出す</p></li><li><p>一致は「共通の文字数 ÷ 長い方の文字数」で出す</p></li></ol><p>PC ではスペースキーを押している間も聞き、←→キーでカードを移ります。iPhone は聞き始めと終わりに本体が音を鳴らすので、PC でも同じ順に音を鳴らします。</p><p>カテゴリを選ぶプルダウンの矢印は、ブラウザ標準の矢印が端末によって崩れていたので、CSS で描いた矢印(線の太さ 2.75px)に替えました。</p><p></p><h2 id=\"index_e1cF0_A9\">いきなり難しい説明が並んでいたので、用語164語から始める3段階にした</h2><p>声に出せるようになったページを開いてもらうと、こう言われました。</p><blockquote><p>いきなり難易度が高い説明ばかり並んでいるので、もっとロボティクスで実用性の高い言葉を選んでまずは単語の意味から習得して、その後に詳しい説明が続くように作り変えてください。</p></blockquote><p>424枚は開発ログから抜き出したものなので、「FK テーブルの最近傍が低い姿勢を選んでいた」のように、そのプロジェクトの文脈を知らないと読めない文が並んでいました。基礎の言葉を先に固めないと、こうした文は覚えられません。</p><p>そこで、ロボティクスの実務や研究の会話でよく出る用語を、9つのカテゴリで164語選びました。各カテゴリの中は、まったくの初心者でも分かる言葉から順に並べています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>カテゴリ</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>語数</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>最初の数語</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>運動学・座標系</p></td><td colspan=\"1\" rowspan=\"1\"><p>18</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節、リンク、自由度、回転関節と直動関節、エンドエフェクタ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>実機・サーボ制御</p></td><td colspan=\"1\" rowspan=\"1\"><p>17</p></td><td colspan=\"1\" rowspan=\"1\"><p>アクチュエータ、サーボモーター、減速機、エンコーダ、トルク</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>シミュレーション・デジタルツイン</p></td><td colspan=\"1\" rowspan=\"1\"><p>19</p></td><td colspan=\"1\" rowspan=\"1\"><p>シミュレーション、デジタルツイン、物理エンジン、タイムステップ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>画像認識・カメラ</p></td><td colspan=\"1\" rowspan=\"1\"><p>19</p></td><td colspan=\"1\" rowspan=\"1\"><p>画素、色空間、二値化、モルフォロジー演算、輪郭抽出</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>視覚サーボ・Pick&Place・受け渡し</p></td><td colspan=\"1\" rowspan=\"1\"><p>18</p></td><td colspan=\"1\" rowspan=\"1\"><p>経由点、経路と軌道、補間、速度プロファイル</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ロボット学習</p></td><td colspan=\"1\" rowspan=\"1\"><p>18</p></td><td colspan=\"1\" rowspan=\"1\"><p>学習、推論、観測と行動、方策、エピソード</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>評価・データ管理</p></td><td colspan=\"1\" rowspan=\"1\"><p>20</p></td><td colspan=\"1\" rowspan=\"1\"><p>試行、成功率、成功条件、ロギング、データセット</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>設計原則・進め方</p></td><td colspan=\"1\" rowspan=\"1\"><p>18</p></td><td colspan=\"1\" rowspan=\"1\"><p>反復開発、仮説検証、切り分け、ボトルネック</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>社会実装・現場</p></td><td colspan=\"1\" rowspan=\"1\"><p>17</p></td><td colspan=\"1\" rowspan=\"1\"><p>産業用ロボット、協働ロボット、サービスロボット、AGV、AMR</p></td></tr></tbody></table></div><p>1語ごとに、声に出す短い意味(20〜45字)と、詳しい説明(80〜160字)を付けました。詳しい説明には、そのプロジェクトの実機で起きた例を1つ入れたものもあります(例:可搬重量の説明に「mechArm 270 の常用ペイロードは250gで、約600gの中身入りコーヒーカップは運べない」)。実機の例を書くときは、記事を検索して書いてある事実だけを書き、出典の DAY を付けました。各カテゴリで最初に覚える4〜6語、合わせて53語に「まず覚える」の印を付けています。</p><p>この164語も、作ったのとは別のエージェントに照合させました。教科書の一般的な定義と、実機の例の出典の両方を確かめて、12件を直しています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>直したカード</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>何が違っていたか</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>座標系</p></td><td colspan=\"1\" rowspan=\"1\"><p>成功率が0になったのは実機ではなく Unity のツインの上だった</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>順運動学</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG192 では FK テーブルを逆向き(目標位置から関節角を引く)に使っていた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>特異点</p></td><td colspan=\"1\" rowspan=\"1\"><p>「特定の向きに動かせなくなる」は姿勢の向きと紛らわしいので「特定の方向へ動けなくなる」に</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>減速機</p></td><td colspan=\"1\" rowspan=\"1\"><p>産業用アームは根元に RV 減速機、手首に波動歯車減速機が多い。片方だけ書いていた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>PID制御</p></td><td colspan=\"1\" rowspan=\"1\"><p>サーボの中で PID が回っているかは公式資料で確認できていない(LOG260)。記述を外した</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>力覚センサー</p></td><td colspan=\"1\" rowspan=\"1\"><p>電流で接触を検出「できる」が正しく、実際に使ってはいなかった</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ハンドアイ校正</p></td><td colspan=\"1\" rowspan=\"1\"><p>カメラを外に固定する場合(mechArm がこれ)の説明が抜けていた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>推論</p></td><td colspan=\"1\" rowspan=\"1\"><p>97ms に GPU の型番(A40)を添えた(速度は型番と組で書く決まり)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>系統誤差</p></td><td colspan=\"1\" rowspan=\"1\"><p>約58mm のずれは高さではなく、マット上の奥行き方向だった</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>統計的なぶれ</p></td><td colspan=\"1\" rowspan=\"1\"><p>信頼区間に信頼度(95%)が書かれていなかった</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ドライラン</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG256 では、実機を通す前に止めてデータの流れを先に整理していた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>安全規格(ISO 10218)</p></td><td colspan=\"1\" rowspan=\"1\"><p>2025年の改訂で、協働運転の要件(旧 ISO/TS 15066)が本体に取り込まれた</p></td></tr></tbody></table></div><p>ページは、上に「1 単語」「2 説明」「3」の切り替えを置きました。カテゴリの選択と、「見て言う/見ないで言う」「全部/苦手だけ」の切り替えは各段階で共通です。「見ないで言う」では、用語だけを見て意味を言い、話し終えると答えが出ます。</p><p></p><h2 id=\"index_q7iw4fXg\">「リンクは、関節と関節の間をつなぐ硬い骨組みの部分」と用語から続けて言う</h2><p>単語の練習で「リンク、関節と関節の間をつなぐ、固い骨組みの部分」と話すと、一致が 82% になりました。カードの文は意味だけなので、自然に言った「リンク」が余分(青)として減点されていたためです。</p><blockquote><p>内容だけではなく、タイトルの言う前提にした方が良くない?どっちがより良くなる印象ですか?</p></blockquote><p>用語と意味を一続きで言う方が良いと答えました。意味だけを言うと意味の文は覚えますが、「この言葉はこの意味」という結び付きが残りにくくなります。実際の会話では用語の方を口に出して使います。</p><p>採点は次のように変えました。</p><ul><li><p>比べる文を「用語+は、+意味」にする。「は」は付けても付けなくてもよい</p></li><li><p>「順運動学(FK)」のようにカッコが付いた用語は、「順運動学」「FK」「順運動学FK」のどれで言ってもよい</p></li><li><p>言い方の候補すべてと比べ、一番一致が高いものを採る</p></li></ul><p>カードに出す文字は変えていません。用語は大きく、意味はその下に出ます。</p><p></p><h2 id=\"index__ZgAWoKQ\">実機の知見424項目は事実の羅列で、声に出して覚える意味がなかった</h2><p>3つ目の段階には、最初の424枚をそのまま置いていました。開いてもらうと、こう言われました。</p><blockquote><p>実機の知見だけど、書いてあることは正しいかもだけど、これを見て読んで覚えるとか無理じゃない?というか意味なさそう。もっと違う知見の書き方があるのでは?</p></blockquote><p>たとえば「j0=ベース回転、j1=肩、j2=肘、j3=腕の捻り、j4=手首の曲げ、j5=グリッパーの捻り」や、「Unity X+ は画面左、Z+ は手前、Y+ は上」です。どちらも正しいのですが、必要なときに調べれば済む資料で、声に出して丸暗記しても使える力にはなりません。</p><p>覚える価値があるのは、事実そのものより、そこから出てきた判断の型です。「こういう場面では、まずこれを疑う/こうする」という形にすると、別のロボットや別の現場でもそのまま使えます。</p><p></p><h2 id=\"index_Ibt2SwNb\">判断の型104枚:場面を見て、教訓をキーワードで言えたら合格</h2><p>424項目を、次の形のカード104枚に作り直しました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>欄</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>例</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>場面(問い)</p></td><td colspan=\"1\" rowspan=\"1\"><p>人の感覚・ロボットのコード・シミュで「X」の向きが違い、指示がすれ違う。どうする?</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>教訓(声に出す一言)</p></td><td colspan=\"1\" rowspan=\"1\"><p>座標系を1つに決めて、全部をそこにそろえる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>キーワード</p></td><td colspan=\"1\" rowspan=\"1\"><p>「座標系/座標」「1つ/ひとつ/統一」「そろえる/揃える/合わせる」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>実例(読むだけ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>mechArm では、人の直感(マット上で X=左右)、ロボット座標(X=前後・Y=左右・Z=高さ)、Unity(X=左右・Y=高さ・Z=前方)で X の意味が3通りに分かれ、会話が何度も噛み合わなかった。Unity 座標系に全部統一すると、Claude Code への指示も一貫した用語で書けるようになった(LOG149・LOG158・LOG160)</p></td></tr></tbody></table></div><p>同じ教訓が別の日付で何度も出ていたもの(「測ってから考える」「補正でごまかさない」など)は1枚にまとめました。関節の割り当てのような資料は練習から外し、<code>_archive/</code> に残しています。ROS と Unity の軸の違いのように、資料から教訓が引き出せるものは、「ツールをまたぐときは、右手系か左手系か・どの軸が上かを最初に確かめる」のような教訓にしました。</p><p>採点は、一字一句の一致ではなく、キーワードの組ごとに1語でも言えていれば合格、という形にしました。「座標系をひとつに決めて、全部合わせる」と自分の言葉で言っても、3つの組がそろっていれば100%です。結果には、言えたキーワードと言えなかったキーワード(赤)を出します。</p><p>104枚も別のエージェントに照合させ、40枚を直しました。多かったのは次の3種類です。</p><ul><li><p>実例の誇張や、計画を実施済みのように書いた箇所(DAY の記事と照合して直した)</p></li><li><p>教訓を一字一句そのまま言っても不合格になるキーワード(例:教訓は「追いかけず」なのに、キーワードが「追いかけない」だけだった)</p></li><li><p>どんな文にも含まれる1文字のキーワード(「さ」「め」など)</p></li></ul><p>直したあと、全104枚について「教訓をそのまま言えば全部のキーワードの組に当たる」ことを、プログラムで確かめています。</p><p></p><h2 id=\"index_J89hmYda\">練習の記録は練習記録フォルダに保存し、集計してページに戻す</h2><blockquote><p>読み上げしたデータを記録できるようにして、データを保管して、習得内容を改善していくようにしてください(〔仕事の台本ページ〕のもそのようになっています。機能を踏襲してください)</p></blockquote><p>元のページと同じく、練習の結果をファイルに保存し、集計して、カードの直しにつなげる流れを作りました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>端末</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>保存のしかた</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Mac(Chrome)</p></td><td colspan=\"1\" rowspan=\"1\"><p>最初の1回だけ <code>練習記録</code> フォルダを選ぶ。次からは「結果を保存」を押すだけで直接書き込む(File System Access API)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>iPhone</p></td><td colspan=\"1\" rowspan=\"1\"><p>「結果を保存」→「開く」で、ショートカット「ロボ見直しを保存」が <code>練習記録</code> に入れる(後の節で書く)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>それ以外</p></td><td colspan=\"1\" rowspan=\"1\"><p>ダウンロードに入る</p></td></tr></tbody></table></div><p>保存するファイルは Markdown で、人が読める部分(カードの文・話した言葉・一致・秒数)と、集計用の JSON の両方を入れています。保存せずにページを閉じようとすると止め、未保存の回数をボタンに出します。</p><p>集計は <code>practice_review.py</code> が行います。</p><ul><li><p><code>練習記録_集計.json</code>:カードと段階ごとの最新の一致、前回の一致、回数、最高の一致</p></li><li><p><code>練習記録_振り返り.md</code>:苦手(最新の一致が80%未満)、身についた(直近2回とも90%以上)、3回続けて80%に届かないカード、よく抜ける言葉、伸びたカード、まだ言っていない「まず覚える」語</p></li></ul><p><code>build_review.py</code> でページを書き出すときに集計 JSON を埋め込むので、iPhone で練習した結果が Mac で開いても「前回」に出ます。「苦手だけ」の切り替えも、この前回の一致で絞ります。</p><p></p><h2 id=\"index_ZzZgsbhc\">単語ひととおり182回の記録:平均92%、低い点のほとんどは聞き違いだった</h2><p>単語の「見て言う」を164語ひととおり練習した記録を集計しました。15:02〜16:13 の間に、9ファイル・182回です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>指標</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>値</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>練習した回数</p></td><td colspan=\"1\" rowspan=\"1\"><p>182回</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>一致の平均(記録したときの採点)</p></td><td colspan=\"1\" rowspan=\"1\"><p>91.9%</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>90%以上</p></td><td colspan=\"1\" rowspan=\"1\"><p>137回</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>80%未満</p></td><td colspan=\"1\" rowspan=\"1\"><p>15回</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1回の平均の秒数</p></td><td colspan=\"1\" rowspan=\"1\"><p>8.6秒</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>まだ言っていない「まず覚える」語</p></td><td colspan=\"1\" rowspan=\"1\"><p>2語(座標系・再現性)</p></td></tr></tbody></table></div><p>80%未満の15回を見ると、言い間違いはほとんどありませんでした。低い点の原因は、音声認識が同じ読みの別の言葉を文字にしていたことです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>カード</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>話した言葉(文字起こし)</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>記録時の一致</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ピックアンドプレース</p></td><td colspan=\"1\" rowspan=\"1\"><p>ビッグ&プレイス 物を掴んで持ち上げ 別の場所へ運んでおく 一連の作業</p></td><td colspan=\"1\" rowspan=\"1\"><p>74%</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Gazebo</p></td><td colspan=\"1\" rowspan=\"1\"><p>ガズボン ロスト 組み合わせて使われてきた無料のロボット用 シミュレーター</p></td><td colspan=\"1\" rowspan=\"1\"><p>75%</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>関節補間と直線補間</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節保管と直接 保管 関節の角度でつなぐか 手先の直線でつなぐかという保管の違い</p></td><td colspan=\"1\" rowspan=\"1\"><p>81%</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Isaac Sim</p></td><td colspan=\"1\" rowspan=\"1\"><p>イザックシム NVIDIA が提供する GPU で動く 高精細なロボットというシュミレーター</p></td><td colspan=\"1\" rowspan=\"1\"><p>73%</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>アイインハンドとアイトゥハンド</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイ イン ハンド と I to Hand カメラを手先につけるか外に固定するかという2つの配置</p></td><td colspan=\"1\" rowspan=\"1\"><p>78%</p></td></tr></tbody></table></div><p>「補間」が「保管」に、「校正」が「構成」に、「ROS」が「ロス」に、「試行」が「思考」になっています。話した本人は正しく言えているので、これを言い間違いとして数えると、練習の記録が実際の覚え具合を表さなくなります。</p><p></p><h2 id=\"index_Nb1eeiph\">聞き違いの一覧を足して採点し直したら、平均95%になった</h2><p>同じ読みの聞き違いや書き分けを、データの <code>homophones</code> に組で持たせました。比べる前に、話した言葉の中の聞き違いを、カードの書き方にそろえます。たとえば <code>[\"補間\", \"保管\"]</code> の組は、カードに「補間」があるときだけ、話した言葉の「保管」を「補間」に置き換えます。カードに「補間」がなければ何もしません。</p><p>そのほかに、次の3つをそろえるようにしました。</p><ul><li><p>話した言葉の空白(「アイ イン ハンド」→「アイインハンド」)</p></li><li><p>英字の大文字と小文字(「I to Hand」→「i to hand」で一覧と照合)</p></li><li><p>カードに長音がない語の長音(「シミュレーター」→ カードが「シミュレータ」なら「シミュレータ」)</p></li></ul><p>一覧は 56 組になりました(全量は記事の後半に載せます)。集計スクリプトにも同じ比べ方を入れ、過去の記録も今の一覧で採点し直すようにしたので、一覧を足すたびに過去の点も直ります。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>指標</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>記録時</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>聞き違いの補正後</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>一致の平均</p></td><td colspan=\"1\" rowspan=\"1\"><p>91.9%</p></td><td colspan=\"1\" rowspan=\"1\"><p>95.3%</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>90%以上</p></td><td colspan=\"1\" rowspan=\"1\"><p>137回</p></td><td colspan=\"1\" rowspan=\"1\"><p>168回</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>採点が変わった回</p></td><td colspan=\"1\" rowspan=\"1\"><p>―</p></td><td colspan=\"1\" rowspan=\"1\"><p>58回</p></td></tr></tbody></table></div><p>補正後もまだ低いのは、「回転関節」が「に点 関節」になった回のように、一覧に入れると別の文まで誤って置き換えかねない聞き違いです。これは無理に足さず、そのままにしています。</p><div class=\"embed\"><iframe src=\"/embeds/speak-to-relearn-robotics-O7uyYP36.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p></p><h2 id=\"index_K71F5Phd\">カードを切り替えた直後に話すと、前のカードで採点されていた</h2><p>記録の中に、一致が 12% と 21% の回が1回ずつありました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>記録上のカード</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>話した言葉</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ベースライン</p></td><td colspan=\"1\" rowspan=\"1\"><p>再現性 同じ手順と条件で同じ結果が もう一度 得られること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>カメラ校正</p></td><td colspan=\"1\" rowspan=\"1\"><p>内部パラメータ 焦点距離や レンズの歪み など カメラ 自体が持つ性質の値</p></td></tr></tbody></table></div><p>話した言葉は、それぞれ隣のカード(再現性・内部パラメータ)の意味で、内容は合っています。カードを切り替えた直後に話し始めたため、ページが前のカードを対象にしたまま採点していました。</p><p>ページは、横スクロールが止まってから 0.08秒後に「今どのカードか」を数え直していました。カードを切り替えるスクロールが止まりきる前に話し始めると、数え直す前のカードが対象になります。ボタンを押した瞬間にその場で数え直すように直しました。</p><p>集計スクリプトには、点が 50% 未満で、話した言葉が別の単語カードの意味と 70% 以上一致する回を「別のカードの文を読んでいた記録」として外す処理を入れました。この2回は集計から外しています。「再現性」が「まだ言っていない語」に残っているのはこのためで、実際には言えています。</p><p></p><h2 id=\"index_bfqmDzCf\">iPhone の「ファイル」アプリのプレビューでは JavaScript が動かない</h2><p>iPhone で iCloud の <code>ロボティクス復習.html</code> を「ファイル」アプリから開くと、段階の切り替えボタンだけが出て、カテゴリもカードも空になりました。「ファイル」アプリのプレビューでは JavaScript が動かないためです。ページが壊れているわけではありません。</p><p>iCloud のファイルをプレビューで開いたときだけ、上に「Safari で開く → pixelstomotors.github.io/robotics-review」のリンクを出すようにしました。リンクは最初から HTML に書いてあり、JavaScript が動いたときに消します。JavaScript が動かないプレビューでは消えずに残ります。</p><p></p><h2 id=\"index_qt8YkBwG\">iPhone からの保存は、ショートカット「ロボ見直しを保存」で練習記録に直接入れる</h2><blockquote><p>iPhoneでデータを保存するとき一発で練習記録に保存できませんか?</p></blockquote><p>iPhone の Safari では、Web ページから決まったフォルダへ、押すだけで保存することはできません。Mac の Chrome で使える、フォルダを選んで直接書き込む仕組み(File System Access API)が、iPhone の Safari にはないためです。</p><p>元の読み合わせページは、iPhone の Safari のダウンロード先(設定 → アプリ → Safari → ダウンロード)を、そのページの練習記録フォルダにしてあります。ただ、ダウンロード先は1か所しか決められず、すでに元のページのフォルダになっています。同じやり方にすると、ロボティクスの記録が元のページのフォルダに入ってしまいます。</p><p>そこで、iPhone の「ショートカット」アプリを使いました。ページは「結果を保存」を押すと、記録をクリップボードに入れてから <code>shortcuts://run-shortcut?name=ロボ見直しを保存&input=clipboard</code> を開きます。ショートカットは、クリップボードの中身を <code>練習記録</code> フォルダに保存します。</p><p>ショートカットの作り方です。</p><ol><li><p>「ショートカット」アプリで + を押し、名前を「ロボ見直しを保存」にする(ページはこの名前で呼び出す)</p></li><li><p>アクション「クリップボードを取得」を追加する</p></li><li><p>アクション「ファイルを保存」を追加する。「クリップボード を Shortcuts に保存」と出る</p></li><li><p>「保存先を尋ねる」をオフにする</p></li><li><p>青い「Shortcuts」をタップし、iCloud Drive → デスクトップ → ADAPT DESIGN → じぶんの → PixelstoMotors → ロボティクス復習 → 練習記録 を選ぶ</p></li><li><p>サブパスに <code>ロボ見直し.md</code> と入れる。「ファイルが存在する場合は上書き」はオフのまま</p></li></ol><p>使うときは「結果を保存」→ Safari の「\"ショートカット\"で開きますか?」に「開く」の2タップです。初回だけ、フォルダへのアクセスの許可を聞かれます。</p><p>試すと、サブパスを <code>.md</code> にしても、ファイルは「ロボ見直し.txt」「ロボ見直し-2.txt」のように <code>.txt</code> で入りました。集計スクリプトは <code>.md</code> と <code>.txt</code> の両方を読むようにしています。</p><p>1つ問題がありました。「ロボ見直し-2.txt」が「ロボ見直し.txt」とまったく同じ回でした。「結果を保存」を2回押すと、1回目に保存した回をもう一度送っていたためです。集計では同じ回(カード・段階・時刻が同じ)を1回として数えていたので数字はずれていませんでしたが、保存済みの回は次の保存に含めないように直しました。「今回の練習」の表には、保存した回に緑の「保存済」を出します。</p><p>ショートカットで入ったばかりのファイルは、Mac 側で iCloud からまだダウンロードされていないことがあり、読もうとすると <code>OSError: [Errno 35] Resource deadlock avoided</code> で止まりました。集計スクリプトは、読めないファイルを飛ばして「まだダウンロードされていない記録」として名前を出し、次の集計で読むようにしています。</p><p></p><h2 id=\"index_eByBD_Dn\">練習記録の全量(182回)</h2><p>単語の「見て言う」ひととおりの記録です。「一致(記録時)」はページで練習したときの点、「一致(聞き違い補正後)」は 56 組の一覧と、用語のすべての言い方で採点し直した点です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>時刻</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>カード</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>一致(記録時)</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>一致(聞き違い補正後)</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>話した言葉(文字起こし)</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:02:26</p></td><td colspan=\"1\" rowspan=\"1\"><p>回転関節と直動関節</p></td><td colspan=\"1\" rowspan=\"1\"><p>73%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>に点 関節と直導関節 回って 角度が変わる関節とまっすぐ伸び縮みする関節</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:02:43</p></td><td colspan=\"1\" rowspan=\"1\"><p>エンドエフェクタ(EE)</p></td><td colspan=\"1\" rowspan=\"1\"><p>66%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>エンド エフェクターアームの先端につけて作業をする道具の部分</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:03:35</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節(ジョイント)</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節ロボットの部品と部品をつなぎ 動きを生む つなぎ目</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:03:53</p></td><td colspan=\"1\" rowspan=\"1\"><p>リンク</p></td><td colspan=\"1\" rowspan=\"1\"><p>95%</p></td><td colspan=\"1\" rowspan=\"1\"><p>95%</p></td><td colspan=\"1\" rowspan=\"1\"><p>リンク関節と関節の間をつなぐ 固い 骨組みの部分</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:04:08</p></td><td colspan=\"1\" rowspan=\"1\"><p>自由度(DOF)</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>自由度ロボットがお互いに独立して動かせる 動きの方向の数</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:04:25</p></td><td colspan=\"1\" rowspan=\"1\"><p>回転関節と直動関節</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>回転関節と極東 関節 回って 角度が変わる関節とまっすぐ伸び縮みする関節</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:04:39</p></td><td colspan=\"1\" rowspan=\"1\"><p>エンドエフェクタ(EE)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>エンド エフェクターアームの先端につけて作業をする道具の部分</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:15:26</p></td><td colspan=\"1\" rowspan=\"1\"><p>座標変換</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>座標を変換ある座標系での位置や 向きを別の座標系の値に直す計算</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:15:39</p></td><td colspan=\"1\" rowspan=\"1\"><p>位置姿勢(ポーズ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>1 姿勢 物や 手先がどこにあり どちらを向いているかの組み合わせ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:15:50</p></td><td colspan=\"1\" rowspan=\"1\"><p>オイラー角(ロール・ピッチ・ヨー)</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>オイラー角 向きを3つの軸周りの回転角の組で表す方法</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:16:01</p></td><td colspan=\"1\" rowspan=\"1\"><p>クォータニオン(四元数)</p></td><td colspan=\"1\" rowspan=\"1\"><p>89%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>コオタニオン 向きを4つの数で表す方法で計算で扱いやすい</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:16:12</p></td><td colspan=\"1\" rowspan=\"1\"><p>順運動学(FK)</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>準運動学 各関節の角度から手先の位置と向きを求める計算</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:16:23</p></td><td colspan=\"1\" rowspan=\"1\"><p>逆運動学(IK)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>逆運動学 手先の目標位置から各関節の角度を逆算する計算</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:16:33</p></td><td colspan=\"1\" rowspan=\"1\"><p>作業空間(ワークスペース)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>作業空間ロボットの手先が届き 作業できる空間の範囲</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:16:44</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節可動域(ジョイントリミット)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節可動域 各関節が 安全に動ける角度の上限と下限の範囲</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:16:54</p></td><td colspan=\"1\" rowspan=\"1\"><p>特異点(特異姿勢)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>特異点 手先が特定の方向へ 動けなくなるアームの特殊な姿勢</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:17:04</p></td><td colspan=\"1\" rowspan=\"1\"><p>ヤコビ行列(ヤコビアン)</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ヤコビー 行列 関節の速さと手先の速さの関係を表す行列</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:17:16</p></td><td colspan=\"1\" rowspan=\"1\"><p>DHパラメータ</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>DH パラメーター ピンクの長さ や ねじれを関節ごとに 4つの数で表す 決まり</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:17:26</p></td><td colspan=\"1\" rowspan=\"1\"><p>アクチュエータ</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>アクチュエーター 電気などのエネルギーを実際の動きに変える装置</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:17:36</p></td><td colspan=\"1\" rowspan=\"1\"><p>サーボモーター</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>サーボモーター 指令した角度や位置へ自分で合わせて止まる モーター</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:17:47</p></td><td colspan=\"1\" rowspan=\"1\"><p>減速機(ギア)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>減速機 モーターの速い 回転を遅くしてその分力を大きくする 部品</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:17:56</p></td><td colspan=\"1\" rowspan=\"1\"><p>エンコーダ</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>エンコーダー モーター や関節がどれだけ回ったか を測るセンサー</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:18:06</p></td><td colspan=\"1\" rowspan=\"1\"><p>トルク</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>トルク 関節や 軸を回転させようとする回す力の強さ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:18:18</p></td><td colspan=\"1\" rowspan=\"1\"><p>サーボオンとサーボオフ</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>サーボ音とサーボオフ モーターに力を入れて 姿勢を保つ状態と力を抜く 状態</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:18:29</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィードバック制御(閉ループ制御)</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィードバック制御 結果を図り 目標とのズレを見ながら 指令を直し続ける 制御</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:18:41</p></td><td colspan=\"1\" rowspan=\"1\"><p>PID制御</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>PID 制御 ずれの今の大きさ たまった量変化の速さで出力を決める制御</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:18:51</p></td><td colspan=\"1\" rowspan=\"1\"><p>可搬重量(ペイロード)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>可搬重量 ロボットが手先で持って動かせる重さの上限</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:19:00</p></td><td colspan=\"1\" rowspan=\"1\"><p>リーチ</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>リーチ 台座の中心から手先が届く 一番遠い点までの距離</p></td></tr></tbody></table></div><div class=\"table\"><table><tbody><tr><td colspan=\"1\" rowspan=\"1\"><p>15:19:10</p></td><td colspan=\"1\" rowspan=\"1\"><p>繰り返し精度</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>繰り返し制度 同じ場所へ 何度も動かした時 止まる位置がどれだけ揃うか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:19:20</p></td><td colspan=\"1\" rowspan=\"1\"><p>絶対精度</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>絶対制度指令した座標に実際どれだけ 正確に到達できるか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:19:32</p></td><td colspan=\"1\" rowspan=\"1\"><p>バックラッシ</p></td><td colspan=\"1\" rowspan=\"1\"><p>88%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>バックラッシュ 歯車の噛み合わせの隙間による動きのガタ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:19:44</p></td><td colspan=\"1\" rowspan=\"1\"><p>力覚センサー(力トルクセンサー)</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>力学 センサー 手先にかかる 押す力とねじる力を直接測るセンサー</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:19:55</p></td><td colspan=\"1\" rowspan=\"1\"><p>マイコン(MCU)</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>マイコン機械に組み込まれ 決まった処理を素早く回す 小さなコンピューター</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:20:07</p></td><td colspan=\"1\" rowspan=\"1\"><p>シリアル通信とボーレート</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>シリアル通信とボーレートデータを1ビットずつ順番に送る通信とその速さの設定</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:20:18</p></td><td colspan=\"1\" rowspan=\"1\"><p>制御周期(制御ループ周波数)</p></td><td colspan=\"1\" rowspan=\"1\"><p>89%</p></td><td colspan=\"1\" rowspan=\"1\"><p>89%</p></td><td colspan=\"1\" rowspan=\"1\"><p>制御周期 測って姿勢を出す 一回りを1秒に何回繰り返せるか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:20:28</p></td><td colspan=\"1\" rowspan=\"1\"><p>シミュレーション(シミュレータ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>シミュレーション 現実の動きをコンピューの他の中で再現して試すこと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:20:39</p></td><td colspan=\"1\" rowspan=\"1\"><p>デジタルツイン</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>デジタルツイン 実物と同じ形と動きを持つ コンピュータの中の双子</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:20:50</p></td><td colspan=\"1\" rowspan=\"1\"><p>物理エンジン</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>物理エンジン 重力 やぶつかりなど物の動きを計算で再現するソフトの心臓部</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:21:00</p></td><td colspan=\"1\" rowspan=\"1\"><p>タイムステップ</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>タイムステップ シュミレーションで時間を一度に進める 刻み幅</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:21:13</p></td><td colspan=\"1\" rowspan=\"1\"><p>剛性と減衰(スティフネスとダンピング)</p></td><td colspan=\"1\" rowspan=\"1\"><p>85%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>強制塔 元帥 関節 を目標へ 引き戻す バネの強さと 揺れを抑える ブレーキの強さ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:21:22</p></td><td colspan=\"1\" rowspan=\"1\"><p>コリジョン形状</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>コリジョン 形状 ぶつかりの判定に使う 見た目より 単純にした形</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:21:34</p></td><td colspan=\"1\" rowspan=\"1\"><p>URDF</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>urdf ロボットのリンクと関節の構成を書き表す標準のファイル形式</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:21:44</p></td><td colspan=\"1\" rowspan=\"1\"><p>ROS 2</p></td><td colspan=\"1\" rowspan=\"1\"><p>88%</p></td><td colspan=\"1\" rowspan=\"1\"><p>88%</p></td><td colspan=\"1\" rowspan=\"1\"><p>露出 ロボットのソフトを部品に分けて つなぎ合わせるための共通基盤</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:21:57</p></td><td colspan=\"1\" rowspan=\"1\"><p>ノードとトピック</p></td><td colspan=\"1\" rowspan=\"1\"><p>82%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ノードとトピックロスで処理の単位とその間を流れるデータの通り道</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:22:08</p></td><td colspan=\"1\" rowspan=\"1\"><p>MoveIt</p></td><td colspan=\"1\" rowspan=\"1\"><p>90%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>MOVE it アームの動作計画をまとめて扱う ロスの代表的な道具</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:22:19</p></td><td colspan=\"1\" rowspan=\"1\"><p>Gazebo</p></td><td colspan=\"1\" rowspan=\"1\"><p>75%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ガズボン ロスト 組み合わせて使われてきた無料のロボット用 シミュレーター</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:22:34</p></td><td colspan=\"1\" rowspan=\"1\"><p>Isaac Sim</p></td><td colspan=\"1\" rowspan=\"1\"><p>73%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>イザックシム NVIDIA が提供する GPU で動く 高精細なロボットというシュミレーター</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:22:45</p></td><td colspan=\"1\" rowspan=\"1\"><p>Isaac Sim</p></td><td colspan=\"1\" rowspan=\"1\"><p>83%</p></td><td colspan=\"1\" rowspan=\"1\"><p>98%</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイザック SIM NVIDIA が提供する GPU で動く 高精細なロボット用 シュミレーター</p></td></tr></tbody></table></div><div class=\"table\"><table><tbody><tr><td colspan=\"1\" rowspan=\"1\"><p>15:23:10</p></td><td colspan=\"1\" rowspan=\"1\"><p>並列シミュレーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>並列 シュミレーション 同じ環境を何個も同時に動かしてデータを一気に集めること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:23:20</p></td><td colspan=\"1\" rowspan=\"1\"><p>sim2real(シムツーリアル)</p></td><td colspan=\"1\" rowspan=\"1\"><p>87%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>SIM 2 リアルシュミレーションで作った 動きや 学習結果を実機へ持っていくこと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:23:29</p></td><td colspan=\"1\" rowspan=\"1\"><p>リアリティギャップ(sim2realギャップ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>リアリティ ギャップ シミュレーションと現実の間にあるズレのこと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:23:44</p></td><td colspan=\"1\" rowspan=\"1\"><p>ドメインランダマイゼーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ドメイン ライダー マイゼーションシミュレーションの条件をわざとばらつかせ 現実のずれに強くする方法</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:23:52</p></td><td colspan=\"1\" rowspan=\"1\"><p>合成データ</p></td><td colspan=\"1\" rowspan=\"1\"><p>52%</p></td><td colspan=\"1\" rowspan=\"1\"><p>52%</p></td><td colspan=\"1\" rowspan=\"1\"><p>合成データシミュレーションや 合成</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:24:00</p></td><td colspan=\"1\" rowspan=\"1\"><p>合成データ</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>合成データシミュレーションや 画像 生成で作った学習用のデータ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:24:12</p></td><td colspan=\"1\" rowspan=\"1\"><p>real2sim(リアルツーシム)</p></td><td colspan=\"1\" rowspan=\"1\"><p>79%</p></td><td colspan=\"1\" rowspan=\"1\"><p>82%</p></td><td colspan=\"1\" rowspan=\"1\"><p>Real to SIM 現実のものや動きを 測り とって シミュレーションの中に</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:24:24</p></td><td colspan=\"1\" rowspan=\"1\"><p>real2sim(リアルツーシム)</p></td><td colspan=\"1\" rowspan=\"1\"><p>85%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>リアル トゥー SIM 現実のものや動きを測りとって シミュレーションの中に再現すること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:24:34</p></td><td colspan=\"1\" rowspan=\"1\"><p>画素(ピクセル)</p></td><td colspan=\"1\" rowspan=\"1\"><p>88%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>画素デジタル画素を作る色の数値を持った最小の点</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:24:44</p></td><td colspan=\"1\" rowspan=\"1\"><p>色空間(RGB・HSV)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>色空間 色を数字で表す時の表し方の決まりごと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:24:58</p></td><td colspan=\"1\" rowspan=\"1\"><p>二値化(しきい値処理)</p></td><td colspan=\"1\" rowspan=\"1\"><p>74%</p></td><td colspan=\"1\" rowspan=\"1\"><p>79%</p></td><td colspan=\"1\" rowspan=\"1\"><p>閾値処理 決めた 堺市の値でガソリン 画素を白と黒の2つに分ける 処理</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:25:11</p></td><td colspan=\"1\" rowspan=\"1\"><p>二値化(しきい値処理)</p></td><td colspan=\"1\" rowspan=\"1\"><p>86%</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>閾値処理 決めた 堺市 境目の値で画素を白と黒の2つに分ける 処理</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:25:21</p></td><td colspan=\"1\" rowspan=\"1\"><p>二値化(しきい値処理)</p></td><td colspan=\"1\" rowspan=\"1\"><p>86%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>閾値処理 決めた 境目の値で画素を白と黒の2つに分ける 処理</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:25:34</p></td><td colspan=\"1\" rowspan=\"1\"><p>モルフォロジー演算</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>モロフォジー演算 白黒画像の小さな粒や穴を形の大きさで整える処理</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:25:45</p></td><td colspan=\"1\" rowspan=\"1\"><p>輪郭抽出(コンター)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>輪郭抽出 白黒画像から物の外側の縁を線としてたどる 処理</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:26:00</p></td><td colspan=\"1\" rowspan=\"1\"><p>ROI(関心領域)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>Roi 画像の中で処理の対象にする範囲だけを切り出した部分</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:26:10</p></td><td colspan=\"1\" rowspan=\"1\"><p>テンプレートマッチング</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>テンプレート マッチング 日本の画像を少しずつずらし 似ている場所を探す方法</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:26:21</p></td><td colspan=\"1\" rowspan=\"1\"><p>特徴点マッチング</p></td><td colspan=\"1\" rowspan=\"1\"><p>83%</p></td><td colspan=\"1\" rowspan=\"1\"><p>83%</p></td><td colspan=\"1\" rowspan=\"1\"><p>徳井町店 マッチング 2点の画像から同じ場所を指す 特徴的な点を結びつける方法</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:26:34</p></td><td colspan=\"1\" rowspan=\"1\"><p>物体検出(オブジェクトディテクション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>物体検出画像の中のものを見つけ 四角い枠と名前と確からしさで返す 処理</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:26:46</p></td><td colspan=\"1\" rowspan=\"1\"><p>バウンディングボックス(BBox)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>バウンディングボックス 検出したものを囲むように描く 画像上の四角い枠</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:26:57</p></td><td colspan=\"1\" rowspan=\"1\"><p>IoU</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>iou 2つの枠がどれだけ重なっているかを 0から1で表す指標</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:27:10</p></td><td colspan=\"1\" rowspan=\"1\"><p>セグメンテーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>セグメンテーション 画像のガスごとにどのものに属するかを 塗り分ける 処理</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:27:21</p></td><td colspan=\"1\" rowspan=\"1\"><p>カメラ校正(キャリブレーション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>カメラ 構成 カメラの特性や 置き場所を数値として求めておく 作業</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:27:32</p></td><td colspan=\"1\" rowspan=\"1\"><p>カメラ校正(キャリブレーション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>21%</p></td><td colspan=\"1\" rowspan=\"1\"><p>21%</p></td><td colspan=\"1\" rowspan=\"1\"><p>内部パラメータ 焦点距離や レンズの歪み など カメラ 自体が持つ性質の値</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:27:47</p></td><td colspan=\"1\" rowspan=\"1\"><p>内部パラメータ</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>内部パラメータ 焦点距離や レンズの歪み など カメラ 自体が持つ性質の値</p></td></tr></tbody></table></div><div class=\"table\"><table><tbody><tr><td colspan=\"1\" rowspan=\"1\"><p>15:27:58</p></td><td colspan=\"1\" rowspan=\"1\"><p>外部パラメータ</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>外部パラメータ カメラが空間のどこにどの向きで置かれているかは 表す値</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:28:08</p></td><td colspan=\"1\" rowspan=\"1\"><p>ホモグラフィ</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ホモグラフィー ある平面の見え方を別の視点から見た平面へ移し替える 変換</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:28:19</p></td><td colspan=\"1\" rowspan=\"1\"><p>視差(パララックス)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>パララックス 見る位置が変わると 物の見える位置がずれて見える現象</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:28:30</p></td><td colspan=\"1\" rowspan=\"1\"><p>深度カメラ(RGB-Dカメラ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>進路カメラ色の画像と一緒に 各画素までの距離も測れるカメラ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:28:42</p></td><td colspan=\"1\" rowspan=\"1\"><p>点群(ポイントクラウド)</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ポイントクラウド 物の表面を3次元の位置を持つ たくさんの点の集まりで表したデータ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:28:52</p></td><td colspan=\"1\" rowspan=\"1\"><p>経由点(ウェイポイント)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ウェイポイント 動きの途中で通るように指定する目印となる点</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:29:07</p></td><td colspan=\"1\" rowspan=\"1\"><p>経路と軌道(パスとトラジェクトリ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>パスト トラジェクトリー 経路は通る道筋 起動はそこに時間と速さを加えたもの</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:29:17</p></td><td colspan=\"1\" rowspan=\"1\"><p>補間(インターポレーション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>86%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>インターポーレーション 離れた点と点の間を滑らかな値でつないで埋めること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:29:27</p></td><td colspan=\"1\" rowspan=\"1\"><p>速度プロファイル(加減速)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>速度プロファイル 動き出し から止まるまで速さをどう変えるかの決め方</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:29:40</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節補間と直線補間</p></td><td colspan=\"1\" rowspan=\"1\"><p>81%</p></td><td colspan=\"1\" rowspan=\"1\"><p>95%</p></td><td colspan=\"1\" rowspan=\"1\"><p>間接保管と直線 保管 関節の角度でつなぐか 手先の直線でつなぐかという保管の違い</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:29:55</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節補間と直線補間</p></td><td colspan=\"1\" rowspan=\"1\"><p>81%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節保管と直接 保管 関節の角度でつなぐか 手先の直線でつなぐかという保管の違い</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:30:07</p></td><td colspan=\"1\" rowspan=\"1\"><p>ティーチング(教示)</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ティーチング ロボットンに動きやす 1 を人が直接教えて記録させること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:30:16</p></td><td colspan=\"1\" rowspan=\"1\"><p>把持(グラスプ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>83%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>恥 グリッパーやハンドで 物を掴んで保持すること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:30:26</p></td><td colspan=\"1\" rowspan=\"1\"><p>ピックアンドプレース(Pick&Place)</p></td><td colspan=\"1\" rowspan=\"1\"><p>74%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ビッグ&プレイス 物を掴んで持ち上げ 別の場所へ運んでおく 一連の作業</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:30:36</p></td><td colspan=\"1\" rowspan=\"1\"><p>アプローチと退避</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>アプローチと対比 対象の手前まで近づく 動きと作業後に離れる 動きのこと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:30:46</p></td><td colspan=\"1\" rowspan=\"1\"><p>許容誤差(トレランス)</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>許容誤差 目標に届いたとみなして良い ズレの許される範囲</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:30:57</p></td><td colspan=\"1\" rowspan=\"1\"><p>動作計画(モーションプランニング)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>モーションプランニング障害物を避けながら 目標までの動きを自動で求めること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:31:09</p></td><td colspan=\"1\" rowspan=\"1\"><p>衝突回避</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>衝突回避 ロボットが周りのものや人 自分の体にぶつからないようにすること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:31:20</p></td><td colspan=\"1\" rowspan=\"1\"><p>視覚サーボ(ビジュアルサーボ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ビジュアルサーボ カメラで見た ズレを使い 目標に合うまで 動き直し続ける 制御</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:31:31</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイインハンドとアイトゥハンド</p></td><td colspan=\"1\" rowspan=\"1\"><p>46%</p></td><td colspan=\"1\" rowspan=\"1\"><p>66%</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイ イン ハンド と I to Hand カメラを手先につけるか先に</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:31:42</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイインハンドとアイトゥハンド</p></td><td colspan=\"1\" rowspan=\"1\"><p>76%</p></td><td colspan=\"1\" rowspan=\"1\"><p>98%</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイ イン ハンド と I to Hand カメラを手先につけるか外に固定するかのという2つの配置</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:31:53</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイインハンドとアイトゥハンド</p></td><td colspan=\"1\" rowspan=\"1\"><p>78%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイ イン ハンド と I to Hand カメラを手先につけるか外に固定するかという2つの配置</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:32:05</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハンドアイ校正(ハンドアイキャリブレーション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハンドアイ 構成 カメラとロボットの位置と向きの関係を求める作業</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:32:17</p></td><td colspan=\"1\" rowspan=\"1\"><p>受け渡し(ハンドオーバー)</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>受け渡し ロボットと人の間で物を手渡したり 受け取ったりする動作</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:32:28</p></td><td colspan=\"1\" rowspan=\"1\"><p>コンプライアンス(柔らかさ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>コンプライアンス 外から力を受けた時 ロボットが逆らわずに少したわむ 性質</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:32:40</p></td><td colspan=\"1\" rowspan=\"1\"><p>インピーダンス制御</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>インピーダンス制御バネとダンパーのような柔らかさを制御でロボットに持たせる方法</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:32:52</p></td><td colspan=\"1\" rowspan=\"1\"><p>学習(トレーニング)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>トレーニングデータを使ってモデルの中の数値を少しずつ調整すること</p></td></tr></tbody></table></div><div class=\"table\"><table><tbody><tr><td colspan=\"1\" rowspan=\"1\"><p>15:33:02</p></td><td colspan=\"1\" rowspan=\"1\"><p>推論(インファレンス)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>インファレンス 学習済みのモデルに入力を渡し 答えを出させること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:33:14</p></td><td colspan=\"1\" rowspan=\"1\"><p>観測と行動(オブザベーションとアクション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>90%</p></td><td colspan=\"1\" rowspan=\"1\"><p>95%</p></td><td colspan=\"1\" rowspan=\"1\"><p>オブザベーションとアクションロボットが受け取る情報とそれを元に 動き の指令</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:33:28</p></td><td colspan=\"1\" rowspan=\"1\"><p>観測と行動(オブザベーションとアクション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>83%</p></td><td colspan=\"1\" rowspan=\"1\"><p>83%</p></td><td colspan=\"1\" rowspan=\"1\"><p>オブザベーションとアクションロボットが受け取る情報とそれをもとに 動き それをもとに出す動きの指令</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:33:39</p></td><td colspan=\"1\" rowspan=\"1\"><p>観測と行動(オブザベーションとアクション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>オブザベーションとアクションロボットが受け取る情報とそれをもとに出す動きの指令</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:33:49</p></td><td colspan=\"1\" rowspan=\"1\"><p>方策(ポリシー)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ポリシー 感染観測を受け取り次にどう動くかを決める仕組み</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:33:58</p></td><td colspan=\"1\" rowspan=\"1\"><p>方策(ポリシー)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ポリシー 観測を受け取り次にどう動くかを決める仕組み</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:34:08</p></td><td colspan=\"1\" rowspan=\"1\"><p>エピソード</p></td><td colspan=\"1\" rowspan=\"1\"><p>85%</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>エピソード タスクの開始から終了までの一区切りの思考</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:34:18</p></td><td colspan=\"1\" rowspan=\"1\"><p>デモンストレーション(実演データ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>デモンストレーション 人がお手本として操作して見せた動きの記録</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:34:27</p></td><td colspan=\"1\" rowspan=\"1\"><p>テレオペレーション(遠隔操作)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>テレオペレーション 人が離れた場所からロボットをリアルタイムに操作すること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:34:36</p></td><td colspan=\"1\" rowspan=\"1\"><p>模倣学習</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>魔法 学習人の実演データをお手本にロボットに動きを覚えさせる学習</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:34:49</p></td><td colspan=\"1\" rowspan=\"1\"><p>行動クローニング(BC)</p></td><td colspan=\"1\" rowspan=\"1\"><p>89%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>高等 クローニング 観測と行動の組をそのまま 真似るように学ぶ最も単純な魔法 学習</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:35:00</p></td><td colspan=\"1\" rowspan=\"1\"><p>強化学習(RL)</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>強化学習 試行錯誤の結果に点数をつけて点数が高くなる動きを学ぶ方法</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:35:11</p></td><td colspan=\"1\" rowspan=\"1\"><p>報酬(リワード)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>リワード 行動の結果がどれだけ良かったかを示す 強化学習の点数</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:35:26</p></td><td colspan=\"1\" rowspan=\"1\"><p>汎化</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハンカ 学習で見たことのない状況でもうまく動けること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:35:36</p></td><td colspan=\"1\" rowspan=\"1\"><p>汎化</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>文化学習 で見たことのない状況でもうまく動けること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:35:48</p></td><td colspan=\"1\" rowspan=\"1\"><p>過学習(オーバーフィッティング)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>オーバー フィッティング学習データを覚えすぎて 新しいデータに弱くなる状態</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:36:01</p></td><td colspan=\"1\" rowspan=\"1\"><p>基盤モデル(ファウンデーションモデル)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ファンデーションモデル 大量データの事前に学習され 多くの用途に転用できる大きなモデル</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:36:13</p></td><td colspan=\"1\" rowspan=\"1\"><p>事前学習(プレトレーニング)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>事前学習 特定の作業の前に大量の一般的なデータで学ばせておく 段階</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:36:24</p></td><td colspan=\"1\" rowspan=\"1\"><p>ファインチューニング</p></td><td colspan=\"1\" rowspan=\"1\"><p>95%</p></td><td colspan=\"1\" rowspan=\"1\"><p>95%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ファインチューニング事前学習済みのモデルを自分のデータで追加学習して合わせること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:36:35</p></td><td colspan=\"1\" rowspan=\"1\"><p>VLM(視覚言語モデル)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>vlm 画像と言葉を一緒に理解し言葉で答える AI モデル</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:36:45</p></td><td colspan=\"1\" rowspan=\"1\"><p>VLA(視覚言語行動モデル)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>vla 画像と言葉の指示からロボットの動きを直接出力するモデル</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:36:54</p></td><td colspan=\"1\" rowspan=\"1\"><p>試行(トライアル)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>トライアル 同じ条件でロボットに動作を1回やらせること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:36:59</p></td><td colspan=\"1\" rowspan=\"1\"><p>成功率(サクセスレート)</p></td><td colspan=\"1\" rowspan=\"1\"><p>21%</p></td><td colspan=\"1\" rowspan=\"1\"><p>21%</p></td><td colspan=\"1\" rowspan=\"1\"><p>サクセストレー</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:37:07</p></td><td colspan=\"1\" rowspan=\"1\"><p>成功率(サクセスレート)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>サクセス レート 試行した回数のうち 成功した回数が占める割合</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:37:16</p></td><td colspan=\"1\" rowspan=\"1\"><p>成功条件</p></td><td colspan=\"1\" rowspan=\"1\"><p>81%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>性交 条件 何を持って成功とするかを 思考の前に決めた 基準</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:37:26</p></td><td colspan=\"1\" rowspan=\"1\"><p>ロギング(ログ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>79%</p></td><td colspan=\"1\" rowspan=\"1\"><p>86%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ロギング動作中の状態や結果を後で見返す 量 記録すること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:37:37</p></td><td colspan=\"1\" rowspan=\"1\"><p>データセット</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>データセット 学習や評価に使うために集めて整理したデータのまとまり</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:37:46</p></td><td colspan=\"1\" rowspan=\"1\"><p>失敗モード</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>失敗モード 失敗をその置き方や 原因 ごとに分けた 種類</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:37:54</p></td><td colspan=\"1\" rowspan=\"1\"><p>ベースライン</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ベースライン 改善の効果を比べるための基準となる 結果</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:38:04</p></td><td colspan=\"1\" rowspan=\"1\"><p>ベースライン</p></td><td colspan=\"1\" rowspan=\"1\"><p>12%</p></td><td colspan=\"1\" rowspan=\"1\"><p>12%</p></td><td colspan=\"1\" rowspan=\"1\"><p>再現性 同じ手順と条件で同じ結果が もう一度 得られること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:38:14</p></td><td colspan=\"1\" rowspan=\"1\"><p>誤差</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>誤差 測った値や 到達した位置と本当の値とのズレ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:38:24</p></td><td colspan=\"1\" rowspan=\"1\"><p>系統誤差</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>系統誤差原因が決まっていて いつも同じ向きにずれる 誤差</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:38:34</p></td><td colspan=\"1\" rowspan=\"1\"><p>偶然誤差(ばらつき)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>偶然誤差 毎回 ランダムに上下する向きの決まらない 誤差</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:38:47</p></td><td colspan=\"1\" rowspan=\"1\"><p>正確さと精密さ</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>正確さと精密さ 狙いに近いのが正確さ 毎回 揃うのが 精密さ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:38:55</p></td><td colspan=\"1\" rowspan=\"1\"><p>外れ値</p></td><td colspan=\"1\" rowspan=\"1\"><p>88%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハズレ 地 他のデータから大きく離れた 例外的な値のこと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:39:05</p></td><td colspan=\"1\" rowspan=\"1\"><p>アノテーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>アノテーション画像やデータに人が正解の情報を書き込む 作業</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:39:15</p></td><td colspan=\"1\" rowspan=\"1\"><p>正解データ(グラウンドトゥルース)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>正解 データ 評価の基準にする本当の値とみなす データ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:39:24</p></td><td colspan=\"1\" rowspan=\"1\"><p>評価指標(メトリクス)</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>評価指標性能 数字で表すために使う決まったものさし</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:39:34</p></td><td colspan=\"1\" rowspan=\"1\"><p>ベンチマーク</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ベンチマーク 性能 比べるための共通の課題と評価のやり方</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:39:44</p></td><td colspan=\"1\" rowspan=\"1\"><p>統計的なぶれ(信頼区間)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>統計的なブレ 思考回数が少ないと成功率が偶然だけで動く 幅</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:39:54</p></td><td colspan=\"1\" rowspan=\"1\"><p>分布外(OOD)</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>分布 街 OOD 学習の時に見たことのない新しい条件や データ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:40:05</p></td><td colspan=\"1\" rowspan=\"1\"><p>アブレーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>アブレーション部品を一つずつ外してそれぞれの効き目を確かめる実験</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:40:17</p></td><td colspan=\"1\" rowspan=\"1\"><p>反復開発(イテレーション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>反復開発 小さく作って試し直す 流れを何度も回す 進め方</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:40:27</p></td><td colspan=\"1\" rowspan=\"1\"><p>仮説検証</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>仮設 検証 原因の予想を立て実験で それが正しいか確かめること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:40:37</p></td><td colspan=\"1\" rowspan=\"1\"><p>切り分け</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>切り分け問題の範囲を少しずつ 狭めて原因の場所を特定すること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:40:46</p></td><td colspan=\"1\" rowspan=\"1\"><p>ボトルネック</p></td><td colspan=\"1\" rowspan=\"1\"><p>86%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ボトルネック 全体の速さや 性能を一番強く 制限している部分</p></td></tr></tbody></table></div><div class=\"table\"><table><tbody><tr><td colspan=\"1\" rowspan=\"1\"><p>15:40:58</p></td><td colspan=\"1\" rowspan=\"1\"><p>根本原因分析(RCA)</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>根本原因分析表に出た症状ではなく 問題を生んだ 大元 突き止めること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:41:10</p></td><td colspan=\"1\" rowspan=\"1\"><p>段階的検証</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>段階的検証小さな単位から順に確かめ 少しずつ 範囲を広げる 検証</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:41:19</p></td><td colspan=\"1\" rowspan=\"1\"><p>ドライラン(dry-run)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ドライラン 実際には動かさず 計画や 処理の流れだけを試すこと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:41:29</p></td><td colspan=\"1\" rowspan=\"1\"><p>フェイルセーフ</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ペールセーフ故障や異常が起きても安全な側に倒れるようにする設計</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:41:41</p></td><td colspan=\"1\" rowspan=\"1\"><p>フォールバック</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ポールバック 主な方法が使えない時に切り替える 代わりの手段</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:41:54</p></td><td colspan=\"1\" rowspan=\"1\"><p>非常停止(E-stop)</p></td><td colspan=\"1\" rowspan=\"1\"><p>84%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>非常停止 E ストップ 非常時に人がすぐ ロボットを止めるための仕組み</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:42:03</p></td><td colspan=\"1\" rowspan=\"1\"><p>インターロック</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>インターロック 条件が揃わない限り 動作できないようにする仕組み</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:42:15</p></td><td colspan=\"1\" rowspan=\"1\"><p>安全マージン</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>安全マージン 限界ギリギリではなく 余裕を残して決める設定の幅</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:42:25</p></td><td colspan=\"1\" rowspan=\"1\"><p>最小構成(MVP)</p></td><td colspan=\"1\" rowspan=\"1\"><p>83%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>最小構成 目的を確かめるのに必要な一番小さな形</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:42:37</p></td><td colspan=\"1\" rowspan=\"1\"><p>技術的負債</p></td><td colspan=\"1\" rowspan=\"1\"><p>85%</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>技術的 夫妻その場しのぎの実装が後で修正の手間として帰ってくること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:42:47</p></td><td colspan=\"1\" rowspan=\"1\"><p>コードレビュー</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>コードレビュー書いたコードを別の人や 別の目で確認すること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:42:58</p></td><td colspan=\"1\" rowspan=\"1\"><p>HRI(ヒューマンロボットインタラクション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ヒューマンロボットインタラクション人とロボットのやり取りや 関わり方を扱う分野</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:43:10</p></td><td colspan=\"1\" rowspan=\"1\"><p>動きの分かりやすさ(レジビリティ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>レジビリティ 動きを見ただけでロボットの狙いが人に伝わる性質</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:43:20</p></td><td colspan=\"1\" rowspan=\"1\"><p>ヒューマンインザループ</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ヒューマンインザループ 判断や修正の流れの中に人を組み込んだ 仕組み</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:43:31</p></td><td colspan=\"1\" rowspan=\"1\"><p>産業用ロボット</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>産業用ロボット工場で溶接や 組み立て 搬送などを自動で行うロボット</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:43:42</p></td><td colspan=\"1\" rowspan=\"1\"><p>協働ロボット(コボット)</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>共同ロボット 柵なしで人のそばで 一緒に働けるよう作られたロボット</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:43:51</p></td><td colspan=\"1\" rowspan=\"1\"><p>サービスロボット</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>サービスロボット工場の外で人の生活や仕事を手伝う ロボット</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:44:02</p></td><td colspan=\"1\" rowspan=\"1\"><p>AGV(無人搬送車)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>AGV 床の線や印に沿って決まった経路で荷物を運ぶ車</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:44:12</p></td><td colspan=\"1\" rowspan=\"1\"><p>AMR(自律移動ロボット)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>AMR 地図とセンサで自分で道を選んで移動するロボット</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:44:22</p></td><td colspan=\"1\" rowspan=\"1\"><p>ヒューマノイド</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ヒューマノイド 人の近い体の形をした 二足歩行 などのロボット</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:44:31</p></td><td colspan=\"1\" rowspan=\"1\"><p>省人化</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>精進化 作業に必要な人の数を機械化や自動化で減らすこと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:44:41</p></td><td colspan=\"1\" rowspan=\"1\"><p>タクトタイム</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>タクトタイム 製品1個を作るのに許される 精算上の目標 時間</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:44:52</p></td><td colspan=\"1\" rowspan=\"1\"><p>SIer(システムインテグレータ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>システムインテグレータ ロボットを現場に合わせて 組み込み 仕組みとして納める会社</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:45:03</p></td><td colspan=\"1\" rowspan=\"1\"><p>PoC(概念実証)</p></td><td colspan=\"1\" rowspan=\"1\"><p>88%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ポック 本格導入の前に小さく 試して効果を確かめること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:45:17</p></td><td colspan=\"1\" rowspan=\"1\"><p>ROI(投資対効果)</p></td><td colspan=\"1\" rowspan=\"1\"><p>88%</p></td><td colspan=\"1\" rowspan=\"1\"><p>88%</p></td><td colspan=\"1\" rowspan=\"1\"><p>Roi かけたをかけたお金に対してどれだけの効果が返ってくるかの指標</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:45:28</p></td><td colspan=\"1\" rowspan=\"1\"><p>安全柵</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>安全策ロボットの動く範囲に人が入れないよう 囲う 柵</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:45:40</p></td><td colspan=\"1\" rowspan=\"1\"><p>リスクアセスメント</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>リスクアセスメント危険を洗い出し その大きさを見積もって対策を決める手順</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:45:51</p></td><td colspan=\"1\" rowspan=\"1\"><p>安全規格(ISO 10218)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>安全規格 産業用ロボットの安全について定めた国際規格</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:46:02</p></td><td colspan=\"1\" rowspan=\"1\"><p>機能安全</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>昨日安全制御の仕組みが壊れても安全を保てるようにする考え方</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:46:13</p></td><td colspan=\"1\" rowspan=\"1\"><p>RaaS(ロボットアズアサービス)</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ロボット アザー サービスロボットを買わずに 月額などの利用料で使う 仕組み</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:46:22</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィジカルAI(Physical AI)</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィジカル AI 現実世界を認識し 体を動かして働く AI</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>16:07:51</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節(ジョイント)</p></td><td colspan=\"1\" rowspan=\"1\"><p>85%</p></td><td colspan=\"1\" rowspan=\"1\"><p>88%</p></td><td colspan=\"1\" rowspan=\"1\"><p>股関節ロボットの部品と部品をつなぎ動きを産むつなぎ目</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>16:08:56</p></td><td colspan=\"1\" rowspan=\"1\"><p>リンク</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>リンク関節と関節の間をつなぐ硬い骨組みの部分</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>16:10:24</p></td><td colspan=\"1\" rowspan=\"1\"><p>自由度(DOF)</p></td><td colspan=\"1\" rowspan=\"1\"><p>85%</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>PO Fロボットがお互いに独立して動かせる簿記の方向の数</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>16:13:25</p></td><td colspan=\"1\" rowspan=\"1\"><p>回転関節と直動関節</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>回転関節と食道関節回って角度が変わる関節とまっすぐ伸び縮みする関節</p></td></tr></tbody></table></div><p></p><h2 id=\"index_xj0uFjsd\">聞き違いの一覧(homophones・56組)</h2><p>各組の中で、カードに出てくる書き方に、話した言葉の別の書き方をそろえます。英字は小文字で持っています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>カードの書き方</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>そろえる言い方</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>補間</p></td><td colspan=\"1\" rowspan=\"1\"><p>保管</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>校正</p></td><td colspan=\"1\" rowspan=\"1\"><p>構成、厚生</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>直動</p></td><td colspan=\"1\" rowspan=\"1\"><p>直導、直道</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>成功</p></td><td colspan=\"1\" rowspan=\"1\"><p>性交</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>試行</p></td><td colspan=\"1\" rowspan=\"1\"><p>思考、施行</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>しきい</p></td><td colspan=\"1\" rowspan=\"1\"><p>閾、堺市、敷居</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ros</p></td><td colspan=\"1\" rowspan=\"1\"><p>ロス</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>gazebo</p></td><td colspan=\"1\" rowspan=\"1\"><p>ガゼボ、ガズボン、ガズボ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ピック</p></td><td colspan=\"1\" rowspan=\"1\"><p>ビッグ、ビック、pick</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>プレース</p></td><td colspan=\"1\" rowspan=\"1\"><p>プレイス、place</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>アイトゥハンド</p></td><td colspan=\"1\" rowspan=\"1\"><p>itohand、アイトゥーハンド</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>アイインハンド</p></td><td colspan=\"1\" rowspan=\"1\"><p>iinhand</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>シミュレータ</p></td><td colspan=\"1\" rowspan=\"1\"><p>シュミレータ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>シミュレーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>シュミレーション</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>isaac</p></td><td colspan=\"1\" rowspan=\"1\"><p>イザック、アイザック</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>real2sim</p></td><td colspan=\"1\" rowspan=\"1\"><p>realtosim、リアルツーシム、リアルトゥーsim、リアルトゥsim、リアルtosim</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>sim2real</p></td><td colspan=\"1\" rowspan=\"1\"><p>simtoreal、シムツーリアル、sim2リアル、シム2リアル、simトゥーリアル</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>二つ</p></td><td colspan=\"1\" rowspan=\"1\"><p>2つ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>一つ</p></td><td colspan=\"1\" rowspan=\"1\"><p>1つ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>三つ</p></td><td colspan=\"1\" rowspan=\"1\"><p>3つ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>掴</p></td><td colspan=\"1\" rowspan=\"1\"><p>つか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>後で</p></td><td colspan=\"1\" rowspan=\"1\"><p>あとで</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>付け</p></td><td colspan=\"1\" rowspan=\"1\"><p>つけ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>置く</p></td><td colspan=\"1\" rowspan=\"1\"><p>おく</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>とおり</p></td><td colspan=\"1\" rowspan=\"1\"><p>通り</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ほか</p></td><td colspan=\"1\" rowspan=\"1\"><p>他</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>わかり</p></td><td colspan=\"1\" rowspan=\"1\"><p>分かり</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>でき</p></td><td colspan=\"1\" rowspan=\"1\"><p>出来</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>くらい</p></td><td colspan=\"1\" rowspan=\"1\"><p>位</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ころ</p></td><td colspan=\"1\" rowspan=\"1\"><p>頃</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ずれ</p></td><td colspan=\"1\" rowspan=\"1\"><p>ズレ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>もの</p></td><td colspan=\"1\" rowspan=\"1\"><p>物</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>こと</p></td><td colspan=\"1\" rowspan=\"1\"><p>事</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>とき</p></td><td colspan=\"1\" rowspan=\"1\"><p>時</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ため</p></td><td colspan=\"1\" rowspan=\"1\"><p>為</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>いちばん</p></td><td colspan=\"1\" rowspan=\"1\"><p>一番</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>あいだ</p></td><td colspan=\"1\" rowspan=\"1\"><p>間</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>なめらか</p></td><td colspan=\"1\" rowspan=\"1\"><p>滑らか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>すき</p></td><td colspan=\"1\" rowspan=\"1\"><p>隙</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>つく</p></td><td colspan=\"1\" rowspan=\"1\"><p>作</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>もと</p></td><td colspan=\"1\" rowspan=\"1\"><p>元</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>把持</p></td><td colspan=\"1\" rowspan=\"1\"><p>恥</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>行動</p></td><td colspan=\"1\" rowspan=\"1\"><p>高等</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>模倣</p></td><td colspan=\"1\" rowspan=\"1\"><p>魔法</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>剛性</p></td><td colspan=\"1\" rowspan=\"1\"><p>強制</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>減衰</p></td><td colspan=\"1\" rowspan=\"1\"><p>元帥</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>外れ値</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハズレ地、はずれち</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>poc</p></td><td colspan=\"1\" rowspan=\"1\"><p>ポック</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>クォータニオン</p></td><td colspan=\"1\" rowspan=\"1\"><p>コオタニオン、クオタニオン</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>二枚</p></td><td colspan=\"1\" rowspan=\"1\"><p>2枚</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>バックラッシ</p></td><td colspan=\"1\" rowspan=\"1\"><p>バックラッシュ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>stop</p></td><td colspan=\"1\" rowspan=\"1\"><p>ストップ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>sim</p></td><td colspan=\"1\" rowspan=\"1\"><p>シム</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>生む</p></td><td colspan=\"1\" rowspan=\"1\"><p>産む</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>互い</p></td><td colspan=\"1\" rowspan=\"1\"><p>お互い</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>dof</p></td><td colspan=\"1\" rowspan=\"1\"><p>pof</p></td></tr></tbody></table></div><p></p><h2 id=\"index_RbStIw79\">ページのテンプレート template.html の全文</h2><p><code>/*DATA*/null</code> と <code>/*PROGRESS*/{}</code> の2か所に、<code>build_review.py</code> が書き出すときにデータと練習の集計を埋め込みます。</p><p>html</p><pre><code class=\"language-html\"><!doctype html>\n<html lang=\"ja\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n<meta name=\"robots\" content=\"noindex, nofollow\">\n<title>ロボティクスの見直し</title>\n<style>\n/* 仕事で使っている「台本読み合わせ」ページと同じ見た目:横スワイプのカード+その下に「押している間だけ聞く」 */\n:root{\n --bg:#f2f3f5; --fg:#1d2129; --muted:#6b7280; --panel:#ffffff; --line:#e3e5e9;\n --card:#1a1b1f; --card-fg:#ffffff; --card-sub:#a9adb6; --gold:#f2c14e; --time:#c98a12;\n --miss:#fde2e1; --miss-fg:#b42318; --extra:#dbeafe; --extra-fg:#1e40af; --ok:#15803d;\n --btn:#1a1b1f; --btn-fg:#ffffff; --rec:#b42318;\n --font:-apple-system,\"Hiragino Sans\",\"Hiragino Kaku Gothic ProN\",sans-serif;\n}\n@media (prefers-color-scheme: dark){:root:not([data-theme=\"light\"]){\n --bg:#111215; --fg:#e8eaee; --muted:#9aa0aa; --panel:#1c1d22; --line:#2c2e35;\n --card:#000000; --miss:#4a1714; --miss-fg:#ffb4ab; --extra:#13284d; --extra-fg:#bcd3ff; --ok:#4ade80;\n --btn:#e8eaee; --btn-fg:#111215; color-scheme:dark}}\n:root[data-theme=\"dark\"]{\n --bg:#111215; --fg:#e8eaee; --muted:#9aa0aa; --panel:#1c1d22; --line:#2c2e35;\n --card:#000000; --miss:#4a1714; --miss-fg:#ffb4ab; --extra:#13284d; --extra-fg:#bcd3ff; --ok:#4ade80;\n --btn:#e8eaee; --btn-fg:#111215; color-scheme:dark}\n*{box-sizing:border-box}\n[hidden]{display:none!important}\nhtml{-webkit-text-size-adjust:100%}\nbody{margin:0;background:var(--bg);color:var(--fg);font:18px/1.7 var(--font);padding:0 0 calc(24px + env(safe-area-inset-bottom))}\nheader{background:#1a1b1f;color:#fff;padding:calc(12px + env(safe-area-inset-top)) 16px 12px}\nheader h1{margin:0;font-size:21px}\nheader p{margin:2px 0 0;font-size:14px;color:rgba(255,255,255,.75)}\nmain{padding:12px 16px 0;max-width:720px;margin:0 auto}\n.bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0 0 10px}\n.selwrap{position:relative;display:block;width:100%}\nselect{-webkit-appearance:none;appearance:none;display:block;width:100%;font:inherit;font-size:16px;font-weight:600;background:var(--btn);color:var(--btn-fg);border:0;border-radius:10px;padding:10px 44px 10px 14px;margin:0}\n.selwrap::after{content:\"\";position:absolute;right:19px;top:50%;width:9px;height:9px;border-right:2.75px solid var(--btn-fg);border-bottom:2.75px solid var(--btn-fg);border-radius:1px;transform:translateY(-70%) rotate(45deg);pointer-events:none}\n.seg{display:inline-flex;border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--panel)}\n.seg button{border:0;background:transparent;color:var(--fg);font:inherit;font-size:15px;padding:8px 12px}\n.seg button[aria-pressed=\"true\"]{background:var(--btn);color:var(--btn-fg)}\n.seg.step{display:flex;margin:0 0 8px}.seg.step button{flex:1;font-size:16px;font-weight:600;padding:10px 6px}\n.card .w{font-size:27px;line-height:1.5;font-weight:700;margin:8px 0 2px}\n.card .w.small{font-size:20px;margin-bottom:0}\n.info{font-size:14px;color:var(--muted);margin:0 2px 8px}\n.track{display:flex;align-items:flex-start;overflow-x:auto;overflow-y:hidden;transition:height .15s;scroll-snap-type:x mandatory;gap:10px;padding:4px 2px 10px;-webkit-overflow-scrolling:touch;scrollbar-width:none}\n.track::-webkit-scrollbar{display:none}\n.card{flex:0 0 88%;scroll-snap-align:center;background:var(--card);color:var(--card-fg);border-radius:14px;padding:16px 16px 12px;min-height:250px;display:flex;flex-direction:column}\n.card .sec{font-size:13.5px;color:var(--card-sub)}\n.card .lb{font-size:13.5px;font-weight:700;color:var(--gold);margin-top:2px}\n.card .q{font-size:15px;color:var(--card-sub);margin-top:6px}\n.card .t{font-size:25px;line-height:1.75;font-weight:600;flex:1;margin:8px 0}\n.card .t.long{font-size:20px}\n.card .t.mid{font-size:22px}\n.card .t.hide{color:var(--card-sub)}\n.card .b{font-size:14.5px;line-height:1.7;color:var(--card-sub);margin:0 0 8px}\n.card .m{font-size:13.5px;color:var(--time);display:flex;justify-content:space-between;gap:8px}\n.card .m .done{color:#9be3b2}.card .m .done.low{color:#ffb4ab}.card .m .done small{color:#a9adb6;font-size:12.5px;margin-left:4px}\n.pos{font-size:14px;color:var(--muted);text-align:center;margin:0 0 8px;font-variant-numeric:tabular-nums}\n.mic{display:block;width:100%;border:0;border-radius:16px;padding:18px 12px;font:inherit;font-size:20px;font-weight:700;background:var(--btn);color:var(--btn-fg);-webkit-user-select:none;user-select:none;-webkit-touch-callout:none;touch-action:manipulation}\n.mic.on{background:var(--rec);color:#fff}\n.mic:disabled{opacity:.45}\n.mic small{display:block;font-size:13.5px;font-weight:400;opacity:.8}\n.res{background:var(--panel);border-radius:14px;padding:12px 14px;margin:12px 0 0;border:1px solid var(--line)}\n.res h2{font-size:15px;margin:0 0 6px;color:var(--muted);font-weight:600}\n.res .said{font-size:19px;line-height:1.8;min-height:1.8em;word-break:break-all}\n.res .cmp{font-size:19px;line-height:1.8;word-break:break-all}\n.res .miss{background:var(--miss);color:var(--miss-fg);border-radius:3px}\n.res .kw{display:inline-block;border-radius:6px;padding:0 8px;margin:2px 6px 2px 0;background:var(--extra);color:var(--extra-fg)}\n.res .kw.miss{background:var(--miss);color:var(--miss-fg)}\n.res .lesson{margin-top:6px}\n.res .extra{background:var(--extra);color:var(--extra-fg);border-radius:3px}\n.stats{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:15px;margin-top:8px;font-variant-numeric:tabular-nums}\n.stats b{font-size:20px}\n.stats .good{color:var(--ok)}.stats .bad{color:var(--miss-fg)}\n.row{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}\n.row #save{background:var(--btn);color:var(--btn-fg);border-color:var(--btn);font-weight:700}\n.row button{border:1px solid var(--line);background:var(--panel);color:var(--fg);font:inherit;font-size:15px;border-radius:10px;padding:8px 12px}\n.log{margin:18px 0 0}\n.log h2{font-size:18px;margin:0 0 8px;padding-left:10px;border-left:4px solid var(--fg)}\n.log table{width:100%;border-collapse:collapse;font-size:15px;background:var(--panel);border-radius:12px;overflow:hidden;font-variant-numeric:tabular-nums}\n.log th,.log td{padding:7px 6px;border-bottom:1px solid var(--line);text-align:right}\n.log th:first-child,.log td:first-child{text-align:left}\n.log th{font-size:13px;color:var(--muted)}\n.note{font-size:13px;color:var(--muted);margin:14px 2px 0}\n.warn{background:var(--miss);color:var(--miss-fg);border-radius:10px;padding:10px 12px;font-size:15px;margin:0 0 10px}\nbutton:focus-visible,select:focus-visible{outline:3px solid var(--time);outline-offset:2px}\n</style>\n</head>\n<body>\n<header>\n <h1>ロボティクスの見直し</h1>\n <p id=\"sub\"></p>\n</header>\n<main>\n <div id=\"nojs\" class=\"warn\" style=\"font-size:17px;line-height:1.8\">このファイルは「ファイル」アプリのプレビューでは動きません(声の聞き取りとカードはブラウザで動きます)。<br><a href=\"https://pixelstomotors.github.io/robotics-review/\" style=\"color:inherit;font-weight:700\">Safari で開く → pixelstomotors.github.io/robotics-review</a></div>\n <div class=\"seg step\" role=\"group\" aria-label=\"段階\">\n <button id=\"s1\" type=\"button\" aria-pressed=\"true\">1 単語</button>\n <button id=\"s2\" type=\"button\" aria-pressed=\"false\">2 説明</button>\n <button id=\"s3\" type=\"button\" aria-pressed=\"false\">3 判断の型</button>\n </div>\n <div class=\"bar\">\n <span class=\"selwrap\"><select id=\"cat\" aria-label=\"カテゴリ\"></select></span>\n <div class=\"seg\" role=\"group\" aria-label=\"言い方\">\n <button id=\"mSee\" type=\"button\" aria-pressed=\"true\">見て言う</button>\n <button id=\"mHide\" type=\"button\" aria-pressed=\"false\">見ないで言う</button>\n </div>\n <div class=\"seg\" role=\"group\" aria-label=\"出す範囲\">\n <button id=\"fAll\" type=\"button\" aria-pressed=\"true\">全部</button>\n <button id=\"fWeak\" type=\"button\" aria-pressed=\"false\">苦手だけ</button>\n </div>\n </div>\n <div id=\"warn\" class=\"warn\" hidden></div>\n <p class=\"info\" id=\"info\"></p>\n <div class=\"track\" id=\"track\" aria-live=\"off\"></div>\n <p class=\"pos\" id=\"pos\"></p>\n <button id=\"mic\" class=\"mic\" type=\"button\">押している間だけ聞く<small>カードを見て話して、指を離す</small></button>\n <section class=\"res\" id=\"res\" aria-live=\"polite\">\n <h2>話した言葉</h2>\n <div class=\"said\" id=\"said\">ここに出ます</div>\n <h2 style=\"margin-top:10px\" id=\"cmpH\">違い(赤=言わなかった・青=余分)</h2>\n <div class=\"cmp\" id=\"cmp\">—</div>\n <div class=\"stats\" id=\"stats\"></div>\n </section>\n <section class=\"log\">\n <h2>今回の練習</h2>\n <table><thead><tr><th>文</th><th>一致</th><th>前回</th></tr></thead><tbody id=\"log\"><tr><td colspan=\"3\">まだありません</td></tr></tbody></table>\n <div class=\"row\"><button id=\"save\" type=\"button\">結果を保存</button><button id=\"copy\" type=\"button\">結果をコピー</button><button id=\"clear\" type=\"button\">今回の結果を消す</button></div>\n <div class=\"row\" id=\"dirRow\" hidden><button id=\"redir\" type=\"button\">保存先を選び直す</button><span id=\"dirName\" class=\"note\" style=\"margin:0;align-self:center\"></span></div>\n <p class=\"note\" id=\"saveNote\" style=\"margin-top:6px\"></p>\n </section>\n <p class=\"note\" id=\"ver\"></p>\n</main>\n<script>\n(() => {\n document.getElementById('nojs').remove(); // ブラウザで開けている(プレビューではない)\n const DATA = /*DATA*/null;\n const LABEL = Object.fromEntries(DATA.categories.map(c => [c.key, c.label]));\n const GOOD = 80; // 一致がこれ未満なら「苦手」\n\n const $ = id => document.getElementById(id);\n const track = $('track'), micBtn = $('mic');\n let step = 1, cat = 'all', mode = 'see', filter = 'all', cards = [], cur = 0, results = {}, tries = {};\n const rk = x => `${x.id}:${step === 3 ? 3 : step}`; // 結果は段階ごとに分けて覚える\n\n // 前回の一致(この端末のブラウザの中だけに覚える)\n const STORE = 'robotics-review-v2';\n let saved = {};\n try { saved = JSON.parse(localStorage.getItem(STORE) || '{}') || {}; } catch (_) { saved = {}; }\n const persist = () => { try { localStorage.setItem(STORE, JSON.stringify(saved)); } catch (_) {} };\n // 保存した練習記録の集計(iCloud の 練習記録 → _tools/practice_review.py → 書き出し時に埋め込む)。端末の記録より新しければこちらを使う\n const PROGRESS = /*PROGRESS*/{};\n Object.entries(PROGRESS).forEach(([k, v]) => { if (!saved[k] || !saved[k].at || saved[k].at < v.at) saved[k] = v; });\n let attempts = []; // 今回の練習の全部の回(記録ファイルに書く)\n\n // 声に出す文:要点はその文、用語・問いは答え\n const sayText = x => x.type === 'word' ? (step === 2 ? x.detail : x.back) : x.type === 'point' ? x.front : x.back;\n const termForms = w => { const m = w.match(/^(.*?)((.*))$/); if (!m) return [w]; const sub = m[2].split(/・|、/).map(t => t.trim()).filter(Boolean); return [m[1], ...sub, m[1] + m[2]]; };\n const targets = x => x.type === 'word' && step === 1 ? termForms(x.front).flatMap(t => [`${t}は、${x.back}`, `${t}、${x.back}`]) : [sayText(x)];\n const pool = () => DATA.items.filter(x => step === 3 ? x.type === 'lesson' : x.type === 'word');\n const esc = s => String(s).replace(/[&<>\"]/g, c => ({'&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;'}[c]));\n\n // カテゴリの選択肢(段階ごとに件数が変わる)\n function fillCats() {\n const P = pool();\n const cnt = k => P.filter(x => k === 'key' ? x.key : k === 'all' ? true : x.category === k).length;\n const keyLabel = step === 3 ? '核心だけ' : 'まず覚える';\n $('cat').innerHTML = [['all', '全部'], ['key', keyLabel]].concat(DATA.categories.map(c => [c.key, c.label]))\n .map(([k, l]) => `<option value=\"${k}\"${k === cat ? ' selected' : ''}>${esc(l)}(${cnt(k)})</option>`).join('');\n }\n $('sub').textContent = `単語 ${DATA.items.filter(x => x.type === 'word').length}・判断の型 ${DATA.items.filter(x => x.type === 'lesson').length}`;\n $('ver').textContent = `データ:${DATA.updated} 更新${Object.keys(PROGRESS).length ? `・記録の集計 ${Object.keys(PROGRESS).length}件を反映` : ''}`;\n\n function render() { // 出すカードを選び直して描く\n cards = pool().filter(x => cat === 'key' ? x.key : cat === 'all' ? true : x.category === cat)\n .filter(x => filter === 'all' || (saved[rk(x)] && saved[rk(x)].s < GOOD));\n draw();\n track.scrollLeft = 0; cur = 0; updatePos(); showResult();\n }\n function draw() { // 今のカードのまま描き直す(言い終えたあと)\n track.innerHTML = cards.map(x => {\n const say = sayText(x);\n const r = results[rk(x)], p = saved[rk(x)];\n const done = r ? `<span class=\"done${r.score < GOOD ? ' low' : ''}\">${r.n > 1 ? `${r.n}回目 ` : ''}一致 ${r.score}%</span>`\n : p ? `<span class=\"done${p.s < GOOD ? ' low' : ''}\">前回 ${p.s}%</span>` : '';\n const lb = x.key ? `<div class=\"lb\">${x.type === 'word' ? 'まず覚える' : '核心'}</div>` : '';\n const sec = x.type === 'word' || x.type === 'lesson' ? LABEL[x.category] : `${cat === 'key' || cat === 'all' ? LABEL[x.category] + '|' : ''}${x.sub}`;\n let q = '', t, cls = say.length > 60 ? ' long' : '', b = '', w = '';\n if (x.type === 'word') {\n const hidden = mode === 'hide' && !r;\n if (step === 1) { w = `<div class=\"w\">${esc(x.front)}</div>`; t = hidden ? 'どういう意味?' : say; cls = hidden ? ' mid hide' : ' mid'; }\n else { w = `<div class=\"w small\">${esc(x.front)}</div><div class=\"q\">${esc(x.back)}</div>`; t = hidden ? 'もう少し詳しく言うと?' : say; cls = hidden ? ' mid hide' : ' long'; }\n } else if (x.type === 'lesson') { // 場面を見て、教訓を自分の言葉で言う。実例は読むだけ\n const hidden = mode === 'hide' && !r;\n if (hidden) { t = x.front; cls = ' mid'; }\n else { q = `<div class=\"q\">${esc(x.front)}</div>`; t = x.back; cls = ''; b = `<div class=\"b\">${esc(x.detail)}</div>`; }\n } else if (x.type === 'point') {\n if (mode === 'hide' && !r) { t = say.slice(0, 10) + '…'; cls = ' hide'; } else t = say;\n if (mode === 'see' && x.back) b = `<div class=\"b\">${esc(x.back)}</div>`;\n } else {\n const ask = x.type === 'term' ? `「${x.front}」とは?` : x.front;\n if (mode === 'hide' && !r) { q = ''; t = ask; cls = x.front.length > 40 ? ' long' : ''; }\n else { q = `<div class=\"q\">${esc(ask)}</div>`; t = say; }\n }\n const kind = x.type === 'word' ? (x.src && step === 2 ? x.src : '') : x.type === 'lesson' ? x.src : `${x.type === 'point' ? '要点' : x.type === 'term' ? '用語' : '問い'}・${x.src}`;\n return `<div class=\"card\"><div class=\"sec\">${esc(sec)}</div>${lb}${w}${q}<div class=\"t${cls}\">${esc(t)}</div>${b}<div class=\"m\"><span>${esc(kind)}</span>${done}</div></div>`;\n }).join('') || '<div class=\"card\"><div class=\"t\">ここに出す項目がありません</div></div>';\n const keys = cards.filter(x => x.key).length;\n $('info').textContent = `${cards.length}枚${keys && cat !== 'key' ? `(${step === 3 ? '核心' : 'まず覚える'} ${keys})` : ''}${step === 1 ? '・用語から続けて言う' : step === 3 ? '・場面を見て、教訓を自分の言葉で' : ''}`;\n }\n\n function updatePos() {\n const el = track.children[0];\n if (!el || !cards.length) { $('pos').textContent = ''; micBtn.disabled = true; return; }\n const w = el.getBoundingClientRect().width + 10;\n cur = Math.min(cards.length - 1, Math.max(0, Math.round(track.scrollLeft / w)));\n $('pos').textContent = `${cur + 1} / ${cards.length}`;\n const cel = track.children[cur]; if (cel) track.style.height = (cel.offsetHeight + 14) + 'px'; // 今のカードの高さに合わせる(長いカードの分の空白を作らない)\n micBtn.disabled = false;\n }\n let posTimer;\n track.addEventListener('scroll', () => { clearTimeout(posTimer); posTimer = setTimeout(() => { const before = cur; updatePos(); if (before !== cur) showResult(); }, 80); }, {passive: true});\n\n // 文字の比較(台本読み合わせページと同じ。句読点と空白は無視し、記号は読み方にそろえる)\n const SYM = [['°', '度'], ['mm', 'ミリ'], ['cm', 'センチ'], ['px', 'ピクセル'], ['%', 'パーセント'], ['%', 'パーセント'], ['±', 'プラスマイナス'], ['×', 'かける'], ['=', 'は'], ['=', 'は'], ['→', ''], ['+', 'プラス'], ['+', 'プラス'], ['&', 'アンド'], ['&', 'アンド']];\n const sym = s => SYM.reduce((t, [a, b]) => t.split(a).join(b), s);\n const PUNCT = /[\\s、。,.,.!!??「」『』()()・…〜~::;;\"'“”‘’\\-―||//①②③④⑤⑥]/;\n const kata2hira = s => s.replace(/[\\u30A1-\\u30F6]/g, ch => String.fromCharCode(ch.charCodeAt(0) - 0x60));\n const half = ch => ch.replace(/[A-Za-z0-9]/g, c => String.fromCharCode(c.charCodeAt(0) - 0xFEE0));\n // 1文字ずつ「表示する字」と「比べる字」を持つ(カタカナ・ひらがな、全角・半角、大文字・小文字は同じとみなす)\n const norm = s => prep(s).map(c => c.k).join('');\n const prep = s => [...sym(s)].filter(ch => !PUNCT.test(ch)).map(ch => ({o: ch, k: kata2hira(half(ch).toLowerCase())}));\n // 音声認識の書き分け・聞き違い(同じ読み)は、カードの書き方にそろえてから比べる。一覧はデータの homophones(練習記録を見て増やす)\n const VARIANTS = DATA.homophones || [];\n const align = (script, said) => {\n script = script.toLowerCase(); said = said.replace(/\\s+/g, '').toLowerCase();\n said = VARIANTS.reduce((t, g) => { const k = g.find(v => script.includes(v)); return k ? g.filter(v => v !== k).reduce((u, v) => u.split(v).join(k), t) : t; }, said);\n return said.replace(/([ァ-ヶ]+)ー/g, (m, w) => script.includes(w + 'ー') || !script.includes(w) ? m : w); // シミュレーター→シミュレータ(カードに長音がないとき)\n };\n function diff(a, b) { // a=カード, b=話した(prep の配列)\n const n = a.length, m = b.length, dp = Array.from({length: n + 1}, () => new Uint16Array(m + 1));\n for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) dp[i][j] = a[i].k === b[j].k ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);\n const ops = []; let i = 0, j = 0;\n while (i < n && j < m) { if (a[i].k === b[j].k) { ops.push(['=', a[i].o]); i++; j++; } else if (dp[i + 1][j] >= dp[i][j + 1]) { ops.push(['-', a[i].o]); i++; } else { ops.push(['+', b[j].o]); j++; } }\n while (i < n) ops.push(['-', a[i++].o]); while (j < m) ops.push(['+', b[j++].o]);\n return {ops, same: dp[0][0]};\n }\n function opsHtml(ops) {\n let h = '', run = '', type = null;\n const flush = () => { if (!run) return; const t = esc(run); h += type === '=' ? t : `<span class=\"${type === '-' ? 'miss' : 'extra'}\">${t}</span>`; run = ''; };\n ops.forEach(([o, ch]) => { if (o !== type) { flush(); type = o; } run += ch; });\n flush(); return h;\n }\n\n function showResult() {\n const x = cards[cur];\n const r = x ? results[rk(x)] : null;\n if (!r) { $('cmpH').textContent = x && x.type === 'lesson' ? 'キーワード(赤=言えなかった)' : '違い(赤=言わなかった・青=余分)'; $('said').textContent = 'ここに出ます'; $('cmp').textContent = '—'; $('stats').innerHTML = ''; return; }\n $('said').textContent = r.raw || '(聞き取れませんでした)';\n $('cmpH').textContent = r.kws ? 'キーワード(赤=言えなかった)' : '違い(赤=言わなかった・青=余分)';\n $('cmp').innerHTML = r.kws ? r.kws.map(k => `<span class=\"kw${k.hit ? '' : ' miss'}\">${esc(k.w)}</span>`).join('') + `<div class=\"lesson\">${esc(x.back)}</div>` : opsHtml(r.ops);\n const p = saved[rk(x)] && saved[rk(x)].prev;\n $('stats').innerHTML = `<span>${r.n}回目</span><span>一致 <b class=\"${r.score >= GOOD ? 'good' : 'bad'}\">${r.score}%</b>${p != null ? `(前回 ${p}%)` : ''}</span>`;\n }\n\n function renderLog() {\n const done = Object.entries(results);\n const STEP = {1: '単語', 2: '説明', 3: '型'};\n $('log').innerHTML = done.length ? done.map(([k, r]) => { const p = saved[k] && saved[k].prev; const s = r.x.type === 'lesson' ? r.x.back : r.x.front;\n const sv = r.ai < savedIdx ? ' <small style=\"color:var(--ok)\">保存済</small>' : '';\n return `<tr><td>${STEP[r.st]}|${esc(s.slice(0, 14))}${s.length > 14 ? '…' : ''}${sv}</td><td>${r.score}%${r.n > 1 ? `<br><small>${r.n}回目</small>` : ''}</td><td>${p != null ? p + '%' : '—'}</td></tr>`; }).join('')\n : '<tr><td colspan=\"3\">まだありません</td></tr>';\n }\n\n\n // 音声認識(押している間だけ)\n const SR = window.SpeechRecognition || window.webkitSpeechRecognition;\n const IOS = /iPhone|iPad|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);\n let rec = null, listening = false, finalText = '', interim = '', t0 = 0, target = null, restarts = 0;\n if (!SR) { micBtn.disabled = true; showWarn('このブラウザでは声を文字にできません。iPhoneはSafari、PCはChromeかSafariで開いてください。'); }\n function makeRec() {\n const r = new SR(); r.lang = 'ja-JP'; r.continuous = true; r.interimResults = true;\n r.onresult = e => { if (r !== rec) return; interim = ''; for (let i = e.resultIndex; i < e.results.length; i++) { const tx = e.results[i][0].transcript; if (e.results[i].isFinal) finalText += tx; else interim += tx; } $('said').textContent = (finalText + interim) || '…'; };\n r.onerror = e => { if (e.error === 'not-allowed' || e.error === 'service-not-allowed') showWarn('マイクが許可されていません。ブラウザの設定でマイクを許可してください。'); };\n r.onend = () => {\n if (r !== rec) return;\n if (listening && restarts < 3) { restarts++; finalText += interim; interim = ''; rec = makeRec(); try { rec.start(); } catch (_) {} }\n else finish();\n };\n return r;\n }\n // PC:iPhoneと同じ「始まり」「終わり」の音を鳴らす\n let actx = null;\n function beep(up) {\n if (IOS) return;\n try {\n actx = actx || new (window.AudioContext || window.webkitAudioContext)();\n if (actx.state === 'suspended') actx.resume();\n const t = actx.currentTime + 0.01;\n (up ? [660, 990] : [990, 660]).forEach((f, k) => {\n const o = actx.createOscillator(), g = actx.createGain();\n o.type = 'sine'; o.frequency.value = f;\n g.gain.setValueAtTime(0.0001, t + k * 0.09); g.gain.exponentialRampToValueAtTime(0.25, t + k * 0.09 + 0.012); g.gain.exponentialRampToValueAtTime(0.0001, t + k * 0.09 + 0.08);\n o.connect(g).connect(actx.destination); o.start(t + k * 0.09); o.stop(t + k * 0.09 + 0.09);\n });\n } catch (_) {}\n }\n const PRE = IOS ? 0 : 200;\n function start(ev) {\n if (micBtn.disabled || listening || !SR) return;\n ev.preventDefault();\n updatePos(); // スワイプ・←→の直後でも、今見えているカードを対象にする\n target = cards[cur]; if (!target) return;\n finalText = ''; interim = ''; listening = true; t0 = performance.now(); restarts = 0;\n micBtn.classList.add('on'); micBtn.firstChild.textContent = '聞いています…';\n $('said').textContent = '…'; $('cmp').textContent = '—'; $('stats').innerHTML = '';\n beep(true);\n try { if (rec) rec.abort(); } catch (_) {}\n rec = makeRec();\n const go = () => { if (!listening) return; try { rec.start(); } catch (_) {} };\n PRE ? setTimeout(go, PRE) : go();\n }\n function stop(ev) {\n if (!listening) return;\n if (ev) ev.preventDefault();\n listening = false; target.sec = (performance.now() - t0) / 1000;\n micBtn.classList.remove('on'); micBtn.firstChild.textContent = '押している間だけ聞く';\n setTimeout(() => { try { rec.stop(); } catch (_) { finish(); } beep(false); }, 250); // 言い終わりを取りこぼさない\n }\n function finish() {\n if (!target) return;\n const x = target; target = null;\n const raw = (finalText + interim).trim();\n if (!raw || x.sec < 0.6) { $('said').textContent = !raw ? '聞き取れませんでした。押したまま話して、言い終わってから離してください' : '押す時間が短すぎました'; return; }\n // 1 単語は「用語+意味」を一続きで言う。用語はカッコの前後どちらで言ってもよく、「は」は無くてもよい(いちばん合う言い方で比べる)\n let best = null;\n if (x.type === 'lesson') { // 教訓はキーワードが言えたかで見る(言い方は自由)\n const s = norm(raw);\n const kws = x.kw.map(g => ({w: g[0], hit: g.some(v => s.includes(norm(v)))}));\n best = {say: x.back, kws, ops: null, score: Math.round(kws.filter(k => k.hit).length / kws.length * 100)};\n } else targets(x).forEach(t => { const a = prep(t), b = prep(align(t, raw)); const d = diff(a, b); const sc = a.length ? Math.round(d.same / Math.max(a.length, b.length) * 100) : 0; if (!best || sc > best.score) best = {say: t, ops: d.ops, score: sc}; });\n const {say, ops, score, kws} = best;\n const key = rk(x);\n (tries[key] = tries[key] || []).push(score);\n const at = stamp();\n results[key] = {raw, ops, kws, score, n: tries[key].length, x, st: step, ai: attempts.length}; // ai:この回が attempts の何番目か(保存済みの表示用)\n if (tries[key].length === 1) saved[key] = {s: score, prev: saved[key] ? saved[key].s : null, at};\n else saved[key] = {s: score, prev: saved[key] ? saved[key].prev : null, at};\n persist();\n attempts.push({id: x.id, step, word: x.type === 'lesson' ? x.back : x.front, card: say, said: raw, score, sec: Math.round(x.sec * 10) / 10, at, ...(kws ? {miss: kws.filter(k => !k.hit).map(k => k.w)} : {})});\n unsaved++; saveLabel();\n const keep = track.scrollLeft; draw(); track.scrollLeft = keep; updatePos(); showResult(); renderLog();\n }\n micBtn.addEventListener('touchstart', start, {passive: false});\n micBtn.addEventListener('touchend', stop, {passive: false});\n micBtn.addEventListener('touchcancel', stop, {passive: false});\n micBtn.addEventListener('mousedown', start);\n window.addEventListener('mouseup', () => stop());\n micBtn.addEventListener('contextmenu', e => e.preventDefault());\n // PC:スペースを押している間だけ聞く/←→でカードを移動\n const typing = e => /INPUT|TEXTAREA|SELECT/.test((e.target || {}).tagName || '');\n window.addEventListener('keydown', e => {\n if (typing(e)) return;\n if (e.code === 'Space') { e.preventDefault(); if (!e.repeat) start(e); }\n else if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {\n e.preventDefault(); const el = track.children[0]; if (!el || listening) return;\n const w = el.getBoundingClientRect().width + 10;\n track.scrollTo({left: Math.round(track.scrollLeft / w + (e.key === 'ArrowRight' ? 1 : -1)) * w, behavior: 'smooth'});\n }\n });\n window.addEventListener('keyup', e => { if (e.code === 'Space' && !typing(e)) { e.preventDefault(); stop(e); if (document.activeElement && document.activeElement.blur) document.activeElement.blur(); } });\n window.addEventListener('blur', () => stop());\n if (!IOS) micBtn.querySelector('small').textContent = 'スペースキーでも同じ。←→でカードを移動';\n\n function showWarn(t) { const w = $('warn'); w.textContent = t; w.hidden = false; }\n\n const press = (on, off, set) => { $(on).setAttribute('aria-pressed', true); $(off).setAttribute('aria-pressed', false); set(); render(); };\n $('mSee').addEventListener('click', () => { press('mSee', 'mHide', () => mode = 'see'); keepView(); });\n $('mHide').addEventListener('click', () => { press('mHide', 'mSee', () => mode = 'hide'); keepView(); });\n $('fAll').addEventListener('click', () => press('fAll', 'fWeak', () => filter = 'all'));\n $('fWeak').addEventListener('click', () => press('fWeak', 'fAll', () => filter = 'weak'));\n const PREF = 'robotics-review-view';\n const keepView = () => { try { localStorage.setItem(PREF, JSON.stringify({step, cat, mode})); } catch (_) {} };\n const setStep = n => { step = n; [1, 2, 3].forEach(k => $('s' + k).setAttribute('aria-pressed', k === n)); fillCats(); render(); keepView(); };\n [1, 2, 3].forEach(k => $('s' + k).addEventListener('click', () => setStep(k)));\n $('cat').addEventListener('change', e => { cat = e.target.value; render(); keepView(); e.target.blur(); });\n $('clear').addEventListener('click', () => { results = {}; tries = {}; attempts = []; unsaved = 0; savedIdx = 0; saveLabel(); draw(); showResult(); renderLog(); });\n\n // ===== 記録の保存(台本読み合わせページと同じ流れ。保存先は iCloud の ロボティクス復習/練習記録) =====\n function stamp() { const d = new Date(), z = n => String(n).padStart(2, '0'); const o = -d.getTimezoneOffset(); return `${d.getFullYear()}-${z(d.getMonth() + 1)}-${z(d.getDate())}T${z(d.getHours())}:${z(d.getMinutes())}:${z(d.getSeconds())}${o >= 0 ? '+' : '-'}${z(Math.floor(Math.abs(o) / 60))}:${z(Math.abs(o) % 60)}`; }\n let unsaved = 0, savedIdx = 0; // savedIdx:保存済みの回の数(次の保存はそれより後の回だけ送る)\n const saveLabel = () => { $('save').textContent = unsaved ? `結果を保存(未保存 ${unsaved}回)` : attempts.length ? `保存済み(${attempts.length}回)` : '結果を保存'; };\n const STEPNAME = {1: '1 単語', 2: '2 説明', 3: '3 判断の型'};\n function report(all) {\n const list = all ? attempts : attempts.slice(savedIdx);\n const d = new Date(); const ts = `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;\n let s = `# ロボティクス見直しの記録(${ts})\\n\\n${list.length}回・データ ${DATA.updated}\\n\\n`;\n list.forEach(a => { s += `## ${STEPNAME[a.step]}|${a.word}\\n- カード:${a.card}\\n- 話した:${a.said}\\n- 一致 ${a.score}%/${a.sec}秒\\n\\n`; });\n s += '<!-- 集計用(_tools/practice_review.py が読む) -->\\n```json\\n' + JSON.stringify({page: 'robotics-review', data: DATA.updated, at: stamp(), tries: list}) + '\\n```\\n';\n return s;\n }\n const fileName = () => { const d = new Date(), z = n => String(n).padStart(2, '0'); return `ロボ見直し_${d.getFullYear()}${z(d.getMonth() + 1)}${z(d.getDate())}_${z(d.getHours())}${z(d.getMinutes())}${z(d.getSeconds())}.md`; };\n const doneSaving = msg => { unsaved = 0; savedIdx = attempts.length; renderLog(); $('save').textContent = msg; setTimeout(saveLabel, 2500); };\n function download() {\n const b = new Blob([report()], {type: 'application/octet-stream'}); const a = document.createElement('a');\n a.href = URL.createObjectURL(b); a.download = fileName(); document.body.appendChild(a); a.click();\n setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 1500);\n doneSaving('保存しました(ダウンロード)');\n }\n // PC(Chrome・Edge):一度選んだ「練習記録」フォルダへ直接書き込む(選んだフォルダはこのブラウザの中だけに覚える)\n const CAN_DIR = !IOS && 'showDirectoryPicker' in window;\n const idb = (mode, fn) => new Promise((ok, ng) => { try { const q = indexedDB.open('robotics-review', 1); q.onupgradeneeded = () => q.result.createObjectStore('kv'); q.onerror = () => ng(q.error);\n q.onsuccess = () => { try { const t = q.result.transaction('kv', mode); const r = fn(t.objectStore('kv')); t.oncomplete = () => ok(r && r.result); t.onerror = () => ng(t.error); } catch (e) { ng(e); } }; } catch (e) { ng(e); } });\n const getDir = () => idb('readonly', st => st.get('dir')).catch(() => null);\n const putDir = h => idb('readwrite', st => st.put(h, 'dir')).catch(() => null);\n const showDirName = h => { $('dirName').textContent = h ? `保存先:${h.name}` : '保存先:まだ選んでいません'; };\n async function pickDir() {\n const h = await window.showDirectoryPicker({id: 'robo-renshu', mode: 'readwrite'});\n if (h.name !== '練習記録') showWarn(`選んだのは「${h.name}」です。iCloud Drive → PixelstoMotors → ロボティクス復習 → 練習記録 を選んでください。`); else $('warn').hidden = true;\n await putDir(h); showDirName(h); return h;\n }\n // iPhone:共有の画面から「\"ファイル\"に保存」→ ロボティクス復習 → 練習記録(2回目からは同じ場所が出る)\n const SHORTCUT = 'ロボ見直しを保存';\n async function shareSave() {\n const f = new File([report()], fileName(), {type: 'text/markdown'});\n if (navigator.canShare && navigator.canShare({files: [f]})) { await navigator.share({files: [f]}); doneSaving('保存しました'); return true; }\n return false;\n }\n $('save').addEventListener('click', async () => {\n if (attempts.length <= savedIdx) { $('save').textContent = attempts.length ? '保存済みです' : 'まだ記録がありません'; setTimeout(saveLabel, 1800); return; }\n if (IOS) { // iPhone:記録をコピーして、ショートカット「ロボ見直しを保存」に渡す(練習記録フォルダに直接入る)\n try { await navigator.clipboard.writeText(report()); doneSaving('ショートカットで保存中'); location.href = 'shortcuts://run-shortcut?name=' + encodeURIComponent(SHORTCUT) + '&input=clipboard'; return; } catch (_) {}\n try { if (await shareSave()) return; } catch (e) { if (e && e.name === 'AbortError') return; } return download();\n }\n if (!CAN_DIR) return download();\n try {\n let h = await getDir();\n if (h && (await h.queryPermission({mode: 'readwrite'})) !== 'granted' && (await h.requestPermission({mode: 'readwrite'})) !== 'granted') h = null;\n if (!h) { showWarn('保存先のフォルダを選んでください:iCloud Drive → PixelstoMotors → ロボティクス復習 → 練習記録(最初の1回だけ)'); h = await pickDir(); }\n const w = await (await h.getFileHandle(fileName(), {create: true})).createWritable(); await w.write(report()); await w.close();\n doneSaving(`${h.name}に保存しました`);\n } catch (e) {\n if (e && e.name === 'AbortError') { $('warn').hidden = true; return; }\n showWarn('練習記録に書き込めなかったので、ダウンロードに保存しました。'); download();\n }\n });\n $('copy').addEventListener('click', async () => { try { await navigator.clipboard.writeText(report(true)); $('copy').textContent = 'コピーしました'; } catch (_) { $('copy').textContent = 'コピーできませんでした'; } setTimeout(() => $('copy').textContent = '結果をコピー', 1800); });\n if (CAN_DIR) { $('dirRow').hidden = false; $('redir').addEventListener('click', () => pickDir().catch(() => {})); getDir().then(showDirName); }\n if (IOS) { const b = document.createElement('button'); b.type = 'button'; b.id = 'share'; b.textContent = '共有から保存';\n b.addEventListener('click', async () => { if (attempts.length <= savedIdx) return; try { if (!(await shareSave())) download(); } catch (e) { if (!(e && e.name === 'AbortError')) download(); } });\n $('clear').before(b); }\n $('saveNote').textContent = IOS ? '' : CAN_DIR ? '' : '保存はダウンロードに入ります。練習記録に直接入れたいときはChromeで開いてください';\n window.addEventListener('beforeunload', e => { if (unsaved) { e.preventDefault(); e.returnValue = ''; } });\n // 前に開いていた段階・カテゴリ・言い方から始める(この端末の中だけ)\n try { const v = JSON.parse(localStorage.getItem(PREF) || 'null'); if (v) { cat = v.cat || 'all'; if (v.mode === 'hide') { mode = 'hide'; $('mHide').setAttribute('aria-pressed', true); $('mSee').setAttribute('aria-pressed', false); } setStep(v.step || 1); } else setStep(1); } catch (_) { setStep(1); }\n})();\n</script>\n</body>\n</html></code></pre><h2 id=\"index_xrC6Iabk\">ページを書き出す build_review.py の全文</h2><p>python</p><pre><code class=\"language-python\">#!/usr/bin/env python3\n\"\"\"robotics_review.json(データ)から ロボティクス復習.html(声に出して見直すページ)を作る。\n\n使い方(このファイルがある _tools の1つ上のフォルダで):\n python3 _tools/practice_review.py # 練習記録があれば先に集計\n python3 _tools/build_review.py\n\n- ページの形は _tools/template.html(仕事で使っている「台本読み合わせ」ページと同じ作り)。データはページの中に埋め込む\n- 書き出した ロボティクス復習.html は、GitHub の PixelstoMotors/robotics-review に index.html として置く\n (声を文字にするには https が要るため。公開URL https://pixelstomotors.github.io/robotics-review/)\n\nデータの形(items の1件):\n {\"id\": \"r001\", \"category\": \"kinematics\", \"sub\": \"小見出し\",\n \"type\": \"word\" | \"lesson\",\n \"front\": \"用語 / 場面(問い)\", \"back\": \"短い意味 / 教訓(声に出す一言)\",\n \"detail\": \"詳しい説明 / 実例(読むだけ)\", \"kw\": [[\"座標系\",\"座標\"],[\"そろえる\",\"揃える\"]](lesson だけ),\n \"key\": true/false, \"src\": \"DAY163\"}\n word は「1 単語」(用語+back を一続きで言う)と「2 説明」(detail を言う)に出る。key は「まず覚える」。\n lesson は「3 判断の型」に出る。場面を見て教訓を自分の言葉で言い、kw の各組から1語ずつ言えていれば合格。key は「核心」。\n id は一度付けたら変えない(ページが「前回の一致」をこの id で覚えている)。新しい項目は末尾の番号の次を付ける。\n\"\"\"\nimport json\nimport sys\nfrom pathlib import Path\n\nROOT = Path(__file__).resolve().parent.parent\nDATA = ROOT / \"robotics_review.json\"\nTEMPLATE = Path(__file__).resolve().parent / \"template.html\"\nOUT = ROOT / \"ロボティクス復習.html\"\nPROGRESS = ROOT / \"練習記録_集計.json\" # _tools/practice_review.py が作る(無ければ空)\n\n\ndef src_label(s):\n return \"・\".join(p.strip() for p in (s or \"\").split(\",\") if p.strip())\n\n\ndef build():\n data = json.loads(DATA.read_text(encoding=\"utf-8\"))\n cats = {c[\"key\"] for c in data[\"categories\"]}\n ids = [x.get(\"id\") for x in data[\"items\"]]\n bad = [x.get(\"id\") for x in data[\"items\"]\n if x.get(\"category\") not in cats or x.get(\"type\") not in (\"word\", \"lesson\", \"point\", \"term\", \"qa\")\n or not x.get(\"front\") or (x.get(\"type\") != \"point\" and not x.get(\"back\"))\n or (x.get(\"type\") in (\"word\", \"lesson\") and not x.get(\"detail\"))\n or (x.get(\"type\") == \"lesson\" and not x.get(\"kw\"))]\n if bad:\n sys.exit(f\"category / type / front / back が不正な項目: {bad}\")\n dup = {i for i in ids if ids.count(i) > 1}\n if dup or None in ids:\n sys.exit(f\"id が重複または空: {sorted(i for i in dup if i)}\")\n\n page = {\n \"updated\": data.get(\"updated\", \"\"),\n \"categories\": data[\"categories\"],\n \"homophones\": data.get(\"homophones\", []),\n \"items\": [{\"id\": x[\"id\"], \"category\": x[\"category\"], \"sub\": x.get(\"sub\", \"\"), \"type\": x[\"type\"],\n \"front\": x[\"front\"], \"back\": x.get(\"back\", \"\"), \"detail\": x.get(\"detail\", \"\"), \"key\": bool(x.get(\"key\")),\n \"src\": src_label(x.get(\"src\")), **({\"kw\": x[\"kw\"]} if x.get(\"kw\") else {})} for x in data[\"items\"]],\n }\n js = json.dumps(page, ensure_ascii=False, separators=(\",\", \":\")).replace(\"</\", \"<\\\\/\")\n html = TEMPLATE.read_text(encoding=\"utf-8\")\n if \"/*DATA*/null\" not in html:\n sys.exit(\"template.html に /*DATA*/null が見つかりません\")\n prog = json.loads(PROGRESS.read_text(encoding=\"utf-8\")) if PROGRESS.exists() else {}\n pj = json.dumps(prog, ensure_ascii=False, separators=(\",\", \":\")).replace(\"</\", \"<\\\\/\")\n if \"/*PROGRESS*/{}\" not in html:\n sys.exit(\"template.html に /*PROGRESS*/{} が見つかりません\")\n OUT.write_text(html.replace(\"/*DATA*/null\", js).replace(\"/*PROGRESS*/{}\", pj), encoding=\"utf-8\")\n print(f\"{OUT.name} を書き出しました({len(page['items'])}項目・練習記録の集計 {len(prog)}件)\")\n\n\nif __name__ == \"__main__\":\n build()</code></pre><p></p><h2 id=\"index_54fjEQ2p\">練習記録を集計する practice_review.py の全文</h2><p>python</p><pre><code class=\"language-python\">#!/usr/bin/env python3\n\"\"\"練習記録(ページの「結果を保存」で 練習記録/ に入った .md)を集計する。\n\n使い方(このファイルがある _tools の1つ上のフォルダで):\n python3 _tools/practice_review.py # 集計と振り返りを作り直す\n python3 _tools/build_review.py # 集計をページに入れて書き出す\n\n作るもの(毎回作り直す):\n- 練習記録_集計.json … カード×段階ごとの最新の一致など。build_review.py がページに埋め込み、\n どの端末で開いても「前回」「苦手だけ」がそろう\n- 練習記録_振り返り.md … 苦手・伸びた・よく抜ける言葉・まだ練習していない「まず覚える」語。\n Claude はこれを読んで、カードの言い方や順番を直す\n仕事で使っている「台本読み合わせ」ページの集計スクリプトと同じ役割。\n\"\"\"\nimport json\nimport re\nimport unicodedata\nfrom collections import Counter, defaultdict\nfrom pathlib import Path\n\nROOT = Path(__file__).resolve().parent.parent\nREC_DIR = ROOT / \"練習記録\"\nDATA = ROOT / \"robotics_review.json\"\nOUT_JSON = ROOT / \"練習記録_集計.json\"\nOUT_MD = ROOT / \"練習記録_振り返り.md\"\nGOOD = 80 # これ未満は「苦手」(ページと同じ)\nMASTER = 90 # 直近2回がこれ以上なら「身についた」\nSTEP = {1: \"1 単語\", 2: \"2 説明\", 3: \"3 判断の型\"}\n\n# ページと同じ比べ方(記号は読み方に、句読点・空白は無視、カナ・全半角・大小は同じとみなす)\nSYM = [(\"°\", \"度\"), (\"mm\", \"ミリ\"), (\"cm\", \"センチ\"), (\"px\", \"ピクセル\"), (\"%\", \"パーセント\"), (\"%\", \"パーセント\"),\n (\"&\", \"アンド\"), (\"&\", \"アンド\"), (\"±\", \"プラスマイナス\"), (\"×\", \"かける\"), (\"=\", \"は\"), (\"=\", \"は\"), (\"→\", \"\"), (\"+\", \"プラス\"), (\"+\", \"プラス\")]\nPUNCT = re.compile(r\"[\\s、。,.,.!!??「」『』()()・…〜~::;;\\\"'“”‘’\\-―||//①②③④⑤⑥]\")\n\n\ndef prep(s):\n for a, b in SYM:\n s = s.replace(a, b)\n out = []\n for ch in s:\n if PUNCT.match(ch):\n continue\n k = unicodedata.normalize(\"NFKC\", ch).lower()\n if \"ァ\" <= k <= \"ヶ\":\n k = chr(ord(k) - 0x60)\n out.append((ch, k))\n return out\n\n\nHOMO = [] # データの homophones(main で読む)\nSKIPPED = [] # 読めなかった記録ファイル\n\n\ndef align(script, said):\n \"\"\"ページと同じ:聞き違い・書き分けをカードの書き方にそろえる\"\"\"\n script, said = script.lower(), re.sub(r\"\\s+\", \"\", said).lower()\n for g in HOMO:\n k = next((v for v in g if v in script), None)\n if k:\n for v in g:\n if v != k:\n said = said.replace(v, k)\n return re.sub(r\"([ァ-ヶ]+)ー\", lambda m: m.group(0) if (m.group(1) + \"ー\") in script or m.group(1) not in script else m.group(1), said)\n\n\ndef lcs(a, b):\n n, m = len(a), len(b)\n dp = [[0] * (m + 1) for _ in range(n + 1)]\n for i in range(n - 1, -1, -1):\n for j in range(m - 1, -1, -1):\n dp[i][j] = dp[i + 1][j + 1] + 1 if a[i][1] == b[j][1] else max(dp[i + 1][j], dp[i][j + 1])\n return dp[0][0]\n\n\ndef rescore(card, said):\n \"\"\"今の比べ方で採点し直す(聞き違いの一覧を増やしたら、過去の記録にも効く)\"\"\"\n a, b = prep(card), prep(align(card, said))\n return round(lcs(a, b) / max(len(a), len(b)) * 100) if a else 0\n\n\ndef targets(x, step, card):\n \"\"\"ページと同じ:1 単語は「用語(カッコの前後どちらでも)+は+意味」の言い方すべてで比べる\"\"\"\n if x[\"type\"] != \"word\" or step != 1:\n return [card]\n m = re.match(r\"^(.*?)((.*))$\", x[\"front\"])\n forms = [x[\"front\"]] if not m else [m.group(1), *[t.strip() for t in re.split(r\"・|、\", m.group(2)) if t.strip()], m.group(1) + m.group(2)]\n return [f\"{f}{sep}{x['back']}\" for f in forms for sep in (\"は、\", \"、\")]\n\n\ndef missing_runs(card, said):\n \"\"\"カードにあって話さなかった部分(2文字以上のかたまり)\"\"\"\n a, b = prep(card), prep(align(card, said))\n n, m = len(a), len(b)\n dp = [[0] * (m + 1) for _ in range(n + 1)]\n for i in range(n - 1, -1, -1):\n for j in range(m - 1, -1, -1):\n dp[i][j] = dp[i + 1][j + 1] + 1 if a[i][1] == b[j][1] else max(dp[i + 1][j], dp[i][j + 1])\n runs, cur, i, j = [], \"\", 0, 0\n while i < n and j < m:\n if a[i][1] == b[j][1]:\n if cur: runs.append(cur); cur = \"\"\n i += 1; j += 1\n elif dp[i + 1][j] >= dp[i][j + 1]:\n cur += a[i][0]; i += 1\n else:\n j += 1\n cur += \"\".join(c for c, _ in a[i:])\n if cur: runs.append(cur)\n return [r for r in runs if len(r) >= 2]\n\n\ndef load_tries():\n seen, tries, files = set(), [], 0\n # .md:ページから保存したもの/.txt:iPhone のショートカット「ロボ見直しを保存」で入ったもの(名前は問わない)\n for f in sorted([*REC_DIR.glob(\"*.md\"), *REC_DIR.glob(\"*.txt\")]) if REC_DIR.exists() else []:\n try:\n text = f.read_text(encoding=\"utf-8\")\n except OSError: # iCloud でまだダウンロードされていないファイル(Mac の「ファイル」で開くか、少し待つと読める)\n SKIPPED.append(f.name)\n continue\n m = re.search(r\"```json\\s*(\\{.*?\\})\\s*```\", text, re.S)\n if not m:\n continue\n try:\n rec = json.loads(m.group(1))\n except json.JSONDecodeError:\n continue\n if rec.get(\"page\") != \"robotics-review\":\n continue\n files += 1\n for t in rec.get(\"tries\", []):\n k = (t.get(\"id\"), t.get(\"step\"), t.get(\"at\"))\n if None in k or k in seen:\n continue\n seen.add(k)\n tries.append(t)\n tries.sort(key=lambda t: t[\"at\"])\n return tries, files\n\n\ndef main():\n data = json.loads(DATA.read_text(encoding=\"utf-8\"))\n items = {x[\"id\"]: x for x in data[\"items\"]}\n HOMO[:] = data.get(\"homophones\", [])\n tries, files = load_tries()\n by = defaultdict(list)\n words = [x for x in data[\"items\"] if x[\"type\"] == \"word\"]\n fixed, wrongcard = [], []\n for t in tries:\n if t[\"id\"] not in items:\n continue\n if \"miss\" not in t: # キーワード採点(判断の型)以外は、今の比べ方で採点し直す\n new = max(rescore(c, t[\"said\"]) for c in targets(items[t[\"id\"]], t[\"step\"], t[\"card\"]))\n if new != t[\"score\"]:\n fixed.append((t, t[\"score\"], new))\n t[\"score\"] = new\n # 別のカードを読んだ記録(カードの切り替え直後に話したなど)は集計から外す\n if t[\"step\"] == 1 and t[\"score\"] < 50:\n other = max((x for x in words if x[\"id\"] != t[\"id\"]), key=lambda x: rescore(x[\"back\"], t[\"said\"]))\n if rescore(other[\"back\"], t[\"said\"]) >= 70:\n wrongcard.append((t, other))\n continue\n by[f'{t[\"id\"]}:{t[\"step\"]}'].append(t)\n\n progress = {}\n for k, ts in by.items():\n progress[k] = {\"s\": ts[-1][\"score\"], \"prev\": ts[-2][\"score\"] if len(ts) > 1 else None,\n \"at\": ts[-1][\"at\"], \"n\": len(ts), \"best\": max(t[\"score\"] for t in ts)}\n OUT_JSON.write_text(json.dumps(progress, ensure_ascii=False, indent=1), encoding=\"utf-8\")\n\n def label(k):\n i, st = k.split(\":\")\n x = items[i]\n return f'{STEP[int(st)]}|{x[\"back\"] if x[\"type\"] == \"lesson\" else x[\"front\"]}'\n\n weak = sorted([k for k, p in progress.items() if p[\"s\"] < GOOD], key=lambda k: progress[k][\"s\"])\n mastered = [k for k, ts in by.items() if len(ts) >= 2 and ts[-1][\"score\"] >= MASTER and ts[-2][\"score\"] >= MASTER]\n grew = sorted([k for k, ts in by.items() if len(ts) >= 2 and ts[-1][\"score\"] - ts[0][\"score\"] >= 15],\n key=lambda k: by[k][0][\"score\"] - by[k][-1][\"score\"])\n stuck = [k for k, ts in by.items() if len(ts) >= 3 and max(t[\"score\"] for t in ts[-3:]) < GOOD]\n miss = Counter()\n for k in weak:\n t = by[k][-1]\n miss.update(t[\"miss\"] if \"miss\" in t else missing_runs(t[\"card\"], t[\"said\"]))\n keywords = [x for x in data[\"items\"] if x[\"type\"] == \"word\" and x.get(\"key\")]\n untouched = [x for x in keywords if f'{x[\"id\"]}:1' not in progress]\n\n L0 = []\n if wrongcard:\n L0 += [\"## 別のカードの文を読んでいた記録(集計から外した)\", \"\"]\n L0 += [f'- 「{items[t[\"id\"]][\"front\"]}」の回に、「{o[\"front\"]}」の文を話していた({t[\"at\"][5:16].replace(\"T\", \" \")})' for t, o in wrongcard]\n L0.append(\"\")\n if fixed:\n L0 += [f\"## 聞き違いの補正で採点し直した記録:{len(fixed)}回\", \"\"]\n L0 += [f'- {items[t[\"id\"]][\"front\"]}:{a}% → {b}%(話した:{t[\"said\"]})' for t, a, b in sorted(fixed, key=lambda z: z[1] - z[2])[:15]]\n L0.append(\"\")\n L = [\"# ロボティクス見直し 練習の振り返り\", \"\",\n f\"記録ファイル {files}・練習 {len(tries)}回・練習したカード {len(progress)}(カード×段階)\", \"\",\n \"## 数字\", \"\",\n f\"- 苦手(最新の一致が{GOOD}%未満):{len(weak)}\",\n f\"- 身についた(直近2回とも{MASTER}%以上):{len(mastered)}\",\n f\"- 3回続けて{GOOD}%に届かない:{len(stuck)}\",\n f\"- まだ1回も言っていない「まず覚える」語:{len(untouched)} / {len(keywords)}\", \"\"]\n if stuck:\n L += [\"## 3回続けて届かない(カードの言い方を見直す候補)\", \"\"]\n L += [f'- {label(k)}:{\" → \".join(str(t[\"score\"]) for t in by[k][-3:])}%(カード:{by[k][-1][\"card\"]})' for k in stuck]\n L.append(\"\")\n if weak:\n L += [\"## 苦手(低い順)\", \"\"]\n L += [f'- {label(k)}:{progress[k][\"s\"]}%({progress[k][\"n\"]}回)' for k in weak[:40]]\n L.append(\"\")\n if miss:\n L += [\"## よく抜ける言葉・キーワード(苦手カードの最新の回から)\", \"\"]\n L += [f\"- {w}({c}回)\" for w, c in miss.most_common(20)]\n L.append(\"\")\n if grew:\n L += [\"## 伸びた(最初から15ポイント以上)\", \"\"]\n L += [f'- {label(k)}:{by[k][0][\"score\"]}% → {by[k][-1][\"score\"]}%' for k in grew[:20]]\n L.append(\"\")\n if untouched:\n L += [\"## まだ言っていない「まず覚える」語\", \"\"]\n L += [\"- \" + \"、\".join(x[\"front\"] for x in untouched), \"\"]\n sc = [t[\"score\"] for ts in by.values() for t in ts]\n if sc:\n L[2:2] = [f\"平均の一致 {round(sum(sc) / len(sc))}%・90%以上 {sum(s >= 90 for s in sc)}回/{len(sc)}回\", \"\"]\n L += L0\n OUT_MD.write_text(\"\\n\".join(L), encoding=\"utf-8\")\n if SKIPPED:\n print(f\"まだダウンロードされていない記録 {len(SKIPPED)}件(次の集計で読む):{'、'.join(SKIPPED)}\")\n print(f\"記録 {files}ファイル・{len(tries)}回 → {OUT_JSON.name}({len(progress)}件)と {OUT_MD.name} を作り直しました\")\n\n\nif __name__ == \"__main__\":\n main()</code></pre><p></p><h2 id=\"index_fh6J9rox\">抜き出しと照合に使った指示文</h2><p>カードを作るときに、エージェントに渡した指示文の全文です。照合の指示文は短いので、本文の節で書いた内容がほぼすべてです。</p><h3 id=\"index_1n65D96N\">開発ログから復習項目を抜き出す指示(INSTRUCTIONS.md)</h3><p>markdown</p><pre><code class=\"language-markdown\"># ロボティクス復習アプリ用の項目抽出\n\nエイイチ(Pixels to Motors)が DAY102〜DAY326 の開発ログ記事で学んできたロボティクスの知識を、\niPhone で見直すための復習カードにする。あなたは担当範囲の記事を読み、復習項目を JSON で書き出す。\n\n## 読み方\n- 担当ファイル一覧の各 .md を読む(`_en.md` で終わる英語版は飛ばす)。\n- 記事にはコード全文・ログ全量が入っていて長い。コードブロックやログは流し読みでよい。本文の説明・判断・結論・表を重視する。\n- 大きいファイルは Read の offset/limit で分けて読む。\n\n## 何を拾うか(ロボティクスの「知識」だけ)\n拾う:概念・用語の意味、仕組み、実機やシミュで確かめた事実・数値、失敗から得た原則、判断の理由、手法の比較、評価の考え方。\n拾わない:SNS戦略・集客・ビジネスの話、記事の書き方、個人の作業段取り、日付だけの進捗、雑談。\n(ただし「ロボットの社会実装・現場導入」の構造的な知見は category \"society\" で拾ってよい)\n\n- 1記事から 0〜6 項目。中身の薄い記事は 0 でよい。合計は担当範囲で 30〜60 項目が目安。\n- 同じ内容を何度も拾わない(担当範囲内で重複したら一番よく説明している記事の1つに絞る)。\n- 記事に書いてある事実だけを書く。あなたの一般知識で補ったり、数値を作ったりしない。一般用語の定義を補う場合も記事の文脈と食い違わないこと。\n\n## カテゴリ(category の値はこの英字キーのどれか)\n- kinematics : 運動学・座標系(FK/IK、CCD、関節角、座標系の向き、EE定義、到達範囲)\n- hardware : 実機・サーボ制御(mechArm 270 Pi の API、send_angles/send_coords、サーボ・速度・ガード、キャリブレーション、通信)\n- sim : シミュレーション・デジタルツイン(Unity、ArticulationBody、Stiffness/Damping、Target/Force mode、URDF、ROS2・MoveIt)\n- vision : 画像認識・カメラ(色検出、色空間、輪郭、透視変換・ホモグラフィ、特徴点、カメラ校正、物体検出モデル、IoU/Precision/Recall/mAP)\n- servo : 視覚サーボ・Pick&Place・受け渡し(補正記憶、積分則、PICK/PLACE、ハンドオーバー、動的追従)\n- learning : ロボット学習(模倣学習、VLA、π0/π0.5、ACT、Diffusion Policy、SmolVLA、GR00T、LeRobot、SO-101、テレオペ、データ収集、リターゲティング、強化学習、報酬)\n- eval : 評価・データ管理・再現性(台帳、ラベリング、VLM-as-Reward、試行回数とぶれ、ログ設計、テスト)\n- principles : 設計原則・進め方(P001/P005/P008 など、測ってから推論、補正で仕事しない、層の分離、Motion UX の考え方)\n- society : 社会実装・現場(配膳・清掃・警備・介護・物流など導入の構造、例外対応、コスト)\n\n## 項目の種類(type)\n- \"point\" : 覚えておきたい要点1文(カードに表示。60〜120字目安、言い切りの文)。\n- \"term\" : 用語。front=用語名(短く)、back=意味を1〜3文で。\n- \"qa\" : 問い。front=「なぜ〜?」「〜はどう違う?」などの問い、back=答え2〜4文。実機で確かめた理由や判断の根拠が入ると良い。\n\nkey: true は「これだけは忘れない」核心(各カテゴリの重要原則・確定事項)。全体の15%程度まで。\n\n## 出力\n`/home/claude/work/parts/{あなたの担当名}.json` に、次の形の JSON 配列を書く(UTF-8、ensure_ascii=False):\n\n```json\n[\n {\"category\":\"kinematics\",\"type\":\"qa\",\"front\":\"なぜ一般的なIKではなくFKテーブルを使ったのか?\",\"back\":\"…\",\"key\":false,\"src\":\"DAY163\"},\n {\"category\":\"principles\",\"type\":\"point\",\"front\":\"測る手段を持つことが推論より先。座標・姿勢・画像を実測する前に仮説を確定しない(P008)。\",\"back\":\"\",\"key\":true,\"src\":\"DAY170\"}\n]\n```\n- src は出典の DAY 番号(例 \"DAY163\")。複数なら \"DAY163,DAY164\"。\n- 文体:です・ます不要。短く言い切る。専門用語は初出で短く言い換える(例「逆運動学(指先位置から関節角を求める計算)」)。英語の略語だけで済ませない。\n- 書き終えたら python で json.load して壊れていないことを確かめる。\n\n最後の返答は「件数(カテゴリ別)」と「書き出したパス」だけを短く返す。</code></pre><h3 id=\"index_BCbwDIrW\">カテゴリごとに重複をまとめる指示(CURATE.md)</h3><p>markdown</p><pre><code class=\"language-markdown\"># 復習カードの整理(カテゴリ単位)\n\n/home/claude/work/merged_raw.json に、開発ログ記事(DAY102〜326)と調査メモから抜き出した復習項目が入っている。\n複数の人が範囲を分けて抜き出したので、カテゴリの中に重複・食い違い・粒度のばらつきがある。\nあなたは担当カテゴリの項目だけを整理し、iPhone で見直す復習アプリのデータとして仕上げる。\n項目の形式・type・key の意味は /home/claude/work/INSTRUCTIONS.md を参照。\n\n## やること\n1. 担当カテゴリの項目を全部読む。\n2. 重複をまとめる:同じ事実・同じ原則を言っている項目は1つに統合する(一番よい説明を残し、src を結合)。\n3. 食い違いを解く:古い DAY の結論と新しい DAY の結論が違うなら、新しい方を正として書き、必要なら「当初は〜と見ていたが、DAY◯◯で〜と分かった」と一言添える。判断できないときは、項目の front/back と src を元記事(/home/claude/r2r2r-content/articles/DAY◯◯*.md)で確かめる。調査メモ由来(src が \"研究メモ:\" で始まる)の数値は /mnt/user-data/uploads/PixelstoMotors/research/ のメモで確かめられる。\n4. 小見出しを付ける:カテゴリの中を 3〜7 個の小見出し(sub)に分け、各項目に \"sub\" を付ける。小見出しは短い日本語(例「座標系の向き」「関節の上限」「FKテーブル」)。復習しやすい順(基礎→応用、原因→対策)に並べる。\n5. 文を整える:front/back が長すぎる・分かりにくいものは短く直す。英略語は初出で短く言い換え。事実は変えない。mechArm 固有の数値は残す(復習の価値がある)。\n6. 削る:知識として薄いもの(その日の作業報告だけ、ロボティクスでない話)は捨てる。目安として元の件数の 60〜80% 程度に収まればよい(無理に削らない)。\n7. key: true は担当カテゴリの 10〜15% に絞る。そのカテゴリで「これだけは忘れない」もの。\n8. type=\"term\" は front が用語名、back が意味。type=\"qa\" は front が問い、back が答え。type=\"point\" は front が要点文で back は空でよい(補足があれば back に1文)。\n\n## 出力\n`/home/claude/work/curated/{担当名}.json` に JSON 配列(UTF-8、ensure_ascii=False)。各要素:\n{\"category\",\"sub\",\"type\",\"front\",\"back\",\"key\",\"src\"}\n配列は「小見出しの並び順 → 小見出し内の復習順」で並べる。\n書き終えたら json.load で確認。最後の返答は、カテゴリごとの件数(元→後)と小見出し一覧だけ。</code></pre><h3 id=\"index_DcXAKdXD\">用語164語を作る指示(VOCAB.md)</h3><p>markdown</p><pre><code class=\"language-markdown\"># ロボティクス用語集の作成(声に出して覚える用)\n\nエイイチ(29年のデザイナー。ロボットアーム mechArm 270 Pi を iPhone・Unity・Python で動かして開発ログを書いてきた。\nロボット工学の正式な教育は受けていない)が、ロボティクスの「現場で本当に使う言葉」を基礎から覚え直すための用語集を作る。\niPhone のページで、①用語を見て短い意味を声に出す → ②詳しい説明を声に出す、の順で練習する。\n\n## 選ぶ言葉\n- ロボティクスの実務・研究・業界の会話で実際によく出る、実用性の高い言葉を選ぶ。マニアックな言葉、この人のプロジェクト固有の造語(lookup、補正記憶、P008 など)は選ばない。\n- 担当カテゴリごとに 14〜20 語。カテゴリの中は「基礎 → 応用」の順に並べる(最初の数語は、まったくの初心者でも分かる言葉から)。\n- 各カテゴリで特に最初に覚えるべき 4〜6 語に key: true を付ける。\n\n## 書き方\n- word:用語。日本語+英語や略語を括弧で(例「逆運動学(IK)」「エンドエフェクタ(EE)」)。\n- short:意味を1文で。**20〜45字**。声に出して言いやすい、言い切りの文。専門用語を重ねない。\n 例:「手先の目標位置から、各関節の角度を逆算する計算」\n- detail:詳しい説明。**2〜3文・80〜160字**。①仕組みや使いどころ ②似た言葉との違いや注意点 ③(あれば)エイイチの実機での例。\n - ③を書くときは、必ず /home/claude/r2r2r-content/articles/ の記事を Grep で確かめ、書いてある事実だけを書き、src にその DAY を入れる(例 \"DAY150\")。確かめられないなら③は書かず src は \"\"。\n - 例は「mechArm では〜」のように短く。数値は記事どおり。\n- 一般知識の部分は、教科書・業界の標準的な定義に合わせる。あいまいなら書かない。誇張しない。\n- 文体:です・ます不要。短く言い切る。声に出したときに読みやすい表記(記号の多用、長いカッコ書きは避ける)。\n\n## 担当カテゴリ(category の値)\n- kinematics : ロボットの体と動き(自由度、関節、リンク、座標系、FK/IK、作業空間、特異点、姿勢の表し方 など)\n- hardware : モーターと制御(サーボ、エンコーダ、トルク、PID、フィードバック制御、可搬重量、繰り返し精度、通信 など)\n- sim : シミュレーションと開発環境(デジタルツイン、物理エンジン、URDF、ROS2、MoveIt、sim2real、ドメインランダマイゼーション など)\n- vision : カメラと画像認識(カメラ校正、内部・外部パラメータ、ホモグラフィ、色空間、物体検出、セグメンテーション、IoU、深度カメラ など)\n- servo : 動作の計画と実行(軌道計画、補間、視覚サーボ、ハンドアイ校正、把持、Pick&Place、衝突回避、コンプライアンス など)\n- learning : ロボット学習(模倣学習、強化学習、報酬、方策、テレオペ、デモデータ、VLA、基盤モデル、汎化、ファインチューニング など)\n- eval : 評価とデータ(成功率、試行、ベースライン、再現性、ロギング、データセット、アノテーション、誤差の種類 など)\n- principles : 開発の進め方(反復開発、ボトルネック、根本原因分析、フェイルセーフ、段階的検証、HRI の基本用語 など)\n- society : 社会実装と業界(協働ロボット、産業用ロボット、AMR、SIer、ROI、安全規格、リスクアセスメント、Physical AI など)\n\n## 出力\n`/home/claude/work/vocab/{担当名}.json` に JSON 配列(UTF-8、ensure_ascii=False):\n{\"category\",\"word\",\"short\",\"detail\",\"key\",\"src\"}\nカテゴリの順は上の並び、カテゴリ内は基礎→応用の順。書き終えたら json.load で確認し、short が20〜45字・detail が160字以内かを python で数えて直す。\n最後の返答は件数(カテゴリ別)だけ。</code></pre><h3 id=\"index_qL8efy5k\">判断の型104枚を作る指示(LESSON.md)</h3><p>markdown</p><pre><code class=\"language-markdown\"># 「実機の知見」を判断の型(教訓カード)に作り直す\n\n/home/claude/work/old_insights.json に、エイイチの開発ログ(mechArm 270 Pi を iPhone・Unity・Python で動かした DAY102〜326)から抜き出した 424 項目がある。\n事実の羅列(関節の割り当て、軸の向き、定数)で、声に出して丸暗記しても使える力にならない。\nこれを「こういう場面では、まずこれを疑う/こうする」という**判断の型**のカードに作り直す。\n別のロボット・別の現場でもそのまま使える形にし、全部が実話から来ているので自分の言葉として話せるようにする。\n\n## カードの形\n- scene(場面・問い):困る状況を1〜2文で。最後は「まず何を疑う?」「どうする?」などの問い。40〜80字。mechArm 固有の名前はなるべく一般化(「実機」「アーム」「カメラ」)。\n 例:「シミュでは合っているのに、実機だと手先が数cmずれる。まず何を確かめる?」\n- lesson(教訓・声に出す一言):15〜35字。言い切り。覚えて口に出せる長さ。\n 例:「座標系を1つに決めて、全部をそこにそろえる」\n- keywords:教訓の中で「これが言えていれば合格」という要の語を 2〜4 個。各要素は言い換えを含む配列(音声認識で漢字・かなが揺れるので、ひらがな表記や同義語も入れる)。\n 例:[[\"座標系\",\"座標\"],[\"1つ\",\"ひとつ\",\"一つ\",\"統一\"],[\"そろえる\",\"揃える\",\"合わせる\"]]\n- example(実例・読むだけ):実際に起きたことを1〜2文、80〜140字。数値は元項目どおり。「mechArm では〜」のように書いてよい。\n- src:実例の出典 DAY(元項目の src を引き継ぐ。複数可、カンマ区切り)\n- key:そのカテゴリで特に大事な型に true(カテゴリの 3 割程度まで)\n\n## やり方\n1. 担当カテゴリの元項目を全部読む(category で絞る)。\n2. 同じ教訓を言っている項目をまとめ、1つの型にする。1つの型の実例は一番分かりやすい1件(必要なら2件)を選ぶ。\n3. 教訓にならない「資料」(関節の割り当て、座標の向き、定数値、ツールの仕様だけの項目)は捨てる。ただしその資料から教訓が引き出せるなら教訓にする(例:ROSとUnityの軸の違い →「ツールをまたぐときは、右手系か左手系か・どの軸が上かを最初に確かめる」)。\n4. 担当カテゴリ合計で、1カテゴリあたり 6〜10 枚を目安(中身が濃いカテゴリは12枚まで)。\n5. 実例の事実は元項目に書いてあることだけ。迷ったら /home/claude/r2r2r-content/articles/DAY{NNN}*.md で確かめる。新しい数字や出来事を作らない。\n6. 文体:です・ます不要、短く言い切る。英略語は最小限。\n\n## 出力\n`/home/claude/work/lessons/{担当名}.json` に JSON 配列(UTF-8、ensure_ascii=False):\n{\"category\",\"scene\",\"lesson\",\"keywords\",\"example\",\"src\",\"key\"}\nカテゴリ内は「基礎的・よく起きる型 → 応用」の順。json.load で確認し、lesson が15〜35字に入っているか python で数える。\n最後の返答はカテゴリ別の枚数だけ。</code></pre><p></p><h2 id=\"index_pr4XWn0H\">次は同じ164語を「見ないで言う」でもう一周する</h2><p>単語の「見て言う」は、聞き違いを補正すると 182回中168回が 90%以上で、ほぼ言えるようになりました。次は同じ164語を「見ないで言う」で一周します。用語だけを見て意味を思い出す形になるので、本当に覚えているかはこちらで分かります。そのあと「2 説明」と「3 判断の型」に進みます。</p><p>練習の記録がたまったら、<code>practice_review.py</code> で集計し、3回続けて 80% に届かないカードの言い回しと、聞き違いの一覧を見直します。</p>", "toc": [{"id": "index_rNSeBrBJ", "text": "仕事で使っている台本の読み合わせページを、ロボティクスの復習用に作り変えた"}, {"id": "index_Ef3BykTT", "text": "開発ログ194本と調査メモから、9つのエージェントで復習項目を549件抜き出した"}, {"id": "index_nY3kXcq7", "text": "424項目を出典の記事と照合し、21件を直した"}, {"id": "index_6jUeiDe0", "text": "最初の版は「読むだけ」のページで、核心のカードが多すぎて見づらかった"}, {"id": "index_atypUTBf", "text": "声で確認するには https が要るので、GitHub Pages に置いた"}, {"id": "index_Lghl9cGq", "text": "押している間だけ聞き、カードの文と1文字ずつ比べる"}, {"id": "index_e1cF0_A9", "text": "いきなり難しい説明が並んでいたので、用語164語から始める3段階にした"}, {"id": "index_q7iw4fXg", "text": "「リンクは、関節と関節の間をつなぐ硬い骨組みの部分」と用語から続けて言う"}, {"id": "index__ZgAWoKQ", "text": "実機の知見424項目は事実の羅列で、声に出して覚える意味がなかった"}, {"id": "index_Ibt2SwNb", "text": "判断の型104枚:場面を見て、教訓をキーワードで言えたら合格"}, {"id": "index_J89hmYda", "text": "練習の記録は練習記録フォルダに保存し、集計してページに戻す"}, {"id": "index_ZzZgsbhc", "text": "単語ひととおり182回の記録:平均92%、低い点のほとんどは聞き違いだった"}, {"id": "index_Nb1eeiph", "text": "聞き違いの一覧を足して採点し直したら、平均95%になった"}, {"id": "index_K71F5Phd", "text": "カードを切り替えた直後に話すと、前のカードで採点されていた"}, {"id": "index_bfqmDzCf", "text": "iPhone の「ファイル」アプリのプレビューでは JavaScript が動かない"}, {"id": "index_qt8YkBwG", "text": "iPhone からの保存は、ショートカット「ロボ見直しを保存」で練習記録に直接入れる"}, {"id": "index_eByBD_Dn", "text": "練習記録の全量(182回)"}, {"id": "index_xj0uFjsd", "text": "聞き違いの一覧(homophones・56組)"}, {"id": "index_RbStIw79", "text": "ページのテンプレート template.html の全文"}, {"id": "index_xrC6Iabk", "text": "ページを書き出す build_review.py の全文"}, {"id": "index_54fjEQ2p", "text": "練習記録を集計する practice_review.py の全文"}, {"id": "index_fh6J9rox", "text": "抜き出しと照合に使った指示文"}, {"id": "index_pr4XWn0H", "text": "次は同じ164語を「見ないで言う」でもう一周する"}]}
\ No newline at end of file
+{"day": "LOG328", "series": "ひとり研究所の裏側", "published": "2026-10-11T07:39:41.000Z", "title": "ロボティクスを声に出して覚え直す ― 開発ログから用語164語と判断の型104枚の練習ページを作った", "slug": "speak-to-relearn-robotics", "cover": null, "thumb": "/thumbs/r-speak-to-relearn-robotics.html", "lang": "JP", "description": "現場でよく使うロボティクスの用語164語と、開発ログの実話から作った判断の型104枚を、押している間だけ聞いて答え合わせする練習ページにした。1周182回は平均92%、聞き違いを補正すると95%。", "updated": "2026-10-11T07:40:04.000Z", "body": "<!--TOC--><p></p><p>ロボットアームを動かしながら300日以上、開発ログを書いてきました。読み返すと、その日に分かったことは書いてあるのに、言葉として口から出てこないものが多くあります。順運動学、ホモグラフィ、視覚サーボ。使っているのに、人に説明しようとすると詰まります。</p><p>そこで、ロボティクスの言葉と判断を「声に出して」覚え直すための練習ページを作りました。スマホやパソコンでカードを見ながら、ボタンを押している間だけ話します。指を離すと、話した言葉が文字になり、カードの文とどこが違ったかが赤と青で出ます。</p><p>形は、仕事で使っている台本の読み合わせページをそのまま借りました。会議の前に、台本の文を一文ずつ声に出して、言い慣れていない言い回しを直すためのページです。読むだけより、声に出して文字で確かめる方が身につくことは、そちらで分かっていました。</p><p>中身は3段階です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>段階</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>カードに出るもの</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>声に出すもの</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>採点</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1 単語</p></td><td colspan=\"1\" rowspan=\"1\"><p>用語(例:リンク)</p></td><td colspan=\"1\" rowspan=\"1\"><p>用語と短い意味を一続きで</p></td><td colspan=\"1\" rowspan=\"1\"><p>カードの文と1文字ずつ比べる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2 説明</p></td><td colspan=\"1\" rowspan=\"1\"><p>用語と短い意味</p></td><td colspan=\"1\" rowspan=\"1\"><p>詳しい説明(2〜3文)</p></td><td colspan=\"1\" rowspan=\"1\"><p>同上</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3 判断の型</p></td><td colspan=\"1\" rowspan=\"1\"><p>困る場面の問い</p></td><td colspan=\"1\" rowspan=\"1\"><p>教訓の一言(自分の言葉でよい)</p></td><td colspan=\"1\" rowspan=\"1\"><p>キーワードが言えたか</p></td></tr></tbody></table></div><p>単語は、現場でよく使うロボティクスの用語を164語選びました。判断の型は、開発ログに書いてきた実話から104枚にしました。</p><p>この記事は、そのページを作った過程の記録です。最初の版は、開発ログから抜き出した事実を424枚のカードに並べただけのもので、声に出して覚えるには向いていませんでした。そこから、用語から始める3段階に作り直し、練習の記録を保存して集計する仕組みを足しました。最後に、単語をひととおり(182回)練習した記録を集計して、採点の作りを直すところまで進めました。</p><p>練習ページは <a href=\"https://pixelstomotors.github.io/robotics-review/\" data-has-link=\"true\">https://pixelstomotors.github.io/robotics-review/</a> で公開しています。</p><p></p><h2 id=\"index_rNSeBrBJ\">仕事で使っている台本の読み合わせページを、ロボティクスの復習用に作り変えた</h2><p>元にしたページは、横にスワイプする黒いカードに台本の文が1つずつ出て、その下に「押している間だけ聞く」の大きなボタンがある作りです。ボタンを押して話し、指を離すと、ブラウザの音声認識が話した言葉を文字にし、台本の文と比べて違うところに色を付けます。</p><p>このページの作りは、ロボティクスの言葉の練習にそのまま使えると考えました。台本の文を、ロボティクスの用語や説明に入れ替えればよいからです。データ(カードの中身)は、自分の Mac と iPhone の両方から触れる iCloud Drive に置きます。</p><p>置き場所は、iCloud Drive の <code>PixelstoMotors/ロボティクス復習/</code> です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>ファイル</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>中身</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>robotics_review.json</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>カードの中身(用語・判断の型)と、聞き違いの一覧。これがデータの正</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>_tools/template.html</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>ページの形。データは書き出すときに埋め込む</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>_tools/build_review.py</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>JSON とテンプレートからページ(<code>ロボティクス復習.html</code>)を書き出す</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>_tools/practice_review.py</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>練習記録を集計し、集計 JSON と振り返りを作る</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>練習記録/</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>ページの「結果を保存」で入る記録(1回の保存=1ファイル)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p><code>_archive/実機の知見_旧424項目.json</code></p></td><td colspan=\"1\" rowspan=\"1\"><p>作り直す前のカード(調べるための資料として残す)</p></td></tr></tbody></table></div><p></p><h2 id=\"index_Ef3BykTT\">開発ログ194本と調査メモから、9つのエージェントで復習項目を549件抜き出した</h2><p>カードの元にしたのは、GitHub の記事リポジトリ(<code>PixelstoMotors/r2r2r-content</code>)にある LOG102〜LOG326 の記事194本(英語版を除く)と、論文や調査をまとめたメモ5本、プロジェクトの原則・運動学・動的追従の設計をまとめた文書3本です。</p><p>記事はコードと実機ログを全量で載せているので、合わせて約6.7MBあります。1つの作業で全部を読むのは無理なので、記事を8つの範囲に分け、調査メモと原則の文書で1つ、合わせて9つのエージェント(Claude が別に立ち上げる作業役)に並行して読ませました。</p><p>それぞれに渡した指示は同じです。ロボティクスの知識だけを拾うこと(SNS や記事の書き方の話は拾わない)、記事に書いてある事実だけを書くこと、カテゴリを9つのどれかにすること、項目の種類を「要点」「用語」「問い」のどれかにすること。指示文の全文は記事の最後に載せます。</p><p>9つの範囲から合わせて549件が出ました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>カテゴリ</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>件数</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>実機・サーボ制御</p></td><td colspan=\"1\" rowspan=\"1\"><p>82</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ロボット学習</p></td><td colspan=\"1\" rowspan=\"1\"><p>79</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>画像認識・カメラ</p></td><td colspan=\"1\" rowspan=\"1\"><p>76</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>評価・データ管理</p></td><td colspan=\"1\" rowspan=\"1\"><p>65</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>設計原則・進め方</p></td><td colspan=\"1\" rowspan=\"1\"><p>60</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>運動学・座標系</p></td><td colspan=\"1\" rowspan=\"1\"><p>55</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>視覚サーボ・Pick&Place・受け渡し</p></td><td colspan=\"1\" rowspan=\"1\"><p>52</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>社会実装・現場</p></td><td colspan=\"1\" rowspan=\"1\"><p>47</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>シミュレーション・デジタルツイン</p></td><td colspan=\"1\" rowspan=\"1\"><p>33</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>合計</p></td><td colspan=\"1\" rowspan=\"1\"><p>549</p></td></tr></tbody></table></div><p>範囲を分けて抜き出したので、同じ内容が別の日付で何度も出ます。そこで、カテゴリを3つずつ受け持つエージェントを3つ立てて、重複をまとめ、古い記事と新しい記事で結論が違うものは新しい方に合わせ、カテゴリの中に小見出しを付けさせました。549件が424件になりました。</p><p>食い違いの例を2つ挙げます。肩の関節(j1)の上限は、LOG231 では「90.87〜91.66°の間」と書いていましたが、LOG246 で刻んで測り直し「90.0°までは届き、90.5°以上を含む指令は丸ごと無視される」と分かっています。カードは LOG246 に合わせました。シミュレーションと実機のどちらを基準にするかも、LOG169 の「Unity を正」から LOG250 の「実機が土台で、ツインが合わせる側」に変わっていたので、後者にしました。</p><p></p><h2 id=\"index_nY3kXcq7\">424項目を出典の記事と照合し、21件を直した</h2><p>抜き出した文が正しいかは、抜き出した本人ではなく、別のエージェントに確かめさせました。同じ書き手が確かめると、同じところを見落とすからです。</p><p>まず424件から90件(核心として印を付けた55件と、無作為の35件)を照合させたところ、7件に問題がありました。8%の割合なので、残りの334件も4つに分けて全部照合しました。そちらで14件が見つかり、合わせて21件を直しました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>問題の種類</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>例</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>数値の取り違え</p></td><td colspan=\"1\" rowspan=\"1\"><p>3回連続の成功の誤差「2.3mm」は3回のうちの最良値で、ほかの2回は約4.6mmだった</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>原因の書き過ぎ</p></td><td colspan=\"1\" rowspan=\"1\"><p>Z方向の振動の原因を「符号の反転」だけにしていたが、実際は符号を直しても収束せず、常駐デーモンに替えて初めて 1.7秒・2.9mm に収まった</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>計画を実施済みのように書いていた</p></td><td colspan=\"1\" rowspan=\"1\"><p>置いた結果を iPhone で真上から撮って判定するのは、LOG125 では計画で、まだやっていなかった</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>出典の不足</p></td><td colspan=\"1\" rowspan=\"1\"><p>「SDK の定義は +120°」は LOG253 の記述で、出典に LOG253 が入っていなかった</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>一部の条件の結果を一般化していた</p></td><td colspan=\"1\" rowspan=\"1\"><p>実データと生成データを混ぜて成功率が上がった結果は、マグをコーヒーメーカーに載せる1つの作業でのものだった</p></td></tr></tbody></table></div><p></p><h2 id=\"index_6jUeiDe0\">最初の版は「読むだけ」のページで、核心のカードが多すぎて見づらかった</h2><p>最初に作ったのは、JavaScript を使わない1枚の HTML です。iPhone の「ファイル」アプリで iCloud の HTML を開くとプレビューになり、そこでは JavaScript が動かないので、動かなくても読める作りにしました。カテゴリを開くと、横にスワイプするカード、用語をタップして意味を出すリスト、問いをタップして答えを出すリストが並びます。</p><p>見てもらうと、2つ指摘がありました(引用の〔 〕は、仕事の案件名を伏せて言い換えた箇所です)。</p><blockquote><p>核心のところが〔仕事の台本ページ〕の時より多いので、黄色の薄いレイヤーが上にかかっているのが視認性を悪くしている。</p></blockquote><p>元のページでは、台本の中で「ここで止まって言い切る1文」だけを金色の枠と茶色の背景で目立たせていました。台本では1つの議題に1文ほどなので目立つ意味がありますが、こちらでは核心が全体の13%(55枚)あり、茶色の背景が続くと読みにくくなっていました。核心の印は、黒いカードの左上に小さな金色の「核心」の字を置くだけにしました。</p><blockquote><p>あと〔仕事の台本ページ〕と同じように声に出して確認した方が覚えるので、同じように作り変えてください(細かい注意書きは読まない)</p></blockquote><p>もう1つは、読むだけでなく、元のページと同じように声に出して確かめたいという指摘です。説明の注意書きも読まないので外してほしいとのことでした。</p><p></p><h2 id=\"index_atypUTBf\">声で確認するには https が要るので、GitHub Pages に置いた</h2><p>ブラウザで声を文字にする仕組み(Web Speech API の SpeechRecognition)は、https で開いたページでしか使えません。iCloud の HTML をプレビューで開く形では動きません。元の読み合わせページも、この理由で GitHub Pages に置いていました。</p><p>そこで、新しく公開リポジトリ <code>PixelstoMotors/robotics-review</code> を作り、Settings → Pages で Branch を <code>main</code>、フォルダを <code>/ (root)</code> にして公開しました。リポジトリの作成と Pages の設定は、こちらの作業環境から操作できなかったので、ブラウザで手で行いました。中身(<code>index.html</code>)は、iCloud で書き出したページをそのまま置いています。</p><p>公開ページは URL を知っていれば誰でも見られますが、<code><meta name=\"robots\" content=\"noindex, nofollow\"></code> を入れているので検索には出ません。練習の記録はページの外には送らず、保存するときも自分の iCloud にだけ書きます。</p><p>iPhone では、Safari で公開ページを開き、共有ボタンから「ホーム画面に追加」しておくと、次からはアイコンから開けます。</p><p></p><h2 id=\"index_Lghl9cGq\">押している間だけ聞き、カードの文と1文字ずつ比べる</h2><p>声で確かめる部分は、元のページの作りを踏襲しました。</p><ol><li><p>ボタンを押した瞬間に音声認識を始める(<code>lang = 'ja-JP'</code>、話している途中の文字も出す設定)</p></li><li><p>指を離してから 0.25秒待って止める(言い終わりを取りこぼさないため)</p></li><li><p>話した言葉とカードの文を、句読点と空白を除き、カタカナとひらがな、全角と半角、大文字と小文字を同じとみなしてから、1文字ずつ並べて比べる</p></li><li><p>両方に共通して並ぶ文字が一番多くなる並べ方(最長共通部分列、LCS)を求め、カードにあって話さなかった文字を赤、カードになくて話した文字を青で出す</p></li><li><p>一致は「共通の文字数 ÷ 長い方の文字数」で出す</p></li></ol><p>PC ではスペースキーを押している間も聞き、←→キーでカードを移ります。iPhone は聞き始めと終わりに本体が音を鳴らすので、PC でも同じ順に音を鳴らします。</p><p>カテゴリを選ぶプルダウンの矢印は、ブラウザ標準の矢印が端末によって崩れていたので、CSS で描いた矢印(線の太さ 2.75px)に替えました。</p><p></p><h2 id=\"index_e1cF0_A9\">いきなり難しい説明が並んでいたので、用語164語から始める3段階にした</h2><p>声に出せるようになったページを開いてもらうと、こう言われました。</p><blockquote><p>いきなり難易度が高い説明ばかり並んでいるので、もっとロボティクスで実用性の高い言葉を選んでまずは単語の意味から習得して、その後に詳しい説明が続くように作り変えてください。</p></blockquote><p>424枚は開発ログから抜き出したものなので、「FK テーブルの最近傍が低い姿勢を選んでいた」のように、そのプロジェクトの文脈を知らないと読めない文が並んでいました。基礎の言葉を先に固めないと、こうした文は覚えられません。</p><p>そこで、ロボティクスの実務や研究の会話でよく出る用語を、9つのカテゴリで164語選びました。各カテゴリの中は、まったくの初心者でも分かる言葉から順に並べています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>カテゴリ</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>語数</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>最初の数語</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>運動学・座標系</p></td><td colspan=\"1\" rowspan=\"1\"><p>18</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節、リンク、自由度、回転関節と直動関節、エンドエフェクタ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>実機・サーボ制御</p></td><td colspan=\"1\" rowspan=\"1\"><p>17</p></td><td colspan=\"1\" rowspan=\"1\"><p>アクチュエータ、サーボモーター、減速機、エンコーダ、トルク</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>シミュレーション・デジタルツイン</p></td><td colspan=\"1\" rowspan=\"1\"><p>19</p></td><td colspan=\"1\" rowspan=\"1\"><p>シミュレーション、デジタルツイン、物理エンジン、タイムステップ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>画像認識・カメラ</p></td><td colspan=\"1\" rowspan=\"1\"><p>19</p></td><td colspan=\"1\" rowspan=\"1\"><p>画素、色空間、二値化、モルフォロジー演算、輪郭抽出</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>視覚サーボ・Pick&Place・受け渡し</p></td><td colspan=\"1\" rowspan=\"1\"><p>18</p></td><td colspan=\"1\" rowspan=\"1\"><p>経由点、経路と軌道、補間、速度プロファイル</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ロボット学習</p></td><td colspan=\"1\" rowspan=\"1\"><p>18</p></td><td colspan=\"1\" rowspan=\"1\"><p>学習、推論、観測と行動、方策、エピソード</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>評価・データ管理</p></td><td colspan=\"1\" rowspan=\"1\"><p>20</p></td><td colspan=\"1\" rowspan=\"1\"><p>試行、成功率、成功条件、ロギング、データセット</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>設計原則・進め方</p></td><td colspan=\"1\" rowspan=\"1\"><p>18</p></td><td colspan=\"1\" rowspan=\"1\"><p>反復開発、仮説検証、切り分け、ボトルネック</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>社会実装・現場</p></td><td colspan=\"1\" rowspan=\"1\"><p>17</p></td><td colspan=\"1\" rowspan=\"1\"><p>産業用ロボット、協働ロボット、サービスロボット、AGV、AMR</p></td></tr></tbody></table></div><p>1語ごとに、声に出す短い意味(20〜45字)と、詳しい説明(80〜160字)を付けました。詳しい説明には、そのプロジェクトの実機で起きた例を1つ入れたものもあります(例:可搬重量の説明に「mechArm 270 の常用ペイロードは250gで、約600gの中身入りコーヒーカップは運べない」)。実機の例を書くときは、記事を検索して書いてある事実だけを書き、出典の DAY を付けました。各カテゴリで最初に覚える4〜6語、合わせて53語に「まず覚える」の印を付けています。</p><p>この164語も、作ったのとは別のエージェントに照合させました。教科書の一般的な定義と、実機の例の出典の両方を確かめて、12件を直しています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>直したカード</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>何が違っていたか</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>座標系</p></td><td colspan=\"1\" rowspan=\"1\"><p>成功率が0になったのは実機ではなく Unity のツインの上だった</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>順運動学</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG192 では FK テーブルを逆向き(目標位置から関節角を引く)に使っていた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>特異点</p></td><td colspan=\"1\" rowspan=\"1\"><p>「特定の向きに動かせなくなる」は姿勢の向きと紛らわしいので「特定の方向へ動けなくなる」に</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>減速機</p></td><td colspan=\"1\" rowspan=\"1\"><p>産業用アームは根元に RV 減速機、手首に波動歯車減速機が多い。片方だけ書いていた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>PID制御</p></td><td colspan=\"1\" rowspan=\"1\"><p>サーボの中で PID が回っているかは公式資料で確認できていない(LOG260)。記述を外した</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>力覚センサー</p></td><td colspan=\"1\" rowspan=\"1\"><p>電流で接触を検出「できる」が正しく、実際に使ってはいなかった</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ハンドアイ校正</p></td><td colspan=\"1\" rowspan=\"1\"><p>カメラを外に固定する場合(mechArm がこれ)の説明が抜けていた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>推論</p></td><td colspan=\"1\" rowspan=\"1\"><p>97ms に GPU の型番(A40)を添えた(速度は型番と組で書く決まり)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>系統誤差</p></td><td colspan=\"1\" rowspan=\"1\"><p>約58mm のずれは高さではなく、マット上の奥行き方向だった</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>統計的なぶれ</p></td><td colspan=\"1\" rowspan=\"1\"><p>信頼区間に信頼度(95%)が書かれていなかった</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ドライラン</p></td><td colspan=\"1\" rowspan=\"1\"><p>LOG256 では、実機を通す前に止めてデータの流れを先に整理していた</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>安全規格(ISO 10218)</p></td><td colspan=\"1\" rowspan=\"1\"><p>2025年の改訂で、協働運転の要件(旧 ISO/TS 15066)が本体に取り込まれた</p></td></tr></tbody></table></div><p>ページは、上に「1 単語」「2 説明」「3」の切り替えを置きました。カテゴリの選択と、「見て言う/見ないで言う」「全部/苦手だけ」の切り替えは各段階で共通です。「見ないで言う」では、用語だけを見て意味を言い、話し終えると答えが出ます。</p><p></p><h2 id=\"index_q7iw4fXg\">「リンクは、関節と関節の間をつなぐ硬い骨組みの部分」と用語から続けて言う</h2><p>単語の練習で「リンク、関節と関節の間をつなぐ、固い骨組みの部分」と話すと、一致が 82% になりました。カードの文は意味だけなので、自然に言った「リンク」が余分(青)として減点されていたためです。</p><blockquote><p>内容だけではなく、タイトルの言う前提にした方が良くない?どっちがより良くなる印象ですか?</p></blockquote><p>用語と意味を一続きで言う方が良いと答えました。意味だけを言うと意味の文は覚えますが、「この言葉はこの意味」という結び付きが残りにくくなります。実際の会話では用語の方を口に出して使います。</p><p>採点は次のように変えました。</p><ul><li><p>比べる文を「用語+は、+意味」にする。「は」は付けても付けなくてもよい</p></li><li><p>「順運動学(FK)」のようにカッコが付いた用語は、「順運動学」「FK」「順運動学FK」のどれで言ってもよい</p></li><li><p>言い方の候補すべてと比べ、一番一致が高いものを採る</p></li></ul><p>カードに出す文字は変えていません。用語は大きく、意味はその下に出ます。</p><p></p><h2 id=\"index__ZgAWoKQ\">実機の知見424項目は事実の羅列で、声に出して覚える意味がなかった</h2><p>3つ目の段階には、最初の424枚をそのまま置いていました。開いてもらうと、こう言われました。</p><blockquote><p>実機の知見だけど、書いてあることは正しいかもだけど、これを見て読んで覚えるとか無理じゃない?というか意味なさそう。もっと違う知見の書き方があるのでは?</p></blockquote><p>たとえば「j0=ベース回転、j1=肩、j2=肘、j3=腕の捻り、j4=手首の曲げ、j5=グリッパーの捻り」や、「Unity X+ は画面左、Z+ は手前、Y+ は上」です。どちらも正しいのですが、必要なときに調べれば済む資料で、声に出して丸暗記しても使える力にはなりません。</p><p>覚える価値があるのは、事実そのものより、そこから出てきた判断の型です。「こういう場面では、まずこれを疑う/こうする」という形にすると、別のロボットや別の現場でもそのまま使えます。</p><p></p><h2 id=\"index_Ibt2SwNb\">判断の型104枚:場面を見て、教訓をキーワードで言えたら合格</h2><p>424項目を、次の形のカード104枚に作り直しました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>欄</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>例</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>場面(問い)</p></td><td colspan=\"1\" rowspan=\"1\"><p>人の感覚・ロボットのコード・シミュで「X」の向きが違い、指示がすれ違う。どうする?</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>教訓(声に出す一言)</p></td><td colspan=\"1\" rowspan=\"1\"><p>座標系を1つに決めて、全部をそこにそろえる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>キーワード</p></td><td colspan=\"1\" rowspan=\"1\"><p>「座標系/座標」「1つ/ひとつ/統一」「そろえる/揃える/合わせる」</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>実例(読むだけ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>mechArm では、人の直感(マット上で X=左右)、ロボット座標(X=前後・Y=左右・Z=高さ)、Unity(X=左右・Y=高さ・Z=前方)で X の意味が3通りに分かれ、会話が何度も噛み合わなかった。Unity 座標系に全部統一すると、Claude Code への指示も一貫した用語で書けるようになった(LOG149・LOG158・LOG160)</p></td></tr></tbody></table></div><p>同じ教訓が別の日付で何度も出ていたもの(「測ってから考える」「補正でごまかさない」など)は1枚にまとめました。関節の割り当てのような資料は練習から外し、<code>_archive/</code> に残しています。ROS と Unity の軸の違いのように、資料から教訓が引き出せるものは、「ツールをまたぐときは、右手系か左手系か・どの軸が上かを最初に確かめる」のような教訓にしました。</p><p>採点は、一字一句の一致ではなく、キーワードの組ごとに1語でも言えていれば合格、という形にしました。「座標系をひとつに決めて、全部合わせる」と自分の言葉で言っても、3つの組がそろっていれば100%です。結果には、言えたキーワードと言えなかったキーワード(赤)を出します。</p><p>104枚も別のエージェントに照合させ、40枚を直しました。多かったのは次の3種類です。</p><ul><li><p>実例の誇張や、計画を実施済みのように書いた箇所(DAY の記事と照合して直した)</p></li><li><p>教訓を一字一句そのまま言っても不合格になるキーワード(例:教訓は「追いかけず」なのに、キーワードが「追いかけない」だけだった)</p></li><li><p>どんな文にも含まれる1文字のキーワード(「さ」「め」など)</p></li></ul><p>直したあと、全104枚について「教訓をそのまま言えば全部のキーワードの組に当たる」ことを、プログラムで確かめています。</p><p></p><h2 id=\"index_J89hmYda\">練習の記録は練習記録フォルダに保存し、集計してページに戻す</h2><blockquote><p>読み上げしたデータを記録できるようにして、データを保管して、習得内容を改善していくようにしてください(〔仕事の台本ページ〕のもそのようになっています。機能を踏襲してください)</p></blockquote><p>元のページと同じく、練習の結果をファイルに保存し、集計して、カードの直しにつなげる流れを作りました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>端末</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>保存のしかた</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Mac(Chrome)</p></td><td colspan=\"1\" rowspan=\"1\"><p>最初の1回だけ <code>練習記録</code> フォルダを選ぶ。次からは「結果を保存」を押すだけで直接書き込む(File System Access API)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>iPhone</p></td><td colspan=\"1\" rowspan=\"1\"><p>「結果を保存」→「開く」で、ショートカット「ロボ見直しを保存」が <code>練習記録</code> に入れる(後の節で書く)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>それ以外</p></td><td colspan=\"1\" rowspan=\"1\"><p>ダウンロードに入る</p></td></tr></tbody></table></div><p>保存するファイルは Markdown で、人が読める部分(カードの文・話した言葉・一致・秒数)と、集計用の JSON の両方を入れています。保存せずにページを閉じようとすると止め、未保存の回数をボタンに出します。</p><p>集計は <code>practice_review.py</code> が行います。</p><ul><li><p><code>練習記録_集計.json</code>:カードと段階ごとの最新の一致、前回の一致、回数、最高の一致</p></li><li><p><code>練習記録_振り返り.md</code>:苦手(最新の一致が80%未満)、身についた(直近2回とも90%以上)、3回続けて80%に届かないカード、よく抜ける言葉、伸びたカード、まだ言っていない「まず覚える」語</p></li></ul><p><code>build_review.py</code> でページを書き出すときに集計 JSON を埋め込むので、iPhone で練習した結果が Mac で開いても「前回」に出ます。「苦手だけ」の切り替えも、この前回の一致で絞ります。</p><p></p><h2 id=\"index_ZzZgsbhc\">単語ひととおり182回の記録:平均92%、低い点のほとんどは聞き違いだった</h2><p>単語の「見て言う」を164語ひととおり練習した記録を集計しました。15:02〜16:13 の間に、9ファイル・182回です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>指標</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>値</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>練習した回数</p></td><td colspan=\"1\" rowspan=\"1\"><p>182回</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>一致の平均(記録したときの採点)</p></td><td colspan=\"1\" rowspan=\"1\"><p>91.9%</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>90%以上</p></td><td colspan=\"1\" rowspan=\"1\"><p>137回</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>80%未満</p></td><td colspan=\"1\" rowspan=\"1\"><p>15回</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1回の平均の秒数</p></td><td colspan=\"1\" rowspan=\"1\"><p>8.6秒</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>まだ言っていない「まず覚える」語</p></td><td colspan=\"1\" rowspan=\"1\"><p>2語(座標系・再現性)</p></td></tr></tbody></table></div><p>80%未満の15回を見ると、言い間違いはほとんどありませんでした。低い点の原因は、音声認識が同じ読みの別の言葉を文字にしていたことです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>カード</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>話した言葉(文字起こし)</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>記録時の一致</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ピックアンドプレース</p></td><td colspan=\"1\" rowspan=\"1\"><p>ビッグ&プレイス 物を掴んで持ち上げ 別の場所へ運んでおく 一連の作業</p></td><td colspan=\"1\" rowspan=\"1\"><p>74%</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Gazebo</p></td><td colspan=\"1\" rowspan=\"1\"><p>ガズボン ロスト 組み合わせて使われてきた無料のロボット用 シミュレーター</p></td><td colspan=\"1\" rowspan=\"1\"><p>75%</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>関節補間と直線補間</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節保管と直接 保管 関節の角度でつなぐか 手先の直線でつなぐかという保管の違い</p></td><td colspan=\"1\" rowspan=\"1\"><p>81%</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Isaac Sim</p></td><td colspan=\"1\" rowspan=\"1\"><p>イザックシム NVIDIA が提供する GPU で動く 高精細なロボットというシュミレーター</p></td><td colspan=\"1\" rowspan=\"1\"><p>73%</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>アイインハンドとアイトゥハンド</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイ イン ハンド と I to Hand カメラを手先につけるか外に固定するかという2つの配置</p></td><td colspan=\"1\" rowspan=\"1\"><p>78%</p></td></tr></tbody></table></div><p>「補間」が「保管」に、「校正」が「構成」に、「ROS」が「ロス」に、「試行」が「思考」になっています。話した本人は正しく言えているので、これを言い間違いとして数えると、練習の記録が実際の覚え具合を表さなくなります。</p><p></p><h2 id=\"index_Nb1eeiph\">聞き違いの一覧を足して採点し直したら、平均95%になった</h2><p>同じ読みの聞き違いや書き分けを、データの <code>homophones</code> に組で持たせました。比べる前に、話した言葉の中の聞き違いを、カードの書き方にそろえます。たとえば <code>[\"補間\", \"保管\"]</code> の組は、カードに「補間」があるときだけ、話した言葉の「保管」を「補間」に置き換えます。カードに「補間」がなければ何もしません。</p><p>そのほかに、次の3つをそろえるようにしました。</p><ul><li><p>話した言葉の空白(「アイ イン ハンド」→「アイインハンド」)</p></li><li><p>英字の大文字と小文字(「I to Hand」→「i to hand」で一覧と照合)</p></li><li><p>カードに長音がない語の長音(「シミュレーター」→ カードが「シミュレータ」なら「シミュレータ」)</p></li></ul><p>一覧は 56 組になりました(全量は記事の後半に載せます)。集計スクリプトにも同じ比べ方を入れ、過去の記録も今の一覧で採点し直すようにしたので、一覧を足すたびに過去の点も直ります。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>指標</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>記録時</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>聞き違いの補正後</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>一致の平均</p></td><td colspan=\"1\" rowspan=\"1\"><p>91.9%</p></td><td colspan=\"1\" rowspan=\"1\"><p>95.3%</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>90%以上</p></td><td colspan=\"1\" rowspan=\"1\"><p>137回</p></td><td colspan=\"1\" rowspan=\"1\"><p>168回</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>採点が変わった回</p></td><td colspan=\"1\" rowspan=\"1\"><p>―</p></td><td colspan=\"1\" rowspan=\"1\"><p>58回</p></td></tr></tbody></table></div><p>補正後もまだ低いのは、「回転関節」が「に点 関節」になった回のように、一覧に入れると別の文まで誤って置き換えかねない聞き違いです。これは無理に足さず、そのままにしています。</p><div class=\"embed\"><iframe src=\"/embeds/speak-to-relearn-robotics-O7uyYP36.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p></p><h2 id=\"index_K71F5Phd\">カードを切り替えた直後に話すと、前のカードで採点されていた</h2><p>記録の中に、一致が 12% と 21% の回が1回ずつありました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>記録上のカード</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>話した言葉</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ベースライン</p></td><td colspan=\"1\" rowspan=\"1\"><p>再現性 同じ手順と条件で同じ結果が もう一度 得られること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>カメラ校正</p></td><td colspan=\"1\" rowspan=\"1\"><p>内部パラメータ 焦点距離や レンズの歪み など カメラ 自体が持つ性質の値</p></td></tr></tbody></table></div><p>話した言葉は、それぞれ隣のカード(再現性・内部パラメータ)の意味で、内容は合っています。カードを切り替えた直後に話し始めたため、ページが前のカードを対象にしたまま採点していました。</p><p>ページは、横スクロールが止まってから 0.08秒後に「今どのカードか」を数え直していました。カードを切り替えるスクロールが止まりきる前に話し始めると、数え直す前のカードが対象になります。ボタンを押した瞬間にその場で数え直すように直しました。</p><p>集計スクリプトには、点が 50% 未満で、話した言葉が別の単語カードの意味と 70% 以上一致する回を「別のカードの文を読んでいた記録」として外す処理を入れました。この2回は集計から外しています。「再現性」が「まだ言っていない語」に残っているのはこのためで、実際には言えています。</p><p></p><h2 id=\"index_bfqmDzCf\">iPhone の「ファイル」アプリのプレビューでは JavaScript が動かない</h2><p>iPhone で iCloud の <code>ロボティクス復習.html</code> を「ファイル」アプリから開くと、段階の切り替えボタンだけが出て、カテゴリもカードも空になりました。「ファイル」アプリのプレビューでは JavaScript が動かないためです。ページが壊れているわけではありません。</p><p>iCloud のファイルをプレビューで開いたときだけ、上に「Safari で開く → pixelstomotors.github.io/robotics-review」のリンクを出すようにしました。リンクは最初から HTML に書いてあり、JavaScript が動いたときに消します。JavaScript が動かないプレビューでは消えずに残ります。</p><p></p><h2 id=\"index_qt8YkBwG\">iPhone からの保存は、ショートカット「ロボ見直しを保存」で練習記録に直接入れる</h2><blockquote><p>iPhoneでデータを保存するとき一発で練習記録に保存できませんか?</p></blockquote><p>iPhone の Safari では、Web ページから決まったフォルダへ、押すだけで保存することはできません。Mac の Chrome で使える、フォルダを選んで直接書き込む仕組み(File System Access API)が、iPhone の Safari にはないためです。</p><p>元の読み合わせページは、iPhone の Safari のダウンロード先(設定 → アプリ → Safari → ダウンロード)を、そのページの練習記録フォルダにしてあります。ただ、ダウンロード先は1か所しか決められず、すでに元のページのフォルダになっています。同じやり方にすると、ロボティクスの記録が元のページのフォルダに入ってしまいます。</p><p>そこで、iPhone の「ショートカット」アプリを使いました。ページは「結果を保存」を押すと、記録をクリップボードに入れてから <code>shortcuts://run-shortcut?name=ロボ見直しを保存&input=clipboard</code> を開きます。ショートカットは、クリップボードの中身を <code>練習記録</code> フォルダに保存します。</p><p>ショートカットの作り方です。</p><ol><li><p>「ショートカット」アプリで + を押し、名前を「ロボ見直しを保存」にする(ページはこの名前で呼び出す)</p></li><li><p>アクション「クリップボードを取得」を追加する</p></li><li><p>アクション「ファイルを保存」を追加する。「クリップボード を Shortcuts に保存」と出る</p></li><li><p>「保存先を尋ねる」をオフにする</p></li><li><p>青い「Shortcuts」をタップし、iCloud Drive → デスクトップ → ADAPT DESIGN → じぶんの → PixelstoMotors → ロボティクス復習 → 練習記録 を選ぶ</p></li><li><p>サブパスに <code>ロボ見直し.md</code> と入れる。「ファイルが存在する場合は上書き」はオフのまま</p></li></ol><p>使うときは「結果を保存」→ Safari の「\"ショートカット\"で開きますか?」に「開く」の2タップです。初回だけ、フォルダへのアクセスの許可を聞かれます。</p><p>試すと、サブパスを <code>.md</code> にしても、ファイルは「ロボ見直し.txt」「ロボ見直し-2.txt」のように <code>.txt</code> で入りました。集計スクリプトは <code>.md</code> と <code>.txt</code> の両方を読むようにしています。</p><p>1つ問題がありました。「ロボ見直し-2.txt」が「ロボ見直し.txt」とまったく同じ回でした。「結果を保存」を2回押すと、1回目に保存した回をもう一度送っていたためです。集計では同じ回(カード・段階・時刻が同じ)を1回として数えていたので数字はずれていませんでしたが、保存済みの回は次の保存に含めないように直しました。「今回の練習」の表には、保存した回に緑の「保存済」を出します。</p><p>ショートカットで入ったばかりのファイルは、Mac 側で iCloud からまだダウンロードされていないことがあり、読もうとすると <code>OSError: [Errno 35] Resource deadlock avoided</code> で止まりました。集計スクリプトは、読めないファイルを飛ばして「まだダウンロードされていない記録」として名前を出し、次の集計で読むようにしています。</p><p></p><h2 id=\"index_eByBD_Dn\">練習記録の全量(182回)</h2><p>単語の「見て言う」ひととおりの記録です。「一致(記録時)」はページで練習したときの点、「一致(聞き違い補正後)」は 56 組の一覧と、用語のすべての言い方で採点し直した点です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>時刻</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>カード</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>一致(記録時)</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>一致(聞き違い補正後)</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>話した言葉(文字起こし)</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:02:26</p></td><td colspan=\"1\" rowspan=\"1\"><p>回転関節と直動関節</p></td><td colspan=\"1\" rowspan=\"1\"><p>73%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>に点 関節と直導関節 回って 角度が変わる関節とまっすぐ伸び縮みする関節</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:02:43</p></td><td colspan=\"1\" rowspan=\"1\"><p>エンドエフェクタ(EE)</p></td><td colspan=\"1\" rowspan=\"1\"><p>66%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>エンド エフェクターアームの先端につけて作業をする道具の部分</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:03:35</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節(ジョイント)</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節ロボットの部品と部品をつなぎ 動きを生む つなぎ目</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:03:53</p></td><td colspan=\"1\" rowspan=\"1\"><p>リンク</p></td><td colspan=\"1\" rowspan=\"1\"><p>95%</p></td><td colspan=\"1\" rowspan=\"1\"><p>95%</p></td><td colspan=\"1\" rowspan=\"1\"><p>リンク関節と関節の間をつなぐ 固い 骨組みの部分</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:04:08</p></td><td colspan=\"1\" rowspan=\"1\"><p>自由度(DOF)</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>自由度ロボットがお互いに独立して動かせる 動きの方向の数</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:04:25</p></td><td colspan=\"1\" rowspan=\"1\"><p>回転関節と直動関節</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>回転関節と極東 関節 回って 角度が変わる関節とまっすぐ伸び縮みする関節</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:04:39</p></td><td colspan=\"1\" rowspan=\"1\"><p>エンドエフェクタ(EE)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>エンド エフェクターアームの先端につけて作業をする道具の部分</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:15:26</p></td><td colspan=\"1\" rowspan=\"1\"><p>座標変換</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>座標を変換ある座標系での位置や 向きを別の座標系の値に直す計算</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:15:39</p></td><td colspan=\"1\" rowspan=\"1\"><p>位置姿勢(ポーズ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>1 姿勢 物や 手先がどこにあり どちらを向いているかの組み合わせ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:15:50</p></td><td colspan=\"1\" rowspan=\"1\"><p>オイラー角(ロール・ピッチ・ヨー)</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>オイラー角 向きを3つの軸周りの回転角の組で表す方法</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:16:01</p></td><td colspan=\"1\" rowspan=\"1\"><p>クォータニオン(四元数)</p></td><td colspan=\"1\" rowspan=\"1\"><p>89%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>コオタニオン 向きを4つの数で表す方法で計算で扱いやすい</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:16:12</p></td><td colspan=\"1\" rowspan=\"1\"><p>順運動学(FK)</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>準運動学 各関節の角度から手先の位置と向きを求める計算</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:16:23</p></td><td colspan=\"1\" rowspan=\"1\"><p>逆運動学(IK)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>逆運動学 手先の目標位置から各関節の角度を逆算する計算</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:16:33</p></td><td colspan=\"1\" rowspan=\"1\"><p>作業空間(ワークスペース)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>作業空間ロボットの手先が届き 作業できる空間の範囲</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:16:44</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節可動域(ジョイントリミット)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節可動域 各関節が 安全に動ける角度の上限と下限の範囲</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:16:54</p></td><td colspan=\"1\" rowspan=\"1\"><p>特異点(特異姿勢)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>特異点 手先が特定の方向へ 動けなくなるアームの特殊な姿勢</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:17:04</p></td><td colspan=\"1\" rowspan=\"1\"><p>ヤコビ行列(ヤコビアン)</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ヤコビー 行列 関節の速さと手先の速さの関係を表す行列</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:17:16</p></td><td colspan=\"1\" rowspan=\"1\"><p>DHパラメータ</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>DH パラメーター ピンクの長さ や ねじれを関節ごとに 4つの数で表す 決まり</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:17:26</p></td><td colspan=\"1\" rowspan=\"1\"><p>アクチュエータ</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>アクチュエーター 電気などのエネルギーを実際の動きに変える装置</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:17:36</p></td><td colspan=\"1\" rowspan=\"1\"><p>サーボモーター</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>サーボモーター 指令した角度や位置へ自分で合わせて止まる モーター</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:17:47</p></td><td colspan=\"1\" rowspan=\"1\"><p>減速機(ギア)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>減速機 モーターの速い 回転を遅くしてその分力を大きくする 部品</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:17:56</p></td><td colspan=\"1\" rowspan=\"1\"><p>エンコーダ</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>エンコーダー モーター や関節がどれだけ回ったか を測るセンサー</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:18:06</p></td><td colspan=\"1\" rowspan=\"1\"><p>トルク</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>トルク 関節や 軸を回転させようとする回す力の強さ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:18:18</p></td><td colspan=\"1\" rowspan=\"1\"><p>サーボオンとサーボオフ</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>サーボ音とサーボオフ モーターに力を入れて 姿勢を保つ状態と力を抜く 状態</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:18:29</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィードバック制御(閉ループ制御)</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィードバック制御 結果を図り 目標とのズレを見ながら 指令を直し続ける 制御</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:18:41</p></td><td colspan=\"1\" rowspan=\"1\"><p>PID制御</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>PID 制御 ずれの今の大きさ たまった量変化の速さで出力を決める制御</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:18:51</p></td><td colspan=\"1\" rowspan=\"1\"><p>可搬重量(ペイロード)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>可搬重量 ロボットが手先で持って動かせる重さの上限</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:19:00</p></td><td colspan=\"1\" rowspan=\"1\"><p>リーチ</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>リーチ 台座の中心から手先が届く 一番遠い点までの距離</p></td></tr></tbody></table></div><div class=\"table\"><table><tbody><tr><td colspan=\"1\" rowspan=\"1\"><p>15:19:10</p></td><td colspan=\"1\" rowspan=\"1\"><p>繰り返し精度</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>繰り返し制度 同じ場所へ 何度も動かした時 止まる位置がどれだけ揃うか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:19:20</p></td><td colspan=\"1\" rowspan=\"1\"><p>絶対精度</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>絶対制度指令した座標に実際どれだけ 正確に到達できるか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:19:32</p></td><td colspan=\"1\" rowspan=\"1\"><p>バックラッシ</p></td><td colspan=\"1\" rowspan=\"1\"><p>88%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>バックラッシュ 歯車の噛み合わせの隙間による動きのガタ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:19:44</p></td><td colspan=\"1\" rowspan=\"1\"><p>力覚センサー(力トルクセンサー)</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>力学 センサー 手先にかかる 押す力とねじる力を直接測るセンサー</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:19:55</p></td><td colspan=\"1\" rowspan=\"1\"><p>マイコン(MCU)</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>マイコン機械に組み込まれ 決まった処理を素早く回す 小さなコンピューター</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:20:07</p></td><td colspan=\"1\" rowspan=\"1\"><p>シリアル通信とボーレート</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>シリアル通信とボーレートデータを1ビットずつ順番に送る通信とその速さの設定</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:20:18</p></td><td colspan=\"1\" rowspan=\"1\"><p>制御周期(制御ループ周波数)</p></td><td colspan=\"1\" rowspan=\"1\"><p>89%</p></td><td colspan=\"1\" rowspan=\"1\"><p>89%</p></td><td colspan=\"1\" rowspan=\"1\"><p>制御周期 測って姿勢を出す 一回りを1秒に何回繰り返せるか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:20:28</p></td><td colspan=\"1\" rowspan=\"1\"><p>シミュレーション(シミュレータ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>シミュレーション 現実の動きをコンピューの他の中で再現して試すこと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:20:39</p></td><td colspan=\"1\" rowspan=\"1\"><p>デジタルツイン</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>デジタルツイン 実物と同じ形と動きを持つ コンピュータの中の双子</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:20:50</p></td><td colspan=\"1\" rowspan=\"1\"><p>物理エンジン</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>物理エンジン 重力 やぶつかりなど物の動きを計算で再現するソフトの心臓部</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:21:00</p></td><td colspan=\"1\" rowspan=\"1\"><p>タイムステップ</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>タイムステップ シュミレーションで時間を一度に進める 刻み幅</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:21:13</p></td><td colspan=\"1\" rowspan=\"1\"><p>剛性と減衰(スティフネスとダンピング)</p></td><td colspan=\"1\" rowspan=\"1\"><p>85%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>強制塔 元帥 関節 を目標へ 引き戻す バネの強さと 揺れを抑える ブレーキの強さ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:21:22</p></td><td colspan=\"1\" rowspan=\"1\"><p>コリジョン形状</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>コリジョン 形状 ぶつかりの判定に使う 見た目より 単純にした形</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:21:34</p></td><td colspan=\"1\" rowspan=\"1\"><p>URDF</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>urdf ロボットのリンクと関節の構成を書き表す標準のファイル形式</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:21:44</p></td><td colspan=\"1\" rowspan=\"1\"><p>ROS 2</p></td><td colspan=\"1\" rowspan=\"1\"><p>88%</p></td><td colspan=\"1\" rowspan=\"1\"><p>88%</p></td><td colspan=\"1\" rowspan=\"1\"><p>露出 ロボットのソフトを部品に分けて つなぎ合わせるための共通基盤</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:21:57</p></td><td colspan=\"1\" rowspan=\"1\"><p>ノードとトピック</p></td><td colspan=\"1\" rowspan=\"1\"><p>82%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ノードとトピックロスで処理の単位とその間を流れるデータの通り道</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:22:08</p></td><td colspan=\"1\" rowspan=\"1\"><p>MoveIt</p></td><td colspan=\"1\" rowspan=\"1\"><p>90%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>MOVE it アームの動作計画をまとめて扱う ロスの代表的な道具</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:22:19</p></td><td colspan=\"1\" rowspan=\"1\"><p>Gazebo</p></td><td colspan=\"1\" rowspan=\"1\"><p>75%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ガズボン ロスト 組み合わせて使われてきた無料のロボット用 シミュレーター</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:22:34</p></td><td colspan=\"1\" rowspan=\"1\"><p>Isaac Sim</p></td><td colspan=\"1\" rowspan=\"1\"><p>73%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>イザックシム NVIDIA が提供する GPU で動く 高精細なロボットというシュミレーター</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:22:45</p></td><td colspan=\"1\" rowspan=\"1\"><p>Isaac Sim</p></td><td colspan=\"1\" rowspan=\"1\"><p>83%</p></td><td colspan=\"1\" rowspan=\"1\"><p>98%</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイザック SIM NVIDIA が提供する GPU で動く 高精細なロボット用 シュミレーター</p></td></tr></tbody></table></div><div class=\"table\"><table><tbody><tr><td colspan=\"1\" rowspan=\"1\"><p>15:23:10</p></td><td colspan=\"1\" rowspan=\"1\"><p>並列シミュレーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>並列 シュミレーション 同じ環境を何個も同時に動かしてデータを一気に集めること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:23:20</p></td><td colspan=\"1\" rowspan=\"1\"><p>sim2real(シムツーリアル)</p></td><td colspan=\"1\" rowspan=\"1\"><p>87%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>SIM 2 リアルシュミレーションで作った 動きや 学習結果を実機へ持っていくこと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:23:29</p></td><td colspan=\"1\" rowspan=\"1\"><p>リアリティギャップ(sim2realギャップ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>リアリティ ギャップ シミュレーションと現実の間にあるズレのこと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:23:44</p></td><td colspan=\"1\" rowspan=\"1\"><p>ドメインランダマイゼーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ドメイン ライダー マイゼーションシミュレーションの条件をわざとばらつかせ 現実のずれに強くする方法</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:23:52</p></td><td colspan=\"1\" rowspan=\"1\"><p>合成データ</p></td><td colspan=\"1\" rowspan=\"1\"><p>52%</p></td><td colspan=\"1\" rowspan=\"1\"><p>52%</p></td><td colspan=\"1\" rowspan=\"1\"><p>合成データシミュレーションや 合成</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:24:00</p></td><td colspan=\"1\" rowspan=\"1\"><p>合成データ</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>合成データシミュレーションや 画像 生成で作った学習用のデータ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:24:12</p></td><td colspan=\"1\" rowspan=\"1\"><p>real2sim(リアルツーシム)</p></td><td colspan=\"1\" rowspan=\"1\"><p>79%</p></td><td colspan=\"1\" rowspan=\"1\"><p>82%</p></td><td colspan=\"1\" rowspan=\"1\"><p>Real to SIM 現実のものや動きを 測り とって シミュレーションの中に</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:24:24</p></td><td colspan=\"1\" rowspan=\"1\"><p>real2sim(リアルツーシム)</p></td><td colspan=\"1\" rowspan=\"1\"><p>85%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>リアル トゥー SIM 現実のものや動きを測りとって シミュレーションの中に再現すること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:24:34</p></td><td colspan=\"1\" rowspan=\"1\"><p>画素(ピクセル)</p></td><td colspan=\"1\" rowspan=\"1\"><p>88%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>画素デジタル画素を作る色の数値を持った最小の点</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:24:44</p></td><td colspan=\"1\" rowspan=\"1\"><p>色空間(RGB・HSV)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>色空間 色を数字で表す時の表し方の決まりごと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:24:58</p></td><td colspan=\"1\" rowspan=\"1\"><p>二値化(しきい値処理)</p></td><td colspan=\"1\" rowspan=\"1\"><p>74%</p></td><td colspan=\"1\" rowspan=\"1\"><p>79%</p></td><td colspan=\"1\" rowspan=\"1\"><p>閾値処理 決めた 堺市の値でガソリン 画素を白と黒の2つに分ける 処理</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:25:11</p></td><td colspan=\"1\" rowspan=\"1\"><p>二値化(しきい値処理)</p></td><td colspan=\"1\" rowspan=\"1\"><p>86%</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>閾値処理 決めた 堺市 境目の値で画素を白と黒の2つに分ける 処理</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:25:21</p></td><td colspan=\"1\" rowspan=\"1\"><p>二値化(しきい値処理)</p></td><td colspan=\"1\" rowspan=\"1\"><p>86%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>閾値処理 決めた 境目の値で画素を白と黒の2つに分ける 処理</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:25:34</p></td><td colspan=\"1\" rowspan=\"1\"><p>モルフォロジー演算</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>モロフォジー演算 白黒画像の小さな粒や穴を形の大きさで整える処理</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:25:45</p></td><td colspan=\"1\" rowspan=\"1\"><p>輪郭抽出(コンター)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>輪郭抽出 白黒画像から物の外側の縁を線としてたどる 処理</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:26:00</p></td><td colspan=\"1\" rowspan=\"1\"><p>ROI(関心領域)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>Roi 画像の中で処理の対象にする範囲だけを切り出した部分</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:26:10</p></td><td colspan=\"1\" rowspan=\"1\"><p>テンプレートマッチング</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>テンプレート マッチング 日本の画像を少しずつずらし 似ている場所を探す方法</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:26:21</p></td><td colspan=\"1\" rowspan=\"1\"><p>特徴点マッチング</p></td><td colspan=\"1\" rowspan=\"1\"><p>83%</p></td><td colspan=\"1\" rowspan=\"1\"><p>83%</p></td><td colspan=\"1\" rowspan=\"1\"><p>徳井町店 マッチング 2点の画像から同じ場所を指す 特徴的な点を結びつける方法</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:26:34</p></td><td colspan=\"1\" rowspan=\"1\"><p>物体検出(オブジェクトディテクション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>物体検出画像の中のものを見つけ 四角い枠と名前と確からしさで返す 処理</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:26:46</p></td><td colspan=\"1\" rowspan=\"1\"><p>バウンディングボックス(BBox)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>バウンディングボックス 検出したものを囲むように描く 画像上の四角い枠</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:26:57</p></td><td colspan=\"1\" rowspan=\"1\"><p>IoU</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>iou 2つの枠がどれだけ重なっているかを 0から1で表す指標</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:27:10</p></td><td colspan=\"1\" rowspan=\"1\"><p>セグメンテーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>セグメンテーション 画像のガスごとにどのものに属するかを 塗り分ける 処理</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:27:21</p></td><td colspan=\"1\" rowspan=\"1\"><p>カメラ校正(キャリブレーション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>カメラ 構成 カメラの特性や 置き場所を数値として求めておく 作業</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:27:32</p></td><td colspan=\"1\" rowspan=\"1\"><p>カメラ校正(キャリブレーション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>21%</p></td><td colspan=\"1\" rowspan=\"1\"><p>21%</p></td><td colspan=\"1\" rowspan=\"1\"><p>内部パラメータ 焦点距離や レンズの歪み など カメラ 自体が持つ性質の値</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:27:47</p></td><td colspan=\"1\" rowspan=\"1\"><p>内部パラメータ</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>内部パラメータ 焦点距離や レンズの歪み など カメラ 自体が持つ性質の値</p></td></tr></tbody></table></div><div class=\"table\"><table><tbody><tr><td colspan=\"1\" rowspan=\"1\"><p>15:27:58</p></td><td colspan=\"1\" rowspan=\"1\"><p>外部パラメータ</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>外部パラメータ カメラが空間のどこにどの向きで置かれているかは 表す値</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:28:08</p></td><td colspan=\"1\" rowspan=\"1\"><p>ホモグラフィ</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ホモグラフィー ある平面の見え方を別の視点から見た平面へ移し替える 変換</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:28:19</p></td><td colspan=\"1\" rowspan=\"1\"><p>視差(パララックス)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>パララックス 見る位置が変わると 物の見える位置がずれて見える現象</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:28:30</p></td><td colspan=\"1\" rowspan=\"1\"><p>深度カメラ(RGB-Dカメラ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>進路カメラ色の画像と一緒に 各画素までの距離も測れるカメラ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:28:42</p></td><td colspan=\"1\" rowspan=\"1\"><p>点群(ポイントクラウド)</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ポイントクラウド 物の表面を3次元の位置を持つ たくさんの点の集まりで表したデータ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:28:52</p></td><td colspan=\"1\" rowspan=\"1\"><p>経由点(ウェイポイント)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ウェイポイント 動きの途中で通るように指定する目印となる点</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:29:07</p></td><td colspan=\"1\" rowspan=\"1\"><p>経路と軌道(パスとトラジェクトリ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>パスト トラジェクトリー 経路は通る道筋 起動はそこに時間と速さを加えたもの</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:29:17</p></td><td colspan=\"1\" rowspan=\"1\"><p>補間(インターポレーション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>86%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>インターポーレーション 離れた点と点の間を滑らかな値でつないで埋めること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:29:27</p></td><td colspan=\"1\" rowspan=\"1\"><p>速度プロファイル(加減速)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>速度プロファイル 動き出し から止まるまで速さをどう変えるかの決め方</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:29:40</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節補間と直線補間</p></td><td colspan=\"1\" rowspan=\"1\"><p>81%</p></td><td colspan=\"1\" rowspan=\"1\"><p>95%</p></td><td colspan=\"1\" rowspan=\"1\"><p>間接保管と直線 保管 関節の角度でつなぐか 手先の直線でつなぐかという保管の違い</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:29:55</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節補間と直線補間</p></td><td colspan=\"1\" rowspan=\"1\"><p>81%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節保管と直接 保管 関節の角度でつなぐか 手先の直線でつなぐかという保管の違い</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:30:07</p></td><td colspan=\"1\" rowspan=\"1\"><p>ティーチング(教示)</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ティーチング ロボットンに動きやす 1 を人が直接教えて記録させること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:30:16</p></td><td colspan=\"1\" rowspan=\"1\"><p>把持(グラスプ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>83%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>恥 グリッパーやハンドで 物を掴んで保持すること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:30:26</p></td><td colspan=\"1\" rowspan=\"1\"><p>ピックアンドプレース(Pick&Place)</p></td><td colspan=\"1\" rowspan=\"1\"><p>74%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ビッグ&プレイス 物を掴んで持ち上げ 別の場所へ運んでおく 一連の作業</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:30:36</p></td><td colspan=\"1\" rowspan=\"1\"><p>アプローチと退避</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>アプローチと対比 対象の手前まで近づく 動きと作業後に離れる 動きのこと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:30:46</p></td><td colspan=\"1\" rowspan=\"1\"><p>許容誤差(トレランス)</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>許容誤差 目標に届いたとみなして良い ズレの許される範囲</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:30:57</p></td><td colspan=\"1\" rowspan=\"1\"><p>動作計画(モーションプランニング)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>モーションプランニング障害物を避けながら 目標までの動きを自動で求めること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:31:09</p></td><td colspan=\"1\" rowspan=\"1\"><p>衝突回避</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>衝突回避 ロボットが周りのものや人 自分の体にぶつからないようにすること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:31:20</p></td><td colspan=\"1\" rowspan=\"1\"><p>視覚サーボ(ビジュアルサーボ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ビジュアルサーボ カメラで見た ズレを使い 目標に合うまで 動き直し続ける 制御</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:31:31</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイインハンドとアイトゥハンド</p></td><td colspan=\"1\" rowspan=\"1\"><p>46%</p></td><td colspan=\"1\" rowspan=\"1\"><p>66%</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイ イン ハンド と I to Hand カメラを手先につけるか先に</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:31:42</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイインハンドとアイトゥハンド</p></td><td colspan=\"1\" rowspan=\"1\"><p>76%</p></td><td colspan=\"1\" rowspan=\"1\"><p>98%</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイ イン ハンド と I to Hand カメラを手先につけるか外に固定するかのという2つの配置</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:31:53</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイインハンドとアイトゥハンド</p></td><td colspan=\"1\" rowspan=\"1\"><p>78%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイ イン ハンド と I to Hand カメラを手先につけるか外に固定するかという2つの配置</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:32:05</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハンドアイ校正(ハンドアイキャリブレーション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハンドアイ 構成 カメラとロボットの位置と向きの関係を求める作業</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:32:17</p></td><td colspan=\"1\" rowspan=\"1\"><p>受け渡し(ハンドオーバー)</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>受け渡し ロボットと人の間で物を手渡したり 受け取ったりする動作</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:32:28</p></td><td colspan=\"1\" rowspan=\"1\"><p>コンプライアンス(柔らかさ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>コンプライアンス 外から力を受けた時 ロボットが逆らわずに少したわむ 性質</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:32:40</p></td><td colspan=\"1\" rowspan=\"1\"><p>インピーダンス制御</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>インピーダンス制御バネとダンパーのような柔らかさを制御でロボットに持たせる方法</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:32:52</p></td><td colspan=\"1\" rowspan=\"1\"><p>学習(トレーニング)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>トレーニングデータを使ってモデルの中の数値を少しずつ調整すること</p></td></tr></tbody></table></div><div class=\"table\"><table><tbody><tr><td colspan=\"1\" rowspan=\"1\"><p>15:33:02</p></td><td colspan=\"1\" rowspan=\"1\"><p>推論(インファレンス)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>インファレンス 学習済みのモデルに入力を渡し 答えを出させること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:33:14</p></td><td colspan=\"1\" rowspan=\"1\"><p>観測と行動(オブザベーションとアクション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>90%</p></td><td colspan=\"1\" rowspan=\"1\"><p>95%</p></td><td colspan=\"1\" rowspan=\"1\"><p>オブザベーションとアクションロボットが受け取る情報とそれを元に 動き の指令</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:33:28</p></td><td colspan=\"1\" rowspan=\"1\"><p>観測と行動(オブザベーションとアクション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>83%</p></td><td colspan=\"1\" rowspan=\"1\"><p>83%</p></td><td colspan=\"1\" rowspan=\"1\"><p>オブザベーションとアクションロボットが受け取る情報とそれをもとに 動き それをもとに出す動きの指令</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:33:39</p></td><td colspan=\"1\" rowspan=\"1\"><p>観測と行動(オブザベーションとアクション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>オブザベーションとアクションロボットが受け取る情報とそれをもとに出す動きの指令</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:33:49</p></td><td colspan=\"1\" rowspan=\"1\"><p>方策(ポリシー)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ポリシー 感染観測を受け取り次にどう動くかを決める仕組み</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:33:58</p></td><td colspan=\"1\" rowspan=\"1\"><p>方策(ポリシー)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ポリシー 観測を受け取り次にどう動くかを決める仕組み</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:34:08</p></td><td colspan=\"1\" rowspan=\"1\"><p>エピソード</p></td><td colspan=\"1\" rowspan=\"1\"><p>85%</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>エピソード タスクの開始から終了までの一区切りの思考</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:34:18</p></td><td colspan=\"1\" rowspan=\"1\"><p>デモンストレーション(実演データ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>デモンストレーション 人がお手本として操作して見せた動きの記録</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:34:27</p></td><td colspan=\"1\" rowspan=\"1\"><p>テレオペレーション(遠隔操作)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>テレオペレーション 人が離れた場所からロボットをリアルタイムに操作すること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:34:36</p></td><td colspan=\"1\" rowspan=\"1\"><p>模倣学習</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>魔法 学習人の実演データをお手本にロボットに動きを覚えさせる学習</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:34:49</p></td><td colspan=\"1\" rowspan=\"1\"><p>行動クローニング(BC)</p></td><td colspan=\"1\" rowspan=\"1\"><p>89%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>高等 クローニング 観測と行動の組をそのまま 真似るように学ぶ最も単純な魔法 学習</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:35:00</p></td><td colspan=\"1\" rowspan=\"1\"><p>強化学習(RL)</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>強化学習 試行錯誤の結果に点数をつけて点数が高くなる動きを学ぶ方法</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:35:11</p></td><td colspan=\"1\" rowspan=\"1\"><p>報酬(リワード)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>リワード 行動の結果がどれだけ良かったかを示す 強化学習の点数</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:35:26</p></td><td colspan=\"1\" rowspan=\"1\"><p>汎化</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハンカ 学習で見たことのない状況でもうまく動けること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:35:36</p></td><td colspan=\"1\" rowspan=\"1\"><p>汎化</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>文化学習 で見たことのない状況でもうまく動けること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:35:48</p></td><td colspan=\"1\" rowspan=\"1\"><p>過学習(オーバーフィッティング)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>オーバー フィッティング学習データを覚えすぎて 新しいデータに弱くなる状態</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:36:01</p></td><td colspan=\"1\" rowspan=\"1\"><p>基盤モデル(ファウンデーションモデル)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ファンデーションモデル 大量データの事前に学習され 多くの用途に転用できる大きなモデル</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:36:13</p></td><td colspan=\"1\" rowspan=\"1\"><p>事前学習(プレトレーニング)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>事前学習 特定の作業の前に大量の一般的なデータで学ばせておく 段階</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:36:24</p></td><td colspan=\"1\" rowspan=\"1\"><p>ファインチューニング</p></td><td colspan=\"1\" rowspan=\"1\"><p>95%</p></td><td colspan=\"1\" rowspan=\"1\"><p>95%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ファインチューニング事前学習済みのモデルを自分のデータで追加学習して合わせること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:36:35</p></td><td colspan=\"1\" rowspan=\"1\"><p>VLM(視覚言語モデル)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>vlm 画像と言葉を一緒に理解し言葉で答える AI モデル</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:36:45</p></td><td colspan=\"1\" rowspan=\"1\"><p>VLA(視覚言語行動モデル)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>vla 画像と言葉の指示からロボットの動きを直接出力するモデル</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:36:54</p></td><td colspan=\"1\" rowspan=\"1\"><p>試行(トライアル)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>トライアル 同じ条件でロボットに動作を1回やらせること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:36:59</p></td><td colspan=\"1\" rowspan=\"1\"><p>成功率(サクセスレート)</p></td><td colspan=\"1\" rowspan=\"1\"><p>21%</p></td><td colspan=\"1\" rowspan=\"1\"><p>21%</p></td><td colspan=\"1\" rowspan=\"1\"><p>サクセストレー</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:37:07</p></td><td colspan=\"1\" rowspan=\"1\"><p>成功率(サクセスレート)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>サクセス レート 試行した回数のうち 成功した回数が占める割合</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:37:16</p></td><td colspan=\"1\" rowspan=\"1\"><p>成功条件</p></td><td colspan=\"1\" rowspan=\"1\"><p>81%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>性交 条件 何を持って成功とするかを 思考の前に決めた 基準</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:37:26</p></td><td colspan=\"1\" rowspan=\"1\"><p>ロギング(ログ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>79%</p></td><td colspan=\"1\" rowspan=\"1\"><p>86%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ロギング動作中の状態や結果を後で見返す 量 記録すること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:37:37</p></td><td colspan=\"1\" rowspan=\"1\"><p>データセット</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>データセット 学習や評価に使うために集めて整理したデータのまとまり</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:37:46</p></td><td colspan=\"1\" rowspan=\"1\"><p>失敗モード</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>失敗モード 失敗をその置き方や 原因 ごとに分けた 種類</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:37:54</p></td><td colspan=\"1\" rowspan=\"1\"><p>ベースライン</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ベースライン 改善の効果を比べるための基準となる 結果</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:38:04</p></td><td colspan=\"1\" rowspan=\"1\"><p>ベースライン</p></td><td colspan=\"1\" rowspan=\"1\"><p>12%</p></td><td colspan=\"1\" rowspan=\"1\"><p>12%</p></td><td colspan=\"1\" rowspan=\"1\"><p>再現性 同じ手順と条件で同じ結果が もう一度 得られること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:38:14</p></td><td colspan=\"1\" rowspan=\"1\"><p>誤差</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>誤差 測った値や 到達した位置と本当の値とのズレ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:38:24</p></td><td colspan=\"1\" rowspan=\"1\"><p>系統誤差</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>系統誤差原因が決まっていて いつも同じ向きにずれる 誤差</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:38:34</p></td><td colspan=\"1\" rowspan=\"1\"><p>偶然誤差(ばらつき)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>偶然誤差 毎回 ランダムに上下する向きの決まらない 誤差</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:38:47</p></td><td colspan=\"1\" rowspan=\"1\"><p>正確さと精密さ</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>正確さと精密さ 狙いに近いのが正確さ 毎回 揃うのが 精密さ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:38:55</p></td><td colspan=\"1\" rowspan=\"1\"><p>外れ値</p></td><td colspan=\"1\" rowspan=\"1\"><p>88%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハズレ 地 他のデータから大きく離れた 例外的な値のこと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:39:05</p></td><td colspan=\"1\" rowspan=\"1\"><p>アノテーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>アノテーション画像やデータに人が正解の情報を書き込む 作業</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:39:15</p></td><td colspan=\"1\" rowspan=\"1\"><p>正解データ(グラウンドトゥルース)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>正解 データ 評価の基準にする本当の値とみなす データ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:39:24</p></td><td colspan=\"1\" rowspan=\"1\"><p>評価指標(メトリクス)</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>評価指標性能 数字で表すために使う決まったものさし</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:39:34</p></td><td colspan=\"1\" rowspan=\"1\"><p>ベンチマーク</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ベンチマーク 性能 比べるための共通の課題と評価のやり方</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:39:44</p></td><td colspan=\"1\" rowspan=\"1\"><p>統計的なぶれ(信頼区間)</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>統計的なブレ 思考回数が少ないと成功率が偶然だけで動く 幅</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:39:54</p></td><td colspan=\"1\" rowspan=\"1\"><p>分布外(OOD)</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>分布 街 OOD 学習の時に見たことのない新しい条件や データ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:40:05</p></td><td colspan=\"1\" rowspan=\"1\"><p>アブレーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>アブレーション部品を一つずつ外してそれぞれの効き目を確かめる実験</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:40:17</p></td><td colspan=\"1\" rowspan=\"1\"><p>反復開発(イテレーション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>反復開発 小さく作って試し直す 流れを何度も回す 進め方</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:40:27</p></td><td colspan=\"1\" rowspan=\"1\"><p>仮説検証</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>仮設 検証 原因の予想を立て実験で それが正しいか確かめること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:40:37</p></td><td colspan=\"1\" rowspan=\"1\"><p>切り分け</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>切り分け問題の範囲を少しずつ 狭めて原因の場所を特定すること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:40:46</p></td><td colspan=\"1\" rowspan=\"1\"><p>ボトルネック</p></td><td colspan=\"1\" rowspan=\"1\"><p>86%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ボトルネック 全体の速さや 性能を一番強く 制限している部分</p></td></tr></tbody></table></div><div class=\"table\"><table><tbody><tr><td colspan=\"1\" rowspan=\"1\"><p>15:40:58</p></td><td colspan=\"1\" rowspan=\"1\"><p>根本原因分析(RCA)</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>根本原因分析表に出た症状ではなく 問題を生んだ 大元 突き止めること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:41:10</p></td><td colspan=\"1\" rowspan=\"1\"><p>段階的検証</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>段階的検証小さな単位から順に確かめ 少しずつ 範囲を広げる 検証</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:41:19</p></td><td colspan=\"1\" rowspan=\"1\"><p>ドライラン(dry-run)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ドライラン 実際には動かさず 計画や 処理の流れだけを試すこと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:41:29</p></td><td colspan=\"1\" rowspan=\"1\"><p>フェイルセーフ</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ペールセーフ故障や異常が起きても安全な側に倒れるようにする設計</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:41:41</p></td><td colspan=\"1\" rowspan=\"1\"><p>フォールバック</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ポールバック 主な方法が使えない時に切り替える 代わりの手段</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:41:54</p></td><td colspan=\"1\" rowspan=\"1\"><p>非常停止(E-stop)</p></td><td colspan=\"1\" rowspan=\"1\"><p>84%</p></td><td colspan=\"1\" rowspan=\"1\"><p>97%</p></td><td colspan=\"1\" rowspan=\"1\"><p>非常停止 E ストップ 非常時に人がすぐ ロボットを止めるための仕組み</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:42:03</p></td><td colspan=\"1\" rowspan=\"1\"><p>インターロック</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>インターロック 条件が揃わない限り 動作できないようにする仕組み</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:42:15</p></td><td colspan=\"1\" rowspan=\"1\"><p>安全マージン</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>安全マージン 限界ギリギリではなく 余裕を残して決める設定の幅</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:42:25</p></td><td colspan=\"1\" rowspan=\"1\"><p>最小構成(MVP)</p></td><td colspan=\"1\" rowspan=\"1\"><p>83%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>最小構成 目的を確かめるのに必要な一番小さな形</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:42:37</p></td><td colspan=\"1\" rowspan=\"1\"><p>技術的負債</p></td><td colspan=\"1\" rowspan=\"1\"><p>85%</p></td><td colspan=\"1\" rowspan=\"1\"><p>91%</p></td><td colspan=\"1\" rowspan=\"1\"><p>技術的 夫妻その場しのぎの実装が後で修正の手間として帰ってくること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:42:47</p></td><td colspan=\"1\" rowspan=\"1\"><p>コードレビュー</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>コードレビュー書いたコードを別の人や 別の目で確認すること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:42:58</p></td><td colspan=\"1\" rowspan=\"1\"><p>HRI(ヒューマンロボットインタラクション)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ヒューマンロボットインタラクション人とロボットのやり取りや 関わり方を扱う分野</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:43:10</p></td><td colspan=\"1\" rowspan=\"1\"><p>動きの分かりやすさ(レジビリティ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>レジビリティ 動きを見ただけでロボットの狙いが人に伝わる性質</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:43:20</p></td><td colspan=\"1\" rowspan=\"1\"><p>ヒューマンインザループ</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ヒューマンインザループ 判断や修正の流れの中に人を組み込んだ 仕組み</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:43:31</p></td><td colspan=\"1\" rowspan=\"1\"><p>産業用ロボット</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>産業用ロボット工場で溶接や 組み立て 搬送などを自動で行うロボット</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:43:42</p></td><td colspan=\"1\" rowspan=\"1\"><p>協働ロボット(コボット)</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>共同ロボット 柵なしで人のそばで 一緒に働けるよう作られたロボット</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:43:51</p></td><td colspan=\"1\" rowspan=\"1\"><p>サービスロボット</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>サービスロボット工場の外で人の生活や仕事を手伝う ロボット</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:44:02</p></td><td colspan=\"1\" rowspan=\"1\"><p>AGV(無人搬送車)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>AGV 床の線や印に沿って決まった経路で荷物を運ぶ車</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:44:12</p></td><td colspan=\"1\" rowspan=\"1\"><p>AMR(自律移動ロボット)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>AMR 地図とセンサで自分で道を選んで移動するロボット</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:44:22</p></td><td colspan=\"1\" rowspan=\"1\"><p>ヒューマノイド</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ヒューマノイド 人の近い体の形をした 二足歩行 などのロボット</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:44:31</p></td><td colspan=\"1\" rowspan=\"1\"><p>省人化</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>精進化 作業に必要な人の数を機械化や自動化で減らすこと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:44:41</p></td><td colspan=\"1\" rowspan=\"1\"><p>タクトタイム</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>タクトタイム 製品1個を作るのに許される 精算上の目標 時間</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:44:52</p></td><td colspan=\"1\" rowspan=\"1\"><p>SIer(システムインテグレータ)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>システムインテグレータ ロボットを現場に合わせて 組み込み 仕組みとして納める会社</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:45:03</p></td><td colspan=\"1\" rowspan=\"1\"><p>PoC(概念実証)</p></td><td colspan=\"1\" rowspan=\"1\"><p>88%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ポック 本格導入の前に小さく 試して効果を確かめること</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:45:17</p></td><td colspan=\"1\" rowspan=\"1\"><p>ROI(投資対効果)</p></td><td colspan=\"1\" rowspan=\"1\"><p>88%</p></td><td colspan=\"1\" rowspan=\"1\"><p>88%</p></td><td colspan=\"1\" rowspan=\"1\"><p>Roi かけたをかけたお金に対してどれだけの効果が返ってくるかの指標</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:45:28</p></td><td colspan=\"1\" rowspan=\"1\"><p>安全柵</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>安全策ロボットの動く範囲に人が入れないよう 囲う 柵</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:45:40</p></td><td colspan=\"1\" rowspan=\"1\"><p>リスクアセスメント</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>リスクアセスメント危険を洗い出し その大きさを見積もって対策を決める手順</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:45:51</p></td><td colspan=\"1\" rowspan=\"1\"><p>安全規格(ISO 10218)</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>安全規格 産業用ロボットの安全について定めた国際規格</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:46:02</p></td><td colspan=\"1\" rowspan=\"1\"><p>機能安全</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>93%</p></td><td colspan=\"1\" rowspan=\"1\"><p>昨日安全制御の仕組みが壊れても安全を保てるようにする考え方</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:46:13</p></td><td colspan=\"1\" rowspan=\"1\"><p>RaaS(ロボットアズアサービス)</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>ロボット アザー サービスロボットを買わずに 月額などの利用料で使う 仕組み</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>15:46:22</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィジカルAI(Physical AI)</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>96%</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィジカル AI 現実世界を認識し 体を動かして働く AI</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>16:07:51</p></td><td colspan=\"1\" rowspan=\"1\"><p>関節(ジョイント)</p></td><td colspan=\"1\" rowspan=\"1\"><p>85%</p></td><td colspan=\"1\" rowspan=\"1\"><p>88%</p></td><td colspan=\"1\" rowspan=\"1\"><p>股関節ロボットの部品と部品をつなぎ動きを産むつなぎ目</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>16:08:56</p></td><td colspan=\"1\" rowspan=\"1\"><p>リンク</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>100%</p></td><td colspan=\"1\" rowspan=\"1\"><p>リンク関節と関節の間をつなぐ硬い骨組みの部分</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>16:10:24</p></td><td colspan=\"1\" rowspan=\"1\"><p>自由度(DOF)</p></td><td colspan=\"1\" rowspan=\"1\"><p>85%</p></td><td colspan=\"1\" rowspan=\"1\"><p>92%</p></td><td colspan=\"1\" rowspan=\"1\"><p>PO Fロボットがお互いに独立して動かせる簿記の方向の数</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>16:13:25</p></td><td colspan=\"1\" rowspan=\"1\"><p>回転関節と直動関節</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>94%</p></td><td colspan=\"1\" rowspan=\"1\"><p>回転関節と食道関節回って角度が変わる関節とまっすぐ伸び縮みする関節</p></td></tr></tbody></table></div><p></p><h2 id=\"index_xj0uFjsd\">聞き違いの一覧(homophones・56組)</h2><p>各組の中で、カードに出てくる書き方に、話した言葉の別の書き方をそろえます。英字は小文字で持っています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>カードの書き方</p></th><th colspan=\"1\" rowspan=\"1\" scope=\"col\"><p>そろえる言い方</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>補間</p></td><td colspan=\"1\" rowspan=\"1\"><p>保管</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>校正</p></td><td colspan=\"1\" rowspan=\"1\"><p>構成、厚生</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>直動</p></td><td colspan=\"1\" rowspan=\"1\"><p>直導、直道</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>成功</p></td><td colspan=\"1\" rowspan=\"1\"><p>性交</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>試行</p></td><td colspan=\"1\" rowspan=\"1\"><p>思考、施行</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>しきい</p></td><td colspan=\"1\" rowspan=\"1\"><p>閾、堺市、敷居</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ros</p></td><td colspan=\"1\" rowspan=\"1\"><p>ロス</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>gazebo</p></td><td colspan=\"1\" rowspan=\"1\"><p>ガゼボ、ガズボン、ガズボ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ピック</p></td><td colspan=\"1\" rowspan=\"1\"><p>ビッグ、ビック、pick</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>プレース</p></td><td colspan=\"1\" rowspan=\"1\"><p>プレイス、place</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>アイトゥハンド</p></td><td colspan=\"1\" rowspan=\"1\"><p>itohand、アイトゥーハンド</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>アイインハンド</p></td><td colspan=\"1\" rowspan=\"1\"><p>iinhand</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>シミュレータ</p></td><td colspan=\"1\" rowspan=\"1\"><p>シュミレータ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>シミュレーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>シュミレーション</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>isaac</p></td><td colspan=\"1\" rowspan=\"1\"><p>イザック、アイザック</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>real2sim</p></td><td colspan=\"1\" rowspan=\"1\"><p>realtosim、リアルツーシム、リアルトゥーsim、リアルトゥsim、リアルtosim</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>sim2real</p></td><td colspan=\"1\" rowspan=\"1\"><p>simtoreal、シムツーリアル、sim2リアル、シム2リアル、simトゥーリアル</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>二つ</p></td><td colspan=\"1\" rowspan=\"1\"><p>2つ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>一つ</p></td><td colspan=\"1\" rowspan=\"1\"><p>1つ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>三つ</p></td><td colspan=\"1\" rowspan=\"1\"><p>3つ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>掴</p></td><td colspan=\"1\" rowspan=\"1\"><p>つか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>後で</p></td><td colspan=\"1\" rowspan=\"1\"><p>あとで</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>付け</p></td><td colspan=\"1\" rowspan=\"1\"><p>つけ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>置く</p></td><td colspan=\"1\" rowspan=\"1\"><p>おく</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>とおり</p></td><td colspan=\"1\" rowspan=\"1\"><p>通り</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ほか</p></td><td colspan=\"1\" rowspan=\"1\"><p>他</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>わかり</p></td><td colspan=\"1\" rowspan=\"1\"><p>分かり</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>でき</p></td><td colspan=\"1\" rowspan=\"1\"><p>出来</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>くらい</p></td><td colspan=\"1\" rowspan=\"1\"><p>位</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ころ</p></td><td colspan=\"1\" rowspan=\"1\"><p>頃</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ずれ</p></td><td colspan=\"1\" rowspan=\"1\"><p>ズレ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>もの</p></td><td colspan=\"1\" rowspan=\"1\"><p>物</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>こと</p></td><td colspan=\"1\" rowspan=\"1\"><p>事</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>とき</p></td><td colspan=\"1\" rowspan=\"1\"><p>時</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ため</p></td><td colspan=\"1\" rowspan=\"1\"><p>為</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>いちばん</p></td><td colspan=\"1\" rowspan=\"1\"><p>一番</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>あいだ</p></td><td colspan=\"1\" rowspan=\"1\"><p>間</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>なめらか</p></td><td colspan=\"1\" rowspan=\"1\"><p>滑らか</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>すき</p></td><td colspan=\"1\" rowspan=\"1\"><p>隙</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>つく</p></td><td colspan=\"1\" rowspan=\"1\"><p>作</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>もと</p></td><td colspan=\"1\" rowspan=\"1\"><p>元</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>把持</p></td><td colspan=\"1\" rowspan=\"1\"><p>恥</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>行動</p></td><td colspan=\"1\" rowspan=\"1\"><p>高等</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>模倣</p></td><td colspan=\"1\" rowspan=\"1\"><p>魔法</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>剛性</p></td><td colspan=\"1\" rowspan=\"1\"><p>強制</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>減衰</p></td><td colspan=\"1\" rowspan=\"1\"><p>元帥</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>外れ値</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハズレ地、はずれち</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>poc</p></td><td colspan=\"1\" rowspan=\"1\"><p>ポック</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>クォータニオン</p></td><td colspan=\"1\" rowspan=\"1\"><p>コオタニオン、クオタニオン</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>二枚</p></td><td colspan=\"1\" rowspan=\"1\"><p>2枚</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>バックラッシ</p></td><td colspan=\"1\" rowspan=\"1\"><p>バックラッシュ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>stop</p></td><td colspan=\"1\" rowspan=\"1\"><p>ストップ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>sim</p></td><td colspan=\"1\" rowspan=\"1\"><p>シム</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>生む</p></td><td colspan=\"1\" rowspan=\"1\"><p>産む</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>互い</p></td><td colspan=\"1\" rowspan=\"1\"><p>お互い</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>dof</p></td><td colspan=\"1\" rowspan=\"1\"><p>pof</p></td></tr></tbody></table></div><p></p><h2 id=\"index_RbStIw79\">ページのテンプレート template.html の全文</h2><p><code>/*DATA*/null</code> と <code>/*PROGRESS*/{}</code> の2か所に、<code>build_review.py</code> が書き出すときにデータと練習の集計を埋め込みます。</p><p>html</p><pre><code class=\"language-html\"><!doctype html>\n<html lang=\"ja\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n<meta name=\"robots\" content=\"noindex, nofollow\">\n<title>ロボティクスの見直し</title>\n<style>\n/* 仕事で使っている「台本読み合わせ」ページと同じ見た目:横スワイプのカード+その下に「押している間だけ聞く」 */\n:root{\n --bg:#f2f3f5; --fg:#1d2129; --muted:#6b7280; --panel:#ffffff; --line:#e3e5e9;\n --card:#1a1b1f; --card-fg:#ffffff; --card-sub:#a9adb6; --gold:#f2c14e; --time:#c98a12;\n --miss:#fde2e1; --miss-fg:#b42318; --extra:#dbeafe; --extra-fg:#1e40af; --ok:#15803d;\n --btn:#1a1b1f; --btn-fg:#ffffff; --rec:#b42318;\n --font:-apple-system,\"Hiragino Sans\",\"Hiragino Kaku Gothic ProN\",sans-serif;\n}\n@media (prefers-color-scheme: dark){:root:not([data-theme=\"light\"]){\n --bg:#111215; --fg:#e8eaee; --muted:#9aa0aa; --panel:#1c1d22; --line:#2c2e35;\n --card:#000000; --miss:#4a1714; --miss-fg:#ffb4ab; --extra:#13284d; --extra-fg:#bcd3ff; --ok:#4ade80;\n --btn:#e8eaee; --btn-fg:#111215; color-scheme:dark}}\n:root[data-theme=\"dark\"]{\n --bg:#111215; --fg:#e8eaee; --muted:#9aa0aa; --panel:#1c1d22; --line:#2c2e35;\n --card:#000000; --miss:#4a1714; --miss-fg:#ffb4ab; --extra:#13284d; --extra-fg:#bcd3ff; --ok:#4ade80;\n --btn:#e8eaee; --btn-fg:#111215; color-scheme:dark}\n*{box-sizing:border-box}\n[hidden]{display:none!important}\nhtml{-webkit-text-size-adjust:100%}\nbody{margin:0;background:var(--bg);color:var(--fg);font:18px/1.7 var(--font);padding:0 0 calc(24px + env(safe-area-inset-bottom))}\nheader{background:#1a1b1f;color:#fff;padding:calc(12px + env(safe-area-inset-top)) 16px 12px}\nheader h1{margin:0;font-size:21px}\nheader p{margin:2px 0 0;font-size:14px;color:rgba(255,255,255,.75)}\nmain{padding:12px 16px 0;max-width:720px;margin:0 auto}\n.bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0 0 10px}\n.selwrap{position:relative;display:block;width:100%}\nselect{-webkit-appearance:none;appearance:none;display:block;width:100%;font:inherit;font-size:16px;font-weight:600;background:var(--btn);color:var(--btn-fg);border:0;border-radius:10px;padding:10px 44px 10px 14px;margin:0}\n.selwrap::after{content:\"\";position:absolute;right:19px;top:50%;width:9px;height:9px;border-right:2.75px solid var(--btn-fg);border-bottom:2.75px solid var(--btn-fg);border-radius:1px;transform:translateY(-70%) rotate(45deg);pointer-events:none}\n.seg{display:inline-flex;border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--panel)}\n.seg button{border:0;background:transparent;color:var(--fg);font:inherit;font-size:15px;padding:8px 12px}\n.seg button[aria-pressed=\"true\"]{background:var(--btn);color:var(--btn-fg)}\n.seg.step{display:flex;margin:0 0 8px}.seg.step button{flex:1;font-size:16px;font-weight:600;padding:10px 6px}\n.card .w{font-size:27px;line-height:1.5;font-weight:700;margin:8px 0 2px}\n.card .w.small{font-size:20px;margin-bottom:0}\n.info{font-size:14px;color:var(--muted);margin:0 2px 8px}\n.track{display:flex;align-items:flex-start;overflow-x:auto;overflow-y:hidden;transition:height .15s;scroll-snap-type:x mandatory;gap:10px;padding:4px 2px 10px;-webkit-overflow-scrolling:touch;scrollbar-width:none}\n.track::-webkit-scrollbar{display:none}\n.card{flex:0 0 88%;scroll-snap-align:center;background:var(--card);color:var(--card-fg);border-radius:14px;padding:16px 16px 12px;min-height:250px;display:flex;flex-direction:column}\n.card .sec{font-size:13.5px;color:var(--card-sub)}\n.card .lb{font-size:13.5px;font-weight:700;color:var(--gold);margin-top:2px}\n.card .q{font-size:15px;color:var(--card-sub);margin-top:6px}\n.card .t{font-size:25px;line-height:1.75;font-weight:600;flex:1;margin:8px 0}\n.card .t.long{font-size:20px}\n.card .t.mid{font-size:22px}\n.card .t.hide{color:var(--card-sub)}\n.card .b{font-size:14.5px;line-height:1.7;color:var(--card-sub);margin:0 0 8px}\n.card .m{font-size:13.5px;color:var(--time);display:flex;justify-content:space-between;gap:8px}\n.card .m .done{color:#9be3b2}.card .m .done.low{color:#ffb4ab}.card .m .done small{color:#a9adb6;font-size:12.5px;margin-left:4px}\n.pos{font-size:14px;color:var(--muted);text-align:center;margin:0 0 8px;font-variant-numeric:tabular-nums}\n.mic{display:block;width:100%;border:0;border-radius:16px;padding:18px 12px;font:inherit;font-size:20px;font-weight:700;background:var(--btn);color:var(--btn-fg);-webkit-user-select:none;user-select:none;-webkit-touch-callout:none;touch-action:manipulation}\n.mic.on{background:var(--rec);color:#fff}\n.mic:disabled{opacity:.45}\n.mic small{display:block;font-size:13.5px;font-weight:400;opacity:.8}\n.res{background:var(--panel);border-radius:14px;padding:12px 14px;margin:12px 0 0;border:1px solid var(--line)}\n.res h2{font-size:15px;margin:0 0 6px;color:var(--muted);font-weight:600}\n.res .said{font-size:19px;line-height:1.8;min-height:1.8em;word-break:break-all}\n.res .cmp{font-size:19px;line-height:1.8;word-break:break-all}\n.res .miss{background:var(--miss);color:var(--miss-fg);border-radius:3px}\n.res .kw{display:inline-block;border-radius:6px;padding:0 8px;margin:2px 6px 2px 0;background:var(--extra);color:var(--extra-fg)}\n.res .kw.miss{background:var(--miss);color:var(--miss-fg)}\n.res .lesson{margin-top:6px}\n.res .extra{background:var(--extra);color:var(--extra-fg);border-radius:3px}\n.stats{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:15px;margin-top:8px;font-variant-numeric:tabular-nums}\n.stats b{font-size:20px}\n.stats .good{color:var(--ok)}.stats .bad{color:var(--miss-fg)}\n.row{display:flex;gap:8px;margin-top:10px;flex-wrap:wrap}\n.row #save{background:var(--btn);color:var(--btn-fg);border-color:var(--btn);font-weight:700}\n.row button{border:1px solid var(--line);background:var(--panel);color:var(--fg);font:inherit;font-size:15px;border-radius:10px;padding:8px 12px}\n.log{margin:18px 0 0}\n.log h2{font-size:18px;margin:0 0 8px;padding-left:10px;border-left:4px solid var(--fg)}\n.log table{width:100%;border-collapse:collapse;font-size:15px;background:var(--panel);border-radius:12px;overflow:hidden;font-variant-numeric:tabular-nums}\n.log th,.log td{padding:7px 6px;border-bottom:1px solid var(--line);text-align:right}\n.log th:first-child,.log td:first-child{text-align:left}\n.log th{font-size:13px;color:var(--muted)}\n.note{font-size:13px;color:var(--muted);margin:14px 2px 0}\n.warn{background:var(--miss);color:var(--miss-fg);border-radius:10px;padding:10px 12px;font-size:15px;margin:0 0 10px}\nbutton:focus-visible,select:focus-visible{outline:3px solid var(--time);outline-offset:2px}\n</style>\n</head>\n<body>\n<header>\n <h1>ロボティクスの見直し</h1>\n <p id=\"sub\"></p>\n</header>\n<main>\n <div id=\"nojs\" class=\"warn\" style=\"font-size:17px;line-height:1.8\">このファイルは「ファイル」アプリのプレビューでは動きません(声の聞き取りとカードはブラウザで動きます)。<br><a href=\"https://pixelstomotors.github.io/robotics-review/\" style=\"color:inherit;font-weight:700\">Safari で開く → pixelstomotors.github.io/robotics-review</a></div>\n <div class=\"seg step\" role=\"group\" aria-label=\"段階\">\n <button id=\"s1\" type=\"button\" aria-pressed=\"true\">1 単語</button>\n <button id=\"s2\" type=\"button\" aria-pressed=\"false\">2 説明</button>\n <button id=\"s3\" type=\"button\" aria-pressed=\"false\">3 判断の型</button>\n </div>\n <div class=\"bar\">\n <span class=\"selwrap\"><select id=\"cat\" aria-label=\"カテゴリ\"></select></span>\n <div class=\"seg\" role=\"group\" aria-label=\"言い方\">\n <button id=\"mSee\" type=\"button\" aria-pressed=\"true\">見て言う</button>\n <button id=\"mHide\" type=\"button\" aria-pressed=\"false\">見ないで言う</button>\n </div>\n <div class=\"seg\" role=\"group\" aria-label=\"出す範囲\">\n <button id=\"fAll\" type=\"button\" aria-pressed=\"true\">全部</button>\n <button id=\"fWeak\" type=\"button\" aria-pressed=\"false\">苦手だけ</button>\n </div>\n </div>\n <div id=\"warn\" class=\"warn\" hidden></div>\n <p class=\"info\" id=\"info\"></p>\n <div class=\"track\" id=\"track\" aria-live=\"off\"></div>\n <p class=\"pos\" id=\"pos\"></p>\n <button id=\"mic\" class=\"mic\" type=\"button\">押している間だけ聞く<small>カードを見て話して、指を離す</small></button>\n <section class=\"res\" id=\"res\" aria-live=\"polite\">\n <h2>話した言葉</h2>\n <div class=\"said\" id=\"said\">ここに出ます</div>\n <h2 style=\"margin-top:10px\" id=\"cmpH\">違い(赤=言わなかった・青=余分)</h2>\n <div class=\"cmp\" id=\"cmp\">—</div>\n <div class=\"stats\" id=\"stats\"></div>\n </section>\n <section class=\"log\">\n <h2>今回の練習</h2>\n <table><thead><tr><th>文</th><th>一致</th><th>前回</th></tr></thead><tbody id=\"log\"><tr><td colspan=\"3\">まだありません</td></tr></tbody></table>\n <div class=\"row\"><button id=\"save\" type=\"button\">結果を保存</button><button id=\"copy\" type=\"button\">結果をコピー</button><button id=\"clear\" type=\"button\">今回の結果を消す</button></div>\n <div class=\"row\" id=\"dirRow\" hidden><button id=\"redir\" type=\"button\">保存先を選び直す</button><span id=\"dirName\" class=\"note\" style=\"margin:0;align-self:center\"></span></div>\n <p class=\"note\" id=\"saveNote\" style=\"margin-top:6px\"></p>\n </section>\n <p class=\"note\" id=\"ver\"></p>\n</main>\n<script>\n(() => {\n document.getElementById('nojs').remove(); // ブラウザで開けている(プレビューではない)\n const DATA = /*DATA*/null;\n const LABEL = Object.fromEntries(DATA.categories.map(c => [c.key, c.label]));\n const GOOD = 80; // 一致がこれ未満なら「苦手」\n\n const $ = id => document.getElementById(id);\n const track = $('track'), micBtn = $('mic');\n let step = 1, cat = 'all', mode = 'see', filter = 'all', cards = [], cur = 0, results = {}, tries = {};\n const rk = x => `${x.id}:${step === 3 ? 3 : step}`; // 結果は段階ごとに分けて覚える\n\n // 前回の一致(この端末のブラウザの中だけに覚える)\n const STORE = 'robotics-review-v2';\n let saved = {};\n try { saved = JSON.parse(localStorage.getItem(STORE) || '{}') || {}; } catch (_) { saved = {}; }\n const persist = () => { try { localStorage.setItem(STORE, JSON.stringify(saved)); } catch (_) {} };\n // 保存した練習記録の集計(iCloud の 練習記録 → _tools/practice_review.py → 書き出し時に埋め込む)。端末の記録より新しければこちらを使う\n const PROGRESS = /*PROGRESS*/{};\n Object.entries(PROGRESS).forEach(([k, v]) => { if (!saved[k] || !saved[k].at || saved[k].at < v.at) saved[k] = v; });\n let attempts = []; // 今回の練習の全部の回(記録ファイルに書く)\n\n // 声に出す文:要点はその文、用語・問いは答え\n const sayText = x => x.type === 'word' ? (step === 2 ? x.detail : x.back) : x.type === 'point' ? x.front : x.back;\n const termForms = w => { const m = w.match(/^(.*?)((.*))$/); if (!m) return [w]; const sub = m[2].split(/・|、/).map(t => t.trim()).filter(Boolean); return [m[1], ...sub, m[1] + m[2]]; };\n const targets = x => x.type === 'word' && step === 1 ? termForms(x.front).flatMap(t => [`${t}は、${x.back}`, `${t}、${x.back}`]) : [sayText(x)];\n const pool = () => DATA.items.filter(x => step === 3 ? x.type === 'lesson' : x.type === 'word');\n const esc = s => String(s).replace(/[&<>\"]/g, c => ({'&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;'}[c]));\n\n // カテゴリの選択肢(段階ごとに件数が変わる)\n function fillCats() {\n const P = pool();\n const cnt = k => P.filter(x => k === 'key' ? x.key : k === 'all' ? true : x.category === k).length;\n const keyLabel = step === 3 ? '核心だけ' : 'まず覚える';\n $('cat').innerHTML = [['all', '全部'], ['key', keyLabel]].concat(DATA.categories.map(c => [c.key, c.label]))\n .map(([k, l]) => `<option value=\"${k}\"${k === cat ? ' selected' : ''}>${esc(l)}(${cnt(k)})</option>`).join('');\n }\n $('sub').textContent = `単語 ${DATA.items.filter(x => x.type === 'word').length}・判断の型 ${DATA.items.filter(x => x.type === 'lesson').length}`;\n $('ver').textContent = `データ:${DATA.updated} 更新${Object.keys(PROGRESS).length ? `・記録の集計 ${Object.keys(PROGRESS).length}件を反映` : ''}`;\n\n function render() { // 出すカードを選び直して描く\n cards = pool().filter(x => cat === 'key' ? x.key : cat === 'all' ? true : x.category === cat)\n .filter(x => filter === 'all' || (saved[rk(x)] && saved[rk(x)].s < GOOD));\n draw();\n track.scrollLeft = 0; cur = 0; updatePos(); showResult();\n }\n function draw() { // 今のカードのまま描き直す(言い終えたあと)\n track.innerHTML = cards.map(x => {\n const say = sayText(x);\n const r = results[rk(x)], p = saved[rk(x)];\n const done = r ? `<span class=\"done${r.score < GOOD ? ' low' : ''}\">${r.n > 1 ? `${r.n}回目 ` : ''}一致 ${r.score}%</span>`\n : p ? `<span class=\"done${p.s < GOOD ? ' low' : ''}\">前回 ${p.s}%</span>` : '';\n const lb = x.key ? `<div class=\"lb\">${x.type === 'word' ? 'まず覚える' : '核心'}</div>` : '';\n const sec = x.type === 'word' || x.type === 'lesson' ? LABEL[x.category] : `${cat === 'key' || cat === 'all' ? LABEL[x.category] + '|' : ''}${x.sub}`;\n let q = '', t, cls = say.length > 60 ? ' long' : '', b = '', w = '';\n if (x.type === 'word') {\n const hidden = mode === 'hide' && !r;\n if (step === 1) { w = `<div class=\"w\">${esc(x.front)}</div>`; t = hidden ? 'どういう意味?' : say; cls = hidden ? ' mid hide' : ' mid'; }\n else { w = `<div class=\"w small\">${esc(x.front)}</div><div class=\"q\">${esc(x.back)}</div>`; t = hidden ? 'もう少し詳しく言うと?' : say; cls = hidden ? ' mid hide' : ' long'; }\n } else if (x.type === 'lesson') { // 場面を見て、教訓を自分の言葉で言う。実例は読むだけ\n const hidden = mode === 'hide' && !r;\n if (hidden) { t = x.front; cls = ' mid'; }\n else { q = `<div class=\"q\">${esc(x.front)}</div>`; t = x.back; cls = ''; b = `<div class=\"b\">${esc(x.detail)}</div>`; }\n } else if (x.type === 'point') {\n if (mode === 'hide' && !r) { t = say.slice(0, 10) + '…'; cls = ' hide'; } else t = say;\n if (mode === 'see' && x.back) b = `<div class=\"b\">${esc(x.back)}</div>`;\n } else {\n const ask = x.type === 'term' ? `「${x.front}」とは?` : x.front;\n if (mode === 'hide' && !r) { q = ''; t = ask; cls = x.front.length > 40 ? ' long' : ''; }\n else { q = `<div class=\"q\">${esc(ask)}</div>`; t = say; }\n }\n const kind = x.type === 'word' ? (x.src && step === 2 ? x.src : '') : x.type === 'lesson' ? x.src : `${x.type === 'point' ? '要点' : x.type === 'term' ? '用語' : '問い'}・${x.src}`;\n return `<div class=\"card\"><div class=\"sec\">${esc(sec)}</div>${lb}${w}${q}<div class=\"t${cls}\">${esc(t)}</div>${b}<div class=\"m\"><span>${esc(kind)}</span>${done}</div></div>`;\n }).join('') || '<div class=\"card\"><div class=\"t\">ここに出す項目がありません</div></div>';\n const keys = cards.filter(x => x.key).length;\n $('info').textContent = `${cards.length}枚${keys && cat !== 'key' ? `(${step === 3 ? '核心' : 'まず覚える'} ${keys})` : ''}${step === 1 ? '・用語から続けて言う' : step === 3 ? '・場面を見て、教訓を自分の言葉で' : ''}`;\n }\n\n function updatePos() {\n const el = track.children[0];\n if (!el || !cards.length) { $('pos').textContent = ''; micBtn.disabled = true; return; }\n const w = el.getBoundingClientRect().width + 10;\n cur = Math.min(cards.length - 1, Math.max(0, Math.round(track.scrollLeft / w)));\n $('pos').textContent = `${cur + 1} / ${cards.length}`;\n const cel = track.children[cur]; if (cel) track.style.height = (cel.offsetHeight + 14) + 'px'; // 今のカードの高さに合わせる(長いカードの分の空白を作らない)\n micBtn.disabled = false;\n }\n let posTimer;\n track.addEventListener('scroll', () => { clearTimeout(posTimer); posTimer = setTimeout(() => { const before = cur; updatePos(); if (before !== cur) showResult(); }, 80); }, {passive: true});\n\n // 文字の比較(台本読み合わせページと同じ。句読点と空白は無視し、記号は読み方にそろえる)\n const SYM = [['°', '度'], ['mm', 'ミリ'], ['cm', 'センチ'], ['px', 'ピクセル'], ['%', 'パーセント'], ['%', 'パーセント'], ['±', 'プラスマイナス'], ['×', 'かける'], ['=', 'は'], ['=', 'は'], ['→', ''], ['+', 'プラス'], ['+', 'プラス'], ['&', 'アンド'], ['&', 'アンド']];\n const sym = s => SYM.reduce((t, [a, b]) => t.split(a).join(b), s);\n const PUNCT = /[\\s、。,.,.!!??「」『』()()・…〜~::;;\"'“”‘’\\-―||//①②③④⑤⑥]/;\n const kata2hira = s => s.replace(/[\\u30A1-\\u30F6]/g, ch => String.fromCharCode(ch.charCodeAt(0) - 0x60));\n const half = ch => ch.replace(/[A-Za-z0-9]/g, c => String.fromCharCode(c.charCodeAt(0) - 0xFEE0));\n // 1文字ずつ「表示する字」と「比べる字」を持つ(カタカナ・ひらがな、全角・半角、大文字・小文字は同じとみなす)\n const norm = s => prep(s).map(c => c.k).join('');\n const prep = s => [...sym(s)].filter(ch => !PUNCT.test(ch)).map(ch => ({o: ch, k: kata2hira(half(ch).toLowerCase())}));\n // 音声認識の書き分け・聞き違い(同じ読み)は、カードの書き方にそろえてから比べる。一覧はデータの homophones(練習記録を見て増やす)\n const VARIANTS = DATA.homophones || [];\n const align = (script, said) => {\n script = script.toLowerCase(); said = said.replace(/\\s+/g, '').toLowerCase();\n said = VARIANTS.reduce((t, g) => { const k = g.find(v => script.includes(v)); return k ? g.filter(v => v !== k).reduce((u, v) => u.split(v).join(k), t) : t; }, said);\n return said.replace(/([ァ-ヶ]+)ー/g, (m, w) => script.includes(w + 'ー') || !script.includes(w) ? m : w); // シミュレーター→シミュレータ(カードに長音がないとき)\n };\n function diff(a, b) { // a=カード, b=話した(prep の配列)\n const n = a.length, m = b.length, dp = Array.from({length: n + 1}, () => new Uint16Array(m + 1));\n for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--) dp[i][j] = a[i].k === b[j].k ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);\n const ops = []; let i = 0, j = 0;\n while (i < n && j < m) { if (a[i].k === b[j].k) { ops.push(['=', a[i].o]); i++; j++; } else if (dp[i + 1][j] >= dp[i][j + 1]) { ops.push(['-', a[i].o]); i++; } else { ops.push(['+', b[j].o]); j++; } }\n while (i < n) ops.push(['-', a[i++].o]); while (j < m) ops.push(['+', b[j++].o]);\n return {ops, same: dp[0][0]};\n }\n function opsHtml(ops) {\n let h = '', run = '', type = null;\n const flush = () => { if (!run) return; const t = esc(run); h += type === '=' ? t : `<span class=\"${type === '-' ? 'miss' : 'extra'}\">${t}</span>`; run = ''; };\n ops.forEach(([o, ch]) => { if (o !== type) { flush(); type = o; } run += ch; });\n flush(); return h;\n }\n\n function showResult() {\n const x = cards[cur];\n const r = x ? results[rk(x)] : null;\n if (!r) { $('cmpH').textContent = x && x.type === 'lesson' ? 'キーワード(赤=言えなかった)' : '違い(赤=言わなかった・青=余分)'; $('said').textContent = 'ここに出ます'; $('cmp').textContent = '—'; $('stats').innerHTML = ''; return; }\n $('said').textContent = r.raw || '(聞き取れませんでした)';\n $('cmpH').textContent = r.kws ? 'キーワード(赤=言えなかった)' : '違い(赤=言わなかった・青=余分)';\n $('cmp').innerHTML = r.kws ? r.kws.map(k => `<span class=\"kw${k.hit ? '' : ' miss'}\">${esc(k.w)}</span>`).join('') + `<div class=\"lesson\">${esc(x.back)}</div>` : opsHtml(r.ops);\n const p = saved[rk(x)] && saved[rk(x)].prev;\n $('stats').innerHTML = `<span>${r.n}回目</span><span>一致 <b class=\"${r.score >= GOOD ? 'good' : 'bad'}\">${r.score}%</b>${p != null ? `(前回 ${p}%)` : ''}</span>`;\n }\n\n function renderLog() {\n const done = Object.entries(results);\n const STEP = {1: '単語', 2: '説明', 3: '型'};\n $('log').innerHTML = done.length ? done.map(([k, r]) => { const p = saved[k] && saved[k].prev; const s = r.x.type === 'lesson' ? r.x.back : r.x.front;\n const sv = r.ai < savedIdx ? ' <small style=\"color:var(--ok)\">保存済</small>' : '';\n return `<tr><td>${STEP[r.st]}|${esc(s.slice(0, 14))}${s.length > 14 ? '…' : ''}${sv}</td><td>${r.score}%${r.n > 1 ? `<br><small>${r.n}回目</small>` : ''}</td><td>${p != null ? p + '%' : '—'}</td></tr>`; }).join('')\n : '<tr><td colspan=\"3\">まだありません</td></tr>';\n }\n\n\n // 音声認識(押している間だけ)\n const SR = window.SpeechRecognition || window.webkitSpeechRecognition;\n const IOS = /iPhone|iPad|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);\n let rec = null, listening = false, finalText = '', interim = '', t0 = 0, target = null, restarts = 0;\n if (!SR) { micBtn.disabled = true; showWarn('このブラウザでは声を文字にできません。iPhoneはSafari、PCはChromeかSafariで開いてください。'); }\n function makeRec() {\n const r = new SR(); r.lang = 'ja-JP'; r.continuous = true; r.interimResults = true;\n r.onresult = e => { if (r !== rec) return; interim = ''; for (let i = e.resultIndex; i < e.results.length; i++) { const tx = e.results[i][0].transcript; if (e.results[i].isFinal) finalText += tx; else interim += tx; } $('said').textContent = (finalText + interim) || '…'; };\n r.onerror = e => { if (e.error === 'not-allowed' || e.error === 'service-not-allowed') showWarn('マイクが許可されていません。ブラウザの設定でマイクを許可してください。'); };\n r.onend = () => {\n if (r !== rec) return;\n if (listening && restarts < 3) { restarts++; finalText += interim; interim = ''; rec = makeRec(); try { rec.start(); } catch (_) {} }\n else finish();\n };\n return r;\n }\n // PC:iPhoneと同じ「始まり」「終わり」の音を鳴らす\n let actx = null;\n function beep(up) {\n if (IOS) return;\n try {\n actx = actx || new (window.AudioContext || window.webkitAudioContext)();\n if (actx.state === 'suspended') actx.resume();\n const t = actx.currentTime + 0.01;\n (up ? [660, 990] : [990, 660]).forEach((f, k) => {\n const o = actx.createOscillator(), g = actx.createGain();\n o.type = 'sine'; o.frequency.value = f;\n g.gain.setValueAtTime(0.0001, t + k * 0.09); g.gain.exponentialRampToValueAtTime(0.25, t + k * 0.09 + 0.012); g.gain.exponentialRampToValueAtTime(0.0001, t + k * 0.09 + 0.08);\n o.connect(g).connect(actx.destination); o.start(t + k * 0.09); o.stop(t + k * 0.09 + 0.09);\n });\n } catch (_) {}\n }\n const PRE = IOS ? 0 : 200;\n function start(ev) {\n if (micBtn.disabled || listening || !SR) return;\n ev.preventDefault();\n updatePos(); // スワイプ・←→の直後でも、今見えているカードを対象にする\n target = cards[cur]; if (!target) return;\n finalText = ''; interim = ''; listening = true; t0 = performance.now(); restarts = 0;\n micBtn.classList.add('on'); micBtn.firstChild.textContent = '聞いています…';\n $('said').textContent = '…'; $('cmp').textContent = '—'; $('stats').innerHTML = '';\n beep(true);\n try { if (rec) rec.abort(); } catch (_) {}\n rec = makeRec();\n const go = () => { if (!listening) return; try { rec.start(); } catch (_) {} };\n PRE ? setTimeout(go, PRE) : go();\n }\n function stop(ev) {\n if (!listening) return;\n if (ev) ev.preventDefault();\n listening = false; target.sec = (performance.now() - t0) / 1000;\n micBtn.classList.remove('on'); micBtn.firstChild.textContent = '押している間だけ聞く';\n setTimeout(() => { try { rec.stop(); } catch (_) { finish(); } beep(false); }, 250); // 言い終わりを取りこぼさない\n }\n function finish() {\n if (!target) return;\n const x = target; target = null;\n const raw = (finalText + interim).trim();\n if (!raw || x.sec < 0.6) { $('said').textContent = !raw ? '聞き取れませんでした。押したまま話して、言い終わってから離してください' : '押す時間が短すぎました'; return; }\n // 1 単語は「用語+意味」を一続きで言う。用語はカッコの前後どちらで言ってもよく、「は」は無くてもよい(いちばん合う言い方で比べる)\n let best = null;\n if (x.type === 'lesson') { // 教訓はキーワードが言えたかで見る(言い方は自由)\n const s = norm(raw);\n const kws = x.kw.map(g => ({w: g[0], hit: g.some(v => s.includes(norm(v)))}));\n best = {say: x.back, kws, ops: null, score: Math.round(kws.filter(k => k.hit).length / kws.length * 100)};\n } else targets(x).forEach(t => { const a = prep(t), b = prep(align(t, raw)); const d = diff(a, b); const sc = a.length ? Math.round(d.same / Math.max(a.length, b.length) * 100) : 0; if (!best || sc > best.score) best = {say: t, ops: d.ops, score: sc}; });\n const {say, ops, score, kws} = best;\n const key = rk(x);\n (tries[key] = tries[key] || []).push(score);\n const at = stamp();\n results[key] = {raw, ops, kws, score, n: tries[key].length, x, st: step, ai: attempts.length}; // ai:この回が attempts の何番目か(保存済みの表示用)\n if (tries[key].length === 1) saved[key] = {s: score, prev: saved[key] ? saved[key].s : null, at};\n else saved[key] = {s: score, prev: saved[key] ? saved[key].prev : null, at};\n persist();\n attempts.push({id: x.id, step, word: x.type === 'lesson' ? x.back : x.front, card: say, said: raw, score, sec: Math.round(x.sec * 10) / 10, at, ...(kws ? {miss: kws.filter(k => !k.hit).map(k => k.w)} : {})});\n unsaved++; saveLabel();\n const keep = track.scrollLeft; draw(); track.scrollLeft = keep; updatePos(); showResult(); renderLog();\n }\n micBtn.addEventListener('touchstart', start, {passive: false});\n micBtn.addEventListener('touchend', stop, {passive: false});\n micBtn.addEventListener('touchcancel', stop, {passive: false});\n micBtn.addEventListener('mousedown', start);\n window.addEventListener('mouseup', () => stop());\n micBtn.addEventListener('contextmenu', e => e.preventDefault());\n // PC:スペースを押している間だけ聞く/←→でカードを移動\n const typing = e => /INPUT|TEXTAREA|SELECT/.test((e.target || {}).tagName || '');\n window.addEventListener('keydown', e => {\n if (typing(e)) return;\n if (e.code === 'Space') { e.preventDefault(); if (!e.repeat) start(e); }\n else if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {\n e.preventDefault(); const el = track.children[0]; if (!el || listening) return;\n const w = el.getBoundingClientRect().width + 10;\n track.scrollTo({left: Math.round(track.scrollLeft / w + (e.key === 'ArrowRight' ? 1 : -1)) * w, behavior: 'smooth'});\n }\n });\n window.addEventListener('keyup', e => { if (e.code === 'Space' && !typing(e)) { e.preventDefault(); stop(e); if (document.activeElement && document.activeElement.blur) document.activeElement.blur(); } });\n window.addEventListener('blur', () => stop());\n if (!IOS) micBtn.querySelector('small').textContent = 'スペースキーでも同じ。←→でカードを移動';\n\n function showWarn(t) { const w = $('warn'); w.textContent = t; w.hidden = false; }\n\n const press = (on, off, set) => { $(on).setAttribute('aria-pressed', true); $(off).setAttribute('aria-pressed', false); set(); render(); };\n $('mSee').addEventListener('click', () => { press('mSee', 'mHide', () => mode = 'see'); keepView(); });\n $('mHide').addEventListener('click', () => { press('mHide', 'mSee', () => mode = 'hide'); keepView(); });\n $('fAll').addEventListener('click', () => press('fAll', 'fWeak', () => filter = 'all'));\n $('fWeak').addEventListener('click', () => press('fWeak', 'fAll', () => filter = 'weak'));\n const PREF = 'robotics-review-view';\n const keepView = () => { try { localStorage.setItem(PREF, JSON.stringify({step, cat, mode})); } catch (_) {} };\n const setStep = n => { step = n; [1, 2, 3].forEach(k => $('s' + k).setAttribute('aria-pressed', k === n)); fillCats(); render(); keepView(); };\n [1, 2, 3].forEach(k => $('s' + k).addEventListener('click', () => setStep(k)));\n $('cat').addEventListener('change', e => { cat = e.target.value; render(); keepView(); e.target.blur(); });\n $('clear').addEventListener('click', () => { results = {}; tries = {}; attempts = []; unsaved = 0; savedIdx = 0; saveLabel(); draw(); showResult(); renderLog(); });\n\n // ===== 記録の保存(台本読み合わせページと同じ流れ。保存先は iCloud の ロボティクス復習/練習記録) =====\n function stamp() { const d = new Date(), z = n => String(n).padStart(2, '0'); const o = -d.getTimezoneOffset(); return `${d.getFullYear()}-${z(d.getMonth() + 1)}-${z(d.getDate())}T${z(d.getHours())}:${z(d.getMinutes())}:${z(d.getSeconds())}${o >= 0 ? '+' : '-'}${z(Math.floor(Math.abs(o) / 60))}:${z(Math.abs(o) % 60)}`; }\n let unsaved = 0, savedIdx = 0; // savedIdx:保存済みの回の数(次の保存はそれより後の回だけ送る)\n const saveLabel = () => { $('save').textContent = unsaved ? `結果を保存(未保存 ${unsaved}回)` : attempts.length ? `保存済み(${attempts.length}回)` : '結果を保存'; };\n const STEPNAME = {1: '1 単語', 2: '2 説明', 3: '3 判断の型'};\n function report(all) {\n const list = all ? attempts : attempts.slice(savedIdx);\n const d = new Date(); const ts = `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;\n let s = `# ロボティクス見直しの記録(${ts})\\n\\n${list.length}回・データ ${DATA.updated}\\n\\n`;\n list.forEach(a => { s += `## ${STEPNAME[a.step]}|${a.word}\\n- カード:${a.card}\\n- 話した:${a.said}\\n- 一致 ${a.score}%/${a.sec}秒\\n\\n`; });\n s += '<!-- 集計用(_tools/practice_review.py が読む) -->\\n```json\\n' + JSON.stringify({page: 'robotics-review', data: DATA.updated, at: stamp(), tries: list}) + '\\n```\\n';\n return s;\n }\n const fileName = () => { const d = new Date(), z = n => String(n).padStart(2, '0'); return `ロボ見直し_${d.getFullYear()}${z(d.getMonth() + 1)}${z(d.getDate())}_${z(d.getHours())}${z(d.getMinutes())}${z(d.getSeconds())}.md`; };\n const doneSaving = msg => { unsaved = 0; savedIdx = attempts.length; renderLog(); $('save').textContent = msg; setTimeout(saveLabel, 2500); };\n function download() {\n const b = new Blob([report()], {type: 'application/octet-stream'}); const a = document.createElement('a');\n a.href = URL.createObjectURL(b); a.download = fileName(); document.body.appendChild(a); a.click();\n setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 1500);\n doneSaving('保存しました(ダウンロード)');\n }\n // PC(Chrome・Edge):一度選んだ「練習記録」フォルダへ直接書き込む(選んだフォルダはこのブラウザの中だけに覚える)\n const CAN_DIR = !IOS && 'showDirectoryPicker' in window;\n const idb = (mode, fn) => new Promise((ok, ng) => { try { const q = indexedDB.open('robotics-review', 1); q.onupgradeneeded = () => q.result.createObjectStore('kv'); q.onerror = () => ng(q.error);\n q.onsuccess = () => { try { const t = q.result.transaction('kv', mode); const r = fn(t.objectStore('kv')); t.oncomplete = () => ok(r && r.result); t.onerror = () => ng(t.error); } catch (e) { ng(e); } }; } catch (e) { ng(e); } });\n const getDir = () => idb('readonly', st => st.get('dir')).catch(() => null);\n const putDir = h => idb('readwrite', st => st.put(h, 'dir')).catch(() => null);\n const showDirName = h => { $('dirName').textContent = h ? `保存先:${h.name}` : '保存先:まだ選んでいません'; };\n async function pickDir() {\n const h = await window.showDirectoryPicker({id: 'robo-renshu', mode: 'readwrite'});\n if (h.name !== '練習記録') showWarn(`選んだのは「${h.name}」です。iCloud Drive → PixelstoMotors → ロボティクス復習 → 練習記録 を選んでください。`); else $('warn').hidden = true;\n await putDir(h); showDirName(h); return h;\n }\n // iPhone:共有の画面から「\"ファイル\"に保存」→ ロボティクス復習 → 練習記録(2回目からは同じ場所が出る)\n const SHORTCUT = 'ロボ見直しを保存';\n async function shareSave() {\n const f = new File([report()], fileName(), {type: 'text/markdown'});\n if (navigator.canShare && navigator.canShare({files: [f]})) { await navigator.share({files: [f]}); doneSaving('保存しました'); return true; }\n return false;\n }\n $('save').addEventListener('click', async () => {\n if (attempts.length <= savedIdx) { $('save').textContent = attempts.length ? '保存済みです' : 'まだ記録がありません'; setTimeout(saveLabel, 1800); return; }\n if (IOS) { // iPhone:記録をコピーして、ショートカット「ロボ見直しを保存」に渡す(練習記録フォルダに直接入る)\n try { await navigator.clipboard.writeText(report()); doneSaving('ショートカットで保存中'); location.href = 'shortcuts://run-shortcut?name=' + encodeURIComponent(SHORTCUT) + '&input=clipboard'; return; } catch (_) {}\n try { if (await shareSave()) return; } catch (e) { if (e && e.name === 'AbortError') return; } return download();\n }\n if (!CAN_DIR) return download();\n try {\n let h = await getDir();\n if (h && (await h.queryPermission({mode: 'readwrite'})) !== 'granted' && (await h.requestPermission({mode: 'readwrite'})) !== 'granted') h = null;\n if (!h) { showWarn('保存先のフォルダを選んでください:iCloud Drive → PixelstoMotors → ロボティクス復習 → 練習記録(最初の1回だけ)'); h = await pickDir(); }\n const w = await (await h.getFileHandle(fileName(), {create: true})).createWritable(); await w.write(report()); await w.close();\n doneSaving(`${h.name}に保存しました`);\n } catch (e) {\n if (e && e.name === 'AbortError') { $('warn').hidden = true; return; }\n showWarn('練習記録に書き込めなかったので、ダウンロードに保存しました。'); download();\n }\n });\n $('copy').addEventListener('click', async () => { try { await navigator.clipboard.writeText(report(true)); $('copy').textContent = 'コピーしました'; } catch (_) { $('copy').textContent = 'コピーできませんでした'; } setTimeout(() => $('copy').textContent = '結果をコピー', 1800); });\n if (CAN_DIR) { $('dirRow').hidden = false; $('redir').addEventListener('click', () => pickDir().catch(() => {})); getDir().then(showDirName); }\n if (IOS) { const b = document.createElement('button'); b.type = 'button'; b.id = 'share'; b.textContent = '共有から保存';\n b.addEventListener('click', async () => { if (attempts.length <= savedIdx) return; try { if (!(await shareSave())) download(); } catch (e) { if (!(e && e.name === 'AbortError')) download(); } });\n $('clear').before(b); }\n $('saveNote').textContent = IOS ? '' : CAN_DIR ? '' : '保存はダウンロードに入ります。練習記録に直接入れたいときはChromeで開いてください';\n window.addEventListener('beforeunload', e => { if (unsaved) { e.preventDefault(); e.returnValue = ''; } });\n // 前に開いていた段階・カテゴリ・言い方から始める(この端末の中だけ)\n try { const v = JSON.parse(localStorage.getItem(PREF) || 'null'); if (v) { cat = v.cat || 'all'; if (v.mode === 'hide') { mode = 'hide'; $('mHide').setAttribute('aria-pressed', true); $('mSee').setAttribute('aria-pressed', false); } setStep(v.step || 1); } else setStep(1); } catch (_) { setStep(1); }\n})();\n</script>\n</body>\n</html></code></pre><h2 id=\"index_xrC6Iabk\">ページを書き出す build_review.py の全文</h2><p>python</p><pre><code class=\"language-python\">#!/usr/bin/env python3\n\"\"\"robotics_review.json(データ)から ロボティクス復習.html(声に出して見直すページ)を作る。\n\n使い方(このファイルがある _tools の1つ上のフォルダで):\n python3 _tools/practice_review.py # 練習記録があれば先に集計\n python3 _tools/build_review.py\n\n- ページの形は _tools/template.html(仕事で使っている「台本読み合わせ」ページと同じ作り)。データはページの中に埋め込む\n- 書き出した ロボティクス復習.html は、GitHub の PixelstoMotors/robotics-review に index.html として置く\n (声を文字にするには https が要るため。公開URL https://pixelstomotors.github.io/robotics-review/)\n\nデータの形(items の1件):\n {\"id\": \"r001\", \"category\": \"kinematics\", \"sub\": \"小見出し\",\n \"type\": \"word\" | \"lesson\",\n \"front\": \"用語 / 場面(問い)\", \"back\": \"短い意味 / 教訓(声に出す一言)\",\n \"detail\": \"詳しい説明 / 実例(読むだけ)\", \"kw\": [[\"座標系\",\"座標\"],[\"そろえる\",\"揃える\"]](lesson だけ),\n \"key\": true/false, \"src\": \"DAY163\"}\n word は「1 単語」(用語+back を一続きで言う)と「2 説明」(detail を言う)に出る。key は「まず覚える」。\n lesson は「3 判断の型」に出る。場面を見て教訓を自分の言葉で言い、kw の各組から1語ずつ言えていれば合格。key は「核心」。\n id は一度付けたら変えない(ページが「前回の一致」をこの id で覚えている)。新しい項目は末尾の番号の次を付ける。\n\"\"\"\nimport json\nimport sys\nfrom pathlib import Path\n\nROOT = Path(__file__).resolve().parent.parent\nDATA = ROOT / \"robotics_review.json\"\nTEMPLATE = Path(__file__).resolve().parent / \"template.html\"\nOUT = ROOT / \"ロボティクス復習.html\"\nPROGRESS = ROOT / \"練習記録_集計.json\" # _tools/practice_review.py が作る(無ければ空)\n\n\ndef src_label(s):\n return \"・\".join(p.strip() for p in (s or \"\").split(\",\") if p.strip())\n\n\ndef build():\n data = json.loads(DATA.read_text(encoding=\"utf-8\"))\n cats = {c[\"key\"] for c in data[\"categories\"]}\n ids = [x.get(\"id\") for x in data[\"items\"]]\n bad = [x.get(\"id\") for x in data[\"items\"]\n if x.get(\"category\") not in cats or x.get(\"type\") not in (\"word\", \"lesson\", \"point\", \"term\", \"qa\")\n or not x.get(\"front\") or (x.get(\"type\") != \"point\" and not x.get(\"back\"))\n or (x.get(\"type\") in (\"word\", \"lesson\") and not x.get(\"detail\"))\n or (x.get(\"type\") == \"lesson\" and not x.get(\"kw\"))]\n if bad:\n sys.exit(f\"category / type / front / back が不正な項目: {bad}\")\n dup = {i for i in ids if ids.count(i) > 1}\n if dup or None in ids:\n sys.exit(f\"id が重複または空: {sorted(i for i in dup if i)}\")\n\n page = {\n \"updated\": data.get(\"updated\", \"\"),\n \"categories\": data[\"categories\"],\n \"homophones\": data.get(\"homophones\", []),\n \"items\": [{\"id\": x[\"id\"], \"category\": x[\"category\"], \"sub\": x.get(\"sub\", \"\"), \"type\": x[\"type\"],\n \"front\": x[\"front\"], \"back\": x.get(\"back\", \"\"), \"detail\": x.get(\"detail\", \"\"), \"key\": bool(x.get(\"key\")),\n \"src\": src_label(x.get(\"src\")), **({\"kw\": x[\"kw\"]} if x.get(\"kw\") else {})} for x in data[\"items\"]],\n }\n js = json.dumps(page, ensure_ascii=False, separators=(\",\", \":\")).replace(\"</\", \"<\\\\/\")\n html = TEMPLATE.read_text(encoding=\"utf-8\")\n if \"/*DATA*/null\" not in html:\n sys.exit(\"template.html に /*DATA*/null が見つかりません\")\n prog = json.loads(PROGRESS.read_text(encoding=\"utf-8\")) if PROGRESS.exists() else {}\n pj = json.dumps(prog, ensure_ascii=False, separators=(\",\", \":\")).replace(\"</\", \"<\\\\/\")\n if \"/*PROGRESS*/{}\" not in html:\n sys.exit(\"template.html に /*PROGRESS*/{} が見つかりません\")\n OUT.write_text(html.replace(\"/*DATA*/null\", js).replace(\"/*PROGRESS*/{}\", pj), encoding=\"utf-8\")\n print(f\"{OUT.name} を書き出しました({len(page['items'])}項目・練習記録の集計 {len(prog)}件)\")\n\n\nif __name__ == \"__main__\":\n build()</code></pre><p></p><h2 id=\"index_54fjEQ2p\">練習記録を集計する practice_review.py の全文</h2><p>python</p><pre><code class=\"language-python\">#!/usr/bin/env python3\n\"\"\"練習記録(ページの「結果を保存」で 練習記録/ に入った .md)を集計する。\n\n使い方(このファイルがある _tools の1つ上のフォルダで):\n python3 _tools/practice_review.py # 集計と振り返りを作り直す\n python3 _tools/build_review.py # 集計をページに入れて書き出す\n\n作るもの(毎回作り直す):\n- 練習記録_集計.json … カード×段階ごとの最新の一致など。build_review.py がページに埋め込み、\n どの端末で開いても「前回」「苦手だけ」がそろう\n- 練習記録_振り返り.md … 苦手・伸びた・よく抜ける言葉・まだ練習していない「まず覚える」語。\n Claude はこれを読んで、カードの言い方や順番を直す\n仕事で使っている「台本読み合わせ」ページの集計スクリプトと同じ役割。\n\"\"\"\nimport json\nimport re\nimport unicodedata\nfrom collections import Counter, defaultdict\nfrom pathlib import Path\n\nROOT = Path(__file__).resolve().parent.parent\nREC_DIR = ROOT / \"練習記録\"\nDATA = ROOT / \"robotics_review.json\"\nOUT_JSON = ROOT / \"練習記録_集計.json\"\nOUT_MD = ROOT / \"練習記録_振り返り.md\"\nGOOD = 80 # これ未満は「苦手」(ページと同じ)\nMASTER = 90 # 直近2回がこれ以上なら「身についた」\nSTEP = {1: \"1 単語\", 2: \"2 説明\", 3: \"3 判断の型\"}\n\n# ページと同じ比べ方(記号は読み方に、句読点・空白は無視、カナ・全半角・大小は同じとみなす)\nSYM = [(\"°\", \"度\"), (\"mm\", \"ミリ\"), (\"cm\", \"センチ\"), (\"px\", \"ピクセル\"), (\"%\", \"パーセント\"), (\"%\", \"パーセント\"),\n (\"&\", \"アンド\"), (\"&\", \"アンド\"), (\"±\", \"プラスマイナス\"), (\"×\", \"かける\"), (\"=\", \"は\"), (\"=\", \"は\"), (\"→\", \"\"), (\"+\", \"プラス\"), (\"+\", \"プラス\")]\nPUNCT = re.compile(r\"[\\s、。,.,.!!??「」『』()()・…〜~::;;\\\"'“”‘’\\-―||//①②③④⑤⑥]\")\n\n\ndef prep(s):\n for a, b in SYM:\n s = s.replace(a, b)\n out = []\n for ch in s:\n if PUNCT.match(ch):\n continue\n k = unicodedata.normalize(\"NFKC\", ch).lower()\n if \"ァ\" <= k <= \"ヶ\":\n k = chr(ord(k) - 0x60)\n out.append((ch, k))\n return out\n\n\nHOMO = [] # データの homophones(main で読む)\nSKIPPED = [] # 読めなかった記録ファイル\n\n\ndef align(script, said):\n \"\"\"ページと同じ:聞き違い・書き分けをカードの書き方にそろえる\"\"\"\n script, said = script.lower(), re.sub(r\"\\s+\", \"\", said).lower()\n for g in HOMO:\n k = next((v for v in g if v in script), None)\n if k:\n for v in g:\n if v != k:\n said = said.replace(v, k)\n return re.sub(r\"([ァ-ヶ]+)ー\", lambda m: m.group(0) if (m.group(1) + \"ー\") in script or m.group(1) not in script else m.group(1), said)\n\n\ndef lcs(a, b):\n n, m = len(a), len(b)\n dp = [[0] * (m + 1) for _ in range(n + 1)]\n for i in range(n - 1, -1, -1):\n for j in range(m - 1, -1, -1):\n dp[i][j] = dp[i + 1][j + 1] + 1 if a[i][1] == b[j][1] else max(dp[i + 1][j], dp[i][j + 1])\n return dp[0][0]\n\n\ndef rescore(card, said):\n \"\"\"今の比べ方で採点し直す(聞き違いの一覧を増やしたら、過去の記録にも効く)\"\"\"\n a, b = prep(card), prep(align(card, said))\n return round(lcs(a, b) / max(len(a), len(b)) * 100) if a else 0\n\n\ndef targets(x, step, card):\n \"\"\"ページと同じ:1 単語は「用語(カッコの前後どちらでも)+は+意味」の言い方すべてで比べる\"\"\"\n if x[\"type\"] != \"word\" or step != 1:\n return [card]\n m = re.match(r\"^(.*?)((.*))$\", x[\"front\"])\n forms = [x[\"front\"]] if not m else [m.group(1), *[t.strip() for t in re.split(r\"・|、\", m.group(2)) if t.strip()], m.group(1) + m.group(2)]\n return [f\"{f}{sep}{x['back']}\" for f in forms for sep in (\"は、\", \"、\")]\n\n\ndef missing_runs(card, said):\n \"\"\"カードにあって話さなかった部分(2文字以上のかたまり)\"\"\"\n a, b = prep(card), prep(align(card, said))\n n, m = len(a), len(b)\n dp = [[0] * (m + 1) for _ in range(n + 1)]\n for i in range(n - 1, -1, -1):\n for j in range(m - 1, -1, -1):\n dp[i][j] = dp[i + 1][j + 1] + 1 if a[i][1] == b[j][1] else max(dp[i + 1][j], dp[i][j + 1])\n runs, cur, i, j = [], \"\", 0, 0\n while i < n and j < m:\n if a[i][1] == b[j][1]:\n if cur: runs.append(cur); cur = \"\"\n i += 1; j += 1\n elif dp[i + 1][j] >= dp[i][j + 1]:\n cur += a[i][0]; i += 1\n else:\n j += 1\n cur += \"\".join(c for c, _ in a[i:])\n if cur: runs.append(cur)\n return [r for r in runs if len(r) >= 2]\n\n\ndef load_tries():\n seen, tries, files = set(), [], 0\n # .md:ページから保存したもの/.txt:iPhone のショートカット「ロボ見直しを保存」で入ったもの(名前は問わない)\n for f in sorted([*REC_DIR.glob(\"*.md\"), *REC_DIR.glob(\"*.txt\")]) if REC_DIR.exists() else []:\n try:\n text = f.read_text(encoding=\"utf-8\")\n except OSError: # iCloud でまだダウンロードされていないファイル(Mac の「ファイル」で開くか、少し待つと読める)\n SKIPPED.append(f.name)\n continue\n m = re.search(r\"```json\\s*(\\{.*?\\})\\s*```\", text, re.S)\n if not m:\n continue\n try:\n rec = json.loads(m.group(1))\n except json.JSONDecodeError:\n continue\n if rec.get(\"page\") != \"robotics-review\":\n continue\n files += 1\n for t in rec.get(\"tries\", []):\n k = (t.get(\"id\"), t.get(\"step\"), t.get(\"at\"))\n if None in k or k in seen:\n continue\n seen.add(k)\n tries.append(t)\n tries.sort(key=lambda t: t[\"at\"])\n return tries, files\n\n\ndef main():\n data = json.loads(DATA.read_text(encoding=\"utf-8\"))\n items = {x[\"id\"]: x for x in data[\"items\"]}\n HOMO[:] = data.get(\"homophones\", [])\n tries, files = load_tries()\n by = defaultdict(list)\n words = [x for x in data[\"items\"] if x[\"type\"] == \"word\"]\n fixed, wrongcard = [], []\n for t in tries:\n if t[\"id\"] not in items:\n continue\n if \"miss\" not in t: # キーワード採点(判断の型)以外は、今の比べ方で採点し直す\n new = max(rescore(c, t[\"said\"]) for c in targets(items[t[\"id\"]], t[\"step\"], t[\"card\"]))\n if new != t[\"score\"]:\n fixed.append((t, t[\"score\"], new))\n t[\"score\"] = new\n # 別のカードを読んだ記録(カードの切り替え直後に話したなど)は集計から外す\n if t[\"step\"] == 1 and t[\"score\"] < 50:\n other = max((x for x in words if x[\"id\"] != t[\"id\"]), key=lambda x: rescore(x[\"back\"], t[\"said\"]))\n if rescore(other[\"back\"], t[\"said\"]) >= 70:\n wrongcard.append((t, other))\n continue\n by[f'{t[\"id\"]}:{t[\"step\"]}'].append(t)\n\n progress = {}\n for k, ts in by.items():\n progress[k] = {\"s\": ts[-1][\"score\"], \"prev\": ts[-2][\"score\"] if len(ts) > 1 else None,\n \"at\": ts[-1][\"at\"], \"n\": len(ts), \"best\": max(t[\"score\"] for t in ts)}\n OUT_JSON.write_text(json.dumps(progress, ensure_ascii=False, indent=1), encoding=\"utf-8\")\n\n def label(k):\n i, st = k.split(\":\")\n x = items[i]\n return f'{STEP[int(st)]}|{x[\"back\"] if x[\"type\"] == \"lesson\" else x[\"front\"]}'\n\n weak = sorted([k for k, p in progress.items() if p[\"s\"] < GOOD], key=lambda k: progress[k][\"s\"])\n mastered = [k for k, ts in by.items() if len(ts) >= 2 and ts[-1][\"score\"] >= MASTER and ts[-2][\"score\"] >= MASTER]\n grew = sorted([k for k, ts in by.items() if len(ts) >= 2 and ts[-1][\"score\"] - ts[0][\"score\"] >= 15],\n key=lambda k: by[k][0][\"score\"] - by[k][-1][\"score\"])\n stuck = [k for k, ts in by.items() if len(ts) >= 3 and max(t[\"score\"] for t in ts[-3:]) < GOOD]\n miss = Counter()\n for k in weak:\n t = by[k][-1]\n miss.update(t[\"miss\"] if \"miss\" in t else missing_runs(t[\"card\"], t[\"said\"]))\n keywords = [x for x in data[\"items\"] if x[\"type\"] == \"word\" and x.get(\"key\")]\n untouched = [x for x in keywords if f'{x[\"id\"]}:1' not in progress]\n\n L0 = []\n if wrongcard:\n L0 += [\"## 別のカードの文を読んでいた記録(集計から外した)\", \"\"]\n L0 += [f'- 「{items[t[\"id\"]][\"front\"]}」の回に、「{o[\"front\"]}」の文を話していた({t[\"at\"][5:16].replace(\"T\", \" \")})' for t, o in wrongcard]\n L0.append(\"\")\n if fixed:\n L0 += [f\"## 聞き違いの補正で採点し直した記録:{len(fixed)}回\", \"\"]\n L0 += [f'- {items[t[\"id\"]][\"front\"]}:{a}% → {b}%(話した:{t[\"said\"]})' for t, a, b in sorted(fixed, key=lambda z: z[1] - z[2])[:15]]\n L0.append(\"\")\n L = [\"# ロボティクス見直し 練習の振り返り\", \"\",\n f\"記録ファイル {files}・練習 {len(tries)}回・練習したカード {len(progress)}(カード×段階)\", \"\",\n \"## 数字\", \"\",\n f\"- 苦手(最新の一致が{GOOD}%未満):{len(weak)}\",\n f\"- 身についた(直近2回とも{MASTER}%以上):{len(mastered)}\",\n f\"- 3回続けて{GOOD}%に届かない:{len(stuck)}\",\n f\"- まだ1回も言っていない「まず覚える」語:{len(untouched)} / {len(keywords)}\", \"\"]\n if stuck:\n L += [\"## 3回続けて届かない(カードの言い方を見直す候補)\", \"\"]\n L += [f'- {label(k)}:{\" → \".join(str(t[\"score\"]) for t in by[k][-3:])}%(カード:{by[k][-1][\"card\"]})' for k in stuck]\n L.append(\"\")\n if weak:\n L += [\"## 苦手(低い順)\", \"\"]\n L += [f'- {label(k)}:{progress[k][\"s\"]}%({progress[k][\"n\"]}回)' for k in weak[:40]]\n L.append(\"\")\n if miss:\n L += [\"## よく抜ける言葉・キーワード(苦手カードの最新の回から)\", \"\"]\n L += [f\"- {w}({c}回)\" for w, c in miss.most_common(20)]\n L.append(\"\")\n if grew:\n L += [\"## 伸びた(最初から15ポイント以上)\", \"\"]\n L += [f'- {label(k)}:{by[k][0][\"score\"]}% → {by[k][-1][\"score\"]}%' for k in grew[:20]]\n L.append(\"\")\n if untouched:\n L += [\"## まだ言っていない「まず覚える」語\", \"\"]\n L += [\"- \" + \"、\".join(x[\"front\"] for x in untouched), \"\"]\n sc = [t[\"score\"] for ts in by.values() for t in ts]\n if sc:\n L[2:2] = [f\"平均の一致 {round(sum(sc) / len(sc))}%・90%以上 {sum(s >= 90 for s in sc)}回/{len(sc)}回\", \"\"]\n L += L0\n OUT_MD.write_text(\"\\n\".join(L), encoding=\"utf-8\")\n if SKIPPED:\n print(f\"まだダウンロードされていない記録 {len(SKIPPED)}件(次の集計で読む):{'、'.join(SKIPPED)}\")\n print(f\"記録 {files}ファイル・{len(tries)}回 → {OUT_JSON.name}({len(progress)}件)と {OUT_MD.name} を作り直しました\")\n\n\nif __name__ == \"__main__\":\n main()</code></pre><p></p><h2 id=\"index_fh6J9rox\">抜き出しと照合に使った指示文</h2><p>カードを作るときに、エージェントに渡した指示文の全文です。照合の指示文は短いので、本文の節で書いた内容がほぼすべてです。</p><h3 id=\"index_1n65D96N\">開発ログから復習項目を抜き出す指示(INSTRUCTIONS.md)</h3><p>markdown</p><pre><code class=\"language-markdown\"># ロボティクス復習アプリ用の項目抽出\n\nエイイチ(Pixels to Motors)が DAY102〜DAY326 の開発ログ記事で学んできたロボティクスの知識を、\niPhone で見直すための復習カードにする。あなたは担当範囲の記事を読み、復習項目を JSON で書き出す。\n\n## 読み方\n- 担当ファイル一覧の各 .md を読む(`_en.md` で終わる英語版は飛ばす)。\n- 記事にはコード全文・ログ全量が入っていて長い。コードブロックやログは流し読みでよい。本文の説明・判断・結論・表を重視する。\n- 大きいファイルは Read の offset/limit で分けて読む。\n\n## 何を拾うか(ロボティクスの「知識」だけ)\n拾う:概念・用語の意味、仕組み、実機やシミュで確かめた事実・数値、失敗から得た原則、判断の理由、手法の比較、評価の考え方。\n拾わない:SNS戦略・集客・ビジネスの話、記事の書き方、個人の作業段取り、日付だけの進捗、雑談。\n(ただし「ロボットの社会実装・現場導入」の構造的な知見は category \"society\" で拾ってよい)\n\n- 1記事から 0〜6 項目。中身の薄い記事は 0 でよい。合計は担当範囲で 30〜60 項目が目安。\n- 同じ内容を何度も拾わない(担当範囲内で重複したら一番よく説明している記事の1つに絞る)。\n- 記事に書いてある事実だけを書く。あなたの一般知識で補ったり、数値を作ったりしない。一般用語の定義を補う場合も記事の文脈と食い違わないこと。\n\n## カテゴリ(category の値はこの英字キーのどれか)\n- kinematics : 運動学・座標系(FK/IK、CCD、関節角、座標系の向き、EE定義、到達範囲)\n- hardware : 実機・サーボ制御(mechArm 270 Pi の API、send_angles/send_coords、サーボ・速度・ガード、キャリブレーション、通信)\n- sim : シミュレーション・デジタルツイン(Unity、ArticulationBody、Stiffness/Damping、Target/Force mode、URDF、ROS2・MoveIt)\n- vision : 画像認識・カメラ(色検出、色空間、輪郭、透視変換・ホモグラフィ、特徴点、カメラ校正、物体検出モデル、IoU/Precision/Recall/mAP)\n- servo : 視覚サーボ・Pick&Place・受け渡し(補正記憶、積分則、PICK/PLACE、ハンドオーバー、動的追従)\n- learning : ロボット学習(模倣学習、VLA、π0/π0.5、ACT、Diffusion Policy、SmolVLA、GR00T、LeRobot、SO-101、テレオペ、データ収集、リターゲティング、強化学習、報酬)\n- eval : 評価・データ管理・再現性(台帳、ラベリング、VLM-as-Reward、試行回数とぶれ、ログ設計、テスト)\n- principles : 設計原則・進め方(P001/P005/P008 など、測ってから推論、補正で仕事しない、層の分離、Motion UX の考え方)\n- society : 社会実装・現場(配膳・清掃・警備・介護・物流など導入の構造、例外対応、コスト)\n\n## 項目の種類(type)\n- \"point\" : 覚えておきたい要点1文(カードに表示。60〜120字目安、言い切りの文)。\n- \"term\" : 用語。front=用語名(短く)、back=意味を1〜3文で。\n- \"qa\" : 問い。front=「なぜ〜?」「〜はどう違う?」などの問い、back=答え2〜4文。実機で確かめた理由や判断の根拠が入ると良い。\n\nkey: true は「これだけは忘れない」核心(各カテゴリの重要原則・確定事項)。全体の15%程度まで。\n\n## 出力\n`/home/claude/work/parts/{あなたの担当名}.json` に、次の形の JSON 配列を書く(UTF-8、ensure_ascii=False):\n\n```json\n[\n {\"category\":\"kinematics\",\"type\":\"qa\",\"front\":\"なぜ一般的なIKではなくFKテーブルを使ったのか?\",\"back\":\"…\",\"key\":false,\"src\":\"DAY163\"},\n {\"category\":\"principles\",\"type\":\"point\",\"front\":\"測る手段を持つことが推論より先。座標・姿勢・画像を実測する前に仮説を確定しない(P008)。\",\"back\":\"\",\"key\":true,\"src\":\"DAY170\"}\n]\n```\n- src は出典の DAY 番号(例 \"DAY163\")。複数なら \"DAY163,DAY164\"。\n- 文体:です・ます不要。短く言い切る。専門用語は初出で短く言い換える(例「逆運動学(指先位置から関節角を求める計算)」)。英語の略語だけで済ませない。\n- 書き終えたら python で json.load して壊れていないことを確かめる。\n\n最後の返答は「件数(カテゴリ別)」と「書き出したパス」だけを短く返す。</code></pre><h3 id=\"index_BCbwDIrW\">カテゴリごとに重複をまとめる指示(CURATE.md)</h3><p>markdown</p><pre><code class=\"language-markdown\"># 復習カードの整理(カテゴリ単位)\n\n/home/claude/work/merged_raw.json に、開発ログ記事(DAY102〜326)と調査メモから抜き出した復習項目が入っている。\n複数の人が範囲を分けて抜き出したので、カテゴリの中に重複・食い違い・粒度のばらつきがある。\nあなたは担当カテゴリの項目だけを整理し、iPhone で見直す復習アプリのデータとして仕上げる。\n項目の形式・type・key の意味は /home/claude/work/INSTRUCTIONS.md を参照。\n\n## やること\n1. 担当カテゴリの項目を全部読む。\n2. 重複をまとめる:同じ事実・同じ原則を言っている項目は1つに統合する(一番よい説明を残し、src を結合)。\n3. 食い違いを解く:古い DAY の結論と新しい DAY の結論が違うなら、新しい方を正として書き、必要なら「当初は〜と見ていたが、DAY◯◯で〜と分かった」と一言添える。判断できないときは、項目の front/back と src を元記事(/home/claude/r2r2r-content/articles/DAY◯◯*.md)で確かめる。調査メモ由来(src が \"研究メモ:\" で始まる)の数値は /mnt/user-data/uploads/PixelstoMotors/research/ のメモで確かめられる。\n4. 小見出しを付ける:カテゴリの中を 3〜7 個の小見出し(sub)に分け、各項目に \"sub\" を付ける。小見出しは短い日本語(例「座標系の向き」「関節の上限」「FKテーブル」)。復習しやすい順(基礎→応用、原因→対策)に並べる。\n5. 文を整える:front/back が長すぎる・分かりにくいものは短く直す。英略語は初出で短く言い換え。事実は変えない。mechArm 固有の数値は残す(復習の価値がある)。\n6. 削る:知識として薄いもの(その日の作業報告だけ、ロボティクスでない話)は捨てる。目安として元の件数の 60〜80% 程度に収まればよい(無理に削らない)。\n7. key: true は担当カテゴリの 10〜15% に絞る。そのカテゴリで「これだけは忘れない」もの。\n8. type=\"term\" は front が用語名、back が意味。type=\"qa\" は front が問い、back が答え。type=\"point\" は front が要点文で back は空でよい(補足があれば back に1文)。\n\n## 出力\n`/home/claude/work/curated/{担当名}.json` に JSON 配列(UTF-8、ensure_ascii=False)。各要素:\n{\"category\",\"sub\",\"type\",\"front\",\"back\",\"key\",\"src\"}\n配列は「小見出しの並び順 → 小見出し内の復習順」で並べる。\n書き終えたら json.load で確認。最後の返答は、カテゴリごとの件数(元→後)と小見出し一覧だけ。</code></pre><h3 id=\"index_DcXAKdXD\">用語164語を作る指示(VOCAB.md)</h3><p>markdown</p><pre><code class=\"language-markdown\"># ロボティクス用語集の作成(声に出して覚える用)\n\nエイイチ(29年のデザイナー。ロボットアーム mechArm 270 Pi を iPhone・Unity・Python で動かして開発ログを書いてきた。\nロボット工学の正式な教育は受けていない)が、ロボティクスの「現場で本当に使う言葉」を基礎から覚え直すための用語集を作る。\niPhone のページで、①用語を見て短い意味を声に出す → ②詳しい説明を声に出す、の順で練習する。\n\n## 選ぶ言葉\n- ロボティクスの実務・研究・業界の会話で実際によく出る、実用性の高い言葉を選ぶ。マニアックな言葉、この人のプロジェクト固有の造語(lookup、補正記憶、P008 など)は選ばない。\n- 担当カテゴリごとに 14〜20 語。カテゴリの中は「基礎 → 応用」の順に並べる(最初の数語は、まったくの初心者でも分かる言葉から)。\n- 各カテゴリで特に最初に覚えるべき 4〜6 語に key: true を付ける。\n\n## 書き方\n- word:用語。日本語+英語や略語を括弧で(例「逆運動学(IK)」「エンドエフェクタ(EE)」)。\n- short:意味を1文で。**20〜45字**。声に出して言いやすい、言い切りの文。専門用語を重ねない。\n 例:「手先の目標位置から、各関節の角度を逆算する計算」\n- detail:詳しい説明。**2〜3文・80〜160字**。①仕組みや使いどころ ②似た言葉との違いや注意点 ③(あれば)エイイチの実機での例。\n - ③を書くときは、必ず /home/claude/r2r2r-content/articles/ の記事を Grep で確かめ、書いてある事実だけを書き、src にその DAY を入れる(例 \"DAY150\")。確かめられないなら③は書かず src は \"\"。\n - 例は「mechArm では〜」のように短く。数値は記事どおり。\n- 一般知識の部分は、教科書・業界の標準的な定義に合わせる。あいまいなら書かない。誇張しない。\n- 文体:です・ます不要。短く言い切る。声に出したときに読みやすい表記(記号の多用、長いカッコ書きは避ける)。\n\n## 担当カテゴリ(category の値)\n- kinematics : ロボットの体と動き(自由度、関節、リンク、座標系、FK/IK、作業空間、特異点、姿勢の表し方 など)\n- hardware : モーターと制御(サーボ、エンコーダ、トルク、PID、フィードバック制御、可搬重量、繰り返し精度、通信 など)\n- sim : シミュレーションと開発環境(デジタルツイン、物理エンジン、URDF、ROS2、MoveIt、sim2real、ドメインランダマイゼーション など)\n- vision : カメラと画像認識(カメラ校正、内部・外部パラメータ、ホモグラフィ、色空間、物体検出、セグメンテーション、IoU、深度カメラ など)\n- servo : 動作の計画と実行(軌道計画、補間、視覚サーボ、ハンドアイ校正、把持、Pick&Place、衝突回避、コンプライアンス など)\n- learning : ロボット学習(模倣学習、強化学習、報酬、方策、テレオペ、デモデータ、VLA、基盤モデル、汎化、ファインチューニング など)\n- eval : 評価とデータ(成功率、試行、ベースライン、再現性、ロギング、データセット、アノテーション、誤差の種類 など)\n- principles : 開発の進め方(反復開発、ボトルネック、根本原因分析、フェイルセーフ、段階的検証、HRI の基本用語 など)\n- society : 社会実装と業界(協働ロボット、産業用ロボット、AMR、SIer、ROI、安全規格、リスクアセスメント、Physical AI など)\n\n## 出力\n`/home/claude/work/vocab/{担当名}.json` に JSON 配列(UTF-8、ensure_ascii=False):\n{\"category\",\"word\",\"short\",\"detail\",\"key\",\"src\"}\nカテゴリの順は上の並び、カテゴリ内は基礎→応用の順。書き終えたら json.load で確認し、short が20〜45字・detail が160字以内かを python で数えて直す。\n最後の返答は件数(カテゴリ別)だけ。</code></pre><h3 id=\"index_qL8efy5k\">判断の型104枚を作る指示(LESSON.md)</h3><p>markdown</p><pre><code class=\"language-markdown\"># 「実機の知見」を判断の型(教訓カード)に作り直す\n\n/home/claude/work/old_insights.json に、エイイチの開発ログ(mechArm 270 Pi を iPhone・Unity・Python で動かした DAY102〜326)から抜き出した 424 項目がある。\n事実の羅列(関節の割り当て、軸の向き、定数)で、声に出して丸暗記しても使える力にならない。\nこれを「こういう場面では、まずこれを疑う/こうする」という**判断の型**のカードに作り直す。\n別のロボット・別の現場でもそのまま使える形にし、全部が実話から来ているので自分の言葉として話せるようにする。\n\n## カードの形\n- scene(場面・問い):困る状況を1〜2文で。最後は「まず何を疑う?」「どうする?」などの問い。40〜80字。mechArm 固有の名前はなるべく一般化(「実機」「アーム」「カメラ」)。\n 例:「シミュでは合っているのに、実機だと手先が数cmずれる。まず何を確かめる?」\n- lesson(教訓・声に出す一言):15〜35字。言い切り。覚えて口に出せる長さ。\n 例:「座標系を1つに決めて、全部をそこにそろえる」\n- keywords:教訓の中で「これが言えていれば合格」という要の語を 2〜4 個。各要素は言い換えを含む配列(音声認識で漢字・かなが揺れるので、ひらがな表記や同義語も入れる)。\n 例:[[\"座標系\",\"座標\"],[\"1つ\",\"ひとつ\",\"一つ\",\"統一\"],[\"そろえる\",\"揃える\",\"合わせる\"]]\n- example(実例・読むだけ):実際に起きたことを1〜2文、80〜140字。数値は元項目どおり。「mechArm では〜」のように書いてよい。\n- src:実例の出典 DAY(元項目の src を引き継ぐ。複数可、カンマ区切り)\n- key:そのカテゴリで特に大事な型に true(カテゴリの 3 割程度まで)\n\n## やり方\n1. 担当カテゴリの元項目を全部読む(category で絞る)。\n2. 同じ教訓を言っている項目をまとめ、1つの型にする。1つの型の実例は一番分かりやすい1件(必要なら2件)を選ぶ。\n3. 教訓にならない「資料」(関節の割り当て、座標の向き、定数値、ツールの仕様だけの項目)は捨てる。ただしその資料から教訓が引き出せるなら教訓にする(例:ROSとUnityの軸の違い →「ツールをまたぐときは、右手系か左手系か・どの軸が上かを最初に確かめる」)。\n4. 担当カテゴリ合計で、1カテゴリあたり 6〜10 枚を目安(中身が濃いカテゴリは12枚まで)。\n5. 実例の事実は元項目に書いてあることだけ。迷ったら /home/claude/r2r2r-content/articles/DAY{NNN}*.md で確かめる。新しい数字や出来事を作らない。\n6. 文体:です・ます不要、短く言い切る。英略語は最小限。\n\n## 出力\n`/home/claude/work/lessons/{担当名}.json` に JSON 配列(UTF-8、ensure_ascii=False):\n{\"category\",\"scene\",\"lesson\",\"keywords\",\"example\",\"src\",\"key\"}\nカテゴリ内は「基礎的・よく起きる型 → 応用」の順。json.load で確認し、lesson が15〜35字に入っているか python で数える。\n最後の返答はカテゴリ別の枚数だけ。</code></pre><p></p><h2 id=\"index_pr4XWn0H\">次は同じ164語を「見ないで言う」でもう一周する</h2><p>単語の「見て言う」は、聞き違いを補正すると 182回中168回が 90%以上で、ほぼ言えるようになりました。次は同じ164語を「見ないで言う」で一周します。用語だけを見て意味を思い出す形になるので、本当に覚えているかはこちらで分かります。そのあと「2 説明」と「3 判断の型」に進みます。</p><p>練習の記録がたまったら、<code>practice_review.py</code> で集計し、3回続けて 80% に届かないカードの言い回しと、聞き違いの一覧を見直します。</p>", "toc": [{"id": "index_rNSeBrBJ", "text": "仕事で使っている台本の読み合わせページを、ロボティクスの復習用に作り変えた"}, {"id": "index_Ef3BykTT", "text": "開発ログ194本と調査メモから、9つのエージェントで復習項目を549件抜き出した"}, {"id": "index_nY3kXcq7", "text": "424項目を出典の記事と照合し、21件を直した"}, {"id": "index_6jUeiDe0", "text": "最初の版は「読むだけ」のページで、核心のカードが多すぎて見づらかった"}, {"id": "index_atypUTBf", "text": "声で確認するには https が要るので、GitHub Pages に置いた"}, {"id": "index_Lghl9cGq", "text": "押している間だけ聞き、カードの文と1文字ずつ比べる"}, {"id": "index_e1cF0_A9", "text": "いきなり難しい説明が並んでいたので、用語164語から始める3段階にした"}, {"id": "index_q7iw4fXg", "text": "「リンクは、関節と関節の間をつなぐ硬い骨組みの部分」と用語から続けて言う"}, {"id": "index__ZgAWoKQ", "text": "実機の知見424項目は事実の羅列で、声に出して覚える意味がなかった"}, {"id": "index_Ibt2SwNb", "text": "判断の型104枚:場面を見て、教訓をキーワードで言えたら合格"}, {"id": "index_J89hmYda", "text": "練習の記録は練習記録フォルダに保存し、集計してページに戻す"}, {"id": "index_ZzZgsbhc", "text": "単語ひととおり182回の記録:平均92%、低い点のほとんどは聞き違いだった"}, {"id": "index_Nb1eeiph", "text": "聞き違いの一覧を足して採点し直したら、平均95%になった"}, {"id": "index_K71F5Phd", "text": "カードを切り替えた直後に話すと、前のカードで採点されていた"}, {"id": "index_bfqmDzCf", "text": "iPhone の「ファイル」アプリのプレビューでは JavaScript が動かない"}, {"id": "index_qt8YkBwG", "text": "iPhone からの保存は、ショートカット「ロボ見直しを保存」で練習記録に直接入れる"}, {"id": "index_eByBD_Dn", "text": "練習記録の全量(182回)"}, {"id": "index_xj0uFjsd", "text": "聞き違いの一覧(homophones・56組)"}, {"id": "index_RbStIw79", "text": "ページのテンプレート template.html の全文"}, {"id": "index_xrC6Iabk", "text": "ページを書き出す build_review.py の全文"}, {"id": "index_54fjEQ2p", "text": "練習記録を集計する practice_review.py の全文"}, {"id": "index_fh6J9rox", "text": "抜き出しと照合に使った指示文"}, {"id": "index_pr4XWn0H", "text": "次は同じ164語を「見ないで言う」でもう一周する"}]}
\ No newline at end of file
diff --git a/src/data/records/visual-article-builder.json b/src/data/records/visual-article-builder.json
index 1a2bbee..a8d6a09 100644
--- a/src/data/records/visual-article-builder.json
+++ b/src/data/records/visual-article-builder.json
@@ -1 +1 @@
-{"day": "LOG193", "series": "ロボティクスデザイン", "published": "2026-05-12T07:43:59.000Z", "title": "記事をビジュアルウィジェットで作り直す日 | STUDIOとGitHub Pagesの二層公開システムを建てた", "slug": "visual-article-builder", "cover": "/media/s-1920x1080_5e830cee-dbdd-4c97-a173-9f57b7f0ced8.webp", "thumb": null, "lang": "JP", "description": "記事のmdをHTML化したい問いから始まり、STUDIOとGitHub Pagesの二層公開システムを建てた日。全テキスト段落を廃止して18種の視覚コンポーネントだけで再構成、Claude Projectで生成を自動化した。", "updated": "2026-05-12T08:29:35.000Z", "body": "<!--TOC--><p></p><p>記事のmdをそのままHTMLで表示したい、という素朴な問いから始まったセッションが、最後はClaude Projectで「ビジュアル記事ビルダー」を組み、STUDIOとGitHub Pagesの二層構造で記事を公開する運用フローを建てるところまで進んだ。テキスト段落を全廃して18種類の視覚コンポーネントだけで記事を再構成する、という振り切りが起点になった。</p><div class=\"embed\"><iframe src=\"/embeds/visual-article-builder-UjyrSni8.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p><strong>💡 Key Takeaway</strong></p><p>毎日のDAYログをSTUDIOにmdで投稿する運用は変えない。その横に「インタラクティブ版」というもう一つの公開層を建てた。インタラクティブ版はテキスト段落を完全に廃止し、全ての情報をカード・タグ・数値・アイコン・グリッド・対比のいずれかの視覚要素に変換する。Claude Projectに記事mdを渡すだけで自動生成され、GitHub Pagesで公開され、STUDIOの記事上部に置いたバナーから1クリックで往復できる構造になった。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>起きたこと</p></th><th colspan=\"1\" rowspan=\"1\"><p>現場判断</p></th><th colspan=\"1\" rowspan=\"1\"><p>結果</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>「mdをそのままHTML化したい」という当初の問いに対してmarkdown-it + highlight.js + 自前CSSのClaude Code変換システムを提案したが、意図がズレていた</p></td><td colspan=\"1\" rowspan=\"1\"><p>添付済みの <code>DAY192_two_cameras_role.html</code>(記事に埋め込むインタラクティブ図解ウィジェット)のフォーマットを記事全体に拡張したい、という意図と判明</p></td><td colspan=\"1\" rowspan=\"1\"><p>テンプレ方式は廃案、「記事内容に合わせた図解生成」へ方針転換</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>図解とテキストを混ぜた49KB・9セクションのHTMLを生成したが、テキスト段落と図解が並列する状態だった</p></td><td colspan=\"1\" rowspan=\"1\"><p>「なんかメリット感じないね。全部図解で作ってみて」という指示で全テキスト段落を視覚要素に変換</p></td><td colspan=\"1\" rowspan=\"1\"><p>value-card / keyphrase-block / spec-block / concern-card / before-after / tag-set など18種類の視覚コンポーネントが定義された</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>STUDIO(adaptdesign.jp)はインタラクティブHTMLが動かない(Embed経由のiframe表示のみ)ため、生成したビジュアル記事をどこで公開するか問題が発生</p></td><td colspan=\"1\" rowspan=\"1\"><p>STUDIO(通常版)とGitHub Pages(インタラクティブ版)の二層構造を採用。両者をバナーとロゴで相互リンク</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>pixelstomotors.github.io/interactive-articles/{slug}/</code> というURL構造で公開、STUDIO記事上部にP2M_Interactive_Bannerを埋め込み</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>記事が増えるたびに <code>index.html</code> を手で書き換えるのは持たない</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>articles.json</code> を別ファイルに切り出してJSが動的にカード描画する設計に変更</p></td><td colspan=\"1\" rowspan=\"1\"><p>新記事追加時は「フォルダ + index.html + articles.jsonの1ブロック追記」の3操作で完結する状態になった</p></td></tr></tbody></table></div><p></p><h2 id=\"index_dE_wyetb\">「mdをそのままHTMLで表示したい」という最初の問い</h2><div class=\"embed\"><iframe src=\"/embeds/visual-article-builder-yaiqXUST.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p>最初の問いはシンプルだった。毎日のDAYログはmdで書いている。SVG図解はHTMLで別添してSTUDIOにiframe埋め込みしている。ではmd本文そのものを「もっと記事のHPに近い見た目」でHTML化するには何がいいか。</p><p>最初の回答は、Claude Codeでmarkdown-itベースの変換システムを組む案だった。</p><pre><code>~/PixelsToMotors/article-html/\n├── template.html # 共通レイアウト\n├── styles.css # ブランド統一CSS\n├── convert.js # MD→HTML変換\n└── articles/\n └── DAY192_article.md</code></pre><p><code>node convert.js articles/DAY192.md</code> のような構造で、tables・fenced_code・toc・attr_list を効かせて、h2見出しから自動目次、Key Takeawayはコールアウト、Pixels to Motorsブランドに合わせたCSS、という方向で提示した。</p><p>技術的には正しい提案だった。ただ意図とズレていた。</p><pre><code>意図していたこと:\n 添付の DAY192_two_cameras_role.html のような\n 記事内に埋め込むインタラクティブ図解ウィジェットを\n 記事全体に対して量産したい\n\n提案していたこと:\n 記事mdを綺麗なHTMLページに変換するシステム</code></pre><p>この食い違いに気づくのに、最初のやりとり1回分を使った。</p><p></p><h2 id=\"index_MHLhPdbw\">テンプレ方式の廃案</h2><p>意図を取り違えていたことが分かった後、最初に提示したのは「タブ切替型・二項対比型・フロー型」の3種類のテンプレを作ってパーツ化する案だった。記事の論点ごとに、どのテンプレに当てはまるかを判定して使い回す、という構造化思考の発想だ。</p><pre><code>~/PixelsToMotors/figure-widgets/\n├── figure-style.css # 共通スタイル\n├── pattern-A-tabs.html # タブ切替テンプレ(2〜4タブ可変)\n├── pattern-B-compare.html # 二項対比テンプレ\n├── pattern-C-flow.html # フロー型テンプレ\n└── articles/\n ├── DAY192_two_cameras_role.html # 既存\n ├── DAY192_yokocam_position.html # 新規\n └── DAY192_grip_orientation.html</code></pre><p>前の記事の論点を9つにマッピングして、そのうち8つがパターンAタブ切替型で表現できる、という分析まで進めた。</p><p>ここで返ってきた答えはこうだった。</p><blockquote><p>記事に合わせて図解作るから(いつも記事の内容から図解を自動生成している)テンプレ方式の必要はないね。</p></blockquote><p>テンプレを作ってそこに記事の論点をはめ込むのではなく、記事の論点を見てから図解の形を作る。順序が逆だった。テンプレを固定すると論点が痩せる、というデザインシステムの古典的なトレードオフが、この場では「テンプレ廃止」という選択になった。</p><p>設計の意思決定として、これは大きい。固定テンプレを持つとブランドは揃うが、毎回の記事内容が痩せる。毎回ユニークに作るとブランドが揺れる可能性があるが、論点は活きる。どちらを取るか。ここでは後者を取った。</p><p>代わりに、ブランドの一貫性は「色は3色のみ・ボーダー1.5px・強調は白黒反転・フォントはIBM Plex MonoとNoto Sans JP」という最小限のルールだけで担保する方針にした。構造は自由、原則は固定。</p><p></p><h2 id=\"index_4SGpI1mu\">「全部図解で作って」への振り切り</h2><p>最初のビジュアル化版は、テキスト段落と図解ウィジェットを混ぜた49KBのHTMLだった。9セクション、図解ウィジェット9本、タブUI12個、フロー13ステップ、対比カラム12個入り。記事として「読む」流れに、要所で図解が挿入される構造。</p><p>これを見て返ってきた答えはこうだった。</p><blockquote><p>なんかメリット感じないね。全部図解で作ってみて。</p></blockquote><p>STUDIOの通常版でも読める内容を、HTMLでもう一度同じように読ませる意味がない、ということだ。インタラクティブ版を別公開層として作るなら、STUDIO版とは別の体験価値を持たせる必要がある。</p><p>全テキスト段落を視覚要素に変換する作業に振り切った。各セクションの導入文(section-lead)は完全廃止、図解の補足文(widget-note)はキーワードのタグ集合や数値カードに変換、引用ブロックは「キーフレーズ・ブロック」、決定カードのスペック列挙はアイコン+キー+値の4分割グリッド、警告文は「concern-card」、フローステップの説明文はタグ集合、締めの一文は「世界の対比」(before-after)として視覚化。</p><p>廃止したテキスト要素と変換先を整理すると、以下のようになった。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>元のテキスト要素</p></th><th colspan=\"1\" rowspan=\"1\"><p>変換先の視覚要素</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>section-lead(各セクションの導入文)</p></td><td colspan=\"1\" rowspan=\"1\"><p>完全廃止</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>widget-note(図解下の補足)</p></td><td colspan=\"1\" rowspan=\"1\"><p>tag-set または value-row</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Z係数 4.786 の説明文</p></td><td colspan=\"1\" rowspan=\"1\"><p>取り消し線付き value-card 3枚並び</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>quote-card の引用文</p></td><td colspan=\"1\" rowspan=\"1\"><p>keyphrase-block(大フレーズ + ソース)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>decision-card のスペック列挙</p></td><td colspan=\"1\" rowspan=\"1\"><p>spec-block(アイコン + キー + 値の4分割グリッド)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>warning-card の警告文</p></td><td colspan=\"1\" rowspan=\"1\"><p>concern-card(アイコン + statsグリッド + actionタグ)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>flow-step-detail の説明文</p></td><td colspan=\"1\" rowspan=\"1\"><p>flow-step-tags(キーワードのタグ集合)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>closing-statement のフレーズ</p></td><td colspan=\"1\" rowspan=\"1\"><p>before-after(左右対比 + チェック項目)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>takeaway-summary の段落</p></td><td colspan=\"1\" rowspan=\"1\"><p>完全廃止(3カードのみに)</p></td></tr></tbody></table></div><p>これで「読む文章」はゼロになり、全情報が「カード・タグ・数値・アイコン・グリッド・対比」のいずれかの視覚要素に変換されたHTMLが完成した。インタラクティブ版の輪郭がここで初めて見えた。</p><p></p><h2 id=\"index_JSlUtY9g\">公開先の選択:STUDIOとGitHub Pages</h2><p>ビジュアル記事ができても、どこに置くかが次の問題になった。STUDIO(adaptdesign.jp)とGitHub Pagesの差を整理した。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>STUDIO</p></th><th colspan=\"1\" rowspan=\"1\"><p>GitHub Pages</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>HTML/CSS/JSの自由度</p></td><td colspan=\"1\" rowspan=\"1\"><p>制限的</p></td><td colspan=\"1\" rowspan=\"1\"><p>完全自由</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>インタラクティブ図解</p></td><td colspan=\"1\" rowspan=\"1\"><p>Embed経由のiframeのみ</p></td><td colspan=\"1\" rowspan=\"1\"><p>そのまま動く</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>サイト統合</p></td><td colspan=\"1\" rowspan=\"1\"><p>adaptdesign.jp内</p></td><td colspan=\"1\" rowspan=\"1\"><p>別URL</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>SEO/OGP</p></td><td colspan=\"1\" rowspan=\"1\"><p>STUDIO側で管理</p></td><td colspan=\"1\" rowspan=\"1\"><p>自前で設定</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>編集の手軽さ</p></td><td colspan=\"1\" rowspan=\"1\"><p>Cmd+Vで貼るだけ</p></td><td colspan=\"1\" rowspan=\"1\"><p>git push必要</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>コスト</p></td><td colspan=\"1\" rowspan=\"1\"><p>有料プラン込み</p></td><td colspan=\"1\" rowspan=\"1\"><p>無料</p></td></tr></tbody></table></div><p>タブ切替・SVG・アコーディオン展開が動くインタラクティブ版は、Markdownでは再現できないし、STUDIOでもネイティブには動かない。だから普段通りSTUDIOにCmd+Vでペーストするmd記事の運用には収まらない。</p><p>選択肢は3つあった。</p><pre><code>A. 全部GitHub Pagesに置く\n → adaptdesign.jp の記事側からリンクで誘導\n → 別URLになるが、見た目もインタラクションもそのまま反映される\n\nB. STUDIO記事内に iframeでGitHub Pages版を埋め込む\n → URLはadaptdesign.jpのまま、中身だけGitHub Pagesから配信\n → 高さ調整やレスポンシブで若干工夫が必要\n\nC. 図解ウィジェットだけ部分埋め込み\n → 普段の運用に近い、これまでと同じ\n → 記事全体の図解化はできない</code></pre><p>ここで採用したのは案Aの拡張形だ。STUDIO側は通常のmd記事をそのまま運用、別途GitHub Pagesに「インタラクティブ版」を公開し、両者を相互リンクで往復させる。</p><blockquote><p>Pixels to Motors の「インタラクティブ版」として GitHub Pages に置き、adaptdesign.jp の通常記事末尾に「→インタラクティブ版を見る」リンクを置く。役割が違うコンテンツとして共存させる形。</p></blockquote><p>役割を分けるという選択だ。STUDIO版は「読む」、インタラクティブ版は「触る」。同じ情報を別の体験として2回提供する。</p><p></p><h2 id=\"index_hBKLtniT\">Claude Projectで生成システム化</h2><p>二層構造を決めた後、毎回手作業で全図解化したHTMLを書くわけにはいかない。生成パイプラインをClaude Projectで組んだ。</p><pre><code>Project: Pixels to Motors Visual Article Builder\n\nカスタム指示:\n PROJECT_INSTRUCTIONS.md(プロジェクト指示文として貼り付け)\n\nナレッジ:\n TEMPLATE.html (34KB) — CSS/JS完備の骨組み\n COMPONENTS.md (16KB) — 18コンポーネントのカタログ</code></pre><p><code>PROJECT_INSTRUCTIONS.md</code> には、白黒ミニマル、段落完全廃止、18コンポーネントの選び方、ブランドURL定義、HTMLサンプル、仕上げチェックリスト、タイトル構造ルールが入っている。プロジェクト内で新しいチャットを開いて、記事のmd(例:<code>DAY{N}_article.md</code>)を添付して送るだけで、毎回同じ品質で <code>DAY{N}_visual_article.html</code> が生成される、という運用になる。</p><p><code>TEMPLATE.html</code> はCSS・JS・Google Fontsのpreconnectまで全部入った骨組みで、body部分はガイドコメント付きの空欄。<code>COMPONENTS.md</code> は18種類のコンポーネント(value-card, keyphrase-block, spec-block, concern-card, before-after, tag-set, flow-step-tags, value-row, tabs, compare-cols, flow-steps, matrix, timeline, annotated-svg, cards, header, tabs-multi, list-stack)について、HTMLサンプル付きで使い分けを定義したもの。</p><p>注意点として、PROJECT_INSTRUCTIONS.md には「読む文章はゼロ、全部図解」という原則を明記している。これがStep 2のコピペで漏れると、毎回の出力で散文段落が混ざりやすくなる。試行錯誤で固まったルールを、ここに集約してある。</p><p></p><h2 id=\"index_yuCd7qg8\">STUDIOとGitHub Pagesをバナーで往復させる</h2><p>二層をどう接続するか。STUDIO記事の上部に、インタラクティブ版へのCTAバナーを置く設計にした。</p><pre><code>┌────────────────────────────────────────────────┐\n│ [■] ▶ INTERACTIVE EDITION │\n│ [■] この記事をインタラクティブ版で読む [View →] │\n│ [■] 全要素図解化 / タブ切替 / SVG動的表示 │\n└────────────────────────────────────────────────┘\n ↑ ↑ ↑\n ミニ図解 テキスト2行 CTAボタン</code></pre><p>仕様:</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>内容</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>サイズ</p></td><td colspan=\"1\" rowspan=\"1\"><p>720×160px (max-width)、レスポンシブで自動縮小</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>カラー</p></td><td colspan=\"1\" rowspan=\"1\"><p>ブラックベース (#0d0d0d) + 白文字 (#f5f5f0)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>構成</p></td><td colspan=\"1\" rowspan=\"1\"><p>ミニ図解アイコン + 2行テキスト + CTAボタン</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>インタラクション</p></td><td colspan=\"1\" rowspan=\"1\"><p>ホバーで背景がスッとグレーに変化(動きはなし)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>設定箇所</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>href=\"https://YOUR-USERNAME.github.io/YOUR-REPO/\"</code> の1か所を実URLに書き換え</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>STUDIOへの設置</p></td><td colspan=\"1\" rowspan=\"1\"><p>「埋め込み」ブロックにHTML全体をコピペ</p></td></tr></tbody></table></div><p>最初の版ではホバー時に2px浮き上がる・CTAボタンが白黒反転・矢印が右にスライド、という動きを入れていたが、「マウスオーバーでグレーになるよう修正(動きはなし)」という指示で全廃した。上下paddingはコンテンツに合わせて自動で詰まる構造にした(aspect-ratio: 720/160を削除、padding 20pxに統一)。</p><p>ブラックベース+白文字+太めボーダー+ミニ図解アイコンで、明らかに「外部リンク・別バージョン」とわかる作りにしている。</p><p></p><h2 id=\"index_wzGQtT5f\">リポジトリ構造とURL設計</h2><p>GitHub Pagesで公開するために、<code>PixelsToMotors/interactive-articles</code> というPublicリポジトリを新設した。<code>PixelsToMotors/r2r2r-content</code> はPrivateで、Free planではPagesが使えない。Public用に新リポジトリを切り出すのが最もシンプルで、記事資産(Private)と公開コンテンツ(Public)を分離できるメリットもある。</p><p>リポジトリ構造:</p><pre><code>interactive-articles/ ← Public\n├── README.md\n├── index.html ← 記事一覧(JSで動的描画)\n├── articles.json ← 記事メタデータ\n└── setting-up-the-side-camera/\n └── index.html ← DAY192のインタラクティブ版</code></pre><p>URLは <code>https://pixelstomotors.github.io/interactive-articles/setting-up-the-side-camera/</code> という形になる。<code>setting-up-the-side-camera</code> はSTUDIO側の記事スラッグと揃えてあり、対応関係が一目で分かる。</p><p>新しい記事を追加する時は、新フォルダを切って中に <code>index.html</code> を置く。例えば次の記事(横カメラのキャリブレーション)なら <code>parallel-slide-kinematics/index.html</code> のような構造になる。</p><p>GitHub Pages有効化の手順は以下の通り。</p><pre><code>1. リポジトリの Settings → Pages\n2. Source: Deploy from a branch を選択\n3. Branch: main / (root) を選択\n4. Save\n5. 1〜2分待つと https://pixelstomotors.github.io/interactive-articles/ で公開</code></pre><p></p><h2 id=\"index_ZykWpOpz\">一覧ページとarticles.json</h2><p>最初の <code>index.html</code> は記事カードをHTMLに直書きする形だった。これだと記事が増えるたびに <code>index.html</code> を毎回書き換えなければならない。3記事目で疲れる構造だ。</p><p>そこで <code>articles.json</code> に記事メタデータを切り出して、<code>index.html</code> はJSで動的に描画する設計に変更した。</p><p><code>articles.json</code> の構造:</p><pre><code class=\"language-json\">{\n \"articles\": [\n {\n \"day\": 193,\n \"slug\": \"calibrate-side-camera\",\n \"date\": \"2026-05-10\",\n \"category\": \"デジタルツイン学習\",\n \"series\": \"mechArm 270 Pi 動的追従の限界を探る ─ Log #11\",\n \"title\": \"横カメラのキャリブレーション完了 ─ アームをキャリブ装置として使う\"\n },\n {\n \"day\": 192,\n \"slug\": \"setting-up-the-side-camera\",\n \"date\": \"2026-05-07\",\n \"category\": \"デジタルツイン学習\",\n \"series\": \"mechArm 270 Pi 動的追従の限界を探る ─ Log #10\",\n \"title\": \"視点を増やす日 ─ 設計判断と iPhone 2台体制の物理構築\"\n }\n ]\n}</code></pre><p><code>index.html</code> 側はfetchで <code>articles.json</code> を読み、day番号の降順で自動ソートしてカードを描画する。一覧上には件数カウンタも入っていて、「01 / 全1件」のように自動更新される。</p><p>これで、新記事を追加する時の作業は以下の3操作だけになる。</p><pre><code>1. interactive-articles リポジトリのトップで Add file → Upload files\n → 新フォルダ(slug)/index.html をアップロード\n\n2. articles.json を開いて鉛筆アイコンで編集\n → 配列の先頭に新記事のメタデータブロックを1つ追加\n\n3. STUDIO通常記事に P2M_Interactive_Banner を埋め込み\n → href を新URLに書き換えるだけ</code></pre><p><code>index.html</code> 自体は最初に作った後は一切触らない。記事が増えても触らない。</p><p></p><h2 id=\"index_fxRVIJXN\">一覧ページの情報設計</h2><p>一覧ページは最初、記事カードだけを並べた最小構成だった。これに対して「Pixels to Motorsのページとわかるように最低限の情報は追加してください」という指示で、以下を補強した。</p><pre><code>1. Heroセクションに小見出しタグ\n ┌──────────────────────────────────────┐\n │ Pixels to Motors — デジタルツインが生み出す次世代ロボティクスUX │\n └──────────────────────────────────────┘\n \n ▶ INTERACTIVE EDITION ▶ 全要素図解版\n \n INTERACTIVE ARTICLES ← 巨大に(40〜72px)\n 記事の判断・設計・データを...\n\n2. Aboutセクション(記事リストの下)\n About / 事業所\n ─────────────\n 29年の UI/UX デザイン経験を | Designer 島津 栄一\n ロボティクスへ。R2R2R プロセスで、 | Office ADAPT DESIGN\n iPhone での 3D スキャンから物理 | Location 秋田県男鹿市\n シミュレーション、実機制御までを | Founded 2021年10月\n 1人で担う実験記録。 |</code></pre><p>これで一覧ページ単体で訪問されても「Pixels to Motors プロジェクトの公式インタラクティブアーカイブ」として完結する。ハコブくんチャットやプロトタイプセクションは含めず、必要な3要素(プロジェクト名 / 記事一覧 / 制作者情報)に絞っている。</p><p>トップバーには <code>Pixels to Motors</code> ロゴ(→ adaptdesign.jp/pixelstomotors)と「← 通常記事一覧」(→ adaptdesign.jp/pixelstomotors)を配置。各記事の右上には <code>DAY 192</code> タグ(→ adaptdesign.jp/pixelstomotors/record/{slug})を置いた。これで以下の往復が成立する。</p><pre><code>STUDIO通常記事 (adaptdesign.jp/pixelstomotors/record/setting-up-the-side-camera)\n ↓\n │ [この記事をインタラクティブ版で読む →] ← バナークリック\n ↓\nGitHub Pages インタラクティブ版 (pixelstomotors.github.io/interactive-articles/setting-up-the-side-camera/)\n ↓\n │ 右上「DAY 192」クリック → STUDIO記事に戻る\n ↓\nSTUDIO記事ページ</code></pre><h2 id=\"index_zJtKONJk\">完成した構成</h2><p>最終的に整った構成はこうなった。</p><pre><code>[STUDIO通常記事] ⇄ [GitHub Pages インタラクティブ版]\n ↑ バナーで誘導 ↑ DAY番号タグでSTUDIOに戻る\n ↑ ブランドロゴで一覧に戻る\n\n[インタラクティブ版一覧]\n └ articles.json を編集するだけで自動更新</code></pre><p>プロジェクトに登録する3点:</p><ul><li><p><code>PROJECT_INSTRUCTIONS.md</code>(カスタム指示)</p></li><li><p><code>TEMPLATE.html</code>(ナレッジ)</p></li><li><p><code>COMPONENTS.md</code>(ナレッジ)</p></li></ul><p>GitHub <code>interactive-articles</code> リポジトリ:</p><ul><li><p><code>index.html</code>(動的描画、編集不要)</p></li><li><p><code>articles.json</code>(記事追加時に編集)</p></li><li><p>各記事フォルダ + <code>index.html</code></p></li></ul><p></p><h2 id=\"index__P1SsnDG\">このセッションが示したこと</h2><p>最初の問いは「mdをHTML化したい」というシンプルなものだった。そこから最終的に建てたのは、生成パイプライン(Claude Project)と公開インフラ(GitHub Pages + STUDIO)と運用フロー(3ステップ)を含む、独立した一つの公開システムだった。</p><p>転換点は2回ある。</p><p>1回目は「テンプレ方式の廃止」。固定テンプレに記事の論点を流し込むのではなく、記事の論点を見てから視覚要素を組み立てる、という順序の選択。デザインシステムとしては「部品は最小限・原則は固定・構造は自由」という非典型な設計になった。</p><p>2回目は「全テキスト段落の廃止」。「全部図解で」という指示が、インタラクティブ版とSTUDIO版の役割分担を初めて明確にした。同じ情報を別の体験として2回提供する、という二層構造の輪郭が、ここで決まった。</p><p>公開インフラとして見ると、STUDIO(編集ラク・有料・JS不可)とGitHub Pages(無料・JS可・git push必要)はトレードオフが完全に逆向きで、どちらかを選ぶというより、両方をそれぞれの役割で使う方が合理的だった。バナーとロゴで相互リンクすると、訪問者から見れば「同じプロジェクトの2つの読み方」になる。</p><p><code>articles.json</code> を分離した判断も後から効いてくる。<code>index.html</code> を毎回書き換える運用は3記事目で破綻するが、JSON1ブロック追加なら100記事目でも持つ。Build-in-publicの長期プロジェクトでは、初期の運用設計が10ヶ月後の負債になりやすい。早めに動的化しておくと、未来の自分が助かる。</p><p>今後の新記事は、<code>DAY{N}_article.md</code> をプロジェクトに添付するだけで <code>DAY{N}_visual_article.html</code> と <code>articles.json</code> 追記ブロックが揃う。後はGitHub Web UIで2操作、STUDIO記事にバナー1枚埋め込み、で完結する。記事を書く側の工数は、STUDIOにmdを貼ってから3〜5分で公開まで進む計算になる。</p><p>このセッションは、Build-in-publicの公開層を「STUDIOだけ」から「STUDIO + インタラクティブ版」に拡張した日として残しておく。</p>", "toc": [{"id": "index_dE_wyetb", "text": "「mdをそのままHTMLで表示したい」という最初の問い"}, {"id": "index_MHLhPdbw", "text": "テンプレ方式の廃案"}, {"id": "index_4SGpI1mu", "text": "「全部図解で作って」への振り切り"}, {"id": "index_JSlUtY9g", "text": "公開先の選択:STUDIOとGitHub Pages"}, {"id": "index_hBKLtniT", "text": "Claude Projectで生成システム化"}, {"id": "index_yuCd7qg8", "text": "STUDIOとGitHub Pagesをバナーで往復させる"}, {"id": "index_wzGQtT5f", "text": "リポジトリ構造とURL設計"}, {"id": "index_ZykWpOpz", "text": "一覧ページとarticles.json"}, {"id": "index_fxRVIJXN", "text": "一覧ページの情報設計"}, {"id": "index_zJtKONJk", "text": "完成した構成"}, {"id": "index__P1SsnDG", "text": "このセッションが示したこと"}]}
\ No newline at end of file
+{"day": "LOG193", "series": "ひとり研究所の裏側", "published": "2026-05-12T07:43:59.000Z", "title": "記事をビジュアルウィジェットで作り直す日 | STUDIOとGitHub Pagesの二層公開システムを建てた", "slug": "visual-article-builder", "cover": "/media/s-1920x1080_5e830cee-dbdd-4c97-a173-9f57b7f0ced8.webp", "thumb": null, "lang": "JP", "description": "記事のmdをHTML化したい問いから始まり、STUDIOとGitHub Pagesの二層公開システムを建てた日。全テキスト段落を廃止して18種の視覚コンポーネントだけで再構成、Claude Projectで生成を自動化した。", "updated": "2026-05-12T08:29:35.000Z", "body": "<!--TOC--><p></p><p>記事のmdをそのままHTMLで表示したい、という素朴な問いから始まったセッションが、最後はClaude Projectで「ビジュアル記事ビルダー」を組み、STUDIOとGitHub Pagesの二層構造で記事を公開する運用フローを建てるところまで進んだ。テキスト段落を全廃して18種類の視覚コンポーネントだけで記事を再構成する、という振り切りが起点になった。</p><div class=\"embed\"><iframe src=\"/embeds/visual-article-builder-UjyrSni8.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p><strong>💡 Key Takeaway</strong></p><p>毎日のDAYログをSTUDIOにmdで投稿する運用は変えない。その横に「インタラクティブ版」というもう一つの公開層を建てた。インタラクティブ版はテキスト段落を完全に廃止し、全ての情報をカード・タグ・数値・アイコン・グリッド・対比のいずれかの視覚要素に変換する。Claude Projectに記事mdを渡すだけで自動生成され、GitHub Pagesで公開され、STUDIOの記事上部に置いたバナーから1クリックで往復できる構造になった。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>起きたこと</p></th><th colspan=\"1\" rowspan=\"1\"><p>現場判断</p></th><th colspan=\"1\" rowspan=\"1\"><p>結果</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>「mdをそのままHTML化したい」という当初の問いに対してmarkdown-it + highlight.js + 自前CSSのClaude Code変換システムを提案したが、意図がズレていた</p></td><td colspan=\"1\" rowspan=\"1\"><p>添付済みの <code>DAY192_two_cameras_role.html</code>(記事に埋め込むインタラクティブ図解ウィジェット)のフォーマットを記事全体に拡張したい、という意図と判明</p></td><td colspan=\"1\" rowspan=\"1\"><p>テンプレ方式は廃案、「記事内容に合わせた図解生成」へ方針転換</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>図解とテキストを混ぜた49KB・9セクションのHTMLを生成したが、テキスト段落と図解が並列する状態だった</p></td><td colspan=\"1\" rowspan=\"1\"><p>「なんかメリット感じないね。全部図解で作ってみて」という指示で全テキスト段落を視覚要素に変換</p></td><td colspan=\"1\" rowspan=\"1\"><p>value-card / keyphrase-block / spec-block / concern-card / before-after / tag-set など18種類の視覚コンポーネントが定義された</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>STUDIO(adaptdesign.jp)はインタラクティブHTMLが動かない(Embed経由のiframe表示のみ)ため、生成したビジュアル記事をどこで公開するか問題が発生</p></td><td colspan=\"1\" rowspan=\"1\"><p>STUDIO(通常版)とGitHub Pages(インタラクティブ版)の二層構造を採用。両者をバナーとロゴで相互リンク</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>pixelstomotors.github.io/interactive-articles/{slug}/</code> というURL構造で公開、STUDIO記事上部にP2M_Interactive_Bannerを埋め込み</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>記事が増えるたびに <code>index.html</code> を手で書き換えるのは持たない</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>articles.json</code> を別ファイルに切り出してJSが動的にカード描画する設計に変更</p></td><td colspan=\"1\" rowspan=\"1\"><p>新記事追加時は「フォルダ + index.html + articles.jsonの1ブロック追記」の3操作で完結する状態になった</p></td></tr></tbody></table></div><p></p><h2 id=\"index_dE_wyetb\">「mdをそのままHTMLで表示したい」という最初の問い</h2><div class=\"embed\"><iframe src=\"/embeds/visual-article-builder-yaiqXUST.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p>最初の問いはシンプルだった。毎日のDAYログはmdで書いている。SVG図解はHTMLで別添してSTUDIOにiframe埋め込みしている。ではmd本文そのものを「もっと記事のHPに近い見た目」でHTML化するには何がいいか。</p><p>最初の回答は、Claude Codeでmarkdown-itベースの変換システムを組む案だった。</p><pre><code>~/PixelsToMotors/article-html/\n├── template.html # 共通レイアウト\n├── styles.css # ブランド統一CSS\n├── convert.js # MD→HTML変換\n└── articles/\n └── DAY192_article.md</code></pre><p><code>node convert.js articles/DAY192.md</code> のような構造で、tables・fenced_code・toc・attr_list を効かせて、h2見出しから自動目次、Key Takeawayはコールアウト、Pixels to Motorsブランドに合わせたCSS、という方向で提示した。</p><p>技術的には正しい提案だった。ただ意図とズレていた。</p><pre><code>意図していたこと:\n 添付の DAY192_two_cameras_role.html のような\n 記事内に埋め込むインタラクティブ図解ウィジェットを\n 記事全体に対して量産したい\n\n提案していたこと:\n 記事mdを綺麗なHTMLページに変換するシステム</code></pre><p>この食い違いに気づくのに、最初のやりとり1回分を使った。</p><p></p><h2 id=\"index_MHLhPdbw\">テンプレ方式の廃案</h2><p>意図を取り違えていたことが分かった後、最初に提示したのは「タブ切替型・二項対比型・フロー型」の3種類のテンプレを作ってパーツ化する案だった。記事の論点ごとに、どのテンプレに当てはまるかを判定して使い回す、という構造化思考の発想だ。</p><pre><code>~/PixelsToMotors/figure-widgets/\n├── figure-style.css # 共通スタイル\n├── pattern-A-tabs.html # タブ切替テンプレ(2〜4タブ可変)\n├── pattern-B-compare.html # 二項対比テンプレ\n├── pattern-C-flow.html # フロー型テンプレ\n└── articles/\n ├── DAY192_two_cameras_role.html # 既存\n ├── DAY192_yokocam_position.html # 新規\n └── DAY192_grip_orientation.html</code></pre><p>前の記事の論点を9つにマッピングして、そのうち8つがパターンAタブ切替型で表現できる、という分析まで進めた。</p><p>ここで返ってきた答えはこうだった。</p><blockquote><p>記事に合わせて図解作るから(いつも記事の内容から図解を自動生成している)テンプレ方式の必要はないね。</p></blockquote><p>テンプレを作ってそこに記事の論点をはめ込むのではなく、記事の論点を見てから図解の形を作る。順序が逆だった。テンプレを固定すると論点が痩せる、というデザインシステムの古典的なトレードオフが、この場では「テンプレ廃止」という選択になった。</p><p>設計の意思決定として、これは大きい。固定テンプレを持つとブランドは揃うが、毎回の記事内容が痩せる。毎回ユニークに作るとブランドが揺れる可能性があるが、論点は活きる。どちらを取るか。ここでは後者を取った。</p><p>代わりに、ブランドの一貫性は「色は3色のみ・ボーダー1.5px・強調は白黒反転・フォントはIBM Plex MonoとNoto Sans JP」という最小限のルールだけで担保する方針にした。構造は自由、原則は固定。</p><p></p><h2 id=\"index_4SGpI1mu\">「全部図解で作って」への振り切り</h2><p>最初のビジュアル化版は、テキスト段落と図解ウィジェットを混ぜた49KBのHTMLだった。9セクション、図解ウィジェット9本、タブUI12個、フロー13ステップ、対比カラム12個入り。記事として「読む」流れに、要所で図解が挿入される構造。</p><p>これを見て返ってきた答えはこうだった。</p><blockquote><p>なんかメリット感じないね。全部図解で作ってみて。</p></blockquote><p>STUDIOの通常版でも読める内容を、HTMLでもう一度同じように読ませる意味がない、ということだ。インタラクティブ版を別公開層として作るなら、STUDIO版とは別の体験価値を持たせる必要がある。</p><p>全テキスト段落を視覚要素に変換する作業に振り切った。各セクションの導入文(section-lead)は完全廃止、図解の補足文(widget-note)はキーワードのタグ集合や数値カードに変換、引用ブロックは「キーフレーズ・ブロック」、決定カードのスペック列挙はアイコン+キー+値の4分割グリッド、警告文は「concern-card」、フローステップの説明文はタグ集合、締めの一文は「世界の対比」(before-after)として視覚化。</p><p>廃止したテキスト要素と変換先を整理すると、以下のようになった。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>元のテキスト要素</p></th><th colspan=\"1\" rowspan=\"1\"><p>変換先の視覚要素</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>section-lead(各セクションの導入文)</p></td><td colspan=\"1\" rowspan=\"1\"><p>完全廃止</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>widget-note(図解下の補足)</p></td><td colspan=\"1\" rowspan=\"1\"><p>tag-set または value-row</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>Z係数 4.786 の説明文</p></td><td colspan=\"1\" rowspan=\"1\"><p>取り消し線付き value-card 3枚並び</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>quote-card の引用文</p></td><td colspan=\"1\" rowspan=\"1\"><p>keyphrase-block(大フレーズ + ソース)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>decision-card のスペック列挙</p></td><td colspan=\"1\" rowspan=\"1\"><p>spec-block(アイコン + キー + 値の4分割グリッド)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>warning-card の警告文</p></td><td colspan=\"1\" rowspan=\"1\"><p>concern-card(アイコン + statsグリッド + actionタグ)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>flow-step-detail の説明文</p></td><td colspan=\"1\" rowspan=\"1\"><p>flow-step-tags(キーワードのタグ集合)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>closing-statement のフレーズ</p></td><td colspan=\"1\" rowspan=\"1\"><p>before-after(左右対比 + チェック項目)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>takeaway-summary の段落</p></td><td colspan=\"1\" rowspan=\"1\"><p>完全廃止(3カードのみに)</p></td></tr></tbody></table></div><p>これで「読む文章」はゼロになり、全情報が「カード・タグ・数値・アイコン・グリッド・対比」のいずれかの視覚要素に変換されたHTMLが完成した。インタラクティブ版の輪郭がここで初めて見えた。</p><p></p><h2 id=\"index_JSlUtY9g\">公開先の選択:STUDIOとGitHub Pages</h2><p>ビジュアル記事ができても、どこに置くかが次の問題になった。STUDIO(adaptdesign.jp)とGitHub Pagesの差を整理した。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>STUDIO</p></th><th colspan=\"1\" rowspan=\"1\"><p>GitHub Pages</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>HTML/CSS/JSの自由度</p></td><td colspan=\"1\" rowspan=\"1\"><p>制限的</p></td><td colspan=\"1\" rowspan=\"1\"><p>完全自由</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>インタラクティブ図解</p></td><td colspan=\"1\" rowspan=\"1\"><p>Embed経由のiframeのみ</p></td><td colspan=\"1\" rowspan=\"1\"><p>そのまま動く</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>サイト統合</p></td><td colspan=\"1\" rowspan=\"1\"><p>adaptdesign.jp内</p></td><td colspan=\"1\" rowspan=\"1\"><p>別URL</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>SEO/OGP</p></td><td colspan=\"1\" rowspan=\"1\"><p>STUDIO側で管理</p></td><td colspan=\"1\" rowspan=\"1\"><p>自前で設定</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>編集の手軽さ</p></td><td colspan=\"1\" rowspan=\"1\"><p>Cmd+Vで貼るだけ</p></td><td colspan=\"1\" rowspan=\"1\"><p>git push必要</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>コスト</p></td><td colspan=\"1\" rowspan=\"1\"><p>有料プラン込み</p></td><td colspan=\"1\" rowspan=\"1\"><p>無料</p></td></tr></tbody></table></div><p>タブ切替・SVG・アコーディオン展開が動くインタラクティブ版は、Markdownでは再現できないし、STUDIOでもネイティブには動かない。だから普段通りSTUDIOにCmd+Vでペーストするmd記事の運用には収まらない。</p><p>選択肢は3つあった。</p><pre><code>A. 全部GitHub Pagesに置く\n → adaptdesign.jp の記事側からリンクで誘導\n → 別URLになるが、見た目もインタラクションもそのまま反映される\n\nB. STUDIO記事内に iframeでGitHub Pages版を埋め込む\n → URLはadaptdesign.jpのまま、中身だけGitHub Pagesから配信\n → 高さ調整やレスポンシブで若干工夫が必要\n\nC. 図解ウィジェットだけ部分埋め込み\n → 普段の運用に近い、これまでと同じ\n → 記事全体の図解化はできない</code></pre><p>ここで採用したのは案Aの拡張形だ。STUDIO側は通常のmd記事をそのまま運用、別途GitHub Pagesに「インタラクティブ版」を公開し、両者を相互リンクで往復させる。</p><blockquote><p>Pixels to Motors の「インタラクティブ版」として GitHub Pages に置き、adaptdesign.jp の通常記事末尾に「→インタラクティブ版を見る」リンクを置く。役割が違うコンテンツとして共存させる形。</p></blockquote><p>役割を分けるという選択だ。STUDIO版は「読む」、インタラクティブ版は「触る」。同じ情報を別の体験として2回提供する。</p><p></p><h2 id=\"index_hBKLtniT\">Claude Projectで生成システム化</h2><p>二層構造を決めた後、毎回手作業で全図解化したHTMLを書くわけにはいかない。生成パイプラインをClaude Projectで組んだ。</p><pre><code>Project: Pixels to Motors Visual Article Builder\n\nカスタム指示:\n PROJECT_INSTRUCTIONS.md(プロジェクト指示文として貼り付け)\n\nナレッジ:\n TEMPLATE.html (34KB) — CSS/JS完備の骨組み\n COMPONENTS.md (16KB) — 18コンポーネントのカタログ</code></pre><p><code>PROJECT_INSTRUCTIONS.md</code> には、白黒ミニマル、段落完全廃止、18コンポーネントの選び方、ブランドURL定義、HTMLサンプル、仕上げチェックリスト、タイトル構造ルールが入っている。プロジェクト内で新しいチャットを開いて、記事のmd(例:<code>DAY{N}_article.md</code>)を添付して送るだけで、毎回同じ品質で <code>DAY{N}_visual_article.html</code> が生成される、という運用になる。</p><p><code>TEMPLATE.html</code> はCSS・JS・Google Fontsのpreconnectまで全部入った骨組みで、body部分はガイドコメント付きの空欄。<code>COMPONENTS.md</code> は18種類のコンポーネント(value-card, keyphrase-block, spec-block, concern-card, before-after, tag-set, flow-step-tags, value-row, tabs, compare-cols, flow-steps, matrix, timeline, annotated-svg, cards, header, tabs-multi, list-stack)について、HTMLサンプル付きで使い分けを定義したもの。</p><p>注意点として、PROJECT_INSTRUCTIONS.md には「読む文章はゼロ、全部図解」という原則を明記している。これがStep 2のコピペで漏れると、毎回の出力で散文段落が混ざりやすくなる。試行錯誤で固まったルールを、ここに集約してある。</p><p></p><h2 id=\"index_yuCd7qg8\">STUDIOとGitHub Pagesをバナーで往復させる</h2><p>二層をどう接続するか。STUDIO記事の上部に、インタラクティブ版へのCTAバナーを置く設計にした。</p><pre><code>┌────────────────────────────────────────────────┐\n│ [■] ▶ INTERACTIVE EDITION │\n│ [■] この記事をインタラクティブ版で読む [View →] │\n│ [■] 全要素図解化 / タブ切替 / SVG動的表示 │\n└────────────────────────────────────────────────┘\n ↑ ↑ ↑\n ミニ図解 テキスト2行 CTAボタン</code></pre><p>仕様:</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>内容</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>サイズ</p></td><td colspan=\"1\" rowspan=\"1\"><p>720×160px (max-width)、レスポンシブで自動縮小</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>カラー</p></td><td colspan=\"1\" rowspan=\"1\"><p>ブラックベース (#0d0d0d) + 白文字 (#f5f5f0)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>構成</p></td><td colspan=\"1\" rowspan=\"1\"><p>ミニ図解アイコン + 2行テキスト + CTAボタン</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>インタラクション</p></td><td colspan=\"1\" rowspan=\"1\"><p>ホバーで背景がスッとグレーに変化(動きはなし)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>設定箇所</p></td><td colspan=\"1\" rowspan=\"1\"><p><code>href=\"https://YOUR-USERNAME.github.io/YOUR-REPO/\"</code> の1か所を実URLに書き換え</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>STUDIOへの設置</p></td><td colspan=\"1\" rowspan=\"1\"><p>「埋め込み」ブロックにHTML全体をコピペ</p></td></tr></tbody></table></div><p>最初の版ではホバー時に2px浮き上がる・CTAボタンが白黒反転・矢印が右にスライド、という動きを入れていたが、「マウスオーバーでグレーになるよう修正(動きはなし)」という指示で全廃した。上下paddingはコンテンツに合わせて自動で詰まる構造にした(aspect-ratio: 720/160を削除、padding 20pxに統一)。</p><p>ブラックベース+白文字+太めボーダー+ミニ図解アイコンで、明らかに「外部リンク・別バージョン」とわかる作りにしている。</p><p></p><h2 id=\"index_wzGQtT5f\">リポジトリ構造とURL設計</h2><p>GitHub Pagesで公開するために、<code>PixelsToMotors/interactive-articles</code> というPublicリポジトリを新設した。<code>PixelsToMotors/r2r2r-content</code> はPrivateで、Free planではPagesが使えない。Public用に新リポジトリを切り出すのが最もシンプルで、記事資産(Private)と公開コンテンツ(Public)を分離できるメリットもある。</p><p>リポジトリ構造:</p><pre><code>interactive-articles/ ← Public\n├── README.md\n├── index.html ← 記事一覧(JSで動的描画)\n├── articles.json ← 記事メタデータ\n└── setting-up-the-side-camera/\n └── index.html ← DAY192のインタラクティブ版</code></pre><p>URLは <code>https://pixelstomotors.github.io/interactive-articles/setting-up-the-side-camera/</code> という形になる。<code>setting-up-the-side-camera</code> はSTUDIO側の記事スラッグと揃えてあり、対応関係が一目で分かる。</p><p>新しい記事を追加する時は、新フォルダを切って中に <code>index.html</code> を置く。例えば次の記事(横カメラのキャリブレーション)なら <code>parallel-slide-kinematics/index.html</code> のような構造になる。</p><p>GitHub Pages有効化の手順は以下の通り。</p><pre><code>1. リポジトリの Settings → Pages\n2. Source: Deploy from a branch を選択\n3. Branch: main / (root) を選択\n4. Save\n5. 1〜2分待つと https://pixelstomotors.github.io/interactive-articles/ で公開</code></pre><p></p><h2 id=\"index_ZykWpOpz\">一覧ページとarticles.json</h2><p>最初の <code>index.html</code> は記事カードをHTMLに直書きする形だった。これだと記事が増えるたびに <code>index.html</code> を毎回書き換えなければならない。3記事目で疲れる構造だ。</p><p>そこで <code>articles.json</code> に記事メタデータを切り出して、<code>index.html</code> はJSで動的に描画する設計に変更した。</p><p><code>articles.json</code> の構造:</p><pre><code class=\"language-json\">{\n \"articles\": [\n {\n \"day\": 193,\n \"slug\": \"calibrate-side-camera\",\n \"date\": \"2026-05-10\",\n \"category\": \"デジタルツイン学習\",\n \"series\": \"mechArm 270 Pi 動的追従の限界を探る ─ Log #11\",\n \"title\": \"横カメラのキャリブレーション完了 ─ アームをキャリブ装置として使う\"\n },\n {\n \"day\": 192,\n \"slug\": \"setting-up-the-side-camera\",\n \"date\": \"2026-05-07\",\n \"category\": \"デジタルツイン学習\",\n \"series\": \"mechArm 270 Pi 動的追従の限界を探る ─ Log #10\",\n \"title\": \"視点を増やす日 ─ 設計判断と iPhone 2台体制の物理構築\"\n }\n ]\n}</code></pre><p><code>index.html</code> 側はfetchで <code>articles.json</code> を読み、day番号の降順で自動ソートしてカードを描画する。一覧上には件数カウンタも入っていて、「01 / 全1件」のように自動更新される。</p><p>これで、新記事を追加する時の作業は以下の3操作だけになる。</p><pre><code>1. interactive-articles リポジトリのトップで Add file → Upload files\n → 新フォルダ(slug)/index.html をアップロード\n\n2. articles.json を開いて鉛筆アイコンで編集\n → 配列の先頭に新記事のメタデータブロックを1つ追加\n\n3. STUDIO通常記事に P2M_Interactive_Banner を埋め込み\n → href を新URLに書き換えるだけ</code></pre><p><code>index.html</code> 自体は最初に作った後は一切触らない。記事が増えても触らない。</p><p></p><h2 id=\"index_fxRVIJXN\">一覧ページの情報設計</h2><p>一覧ページは最初、記事カードだけを並べた最小構成だった。これに対して「Pixels to Motorsのページとわかるように最低限の情報は追加してください」という指示で、以下を補強した。</p><pre><code>1. Heroセクションに小見出しタグ\n ┌──────────────────────────────────────┐\n │ Pixels to Motors — デジタルツインが生み出す次世代ロボティクスUX │\n └──────────────────────────────────────┘\n \n ▶ INTERACTIVE EDITION ▶ 全要素図解版\n \n INTERACTIVE ARTICLES ← 巨大に(40〜72px)\n 記事の判断・設計・データを...\n\n2. Aboutセクション(記事リストの下)\n About / 事業所\n ─────────────\n 29年の UI/UX デザイン経験を | Designer 島津 栄一\n ロボティクスへ。R2R2R プロセスで、 | Office ADAPT DESIGN\n iPhone での 3D スキャンから物理 | Location 秋田県男鹿市\n シミュレーション、実機制御までを | Founded 2021年10月\n 1人で担う実験記録。 |</code></pre><p>これで一覧ページ単体で訪問されても「Pixels to Motors プロジェクトの公式インタラクティブアーカイブ」として完結する。ハコブくんチャットやプロトタイプセクションは含めず、必要な3要素(プロジェクト名 / 記事一覧 / 制作者情報)に絞っている。</p><p>トップバーには <code>Pixels to Motors</code> ロゴ(→ adaptdesign.jp/pixelstomotors)と「← 通常記事一覧」(→ adaptdesign.jp/pixelstomotors)を配置。各記事の右上には <code>DAY 192</code> タグ(→ adaptdesign.jp/pixelstomotors/record/{slug})を置いた。これで以下の往復が成立する。</p><pre><code>STUDIO通常記事 (adaptdesign.jp/pixelstomotors/record/setting-up-the-side-camera)\n ↓\n │ [この記事をインタラクティブ版で読む →] ← バナークリック\n ↓\nGitHub Pages インタラクティブ版 (pixelstomotors.github.io/interactive-articles/setting-up-the-side-camera/)\n ↓\n │ 右上「DAY 192」クリック → STUDIO記事に戻る\n ↓\nSTUDIO記事ページ</code></pre><h2 id=\"index_zJtKONJk\">完成した構成</h2><p>最終的に整った構成はこうなった。</p><pre><code>[STUDIO通常記事] ⇄ [GitHub Pages インタラクティブ版]\n ↑ バナーで誘導 ↑ DAY番号タグでSTUDIOに戻る\n ↑ ブランドロゴで一覧に戻る\n\n[インタラクティブ版一覧]\n └ articles.json を編集するだけで自動更新</code></pre><p>プロジェクトに登録する3点:</p><ul><li><p><code>PROJECT_INSTRUCTIONS.md</code>(カスタム指示)</p></li><li><p><code>TEMPLATE.html</code>(ナレッジ)</p></li><li><p><code>COMPONENTS.md</code>(ナレッジ)</p></li></ul><p>GitHub <code>interactive-articles</code> リポジトリ:</p><ul><li><p><code>index.html</code>(動的描画、編集不要)</p></li><li><p><code>articles.json</code>(記事追加時に編集)</p></li><li><p>各記事フォルダ + <code>index.html</code></p></li></ul><p></p><h2 id=\"index__P1SsnDG\">このセッションが示したこと</h2><p>最初の問いは「mdをHTML化したい」というシンプルなものだった。そこから最終的に建てたのは、生成パイプライン(Claude Project)と公開インフラ(GitHub Pages + STUDIO)と運用フロー(3ステップ)を含む、独立した一つの公開システムだった。</p><p>転換点は2回ある。</p><p>1回目は「テンプレ方式の廃止」。固定テンプレに記事の論点を流し込むのではなく、記事の論点を見てから視覚要素を組み立てる、という順序の選択。デザインシステムとしては「部品は最小限・原則は固定・構造は自由」という非典型な設計になった。</p><p>2回目は「全テキスト段落の廃止」。「全部図解で」という指示が、インタラクティブ版とSTUDIO版の役割分担を初めて明確にした。同じ情報を別の体験として2回提供する、という二層構造の輪郭が、ここで決まった。</p><p>公開インフラとして見ると、STUDIO(編集ラク・有料・JS不可)とGitHub Pages(無料・JS可・git push必要)はトレードオフが完全に逆向きで、どちらかを選ぶというより、両方をそれぞれの役割で使う方が合理的だった。バナーとロゴで相互リンクすると、訪問者から見れば「同じプロジェクトの2つの読み方」になる。</p><p><code>articles.json</code> を分離した判断も後から効いてくる。<code>index.html</code> を毎回書き換える運用は3記事目で破綻するが、JSON1ブロック追加なら100記事目でも持つ。Build-in-publicの長期プロジェクトでは、初期の運用設計が10ヶ月後の負債になりやすい。早めに動的化しておくと、未来の自分が助かる。</p><p>今後の新記事は、<code>DAY{N}_article.md</code> をプロジェクトに添付するだけで <code>DAY{N}_visual_article.html</code> と <code>articles.json</code> 追記ブロックが揃う。後はGitHub Web UIで2操作、STUDIO記事にバナー1枚埋め込み、で完結する。記事を書く側の工数は、STUDIOにmdを貼ってから3〜5分で公開まで進む計算になる。</p><p>このセッションは、Build-in-publicの公開層を「STUDIOだけ」から「STUDIO + インタラクティブ版」に拡張した日として残しておく。</p>", "toc": [{"id": "index_dE_wyetb", "text": "「mdをそのままHTMLで表示したい」という最初の問い"}, {"id": "index_MHLhPdbw", "text": "テンプレ方式の廃案"}, {"id": "index_4SGpI1mu", "text": "「全部図解で作って」への振り切り"}, {"id": "index_JSlUtY9g", "text": "公開先の選択:STUDIOとGitHub Pages"}, {"id": "index_hBKLtniT", "text": "Claude Projectで生成システム化"}, {"id": "index_yuCd7qg8", "text": "STUDIOとGitHub Pagesをバナーで往復させる"}, {"id": "index_wzGQtT5f", "text": "リポジトリ構造とURL設計"}, {"id": "index_ZykWpOpz", "text": "一覧ページとarticles.json"}, {"id": "index_fxRVIJXN", "text": "一覧ページの情報設計"}, {"id": "index_zJtKONJk", "text": "完成した構成"}, {"id": "index__P1SsnDG", "text": "このセッションが示したこと"}]}
\ No newline at end of file
diff --git a/src/data/records/youtube-channel-identity.json b/src/data/records/youtube-channel-identity.json
index 12cf8bb..a746ce6 100644
--- a/src/data/records/youtube-channel-identity.json
+++ b/src/data/records/youtube-channel-identity.json
@@ -1 +1 @@
-{"day": "LOG332", "series": "ロボティクスデザイン", "published": "2026-10-12T00:28:45.000Z", "title": "YouTubeチャンネルの顔を作る ― チャンネル名・アイコン・バナーをハコブくんにそろえるまで", "slug": "youtube-channel-identity", "cover": null, "thumb": "/thumbs/r-youtube-channel-identity.html", "lang": "JP", "description": "解説動画を出す前に、YouTubeチャンネルを運用用の新アカウントへ引き継ぎ、説明文・アイコン・バナーをナレーション役のハコブくんにそろえた。バナーは5回作り直し、計18案から決めた。", "updated": "2026-10-12T00:28:42.000Z", "body": "<!--TOC--><p></p><p>Pixels to Motors の YouTube チャンネルで解説動画を出す準備として、チャンネルの見た目と運用の仕組みを Claude と対話しながら整えました。解説動画のナレーションは、病院で薬を運ぶロボットのキャラクター「ハコブくん」が担当すると決めています(LOG331)。そこで、チャンネルを開いた人が最初に目にするアイコンとバナーも、ハコブくんにそろえることにしました。</p><p>やったことは大きく4つです。YouTube を運用するための Google アカウントを新しく作り、既存のチャンネルを引き継ぐ。チャンネルの説明文を書き直す。バナーを作る。アイコンをハコブくんにする。バナーは文字入りの3案から始めて、5回作り直し、計18案を見たうえで決めました。</p><p>本記事は、その手順と、途中で方針が変わった場面の記録です。アカウントの引き継ぎで Claude が誤った案内をした場面も残しています。バナーとアイコンを作った Python スクリプトは、記事の後半に全文を載せています。メールアドレスと Google Workspace の料金の具体的な金額は伏せました。</p><p></p><figure><img src=\"/media/s-2704x1546_v-frms_webp_69baf643-e22a-4e3e-9067-63e9453efb8b.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index__Ynzo3Cp\">YouTube用のアカウントは、Google Workspaceのエイリアスではなく新しいユーザーにした</h2><p>Pixels to Motors のメールは、ADAPT DESIGN の独自ドメインで Google Workspace(Google のメールやドライブを会社のドメインで使うサービス)を契約して運用しています。ここに Pixels to Motors 用のアドレスを足す方法は2つありました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>方法</p></th><th colspan=\"1\" rowspan=\"1\"><p>できること</p></th><th colspan=\"1\" rowspan=\"1\"><p>できないこと</p></th><th colspan=\"1\" rowspan=\"1\"><p>費用</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>エイリアス(既存のユーザーに別名のアドレスを付ける)</p></td><td colspan=\"1\" rowspan=\"1\"><p>メールの受信。設定すれば、そのアドレス名義での送信</p></td><td colspan=\"1\" rowspan=\"1\"><p>そのアドレスで Google にログインすること</p></td><td colspan=\"1\" rowspan=\"1\"><p>かからない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>新しいユーザー</p></td><td colspan=\"1\" rowspan=\"1\"><p>専用のログイン、ドライブ、YouTube チャンネル、外部サービスのアカウントを完全に分けること</p></td><td colspan=\"1\" rowspan=\"1\"><p>―</p></td><td colspan=\"1\" rowspan=\"1\"><p>1ユーザー分の月額が増える</p></td></tr></tbody></table></div><p>YouTube チャンネルを、普段使いのアカウントから切り離して運用したかったので、新しいユーザーにしました。契約が年間プランのため、途中で足したユーザーは更新時期まで減らせない前提で決めています。</p><p></p><h2 id=\"index_8rfkxb9m\">管理コンソールには、普段使いのアカウントでは入れなかった</h2><p>ユーザーを足す画面は、Google Workspace の管理コンソール(admin.google.com)にあります。最初に普段使いのアカウントでログインしたところ、「管理者アカウントでログインしてください」と表示されて入れませんでした。</p><p>ドメインには、普段使いのアカウントともう1つ、合わせて2つのユーザーがありました。管理者の権限を持っていたのは、もう1つのほうでした。そちらで入り直すと、Chrome が「このプロフィールで作業を続けるか、仕事用のプロフィールを新しく作成するか」を聞いてきます。以前、Chrome のプロフィールが10個以上重複して増えたことがあったため、どちらを選ぶかは「そのプロフィールを何に使っているか」で決めました。仕事用のプロフィールに切り替わったあとは、管理コンソールが自動では開き直らなかったため、アドレスを入れて開き直しています。</p><p></p><h2 id=\"index_SZwLHwTg\">「このユーザーは既に存在しています」の原因は、3月に付けたエイリアスだった</h2><p>「新しいユーザーの追加」で、作りたいアドレスを入れると、次の赤字が出て作成できませんでした。</p><blockquote><p>すでにユーザーの予備のメールアドレス(メール エイリアス)として使用されています</p></blockquote><p>2026年3月、同じアドレスをエイリアスとして管理者アカウントに付けていました(当時はユーザーを増やさず、無料のエイリアスで済ませる判断をしていた)。エイリアスとして使っているアドレスと同じ名前のユーザーは作れません。</p><p>管理者アカウントの詳細画面から「メールエイリアス」を開き、そのエイリアスを削除して保存しました。そのあと「新しいユーザーの追加」をやり直すと、ユーザーが作成されました。名前は姓「Pixels to」、名「Motors」で入れています。この入れ方だと、YouTube の画面によっては「Motors Pixels to」と、姓と名が逆の順に表示されることが後で分かりました。</p><p></p><h2 id=\"index_EnRPO5Pq\">作ろうとしたチャンネルのハンドルは、自分の既存チャンネルが使っていた</h2><p>新しいアカウントで YouTube を開き、チャンネルを作ろうとすると、ハンドル(チャンネルの @ から始まる固有の名前)の欄に次のように出ました。</p><blockquote><p>このハンドルは使用できません。 こちらは使用可能です: @PixelstoMotors-r2y</p></blockquote><p>@PixelstoMotors を使っていたのは、2026年3月21日に管理者アカウントで作った、自分の既存チャンネルでした。登録者1人、動画1本(mechArm 270-Pi の開封動画)がすでにあります。</p><p>新しいチャンネルを別に作ると、動画と登録者が既存チャンネルに残ったまま、ハンドルも別のものになります。そこで新しいチャンネルは作らず、既存チャンネルを新しいアカウントから操作できるようにする方針に切り替えました。</p><p></p><h2 id=\"index_04xwKQv5\">既存チャンネルは、新しいアカウントを「管理者」として招待して引き継いだ</h2><p>YouTube Studio の「設定」→「権限」で、新しいアカウントのアドレスを招待しました。選べる役割は次のとおりで、「オーナー」は一覧にありませんでした。YouTube のヘルプにも、オーナーの役割は他のユーザーに移せないと書かれています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>役割</p></th><th colspan=\"1\" rowspan=\"1\"><p>できること</p></th><th colspan=\"1\" rowspan=\"1\"><p>できないこと</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>オーナー</p></td><td colspan=\"1\" rowspan=\"1\"><p>すべての操作。チャンネルの削除、権限の管理</p></td><td colspan=\"1\" rowspan=\"1\"><p>オーナーの役割を他のユーザーに移すこと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>管理者</p></td><td colspan=\"1\" rowspan=\"1\"><p>全データの閲覧、権限の管理、チャンネル情報の編集、動画の作成・アップロード・公開・削除</p></td><td colspan=\"1\" rowspan=\"1\"><p>チャンネルの削除</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>編集者</p></td><td colspan=\"1\" rowspan=\"1\"><p>全データの閲覧、編集、アップロード・公開</p></td><td colspan=\"1\" rowspan=\"1\"><p>公開済みの動画の削除、権限の管理</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>字幕編集者</p></td><td colspan=\"1\" rowspan=\"1\"><p>字幕の追加・編集・削除</p></td><td colspan=\"1\" rowspan=\"1\"><p>字幕以外の編集</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>閲覧者</p></td><td colspan=\"1\" rowspan=\"1\"><p>全データの閲覧</p></td><td colspan=\"1\" rowspan=\"1\"><p>編集</p></td></tr></tbody></table></div><p>新しいアカウントには「管理者」を付けました。動画の投稿から分析、説明文やバナーの変更まで、運用に必要なことはすべてできます。</p><h3 id=\"index_YvFm6X9v\">招待の承諾には、受け取る側にもチャンネルが必要だった</h3><p>招待メールを新しいアカウントで開いて進むと、「チャンネルのプロフィール」という、新しいチャンネルを作る画面が出ました。Claude はこれを「余計な空のチャンネルができてしまう画面」と判断し、キャンセルするよう案内しました。</p><p>キャンセルしたあと Studio の権限の画面を見ると、新しいアカウントは「承認保留中」のままでした。招待メールを開き直すと、手順は次の2段になっていました。</p><blockquote><p>ステップ 1. Pixels to Motors にアクセスするには YouTube チャンネルが必要です。 ステップ 2. チャンネルの作成後、この招待を承諾すると Pixels to Motors にアクセスできるようになります。</p></blockquote><p>キャンセルを勧めた画面は、承諾の途中の「ステップ1」でした。招待を受ける側のアカウントにも自分のチャンネルが必要、という YouTube の仕様を Claude が知らずに案内した誤りです。新しいアカウント側に空のチャンネルを作ってから「招待を承諾」を押すと、承諾済みになりました。空のチャンネルは使わずに置いておきます。</p><p>承諾したあとは、Studio の右上で「チャンネルを切り替え」から Pixels to Motors を選ぶと、「あなたは管理者です」と表示された状態で管理画面に入れました。</p><h3 id=\"index_7woCpmaH\">管理者で入っていても、YouTube本体での名義は空のチャンネルになる</h3><p>管理者の権限で使えるのは、Studio の中の操作です。YouTube 本体の画面(動画の視聴ページなど)に移ると、操作の名義は新しいアカウント自身の空のチャンネルになります。他の人の動画に Pixels to Motors の名前でコメントしたり、チャンネル登録したりはできません。</p><p>これも必要になったら、チャンネルの所有の形そのものを変える必要があります。どう変えるのが正しいかは、今回は確かめていません。</p><p></p><h2 id=\"index_6tAUffjk\">チャンネルの説明文は、フィジカルAIを先頭に置いて書き直した</h2><p>書き直す前の説明文は、次のとおりでした。</p><pre><code>UX × AI × Robotics の交差点に存在する、\nまだ誰も名前をつけていない領域「Motion UX」を探求しています。\n\niPhoneの3Dスキャン → Unityシミュレーション → 実機ロボットアーム\nという低コストなパイプラインを一人で構築中。\n\nすべてのプロセスをオープンに記録しています。\n\n// ADAPT DESIGN — Pixels to Motors\n// adaptdesign.jp/pixelstomotors</code></pre><p>iPhone → Unity → 実機というパイプラインは、いまの開発の中心ではなくなっています。冒頭の2〜3行は、検索結果やチャンネルページで最初に見える部分です。そこで、機材の名前ではなく、何のチャンネルかが伝わる文にしました。方向の違う3案(フィジカルAI・実装寄り/UX・体験寄り/個人・低コスト寄り)から、フィジカルAI寄りの案を選び、機材名の行は削りました。英語の段落は、日本語と同じ内容にそろえています。</p><pre><code>フィジカルAIを、現場で使える形にするまでを記録するチャンネルです。\nAIが考え、ロボットが動く。その間にある設計・調整・失敗を、\n小さなロボットアームで一つずつ確かめています。\n\n精度より、まず体験が成立すること。\n精度はあとからお金で解決できる。\n\nDocumenting the journey of making Physical AI work in real-world settings.\nBetween AI thinking and robots moving lies design, tuning, and failure.\nI test each step with a small robot arm.\n\nExperience first. Precision can be bought later.\n\n// ADAPT DESIGN — Pixels to Motors\n// adaptdesign.jp/pixelstomotors</code></pre><p>Studio の「カスタマイズ」→「プロフィール」→「説明」に貼り付けて公開しました。公開ボタンが押せない状態(灰色)になっていれば保存済みです。ただ、チャンネルページの表示は、記事を書いている時点でもまだ古い説明文のままでした。</p><p></p><h2 id=\"index_GNMYvKmk\">バナーは、どの端末でも切れない中央の範囲を基準に作った</h2><p>YouTube のヘルプでは、バナー画像は次のように決まっています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>値</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>最小サイズ</p></td><td colspan=\"1\" rowspan=\"1\"><p>2048×1152px(16:9)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>テキストやロゴが切れずに表示される範囲</p></td><td colspan=\"1\" rowspan=\"1\"><p>中央の 1235×338px</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>推奨サイズ(テレビでも見やすい)</p></td><td colspan=\"1\" rowspan=\"1\"><p>2560×1440px</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ファイルサイズ</p></td><td colspan=\"1\" rowspan=\"1\"><p>6MB 以下</p></td></tr></tbody></table></div><p>同じ 16:9 の画像が、テレビでは全体、パソコンでは横長の帯、スマホではさらに左右を切った中央だけで表示されます。そのため、主役になる絵は中央の 1235×338px(2560×1440px の画像なら、同じ比率で 1546×423px)に収めました。作った案は、パソコンでの見え方(横長の帯)と、全体図に安全範囲の枠を重ねた図の2つで確認しています。</p><div class=\"embed\"><iframe src=\"/embeds/youtube-channel-identity-MHKfODFx.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p></p><h2 id=\"index_BpOCllXq\">バナーの1回目は、実機の写真に文字を入れた3案だった</h2><p>最初は、手元にある素材から作りました。サイトの画像はこちらの作業環境から取得できなかったため、Mac の r2r2r-content フォルダにあった画像認識シリーズの写真を使っています。</p><ul><li><p>A:mechArm 270-Pi のグリッパーと赤・青のキューブを真上から撮った写真に、「フィジカルAIを、現場で使える形に。」の文字</p></li><li><p>B:アームの根元側をドット状にぼかし、先端に向かってくっきりさせた「Pixels to Motors」の絵</p></li><li><p>C:明るい背景に、キューブの写真・色で切り出したマスク・グリッパーの3コマを並べた「見る → 判断する → 動かす」</p></li></ul><p></p><figure><img src=\"/media/s-1280x636_v-fms_webp_406c9cd8-a337-4559-9ae0-f3c27acf5acd.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>これに対しての返事は次のとおりでした。</p><blockquote><p>テキスト情報は必要なくイメージだけでいいですよ。同じアームだけを使ったバナーではなく、もっといろんなパターンが考えられるはずです。最初はもっと違うパターンを見せて欲しい(今回のは1種類カウントです)</p></blockquote><p>3案とも同じアームの写真が主役で、見せ方の幅が狭かったということです。</p><p></p><h2 id=\"index_KWVQksqt\">2回目は、サイトの画像から6つの方向を出した</h2><p>サイトのトップページを画面ごとPDFに保存したものを受け取り、そこから記事のサムネイル、4コマ漫画、シリーズの扉絵などを切り出しました。文字は入れず、方向がはっきり違う6案にしています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>案</p></th><th colspan=\"1\" rowspan=\"1\"><p>中身</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>アーカイブの壁</p></td><td colspan=\"1\" rowspan=\"1\"><p>記事のサムネイルと4コマを3段に並べる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>白黒スプリット</p></td><td colspan=\"1\" rowspan=\"1\"><p>左が黒、右がクリーム。境目でピクセルが崩れ、斜線の円と点線のグラフを重ねる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>シミュレーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>Unity で並べたたくさんのアームを網点にする</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ハコブくん 4コマ</p></td><td colspan=\"1\" rowspan=\"1\"><p>4コマ漫画を横一列に並べ、両側に集中線</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ドットマトリクス</p></td><td colspan=\"1\" rowspan=\"1\"><p>「フィジカルAIが社会実装される前に解くべき課題」の丸を全面に広げる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ハコブくん 写真</p></td><td colspan=\"1\" rowspan=\"1\"><p>病院の廊下にいるハコブくんの写真</p></td></tr></tbody></table></div><p></p><figure><img src=\"/media/s-1717x1722_v-fms_webp_c6c4e138-f06b-41a1-bfec-053d0dfc7356.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>このあと、YouTube に合わせて全案を 2048×1152px でも書き出しました。</p><p></p><h2 id=\"index_wfU0AzRw\">アイコンは、動画で話しているハコブくんの1コマから作った</h2><p>チャンネル名とアイコンを、YouTube で覚えてもらいやすいものに変えることになりました。アイコンはハコブくんにする前提です。</p><p>最初に Claude が出したのは、ハコブくんの顔(黒いディスプレイ、白いカプセル形の目、水色の口)をコードで描き起こした3案でした。顔のアップ、オレンジの背景に頭と胴、いまのアイコンの市松模様を引き継いだもの、の3つです。</p><p></p><figure><img src=\"/media/s-1500x940_v-fms_webp_c3582154-8fcc-4086-b283-c9ee9792e885.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>これには、試作動画の画面を書き出した MP4 が返ってきました。</p><blockquote><p>アイコンはこのハコブくん使えばいいんじゃないの?</p></blockquote><p>動画で話しているハコブくんと、アイコンのハコブくんが同じ見た目なら、アイコンから動画を開いた人に同じキャラクターだと伝わります。コードで描いた顔は、仕様には合っていても、動画のハコブくんとは別物でした。</p><p>動画(1920×1080px)を1秒に3コマずつ書き出して、表情を見比べ、口を閉じて笑っているコマを選びました。ハコブくんは画面の左下にいて、大きさは横幅が約320pxです。画面の右側にある出典の文字などが入らないよう、ハコブくんの範囲の外は背景色で塗りつぶしてから切り抜いています。案は3つです。</p><ul><li><p>A:動画のまま。上半身をクリーム地に置く</p></li><li><p>B:A にオレンジの輪を付ける。ダークモードでもライトモードでも輪郭が見える</p></li><li><p>C:顔に寄せる</p></li></ul><p></p><figure><img src=\"/media/s-1500x940_v-fms_webp_27d8616b-248f-424b-bbc8-265007ee2c33.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>各案は、YouTube の表示サイズ(200px・98px・48px・24px)で、ダークモードとライトモードの背景に並べて確認しました。チャンネルには A を設定しています。元が動画の1コマなので、800×800px にするために約2倍に拡大しています。</p><p></p><h2 id=\"index_BZlJ8lML\">バナーの3回目は、ハコブくんだけで5つのタッチを作った</h2><p>次の依頼は、ハコブくんだけを使って、タッチの違う5種類を作ることでした。</p><blockquote><p>イラスト系、4コマまんが風、写実的、写真系、グラフィックデザインの5種類</p></blockquote><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>タッチ</p></th><th colspan=\"1\" rowspan=\"1\"><p>作り方</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>イラスト系</p></td><td colspan=\"1\" rowspan=\"1\"><p>病院の廊下とハコブくんを、線のないフラットな絵としてコードで描く</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4コマまんが風</p></td><td colspan=\"1\" rowspan=\"1\"><p>動画のハコブくんを白黒のトーン調に変え、表情の違う4コマを文字なしで並べる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>写実的</p></td><td colspan=\"1\" rowspan=\"1\"><p>動画の3Dのハコブくんを、表情を変えて5体並べる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>写真系</p></td><td colspan=\"1\" rowspan=\"1\"><p>病院の廊下にいるハコブくんの写真</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>グラフィックデザイン</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの顔のディスプレイを大きく置き、ピクセルの崩れ・寸法線・緑のリングを添える</p></td></tr></tbody></table></div><p></p><figure><img src=\"/media/s-1360x1165_v-fms_webp_6153354f-d317-4c3b-9562-e9b084372e61.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>4コマまんが風は、動画のコマを次の手順で白黒にしています。輪郭を検出して黒い線にする。暗い部分(明るさ70未満)は黒で塗る。中間の明るさ(215未満)は、7px 間隔の網点にする。それ以外は白にする。</p><p>返事は次のとおりでした。</p><blockquote><p>イラスト系か4コママンガ風がいいけど、イラストはハコブくんじゃないし、4コママンガ風はハコブくんだけしか描かれてない。</p></blockquote><p>イラスト系のハコブくんは、Claude が写真を見ながら比率を推測して描いたもので、実物と形が違いました。4コマまんが風は、4コマとも同じハコブくんで、漫画に出てくる他の登場人物がいませんでした。</p><h3 id=\"index_elH2fOh3\">ハコブくんの三面図を受け取り、寸法を読み取った</h3><p>ここで、ハコブくんの三面図(正面・側面・背面を漫画のタッチで描いたもの)を受け取りました。</p><p></p><figure><img src=\"/media/s-1080x1350_v-fms_webp_b8890bd5-e649-4af1-9361-84759664f745.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>画像に座標の目盛りを重ねて、正面図の各部の位置を読み取りました(単位は三面図の画像上の px)。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>部分</p></th><th colspan=\"1\" rowspan=\"1\"><p>位置</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>全体</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 70〜331、縦 415〜874(高さ約460)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>頭の外装</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 100〜300、上端 413</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ディスプレイ</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 120〜283、縦 427〜541</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>目</p></td><td colspan=\"1\" rowspan=\"1\"><p>中心が横 152 と 241、縦 486。幅 22、高さ 40</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>口</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 184〜215、縦 490〜505</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>胴</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 76〜324、縦 505〜816</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>引き出しのスリット</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 167〜225、縦 606〜630</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>黒い帯</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 78〜322、縦 767〜786</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>台座</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 72〜328、縦 806〜848</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>車輪</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 95〜120 と 280〜305、縦 836〜874</p></td></tr></tbody></table></div><p>側面図のリングは、中心が(554, 663)、内側の線が半径約51、外側の線が半径約62でした。リングの位置は、中心の候補を動かしながら半径ごとの平均の明るさを測り、暗い線が2本くっきり出る点を探して決めています。</p><h3 id=\"index_nkmV04Yr\">4回目は、三面図そのものと、他の登場人物を使った</h3><p>三面図のハコブくんを使って、イラスト系と4コマまんが風を2案ずつ作りました。</p><ul><li><p>イラスト系 A:方眼の上に正面・背面・側面を並べ、寸法線と、顔とリングの拡大図を添える</p></li><li><p>イラスト系 B:線画に淡い色を付けた廊下を、三面図の側面のハコブくんが走る</p></li><li><p>4コマまんが風 A:斜めのコマ割りで、中央にハコブくん、左右にツカムくん(顔の付いたアーム)、エンジニア、看護師</p></li><li><p>4コマまんが風 B:ハコブくんを中央の爆発形の枠に置き、他の登場人物を丸い枠で並べる</p></li></ul><p></p><figure><img src=\"/media/s-1360x936_v-fms_webp_2febf659-d311-4869-ace6-89ed02c61b14.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>返事は次のとおりでした。</p><blockquote><p>イラストは背景のイラストタッチにハコブくんを修正しないとダメだよ(イラストと漫画風を一緒にしたらNG)</p><p>4コマまんがの処理は前の方が良かった。このタッチのまま、登場するキャラを入れ替えればいいだけです。</p></blockquote><p>イラスト系 B は、背景は線画に色を付けたイラストなのに、ハコブくんだけ漫画のトーンのままでした。1枚の絵の中で、タッチが2種類混ざっていたことになります。4コマまんが風は、コマ割りを変えたことで、3回目で良かった処理(輪郭線と網点)が失われていました。</p><p></p><h2 id=\"index_aoA9mNR9\">5回目で、ハコブくんを背景と同じタッチで描き直した</h2><p>イラスト系は、三面図から読み取った寸法を使い、ハコブくんを背景と同じタッチで描き直しました。三面図の座標をそのまま渡すと、バナーの上の位置と大きさに変換して描く仕組みです(スクリプトの <code>Pen</code> クラス)。線のない背景には線のないハコブくん、黒い輪郭線の背景には同じ太さの輪郭線のハコブくん、と2つのタッチで作りました。</p><p>4コマまんが風は、3回目の処理と並び(4コマを横一列・両側に集中線)に戻し、コマごとの登場人物を入れ替えました。左からハコブくん、看護師(「!」)、エンジニア(汗)、ツカムくん(キラキラ)です。看護師たちは元が線画なので、3回目の処理(輪郭検出)をそのままかけると全体が黒くつぶれました。線画には輪郭検出をかけず、明るさだけで黒・網点・白に分ける処理にしています。網点の間隔は、ハコブくんのコマと同じ 7px にそろえました。</p><p></p><figure><img src=\"/media/s-1360x707_v-fms_webp_3a2d5396-3c1f-4313-b446-6381c5f222fe.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>決まったのは、線のないフラットなイラストでした。</p><blockquote><p>イラスト系、ハコブくんは1体だけでいいよ。タッチはこれがいいかな。</p></blockquote><p>横向きのハコブくんと動きの線を消し、正面のハコブくんだけをドアとドアの間に立たせて完成にしました。ハコブくんは、顔から足元まで、どの端末でも切れない範囲に収まっています。</p><p></p><figure><img src=\"/media/s-2048x1152_v-frms_webp_9fd7c7eb-5d31-479a-bef5-522b73fab189.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>1回目から5回目までに出したバナーは次のとおりです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>回</p></th><th colspan=\"1\" rowspan=\"1\"><p>案の数</p></th><th colspan=\"1\" rowspan=\"1\"><p>主な中身</p></th><th colspan=\"1\" rowspan=\"1\"><p>返ってきた判断</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>実機の写真+文字</p></td><td colspan=\"1\" rowspan=\"1\"><p>文字は要らない。同じアームだけで幅が狭い</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>7</p></td><td colspan=\"1\" rowspan=\"1\"><p>サイトの画像から6方向+1回目の写真(文字なし)</p></td><td colspan=\"1\" rowspan=\"1\"><p>方向の幅を確認</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんだけで5つのタッチ</p></td><td colspan=\"1\" rowspan=\"1\"><p>イラストと4コマが良い。ただし形が違う/他の人物がいない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>三面図と他の登場人物</p></td><td colspan=\"1\" rowspan=\"1\"><p>タッチを混ぜない。4コマは3回目の処理に戻す</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>3+1</p></td><td colspan=\"1\" rowspan=\"1\"><p>タッチをそろえて描き直し</p></td><td colspan=\"1\" rowspan=\"1\"><p>フラットのイラスト、ハコブくん1体で決定</p></td></tr></tbody></table></div><p></p><h2 id=\"index_8PXH6omo\">チャンネル名は「フィジカルAI解説|Pixels to Motors」を候補にした</h2><p>チャンネル名は、ブランド名の Pixels to Motors を残したまま、何のチャンネルかが分かる言葉を足す案にしました。Pixels to Motors のルールで、外に出すブランド名は Pixels to Motors だけと決めているためです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>案</p></th><th colspan=\"1\" rowspan=\"1\"><p>名前</p></th><th colspan=\"1\" rowspan=\"1\"><p>良い点</p></th><th colspan=\"1\" rowspan=\"1\"><p>気になる点</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>Pixels to Motors|フィジカルAI解説</p></td><td colspan=\"1\" rowspan=\"1\"><p>ブランド名が先に来る</p></td><td colspan=\"1\" rowspan=\"1\"><p>少し長い</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィジカルAI解説|Pixels to Motors</p></td><td colspan=\"1\" rowspan=\"1\"><p>日本語が先に来て、内容が一番早く伝わる</p></td><td colspan=\"1\" rowspan=\"1\"><p>幅が狭い画面ではブランド名が切れやすい</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんのフィジカルAI解説</p></td><td colspan=\"1\" rowspan=\"1\"><p>覚えやすく、アイコンと一体になる</p></td><td colspan=\"1\" rowspan=\"1\"><p>ブランド名と、動画を作っている本人が見えなくなる</p></td></tr></tbody></table></div><p></p><figure><img src=\"/media/s-1500x690_v-fms_webp_29ae9ada-591e-4c09-92a0-8b1d5224d689.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>選ばれたのは2の並びでした。そのうえで、次の質問がありました。</p><blockquote><p>ロボティクスとフィジカルAIでどちらがタイトルの言葉として使うのがいい?</p></blockquote><p>Claude の答えは「フィジカルAI」です。理由は3つです。動画を届けたい相手(ロボットに関心のある経営者・PM・マネージャー)が、ニュースや業界の発表で目にしている言葉であること。動画の中身が「AIがロボットを動かす」話で、ロボティクス全般より範囲がはっきりすること。サイトの課題地図や研究室のシリーズ名でも「フィジカルAI」を使っていること。流行りの言葉なので数年後に古く聞こえるおそれはありますが、チャンネル名は後から変えられ、ハンドル(@PixelstoMotors)は変わりません。</p><p>チャンネル名の変更は、記事を書いている時点ではまだ行っていません。</p><p></p><h2 id=\"index_mtwyUJht\">チャンネル名の変更と、アイコン・4コマの作り直しが残っている</h2><ul><li><p>チャンネル名を「フィジカルAI解説|Pixels to Motors」に変える</p></li><li><p>新しい説明文がチャンネルページに反映されたかを確かめる</p></li><li><p>新しいアカウントに2段階認証を設定する</p></li><li><p>アイコンは動画の1コマを約2倍に拡大している。元の切り抜き画像(動画の制作ベースのフォルダにある)から作り直すと、もっと鮮明になる</p></li><li><p>4コマまんが風の看護師たちは、サイトの画面を保存した PDF から切り出した。採用するなら元の漫画画像で作り直す</p></li></ul><p>細かい修正は、動画をアップしながら進める予定です。</p><p></p><h2 id=\"index_LGOmRd2E\">制作スクリプトの全文</h2><p>バナーとアイコンは、すべて Python(Pillow と NumPy)で作りました。作業フォルダは <code>/home/claude/banner/</code> です。スクリプトは作った順に載せています。採用したバナーは make5_single.py、採用したアイコンは hakobu_icon.py の出力です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>ファイル</p></th><th colspan=\"1\" rowspan=\"1\"><p>作ったもの</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>1回目:実機の写真に文字を入れた3案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make2.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>2回目:サイトの画像から作った6案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>icons.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイコン:ハコブくんの顔をコードで描いた3案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>hakobu_icon.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイコン:動画の1コマから作った3案(採用)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make3.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>3回目:ハコブくんだけで5つのタッチ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make4.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>4回目:三面図と他の登場人物を使った4案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make5.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>5回目:タッチをそろえた3案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make5_single.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>決定版:フラットのイラスト、ハコブくん1体(採用)</p></td></tr></tbody></table></div><h3 id=\"index_LAOak7lY\">make.py(1回目の3案)</h3><pre><code class=\"language-python\">import numpy as np\nfrom PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageOps, ImageEnhance\n\nSRC = '/mnt/user-data/uploads/r2r2r-content/articles/'\nOUT = '/home/claude/banner/out/'\nW, H = 2560, 1440\nSAFE = (507, 508, 2053, 932) # x0,y0,x1,y1 (1546 x 423 visible on every device)\n\nCJK_B = '/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc'\nCJK_M = '/usr/share/fonts/opentype/noto/NotoSansCJK-Medium.ttc'\nINTER = '/usr/share/fonts/opentype/inter/'\n\ndef f_cjk(size, bold=True):\n return ImageFont.truetype(CJK_B if bold else CJK_M, size, index=0)\n\ndef f_inter(size, w='SemiBold'):\n return ImageFont.truetype(INTER + f'Inter-{w}.otf', size)\n\ndef tracked(draw, xy, text, font, fill, track=0):\n x, y = xy\n for ch in text:\n draw.text((x, y), ch, font=font, fill=fill)\n x += draw.textlength(ch, font=font) + track\n return x\n\ndef load(name):\n return ImageOps.exif_transpose(Image.open(SRC + name)).convert('RGB')\n\ndef place(canvas, img, scale, src_pt, dst_pt):\n \"\"\"paste img scaled so that src_pt (in img coords) lands on dst_pt\"\"\"\n im = img.resize((round(img.width * scale), round(img.height * scale)), Image.LANCZOS)\n ox = round(dst_pt[0] - src_pt[0] * scale)\n oy = round(dst_pt[1] - src_pt[1] * scale)\n layer = Image.new('RGB', (W, H))\n layer.paste(im, (ox, oy))\n return layer, (ox, oy, im.width, im.height)\n\ndef rect_mask(box, feather):\n m = Image.new('L', (W, H), 0)\n ImageDraw.Draw(m).rectangle(box, fill=255)\n return m.filter(ImageFilter.GaussianBlur(feather))\n\ndef hgrad(x0, x1, a0, a1):\n \"\"\"horizontal alpha ramp, a0 at x<=x0 to a1 at x>=x1\"\"\"\n xs = np.clip((np.arange(W) - x0) / max(1, x1 - x0), 0, 1)\n row = (a0 + (a1 - a0) * xs).astype(np.uint8)\n return Image.fromarray(np.tile(row, (H, 1)))\n\ndef pixel_squares(draw, x, y, s, gap, cols):\n for i, c in enumerate(cols):\n draw.rectangle((x + i * (s + gap), y, x + i * (s + gap) + s, y + s), fill=c)\n\nRED, BLUE = (214, 52, 52), (36, 128, 214)\nMAT = (30, 27, 31)\n\nphoto = load('IMG_7933.jpg') # top-down: mechArm on cutting mat, red & blue cubes\nMAT_BOX = (697, 639, 3211, 2471) # cutting-mat area in full-res coords\n\n# ---------------------------------------------------------------- A: GRASP\ndef banner_a():\n bg = Image.new('RGB', (W, H), MAT)\n subj = ((1282 + 2650) / 2, 1790) # midpoint between the two cubes, gripper level\n layer, _ = place(bg, photo, 0.55, subj, (1530, 728))\n # keep only the mat, fade its edges into the background\n s = 0.55\n ox = 1530 - subj[0] * s; oy = 728 - subj[1] * s\n mb = (ox + MAT_BOX[0] * s + 70, oy + MAT_BOX[1] * s + 70,\n ox + MAT_BOX[2] * s - 70, oy + MAT_BOX[3] * s - 70)\n m = rect_mask(mb, 60)\n layer = ImageEnhance.Brightness(layer).enhance(0.92)\n bg.paste(layer, (0, 0), m)\n # darken left for the type\n shade = Image.new('RGB', (W, H), MAT)\n bg.paste(shade, (0, 0), hgrad(900, 1080, 235, 0))\n d = ImageDraw.Draw(bg)\n x = SAFE[0] + 20\n pixel_squares(d, x, 586, 18, 8, [RED, BLUE, (235, 235, 235)])\n tracked(d, (x + 92, 580), 'PIXELS TO MOTORS', f_inter(28, 'SemiBold'), (190, 186, 194), track=6)\n d.text((x, 640), 'フィジカルAIを、', font=f_cjk(68), fill=(245, 245, 245))\n d.text((x, 728), '現場で使える形に。', font=f_cjk(68), fill=(245, 245, 245))\n d.text((x, 836), 'Making Physical AI work in the real world.', font=f_inter(26, 'Regular'), fill=(160, 156, 166))\n return bg\n\n# ---------------------------------------------------------------- B: PIXELS → MOTORS (dissolve)\ndef banner_b():\n rot = photo.rotate(90, expand=True) # CCW: robot base on the left, gripper reaching right\n # mapping for CCW 90 with expand: (x, y) -> (y, W0 - x)\n W0 = photo.width\n def r(pt): return (pt[1], W0 - pt[0])\n base = r((2010, 700)); tips = r((2010, 2045))\n # arm axis: y = W0 - 2010 ; x from 700 to 2045\n s = 0.50\n layer, (ox, oy, iw, ih) = place(None, rot, s, (tips[0], tips[1]), (2030, 720))\n # mat region after rotation\n mb0 = (MAT_BOX[1], W0 - MAT_BOX[2], MAT_BOX[3], W0 - MAT_BOX[0])\n mb = (ox + mb0[0] * s + 70, oy + mb0[1] * s + 70, ox + mb0[2] * s - 70, oy + mb0[3] * s - 70)\n bg = Image.new('RGB', (W, H), MAT)\n bg.paste(layer, (0, 0), rect_mask(mb, 50))\n # progressive pixelation, left -> right, in vertical strips\n arr = np.array(bg)\n out = arr.copy()\n sizes = [96, 84, 72, 64, 56, 48, 40, 34, 28, 22, 16, 11, 7, 4]\n x_start, x_end = 0, 1900\n sw = (x_end - x_start) / len(sizes)\n for i, b in enumerate(sizes):\n xa, xb = int(x_start + i * sw), int(x_start + (i + 1) * sw)\n small = bg.resize((max(1, W // b), max(1, H // b)), Image.BOX).resize(\n (W // b * b, H // b * b), Image.NEAREST)\n sm = np.zeros_like(arr); sa = np.array(small)\n sm[:sa.shape[0], :sa.shape[1]] = sa\n out[:, xa:xb] = sm[:, xa:xb]\n bg = Image.fromarray(out)\n shade = Image.new('RGB', (W, H), MAT)\n bg.paste(shade, (0, 0), hgrad(760, 1060, 205, 0))\n d = ImageDraw.Draw(bg)\n # detection-style bracket around the gripper (perception -> action)\n gx, gy, hw, hh = 1932, 720, 108, 118\n c = (120, 200, 255); L = 34; t = 4\n for sx in (-1, 1):\n for sy in (-1, 1):\n px, py = gx + sx * hw, gy + sy * hh\n d.line((px, py, px - sx * L, py), fill=c, width=t)\n d.line((px, py, px, py - sy * L), fill=c, width=t)\n tracked(d, (gx - hw, gy - hh - 44), 'GRASP', f_inter(24, 'SemiBold'), c, track=5)\n x = SAFE[0] + 20\n d.text((x, 572), 'Pixels to Motors', font=ImageFont.truetype(INTER + 'InterDisplay-Bold.otf', 88), fill=(248, 248, 248))\n d.text((x, 696), 'フィジカルAIを、現場で使える形にするまで。', font=f_cjk(38, bold=False), fill=(225, 222, 228))\n tracked(d, (x, 786), 'PIXELS → PERCEPTION → MOTION', f_inter(26, 'Medium'), (150, 146, 156), track=4)\n return bg\n\n# ---------------------------------------------------------------- C: SEE / DECIDE / MOVE (light)\ndef banner_c():\n NAVY = (26, 35, 50); BG = (244, 246, 248); GRID = (226, 231, 236)\n bg = Image.new('RGB', (W, H), BG)\n d = ImageDraw.Draw(bg)\n for gx in range(0, W, 40):\n d.line((gx, 0, gx, H), fill=GRID if gx % 200 else (214, 221, 228), width=1)\n for gy in range(0, H, 40):\n d.line((0, gy, W, gy), fill=GRID if gy % 200 else (214, 221, 228), width=1)\n # panel sources\n cubes = load('IMG_7912.JPG') # red & blue cubes, white paper\n crop = (1500, 700, 3500, 2700)\n p1 = cubes.crop(crop)\n hsv = np.array(p1.convert('HSV')).astype(int)\n rgb = np.array(p1).astype(int)\n sat = hsv[..., 1] > 110\n red = sat & ((hsv[..., 0] < 12) | (hsv[..., 0] > 235)) & (rgb[..., 0] > rgb[..., 2])\n blue = sat & (hsv[..., 0] > 120) & (hsv[..., 0] < 170)\n mask = np.zeros(rgb.shape, np.uint8)\n mask[red] = (255, 90, 90); mask[blue] = (90, 170, 255)\n p2 = Image.fromarray(mask).filter(ImageFilter.MedianFilter(7))\n # bounding boxes on mask\n d2 = ImageDraw.Draw(p2)\n for m, col in ((red, (255, 90, 90)), (blue, (90, 170, 255))):\n ys, xs = np.where(m)\n if len(xs):\n x0, x1 = np.percentile(xs, [1, 99]); y0, y1 = np.percentile(ys, [1, 99])\n d2.rectangle((x0 - 30, y0 - 30, x1 + 30, y1 + 30), outline=col, width=14)\n p3 = photo.crop((1100, 1250, 2900, 2250)) # gripper between the cubes\n P = 290; gap = 44\n x0 = 1062; y0 = 540\n panels = [(p1, '01', '見る', 'SEE'), (p2, '02', '判断する', 'DECIDE'), (p3, '03', '動かす', 'MOVE')]\n for i, (im, n, jp, en) in enumerate(panels):\n px = x0 + i * (P + gap)\n tile = ImageOps.fit(im, (P, P), Image.LANCZOS)\n shadow = Image.new('L', (W, H), 0)\n ImageDraw.Draw(shadow).rectangle((px + 6, y0 + 10, px + P + 6, y0 + P + 10), fill=70)\n bg.paste((180, 190, 200), (0, 0), shadow.filter(ImageFilter.GaussianBlur(12)))\n bg.paste(tile, (px, y0))\n d = ImageDraw.Draw(bg)\n d.text((px, y0 + P + 20), n, font=f_inter(24, 'SemiBold'), fill=(138, 155, 168))\n d.text((px + 44, y0 + P + 14), jp, font=f_cjk(30), fill=NAVY)\n tw = d.textlength(en, font=f_inter(22, 'Medium'))\n tracked(d, (px + P - tw - 12, y0 + P + 22), en, f_inter(22, 'Medium'), (138, 155, 168), track=3)\n if i < 2:\n ax = px + P + gap // 2; ay = y0 + P // 2\n d.polygon([(ax - 9, ay - 12), (ax + 9, ay), (ax - 9, ay + 12)], fill=NAVY)\n d = ImageDraw.Draw(bg)\n x = SAFE[0] + 20\n pixel_squares(d, x, 598, 16, 7, [NAVY, RED, BLUE])\n tracked(d, (x + 82, 592), 'PIXELS TO MOTORS', f_inter(26, 'SemiBold'), (90, 105, 120), track=6)\n d.text((x, 646), 'フィジカルAIを、', font=f_cjk(58), fill=NAVY)\n d.text((x, 722), '現場で使える形に。', font=f_cjk(58), fill=NAVY)\n d.text((x, 818), 'Experience first.', font=f_inter(30, 'Medium'), fill=(184, 90, 90))\n d.text((x, 856), 'Precision can be bought later.', font=f_inter(30, 'Regular'), fill=(110, 125, 140))\n return bg\n\ndef preview(img, name):\n \"\"\"safe-area overlay + desktop/mobile crops for checking\"\"\"\n ov = img.copy(); d = ImageDraw.Draw(ov, 'RGBA')\n d.rectangle((0, 0, W, SAFE[1]), fill=(0, 0, 0, 110)); d.rectangle((0, SAFE[3], W, H), fill=(0, 0, 0, 110))\n d.rectangle((0, SAFE[1], SAFE[0], SAFE[3]), fill=(0, 0, 0, 60)); d.rectangle((SAFE[2], SAFE[1], W, SAFE[3]), fill=(0, 0, 0, 60))\n d.rectangle(SAFE, outline=(255, 220, 0, 255), width=4)\n ov.resize((1280, 720)).save(OUT + f'check_{name}_overlay.jpg', quality=88)\n img.crop((0, SAFE[1], W, SAFE[3])).resize((1280, 212)).save(OUT + f'check_{name}_desktop.jpg', quality=88)\n img.crop(SAFE).save(OUT + f'check_{name}_mobile.jpg', quality=90)\n\nimport os\nos.makedirs(OUT, exist_ok=True)\nfor name, fn in (('A_grasp', banner_a), ('B_pixels_to_motors', banner_b), ('C_see_decide_move', banner_c)):\n im = fn()\n im.save(OUT + f'pixelstomotors_banner_{name}.jpg', quality=92, subsampling=0)\n preview(im, name)\n print(name, os.path.getsize(OUT + f'pixelstomotors_banner_{name}.jpg') // 1024, 'KB')</code></pre><h3 id=\"index_MFH88eN7\">make2.py(2回目の6案)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps\n\nS = '/home/claude/banner/site/'\nOUT = '/home/claude/banner/out2/'\nos.makedirs(OUT, exist_ok=True)\nW, H = 2560, 1440\nSAFE = (507, 508, 2053, 932)\nCY = 720\nINK = (17, 17, 17); CREAM = (242, 241, 236); GRAY = (152, 152, 148)\n\ndef L(name): return Image.open(S + f'c_{name}.png').convert('RGB')\n\ndef rounded(im, r):\n m = Image.new('L', im.size, 0)\n ImageDraw.Draw(m).rounded_rectangle((0, 0, im.width - 1, im.height - 1), r, fill=255)\n return m\n\ndef card(canvas, im, box, r=14, shadow=True):\n x0, y0, x1, y1 = box\n tile = ImageOps.fit(im, (x1 - x0, y1 - y0), Image.LANCZOS)\n if shadow:\n sh = Image.new('L', (W, H), 0)\n ImageDraw.Draw(sh).rounded_rectangle((x0 + 4, y0 + 12, x1 + 4, y1 + 12), r, fill=60)\n canvas.paste((150, 150, 150), (0, 0), sh.filter(ImageFilter.GaussianBlur(16)))\n canvas.paste(tile, (x0, y0), rounded(tile, r))\n\n# ------------------------------------------------ 1. Archive wall\ndef archive():\n c = Image.new('RGB', (W, H), (236, 236, 233))\n rows = [\n ['inside', 'panels320', 'gpu', 'panels324', 'film', 'rec_say'],\n ['manga2', 'lab', 'manga3', 'sim', 'manga0', 'drug', 'manga1'],\n ['who03', 'panels319', 'bowl', 'panels323', 'imgrec_top', 'rec_micro'],\n ]\n hs = [300, 390, 300]\n ys = [CY - 195 - 36 - 300, CY - 195, CY + 195 + 36]\n offs = [-170, -60, -250]\n for row, h, y, off in zip(rows, hs, ys, offs):\n x = off\n while x < W:\n for n in row:\n im = L(n)\n w = int(h * im.width / im.height)\n if n.startswith('manga'): w = int(h * 1.05)\n card(c, im, (x, y, x + w, y + h))\n x += w + 36\n if x >= W: break\n return c\n\n# ------------------------------------------------ 2. Mono split (vector)\ndef split():\n c = Image.new('RGB', (W, H), CREAM)\n d = ImageDraw.Draw(c)\n mid = 1240\n d.rectangle((0, 0, mid, H), fill=INK)\n # pixel dissolve across the seam: black squares scattered into cream, cream squares into black\n random.seed(7); cell = 44\n for gx in range(mid - cell * 10, mid + cell * 14, cell):\n for gy in range(0, H, cell):\n t = (gx - mid) / (cell * 12)\n if gx >= mid and random.random() < max(0, 0.9 - t):\n d.rectangle((gx + 3, gy + 3, gx + cell - 3, gy + cell - 3), fill=INK)\n if gx < mid and random.random() < max(0, 0.75 + t * 1.4):\n d.rectangle((gx + 3, gy + 3, gx + cell - 3, gy + cell - 3), fill=CREAM)\n # hatched block (top-left of band) and dot grids\n hx0, hy0, hx1, hy1 = 560, 540, 760, 740\n hl = Image.new('RGB', (W, H), INK); hd = ImageDraw.Draw(hl)\n for k in range(-220, 240, 16):\n hd.line((hx0 + k, hy1, hx0 + k + 200, hy0), fill=CREAM, width=4)\n hm = Image.new('L', (W, H), 0); ImageDraw.Draw(hm).rectangle((hx0, hy0, hx1, hy1), fill=255)\n c.paste(hl, (0, 0), hm)\n for gx in range(1800, 2040, 22):\n for gy in range(540, 700, 22):\n d.ellipse((gx - 3, gy - 3, gx + 3, gy + 3), fill=INK)\n for gx in range(560, 760, 22):\n for gy in range(800, 900, 22):\n d.ellipse((gx - 3, gy - 3, gx + 3, gy + 3), fill=(120, 120, 120))\n # half-hatched circle (right)\n cx, cy, r = 1700, 735, 175\n circ = Image.new('L', (W, H), 0); ImageDraw.Draw(circ).ellipse((cx - r, cy - r, cx + r, cy + r), fill=255)\n hatch = Image.new('RGB', (W, H), CREAM); hd = ImageDraw.Draw(hatch)\n for k in range(-400, 400, 18):\n hd.line((cx + k - 200, cy + 200, cx + k + 200, cy - 200), fill=INK, width=5)\n half = Image.new('L', (W, H), 0); ImageDraw.Draw(half).rectangle((cx, 0, W, H), fill=255)\n m = Image.fromarray((np.array(circ) & np.array(half)).astype(np.uint8))\n c.paste(hatch, (0, 0), m)\n d.ellipse((cx - r, cy - r, cx + r, cy + r), outline=INK, width=6)\n for a in (0, 90, 180, 270):\n px = cx + r * math.cos(math.radians(a)); py = cy + r * math.sin(math.radians(a))\n d.ellipse((px - 12, py - 12, px + 12, py + 12), fill=INK)\n # dotted signal line running from black side into cream (noisy -> settled)\n random.seed(3)\n pts = []\n for i, x in enumerate(range(520, 2060, 60)):\n amp = 90 * max(0, 1 - (x - 520) / 1200)\n pts.append((x, 870 - random.uniform(-1, 1) * amp))\n for (x0, y0), (x1, y1) in zip(pts, pts[1:]):\n steps = int(math.hypot(x1 - x0, y1 - y0) // 14)\n for s in range(steps):\n t = s / steps; px = x0 + (x1 - x0) * t; py = y0 + (y1 - y0) * t\n col = CREAM if px < mid else INK\n d.ellipse((px - 2.5, py - 2.5, px + 2.5, py + 2.5), fill=col)\n for x, y in pts:\n col = CREAM if x < mid else INK\n d.ellipse((x - 9, y - 9, x + 9, y + 9), fill=INK if x >= mid else INK, outline=col, width=4)\n return c\n\n# ------------------------------------------------ 3. Simulation halftone\ndef halftone():\n src = ImageOps.autocontrast(L('sim').convert('L'), cutoff=1)\n sc = W / src.width; bh = int(src.height * sc)\n big = src.resize((W, bh), Image.BICUBIC)\n top = int(0.30 * bh) - SAFE[1] - 340\n full = Image.new('L', (W, H), 0)\n full.paste(big, (0, -top))\n a = np.array(full).astype(float) / 255\n c = Image.new('RGB', (W, H), CREAM); d = ImageDraw.Draw(c)\n step = 10\n for y in range(0, H, step):\n for x in range((y // step % 2) * step // 2, W, step):\n v = a[y, min(W - 1, x)]\n r = v ** 1.6 * step * 0.66\n if r > 0.6:\n d.ellipse((x - r, y - r, x + r, y + r), fill=INK)\n return c\n\n# ------------------------------------------------ 4. Manga strip\ndef manga():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n # screentone background (fading dots towards the centre)\n for y in range(0, H, 16):\n for x in range(0, W, 16):\n dist = abs(x - W / 2) / (W / 2)\n r = 3.2 * max(0, dist - 0.35) / 0.65\n if r > 0.4:\n d.ellipse((x - r, y - r, x + r, y + r), fill=(200, 200, 198))\n order = ['manga3', 'manga0', 'manga2', 'manga1']\n ph = 372; pw = 366; gap = 22\n total = 4 * pw + 3 * gap\n x = (W - total) // 2; y = CY - ph // 2\n for n in order:\n im = L(n)\n tile = ImageOps.fit(im, (pw, ph), Image.LANCZOS, centering=(0.5, 0.6))\n c.paste(tile, (x, y))\n d.rectangle((x - 5, y - 5, x + pw + 4, y + ph + 4), outline=INK, width=6)\n x += pw + gap\n # speed lines on both sides\n for k in range(26):\n yy = y + 10 + k * (ph - 20) / 25\n ln = 120 + (k * 37 % 5) * 60\n d.line((x + 30, yy, x + 30 + ln, yy), fill=INK, width=3)\n x0 = (W - total) // 2 - 40\n d.line((x0 - ln, yy, x0, yy), fill=INK, width=3)\n return c\n\n# ------------------------------------------------ 5. Dot matrix\ndef dots():\n c = Image.new('RGB', (W, H), CREAM); d = ImageDraw.Draw(c)\n D = 82; G = 24; P = D + G\n random.seed(36)\n cols = W // P + 2; rows = H // P + 4\n ox = (W - cols * P) // 2 + G // 2; oy = SAFE[1] + G // 2 + 2 - P * 6\n for j in range(rows):\n for i in range(cols):\n x = ox + i * P; y = oy + j * P\n t = i / cols\n r = random.random()\n # left: mostly solved (black), middle: in progress (gray), right: open (outline)\n pb = max(0, 0.75 - t * 1.3); pg = 0.55 - abs(t - 0.5) * 0.6\n if r < pb: d.ellipse((x, y, x + D, y + D), fill=INK)\n elif r < pb + pg: d.ellipse((x, y, x + D, y + D), fill=GRAY)\n else: d.ellipse((x + 2, y + 2, x + D - 2, y + D - 2), outline=INK, width=4)\n return c\n\n# ------------------------------------------------ 6. Hakobu photo\ndef hakobu():\n src = L('drug')\n sw = int(src.width * 0.30)\n left = src.crop((0, 0, sw, src.height)); right = src.crop((src.width - sw, 0, src.width, src.height))\n pano = Image.new('RGB', (sw * 2, src.height)); pano.paste(left, (0, 0)); pano.paste(right, (sw, 0))\n bgim = pano.resize((W, H), Image.BICUBIC).filter(ImageFilter.GaussianBlur(30))\n bgim = Image.blend(bgim, Image.new('RGB', (W, H), (238, 236, 232)), 0.25)\n # sharp copy: robot fits inside the band\n h = 470; s = h / src.height; w = int(src.width * s)\n sm = src.resize((w, h), Image.LANCZOS)\n x = 1500 - w // 2; y = CY - h // 2 + 12\n m = Image.new('L', (w, h), 0)\n ImageDraw.Draw(m).rectangle((60, 45, w - 60, h - 45), fill=255)\n m = m.filter(ImageFilter.GaussianBlur(45))\n bgim.paste(sm, (x, y), m)\n return bgim\n\ndef preview(img, name):\n img.crop((0, SAFE[1], W, SAFE[3])).save(OUT + f'_band_{name}.png')\n\nitems = [('B_archive_wall', archive), ('C_mono_split', split), ('D_sim_halftone', halftone),\n ('E_hakobu_manga', manga), ('F_dot_matrix', dots), ('G_hakobu_photo', hakobu)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'pixelstomotors_banner_{name}.jpg', quality=92, subsampling=0)\n preview(im, name)\n print(name, os.path.getsize(OUT + f'pixelstomotors_banner_{name}.jpg') // 1024, 'KB')</code></pre><h3 id=\"index__zJBabGH\">icons.py(コードで描いたアイコン3案)</h3><pre><code class=\"language-python\">import os, random\nfrom PIL import Image, ImageDraw, ImageFilter\n\nOUT = '/home/claude/banner/icons/'\nos.makedirs(OUT, exist_ok=True)\nSS = 4; N = 800 * SS\nSHELL = (246, 245, 241); SHELL_EDGE = (214, 214, 208)\nDISP = (16, 16, 18); EYE = (246, 248, 252); MOUTH = (87, 181, 248)\nCREAM = (245, 245, 240); ORANGE = (196, 71, 31); INK = (13, 13, 13)\n\ndef face(img, box):\n \"\"\"draw display + eyes + smile inside box (x0,y0,x1,y1)\"\"\"\n x0, y0, x1, y1 = box; w = x1 - x0; h = y1 - y0\n d = ImageDraw.Draw(img)\n d.rounded_rectangle(box, radius=int(w * 0.16), fill=DISP)\n ew, eh = w * 0.13, h * 0.36\n cy = y0 + h * 0.42\n # glow\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gd = ImageDraw.Draw(glow)\n for cx in (x0 + w * 0.31, x0 + w * 0.69):\n gd.rounded_rectangle((cx - ew * 0.75, cy - eh * 0.62, cx + ew * 0.75, cy + eh * 0.62), radius=int(ew * 0.75), fill=(246, 248, 252, 90))\n glow = glow.filter(ImageFilter.GaussianBlur(w * 0.03))\n img.alpha_composite(glow) if img.mode == 'RGBA' else img.paste(glow, (0, 0), glow)\n d = ImageDraw.Draw(img)\n for cx in (x0 + w * 0.31, x0 + w * 0.69):\n d.rounded_rectangle((cx - ew / 2, cy - eh / 2, cx + ew / 2, cy + eh / 2), radius=int(ew / 2), fill=EYE)\n # smile: thick arc with round caps\n mw = w * 0.24; my = y0 + h * 0.70; t = int(h * 0.075)\n d.arc((x0 + w / 2 - mw / 2, my - mw * 0.55, x0 + w / 2 + mw / 2, my + mw * 0.30), 25, 155, fill=MOUTH, width=t)\n import math\n for a in (25, 155):\n rx = mw / 2 - t / 2; ry = (mw * 0.85) / 2 - t / 2\n cx = x0 + w / 2 + rx * math.cos(math.radians(a)); cyy = (my - mw * 0.55 + my + mw * 0.30) / 2 + ry * math.sin(math.radians(a))\n d.ellipse((cx - t / 2, cyy - t / 2, cx + t / 2, cyy + t / 2), fill=MOUTH)\n\ndef head(img, cx, cy, w, h):\n d = ImageDraw.Draw(img)\n d.rounded_rectangle((cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2), radius=int(w * 0.22), fill=SHELL, outline=SHELL_EDGE, width=int(w * 0.012))\n m = w * 0.075\n face(img, (cx - w / 2 + m, cy - h / 2 + m, cx + w / 2 - m, cy + h / 2 - m * 1.1))\n\ndef circle_out(img, name):\n im = img.convert('RGB').resize((800, 800), Image.LANCZOS)\n im.save(OUT + f'{name}.png')\n m = Image.new('L', (800, 800), 0); ImageDraw.Draw(m).ellipse((0, 0, 799, 799), fill=255)\n c = Image.new('RGBA', (800, 800), (0, 0, 0, 0)); c.paste(im, (0, 0), m)\n c.save(OUT + f'{name}_circle.png')\n return c\n\n# 1. close-up face (shell fills the circle)\ndef i1():\n img = Image.new('RGBA', (N, N), CREAM + (255,))\n head(img, N / 2, N * 0.53, N * 0.98, N * 0.80)\n return circle_out(img, 'icon1_face')\n\n# 2. orange ground, head + top of body\ndef i2():\n img = Image.new('RGBA', (N, N), ORANGE + (255,))\n d = ImageDraw.Draw(img)\n bw = N * 0.66\n d.rounded_rectangle((N / 2 - bw / 2, N * 0.70, N / 2 + bw / 2, N * 1.2), radius=int(bw * 0.14), fill=SHELL, outline=SHELL_EDGE, width=SS * 3)\n d.rounded_rectangle((N / 2 - bw * 0.12, N * 0.80, N / 2 + bw * 0.12, N * 0.84), radius=SS * 8, fill=(40, 40, 40))\n head(img, N / 2, N * 0.43, N * 0.70, N * 0.50)\n return circle_out(img, 'icon2_orange')\n\n# 3. pixel dissolve (continuity with the current checker logo)\ndef i3():\n img = Image.new('RGBA', (N, N), INK + (255,))\n face(img, (N * 0.16, N * 0.22, N * 0.92, N * 0.80))\n d = ImageDraw.Draw(img)\n random.seed(4); cell = N // 11\n for gx in range(0, N // 2, cell):\n for gy in range(0, N, cell):\n p = 0.85 - gx / (N * 0.42)\n if random.random() < p:\n d.rectangle((gx, gy, gx + cell - SS * 6, gy + cell - SS * 6), fill=(245, 245, 240))\n return circle_out(img, 'icon3_pixel')\n\nicons = [i1(), i2(), i3()]\n\n# size check sheet on YouTube dark + light backgrounds\nfrom PIL import ImageFont\nfnt = ImageFont.truetype('/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc', 22, index=0)\nsheet = Image.new('RGB', (1500, 3 * 300 + 40), (255, 255, 255)); sd = ImageDraw.Draw(sheet)\nlabels = ['案1 顔のアップ', '案2 オレンジ地', '案3 ピクセル継承']\nfor r, (ic, lab) in enumerate(zip(icons, labels)):\n y = 20 + r * 300\n sd.text((20, y), lab, font=fnt, fill=(20, 20, 20))\n for bgc, x0 in (((15, 15, 15), 20), ((255, 255, 255), 760)):\n sd.rectangle((x0, y + 36, x0 + 720, y + 280), fill=bgc)\n x = x0 + 20\n for s in (200, 98, 48, 24):\n sheet.paste(ic.resize((s, s), Image.LANCZOS), (x, y + 58 + (200 - s) // 2), ic.resize((s, s), Image.LANCZOS))\n x += s + 40\nsheet.save('/home/claude/banner/icons_sizes.jpg', quality=90)</code></pre><h3 id=\"index_ttHgfGc2\">hakobu_icon.py(動画の1コマから作ったアイコン3案)</h3><pre><code class=\"language-python\">from PIL import Image, ImageDraw, ImageFilter, ImageFont\nfr=Image.open('fr_0.png').convert('RGB'); W0,H0=fr.size\nBG=(245,245,240); OR=(196,71,31)\nclean=Image.new('RGB',(W0,H0),BG); clean.paste(fr.crop((0,758,360,H0)),(0,758))\ndef grab(box):\n x0,y0,x1,y1=box; c=Image.new('RGB',(x1-x0,y1-y0),BG)\n c.paste(clean.crop((max(0,x0),max(0,y0),min(W0,x1),min(H0,y1))),(max(0,-x0),max(0,-y0))); return c\ndef up(c,s=800): return c.resize((s,s),Image.LANCZOS).filter(ImageFilter.UnsharpMask(2,60,2))\ndef circ(im):\n m=Image.new('L',im.size,0);ImageDraw.Draw(m).ellipse((0,0,im.width-1,im.height-1),fill=255)\n o=Image.new('RGBA',im.size,(0,0,0,0));o.paste(im,(0,0),m);return o\nA=up(grab((30,660,450,1080)))\nC=up(grab((90,690,390,990)))\n# B: orange ring around the cream picture (no cut-out edges)\ninner=up(grab((50,690,430,1070)),640)\nB=Image.new('RGB',(800,800),OR); m=Image.new('L',(640,640),0); ImageDraw.Draw(m).ellipse((0,0,639,639),fill=255)\nB.paste(inner,(80,80),m)\nout={'A_upper':A,'B_orange_ring':B,'C_face':C}\nimport glob,os\nfor p in glob.glob('../icons/hakobu_icon_*'): os.remove(p)\nfor p in glob.glob('../icons/zoomB.png'): os.remove(p)\nfor k,v in out.items():\n v.save(f'../icons/hakobu_icon_{k}.png'); circ(v).save(f'../icons/hakobu_icon_{k}_circle.png')\nf=ImageFont.truetype('/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc',22,index=0)\nlabels={'A_upper':'案A 動画のまま(上半身・クリーム地)','B_orange_ring':'案B オレンジの縁取り','C_face':'案C 顔に寄せる'}\nsh=Image.new('RGB',(1500,3*300+40),(255,255,255));d=ImageDraw.Draw(sh)\nfor r,k in enumerate(out):\n y=20+r*300; d.text((20,y),labels[k],font=f,fill=(20,20,20)); ic=circ(out[k])\n for bgc,x0 in (((15,15,15),20),((255,255,255),760)):\n d.rectangle((x0,y+36,x0+720,y+280),fill=bgc);x=x0+20\n for s in (200,98,48,24):\n t=ic.resize((s,s),Image.LANCZOS); sh.paste(t,(x,y+58+(200-s)//2),t); x+=s+40\nsh.save('../hakobu_icons_sizes.jpg',quality=90)</code></pre><h3 id=\"index_ox6KpGZu\">make3.py(3回目の5つのタッチ)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps, ImageChops\n\nOUT = '/home/claude/banner/hakobu/'\nos.makedirs(OUT, exist_ok=True)\nW, H = 2048, 1152\nBAND = (0, 407, 2048, 745) # visible on desktop\nSAFE = (406, 407, 1642, 745) # visible everywhere\nCREAM = (245, 245, 240); INK = (13, 13, 13); ORANGE = (196, 71, 31); BLUE = (26, 108, 152)\nSHELL = (247, 246, 242); SHELL2 = (226, 223, 215); DISP = (20, 20, 22)\nEYE = (246, 248, 252); MOUTH = (87, 181, 248); RING = (120, 226, 160)\nFR = '/home/claude/banner/vid/fx/'\nSS = 3 # supersampling for vector work\n\ndef frame(i):\n \"\"\"video frame with everything except Hakobu painted over with the background\"\"\"\n f = Image.open(FR + f'e_{i}.png').convert('RGB')\n c = Image.new('RGB', f.size, CREAM)\n c.paste(f.crop((0, 758, 362, 1080)), (0, 758))\n return c\n\n# ---------------------------------------------------------------- vector Hakobu\ndef face(d, box, expr='smile', glow_layer=None):\n x0, y0, x1, y1 = box; w = x1 - x0; h = y1 - y0\n d.rounded_rectangle(box, radius=int(min(w, h) * 0.16), fill=DISP)\n ew, eh = w * 0.11, h * 0.36; cy = y0 + h * 0.43\n for cx in (x0 + w * 0.32, x0 + w * 0.68):\n if expr == 'blink':\n d.rounded_rectangle((cx - ew * 0.8, cy - ew * 0.18, cx + ew * 0.8, cy + ew * 0.18), radius=int(ew * 0.18), fill=EYE)\n elif expr == 'happy':\n d.arc((cx - ew * 0.9, cy - ew * 0.3, cx + ew * 0.9, cy + ew * 1.5), 200, 340, fill=EYE, width=int(ew * 0.35))\n else:\n if glow_layer is not None:\n ImageDraw.Draw(glow_layer).rounded_rectangle((cx - ew * 0.9, cy - eh * 0.62, cx + ew * 0.9, cy + eh * 0.62), radius=int(ew * 0.9), fill=(246, 248, 252, 110))\n d.rounded_rectangle((cx - ew / 2, cy - eh / 2, cx + ew / 2, cy + eh / 2), radius=int(ew / 2), fill=EYE)\n mw = w * 0.22; my = y0 + h * 0.74; t = max(2, int(h * 0.07))\n if expr in ('open', 'happy'):\n d.chord((x0 + w / 2 - mw / 2, my - mw * 0.45, x0 + w / 2 + mw / 2, my + mw * 0.45), 0, 180, fill=MOUTH)\n elif expr == 'o':\n r = mw * 0.18; d.ellipse((x0 + w / 2 - r, my - r * 0.6, x0 + w / 2 + r, my + r * 1.2), fill=MOUTH)\n else:\n d.arc((x0 + w / 2 - mw / 2, my - mw * 0.55, x0 + w / 2 + mw / 2, my + mw * 0.30), 25, 155, fill=MOUTH, width=t)\n\ndef hakobu(img, cx, base, h, expr='smile', side=True):\n \"\"\"flat front-ish Hakobu, bottom of wheels at `base`, total height h\"\"\"\n d = ImageDraw.Draw(img)\n hw = h * 0.31; bw = h * 0.33; sd = h * 0.07 if side else 0\n # body\n by0 = base - h * 0.70; by1 = base - h * 0.08\n if side:\n d.rounded_rectangle((cx + bw - sd * 2, by0, cx + bw + sd, by1), radius=int(h * 0.05), fill=SHELL2)\n d.ellipse((cx + bw - sd * 0.2, by0 + (by1 - by0) * 0.28, cx + bw + sd * 0.75, by0 + (by1 - by0) * 0.62), outline=RING, width=max(2, int(h * 0.012)))\n d.rounded_rectangle((cx - bw, by0, cx + bw, by1), radius=int(h * 0.06), fill=SHELL)\n d.line((cx - bw + h * 0.01, by0 + h * 0.08, cx + bw - h * 0.01, by0 + h * 0.08), fill=SHELL2, width=max(1, int(h * 0.006)))\n d.rounded_rectangle((cx - h * 0.06, by0 + h * 0.20, cx + h * 0.06, by0 + h * 0.225), radius=int(h * 0.012), fill=(40, 40, 42))\n # base\n d.rounded_rectangle((cx - bw * 1.02, by1 - h * 0.01, cx + bw * 1.02 + sd * 0.5, base - h * 0.02), radius=int(h * 0.02), fill=(58, 58, 60))\n d.rounded_rectangle((cx - bw * 0.9, base - h * 0.03, cx + bw * 0.9, base), radius=int(h * 0.012), fill=(30, 30, 32))\n # head\n hy0 = base - h; hy1 = base - h * 0.70 + h * 0.015\n if side:\n d.rounded_rectangle((cx + hw - sd * 2, hy0 + h * 0.01, cx + hw + sd, hy1), radius=int(h * 0.05), fill=SHELL2)\n d.rounded_rectangle((cx - hw, hy0, cx + hw, hy1), radius=int(h * 0.07), fill=SHELL)\n m = h * 0.03\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0))\n face(d, (cx - hw + m, hy0 + m, cx + hw - m, hy1 - m * 1.1), expr, glow)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(h * 0.012)))\n\ndef ss_canvas(color):\n return Image.new('RGBA', (W * SS, H * SS), color + (255,))\n\ndef finish(img):\n return img.convert('RGB').resize((W, H), Image.LANCZOS)\n\n# ---------------------------------------------------------------- 1. illustration\ndef illustration():\n S = SS; img = ss_canvas((228, 238, 235)); d = ImageDraw.Draw(img)\n floor_y = 700 * S\n d.rectangle((0, floor_y, W * S, H * S), fill=(214, 207, 194))\n d.rectangle((0, floor_y - 6 * S, W * S, floor_y), fill=(196, 208, 204)) # skirting\n d.rectangle((0, 560 * S, W * S, 568 * S), fill=(180, 196, 192)) # handrail\n for x in range(0, W, 520): # doors\n dx = (x + 140) * S\n d.rounded_rectangle((dx, 300 * S, dx + 190 * S, floor_y - 6 * S), radius=10 * S, fill=(201, 219, 214))\n d.rounded_rectangle((dx + 55 * S, 360 * S, dx + 135 * S, 470 * S), radius=8 * S, fill=(237, 245, 243))\n d.rectangle((dx + 150 * S, 520 * S, dx + 170 * S, 530 * S), fill=(150, 168, 164))\n # room number plate (blank)\n d.rounded_rectangle((dx + 60 * S, 270 * S, dx + 130 * S, 290 * S), radius=4 * S, fill=(196, 71, 31))\n for x in (400, 1460): # plants\n px = x * S\n d.rounded_rectangle((px, 640 * S, px + 60 * S, floor_y), radius=6 * S, fill=(196, 160, 120))\n for a, r in ((-30, 46), (0, 60), (30, 46), (-60, 36), (60, 36)):\n ex = px + 30 * S + math.sin(math.radians(a)) * r * S; ey = 640 * S - math.cos(math.radians(a)) * r * S\n d.ellipse((ex - 22 * S, ey - 34 * S, ex + 22 * S, ey + 34 * S), fill=(110, 170, 130))\n # floor reflection lines\n for x in range(0, W, 90):\n d.line(((x + 30) * S, (floor_y + 40 * S), (x + 70) * S, (floor_y + 40 * S)), fill=(226, 220, 208), width=4 * S)\n # Hakobu, carrying a medicine tray on its head\n cx, base, h = 1080 * S, 736 * S, 300 * S\n hakobu(img, cx, base, h, 'smile')\n d = ImageDraw.Draw(img)\n # motion lines behind\n for k, yy in enumerate((520, 580, 640, 690)):\n d.rounded_rectangle(((880 - k * 25) * S, yy * S, (960 - k * 10) * S, (yy + 8) * S), radius=4 * S, fill=(190, 206, 202))\n return finish(img)\n\n# ---------------------------------------------------------------- 2. 4-koma manga style (wordless)\ndef to_manga(im):\n g = ImageOps.grayscale(im)\n g = ImageOps.autocontrast(g, cutoff=1)\n e = g.filter(ImageFilter.FIND_EDGES).point(lambda v: 255 if v > 28 else 0).filter(ImageFilter.MaxFilter(3))\n a = np.array(g).astype(int)\n out = np.full(a.shape, 255, np.uint8)\n yy, xx = np.mgrid[0:a.shape[0], 0:a.shape[1]]\n dots = ((xx % 7 - 3) ** 2 + (yy % 7 - 3) ** 2) <= 3\n out[(a < 215) & dots] = 0\n out[a < 70] = 0\n out[np.array(e) > 0] = 0\n return Image.fromarray(out).convert('RGB')\n\ndef manga():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 12):\n for x in range(0, W, 12):\n dist = abs(x - W / 2) / (W / 2); r = 2.6 * max(0, dist - 0.4) / 0.6\n if r > 0.4: d.ellipse((x - r, y - r, x + r, y + r), fill=(205, 205, 203))\n seq = [(1, 'smile'), (8, 'surprise'), (15, 'blink'), (9, 'joy')]\n pw, ph, gap = 290, 300, 18\n x = (W - (4 * pw + 3 * gap)) // 2; y = (BAND[1] + BAND[3]) // 2 - ph // 2\n for i, (fi, fx) in enumerate(seq):\n crop = frame(fi).crop((0, 730, 380, 1080))\n if i % 2 == 1: crop = ImageOps.mirror(crop)\n panel = ImageOps.fit(to_manga(crop), (pw, ph), Image.LANCZOS, centering=(0.5, 0.15))\n pd = ImageDraw.Draw(panel)\n if fx == 'surprise': # focus lines + exclamation mark\n for a in range(0, 360, 9):\n r0, r1 = 150, 260; ca, sa = math.cos(math.radians(a)), math.sin(math.radians(a))\n pd.line((pw / 2 + ca * r0, ph * 0.35 + sa * r0, pw / 2 + ca * r1, ph * 0.35 + sa * r1), fill=INK, width=2)\n pd.polygon([(250, 20), (268, 20), (263, 70), (255, 70)], fill=INK); pd.ellipse((252, 78, 266, 92), fill=INK)\n if fx == 'blink': # sweat drop\n pd.polygon([(60, 40), (48, 70), (72, 70)], fill=(255, 255, 255), outline=INK)\n pd.ellipse((46, 58, 74, 86), fill=(255, 255, 255), outline=INK, width=2)\n if fx == 'joy': # sparkles\n for sx, sy, s in ((40, 40, 18), (255, 60, 14), (60, 120, 10)):\n pd.polygon([(sx, sy - s), (sx + s * 0.25, sy - s * 0.25), (sx + s, sy), (sx + s * 0.25, sy + s * 0.25),\n (sx, sy + s), (sx - s * 0.25, sy + s * 0.25), (sx - s, sy), (sx - s * 0.25, sy - s * 0.25)], fill=INK)\n c.paste(panel, (x, y))\n d.rectangle((x - 4, y - 4, x + pw + 3, y + ph + 3), outline=INK, width=5)\n x += pw + gap\n x0 = (W - (4 * pw + 3 * gap)) // 2\n for k in range(24):\n yy = y + 10 + k * (ph - 20) / 23; ln = 90 + (k * 37 % 5) * 45\n d.line((x + 20, yy, x + 20 + ln, yy), fill=INK, width=3)\n d.line((x0 - 40 - ln, yy, x0 - 40, yy), fill=INK, width=3)\n return c\n\n# ---------------------------------------------------------------- 3. realistic (3D render line-up)\ndef realistic():\n c = Image.new('RGB', (W, H), CREAM)\n order = [15, 8, 1, 9, 4]\n step = 390; x = W // 2 - 2 * step - 190\n fade = Image.new('L', (380, 350), 255); fd = ImageDraw.Draw(fade)\n for k in range(90):\n fd.line((0, 349 - k, 380, 349 - k), fill=int(255 * k / 90))\n for i, fi in enumerate(order):\n crop = frame(fi).crop((0, 730, 380, 1080))\n if i < 2: crop = ImageOps.mirror(crop) # left side looks toward the centre\n c.paste(crop, (x, 412), fade)\n x += step\n # soft floor line\n d = ImageDraw.Draw(c, 'RGBA')\n return c\n\n# ---------------------------------------------------------------- 4. photo (hospital corridor)\ndef photo():\n src = Image.open('/home/claude/banner/site/c_drug.png').convert('RGB')\n sw = int(src.width * 0.30)\n pano = Image.new('RGB', (sw * 2, src.height))\n pano.paste(src.crop((0, 0, sw, src.height)), (0, 0)); pano.paste(src.crop((src.width - sw, 0, src.width, src.height)), (sw, 0))\n bg = pano.resize((W, H), Image.BICUBIC).filter(ImageFilter.GaussianBlur(24))\n bg = Image.blend(bg, Image.new('RGB', (W, H), (238, 236, 232)), 0.2)\n h = 400; s = h / src.height; w = int(src.width * s)\n sm = src.resize((w, h), Image.LANCZOS)\n m = Image.new('L', (w, h), 0); ImageDraw.Draw(m).rectangle((50, 35, w - 50, h - 35), fill=255)\n bg.paste(sm, (W // 2 - w // 2 + 160, 390), m.filter(ImageFilter.GaussianBlur(38)))\n return bg\n\n# ---------------------------------------------------------------- 5. graphic design (face as geometry)\ndef graphic():\n S = SS; img = ss_canvas(CREAM); d = ImageDraw.Draw(img)\n for gx in range(0, W, 32):\n d.line((gx * S, 0, gx * S, H * S), fill=(232, 232, 226), width=S)\n for gy in range(0, H, 32):\n d.line((0, gy * S, W * S, gy * S), fill=(232, 232, 226), width=S)\n # huge display, face fills the band\n box = (712 * S, 352 * S, 1336 * S, 792 * S)\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0))\n face(d, box, 'smile', glow)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(14 * S)))\n d = ImageDraw.Draw(img)\n # pixel squares leaving the display (left) – echo of the brand mark\n random.seed(5); cell = 34\n for gx in range(520, 712, cell):\n for gy in range(424, 740, cell):\n if random.random() < (gx - 520) / 200:\n d.rectangle((gx * S, gy * S, (gx + cell - 6) * S, (gy + cell - 6) * S), fill=INK)\n # orange dimension lines (right) and blue dot (ring) accent\n ox = 1400\n d.line((ox * S, 430 * S, ox * S, 730 * S), fill=ORANGE, width=4 * S)\n for yy in (430, 730):\n d.line(((ox - 18) * S, yy * S, (ox + 18) * S, yy * S), fill=ORANGE, width=4 * S)\n d.polygon([(ox * S, 430 * S), ((ox - 10) * S, 452 * S), ((ox + 10) * S, 452 * S)], fill=ORANGE)\n d.polygon([(ox * S, 730 * S), ((ox - 10) * S, 708 * S), ((ox + 10) * S, 708 * S)], fill=ORANGE)\n d.ellipse((1460 * S, 520 * S, 1580 * S, 640 * S), outline=RING, width=10 * S)\n for gx in range(1640, 1900, 22):\n for gy in range(440, 720, 22):\n d.ellipse(((gx - 3) * S, (gy - 3) * S, (gx + 3) * S, (gy + 3) * S), fill=INK)\n return finish(img)\n\nitems = [('1_illustration', illustration), ('2_manga', manga), ('3_realistic', realistic),\n ('4_photo', photo), ('5_graphic', graphic)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'hakobu_banner_{name}_2048x1152.jpg', quality=92, subsampling=0)\n im.crop(BAND).save(OUT + f'_band_{name}.png')\n print(name)</code></pre><h3 id=\"index_TtkMtlZh\">make4.py(4回目の4案)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps\n\nOUT = '/home/claude/banner/hakobu2/'\nos.makedirs(OUT, exist_ok=True)\nW, H = 2048, 1152\nBAND = (0, 407, 2048, 745)\nCREAM = (245, 245, 240); INK = (13, 13, 13); ORANGE = (196, 71, 31)\nMOUTH = (87, 181, 248); RING = (140, 226, 172)\nSITE = '/home/claude/banner/site/'\n\n# ------------------------------------------------------------ three-view sheet\nsrc = Image.open('/home/claude/banner/sheet/src.png').convert('RGB')\ng = np.array(src.convert('L')).astype(int)\nclean = np.array(src).astype(int)\nclean[g > 212] = 255 # remove faint paper pattern\n# colour accents: mouth blue, side ring green\nyy, xx = np.mgrid[0:g.shape[0], 0:g.shape[1]]\nmouth = (xx >= 184) & (xx <= 215) & (yy >= 490) & (yy <= 505) & (g > 70) & (g < 200)\nclean[mouth] = MOUTH\nr = np.hypot(xx - 554, yy - 663)\nring = (r >= 50.5) & (r <= 62.5) & (g > 95)\nclean[ring] = RING\nsheet = Image.fromarray(clean.astype(np.uint8))\nVIEWS = {'front': (64, 409, 338, 881), 'side': (390, 413, 705, 881), 'back': (743, 414, 1019, 882)}\n\ndef view(name):\n \"\"\"RGBA cut-out of one view (white outside the outline becomes transparent)\"\"\"\n box = VIEWS[name]\n im = sheet.crop(box)\n gg = np.array(im.convert('L'))\n barrier = Image.fromarray(np.where(gg < 200, 0, 255).astype(np.uint8)).filter(ImageFilter.MinFilter(5))\n ff = barrier.copy()\n for p in ((0, 0), (ff.width - 1, 0), (0, ff.height - 1), (ff.width - 1, ff.height - 1)):\n if ff.getpixel(p) == 255:\n ImageDraw.floodfill(ff, p, 128)\n m = np.where(np.array(ff) == 128, 0, 255).astype(np.uint8)\n mask = Image.fromarray(m).filter(ImageFilter.MinFilter(3)).filter(ImageFilter.GaussianBlur(0.7))\n out = im.convert('RGBA'); out.putalpha(mask)\n return out\n\ndef scaled(im, h):\n return im.resize((int(im.width * h / im.height), h), Image.LANCZOS)\n\ndef manga_crop(n, box):\n return Image.open(SITE + f'c_manga{n}.png').convert('RGB').crop(box)\n\ndef grid(c, step=32, major=160):\n d = ImageDraw.Draw(c)\n for x in range(0, W, step):\n d.line((x, 0, x, H), fill=(214, 214, 208) if x % major == 0 else (230, 230, 224), width=1)\n for y in range(0, H, step):\n d.line((0, y, W, y), fill=(214, 214, 208) if y % major == 0 else (230, 230, 224), width=1)\n\ndef arrow_dim(d, x, y0, y1, col=ORANGE, w=3):\n d.line((x, y0, x, y1), fill=col, width=w)\n for yy_, s in ((y0, 1), (y1, -1)):\n d.line((x - 12, yy_, x + 12, yy_), fill=col, width=w)\n d.polygon([(x, yy_), (x - 7, yy_ + 16 * s), (x + 7, yy_ + 16 * s)], fill=col)\n\ndef hdim(d, x0, x1, y, col=ORANGE, w=3):\n d.line((x0, y, x1, y), fill=col, width=w)\n for xx_, s in ((x0, 1), (x1, -1)):\n d.line((xx_, y - 12, xx_, y + 12), fill=col, width=w)\n d.polygon([(xx_, y), (xx_ + 16 * s, y - 7), (xx_ + 16 * s, y + 7)], fill=col)\n\ndef dashdot(d, x, y0, y1, col=(170, 170, 165)):\n y = y0\n while y < y1:\n d.line((x, y, x, min(y1, y + 22)), fill=col, width=2); y += 30\n d.line((x, y, x, min(y1, y + 3)), fill=col, width=2); y += 10\n\ndef circle_img(c, im, cx, cy, r, border=5):\n tile = ImageOps.fit(im, (2 * r, 2 * r), Image.LANCZOS)\n m = Image.new('L', tile.size, 0); ImageDraw.Draw(m).ellipse((0, 0, 2 * r - 1, 2 * r - 1), fill=255)\n c.paste(tile, (cx - r, cy - r), m)\n ImageDraw.Draw(c).ellipse((cx - r, cy - r, cx + r, cy + r), outline=INK, width=border)\n\n# ============================================================ I-a three-view design sheet\ndef illust_threeview():\n c = Image.new('RGB', (W, H), CREAM); grid(c); d = ImageDraw.Draw(c)\n h = 300; ground = 728\n xs = {'front': 760, 'back': 1060, 'side': 1360}\n for n, cx in xs.items():\n v = scaled(view(n), h)\n c.paste(v, (cx - v.width // 2, ground - h), v)\n dashdot(d, cx, ground - h - 6, ground + 6)\n d.line((560, ground, 1520, ground), fill=INK, width=3)\n fv = scaled(view('front'), h)\n arrow_dim(d, xs['front'] - fv.width // 2 - 40, ground - h, ground)\n sv = scaled(view('side'), h)\n hdim(d, xs['side'] - sv.width // 2, xs['side'] + sv.width // 2, ground - h - 2 + 0, ORANGE)\n # detail circles: face (left) and side ring (right)\n face = sheet.crop((100, 425, 300, 560))\n circle_img(c, face, 520, 576, 118)\n d.line((520 + 118, 576, xs['front'] - 40, 500), fill=ORANGE, width=3)\n ringc = sheet.crop((474, 583, 634, 743))\n circle_img(c, ringc, 1580, 576, 118)\n d.line((1580 - 118, 576, 1388, 610), fill=ORANGE, width=3)\n # dots at the far edges\n for gx in range(60, 330, 22):\n for gy in range(460, 700, 22):\n d.ellipse((gx - 3, gy - 3, gx + 3, gy + 3), fill=(120, 120, 116))\n for gx in range(1740, 2000, 22):\n for gy in range(460, 700, 22):\n d.ellipse((gx - 3, gy - 3, gx + 3, gy + 3), fill=(120, 120, 116))\n return c\n\n# ============================================================ I-b corridor illustration (line + flat colour)\ndef illust_corridor():\n c = Image.new('RGB', (W, H), (230, 239, 236)); d = ImageDraw.Draw(c)\n floor = 712\n d.rectangle((0, floor, W, H), fill=(234, 228, 216)); d.line((0, floor, W, floor), fill=INK, width=4)\n d.line((0, 590, W, 590), fill=INK, width=3); d.line((0, 600, W, 600), fill=INK, width=3)\n for i, x in enumerate(range(-80, W, 470)):\n d.rectangle((x, 330, x + 200, floor), fill=(206, 224, 219), outline=INK, width=4)\n d.rectangle((x + 60, 380, x + 140, 480), fill=(246, 250, 249), outline=INK, width=3)\n d.rounded_rectangle((x + 160, 540, x + 184, 556), radius=4, fill=INK)\n d.rectangle((x + 70, 300, x + 130, 318), fill=ORANGE, outline=INK, width=3)\n for x in (300, 1530):\n d.rounded_rectangle((x, 640, x + 64, floor), radius=6, fill=(214, 172, 128), outline=INK, width=3)\n for a, rr in ((-35, 50), (0, 66), (35, 50)):\n ex = x + 32 + math.sin(math.radians(a)) * rr; ey = 640 - math.cos(math.radians(a)) * rr\n d.ellipse((ex - 24, ey - 38, ex + 24, ey + 38), fill=(132, 190, 150), outline=INK, width=3)\n for x in range(40, W, 110):\n d.line((x, floor + 30, x + 50, floor + 30), fill=(210, 202, 188), width=4)\n # Hakobu (side view, moving left) + motion lines behind\n v = scaled(view('side'), 296)\n hx = 1000; c.paste(v, (hx - v.width // 2, floor + 10 - v.height), v)\n d = ImageDraw.Draw(c)\n for k, yy_ in enumerate((470, 530, 600, 660)):\n x0 = hx + v.width // 2 + 30 + (k % 2) * 30\n d.line((x0, yy_, x0 + 120 - k * 15, yy_), fill=INK, width=4)\n # a second Hakobu waiting at the far door (front view, small)\n f = scaled(view('front'), 170)\n c.paste(f, (1340, floor + 4 - f.height), f)\n return c\n\n# ============================================================ M-a manga page, slanted panels\ndef manga_page():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 12):\n for x in range(0, W, 12):\n d.ellipse((x - 1.6, y - 1.6, x + 1.6, y + 1.6), fill=(212, 212, 210))\n top, bot, sl = 418, 734, 36\n cuts = [-40, 392, 790, 1258, 1656, 2090]\n imgs = [manga_crop(3, (0, 40, 561, 470)), manga_crop(0, (0, 60, 561, 536)), None,\n manga_crop(1, (250, 0, 561, 400)), manga_crop(2, (100, 0, 561, 400))]\n for i in range(5):\n xl, xr = cuts[i] + 8, cuts[i + 1] - 8\n poly = [(xl + sl, top), (xr + sl, top), (xr - sl, bot), (xl - sl, bot)]\n bw, bh = xr - xl + 2 * sl, bot - top\n if imgs[i] is None: # centre: Hakobu front with focus lines\n tile = Image.new('RGB', (bw, bh), (255, 255, 255)); td = ImageDraw.Draw(tile)\n cx0, cy0 = bw / 2, bh * 0.45\n for a in range(0, 360, 5):\n ca, sa = math.cos(math.radians(a)), math.sin(math.radians(a))\n r0 = 150 + (a * 7 % 40)\n td.line((cx0 + ca * r0, cy0 + sa * r0, cx0 + ca * 600, cy0 + sa * 600), fill=INK, width=2)\n v = scaled(view('front'), bh - 26)\n tile.paste(v, (int(cx0 - v.width / 2), bh - v.height - 4), v)\n else:\n tile = ImageOps.fit(imgs[i], (bw, bh), Image.LANCZOS, centering=(0.5, 0.4))\n m = Image.new('L', (W, H), 0); ImageDraw.Draw(m).polygon(poly, fill=255)\n layer = Image.new('RGB', (W, H)); layer.paste(tile, (xl - sl, top))\n c.paste(layer, (0, 0), m)\n ImageDraw.Draw(c).polygon(poly, outline=INK, width=5)\n return c\n\n# ============================================================ M-b cast introduction\ndef manga_cast():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 11):\n for x in range(0, W, 11):\n rr = 2.2 if (x // 11 + y // 11) % 2 == 0 else 1.2\n d.ellipse((x - rr, y - rr, x + rr, y + rr), fill=(218, 218, 216))\n cy = 578\n # starburst behind Hakobu\n pts = []\n for k in range(36):\n a = math.radians(k * 10); rr = 180 if k % 2 == 0 else 128\n pts.append((1024 + math.cos(a) * rr * 0.95, cy + math.sin(a) * rr))\n d.polygon(pts, fill=(255, 255, 255), outline=INK, width=4)\n v = scaled(view('front'), 316)\n c.paste(v, (1024 - v.width // 2, 736 - v.height), v)\n # cast circles\n cast = [(520, manga_crop(1, (300, 0, 540, 240))), # nurse\n (770, manga_crop(0, (360, 40, 561, 241))), # engineer\n (1278, manga_crop(2, (160, 20, 380, 240))), # arm robot with a face\n (1528, manga_crop(3, (40, 20, 561, 541)))] # the twin arms\n for x, im in cast:\n circle_img(c, im, x, cy, 112, 5)\n d = ImageDraw.Draw(c)\n # speed lines at both ends\n for k in range(22):\n yy_ = 440 + k * 13; ln = 80 + (k * 37 % 5) * 40\n d.line((1680, yy_, 1680 + ln, yy_), fill=INK, width=3)\n d.line((368 - ln, yy_, 368, yy_), fill=INK, width=3)\n return c\n\nitems = [('I1_threeview', illust_threeview), ('I2_corridor', illust_corridor),\n ('M1_manga_page', manga_page), ('M2_cast', manga_cast)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'hakobu_banner_{name}_2048x1152.jpg', quality=92, subsampling=0)\n im.crop(BAND).save(OUT + f'_band_{name}.png')\n print(name)</code></pre><h3 id=\"index_fHfez7_y\">make5.py(5回目の3案)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps\n\n_src = open('/home/claude/banner/make3.py').read()\nexec(_src[:_src.index(\"items = [\")]) # reuse frame(), to_manga(), constants\n\nOUT = '/home/claude/banner/hakobu3/'\nos.makedirs(OUT, exist_ok=True)\nSITE = '/home/claude/banner/site/'\nDARK = (40, 40, 42); BASE = (205, 201, 192); BASE2 = (184, 180, 171)\nSHELL = (247, 246, 242); SHELL2 = (226, 223, 215); SHELL_L = (251, 250, 247); SEAM = (214, 211, 203)\n\n# ------------------------------------------------------------ Hakobu redrawn from the three-view sheet\nclass Pen:\n \"\"\"maps three-view sheet coordinates to the canvas\"\"\"\n def __init__(self, img, x0, y0, ox, oy, s, line=None, lw=0):\n self.d = ImageDraw.Draw(img); self.x0, self.y0, self.ox, self.oy, self.s = x0, y0, ox, oy, s\n self.line, self.lw = line, lw\n def p(self, x, y): return (self.ox + (x - self.x0) * self.s, self.oy + (y - self.y0) * self.s)\n def box(self, b): a = self.p(b[0], b[1]); c = self.p(b[2], b[3]); return (a[0], a[1], c[0], c[1])\n def rr(self, b, r, fill, outline=True):\n self.d.rounded_rectangle(self.box(b), radius=r * self.s, fill=fill,\n outline=self.line if (outline and self.line) else None, width=int(self.lw))\n def poly(self, pts, fill, outline=True):\n self.d.polygon([self.p(*q) for q in pts], fill=fill, outline=self.line if (outline and self.line) else None,\n width=int(self.lw) if self.line else 1)\n def ell(self, b, fill, outline=True):\n self.d.ellipse(self.box(b), fill=fill, outline=self.line if (outline and self.line) else None, width=int(self.lw))\n def ln(self, a, b, fill, w):\n self.d.line((self.p(*a), self.p(*b)), fill=fill, width=max(1, int(w * self.s)))\n\ndef hakobu_front(img, cx, ground, h, line=None, lw=0, expr='smile'):\n s = h / 461.0\n pen = Pen(img, 200, 874, cx, ground, s, line, lw)\n seam = line or SEAM\n pen.rr((95, 836, 120, 874), 6, DARK); pen.rr((280, 836, 305, 874), 6, DARK) # wheels\n pen.rr((72, 806, 328, 848), 12, BASE) # base\n pen.rr((76, 505, 324, 816), 26, SHELL) # body\n pen.rr((78, 525, 98, 800), 8, SHELL2, False); pen.rr((302, 525, 322, 800), 8, SHELL2, False)\n pen.rr((100, 413, 300, 566), 34, SHELL) # head\n pen.rr((112, 546, 288, 566), 10, SHELL2, False)\n pen.rr((120, 427, 283, 541), 12, DISP) # display\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gp = Pen(glow, 200, 874, cx, ground, s)\n for ex in (152, 241):\n gp.rr((ex - 17, 460, ex + 17, 512), 17, (246, 248, 252, 110), False)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(4 * s)))\n pen.d = ImageDraw.Draw(img)\n for ex in (152, 241):\n if expr == 'blink':\n pen.rr((ex - 13, 484, ex + 13, 490), 3, EYE, False)\n else:\n pen.rr((ex - 11, 466, ex + 11, 506), 11, EYE, False)\n pen.d.chord(pen.box((191, 484, 217, 510)), 0, 180, fill=MOUTH) # mouth\n pen.ln((80, 588), (320, 588), seam, 2.2)\n pen.rr((167, 606, 225, 630), 11, DARK, False) # drawer slot\n pen.d.arc(pen.box((78, 668, 322, 730)), 15, 165, fill=seam, width=max(1, int(2.2 * s)))\n pen.rr((78, 767, 322, 786), 6, DARK, False) # black stripe\n pen.rr((177, 796, 224, 810), 7, DARK, False)\n\ndef hakobu_side(img, cx, ground, h, line=None, lw=0):\n \"\"\"side view, front of the robot on the left\"\"\"\n s = h / 461.0\n pen = Pen(img, 548, 874, cx, ground, s, line, lw)\n pen.ell((426, 830, 472, 876), DARK); pen.ell((638, 830, 684, 876), DARK) # wheels\n pen.rr((404, 798, 692, 862), 18, BASE)\n pen.rr((468, 808, 626, 860), 14, BASE2, False)\n pen.poly([(430, 505), (418, 560), (410, 700), (404, 805), (692, 805), (684, 515), (656, 505)], SHELL)\n pen.poly([(455, 418), (628, 418), (650, 432), (657, 505), (430, 505)], SHELL) # head\n pen.poly([(442, 464), (654, 464), (657, 505), (431, 505)], SHELL2, False)\n pen.rr((413, 596, 441, 720), 8, SHELL2, False) # front strip\n pen.rr((446, 526, 672, 800), 70, SHELL_L) # side panel\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gp = Pen(glow, 548, 874, cx, ground, s)\n gp.ell((554 - 74, 663 - 74, 554 + 74, 663 + 74), RING + (90,), False)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(10 * s)))\n pen.d = ImageDraw.Draw(img)\n pen.ell((554 - 62, 663 - 62, 554 + 62, 663 + 62), RING) # ring\n pen.ell((554 - 50, 663 - 50, 554 + 50, 663 + 50), SHELL_L)\n pen.rr((408, 768, 456, 790), 8, DARK) # bumper\n pen.rr((410, 790, 452, 800), 4, (200, 200, 198), False)\n\n# ------------------------------------------------------------ I-flat: flat corridor (no outlines)\ndef corridor_flat():\n S = SS; img = ss_canvas((228, 238, 235)); d = ImageDraw.Draw(img)\n floor = 700 * S\n d.rectangle((0, floor, W * S, H * S), fill=(214, 207, 194))\n d.rectangle((0, floor - 6 * S, W * S, floor), fill=(196, 208, 204))\n d.rectangle((0, 560 * S, W * S, 568 * S), fill=(180, 196, 192))\n for x in range(0, W, 520):\n dx = (x + 140) * S\n d.rounded_rectangle((dx, 300 * S, dx + 190 * S, floor - 6 * S), radius=10 * S, fill=(201, 219, 214))\n d.rounded_rectangle((dx + 55 * S, 360 * S, dx + 135 * S, 470 * S), radius=8 * S, fill=(237, 245, 243))\n d.rectangle((dx + 150 * S, 520 * S, dx + 170 * S, 530 * S), fill=(150, 168, 164))\n d.rounded_rectangle((dx + 60 * S, 270 * S, dx + 130 * S, 290 * S), radius=4 * S, fill=ORANGE)\n for x in (400, 1460):\n px = x * S\n d.rounded_rectangle((px, 640 * S, px + 60 * S, floor), radius=6 * S, fill=(196, 160, 120))\n for a, rr_ in ((-30, 46), (0, 60), (30, 46), (-60, 36), (60, 36)):\n ex = px + 30 * S + math.sin(math.radians(a)) * rr_ * S; ey = 640 * S - math.cos(math.radians(a)) * rr_ * S\n d.ellipse((ex - 22 * S, ey - 34 * S, ex + 22 * S, ey + 34 * S), fill=(110, 170, 130))\n for x in range(0, W, 90):\n d.line(((x + 30) * S, floor + 40 * S, (x + 70) * S, floor + 40 * S), fill=(226, 220, 208), width=4 * S)\n # moving Hakobu (side) + motion lines, and one waiting at a door (front)\n hakobu_side(img, 660 * S, 706 * S, 210 * S)\n d = ImageDraw.Draw(img)\n for k, yy_ in enumerate((540, 590, 640)):\n x0 = (762 + (k % 2) * 22) * S\n d.rounded_rectangle((x0, yy_ * S, x0 + (100 - k * 14) * S, (yy_ + 8) * S), radius=4 * S, fill=(176, 194, 190))\n hakobu_front(img, 1080 * S, 730 * S, 300 * S)\n return finish(img)\n\n# ------------------------------------------------------------ I-line: line + flat colour corridor\ndef corridor_line():\n S = SS; img = ss_canvas((230, 239, 236)); d = ImageDraw.Draw(img); L = 4 * S\n floor = 712 * S\n d.rectangle((0, floor, W * S, H * S), fill=(234, 228, 216)); d.line((0, floor, W * S, floor), fill=INK, width=L)\n d.line((0, 590 * S, W * S, 590 * S), fill=INK, width=3 * S); d.line((0, 600 * S, W * S, 600 * S), fill=INK, width=3 * S)\n for x in range(-80, W, 470):\n d.rectangle((x * S, 330 * S, (x + 200) * S, floor), fill=(206, 224, 219), outline=INK, width=L)\n d.rectangle(((x + 60) * S, 380 * S, (x + 140) * S, 480 * S), fill=(246, 250, 249), outline=INK, width=3 * S)\n d.rounded_rectangle(((x + 160) * S, 540 * S, (x + 184) * S, 556 * S), radius=4 * S, fill=INK)\n d.rectangle(((x + 70) * S, 300 * S, (x + 130) * S, 318 * S), fill=ORANGE, outline=INK, width=3 * S)\n for x in (300, 1530):\n d.rounded_rectangle((x * S, 640 * S, (x + 64) * S, floor), radius=6 * S, fill=(214, 172, 128), outline=INK, width=3 * S)\n for a, rr_ in ((-35, 50), (0, 66), (35, 50)):\n ex = (x + 32 + math.sin(math.radians(a)) * rr_) * S; ey = (640 - math.cos(math.radians(a)) * rr_) * S\n d.ellipse((ex - 24 * S, ey - 38 * S, ex + 24 * S, ey + 38 * S), fill=(132, 190, 150), outline=INK, width=3 * S)\n for x in range(40, W, 110):\n d.line(((x) * S, (floor + 30 * S), (x + 50) * S, (floor + 30 * S)), fill=(210, 202, 188), width=4 * S)\n hakobu_side(img, 660 * S, 714 * S, 210 * S, line=INK, lw=3 * S)\n d = ImageDraw.Draw(img)\n for k, yy_ in enumerate((545, 600, 655)):\n x0 = (770 + (k % 2) * 24) * S\n d.line((x0, yy_ * S, x0 + (100 - k * 14) * S, yy_ * S), fill=INK, width=L)\n hakobu_front(img, 1080 * S, 734 * S, 300 * S, line=INK, lw=3.5 * S)\n return finish(img)\n\ndef ink_manga(im):\n g = ImageOps.autocontrast(ImageOps.grayscale(im), cutoff=2)\n a = np.array(g).astype(int)\n yy_, xx_ = np.mgrid[0:a.shape[0], 0:a.shape[1]]\n dots = ((xx_ % 7 - 3) ** 2 + (yy_ % 7 - 3) ** 2) <= 3\n out = np.full(a.shape, 255, np.uint8)\n out[(a < 205) & dots] = 0\n out[a < 120] = 0\n return Image.fromarray(out).convert('RGB')\n\n# ------------------------------------------------------------ manga: v1 touch, different characters per panel\ndef manga_cast_strip():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 16):\n for x in range(0, W, 16):\n dist = abs(x - W / 2) / (W / 2); r = 3.2 * max(0, dist - 0.35) / 0.65\n if r > 0.4: d.ellipse((x - r, y - r, x + r, y + r), fill=(200, 200, 198))\n mg = lambda n, b: Image.open(SITE + f'c_manga{n}.png').convert('RGB').crop(b)\n panels = [\n (frame(1).crop((0, 730, 380, 1080)), 'none', (0.5, 0.15)), # Hakobu\n (mg(1, (290, 0, 545, 330)), 'surprise', (0.5, 0.1)), # nurse\n (mg(0, (330, 30, 561, 330)), 'sweat', (0.5, 0.1)), # engineer\n (mg(2, (170, 0, 400, 270)), 'joy', (0.5, 0.5)), # arm robot with a face\n ]\n pw, ph, gap = 290, 300, 18\n x = (W - (4 * pw + 3 * gap)) // 2; y = (BAND[1] + BAND[3]) // 2 - ph // 2\n for i, (im, fx, cen) in enumerate(panels):\n big = ImageOps.fit(im, (pw, ph), Image.LANCZOS, centering=cen)\n panel = to_manga(big) if i == 0 else ink_manga(big)\n pd = ImageDraw.Draw(panel)\n if fx == 'surprise':\n for a in range(0, 360, 9):\n ca, sa = math.cos(math.radians(a)), math.sin(math.radians(a))\n pd.line((pw / 2 + ca * 165, ph * 0.3 + sa * 165, pw / 2 + ca * 260, ph * 0.3 + sa * 260), fill=INK, width=2)\n pd.polygon([(30, 20), (48, 20), (43, 70), (35, 70)], fill=INK); pd.ellipse((32, 78, 46, 92), fill=INK)\n if fx == 'sweat':\n pd.polygon([(40, 34), (28, 64), (52, 64)], fill=(255, 255, 255), outline=INK)\n pd.ellipse((26, 52, 54, 80), fill=(255, 255, 255), outline=INK, width=2)\n if fx == 'joy':\n for sx, sy, s in ((40, 40, 18), (255, 60, 14), (60, 120, 10)):\n pd.polygon([(sx, sy - s), (sx + s * 0.25, sy - s * 0.25), (sx + s, sy), (sx + s * 0.25, sy + s * 0.25),\n (sx, sy + s), (sx - s * 0.25, sy + s * 0.25), (sx - s, sy), (sx - s * 0.25, sy - s * 0.25)], fill=INK)\n c.paste(panel, (x, y))\n d.rectangle((x - 4, y - 4, x + pw + 3, y + ph + 3), outline=INK, width=5)\n x += pw + gap\n x0 = (W - (4 * pw + 3 * gap)) // 2\n for k in range(24):\n yy_ = y + 10 + k * (ph - 20) / 23; ln = 90 + (k * 37 % 5) * 45\n d.line((x + 20, yy_, x + 20 + ln, yy_), fill=INK, width=3)\n d.line((x0 - 40 - ln, yy_, x0 - 40, yy_), fill=INK, width=3)\n return c\n\nitems = [('illust_flat', corridor_flat), ('illust_line', corridor_line), ('manga_cast', manga_cast_strip)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'hakobu_banner_{name}_2048x1152.jpg', quality=92, subsampling=0)\n im.crop(BAND).save(OUT + f'_band_{name}.png')\n print(name)</code></pre><h3 id=\"index_sTlRYvOF\">make5_single.py(決定版のバナー)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps\n\n_src = open('/home/claude/banner/make3.py').read()\nexec(_src[:_src.index(\"items = [\")]) # reuse frame(), to_manga(), constants\n\nOUT = '/home/claude/banner/hakobu3/'\nos.makedirs(OUT, exist_ok=True)\nSITE = '/home/claude/banner/site/'\nDARK = (40, 40, 42); BASE = (205, 201, 192); BASE2 = (184, 180, 171)\nSHELL = (247, 246, 242); SHELL2 = (226, 223, 215); SHELL_L = (251, 250, 247); SEAM = (214, 211, 203)\n\n# ------------------------------------------------------------ Hakobu redrawn from the three-view sheet\nclass Pen:\n \"\"\"maps three-view sheet coordinates to the canvas\"\"\"\n def __init__(self, img, x0, y0, ox, oy, s, line=None, lw=0):\n self.d = ImageDraw.Draw(img); self.x0, self.y0, self.ox, self.oy, self.s = x0, y0, ox, oy, s\n self.line, self.lw = line, lw\n def p(self, x, y): return (self.ox + (x - self.x0) * self.s, self.oy + (y - self.y0) * self.s)\n def box(self, b): a = self.p(b[0], b[1]); c = self.p(b[2], b[3]); return (a[0], a[1], c[0], c[1])\n def rr(self, b, r, fill, outline=True):\n self.d.rounded_rectangle(self.box(b), radius=r * self.s, fill=fill,\n outline=self.line if (outline and self.line) else None, width=int(self.lw))\n def poly(self, pts, fill, outline=True):\n self.d.polygon([self.p(*q) for q in pts], fill=fill, outline=self.line if (outline and self.line) else None,\n width=int(self.lw) if self.line else 1)\n def ell(self, b, fill, outline=True):\n self.d.ellipse(self.box(b), fill=fill, outline=self.line if (outline and self.line) else None, width=int(self.lw))\n def ln(self, a, b, fill, w):\n self.d.line((self.p(*a), self.p(*b)), fill=fill, width=max(1, int(w * self.s)))\n\ndef hakobu_front(img, cx, ground, h, line=None, lw=0, expr='smile'):\n s = h / 461.0\n pen = Pen(img, 200, 874, cx, ground, s, line, lw)\n seam = line or SEAM\n pen.rr((95, 836, 120, 874), 6, DARK); pen.rr((280, 836, 305, 874), 6, DARK) # wheels\n pen.rr((72, 806, 328, 848), 12, BASE) # base\n pen.rr((76, 505, 324, 816), 26, SHELL) # body\n pen.rr((78, 525, 98, 800), 8, SHELL2, False); pen.rr((302, 525, 322, 800), 8, SHELL2, False)\n pen.rr((100, 413, 300, 566), 34, SHELL) # head\n pen.rr((112, 546, 288, 566), 10, SHELL2, False)\n pen.rr((120, 427, 283, 541), 12, DISP) # display\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gp = Pen(glow, 200, 874, cx, ground, s)\n for ex in (152, 241):\n gp.rr((ex - 17, 460, ex + 17, 512), 17, (246, 248, 252, 110), False)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(4 * s)))\n pen.d = ImageDraw.Draw(img)\n for ex in (152, 241):\n if expr == 'blink':\n pen.rr((ex - 13, 484, ex + 13, 490), 3, EYE, False)\n else:\n pen.rr((ex - 11, 466, ex + 11, 506), 11, EYE, False)\n pen.d.chord(pen.box((191, 484, 217, 510)), 0, 180, fill=MOUTH) # mouth\n pen.ln((80, 588), (320, 588), seam, 2.2)\n pen.rr((167, 606, 225, 630), 11, DARK, False) # drawer slot\n pen.d.arc(pen.box((78, 668, 322, 730)), 15, 165, fill=seam, width=max(1, int(2.2 * s)))\n pen.rr((78, 767, 322, 786), 6, DARK, False) # black stripe\n pen.rr((177, 796, 224, 810), 7, DARK, False)\n\ndef hakobu_side(img, cx, ground, h, line=None, lw=0):\n \"\"\"side view, front of the robot on the left\"\"\"\n s = h / 461.0\n pen = Pen(img, 548, 874, cx, ground, s, line, lw)\n pen.ell((426, 830, 472, 876), DARK); pen.ell((638, 830, 684, 876), DARK) # wheels\n pen.rr((404, 798, 692, 862), 18, BASE)\n pen.rr((468, 808, 626, 860), 14, BASE2, False)\n pen.poly([(430, 505), (418, 560), (410, 700), (404, 805), (692, 805), (684, 515), (656, 505)], SHELL)\n pen.poly([(455, 418), (628, 418), (650, 432), (657, 505), (430, 505)], SHELL) # head\n pen.poly([(442, 464), (654, 464), (657, 505), (431, 505)], SHELL2, False)\n pen.rr((413, 596, 441, 720), 8, SHELL2, False) # front strip\n pen.rr((446, 526, 672, 800), 70, SHELL_L) # side panel\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gp = Pen(glow, 548, 874, cx, ground, s)\n gp.ell((554 - 74, 663 - 74, 554 + 74, 663 + 74), RING + (90,), False)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(10 * s)))\n pen.d = ImageDraw.Draw(img)\n pen.ell((554 - 62, 663 - 62, 554 + 62, 663 + 62), RING) # ring\n pen.ell((554 - 50, 663 - 50, 554 + 50, 663 + 50), SHELL_L)\n pen.rr((408, 768, 456, 790), 8, DARK) # bumper\n pen.rr((410, 790, 452, 800), 4, (200, 200, 198), False)\n\n# ------------------------------------------------------------ I-flat: flat corridor (no outlines)\ndef corridor_flat():\n S = SS; img = ss_canvas((228, 238, 235)); d = ImageDraw.Draw(img)\n floor = 700 * S\n d.rectangle((0, floor, W * S, H * S), fill=(214, 207, 194))\n d.rectangle((0, floor - 6 * S, W * S, floor), fill=(196, 208, 204))\n d.rectangle((0, 560 * S, W * S, 568 * S), fill=(180, 196, 192))\n for x in range(0, W, 520):\n dx = (x + 140) * S\n d.rounded_rectangle((dx, 300 * S, dx + 190 * S, floor - 6 * S), radius=10 * S, fill=(201, 219, 214))\n d.rounded_rectangle((dx + 55 * S, 360 * S, dx + 135 * S, 470 * S), radius=8 * S, fill=(237, 245, 243))\n d.rectangle((dx + 150 * S, 520 * S, dx + 170 * S, 530 * S), fill=(150, 168, 164))\n d.rounded_rectangle((dx + 60 * S, 270 * S, dx + 130 * S, 290 * S), radius=4 * S, fill=ORANGE)\n for x in (400, 1460):\n px = x * S\n d.rounded_rectangle((px, 640 * S, px + 60 * S, floor), radius=6 * S, fill=(196, 160, 120))\n for a, rr_ in ((-30, 46), (0, 60), (30, 46), (-60, 36), (60, 36)):\n ex = px + 30 * S + math.sin(math.radians(a)) * rr_ * S; ey = 640 * S - math.cos(math.radians(a)) * rr_ * S\n d.ellipse((ex - 22 * S, ey - 34 * S, ex + 22 * S, ey + 34 * S), fill=(110, 170, 130))\n for x in range(0, W, 90):\n d.line(((x + 30) * S, floor + 40 * S, (x + 70) * S, floor + 40 * S), fill=(226, 220, 208), width=4 * S)\n # moving Hakobu (side) + motion lines, and one waiting at a door (front)\n hakobu_front(img, 1015 * S, 730 * S, 300 * S)\n return finish(img)\n\n# ------------------------------------------------------------ I-line: line + flat colour corridor\ndef corridor_line():\n S = SS; img = ss_canvas((230, 239, 236)); d = ImageDraw.Draw(img); L = 4 * S\n floor = 712 * S\n d.rectangle((0, floor, W * S, H * S), fill=(234, 228, 216)); d.line((0, floor, W * S, floor), fill=INK, width=L)\n d.line((0, 590 * S, W * S, 590 * S), fill=INK, width=3 * S); d.line((0, 600 * S, W * S, 600 * S), fill=INK, width=3 * S)\n for x in range(-80, W, 470):\n d.rectangle((x * S, 330 * S, (x + 200) * S, floor), fill=(206, 224, 219), outline=INK, width=L)\n d.rectangle(((x + 60) * S, 380 * S, (x + 140) * S, 480 * S), fill=(246, 250, 249), outline=INK, width=3 * S)\n d.rounded_rectangle(((x + 160) * S, 540 * S, (x + 184) * S, 556 * S), radius=4 * S, fill=INK)\n d.rectangle(((x + 70) * S, 300 * S, (x + 130) * S, 318 * S), fill=ORANGE, outline=INK, width=3 * S)\n for x in (300, 1530):\n d.rounded_rectangle((x * S, 640 * S, (x + 64) * S, floor), radius=6 * S, fill=(214, 172, 128), outline=INK, width=3 * S)\n for a, rr_ in ((-35, 50), (0, 66), (35, 50)):\n ex = (x + 32 + math.sin(math.radians(a)) * rr_) * S; ey = (640 - math.cos(math.radians(a)) * rr_) * S\n d.ellipse((ex - 24 * S, ey - 38 * S, ex + 24 * S, ey + 38 * S), fill=(132, 190, 150), outline=INK, width=3 * S)\n for x in range(40, W, 110):\n d.line(((x) * S, (floor + 30 * S), (x + 50) * S, (floor + 30 * S)), fill=(210, 202, 188), width=4 * S)\n hakobu_side(img, 660 * S, 714 * S, 210 * S, line=INK, lw=3 * S)\n d = ImageDraw.Draw(img)\n for k, yy_ in enumerate((545, 600, 655)):\n x0 = (770 + (k % 2) * 24) * S\n d.line((x0, yy_ * S, x0 + (100 - k * 14) * S, yy_ * S), fill=INK, width=L)\n hakobu_front(img, 1080 * S, 734 * S, 300 * S, line=INK, lw=3.5 * S)\n return finish(img)\n\ndef ink_manga(im):\n g = ImageOps.autocontrast(ImageOps.grayscale(im), cutoff=2)\n a = np.array(g).astype(int)\n yy_, xx_ = np.mgrid[0:a.shape[0], 0:a.shape[1]]\n dots = ((xx_ % 7 - 3) ** 2 + (yy_ % 7 - 3) ** 2) <= 3\n out = np.full(a.shape, 255, np.uint8)\n out[(a < 205) & dots] = 0\n out[a < 120] = 0\n return Image.fromarray(out).convert('RGB')\n\n# ------------------------------------------------------------ manga: v1 touch, different characters per panel\ndef manga_cast_strip():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 16):\n for x in range(0, W, 16):\n dist = abs(x - W / 2) / (W / 2); r = 3.2 * max(0, dist - 0.35) / 0.65\n if r > 0.4: d.ellipse((x - r, y - r, x + r, y + r), fill=(200, 200, 198))\n mg = lambda n, b: Image.open(SITE + f'c_manga{n}.png').convert('RGB').crop(b)\n panels = [\n (frame(1).crop((0, 730, 380, 1080)), 'none', (0.5, 0.15)), # Hakobu\n (mg(1, (290, 0, 545, 330)), 'surprise', (0.5, 0.1)), # nurse\n (mg(0, (330, 30, 561, 330)), 'sweat', (0.5, 0.1)), # engineer\n (mg(2, (170, 0, 400, 270)), 'joy', (0.5, 0.5)), # arm robot with a face\n ]\n pw, ph, gap = 290, 300, 18\n x = (W - (4 * pw + 3 * gap)) // 2; y = (BAND[1] + BAND[3]) // 2 - ph // 2\n for i, (im, fx, cen) in enumerate(panels):\n big = ImageOps.fit(im, (pw, ph), Image.LANCZOS, centering=cen)\n panel = to_manga(big) if i == 0 else ink_manga(big)\n pd = ImageDraw.Draw(panel)\n if fx == 'surprise':\n for a in range(0, 360, 9):\n ca, sa = math.cos(math.radians(a)), math.sin(math.radians(a))\n pd.line((pw / 2 + ca * 165, ph * 0.3 + sa * 165, pw / 2 + ca * 260, ph * 0.3 + sa * 260), fill=INK, width=2)\n pd.polygon([(30, 20), (48, 20), (43, 70), (35, 70)], fill=INK); pd.ellipse((32, 78, 46, 92), fill=INK)\n if fx == 'sweat':\n pd.polygon([(40, 34), (28, 64), (52, 64)], fill=(255, 255, 255), outline=INK)\n pd.ellipse((26, 52, 54, 80), fill=(255, 255, 255), outline=INK, width=2)\n if fx == 'joy':\n for sx, sy, s in ((40, 40, 18), (255, 60, 14), (60, 120, 10)):\n pd.polygon([(sx, sy - s), (sx + s * 0.25, sy - s * 0.25), (sx + s, sy), (sx + s * 0.25, sy + s * 0.25),\n (sx, sy + s), (sx - s * 0.25, sy + s * 0.25), (sx - s, sy), (sx - s * 0.25, sy - s * 0.25)], fill=INK)\n c.paste(panel, (x, y))\n d.rectangle((x - 4, y - 4, x + pw + 3, y + ph + 3), outline=INK, width=5)\n x += pw + gap\n x0 = (W - (4 * pw + 3 * gap)) // 2\n for k in range(24):\n yy_ = y + 10 + k * (ph - 20) / 23; ln = 90 + (k * 37 % 5) * 45\n d.line((x + 20, yy_, x + 20 + ln, yy_), fill=INK, width=3)\n d.line((x0 - 40 - ln, yy_, x0 - 40, yy_), fill=INK, width=3)\n return c\n\nitems = [('illust_flat_single', corridor_flat)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'hakobu_banner_{name}_2048x1152.jpg', quality=92, subsampling=0)\n im.crop(BAND).save(OUT + f'_band_{name}.png')\n print(name)</code></pre><h2 id=\"index_NP8gCgQ3\">スクリプト以外に実行したコマンド</h2><h3 id=\"index_VpcG8_8h\">サイトの画面を保存したPDFから画像を取り出し、ページを縦につないだ</h3><pre><code class=\"language-bash\">cd /home/claude/banner/site && pdfimages -j \"/root/.claude/uploads/6f54b9ec-7b51-53dd-a180-2e8bdb7969c6/f328e762-screencapture-adaptdesign-jp-pixelstomotors-2026-10-12-07_46_26.pdf\" pg && convert pg-000.jpg pg-001.jpg pg-002.jpg -append full.jpg && identify full.jpg && for i in 0 1 2 3 4; do convert full.jpg -crop 2728x2100+0+$((i*2100)) -resize 1100x prev$i.jpg; done; ls</code></pre><h3 id=\"index_naTwarcA\">つないだ画像から、サムネイルや4コマを切り出した</h3><pre><code class=\"language-python\">from PIL import Image\nim=Image.open('full.jpg')\nk=1/0.4032\ndef c(name, x0,y0,x1,y1,page):\n box=(int(x0*k),int(page*2100+y0*k),int(x1*k),int(page*2100+y1*k))\n im.crop(box).save(f'c_{name}.png'); print(name, box, (box[2]-box[0],box[3]-box[1]))\nc('hero',670,86,936,283,0)\nfor i,(a,b) in enumerate([(62,288),(312,538),(562,788),(812,1038)]):\n c(f'manga{i}',a,512,b,728,0)\nc('drug',62,158,535,481,1)\nc('lab',565,158,1038,481,1)\nc('imgrec_top',62,598,535,846,1)\nc('deep_top',565,598,1038,846,1)\nc('sim',62,245,535,533,2)\nc('flow',565,210,1038,533,2)\nc('rec_study',62,0,368,116,3); c('rec_micro',397,0,703,116,3); c('rec_say',731,0,1038,116,3)\nc('bowl',62,254,368,425,3); c('gpu',397,254,703,425,3); c('film',731,254,1038,425,3)\nc('panels324',62,605,368,775,3); c('panels323',397,605,703,775,3); c('who03',731,605,1038,775,3)\nc('inside',62,65,368,236,4); c('panels320',397,65,703,236,4); c('panels319',731,65,1038,236,4)\nc('issues',620,488,900,612,4)</code></pre><h3 id=\"index_egF7Khp0\">試作動画から、ハコブくんの表情のコマを書き出した</h3><pre><code class=\"language-bash\">cd /home/claude/banner/vid && V=\"/root/.claude/uploads/6f54b9ec-7b51-53dd-a180-2e8bdb7969c6/d2aa7563-hakobu_demo_upper.mp4\"; ffmpeg -v error -y -i \"$V\" -vf \"fps=3,crop=400:340:0:740\" h_%03d.png; ls h_*.png | wc -l; montage h_*.png -tile 10x -geometry 200x170+2+2 -label '%t' -pointsize 10 hsheet.jpg</code></pre><pre><code class=\"language-bash\">cd /home/claude/banner/vid && V=\"/root/.claude/uploads/6f54b9ec-7b51-53dd-a180-2e8bdb7969c6/d2aa7563-hakobu_demo_upper.mp4\"; for t in 0 3.0; do ffmpeg -v error -y -ss $t -i \"$V\" -frames:v 1 fr_$t.png; done</code></pre><pre><code class=\"language-bash\">cd /home/claude/banner/vid && V=\"/root/.claude/uploads/6f54b9ec-7b51-53dd-a180-2e8bdb7969c6/d2aa7563-hakobu_demo_upper.mp4\"; mkdir -p fx && for i in 1 4 8 9 15 24; do t=$(python3 -c \"print(($i-1)/3)\"); ffmpeg -v error -y -ss $t -i \"$V\" -frames:v 1 fx/e_$i.png; done; ls fx</code></pre><h3 id=\"index_hy_pvXuR\">動画のコマで、ハコブくんの範囲とディスプレイの位置を測った</h3><pre><code class=\"language-python\">from PIL import Image\nimport numpy as np\nim=Image.open('fr_0.png').convert('RGB')\na=np.array(im).astype(int)\nbg=np.array([245,245,240])\nreg=a[700:1080,0:420]\ndist=np.abs(reg-bg).sum(-1)\nys,xs=np.where(dist>18)\nprint('bbox',xs.min(),xs.max(),ys.min()+700,ys.max()+700)\nprint('corner color',a[720,5],a[1075,410])</code></pre><pre><code class=\"language-python\">from PIL import Image; import numpy as np\na=np.array(Image.open('fr_0.png').convert('RGB')).astype(int)[700:1080,0:420]\nys,xs=np.where(a.sum(-1)<90); print(xs.min(),xs.max(),ys.min()+700,ys.max()+700)</code></pre><h3 id=\"index_rLSvvOLQ\">三面図の3つの図の範囲、リングの中心と半径、口の位置を測った</h3><pre><code class=\"language-python\">from PIL import Image\nimport numpy as np\na=np.array(Image.open('src.png').convert('L')).astype(int)\n# column/row darkness profile to find view bboxes\ndark=a<120\ncols=dark[380:900].sum(0); rows=dark[:, :].sum(1)\nruns=[];inrun=False\nfor x,v in enumerate(cols):\n if v>3 and not inrun: s=x;inrun=True\n if v<=3 and inrun: runs.append((s,x));inrun=False\nprint([r for r in runs if r[1]-r[0]>20])\nfor (x0,x1) in [r for r in runs if r[1]-r[0]>20]:\n ys=np.where(dark[:,x0:x1].sum(1)>2)[0]; ys=ys[(ys>300)&(ys<1000)]; print(x0,x1,ys.min(),ys.max())\n# ring: find dark circle near side center</code></pre><pre><code class=\"language-python\">from PIL import Image\nimport numpy as np\na=np.array(Image.open('src.png').convert('L')).astype(float)\nbest=None\nfor cx in range(540,568,2):\n for cy in range(655,690,2):\n yy,xx=np.mgrid[cy-80:cy+80,cx-80:cx+80]; r=np.hypot(xx-cx,yy-cy); sub=a[cy-80:cy+80,cx-80:cx+80]\n prof=[sub[(r>=k)&(r<k+1)].mean() for k in range(30,75)]\n score=np.std(prof)\n sharp=sorted(prof)[:2]\n s=sum(sharp)\n if best is None or s<best[0]: best=(s,cx,cy,prof)\ns,cx,cy,prof=best; print(cx,cy); print([int(p) for p in prof])\n# mouth gray pixels\nsub=a[480:515,175:230]; ys,xs=np.where((sub>70)&(sub<200)); print('mouth', xs.min()+175,xs.max()+175,ys.min()+480,ys.max()+480)</code></pre><h3 id=\"index_kGmWoZfd\">2回目の全案を 2048×1152px で書き出した(1回目の写真は文字を外して作り直した)</h3><pre><code class=\"language-bash\">cd /home/claude/banner && python3 - <<'EOF'\nsrc=open('make.py').read()\nsrc=src[:src.index(\"import os\\nos.makedirs\")]\nexec(src)\ndef arm_clean():\n bg = Image.new('RGB', (W, H), MAT)\n s=0.74; subj=((1282+2650)/2, 1790)\n layer,_=place(bg, photo, s, subj, (1280, 730))\n ox=1280-subj[0]*s; oy=730-subj[1]*s\n mb=(ox+MAT_BOX[0]*s+70, oy+MAT_BOX[1]*s+70, ox+MAT_BOX[2]*s-70, oy+MAT_BOX[3]*s-70)\n bg.paste(ImageEnhance.Brightness(layer).enhance(0.95),(0,0),rect_mask(mb,60))\n return bg\nim=arm_clean(); im.save('/home/claude/banner/deliver/pixelstomotors_banner_A_arm_photo.jpg',quality=92,subsampling=0)\nEOF\nmkdir -p final_2048 && cd deliver && for f in pixelstomotors_banner_*.jpg; do n=${f%.jpg}; convert \"$f\" -resize 2048x1152! -quality 92 -sampling-factor 4:4:4 ../final_2048/${n}_2048x1152.jpg; done; cd ../final_2048 && identify -format \"%f %wx%h %b\\n\" *.jpg</code></pre><p></p><h2 id=\"index_dxzTRtXR\">実行ログ</h2><h3 id=\"index_6qQln2AW\">1回目の書き出し(make.py)</h3><pre><code>A_grasp 464 KB\nB_pixels_to_motors 341 KB\nC_see_decide_move 461 KB</code></pre><h3 id=\"index_fJsloe7H\">2回目の書き出し(make2.py)</h3><pre><code>B_archive_wall 769 KB\nC_mono_split 315 KB\nD_sim_halftone 1300 KB\nE_hakobu_manga 908 KB\nF_dot_matrix 669 KB\nG_hakobu_photo 170 KB</code></pre><h3 id=\"index_gljFeQAL\">サイトの画面から切り出した範囲</h3><pre><code>hero (1661, 213, 2321, 701) (660, 488)\nmanga0 (153, 1269, 714, 1805) (561, 536)\nmanga1 (773, 1269, 1334, 1805) (561, 536)\nmanga2 (1393, 1269, 1954, 1805) (561, 536)\nmanga3 (2013, 1269, 2574, 1805) (561, 536)\ndrug (153, 2491, 1326, 3292) (1173, 801)\nlab (1401, 2491, 2574, 3292) (1173, 801)\nimgrec_top (153, 3583, 1326, 4198) (1173, 615)\ndeep_top (1401, 3583, 2574, 4198) (1173, 615)\nsim (153, 4807, 1326, 5521) (1173, 714)\nflow (1401, 4720, 2574, 5521) (1173, 801)\nrec_study (153, 6300, 912, 6587) (759, 287)\nrec_micro (984, 6300, 1743, 6587) (759, 287)\nrec_say (1812, 6300, 2574, 6587) (762, 287)\nbowl (153, 6929, 912, 7354) (759, 425)\ngpu (984, 6929, 1743, 7354) (759, 425)\nfilm (1812, 6929, 2574, 7354) (762, 425)\npanels324 (153, 7800, 912, 8222) (759, 422)\npanels323 (984, 7800, 1743, 8222) (759, 422)\nwho03 (1812, 7800, 2574, 8222) (762, 422)\ninside (153, 8561, 912, 8985) (759, 424)\npanels320 (984, 8561, 1743, 8985) (759, 424)\npanels319 (1812, 8561, 2574, 8985) (762, 424)\nissues (1537, 9610, 2232, 9917) (695, 307)</code></pre><h3 id=\"index_JyBXovfG\">試作動画の情報と、ハコブくんの範囲</h3><pre><code>1920,1080,30/1,13.266667\n0/0,13.266000</code></pre><pre><code>bbox 29 351 766 1079\ncorner color [244 245 237] [244 245 237]</code></pre><pre><code>155 326 787 1022</code></pre><h3 id=\"index_GC_7inZM\">三面図の測定</h3><pre><code>[(70, 331), (396, 698), (749, 1012)]\n70 331 415 874\n396 698 419 874\n749 1012 420 875</code></pre><pre><code>554 663\n[250, 250, 249, 248, 247, 246, 245, 244, 242, 241, 240, 239, 238, 237, 235, 235, 234, 232, 225, 188, 140, 122, 140, 173, 179, 182, 188, 189, 177, 181, 174, 131, 114, 155, 194, 219, 227, 228, 229, 230, 230, 231, 232, 232, 233]\nmouth 184 215 490 505</code></pre><h3 id=\"index_htfRwQhT\">2048×1152px への書き出し</h3><pre><code>pixelstomotors_banner_A_arm_photo_2048x1152.jpg 2048x1152 478891B\npixelstomotors_banner_B_archive_wall_2048x1152.jpg 2048x1152 522786B\npixelstomotors_banner_C_mono_split_2048x1152.jpg 2048x1152 182721B\npixelstomotors_banner_D_sim_halftone_2048x1152.jpg 2048x1152 506840B\npixelstomotors_banner_E_hakobu_manga_2048x1152.jpg 2048x1152 506732B\npixelstomotors_banner_F_dot_matrix_2048x1152.jpg 2048x1152 481321B\npixelstomotors_banner_G_hakobu_photo_2048x1152.jpg 2048x1152 96050B</code></pre><h3 id=\"index_l3SpavP3\">3回目〜決定版の書き出し</h3><pre><code>1_illustration\n2_manga\n3_realistic\n4_photo\n5_graphic</code></pre><pre><code>I1_threeview\nI2_corridor\nM1_manga_page\nM2_cast</code></pre><pre><code>illust_flat\nillust_line\nmanga_cast</code></pre><pre><code>illust_flat_single</code></pre><p></p><h2 id=\"index_QUNQPzA5\">参照した情報</h2><ul><li><p><a href=\"https://support.google.com/youtube/answer/10456525?hl=ja\" data-has-link=\"true\">YouTube ヘルプ:チャンネルのブランディングを管理する</a></p></li><li><p><a href=\"https://support.google.com/youtube/answer/9481328?hl=ja\" data-has-link=\"true\">YouTube ヘルプ:チャンネルの権限</a></p></li><li><p><a href=\"/\" data-has-link=\"true\">Pixels to Motors</a></p></li></ul>", "toc": [{"id": "index__Ynzo3Cp", "text": "YouTube用のアカウントは、Google Workspaceのエイリアスではなく新しいユーザーにした"}, {"id": "index_8rfkxb9m", "text": "管理コンソールには、普段使いのアカウントでは入れなかった"}, {"id": "index_SZwLHwTg", "text": "「このユーザーは既に存在しています」の原因は、3月に付けたエイリアスだった"}, {"id": "index_EnRPO5Pq", "text": "作ろうとしたチャンネルのハンドルは、自分の既存チャンネルが使っていた"}, {"id": "index_04xwKQv5", "text": "既存チャンネルは、新しいアカウントを「管理者」として招待して引き継いだ"}, {"id": "index_6tAUffjk", "text": "チャンネルの説明文は、フィジカルAIを先頭に置いて書き直した"}, {"id": "index_GNMYvKmk", "text": "バナーは、どの端末でも切れない中央の範囲を基準に作った"}, {"id": "index_BpOCllXq", "text": "バナーの1回目は、実機の写真に文字を入れた3案だった"}, {"id": "index_KWVQksqt", "text": "2回目は、サイトの画像から6つの方向を出した"}, {"id": "index_wfU0AzRw", "text": "アイコンは、動画で話しているハコブくんの1コマから作った"}, {"id": "index_BZlJ8lML", "text": "バナーの3回目は、ハコブくんだけで5つのタッチを作った"}, {"id": "index_aoA9mNR9", "text": "5回目で、ハコブくんを背景と同じタッチで描き直した"}, {"id": "index_8PXH6omo", "text": "チャンネル名は「フィジカルAI解説|Pixels to Motors」を候補にした"}, {"id": "index_mtwyUJht", "text": "チャンネル名の変更と、アイコン・4コマの作り直しが残っている"}, {"id": "index_LGOmRd2E", "text": "制作スクリプトの全文"}, {"id": "index_NP8gCgQ3", "text": "スクリプト以外に実行したコマンド"}, {"id": "index_dxzTRtXR", "text": "実行ログ"}, {"id": "index_QUNQPzA5", "text": "参照した情報"}]}
\ No newline at end of file
+{"day": "LOG332", "series": "ひとり研究所の裏側", "published": "2026-10-12T00:28:45.000Z", "title": "YouTubeチャンネルの顔を作る ― チャンネル名・アイコン・バナーをハコブくんにそろえるまで", "slug": "youtube-channel-identity", "cover": null, "thumb": "/thumbs/r-youtube-channel-identity.html", "lang": "JP", "description": "解説動画を出す前に、YouTubeチャンネルを運用用の新アカウントへ引き継ぎ、説明文・アイコン・バナーをナレーション役のハコブくんにそろえた。バナーは5回作り直し、計18案から決めた。", "updated": "2026-10-12T00:28:42.000Z", "body": "<!--TOC--><p></p><p>Pixels to Motors の YouTube チャンネルで解説動画を出す準備として、チャンネルの見た目と運用の仕組みを Claude と対話しながら整えました。解説動画のナレーションは、病院で薬を運ぶロボットのキャラクター「ハコブくん」が担当すると決めています(LOG331)。そこで、チャンネルを開いた人が最初に目にするアイコンとバナーも、ハコブくんにそろえることにしました。</p><p>やったことは大きく4つです。YouTube を運用するための Google アカウントを新しく作り、既存のチャンネルを引き継ぐ。チャンネルの説明文を書き直す。バナーを作る。アイコンをハコブくんにする。バナーは文字入りの3案から始めて、5回作り直し、計18案を見たうえで決めました。</p><p>本記事は、その手順と、途中で方針が変わった場面の記録です。アカウントの引き継ぎで Claude が誤った案内をした場面も残しています。バナーとアイコンを作った Python スクリプトは、記事の後半に全文を載せています。メールアドレスと Google Workspace の料金の具体的な金額は伏せました。</p><p></p><figure><img src=\"/media/s-2704x1546_v-frms_webp_69baf643-e22a-4e3e-9067-63e9453efb8b.png\" alt=\"\"><figcaption></figcaption></figure><p></p><h2 id=\"index__Ynzo3Cp\">YouTube用のアカウントは、Google Workspaceのエイリアスではなく新しいユーザーにした</h2><p>Pixels to Motors のメールは、ADAPT DESIGN の独自ドメインで Google Workspace(Google のメールやドライブを会社のドメインで使うサービス)を契約して運用しています。ここに Pixels to Motors 用のアドレスを足す方法は2つありました。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>方法</p></th><th colspan=\"1\" rowspan=\"1\"><p>できること</p></th><th colspan=\"1\" rowspan=\"1\"><p>できないこと</p></th><th colspan=\"1\" rowspan=\"1\"><p>費用</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>エイリアス(既存のユーザーに別名のアドレスを付ける)</p></td><td colspan=\"1\" rowspan=\"1\"><p>メールの受信。設定すれば、そのアドレス名義での送信</p></td><td colspan=\"1\" rowspan=\"1\"><p>そのアドレスで Google にログインすること</p></td><td colspan=\"1\" rowspan=\"1\"><p>かからない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>新しいユーザー</p></td><td colspan=\"1\" rowspan=\"1\"><p>専用のログイン、ドライブ、YouTube チャンネル、外部サービスのアカウントを完全に分けること</p></td><td colspan=\"1\" rowspan=\"1\"><p>―</p></td><td colspan=\"1\" rowspan=\"1\"><p>1ユーザー分の月額が増える</p></td></tr></tbody></table></div><p>YouTube チャンネルを、普段使いのアカウントから切り離して運用したかったので、新しいユーザーにしました。契約が年間プランのため、途中で足したユーザーは更新時期まで減らせない前提で決めています。</p><p></p><h2 id=\"index_8rfkxb9m\">管理コンソールには、普段使いのアカウントでは入れなかった</h2><p>ユーザーを足す画面は、Google Workspace の管理コンソール(admin.google.com)にあります。最初に普段使いのアカウントでログインしたところ、「管理者アカウントでログインしてください」と表示されて入れませんでした。</p><p>ドメインには、普段使いのアカウントともう1つ、合わせて2つのユーザーがありました。管理者の権限を持っていたのは、もう1つのほうでした。そちらで入り直すと、Chrome が「このプロフィールで作業を続けるか、仕事用のプロフィールを新しく作成するか」を聞いてきます。以前、Chrome のプロフィールが10個以上重複して増えたことがあったため、どちらを選ぶかは「そのプロフィールを何に使っているか」で決めました。仕事用のプロフィールに切り替わったあとは、管理コンソールが自動では開き直らなかったため、アドレスを入れて開き直しています。</p><p></p><h2 id=\"index_SZwLHwTg\">「このユーザーは既に存在しています」の原因は、3月に付けたエイリアスだった</h2><p>「新しいユーザーの追加」で、作りたいアドレスを入れると、次の赤字が出て作成できませんでした。</p><blockquote><p>すでにユーザーの予備のメールアドレス(メール エイリアス)として使用されています</p></blockquote><p>2026年3月、同じアドレスをエイリアスとして管理者アカウントに付けていました(当時はユーザーを増やさず、無料のエイリアスで済ませる判断をしていた)。エイリアスとして使っているアドレスと同じ名前のユーザーは作れません。</p><p>管理者アカウントの詳細画面から「メールエイリアス」を開き、そのエイリアスを削除して保存しました。そのあと「新しいユーザーの追加」をやり直すと、ユーザーが作成されました。名前は姓「Pixels to」、名「Motors」で入れています。この入れ方だと、YouTube の画面によっては「Motors Pixels to」と、姓と名が逆の順に表示されることが後で分かりました。</p><p></p><h2 id=\"index_EnRPO5Pq\">作ろうとしたチャンネルのハンドルは、自分の既存チャンネルが使っていた</h2><p>新しいアカウントで YouTube を開き、チャンネルを作ろうとすると、ハンドル(チャンネルの @ から始まる固有の名前)の欄に次のように出ました。</p><blockquote><p>このハンドルは使用できません。 こちらは使用可能です: @PixelstoMotors-r2y</p></blockquote><p>@PixelstoMotors を使っていたのは、2026年3月21日に管理者アカウントで作った、自分の既存チャンネルでした。登録者1人、動画1本(mechArm 270-Pi の開封動画)がすでにあります。</p><p>新しいチャンネルを別に作ると、動画と登録者が既存チャンネルに残ったまま、ハンドルも別のものになります。そこで新しいチャンネルは作らず、既存チャンネルを新しいアカウントから操作できるようにする方針に切り替えました。</p><p></p><h2 id=\"index_04xwKQv5\">既存チャンネルは、新しいアカウントを「管理者」として招待して引き継いだ</h2><p>YouTube Studio の「設定」→「権限」で、新しいアカウントのアドレスを招待しました。選べる役割は次のとおりで、「オーナー」は一覧にありませんでした。YouTube のヘルプにも、オーナーの役割は他のユーザーに移せないと書かれています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>役割</p></th><th colspan=\"1\" rowspan=\"1\"><p>できること</p></th><th colspan=\"1\" rowspan=\"1\"><p>できないこと</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>オーナー</p></td><td colspan=\"1\" rowspan=\"1\"><p>すべての操作。チャンネルの削除、権限の管理</p></td><td colspan=\"1\" rowspan=\"1\"><p>オーナーの役割を他のユーザーに移すこと</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>管理者</p></td><td colspan=\"1\" rowspan=\"1\"><p>全データの閲覧、権限の管理、チャンネル情報の編集、動画の作成・アップロード・公開・削除</p></td><td colspan=\"1\" rowspan=\"1\"><p>チャンネルの削除</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>編集者</p></td><td colspan=\"1\" rowspan=\"1\"><p>全データの閲覧、編集、アップロード・公開</p></td><td colspan=\"1\" rowspan=\"1\"><p>公開済みの動画の削除、権限の管理</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>字幕編集者</p></td><td colspan=\"1\" rowspan=\"1\"><p>字幕の追加・編集・削除</p></td><td colspan=\"1\" rowspan=\"1\"><p>字幕以外の編集</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>閲覧者</p></td><td colspan=\"1\" rowspan=\"1\"><p>全データの閲覧</p></td><td colspan=\"1\" rowspan=\"1\"><p>編集</p></td></tr></tbody></table></div><p>新しいアカウントには「管理者」を付けました。動画の投稿から分析、説明文やバナーの変更まで、運用に必要なことはすべてできます。</p><h3 id=\"index_YvFm6X9v\">招待の承諾には、受け取る側にもチャンネルが必要だった</h3><p>招待メールを新しいアカウントで開いて進むと、「チャンネルのプロフィール」という、新しいチャンネルを作る画面が出ました。Claude はこれを「余計な空のチャンネルができてしまう画面」と判断し、キャンセルするよう案内しました。</p><p>キャンセルしたあと Studio の権限の画面を見ると、新しいアカウントは「承認保留中」のままでした。招待メールを開き直すと、手順は次の2段になっていました。</p><blockquote><p>ステップ 1. Pixels to Motors にアクセスするには YouTube チャンネルが必要です。 ステップ 2. チャンネルの作成後、この招待を承諾すると Pixels to Motors にアクセスできるようになります。</p></blockquote><p>キャンセルを勧めた画面は、承諾の途中の「ステップ1」でした。招待を受ける側のアカウントにも自分のチャンネルが必要、という YouTube の仕様を Claude が知らずに案内した誤りです。新しいアカウント側に空のチャンネルを作ってから「招待を承諾」を押すと、承諾済みになりました。空のチャンネルは使わずに置いておきます。</p><p>承諾したあとは、Studio の右上で「チャンネルを切り替え」から Pixels to Motors を選ぶと、「あなたは管理者です」と表示された状態で管理画面に入れました。</p><h3 id=\"index_7woCpmaH\">管理者で入っていても、YouTube本体での名義は空のチャンネルになる</h3><p>管理者の権限で使えるのは、Studio の中の操作です。YouTube 本体の画面(動画の視聴ページなど)に移ると、操作の名義は新しいアカウント自身の空のチャンネルになります。他の人の動画に Pixels to Motors の名前でコメントしたり、チャンネル登録したりはできません。</p><p>これも必要になったら、チャンネルの所有の形そのものを変える必要があります。どう変えるのが正しいかは、今回は確かめていません。</p><p></p><h2 id=\"index_6tAUffjk\">チャンネルの説明文は、フィジカルAIを先頭に置いて書き直した</h2><p>書き直す前の説明文は、次のとおりでした。</p><pre><code>UX × AI × Robotics の交差点に存在する、\nまだ誰も名前をつけていない領域「Motion UX」を探求しています。\n\niPhoneの3Dスキャン → Unityシミュレーション → 実機ロボットアーム\nという低コストなパイプラインを一人で構築中。\n\nすべてのプロセスをオープンに記録しています。\n\n// ADAPT DESIGN — Pixels to Motors\n// adaptdesign.jp/pixelstomotors</code></pre><p>iPhone → Unity → 実機というパイプラインは、いまの開発の中心ではなくなっています。冒頭の2〜3行は、検索結果やチャンネルページで最初に見える部分です。そこで、機材の名前ではなく、何のチャンネルかが伝わる文にしました。方向の違う3案(フィジカルAI・実装寄り/UX・体験寄り/個人・低コスト寄り)から、フィジカルAI寄りの案を選び、機材名の行は削りました。英語の段落は、日本語と同じ内容にそろえています。</p><pre><code>フィジカルAIを、現場で使える形にするまでを記録するチャンネルです。\nAIが考え、ロボットが動く。その間にある設計・調整・失敗を、\n小さなロボットアームで一つずつ確かめています。\n\n精度より、まず体験が成立すること。\n精度はあとからお金で解決できる。\n\nDocumenting the journey of making Physical AI work in real-world settings.\nBetween AI thinking and robots moving lies design, tuning, and failure.\nI test each step with a small robot arm.\n\nExperience first. Precision can be bought later.\n\n// ADAPT DESIGN — Pixels to Motors\n// adaptdesign.jp/pixelstomotors</code></pre><p>Studio の「カスタマイズ」→「プロフィール」→「説明」に貼り付けて公開しました。公開ボタンが押せない状態(灰色)になっていれば保存済みです。ただ、チャンネルページの表示は、記事を書いている時点でもまだ古い説明文のままでした。</p><p></p><h2 id=\"index_GNMYvKmk\">バナーは、どの端末でも切れない中央の範囲を基準に作った</h2><p>YouTube のヘルプでは、バナー画像は次のように決まっています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>項目</p></th><th colspan=\"1\" rowspan=\"1\"><p>値</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>最小サイズ</p></td><td colspan=\"1\" rowspan=\"1\"><p>2048×1152px(16:9)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>テキストやロゴが切れずに表示される範囲</p></td><td colspan=\"1\" rowspan=\"1\"><p>中央の 1235×338px</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>推奨サイズ(テレビでも見やすい)</p></td><td colspan=\"1\" rowspan=\"1\"><p>2560×1440px</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ファイルサイズ</p></td><td colspan=\"1\" rowspan=\"1\"><p>6MB 以下</p></td></tr></tbody></table></div><p>同じ 16:9 の画像が、テレビでは全体、パソコンでは横長の帯、スマホではさらに左右を切った中央だけで表示されます。そのため、主役になる絵は中央の 1235×338px(2560×1440px の画像なら、同じ比率で 1546×423px)に収めました。作った案は、パソコンでの見え方(横長の帯)と、全体図に安全範囲の枠を重ねた図の2つで確認しています。</p><div class=\"embed\"><iframe src=\"/embeds/youtube-channel-identity-MHKfODFx.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div><p></p><h2 id=\"index_BpOCllXq\">バナーの1回目は、実機の写真に文字を入れた3案だった</h2><p>最初は、手元にある素材から作りました。サイトの画像はこちらの作業環境から取得できなかったため、Mac の r2r2r-content フォルダにあった画像認識シリーズの写真を使っています。</p><ul><li><p>A:mechArm 270-Pi のグリッパーと赤・青のキューブを真上から撮った写真に、「フィジカルAIを、現場で使える形に。」の文字</p></li><li><p>B:アームの根元側をドット状にぼかし、先端に向かってくっきりさせた「Pixels to Motors」の絵</p></li><li><p>C:明るい背景に、キューブの写真・色で切り出したマスク・グリッパーの3コマを並べた「見る → 判断する → 動かす」</p></li></ul><p></p><figure><img src=\"/media/s-1280x636_v-fms_webp_406c9cd8-a337-4559-9ae0-f3c27acf5acd.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>これに対しての返事は次のとおりでした。</p><blockquote><p>テキスト情報は必要なくイメージだけでいいですよ。同じアームだけを使ったバナーではなく、もっといろんなパターンが考えられるはずです。最初はもっと違うパターンを見せて欲しい(今回のは1種類カウントです)</p></blockquote><p>3案とも同じアームの写真が主役で、見せ方の幅が狭かったということです。</p><p></p><h2 id=\"index_KWVQksqt\">2回目は、サイトの画像から6つの方向を出した</h2><p>サイトのトップページを画面ごとPDFに保存したものを受け取り、そこから記事のサムネイル、4コマ漫画、シリーズの扉絵などを切り出しました。文字は入れず、方向がはっきり違う6案にしています。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>案</p></th><th colspan=\"1\" rowspan=\"1\"><p>中身</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>アーカイブの壁</p></td><td colspan=\"1\" rowspan=\"1\"><p>記事のサムネイルと4コマを3段に並べる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>白黒スプリット</p></td><td colspan=\"1\" rowspan=\"1\"><p>左が黒、右がクリーム。境目でピクセルが崩れ、斜線の円と点線のグラフを重ねる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>シミュレーション</p></td><td colspan=\"1\" rowspan=\"1\"><p>Unity で並べたたくさんのアームを網点にする</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ハコブくん 4コマ</p></td><td colspan=\"1\" rowspan=\"1\"><p>4コマ漫画を横一列に並べ、両側に集中線</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ドットマトリクス</p></td><td colspan=\"1\" rowspan=\"1\"><p>「フィジカルAIが社会実装される前に解くべき課題」の丸を全面に広げる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ハコブくん 写真</p></td><td colspan=\"1\" rowspan=\"1\"><p>病院の廊下にいるハコブくんの写真</p></td></tr></tbody></table></div><p></p><figure><img src=\"/media/s-1717x1722_v-fms_webp_c6c4e138-f06b-41a1-bfec-053d0dfc7356.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>このあと、YouTube に合わせて全案を 2048×1152px でも書き出しました。</p><p></p><h2 id=\"index_wfU0AzRw\">アイコンは、動画で話しているハコブくんの1コマから作った</h2><p>チャンネル名とアイコンを、YouTube で覚えてもらいやすいものに変えることになりました。アイコンはハコブくんにする前提です。</p><p>最初に Claude が出したのは、ハコブくんの顔(黒いディスプレイ、白いカプセル形の目、水色の口)をコードで描き起こした3案でした。顔のアップ、オレンジの背景に頭と胴、いまのアイコンの市松模様を引き継いだもの、の3つです。</p><p></p><figure><img src=\"/media/s-1500x940_v-fms_webp_c3582154-8fcc-4086-b283-c9ee9792e885.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>これには、試作動画の画面を書き出した MP4 が返ってきました。</p><blockquote><p>アイコンはこのハコブくん使えばいいんじゃないの?</p></blockquote><p>動画で話しているハコブくんと、アイコンのハコブくんが同じ見た目なら、アイコンから動画を開いた人に同じキャラクターだと伝わります。コードで描いた顔は、仕様には合っていても、動画のハコブくんとは別物でした。</p><p>動画(1920×1080px)を1秒に3コマずつ書き出して、表情を見比べ、口を閉じて笑っているコマを選びました。ハコブくんは画面の左下にいて、大きさは横幅が約320pxです。画面の右側にある出典の文字などが入らないよう、ハコブくんの範囲の外は背景色で塗りつぶしてから切り抜いています。案は3つです。</p><ul><li><p>A:動画のまま。上半身をクリーム地に置く</p></li><li><p>B:A にオレンジの輪を付ける。ダークモードでもライトモードでも輪郭が見える</p></li><li><p>C:顔に寄せる</p></li></ul><p></p><figure><img src=\"/media/s-1500x940_v-fms_webp_27d8616b-248f-424b-bbc8-265007ee2c33.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>各案は、YouTube の表示サイズ(200px・98px・48px・24px)で、ダークモードとライトモードの背景に並べて確認しました。チャンネルには A を設定しています。元が動画の1コマなので、800×800px にするために約2倍に拡大しています。</p><p></p><h2 id=\"index_BZlJ8lML\">バナーの3回目は、ハコブくんだけで5つのタッチを作った</h2><p>次の依頼は、ハコブくんだけを使って、タッチの違う5種類を作ることでした。</p><blockquote><p>イラスト系、4コマまんが風、写実的、写真系、グラフィックデザインの5種類</p></blockquote><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>タッチ</p></th><th colspan=\"1\" rowspan=\"1\"><p>作り方</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>イラスト系</p></td><td colspan=\"1\" rowspan=\"1\"><p>病院の廊下とハコブくんを、線のないフラットな絵としてコードで描く</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4コマまんが風</p></td><td colspan=\"1\" rowspan=\"1\"><p>動画のハコブくんを白黒のトーン調に変え、表情の違う4コマを文字なしで並べる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>写実的</p></td><td colspan=\"1\" rowspan=\"1\"><p>動画の3Dのハコブくんを、表情を変えて5体並べる</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>写真系</p></td><td colspan=\"1\" rowspan=\"1\"><p>病院の廊下にいるハコブくんの写真</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>グラフィックデザイン</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんの顔のディスプレイを大きく置き、ピクセルの崩れ・寸法線・緑のリングを添える</p></td></tr></tbody></table></div><p></p><figure><img src=\"/media/s-1360x1165_v-fms_webp_6153354f-d317-4c3b-9562-e9b084372e61.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>4コマまんが風は、動画のコマを次の手順で白黒にしています。輪郭を検出して黒い線にする。暗い部分(明るさ70未満)は黒で塗る。中間の明るさ(215未満)は、7px 間隔の網点にする。それ以外は白にする。</p><p>返事は次のとおりでした。</p><blockquote><p>イラスト系か4コママンガ風がいいけど、イラストはハコブくんじゃないし、4コママンガ風はハコブくんだけしか描かれてない。</p></blockquote><p>イラスト系のハコブくんは、Claude が写真を見ながら比率を推測して描いたもので、実物と形が違いました。4コマまんが風は、4コマとも同じハコブくんで、漫画に出てくる他の登場人物がいませんでした。</p><h3 id=\"index_elH2fOh3\">ハコブくんの三面図を受け取り、寸法を読み取った</h3><p>ここで、ハコブくんの三面図(正面・側面・背面を漫画のタッチで描いたもの)を受け取りました。</p><p></p><figure><img src=\"/media/s-1080x1350_v-fms_webp_b8890bd5-e649-4af1-9361-84759664f745.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>画像に座標の目盛りを重ねて、正面図の各部の位置を読み取りました(単位は三面図の画像上の px)。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>部分</p></th><th colspan=\"1\" rowspan=\"1\"><p>位置</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>全体</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 70〜331、縦 415〜874(高さ約460)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>頭の外装</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 100〜300、上端 413</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>ディスプレイ</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 120〜283、縦 427〜541</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>目</p></td><td colspan=\"1\" rowspan=\"1\"><p>中心が横 152 と 241、縦 486。幅 22、高さ 40</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>口</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 184〜215、縦 490〜505</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>胴</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 76〜324、縦 505〜816</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>引き出しのスリット</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 167〜225、縦 606〜630</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>黒い帯</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 78〜322、縦 767〜786</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>台座</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 72〜328、縦 806〜848</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>車輪</p></td><td colspan=\"1\" rowspan=\"1\"><p>横 95〜120 と 280〜305、縦 836〜874</p></td></tr></tbody></table></div><p>側面図のリングは、中心が(554, 663)、内側の線が半径約51、外側の線が半径約62でした。リングの位置は、中心の候補を動かしながら半径ごとの平均の明るさを測り、暗い線が2本くっきり出る点を探して決めています。</p><h3 id=\"index_nkmV04Yr\">4回目は、三面図そのものと、他の登場人物を使った</h3><p>三面図のハコブくんを使って、イラスト系と4コマまんが風を2案ずつ作りました。</p><ul><li><p>イラスト系 A:方眼の上に正面・背面・側面を並べ、寸法線と、顔とリングの拡大図を添える</p></li><li><p>イラスト系 B:線画に淡い色を付けた廊下を、三面図の側面のハコブくんが走る</p></li><li><p>4コマまんが風 A:斜めのコマ割りで、中央にハコブくん、左右にツカムくん(顔の付いたアーム)、エンジニア、看護師</p></li><li><p>4コマまんが風 B:ハコブくんを中央の爆発形の枠に置き、他の登場人物を丸い枠で並べる</p></li></ul><p></p><figure><img src=\"/media/s-1360x936_v-fms_webp_2febf659-d311-4869-ace6-89ed02c61b14.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>返事は次のとおりでした。</p><blockquote><p>イラストは背景のイラストタッチにハコブくんを修正しないとダメだよ(イラストと漫画風を一緒にしたらNG)</p><p>4コマまんがの処理は前の方が良かった。このタッチのまま、登場するキャラを入れ替えればいいだけです。</p></blockquote><p>イラスト系 B は、背景は線画に色を付けたイラストなのに、ハコブくんだけ漫画のトーンのままでした。1枚の絵の中で、タッチが2種類混ざっていたことになります。4コマまんが風は、コマ割りを変えたことで、3回目で良かった処理(輪郭線と網点)が失われていました。</p><p></p><h2 id=\"index_aoA9mNR9\">5回目で、ハコブくんを背景と同じタッチで描き直した</h2><p>イラスト系は、三面図から読み取った寸法を使い、ハコブくんを背景と同じタッチで描き直しました。三面図の座標をそのまま渡すと、バナーの上の位置と大きさに変換して描く仕組みです(スクリプトの <code>Pen</code> クラス)。線のない背景には線のないハコブくん、黒い輪郭線の背景には同じ太さの輪郭線のハコブくん、と2つのタッチで作りました。</p><p>4コマまんが風は、3回目の処理と並び(4コマを横一列・両側に集中線)に戻し、コマごとの登場人物を入れ替えました。左からハコブくん、看護師(「!」)、エンジニア(汗)、ツカムくん(キラキラ)です。看護師たちは元が線画なので、3回目の処理(輪郭検出)をそのままかけると全体が黒くつぶれました。線画には輪郭検出をかけず、明るさだけで黒・網点・白に分ける処理にしています。網点の間隔は、ハコブくんのコマと同じ 7px にそろえました。</p><p></p><figure><img src=\"/media/s-1360x707_v-fms_webp_3a2d5396-3c1f-4313-b446-6381c5f222fe.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>決まったのは、線のないフラットなイラストでした。</p><blockquote><p>イラスト系、ハコブくんは1体だけでいいよ。タッチはこれがいいかな。</p></blockquote><p>横向きのハコブくんと動きの線を消し、正面のハコブくんだけをドアとドアの間に立たせて完成にしました。ハコブくんは、顔から足元まで、どの端末でも切れない範囲に収まっています。</p><p></p><figure><img src=\"/media/s-2048x1152_v-frms_webp_9fd7c7eb-5d31-479a-bef5-522b73fab189.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>1回目から5回目までに出したバナーは次のとおりです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>回</p></th><th colspan=\"1\" rowspan=\"1\"><p>案の数</p></th><th colspan=\"1\" rowspan=\"1\"><p>主な中身</p></th><th colspan=\"1\" rowspan=\"1\"><p>返ってきた判断</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>実機の写真+文字</p></td><td colspan=\"1\" rowspan=\"1\"><p>文字は要らない。同じアームだけで幅が狭い</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>7</p></td><td colspan=\"1\" rowspan=\"1\"><p>サイトの画像から6方向+1回目の写真(文字なし)</p></td><td colspan=\"1\" rowspan=\"1\"><p>方向の幅を確認</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんだけで5つのタッチ</p></td><td colspan=\"1\" rowspan=\"1\"><p>イラストと4コマが良い。ただし形が違う/他の人物がいない</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>4</p></td><td colspan=\"1\" rowspan=\"1\"><p>三面図と他の登場人物</p></td><td colspan=\"1\" rowspan=\"1\"><p>タッチを混ぜない。4コマは3回目の処理に戻す</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>5</p></td><td colspan=\"1\" rowspan=\"1\"><p>3+1</p></td><td colspan=\"1\" rowspan=\"1\"><p>タッチをそろえて描き直し</p></td><td colspan=\"1\" rowspan=\"1\"><p>フラットのイラスト、ハコブくん1体で決定</p></td></tr></tbody></table></div><p></p><h2 id=\"index_8PXH6omo\">チャンネル名は「フィジカルAI解説|Pixels to Motors」を候補にした</h2><p>チャンネル名は、ブランド名の Pixels to Motors を残したまま、何のチャンネルかが分かる言葉を足す案にしました。Pixels to Motors のルールで、外に出すブランド名は Pixels to Motors だけと決めているためです。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>案</p></th><th colspan=\"1\" rowspan=\"1\"><p>名前</p></th><th colspan=\"1\" rowspan=\"1\"><p>良い点</p></th><th colspan=\"1\" rowspan=\"1\"><p>気になる点</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>1</p></td><td colspan=\"1\" rowspan=\"1\"><p>Pixels to Motors|フィジカルAI解説</p></td><td colspan=\"1\" rowspan=\"1\"><p>ブランド名が先に来る</p></td><td colspan=\"1\" rowspan=\"1\"><p>少し長い</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>2</p></td><td colspan=\"1\" rowspan=\"1\"><p>フィジカルAI解説|Pixels to Motors</p></td><td colspan=\"1\" rowspan=\"1\"><p>日本語が先に来て、内容が一番早く伝わる</p></td><td colspan=\"1\" rowspan=\"1\"><p>幅が狭い画面ではブランド名が切れやすい</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>3</p></td><td colspan=\"1\" rowspan=\"1\"><p>ハコブくんのフィジカルAI解説</p></td><td colspan=\"1\" rowspan=\"1\"><p>覚えやすく、アイコンと一体になる</p></td><td colspan=\"1\" rowspan=\"1\"><p>ブランド名と、動画を作っている本人が見えなくなる</p></td></tr></tbody></table></div><p></p><figure><img src=\"/media/s-1500x690_v-fms_webp_29ae9ada-591e-4c09-92a0-8b1d5224d689.png\" alt=\"\"><figcaption></figcaption></figure><p></p><p>選ばれたのは2の並びでした。そのうえで、次の質問がありました。</p><blockquote><p>ロボティクスとフィジカルAIでどちらがタイトルの言葉として使うのがいい?</p></blockquote><p>Claude の答えは「フィジカルAI」です。理由は3つです。動画を届けたい相手(ロボットに関心のある経営者・PM・マネージャー)が、ニュースや業界の発表で目にしている言葉であること。動画の中身が「AIがロボットを動かす」話で、ロボティクス全般より範囲がはっきりすること。サイトの課題地図や研究室のシリーズ名でも「フィジカルAI」を使っていること。流行りの言葉なので数年後に古く聞こえるおそれはありますが、チャンネル名は後から変えられ、ハンドル(@PixelstoMotors)は変わりません。</p><p>チャンネル名の変更は、記事を書いている時点ではまだ行っていません。</p><p></p><h2 id=\"index_mtwyUJht\">チャンネル名の変更と、アイコン・4コマの作り直しが残っている</h2><ul><li><p>チャンネル名を「フィジカルAI解説|Pixels to Motors」に変える</p></li><li><p>新しい説明文がチャンネルページに反映されたかを確かめる</p></li><li><p>新しいアカウントに2段階認証を設定する</p></li><li><p>アイコンは動画の1コマを約2倍に拡大している。元の切り抜き画像(動画の制作ベースのフォルダにある)から作り直すと、もっと鮮明になる</p></li><li><p>4コマまんが風の看護師たちは、サイトの画面を保存した PDF から切り出した。採用するなら元の漫画画像で作り直す</p></li></ul><p>細かい修正は、動画をアップしながら進める予定です。</p><p></p><h2 id=\"index_LGOmRd2E\">制作スクリプトの全文</h2><p>バナーとアイコンは、すべて Python(Pillow と NumPy)で作りました。作業フォルダは <code>/home/claude/banner/</code> です。スクリプトは作った順に載せています。採用したバナーは make5_single.py、採用したアイコンは hakobu_icon.py の出力です。</p><div class=\"table\"><table><tbody><tr><th colspan=\"1\" rowspan=\"1\"><p>ファイル</p></th><th colspan=\"1\" rowspan=\"1\"><p>作ったもの</p></th></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>1回目:実機の写真に文字を入れた3案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make2.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>2回目:サイトの画像から作った6案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>icons.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイコン:ハコブくんの顔をコードで描いた3案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>hakobu_icon.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>アイコン:動画の1コマから作った3案(採用)</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make3.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>3回目:ハコブくんだけで5つのタッチ</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make4.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>4回目:三面図と他の登場人物を使った4案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make5.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>5回目:タッチをそろえた3案</p></td></tr><tr><td colspan=\"1\" rowspan=\"1\"><p>make5_single.py</p></td><td colspan=\"1\" rowspan=\"1\"><p>決定版:フラットのイラスト、ハコブくん1体(採用)</p></td></tr></tbody></table></div><h3 id=\"index_LAOak7lY\">make.py(1回目の3案)</h3><pre><code class=\"language-python\">import numpy as np\nfrom PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageOps, ImageEnhance\n\nSRC = '/mnt/user-data/uploads/r2r2r-content/articles/'\nOUT = '/home/claude/banner/out/'\nW, H = 2560, 1440\nSAFE = (507, 508, 2053, 932) # x0,y0,x1,y1 (1546 x 423 visible on every device)\n\nCJK_B = '/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc'\nCJK_M = '/usr/share/fonts/opentype/noto/NotoSansCJK-Medium.ttc'\nINTER = '/usr/share/fonts/opentype/inter/'\n\ndef f_cjk(size, bold=True):\n return ImageFont.truetype(CJK_B if bold else CJK_M, size, index=0)\n\ndef f_inter(size, w='SemiBold'):\n return ImageFont.truetype(INTER + f'Inter-{w}.otf', size)\n\ndef tracked(draw, xy, text, font, fill, track=0):\n x, y = xy\n for ch in text:\n draw.text((x, y), ch, font=font, fill=fill)\n x += draw.textlength(ch, font=font) + track\n return x\n\ndef load(name):\n return ImageOps.exif_transpose(Image.open(SRC + name)).convert('RGB')\n\ndef place(canvas, img, scale, src_pt, dst_pt):\n \"\"\"paste img scaled so that src_pt (in img coords) lands on dst_pt\"\"\"\n im = img.resize((round(img.width * scale), round(img.height * scale)), Image.LANCZOS)\n ox = round(dst_pt[0] - src_pt[0] * scale)\n oy = round(dst_pt[1] - src_pt[1] * scale)\n layer = Image.new('RGB', (W, H))\n layer.paste(im, (ox, oy))\n return layer, (ox, oy, im.width, im.height)\n\ndef rect_mask(box, feather):\n m = Image.new('L', (W, H), 0)\n ImageDraw.Draw(m).rectangle(box, fill=255)\n return m.filter(ImageFilter.GaussianBlur(feather))\n\ndef hgrad(x0, x1, a0, a1):\n \"\"\"horizontal alpha ramp, a0 at x<=x0 to a1 at x>=x1\"\"\"\n xs = np.clip((np.arange(W) - x0) / max(1, x1 - x0), 0, 1)\n row = (a0 + (a1 - a0) * xs).astype(np.uint8)\n return Image.fromarray(np.tile(row, (H, 1)))\n\ndef pixel_squares(draw, x, y, s, gap, cols):\n for i, c in enumerate(cols):\n draw.rectangle((x + i * (s + gap), y, x + i * (s + gap) + s, y + s), fill=c)\n\nRED, BLUE = (214, 52, 52), (36, 128, 214)\nMAT = (30, 27, 31)\n\nphoto = load('IMG_7933.jpg') # top-down: mechArm on cutting mat, red & blue cubes\nMAT_BOX = (697, 639, 3211, 2471) # cutting-mat area in full-res coords\n\n# ---------------------------------------------------------------- A: GRASP\ndef banner_a():\n bg = Image.new('RGB', (W, H), MAT)\n subj = ((1282 + 2650) / 2, 1790) # midpoint between the two cubes, gripper level\n layer, _ = place(bg, photo, 0.55, subj, (1530, 728))\n # keep only the mat, fade its edges into the background\n s = 0.55\n ox = 1530 - subj[0] * s; oy = 728 - subj[1] * s\n mb = (ox + MAT_BOX[0] * s + 70, oy + MAT_BOX[1] * s + 70,\n ox + MAT_BOX[2] * s - 70, oy + MAT_BOX[3] * s - 70)\n m = rect_mask(mb, 60)\n layer = ImageEnhance.Brightness(layer).enhance(0.92)\n bg.paste(layer, (0, 0), m)\n # darken left for the type\n shade = Image.new('RGB', (W, H), MAT)\n bg.paste(shade, (0, 0), hgrad(900, 1080, 235, 0))\n d = ImageDraw.Draw(bg)\n x = SAFE[0] + 20\n pixel_squares(d, x, 586, 18, 8, [RED, BLUE, (235, 235, 235)])\n tracked(d, (x + 92, 580), 'PIXELS TO MOTORS', f_inter(28, 'SemiBold'), (190, 186, 194), track=6)\n d.text((x, 640), 'フィジカルAIを、', font=f_cjk(68), fill=(245, 245, 245))\n d.text((x, 728), '現場で使える形に。', font=f_cjk(68), fill=(245, 245, 245))\n d.text((x, 836), 'Making Physical AI work in the real world.', font=f_inter(26, 'Regular'), fill=(160, 156, 166))\n return bg\n\n# ---------------------------------------------------------------- B: PIXELS → MOTORS (dissolve)\ndef banner_b():\n rot = photo.rotate(90, expand=True) # CCW: robot base on the left, gripper reaching right\n # mapping for CCW 90 with expand: (x, y) -> (y, W0 - x)\n W0 = photo.width\n def r(pt): return (pt[1], W0 - pt[0])\n base = r((2010, 700)); tips = r((2010, 2045))\n # arm axis: y = W0 - 2010 ; x from 700 to 2045\n s = 0.50\n layer, (ox, oy, iw, ih) = place(None, rot, s, (tips[0], tips[1]), (2030, 720))\n # mat region after rotation\n mb0 = (MAT_BOX[1], W0 - MAT_BOX[2], MAT_BOX[3], W0 - MAT_BOX[0])\n mb = (ox + mb0[0] * s + 70, oy + mb0[1] * s + 70, ox + mb0[2] * s - 70, oy + mb0[3] * s - 70)\n bg = Image.new('RGB', (W, H), MAT)\n bg.paste(layer, (0, 0), rect_mask(mb, 50))\n # progressive pixelation, left -> right, in vertical strips\n arr = np.array(bg)\n out = arr.copy()\n sizes = [96, 84, 72, 64, 56, 48, 40, 34, 28, 22, 16, 11, 7, 4]\n x_start, x_end = 0, 1900\n sw = (x_end - x_start) / len(sizes)\n for i, b in enumerate(sizes):\n xa, xb = int(x_start + i * sw), int(x_start + (i + 1) * sw)\n small = bg.resize((max(1, W // b), max(1, H // b)), Image.BOX).resize(\n (W // b * b, H // b * b), Image.NEAREST)\n sm = np.zeros_like(arr); sa = np.array(small)\n sm[:sa.shape[0], :sa.shape[1]] = sa\n out[:, xa:xb] = sm[:, xa:xb]\n bg = Image.fromarray(out)\n shade = Image.new('RGB', (W, H), MAT)\n bg.paste(shade, (0, 0), hgrad(760, 1060, 205, 0))\n d = ImageDraw.Draw(bg)\n # detection-style bracket around the gripper (perception -> action)\n gx, gy, hw, hh = 1932, 720, 108, 118\n c = (120, 200, 255); L = 34; t = 4\n for sx in (-1, 1):\n for sy in (-1, 1):\n px, py = gx + sx * hw, gy + sy * hh\n d.line((px, py, px - sx * L, py), fill=c, width=t)\n d.line((px, py, px, py - sy * L), fill=c, width=t)\n tracked(d, (gx - hw, gy - hh - 44), 'GRASP', f_inter(24, 'SemiBold'), c, track=5)\n x = SAFE[0] + 20\n d.text((x, 572), 'Pixels to Motors', font=ImageFont.truetype(INTER + 'InterDisplay-Bold.otf', 88), fill=(248, 248, 248))\n d.text((x, 696), 'フィジカルAIを、現場で使える形にするまで。', font=f_cjk(38, bold=False), fill=(225, 222, 228))\n tracked(d, (x, 786), 'PIXELS → PERCEPTION → MOTION', f_inter(26, 'Medium'), (150, 146, 156), track=4)\n return bg\n\n# ---------------------------------------------------------------- C: SEE / DECIDE / MOVE (light)\ndef banner_c():\n NAVY = (26, 35, 50); BG = (244, 246, 248); GRID = (226, 231, 236)\n bg = Image.new('RGB', (W, H), BG)\n d = ImageDraw.Draw(bg)\n for gx in range(0, W, 40):\n d.line((gx, 0, gx, H), fill=GRID if gx % 200 else (214, 221, 228), width=1)\n for gy in range(0, H, 40):\n d.line((0, gy, W, gy), fill=GRID if gy % 200 else (214, 221, 228), width=1)\n # panel sources\n cubes = load('IMG_7912.JPG') # red & blue cubes, white paper\n crop = (1500, 700, 3500, 2700)\n p1 = cubes.crop(crop)\n hsv = np.array(p1.convert('HSV')).astype(int)\n rgb = np.array(p1).astype(int)\n sat = hsv[..., 1] > 110\n red = sat & ((hsv[..., 0] < 12) | (hsv[..., 0] > 235)) & (rgb[..., 0] > rgb[..., 2])\n blue = sat & (hsv[..., 0] > 120) & (hsv[..., 0] < 170)\n mask = np.zeros(rgb.shape, np.uint8)\n mask[red] = (255, 90, 90); mask[blue] = (90, 170, 255)\n p2 = Image.fromarray(mask).filter(ImageFilter.MedianFilter(7))\n # bounding boxes on mask\n d2 = ImageDraw.Draw(p2)\n for m, col in ((red, (255, 90, 90)), (blue, (90, 170, 255))):\n ys, xs = np.where(m)\n if len(xs):\n x0, x1 = np.percentile(xs, [1, 99]); y0, y1 = np.percentile(ys, [1, 99])\n d2.rectangle((x0 - 30, y0 - 30, x1 + 30, y1 + 30), outline=col, width=14)\n p3 = photo.crop((1100, 1250, 2900, 2250)) # gripper between the cubes\n P = 290; gap = 44\n x0 = 1062; y0 = 540\n panels = [(p1, '01', '見る', 'SEE'), (p2, '02', '判断する', 'DECIDE'), (p3, '03', '動かす', 'MOVE')]\n for i, (im, n, jp, en) in enumerate(panels):\n px = x0 + i * (P + gap)\n tile = ImageOps.fit(im, (P, P), Image.LANCZOS)\n shadow = Image.new('L', (W, H), 0)\n ImageDraw.Draw(shadow).rectangle((px + 6, y0 + 10, px + P + 6, y0 + P + 10), fill=70)\n bg.paste((180, 190, 200), (0, 0), shadow.filter(ImageFilter.GaussianBlur(12)))\n bg.paste(tile, (px, y0))\n d = ImageDraw.Draw(bg)\n d.text((px, y0 + P + 20), n, font=f_inter(24, 'SemiBold'), fill=(138, 155, 168))\n d.text((px + 44, y0 + P + 14), jp, font=f_cjk(30), fill=NAVY)\n tw = d.textlength(en, font=f_inter(22, 'Medium'))\n tracked(d, (px + P - tw - 12, y0 + P + 22), en, f_inter(22, 'Medium'), (138, 155, 168), track=3)\n if i < 2:\n ax = px + P + gap // 2; ay = y0 + P // 2\n d.polygon([(ax - 9, ay - 12), (ax + 9, ay), (ax - 9, ay + 12)], fill=NAVY)\n d = ImageDraw.Draw(bg)\n x = SAFE[0] + 20\n pixel_squares(d, x, 598, 16, 7, [NAVY, RED, BLUE])\n tracked(d, (x + 82, 592), 'PIXELS TO MOTORS', f_inter(26, 'SemiBold'), (90, 105, 120), track=6)\n d.text((x, 646), 'フィジカルAIを、', font=f_cjk(58), fill=NAVY)\n d.text((x, 722), '現場で使える形に。', font=f_cjk(58), fill=NAVY)\n d.text((x, 818), 'Experience first.', font=f_inter(30, 'Medium'), fill=(184, 90, 90))\n d.text((x, 856), 'Precision can be bought later.', font=f_inter(30, 'Regular'), fill=(110, 125, 140))\n return bg\n\ndef preview(img, name):\n \"\"\"safe-area overlay + desktop/mobile crops for checking\"\"\"\n ov = img.copy(); d = ImageDraw.Draw(ov, 'RGBA')\n d.rectangle((0, 0, W, SAFE[1]), fill=(0, 0, 0, 110)); d.rectangle((0, SAFE[3], W, H), fill=(0, 0, 0, 110))\n d.rectangle((0, SAFE[1], SAFE[0], SAFE[3]), fill=(0, 0, 0, 60)); d.rectangle((SAFE[2], SAFE[1], W, SAFE[3]), fill=(0, 0, 0, 60))\n d.rectangle(SAFE, outline=(255, 220, 0, 255), width=4)\n ov.resize((1280, 720)).save(OUT + f'check_{name}_overlay.jpg', quality=88)\n img.crop((0, SAFE[1], W, SAFE[3])).resize((1280, 212)).save(OUT + f'check_{name}_desktop.jpg', quality=88)\n img.crop(SAFE).save(OUT + f'check_{name}_mobile.jpg', quality=90)\n\nimport os\nos.makedirs(OUT, exist_ok=True)\nfor name, fn in (('A_grasp', banner_a), ('B_pixels_to_motors', banner_b), ('C_see_decide_move', banner_c)):\n im = fn()\n im.save(OUT + f'pixelstomotors_banner_{name}.jpg', quality=92, subsampling=0)\n preview(im, name)\n print(name, os.path.getsize(OUT + f'pixelstomotors_banner_{name}.jpg') // 1024, 'KB')</code></pre><h3 id=\"index_MFH88eN7\">make2.py(2回目の6案)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps\n\nS = '/home/claude/banner/site/'\nOUT = '/home/claude/banner/out2/'\nos.makedirs(OUT, exist_ok=True)\nW, H = 2560, 1440\nSAFE = (507, 508, 2053, 932)\nCY = 720\nINK = (17, 17, 17); CREAM = (242, 241, 236); GRAY = (152, 152, 148)\n\ndef L(name): return Image.open(S + f'c_{name}.png').convert('RGB')\n\ndef rounded(im, r):\n m = Image.new('L', im.size, 0)\n ImageDraw.Draw(m).rounded_rectangle((0, 0, im.width - 1, im.height - 1), r, fill=255)\n return m\n\ndef card(canvas, im, box, r=14, shadow=True):\n x0, y0, x1, y1 = box\n tile = ImageOps.fit(im, (x1 - x0, y1 - y0), Image.LANCZOS)\n if shadow:\n sh = Image.new('L', (W, H), 0)\n ImageDraw.Draw(sh).rounded_rectangle((x0 + 4, y0 + 12, x1 + 4, y1 + 12), r, fill=60)\n canvas.paste((150, 150, 150), (0, 0), sh.filter(ImageFilter.GaussianBlur(16)))\n canvas.paste(tile, (x0, y0), rounded(tile, r))\n\n# ------------------------------------------------ 1. Archive wall\ndef archive():\n c = Image.new('RGB', (W, H), (236, 236, 233))\n rows = [\n ['inside', 'panels320', 'gpu', 'panels324', 'film', 'rec_say'],\n ['manga2', 'lab', 'manga3', 'sim', 'manga0', 'drug', 'manga1'],\n ['who03', 'panels319', 'bowl', 'panels323', 'imgrec_top', 'rec_micro'],\n ]\n hs = [300, 390, 300]\n ys = [CY - 195 - 36 - 300, CY - 195, CY + 195 + 36]\n offs = [-170, -60, -250]\n for row, h, y, off in zip(rows, hs, ys, offs):\n x = off\n while x < W:\n for n in row:\n im = L(n)\n w = int(h * im.width / im.height)\n if n.startswith('manga'): w = int(h * 1.05)\n card(c, im, (x, y, x + w, y + h))\n x += w + 36\n if x >= W: break\n return c\n\n# ------------------------------------------------ 2. Mono split (vector)\ndef split():\n c = Image.new('RGB', (W, H), CREAM)\n d = ImageDraw.Draw(c)\n mid = 1240\n d.rectangle((0, 0, mid, H), fill=INK)\n # pixel dissolve across the seam: black squares scattered into cream, cream squares into black\n random.seed(7); cell = 44\n for gx in range(mid - cell * 10, mid + cell * 14, cell):\n for gy in range(0, H, cell):\n t = (gx - mid) / (cell * 12)\n if gx >= mid and random.random() < max(0, 0.9 - t):\n d.rectangle((gx + 3, gy + 3, gx + cell - 3, gy + cell - 3), fill=INK)\n if gx < mid and random.random() < max(0, 0.75 + t * 1.4):\n d.rectangle((gx + 3, gy + 3, gx + cell - 3, gy + cell - 3), fill=CREAM)\n # hatched block (top-left of band) and dot grids\n hx0, hy0, hx1, hy1 = 560, 540, 760, 740\n hl = Image.new('RGB', (W, H), INK); hd = ImageDraw.Draw(hl)\n for k in range(-220, 240, 16):\n hd.line((hx0 + k, hy1, hx0 + k + 200, hy0), fill=CREAM, width=4)\n hm = Image.new('L', (W, H), 0); ImageDraw.Draw(hm).rectangle((hx0, hy0, hx1, hy1), fill=255)\n c.paste(hl, (0, 0), hm)\n for gx in range(1800, 2040, 22):\n for gy in range(540, 700, 22):\n d.ellipse((gx - 3, gy - 3, gx + 3, gy + 3), fill=INK)\n for gx in range(560, 760, 22):\n for gy in range(800, 900, 22):\n d.ellipse((gx - 3, gy - 3, gx + 3, gy + 3), fill=(120, 120, 120))\n # half-hatched circle (right)\n cx, cy, r = 1700, 735, 175\n circ = Image.new('L', (W, H), 0); ImageDraw.Draw(circ).ellipse((cx - r, cy - r, cx + r, cy + r), fill=255)\n hatch = Image.new('RGB', (W, H), CREAM); hd = ImageDraw.Draw(hatch)\n for k in range(-400, 400, 18):\n hd.line((cx + k - 200, cy + 200, cx + k + 200, cy - 200), fill=INK, width=5)\n half = Image.new('L', (W, H), 0); ImageDraw.Draw(half).rectangle((cx, 0, W, H), fill=255)\n m = Image.fromarray((np.array(circ) & np.array(half)).astype(np.uint8))\n c.paste(hatch, (0, 0), m)\n d.ellipse((cx - r, cy - r, cx + r, cy + r), outline=INK, width=6)\n for a in (0, 90, 180, 270):\n px = cx + r * math.cos(math.radians(a)); py = cy + r * math.sin(math.radians(a))\n d.ellipse((px - 12, py - 12, px + 12, py + 12), fill=INK)\n # dotted signal line running from black side into cream (noisy -> settled)\n random.seed(3)\n pts = []\n for i, x in enumerate(range(520, 2060, 60)):\n amp = 90 * max(0, 1 - (x - 520) / 1200)\n pts.append((x, 870 - random.uniform(-1, 1) * amp))\n for (x0, y0), (x1, y1) in zip(pts, pts[1:]):\n steps = int(math.hypot(x1 - x0, y1 - y0) // 14)\n for s in range(steps):\n t = s / steps; px = x0 + (x1 - x0) * t; py = y0 + (y1 - y0) * t\n col = CREAM if px < mid else INK\n d.ellipse((px - 2.5, py - 2.5, px + 2.5, py + 2.5), fill=col)\n for x, y in pts:\n col = CREAM if x < mid else INK\n d.ellipse((x - 9, y - 9, x + 9, y + 9), fill=INK if x >= mid else INK, outline=col, width=4)\n return c\n\n# ------------------------------------------------ 3. Simulation halftone\ndef halftone():\n src = ImageOps.autocontrast(L('sim').convert('L'), cutoff=1)\n sc = W / src.width; bh = int(src.height * sc)\n big = src.resize((W, bh), Image.BICUBIC)\n top = int(0.30 * bh) - SAFE[1] - 340\n full = Image.new('L', (W, H), 0)\n full.paste(big, (0, -top))\n a = np.array(full).astype(float) / 255\n c = Image.new('RGB', (W, H), CREAM); d = ImageDraw.Draw(c)\n step = 10\n for y in range(0, H, step):\n for x in range((y // step % 2) * step // 2, W, step):\n v = a[y, min(W - 1, x)]\n r = v ** 1.6 * step * 0.66\n if r > 0.6:\n d.ellipse((x - r, y - r, x + r, y + r), fill=INK)\n return c\n\n# ------------------------------------------------ 4. Manga strip\ndef manga():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n # screentone background (fading dots towards the centre)\n for y in range(0, H, 16):\n for x in range(0, W, 16):\n dist = abs(x - W / 2) / (W / 2)\n r = 3.2 * max(0, dist - 0.35) / 0.65\n if r > 0.4:\n d.ellipse((x - r, y - r, x + r, y + r), fill=(200, 200, 198))\n order = ['manga3', 'manga0', 'manga2', 'manga1']\n ph = 372; pw = 366; gap = 22\n total = 4 * pw + 3 * gap\n x = (W - total) // 2; y = CY - ph // 2\n for n in order:\n im = L(n)\n tile = ImageOps.fit(im, (pw, ph), Image.LANCZOS, centering=(0.5, 0.6))\n c.paste(tile, (x, y))\n d.rectangle((x - 5, y - 5, x + pw + 4, y + ph + 4), outline=INK, width=6)\n x += pw + gap\n # speed lines on both sides\n for k in range(26):\n yy = y + 10 + k * (ph - 20) / 25\n ln = 120 + (k * 37 % 5) * 60\n d.line((x + 30, yy, x + 30 + ln, yy), fill=INK, width=3)\n x0 = (W - total) // 2 - 40\n d.line((x0 - ln, yy, x0, yy), fill=INK, width=3)\n return c\n\n# ------------------------------------------------ 5. Dot matrix\ndef dots():\n c = Image.new('RGB', (W, H), CREAM); d = ImageDraw.Draw(c)\n D = 82; G = 24; P = D + G\n random.seed(36)\n cols = W // P + 2; rows = H // P + 4\n ox = (W - cols * P) // 2 + G // 2; oy = SAFE[1] + G // 2 + 2 - P * 6\n for j in range(rows):\n for i in range(cols):\n x = ox + i * P; y = oy + j * P\n t = i / cols\n r = random.random()\n # left: mostly solved (black), middle: in progress (gray), right: open (outline)\n pb = max(0, 0.75 - t * 1.3); pg = 0.55 - abs(t - 0.5) * 0.6\n if r < pb: d.ellipse((x, y, x + D, y + D), fill=INK)\n elif r < pb + pg: d.ellipse((x, y, x + D, y + D), fill=GRAY)\n else: d.ellipse((x + 2, y + 2, x + D - 2, y + D - 2), outline=INK, width=4)\n return c\n\n# ------------------------------------------------ 6. Hakobu photo\ndef hakobu():\n src = L('drug')\n sw = int(src.width * 0.30)\n left = src.crop((0, 0, sw, src.height)); right = src.crop((src.width - sw, 0, src.width, src.height))\n pano = Image.new('RGB', (sw * 2, src.height)); pano.paste(left, (0, 0)); pano.paste(right, (sw, 0))\n bgim = pano.resize((W, H), Image.BICUBIC).filter(ImageFilter.GaussianBlur(30))\n bgim = Image.blend(bgim, Image.new('RGB', (W, H), (238, 236, 232)), 0.25)\n # sharp copy: robot fits inside the band\n h = 470; s = h / src.height; w = int(src.width * s)\n sm = src.resize((w, h), Image.LANCZOS)\n x = 1500 - w // 2; y = CY - h // 2 + 12\n m = Image.new('L', (w, h), 0)\n ImageDraw.Draw(m).rectangle((60, 45, w - 60, h - 45), fill=255)\n m = m.filter(ImageFilter.GaussianBlur(45))\n bgim.paste(sm, (x, y), m)\n return bgim\n\ndef preview(img, name):\n img.crop((0, SAFE[1], W, SAFE[3])).save(OUT + f'_band_{name}.png')\n\nitems = [('B_archive_wall', archive), ('C_mono_split', split), ('D_sim_halftone', halftone),\n ('E_hakobu_manga', manga), ('F_dot_matrix', dots), ('G_hakobu_photo', hakobu)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'pixelstomotors_banner_{name}.jpg', quality=92, subsampling=0)\n preview(im, name)\n print(name, os.path.getsize(OUT + f'pixelstomotors_banner_{name}.jpg') // 1024, 'KB')</code></pre><h3 id=\"index__zJBabGH\">icons.py(コードで描いたアイコン3案)</h3><pre><code class=\"language-python\">import os, random\nfrom PIL import Image, ImageDraw, ImageFilter\n\nOUT = '/home/claude/banner/icons/'\nos.makedirs(OUT, exist_ok=True)\nSS = 4; N = 800 * SS\nSHELL = (246, 245, 241); SHELL_EDGE = (214, 214, 208)\nDISP = (16, 16, 18); EYE = (246, 248, 252); MOUTH = (87, 181, 248)\nCREAM = (245, 245, 240); ORANGE = (196, 71, 31); INK = (13, 13, 13)\n\ndef face(img, box):\n \"\"\"draw display + eyes + smile inside box (x0,y0,x1,y1)\"\"\"\n x0, y0, x1, y1 = box; w = x1 - x0; h = y1 - y0\n d = ImageDraw.Draw(img)\n d.rounded_rectangle(box, radius=int(w * 0.16), fill=DISP)\n ew, eh = w * 0.13, h * 0.36\n cy = y0 + h * 0.42\n # glow\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gd = ImageDraw.Draw(glow)\n for cx in (x0 + w * 0.31, x0 + w * 0.69):\n gd.rounded_rectangle((cx - ew * 0.75, cy - eh * 0.62, cx + ew * 0.75, cy + eh * 0.62), radius=int(ew * 0.75), fill=(246, 248, 252, 90))\n glow = glow.filter(ImageFilter.GaussianBlur(w * 0.03))\n img.alpha_composite(glow) if img.mode == 'RGBA' else img.paste(glow, (0, 0), glow)\n d = ImageDraw.Draw(img)\n for cx in (x0 + w * 0.31, x0 + w * 0.69):\n d.rounded_rectangle((cx - ew / 2, cy - eh / 2, cx + ew / 2, cy + eh / 2), radius=int(ew / 2), fill=EYE)\n # smile: thick arc with round caps\n mw = w * 0.24; my = y0 + h * 0.70; t = int(h * 0.075)\n d.arc((x0 + w / 2 - mw / 2, my - mw * 0.55, x0 + w / 2 + mw / 2, my + mw * 0.30), 25, 155, fill=MOUTH, width=t)\n import math\n for a in (25, 155):\n rx = mw / 2 - t / 2; ry = (mw * 0.85) / 2 - t / 2\n cx = x0 + w / 2 + rx * math.cos(math.radians(a)); cyy = (my - mw * 0.55 + my + mw * 0.30) / 2 + ry * math.sin(math.radians(a))\n d.ellipse((cx - t / 2, cyy - t / 2, cx + t / 2, cyy + t / 2), fill=MOUTH)\n\ndef head(img, cx, cy, w, h):\n d = ImageDraw.Draw(img)\n d.rounded_rectangle((cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2), radius=int(w * 0.22), fill=SHELL, outline=SHELL_EDGE, width=int(w * 0.012))\n m = w * 0.075\n face(img, (cx - w / 2 + m, cy - h / 2 + m, cx + w / 2 - m, cy + h / 2 - m * 1.1))\n\ndef circle_out(img, name):\n im = img.convert('RGB').resize((800, 800), Image.LANCZOS)\n im.save(OUT + f'{name}.png')\n m = Image.new('L', (800, 800), 0); ImageDraw.Draw(m).ellipse((0, 0, 799, 799), fill=255)\n c = Image.new('RGBA', (800, 800), (0, 0, 0, 0)); c.paste(im, (0, 0), m)\n c.save(OUT + f'{name}_circle.png')\n return c\n\n# 1. close-up face (shell fills the circle)\ndef i1():\n img = Image.new('RGBA', (N, N), CREAM + (255,))\n head(img, N / 2, N * 0.53, N * 0.98, N * 0.80)\n return circle_out(img, 'icon1_face')\n\n# 2. orange ground, head + top of body\ndef i2():\n img = Image.new('RGBA', (N, N), ORANGE + (255,))\n d = ImageDraw.Draw(img)\n bw = N * 0.66\n d.rounded_rectangle((N / 2 - bw / 2, N * 0.70, N / 2 + bw / 2, N * 1.2), radius=int(bw * 0.14), fill=SHELL, outline=SHELL_EDGE, width=SS * 3)\n d.rounded_rectangle((N / 2 - bw * 0.12, N * 0.80, N / 2 + bw * 0.12, N * 0.84), radius=SS * 8, fill=(40, 40, 40))\n head(img, N / 2, N * 0.43, N * 0.70, N * 0.50)\n return circle_out(img, 'icon2_orange')\n\n# 3. pixel dissolve (continuity with the current checker logo)\ndef i3():\n img = Image.new('RGBA', (N, N), INK + (255,))\n face(img, (N * 0.16, N * 0.22, N * 0.92, N * 0.80))\n d = ImageDraw.Draw(img)\n random.seed(4); cell = N // 11\n for gx in range(0, N // 2, cell):\n for gy in range(0, N, cell):\n p = 0.85 - gx / (N * 0.42)\n if random.random() < p:\n d.rectangle((gx, gy, gx + cell - SS * 6, gy + cell - SS * 6), fill=(245, 245, 240))\n return circle_out(img, 'icon3_pixel')\n\nicons = [i1(), i2(), i3()]\n\n# size check sheet on YouTube dark + light backgrounds\nfrom PIL import ImageFont\nfnt = ImageFont.truetype('/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc', 22, index=0)\nsheet = Image.new('RGB', (1500, 3 * 300 + 40), (255, 255, 255)); sd = ImageDraw.Draw(sheet)\nlabels = ['案1 顔のアップ', '案2 オレンジ地', '案3 ピクセル継承']\nfor r, (ic, lab) in enumerate(zip(icons, labels)):\n y = 20 + r * 300\n sd.text((20, y), lab, font=fnt, fill=(20, 20, 20))\n for bgc, x0 in (((15, 15, 15), 20), ((255, 255, 255), 760)):\n sd.rectangle((x0, y + 36, x0 + 720, y + 280), fill=bgc)\n x = x0 + 20\n for s in (200, 98, 48, 24):\n sheet.paste(ic.resize((s, s), Image.LANCZOS), (x, y + 58 + (200 - s) // 2), ic.resize((s, s), Image.LANCZOS))\n x += s + 40\nsheet.save('/home/claude/banner/icons_sizes.jpg', quality=90)</code></pre><h3 id=\"index_ttHgfGc2\">hakobu_icon.py(動画の1コマから作ったアイコン3案)</h3><pre><code class=\"language-python\">from PIL import Image, ImageDraw, ImageFilter, ImageFont\nfr=Image.open('fr_0.png').convert('RGB'); W0,H0=fr.size\nBG=(245,245,240); OR=(196,71,31)\nclean=Image.new('RGB',(W0,H0),BG); clean.paste(fr.crop((0,758,360,H0)),(0,758))\ndef grab(box):\n x0,y0,x1,y1=box; c=Image.new('RGB',(x1-x0,y1-y0),BG)\n c.paste(clean.crop((max(0,x0),max(0,y0),min(W0,x1),min(H0,y1))),(max(0,-x0),max(0,-y0))); return c\ndef up(c,s=800): return c.resize((s,s),Image.LANCZOS).filter(ImageFilter.UnsharpMask(2,60,2))\ndef circ(im):\n m=Image.new('L',im.size,0);ImageDraw.Draw(m).ellipse((0,0,im.width-1,im.height-1),fill=255)\n o=Image.new('RGBA',im.size,(0,0,0,0));o.paste(im,(0,0),m);return o\nA=up(grab((30,660,450,1080)))\nC=up(grab((90,690,390,990)))\n# B: orange ring around the cream picture (no cut-out edges)\ninner=up(grab((50,690,430,1070)),640)\nB=Image.new('RGB',(800,800),OR); m=Image.new('L',(640,640),0); ImageDraw.Draw(m).ellipse((0,0,639,639),fill=255)\nB.paste(inner,(80,80),m)\nout={'A_upper':A,'B_orange_ring':B,'C_face':C}\nimport glob,os\nfor p in glob.glob('../icons/hakobu_icon_*'): os.remove(p)\nfor p in glob.glob('../icons/zoomB.png'): os.remove(p)\nfor k,v in out.items():\n v.save(f'../icons/hakobu_icon_{k}.png'); circ(v).save(f'../icons/hakobu_icon_{k}_circle.png')\nf=ImageFont.truetype('/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc',22,index=0)\nlabels={'A_upper':'案A 動画のまま(上半身・クリーム地)','B_orange_ring':'案B オレンジの縁取り','C_face':'案C 顔に寄せる'}\nsh=Image.new('RGB',(1500,3*300+40),(255,255,255));d=ImageDraw.Draw(sh)\nfor r,k in enumerate(out):\n y=20+r*300; d.text((20,y),labels[k],font=f,fill=(20,20,20)); ic=circ(out[k])\n for bgc,x0 in (((15,15,15),20),((255,255,255),760)):\n d.rectangle((x0,y+36,x0+720,y+280),fill=bgc);x=x0+20\n for s in (200,98,48,24):\n t=ic.resize((s,s),Image.LANCZOS); sh.paste(t,(x,y+58+(200-s)//2),t); x+=s+40\nsh.save('../hakobu_icons_sizes.jpg',quality=90)</code></pre><h3 id=\"index_ox6KpGZu\">make3.py(3回目の5つのタッチ)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps, ImageChops\n\nOUT = '/home/claude/banner/hakobu/'\nos.makedirs(OUT, exist_ok=True)\nW, H = 2048, 1152\nBAND = (0, 407, 2048, 745) # visible on desktop\nSAFE = (406, 407, 1642, 745) # visible everywhere\nCREAM = (245, 245, 240); INK = (13, 13, 13); ORANGE = (196, 71, 31); BLUE = (26, 108, 152)\nSHELL = (247, 246, 242); SHELL2 = (226, 223, 215); DISP = (20, 20, 22)\nEYE = (246, 248, 252); MOUTH = (87, 181, 248); RING = (120, 226, 160)\nFR = '/home/claude/banner/vid/fx/'\nSS = 3 # supersampling for vector work\n\ndef frame(i):\n \"\"\"video frame with everything except Hakobu painted over with the background\"\"\"\n f = Image.open(FR + f'e_{i}.png').convert('RGB')\n c = Image.new('RGB', f.size, CREAM)\n c.paste(f.crop((0, 758, 362, 1080)), (0, 758))\n return c\n\n# ---------------------------------------------------------------- vector Hakobu\ndef face(d, box, expr='smile', glow_layer=None):\n x0, y0, x1, y1 = box; w = x1 - x0; h = y1 - y0\n d.rounded_rectangle(box, radius=int(min(w, h) * 0.16), fill=DISP)\n ew, eh = w * 0.11, h * 0.36; cy = y0 + h * 0.43\n for cx in (x0 + w * 0.32, x0 + w * 0.68):\n if expr == 'blink':\n d.rounded_rectangle((cx - ew * 0.8, cy - ew * 0.18, cx + ew * 0.8, cy + ew * 0.18), radius=int(ew * 0.18), fill=EYE)\n elif expr == 'happy':\n d.arc((cx - ew * 0.9, cy - ew * 0.3, cx + ew * 0.9, cy + ew * 1.5), 200, 340, fill=EYE, width=int(ew * 0.35))\n else:\n if glow_layer is not None:\n ImageDraw.Draw(glow_layer).rounded_rectangle((cx - ew * 0.9, cy - eh * 0.62, cx + ew * 0.9, cy + eh * 0.62), radius=int(ew * 0.9), fill=(246, 248, 252, 110))\n d.rounded_rectangle((cx - ew / 2, cy - eh / 2, cx + ew / 2, cy + eh / 2), radius=int(ew / 2), fill=EYE)\n mw = w * 0.22; my = y0 + h * 0.74; t = max(2, int(h * 0.07))\n if expr in ('open', 'happy'):\n d.chord((x0 + w / 2 - mw / 2, my - mw * 0.45, x0 + w / 2 + mw / 2, my + mw * 0.45), 0, 180, fill=MOUTH)\n elif expr == 'o':\n r = mw * 0.18; d.ellipse((x0 + w / 2 - r, my - r * 0.6, x0 + w / 2 + r, my + r * 1.2), fill=MOUTH)\n else:\n d.arc((x0 + w / 2 - mw / 2, my - mw * 0.55, x0 + w / 2 + mw / 2, my + mw * 0.30), 25, 155, fill=MOUTH, width=t)\n\ndef hakobu(img, cx, base, h, expr='smile', side=True):\n \"\"\"flat front-ish Hakobu, bottom of wheels at `base`, total height h\"\"\"\n d = ImageDraw.Draw(img)\n hw = h * 0.31; bw = h * 0.33; sd = h * 0.07 if side else 0\n # body\n by0 = base - h * 0.70; by1 = base - h * 0.08\n if side:\n d.rounded_rectangle((cx + bw - sd * 2, by0, cx + bw + sd, by1), radius=int(h * 0.05), fill=SHELL2)\n d.ellipse((cx + bw - sd * 0.2, by0 + (by1 - by0) * 0.28, cx + bw + sd * 0.75, by0 + (by1 - by0) * 0.62), outline=RING, width=max(2, int(h * 0.012)))\n d.rounded_rectangle((cx - bw, by0, cx + bw, by1), radius=int(h * 0.06), fill=SHELL)\n d.line((cx - bw + h * 0.01, by0 + h * 0.08, cx + bw - h * 0.01, by0 + h * 0.08), fill=SHELL2, width=max(1, int(h * 0.006)))\n d.rounded_rectangle((cx - h * 0.06, by0 + h * 0.20, cx + h * 0.06, by0 + h * 0.225), radius=int(h * 0.012), fill=(40, 40, 42))\n # base\n d.rounded_rectangle((cx - bw * 1.02, by1 - h * 0.01, cx + bw * 1.02 + sd * 0.5, base - h * 0.02), radius=int(h * 0.02), fill=(58, 58, 60))\n d.rounded_rectangle((cx - bw * 0.9, base - h * 0.03, cx + bw * 0.9, base), radius=int(h * 0.012), fill=(30, 30, 32))\n # head\n hy0 = base - h; hy1 = base - h * 0.70 + h * 0.015\n if side:\n d.rounded_rectangle((cx + hw - sd * 2, hy0 + h * 0.01, cx + hw + sd, hy1), radius=int(h * 0.05), fill=SHELL2)\n d.rounded_rectangle((cx - hw, hy0, cx + hw, hy1), radius=int(h * 0.07), fill=SHELL)\n m = h * 0.03\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0))\n face(d, (cx - hw + m, hy0 + m, cx + hw - m, hy1 - m * 1.1), expr, glow)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(h * 0.012)))\n\ndef ss_canvas(color):\n return Image.new('RGBA', (W * SS, H * SS), color + (255,))\n\ndef finish(img):\n return img.convert('RGB').resize((W, H), Image.LANCZOS)\n\n# ---------------------------------------------------------------- 1. illustration\ndef illustration():\n S = SS; img = ss_canvas((228, 238, 235)); d = ImageDraw.Draw(img)\n floor_y = 700 * S\n d.rectangle((0, floor_y, W * S, H * S), fill=(214, 207, 194))\n d.rectangle((0, floor_y - 6 * S, W * S, floor_y), fill=(196, 208, 204)) # skirting\n d.rectangle((0, 560 * S, W * S, 568 * S), fill=(180, 196, 192)) # handrail\n for x in range(0, W, 520): # doors\n dx = (x + 140) * S\n d.rounded_rectangle((dx, 300 * S, dx + 190 * S, floor_y - 6 * S), radius=10 * S, fill=(201, 219, 214))\n d.rounded_rectangle((dx + 55 * S, 360 * S, dx + 135 * S, 470 * S), radius=8 * S, fill=(237, 245, 243))\n d.rectangle((dx + 150 * S, 520 * S, dx + 170 * S, 530 * S), fill=(150, 168, 164))\n # room number plate (blank)\n d.rounded_rectangle((dx + 60 * S, 270 * S, dx + 130 * S, 290 * S), radius=4 * S, fill=(196, 71, 31))\n for x in (400, 1460): # plants\n px = x * S\n d.rounded_rectangle((px, 640 * S, px + 60 * S, floor_y), radius=6 * S, fill=(196, 160, 120))\n for a, r in ((-30, 46), (0, 60), (30, 46), (-60, 36), (60, 36)):\n ex = px + 30 * S + math.sin(math.radians(a)) * r * S; ey = 640 * S - math.cos(math.radians(a)) * r * S\n d.ellipse((ex - 22 * S, ey - 34 * S, ex + 22 * S, ey + 34 * S), fill=(110, 170, 130))\n # floor reflection lines\n for x in range(0, W, 90):\n d.line(((x + 30) * S, (floor_y + 40 * S), (x + 70) * S, (floor_y + 40 * S)), fill=(226, 220, 208), width=4 * S)\n # Hakobu, carrying a medicine tray on its head\n cx, base, h = 1080 * S, 736 * S, 300 * S\n hakobu(img, cx, base, h, 'smile')\n d = ImageDraw.Draw(img)\n # motion lines behind\n for k, yy in enumerate((520, 580, 640, 690)):\n d.rounded_rectangle(((880 - k * 25) * S, yy * S, (960 - k * 10) * S, (yy + 8) * S), radius=4 * S, fill=(190, 206, 202))\n return finish(img)\n\n# ---------------------------------------------------------------- 2. 4-koma manga style (wordless)\ndef to_manga(im):\n g = ImageOps.grayscale(im)\n g = ImageOps.autocontrast(g, cutoff=1)\n e = g.filter(ImageFilter.FIND_EDGES).point(lambda v: 255 if v > 28 else 0).filter(ImageFilter.MaxFilter(3))\n a = np.array(g).astype(int)\n out = np.full(a.shape, 255, np.uint8)\n yy, xx = np.mgrid[0:a.shape[0], 0:a.shape[1]]\n dots = ((xx % 7 - 3) ** 2 + (yy % 7 - 3) ** 2) <= 3\n out[(a < 215) & dots] = 0\n out[a < 70] = 0\n out[np.array(e) > 0] = 0\n return Image.fromarray(out).convert('RGB')\n\ndef manga():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 12):\n for x in range(0, W, 12):\n dist = abs(x - W / 2) / (W / 2); r = 2.6 * max(0, dist - 0.4) / 0.6\n if r > 0.4: d.ellipse((x - r, y - r, x + r, y + r), fill=(205, 205, 203))\n seq = [(1, 'smile'), (8, 'surprise'), (15, 'blink'), (9, 'joy')]\n pw, ph, gap = 290, 300, 18\n x = (W - (4 * pw + 3 * gap)) // 2; y = (BAND[1] + BAND[3]) // 2 - ph // 2\n for i, (fi, fx) in enumerate(seq):\n crop = frame(fi).crop((0, 730, 380, 1080))\n if i % 2 == 1: crop = ImageOps.mirror(crop)\n panel = ImageOps.fit(to_manga(crop), (pw, ph), Image.LANCZOS, centering=(0.5, 0.15))\n pd = ImageDraw.Draw(panel)\n if fx == 'surprise': # focus lines + exclamation mark\n for a in range(0, 360, 9):\n r0, r1 = 150, 260; ca, sa = math.cos(math.radians(a)), math.sin(math.radians(a))\n pd.line((pw / 2 + ca * r0, ph * 0.35 + sa * r0, pw / 2 + ca * r1, ph * 0.35 + sa * r1), fill=INK, width=2)\n pd.polygon([(250, 20), (268, 20), (263, 70), (255, 70)], fill=INK); pd.ellipse((252, 78, 266, 92), fill=INK)\n if fx == 'blink': # sweat drop\n pd.polygon([(60, 40), (48, 70), (72, 70)], fill=(255, 255, 255), outline=INK)\n pd.ellipse((46, 58, 74, 86), fill=(255, 255, 255), outline=INK, width=2)\n if fx == 'joy': # sparkles\n for sx, sy, s in ((40, 40, 18), (255, 60, 14), (60, 120, 10)):\n pd.polygon([(sx, sy - s), (sx + s * 0.25, sy - s * 0.25), (sx + s, sy), (sx + s * 0.25, sy + s * 0.25),\n (sx, sy + s), (sx - s * 0.25, sy + s * 0.25), (sx - s, sy), (sx - s * 0.25, sy - s * 0.25)], fill=INK)\n c.paste(panel, (x, y))\n d.rectangle((x - 4, y - 4, x + pw + 3, y + ph + 3), outline=INK, width=5)\n x += pw + gap\n x0 = (W - (4 * pw + 3 * gap)) // 2\n for k in range(24):\n yy = y + 10 + k * (ph - 20) / 23; ln = 90 + (k * 37 % 5) * 45\n d.line((x + 20, yy, x + 20 + ln, yy), fill=INK, width=3)\n d.line((x0 - 40 - ln, yy, x0 - 40, yy), fill=INK, width=3)\n return c\n\n# ---------------------------------------------------------------- 3. realistic (3D render line-up)\ndef realistic():\n c = Image.new('RGB', (W, H), CREAM)\n order = [15, 8, 1, 9, 4]\n step = 390; x = W // 2 - 2 * step - 190\n fade = Image.new('L', (380, 350), 255); fd = ImageDraw.Draw(fade)\n for k in range(90):\n fd.line((0, 349 - k, 380, 349 - k), fill=int(255 * k / 90))\n for i, fi in enumerate(order):\n crop = frame(fi).crop((0, 730, 380, 1080))\n if i < 2: crop = ImageOps.mirror(crop) # left side looks toward the centre\n c.paste(crop, (x, 412), fade)\n x += step\n # soft floor line\n d = ImageDraw.Draw(c, 'RGBA')\n return c\n\n# ---------------------------------------------------------------- 4. photo (hospital corridor)\ndef photo():\n src = Image.open('/home/claude/banner/site/c_drug.png').convert('RGB')\n sw = int(src.width * 0.30)\n pano = Image.new('RGB', (sw * 2, src.height))\n pano.paste(src.crop((0, 0, sw, src.height)), (0, 0)); pano.paste(src.crop((src.width - sw, 0, src.width, src.height)), (sw, 0))\n bg = pano.resize((W, H), Image.BICUBIC).filter(ImageFilter.GaussianBlur(24))\n bg = Image.blend(bg, Image.new('RGB', (W, H), (238, 236, 232)), 0.2)\n h = 400; s = h / src.height; w = int(src.width * s)\n sm = src.resize((w, h), Image.LANCZOS)\n m = Image.new('L', (w, h), 0); ImageDraw.Draw(m).rectangle((50, 35, w - 50, h - 35), fill=255)\n bg.paste(sm, (W // 2 - w // 2 + 160, 390), m.filter(ImageFilter.GaussianBlur(38)))\n return bg\n\n# ---------------------------------------------------------------- 5. graphic design (face as geometry)\ndef graphic():\n S = SS; img = ss_canvas(CREAM); d = ImageDraw.Draw(img)\n for gx in range(0, W, 32):\n d.line((gx * S, 0, gx * S, H * S), fill=(232, 232, 226), width=S)\n for gy in range(0, H, 32):\n d.line((0, gy * S, W * S, gy * S), fill=(232, 232, 226), width=S)\n # huge display, face fills the band\n box = (712 * S, 352 * S, 1336 * S, 792 * S)\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0))\n face(d, box, 'smile', glow)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(14 * S)))\n d = ImageDraw.Draw(img)\n # pixel squares leaving the display (left) – echo of the brand mark\n random.seed(5); cell = 34\n for gx in range(520, 712, cell):\n for gy in range(424, 740, cell):\n if random.random() < (gx - 520) / 200:\n d.rectangle((gx * S, gy * S, (gx + cell - 6) * S, (gy + cell - 6) * S), fill=INK)\n # orange dimension lines (right) and blue dot (ring) accent\n ox = 1400\n d.line((ox * S, 430 * S, ox * S, 730 * S), fill=ORANGE, width=4 * S)\n for yy in (430, 730):\n d.line(((ox - 18) * S, yy * S, (ox + 18) * S, yy * S), fill=ORANGE, width=4 * S)\n d.polygon([(ox * S, 430 * S), ((ox - 10) * S, 452 * S), ((ox + 10) * S, 452 * S)], fill=ORANGE)\n d.polygon([(ox * S, 730 * S), ((ox - 10) * S, 708 * S), ((ox + 10) * S, 708 * S)], fill=ORANGE)\n d.ellipse((1460 * S, 520 * S, 1580 * S, 640 * S), outline=RING, width=10 * S)\n for gx in range(1640, 1900, 22):\n for gy in range(440, 720, 22):\n d.ellipse(((gx - 3) * S, (gy - 3) * S, (gx + 3) * S, (gy + 3) * S), fill=INK)\n return finish(img)\n\nitems = [('1_illustration', illustration), ('2_manga', manga), ('3_realistic', realistic),\n ('4_photo', photo), ('5_graphic', graphic)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'hakobu_banner_{name}_2048x1152.jpg', quality=92, subsampling=0)\n im.crop(BAND).save(OUT + f'_band_{name}.png')\n print(name)</code></pre><h3 id=\"index_TtkMtlZh\">make4.py(4回目の4案)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps\n\nOUT = '/home/claude/banner/hakobu2/'\nos.makedirs(OUT, exist_ok=True)\nW, H = 2048, 1152\nBAND = (0, 407, 2048, 745)\nCREAM = (245, 245, 240); INK = (13, 13, 13); ORANGE = (196, 71, 31)\nMOUTH = (87, 181, 248); RING = (140, 226, 172)\nSITE = '/home/claude/banner/site/'\n\n# ------------------------------------------------------------ three-view sheet\nsrc = Image.open('/home/claude/banner/sheet/src.png').convert('RGB')\ng = np.array(src.convert('L')).astype(int)\nclean = np.array(src).astype(int)\nclean[g > 212] = 255 # remove faint paper pattern\n# colour accents: mouth blue, side ring green\nyy, xx = np.mgrid[0:g.shape[0], 0:g.shape[1]]\nmouth = (xx >= 184) & (xx <= 215) & (yy >= 490) & (yy <= 505) & (g > 70) & (g < 200)\nclean[mouth] = MOUTH\nr = np.hypot(xx - 554, yy - 663)\nring = (r >= 50.5) & (r <= 62.5) & (g > 95)\nclean[ring] = RING\nsheet = Image.fromarray(clean.astype(np.uint8))\nVIEWS = {'front': (64, 409, 338, 881), 'side': (390, 413, 705, 881), 'back': (743, 414, 1019, 882)}\n\ndef view(name):\n \"\"\"RGBA cut-out of one view (white outside the outline becomes transparent)\"\"\"\n box = VIEWS[name]\n im = sheet.crop(box)\n gg = np.array(im.convert('L'))\n barrier = Image.fromarray(np.where(gg < 200, 0, 255).astype(np.uint8)).filter(ImageFilter.MinFilter(5))\n ff = barrier.copy()\n for p in ((0, 0), (ff.width - 1, 0), (0, ff.height - 1), (ff.width - 1, ff.height - 1)):\n if ff.getpixel(p) == 255:\n ImageDraw.floodfill(ff, p, 128)\n m = np.where(np.array(ff) == 128, 0, 255).astype(np.uint8)\n mask = Image.fromarray(m).filter(ImageFilter.MinFilter(3)).filter(ImageFilter.GaussianBlur(0.7))\n out = im.convert('RGBA'); out.putalpha(mask)\n return out\n\ndef scaled(im, h):\n return im.resize((int(im.width * h / im.height), h), Image.LANCZOS)\n\ndef manga_crop(n, box):\n return Image.open(SITE + f'c_manga{n}.png').convert('RGB').crop(box)\n\ndef grid(c, step=32, major=160):\n d = ImageDraw.Draw(c)\n for x in range(0, W, step):\n d.line((x, 0, x, H), fill=(214, 214, 208) if x % major == 0 else (230, 230, 224), width=1)\n for y in range(0, H, step):\n d.line((0, y, W, y), fill=(214, 214, 208) if y % major == 0 else (230, 230, 224), width=1)\n\ndef arrow_dim(d, x, y0, y1, col=ORANGE, w=3):\n d.line((x, y0, x, y1), fill=col, width=w)\n for yy_, s in ((y0, 1), (y1, -1)):\n d.line((x - 12, yy_, x + 12, yy_), fill=col, width=w)\n d.polygon([(x, yy_), (x - 7, yy_ + 16 * s), (x + 7, yy_ + 16 * s)], fill=col)\n\ndef hdim(d, x0, x1, y, col=ORANGE, w=3):\n d.line((x0, y, x1, y), fill=col, width=w)\n for xx_, s in ((x0, 1), (x1, -1)):\n d.line((xx_, y - 12, xx_, y + 12), fill=col, width=w)\n d.polygon([(xx_, y), (xx_ + 16 * s, y - 7), (xx_ + 16 * s, y + 7)], fill=col)\n\ndef dashdot(d, x, y0, y1, col=(170, 170, 165)):\n y = y0\n while y < y1:\n d.line((x, y, x, min(y1, y + 22)), fill=col, width=2); y += 30\n d.line((x, y, x, min(y1, y + 3)), fill=col, width=2); y += 10\n\ndef circle_img(c, im, cx, cy, r, border=5):\n tile = ImageOps.fit(im, (2 * r, 2 * r), Image.LANCZOS)\n m = Image.new('L', tile.size, 0); ImageDraw.Draw(m).ellipse((0, 0, 2 * r - 1, 2 * r - 1), fill=255)\n c.paste(tile, (cx - r, cy - r), m)\n ImageDraw.Draw(c).ellipse((cx - r, cy - r, cx + r, cy + r), outline=INK, width=border)\n\n# ============================================================ I-a three-view design sheet\ndef illust_threeview():\n c = Image.new('RGB', (W, H), CREAM); grid(c); d = ImageDraw.Draw(c)\n h = 300; ground = 728\n xs = {'front': 760, 'back': 1060, 'side': 1360}\n for n, cx in xs.items():\n v = scaled(view(n), h)\n c.paste(v, (cx - v.width // 2, ground - h), v)\n dashdot(d, cx, ground - h - 6, ground + 6)\n d.line((560, ground, 1520, ground), fill=INK, width=3)\n fv = scaled(view('front'), h)\n arrow_dim(d, xs['front'] - fv.width // 2 - 40, ground - h, ground)\n sv = scaled(view('side'), h)\n hdim(d, xs['side'] - sv.width // 2, xs['side'] + sv.width // 2, ground - h - 2 + 0, ORANGE)\n # detail circles: face (left) and side ring (right)\n face = sheet.crop((100, 425, 300, 560))\n circle_img(c, face, 520, 576, 118)\n d.line((520 + 118, 576, xs['front'] - 40, 500), fill=ORANGE, width=3)\n ringc = sheet.crop((474, 583, 634, 743))\n circle_img(c, ringc, 1580, 576, 118)\n d.line((1580 - 118, 576, 1388, 610), fill=ORANGE, width=3)\n # dots at the far edges\n for gx in range(60, 330, 22):\n for gy in range(460, 700, 22):\n d.ellipse((gx - 3, gy - 3, gx + 3, gy + 3), fill=(120, 120, 116))\n for gx in range(1740, 2000, 22):\n for gy in range(460, 700, 22):\n d.ellipse((gx - 3, gy - 3, gx + 3, gy + 3), fill=(120, 120, 116))\n return c\n\n# ============================================================ I-b corridor illustration (line + flat colour)\ndef illust_corridor():\n c = Image.new('RGB', (W, H), (230, 239, 236)); d = ImageDraw.Draw(c)\n floor = 712\n d.rectangle((0, floor, W, H), fill=(234, 228, 216)); d.line((0, floor, W, floor), fill=INK, width=4)\n d.line((0, 590, W, 590), fill=INK, width=3); d.line((0, 600, W, 600), fill=INK, width=3)\n for i, x in enumerate(range(-80, W, 470)):\n d.rectangle((x, 330, x + 200, floor), fill=(206, 224, 219), outline=INK, width=4)\n d.rectangle((x + 60, 380, x + 140, 480), fill=(246, 250, 249), outline=INK, width=3)\n d.rounded_rectangle((x + 160, 540, x + 184, 556), radius=4, fill=INK)\n d.rectangle((x + 70, 300, x + 130, 318), fill=ORANGE, outline=INK, width=3)\n for x in (300, 1530):\n d.rounded_rectangle((x, 640, x + 64, floor), radius=6, fill=(214, 172, 128), outline=INK, width=3)\n for a, rr in ((-35, 50), (0, 66), (35, 50)):\n ex = x + 32 + math.sin(math.radians(a)) * rr; ey = 640 - math.cos(math.radians(a)) * rr\n d.ellipse((ex - 24, ey - 38, ex + 24, ey + 38), fill=(132, 190, 150), outline=INK, width=3)\n for x in range(40, W, 110):\n d.line((x, floor + 30, x + 50, floor + 30), fill=(210, 202, 188), width=4)\n # Hakobu (side view, moving left) + motion lines behind\n v = scaled(view('side'), 296)\n hx = 1000; c.paste(v, (hx - v.width // 2, floor + 10 - v.height), v)\n d = ImageDraw.Draw(c)\n for k, yy_ in enumerate((470, 530, 600, 660)):\n x0 = hx + v.width // 2 + 30 + (k % 2) * 30\n d.line((x0, yy_, x0 + 120 - k * 15, yy_), fill=INK, width=4)\n # a second Hakobu waiting at the far door (front view, small)\n f = scaled(view('front'), 170)\n c.paste(f, (1340, floor + 4 - f.height), f)\n return c\n\n# ============================================================ M-a manga page, slanted panels\ndef manga_page():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 12):\n for x in range(0, W, 12):\n d.ellipse((x - 1.6, y - 1.6, x + 1.6, y + 1.6), fill=(212, 212, 210))\n top, bot, sl = 418, 734, 36\n cuts = [-40, 392, 790, 1258, 1656, 2090]\n imgs = [manga_crop(3, (0, 40, 561, 470)), manga_crop(0, (0, 60, 561, 536)), None,\n manga_crop(1, (250, 0, 561, 400)), manga_crop(2, (100, 0, 561, 400))]\n for i in range(5):\n xl, xr = cuts[i] + 8, cuts[i + 1] - 8\n poly = [(xl + sl, top), (xr + sl, top), (xr - sl, bot), (xl - sl, bot)]\n bw, bh = xr - xl + 2 * sl, bot - top\n if imgs[i] is None: # centre: Hakobu front with focus lines\n tile = Image.new('RGB', (bw, bh), (255, 255, 255)); td = ImageDraw.Draw(tile)\n cx0, cy0 = bw / 2, bh * 0.45\n for a in range(0, 360, 5):\n ca, sa = math.cos(math.radians(a)), math.sin(math.radians(a))\n r0 = 150 + (a * 7 % 40)\n td.line((cx0 + ca * r0, cy0 + sa * r0, cx0 + ca * 600, cy0 + sa * 600), fill=INK, width=2)\n v = scaled(view('front'), bh - 26)\n tile.paste(v, (int(cx0 - v.width / 2), bh - v.height - 4), v)\n else:\n tile = ImageOps.fit(imgs[i], (bw, bh), Image.LANCZOS, centering=(0.5, 0.4))\n m = Image.new('L', (W, H), 0); ImageDraw.Draw(m).polygon(poly, fill=255)\n layer = Image.new('RGB', (W, H)); layer.paste(tile, (xl - sl, top))\n c.paste(layer, (0, 0), m)\n ImageDraw.Draw(c).polygon(poly, outline=INK, width=5)\n return c\n\n# ============================================================ M-b cast introduction\ndef manga_cast():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 11):\n for x in range(0, W, 11):\n rr = 2.2 if (x // 11 + y // 11) % 2 == 0 else 1.2\n d.ellipse((x - rr, y - rr, x + rr, y + rr), fill=(218, 218, 216))\n cy = 578\n # starburst behind Hakobu\n pts = []\n for k in range(36):\n a = math.radians(k * 10); rr = 180 if k % 2 == 0 else 128\n pts.append((1024 + math.cos(a) * rr * 0.95, cy + math.sin(a) * rr))\n d.polygon(pts, fill=(255, 255, 255), outline=INK, width=4)\n v = scaled(view('front'), 316)\n c.paste(v, (1024 - v.width // 2, 736 - v.height), v)\n # cast circles\n cast = [(520, manga_crop(1, (300, 0, 540, 240))), # nurse\n (770, manga_crop(0, (360, 40, 561, 241))), # engineer\n (1278, manga_crop(2, (160, 20, 380, 240))), # arm robot with a face\n (1528, manga_crop(3, (40, 20, 561, 541)))] # the twin arms\n for x, im in cast:\n circle_img(c, im, x, cy, 112, 5)\n d = ImageDraw.Draw(c)\n # speed lines at both ends\n for k in range(22):\n yy_ = 440 + k * 13; ln = 80 + (k * 37 % 5) * 40\n d.line((1680, yy_, 1680 + ln, yy_), fill=INK, width=3)\n d.line((368 - ln, yy_, 368, yy_), fill=INK, width=3)\n return c\n\nitems = [('I1_threeview', illust_threeview), ('I2_corridor', illust_corridor),\n ('M1_manga_page', manga_page), ('M2_cast', manga_cast)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'hakobu_banner_{name}_2048x1152.jpg', quality=92, subsampling=0)\n im.crop(BAND).save(OUT + f'_band_{name}.png')\n print(name)</code></pre><h3 id=\"index_fHfez7_y\">make5.py(5回目の3案)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps\n\n_src = open('/home/claude/banner/make3.py').read()\nexec(_src[:_src.index(\"items = [\")]) # reuse frame(), to_manga(), constants\n\nOUT = '/home/claude/banner/hakobu3/'\nos.makedirs(OUT, exist_ok=True)\nSITE = '/home/claude/banner/site/'\nDARK = (40, 40, 42); BASE = (205, 201, 192); BASE2 = (184, 180, 171)\nSHELL = (247, 246, 242); SHELL2 = (226, 223, 215); SHELL_L = (251, 250, 247); SEAM = (214, 211, 203)\n\n# ------------------------------------------------------------ Hakobu redrawn from the three-view sheet\nclass Pen:\n \"\"\"maps three-view sheet coordinates to the canvas\"\"\"\n def __init__(self, img, x0, y0, ox, oy, s, line=None, lw=0):\n self.d = ImageDraw.Draw(img); self.x0, self.y0, self.ox, self.oy, self.s = x0, y0, ox, oy, s\n self.line, self.lw = line, lw\n def p(self, x, y): return (self.ox + (x - self.x0) * self.s, self.oy + (y - self.y0) * self.s)\n def box(self, b): a = self.p(b[0], b[1]); c = self.p(b[2], b[3]); return (a[0], a[1], c[0], c[1])\n def rr(self, b, r, fill, outline=True):\n self.d.rounded_rectangle(self.box(b), radius=r * self.s, fill=fill,\n outline=self.line if (outline and self.line) else None, width=int(self.lw))\n def poly(self, pts, fill, outline=True):\n self.d.polygon([self.p(*q) for q in pts], fill=fill, outline=self.line if (outline and self.line) else None,\n width=int(self.lw) if self.line else 1)\n def ell(self, b, fill, outline=True):\n self.d.ellipse(self.box(b), fill=fill, outline=self.line if (outline and self.line) else None, width=int(self.lw))\n def ln(self, a, b, fill, w):\n self.d.line((self.p(*a), self.p(*b)), fill=fill, width=max(1, int(w * self.s)))\n\ndef hakobu_front(img, cx, ground, h, line=None, lw=0, expr='smile'):\n s = h / 461.0\n pen = Pen(img, 200, 874, cx, ground, s, line, lw)\n seam = line or SEAM\n pen.rr((95, 836, 120, 874), 6, DARK); pen.rr((280, 836, 305, 874), 6, DARK) # wheels\n pen.rr((72, 806, 328, 848), 12, BASE) # base\n pen.rr((76, 505, 324, 816), 26, SHELL) # body\n pen.rr((78, 525, 98, 800), 8, SHELL2, False); pen.rr((302, 525, 322, 800), 8, SHELL2, False)\n pen.rr((100, 413, 300, 566), 34, SHELL) # head\n pen.rr((112, 546, 288, 566), 10, SHELL2, False)\n pen.rr((120, 427, 283, 541), 12, DISP) # display\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gp = Pen(glow, 200, 874, cx, ground, s)\n for ex in (152, 241):\n gp.rr((ex - 17, 460, ex + 17, 512), 17, (246, 248, 252, 110), False)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(4 * s)))\n pen.d = ImageDraw.Draw(img)\n for ex in (152, 241):\n if expr == 'blink':\n pen.rr((ex - 13, 484, ex + 13, 490), 3, EYE, False)\n else:\n pen.rr((ex - 11, 466, ex + 11, 506), 11, EYE, False)\n pen.d.chord(pen.box((191, 484, 217, 510)), 0, 180, fill=MOUTH) # mouth\n pen.ln((80, 588), (320, 588), seam, 2.2)\n pen.rr((167, 606, 225, 630), 11, DARK, False) # drawer slot\n pen.d.arc(pen.box((78, 668, 322, 730)), 15, 165, fill=seam, width=max(1, int(2.2 * s)))\n pen.rr((78, 767, 322, 786), 6, DARK, False) # black stripe\n pen.rr((177, 796, 224, 810), 7, DARK, False)\n\ndef hakobu_side(img, cx, ground, h, line=None, lw=0):\n \"\"\"side view, front of the robot on the left\"\"\"\n s = h / 461.0\n pen = Pen(img, 548, 874, cx, ground, s, line, lw)\n pen.ell((426, 830, 472, 876), DARK); pen.ell((638, 830, 684, 876), DARK) # wheels\n pen.rr((404, 798, 692, 862), 18, BASE)\n pen.rr((468, 808, 626, 860), 14, BASE2, False)\n pen.poly([(430, 505), (418, 560), (410, 700), (404, 805), (692, 805), (684, 515), (656, 505)], SHELL)\n pen.poly([(455, 418), (628, 418), (650, 432), (657, 505), (430, 505)], SHELL) # head\n pen.poly([(442, 464), (654, 464), (657, 505), (431, 505)], SHELL2, False)\n pen.rr((413, 596, 441, 720), 8, SHELL2, False) # front strip\n pen.rr((446, 526, 672, 800), 70, SHELL_L) # side panel\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gp = Pen(glow, 548, 874, cx, ground, s)\n gp.ell((554 - 74, 663 - 74, 554 + 74, 663 + 74), RING + (90,), False)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(10 * s)))\n pen.d = ImageDraw.Draw(img)\n pen.ell((554 - 62, 663 - 62, 554 + 62, 663 + 62), RING) # ring\n pen.ell((554 - 50, 663 - 50, 554 + 50, 663 + 50), SHELL_L)\n pen.rr((408, 768, 456, 790), 8, DARK) # bumper\n pen.rr((410, 790, 452, 800), 4, (200, 200, 198), False)\n\n# ------------------------------------------------------------ I-flat: flat corridor (no outlines)\ndef corridor_flat():\n S = SS; img = ss_canvas((228, 238, 235)); d = ImageDraw.Draw(img)\n floor = 700 * S\n d.rectangle((0, floor, W * S, H * S), fill=(214, 207, 194))\n d.rectangle((0, floor - 6 * S, W * S, floor), fill=(196, 208, 204))\n d.rectangle((0, 560 * S, W * S, 568 * S), fill=(180, 196, 192))\n for x in range(0, W, 520):\n dx = (x + 140) * S\n d.rounded_rectangle((dx, 300 * S, dx + 190 * S, floor - 6 * S), radius=10 * S, fill=(201, 219, 214))\n d.rounded_rectangle((dx + 55 * S, 360 * S, dx + 135 * S, 470 * S), radius=8 * S, fill=(237, 245, 243))\n d.rectangle((dx + 150 * S, 520 * S, dx + 170 * S, 530 * S), fill=(150, 168, 164))\n d.rounded_rectangle((dx + 60 * S, 270 * S, dx + 130 * S, 290 * S), radius=4 * S, fill=ORANGE)\n for x in (400, 1460):\n px = x * S\n d.rounded_rectangle((px, 640 * S, px + 60 * S, floor), radius=6 * S, fill=(196, 160, 120))\n for a, rr_ in ((-30, 46), (0, 60), (30, 46), (-60, 36), (60, 36)):\n ex = px + 30 * S + math.sin(math.radians(a)) * rr_ * S; ey = 640 * S - math.cos(math.radians(a)) * rr_ * S\n d.ellipse((ex - 22 * S, ey - 34 * S, ex + 22 * S, ey + 34 * S), fill=(110, 170, 130))\n for x in range(0, W, 90):\n d.line(((x + 30) * S, floor + 40 * S, (x + 70) * S, floor + 40 * S), fill=(226, 220, 208), width=4 * S)\n # moving Hakobu (side) + motion lines, and one waiting at a door (front)\n hakobu_side(img, 660 * S, 706 * S, 210 * S)\n d = ImageDraw.Draw(img)\n for k, yy_ in enumerate((540, 590, 640)):\n x0 = (762 + (k % 2) * 22) * S\n d.rounded_rectangle((x0, yy_ * S, x0 + (100 - k * 14) * S, (yy_ + 8) * S), radius=4 * S, fill=(176, 194, 190))\n hakobu_front(img, 1080 * S, 730 * S, 300 * S)\n return finish(img)\n\n# ------------------------------------------------------------ I-line: line + flat colour corridor\ndef corridor_line():\n S = SS; img = ss_canvas((230, 239, 236)); d = ImageDraw.Draw(img); L = 4 * S\n floor = 712 * S\n d.rectangle((0, floor, W * S, H * S), fill=(234, 228, 216)); d.line((0, floor, W * S, floor), fill=INK, width=L)\n d.line((0, 590 * S, W * S, 590 * S), fill=INK, width=3 * S); d.line((0, 600 * S, W * S, 600 * S), fill=INK, width=3 * S)\n for x in range(-80, W, 470):\n d.rectangle((x * S, 330 * S, (x + 200) * S, floor), fill=(206, 224, 219), outline=INK, width=L)\n d.rectangle(((x + 60) * S, 380 * S, (x + 140) * S, 480 * S), fill=(246, 250, 249), outline=INK, width=3 * S)\n d.rounded_rectangle(((x + 160) * S, 540 * S, (x + 184) * S, 556 * S), radius=4 * S, fill=INK)\n d.rectangle(((x + 70) * S, 300 * S, (x + 130) * S, 318 * S), fill=ORANGE, outline=INK, width=3 * S)\n for x in (300, 1530):\n d.rounded_rectangle((x * S, 640 * S, (x + 64) * S, floor), radius=6 * S, fill=(214, 172, 128), outline=INK, width=3 * S)\n for a, rr_ in ((-35, 50), (0, 66), (35, 50)):\n ex = (x + 32 + math.sin(math.radians(a)) * rr_) * S; ey = (640 - math.cos(math.radians(a)) * rr_) * S\n d.ellipse((ex - 24 * S, ey - 38 * S, ex + 24 * S, ey + 38 * S), fill=(132, 190, 150), outline=INK, width=3 * S)\n for x in range(40, W, 110):\n d.line(((x) * S, (floor + 30 * S), (x + 50) * S, (floor + 30 * S)), fill=(210, 202, 188), width=4 * S)\n hakobu_side(img, 660 * S, 714 * S, 210 * S, line=INK, lw=3 * S)\n d = ImageDraw.Draw(img)\n for k, yy_ in enumerate((545, 600, 655)):\n x0 = (770 + (k % 2) * 24) * S\n d.line((x0, yy_ * S, x0 + (100 - k * 14) * S, yy_ * S), fill=INK, width=L)\n hakobu_front(img, 1080 * S, 734 * S, 300 * S, line=INK, lw=3.5 * S)\n return finish(img)\n\ndef ink_manga(im):\n g = ImageOps.autocontrast(ImageOps.grayscale(im), cutoff=2)\n a = np.array(g).astype(int)\n yy_, xx_ = np.mgrid[0:a.shape[0], 0:a.shape[1]]\n dots = ((xx_ % 7 - 3) ** 2 + (yy_ % 7 - 3) ** 2) <= 3\n out = np.full(a.shape, 255, np.uint8)\n out[(a < 205) & dots] = 0\n out[a < 120] = 0\n return Image.fromarray(out).convert('RGB')\n\n# ------------------------------------------------------------ manga: v1 touch, different characters per panel\ndef manga_cast_strip():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 16):\n for x in range(0, W, 16):\n dist = abs(x - W / 2) / (W / 2); r = 3.2 * max(0, dist - 0.35) / 0.65\n if r > 0.4: d.ellipse((x - r, y - r, x + r, y + r), fill=(200, 200, 198))\n mg = lambda n, b: Image.open(SITE + f'c_manga{n}.png').convert('RGB').crop(b)\n panels = [\n (frame(1).crop((0, 730, 380, 1080)), 'none', (0.5, 0.15)), # Hakobu\n (mg(1, (290, 0, 545, 330)), 'surprise', (0.5, 0.1)), # nurse\n (mg(0, (330, 30, 561, 330)), 'sweat', (0.5, 0.1)), # engineer\n (mg(2, (170, 0, 400, 270)), 'joy', (0.5, 0.5)), # arm robot with a face\n ]\n pw, ph, gap = 290, 300, 18\n x = (W - (4 * pw + 3 * gap)) // 2; y = (BAND[1] + BAND[3]) // 2 - ph // 2\n for i, (im, fx, cen) in enumerate(panels):\n big = ImageOps.fit(im, (pw, ph), Image.LANCZOS, centering=cen)\n panel = to_manga(big) if i == 0 else ink_manga(big)\n pd = ImageDraw.Draw(panel)\n if fx == 'surprise':\n for a in range(0, 360, 9):\n ca, sa = math.cos(math.radians(a)), math.sin(math.radians(a))\n pd.line((pw / 2 + ca * 165, ph * 0.3 + sa * 165, pw / 2 + ca * 260, ph * 0.3 + sa * 260), fill=INK, width=2)\n pd.polygon([(30, 20), (48, 20), (43, 70), (35, 70)], fill=INK); pd.ellipse((32, 78, 46, 92), fill=INK)\n if fx == 'sweat':\n pd.polygon([(40, 34), (28, 64), (52, 64)], fill=(255, 255, 255), outline=INK)\n pd.ellipse((26, 52, 54, 80), fill=(255, 255, 255), outline=INK, width=2)\n if fx == 'joy':\n for sx, sy, s in ((40, 40, 18), (255, 60, 14), (60, 120, 10)):\n pd.polygon([(sx, sy - s), (sx + s * 0.25, sy - s * 0.25), (sx + s, sy), (sx + s * 0.25, sy + s * 0.25),\n (sx, sy + s), (sx - s * 0.25, sy + s * 0.25), (sx - s, sy), (sx - s * 0.25, sy - s * 0.25)], fill=INK)\n c.paste(panel, (x, y))\n d.rectangle((x - 4, y - 4, x + pw + 3, y + ph + 3), outline=INK, width=5)\n x += pw + gap\n x0 = (W - (4 * pw + 3 * gap)) // 2\n for k in range(24):\n yy_ = y + 10 + k * (ph - 20) / 23; ln = 90 + (k * 37 % 5) * 45\n d.line((x + 20, yy_, x + 20 + ln, yy_), fill=INK, width=3)\n d.line((x0 - 40 - ln, yy_, x0 - 40, yy_), fill=INK, width=3)\n return c\n\nitems = [('illust_flat', corridor_flat), ('illust_line', corridor_line), ('manga_cast', manga_cast_strip)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'hakobu_banner_{name}_2048x1152.jpg', quality=92, subsampling=0)\n im.crop(BAND).save(OUT + f'_band_{name}.png')\n print(name)</code></pre><h3 id=\"index_sTlRYvOF\">make5_single.py(決定版のバナー)</h3><pre><code class=\"language-python\">import os, math, random\nimport numpy as np\nfrom PIL import Image, ImageDraw, ImageFilter, ImageOps\n\n_src = open('/home/claude/banner/make3.py').read()\nexec(_src[:_src.index(\"items = [\")]) # reuse frame(), to_manga(), constants\n\nOUT = '/home/claude/banner/hakobu3/'\nos.makedirs(OUT, exist_ok=True)\nSITE = '/home/claude/banner/site/'\nDARK = (40, 40, 42); BASE = (205, 201, 192); BASE2 = (184, 180, 171)\nSHELL = (247, 246, 242); SHELL2 = (226, 223, 215); SHELL_L = (251, 250, 247); SEAM = (214, 211, 203)\n\n# ------------------------------------------------------------ Hakobu redrawn from the three-view sheet\nclass Pen:\n \"\"\"maps three-view sheet coordinates to the canvas\"\"\"\n def __init__(self, img, x0, y0, ox, oy, s, line=None, lw=0):\n self.d = ImageDraw.Draw(img); self.x0, self.y0, self.ox, self.oy, self.s = x0, y0, ox, oy, s\n self.line, self.lw = line, lw\n def p(self, x, y): return (self.ox + (x - self.x0) * self.s, self.oy + (y - self.y0) * self.s)\n def box(self, b): a = self.p(b[0], b[1]); c = self.p(b[2], b[3]); return (a[0], a[1], c[0], c[1])\n def rr(self, b, r, fill, outline=True):\n self.d.rounded_rectangle(self.box(b), radius=r * self.s, fill=fill,\n outline=self.line if (outline and self.line) else None, width=int(self.lw))\n def poly(self, pts, fill, outline=True):\n self.d.polygon([self.p(*q) for q in pts], fill=fill, outline=self.line if (outline and self.line) else None,\n width=int(self.lw) if self.line else 1)\n def ell(self, b, fill, outline=True):\n self.d.ellipse(self.box(b), fill=fill, outline=self.line if (outline and self.line) else None, width=int(self.lw))\n def ln(self, a, b, fill, w):\n self.d.line((self.p(*a), self.p(*b)), fill=fill, width=max(1, int(w * self.s)))\n\ndef hakobu_front(img, cx, ground, h, line=None, lw=0, expr='smile'):\n s = h / 461.0\n pen = Pen(img, 200, 874, cx, ground, s, line, lw)\n seam = line or SEAM\n pen.rr((95, 836, 120, 874), 6, DARK); pen.rr((280, 836, 305, 874), 6, DARK) # wheels\n pen.rr((72, 806, 328, 848), 12, BASE) # base\n pen.rr((76, 505, 324, 816), 26, SHELL) # body\n pen.rr((78, 525, 98, 800), 8, SHELL2, False); pen.rr((302, 525, 322, 800), 8, SHELL2, False)\n pen.rr((100, 413, 300, 566), 34, SHELL) # head\n pen.rr((112, 546, 288, 566), 10, SHELL2, False)\n pen.rr((120, 427, 283, 541), 12, DISP) # display\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gp = Pen(glow, 200, 874, cx, ground, s)\n for ex in (152, 241):\n gp.rr((ex - 17, 460, ex + 17, 512), 17, (246, 248, 252, 110), False)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(4 * s)))\n pen.d = ImageDraw.Draw(img)\n for ex in (152, 241):\n if expr == 'blink':\n pen.rr((ex - 13, 484, ex + 13, 490), 3, EYE, False)\n else:\n pen.rr((ex - 11, 466, ex + 11, 506), 11, EYE, False)\n pen.d.chord(pen.box((191, 484, 217, 510)), 0, 180, fill=MOUTH) # mouth\n pen.ln((80, 588), (320, 588), seam, 2.2)\n pen.rr((167, 606, 225, 630), 11, DARK, False) # drawer slot\n pen.d.arc(pen.box((78, 668, 322, 730)), 15, 165, fill=seam, width=max(1, int(2.2 * s)))\n pen.rr((78, 767, 322, 786), 6, DARK, False) # black stripe\n pen.rr((177, 796, 224, 810), 7, DARK, False)\n\ndef hakobu_side(img, cx, ground, h, line=None, lw=0):\n \"\"\"side view, front of the robot on the left\"\"\"\n s = h / 461.0\n pen = Pen(img, 548, 874, cx, ground, s, line, lw)\n pen.ell((426, 830, 472, 876), DARK); pen.ell((638, 830, 684, 876), DARK) # wheels\n pen.rr((404, 798, 692, 862), 18, BASE)\n pen.rr((468, 808, 626, 860), 14, BASE2, False)\n pen.poly([(430, 505), (418, 560), (410, 700), (404, 805), (692, 805), (684, 515), (656, 505)], SHELL)\n pen.poly([(455, 418), (628, 418), (650, 432), (657, 505), (430, 505)], SHELL) # head\n pen.poly([(442, 464), (654, 464), (657, 505), (431, 505)], SHELL2, False)\n pen.rr((413, 596, 441, 720), 8, SHELL2, False) # front strip\n pen.rr((446, 526, 672, 800), 70, SHELL_L) # side panel\n glow = Image.new('RGBA', img.size, (0, 0, 0, 0)); gp = Pen(glow, 548, 874, cx, ground, s)\n gp.ell((554 - 74, 663 - 74, 554 + 74, 663 + 74), RING + (90,), False)\n img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(10 * s)))\n pen.d = ImageDraw.Draw(img)\n pen.ell((554 - 62, 663 - 62, 554 + 62, 663 + 62), RING) # ring\n pen.ell((554 - 50, 663 - 50, 554 + 50, 663 + 50), SHELL_L)\n pen.rr((408, 768, 456, 790), 8, DARK) # bumper\n pen.rr((410, 790, 452, 800), 4, (200, 200, 198), False)\n\n# ------------------------------------------------------------ I-flat: flat corridor (no outlines)\ndef corridor_flat():\n S = SS; img = ss_canvas((228, 238, 235)); d = ImageDraw.Draw(img)\n floor = 700 * S\n d.rectangle((0, floor, W * S, H * S), fill=(214, 207, 194))\n d.rectangle((0, floor - 6 * S, W * S, floor), fill=(196, 208, 204))\n d.rectangle((0, 560 * S, W * S, 568 * S), fill=(180, 196, 192))\n for x in range(0, W, 520):\n dx = (x + 140) * S\n d.rounded_rectangle((dx, 300 * S, dx + 190 * S, floor - 6 * S), radius=10 * S, fill=(201, 219, 214))\n d.rounded_rectangle((dx + 55 * S, 360 * S, dx + 135 * S, 470 * S), radius=8 * S, fill=(237, 245, 243))\n d.rectangle((dx + 150 * S, 520 * S, dx + 170 * S, 530 * S), fill=(150, 168, 164))\n d.rounded_rectangle((dx + 60 * S, 270 * S, dx + 130 * S, 290 * S), radius=4 * S, fill=ORANGE)\n for x in (400, 1460):\n px = x * S\n d.rounded_rectangle((px, 640 * S, px + 60 * S, floor), radius=6 * S, fill=(196, 160, 120))\n for a, rr_ in ((-30, 46), (0, 60), (30, 46), (-60, 36), (60, 36)):\n ex = px + 30 * S + math.sin(math.radians(a)) * rr_ * S; ey = 640 * S - math.cos(math.radians(a)) * rr_ * S\n d.ellipse((ex - 22 * S, ey - 34 * S, ex + 22 * S, ey + 34 * S), fill=(110, 170, 130))\n for x in range(0, W, 90):\n d.line(((x + 30) * S, floor + 40 * S, (x + 70) * S, floor + 40 * S), fill=(226, 220, 208), width=4 * S)\n # moving Hakobu (side) + motion lines, and one waiting at a door (front)\n hakobu_front(img, 1015 * S, 730 * S, 300 * S)\n return finish(img)\n\n# ------------------------------------------------------------ I-line: line + flat colour corridor\ndef corridor_line():\n S = SS; img = ss_canvas((230, 239, 236)); d = ImageDraw.Draw(img); L = 4 * S\n floor = 712 * S\n d.rectangle((0, floor, W * S, H * S), fill=(234, 228, 216)); d.line((0, floor, W * S, floor), fill=INK, width=L)\n d.line((0, 590 * S, W * S, 590 * S), fill=INK, width=3 * S); d.line((0, 600 * S, W * S, 600 * S), fill=INK, width=3 * S)\n for x in range(-80, W, 470):\n d.rectangle((x * S, 330 * S, (x + 200) * S, floor), fill=(206, 224, 219), outline=INK, width=L)\n d.rectangle(((x + 60) * S, 380 * S, (x + 140) * S, 480 * S), fill=(246, 250, 249), outline=INK, width=3 * S)\n d.rounded_rectangle(((x + 160) * S, 540 * S, (x + 184) * S, 556 * S), radius=4 * S, fill=INK)\n d.rectangle(((x + 70) * S, 300 * S, (x + 130) * S, 318 * S), fill=ORANGE, outline=INK, width=3 * S)\n for x in (300, 1530):\n d.rounded_rectangle((x * S, 640 * S, (x + 64) * S, floor), radius=6 * S, fill=(214, 172, 128), outline=INK, width=3 * S)\n for a, rr_ in ((-35, 50), (0, 66), (35, 50)):\n ex = (x + 32 + math.sin(math.radians(a)) * rr_) * S; ey = (640 - math.cos(math.radians(a)) * rr_) * S\n d.ellipse((ex - 24 * S, ey - 38 * S, ex + 24 * S, ey + 38 * S), fill=(132, 190, 150), outline=INK, width=3 * S)\n for x in range(40, W, 110):\n d.line(((x) * S, (floor + 30 * S), (x + 50) * S, (floor + 30 * S)), fill=(210, 202, 188), width=4 * S)\n hakobu_side(img, 660 * S, 714 * S, 210 * S, line=INK, lw=3 * S)\n d = ImageDraw.Draw(img)\n for k, yy_ in enumerate((545, 600, 655)):\n x0 = (770 + (k % 2) * 24) * S\n d.line((x0, yy_ * S, x0 + (100 - k * 14) * S, yy_ * S), fill=INK, width=L)\n hakobu_front(img, 1080 * S, 734 * S, 300 * S, line=INK, lw=3.5 * S)\n return finish(img)\n\ndef ink_manga(im):\n g = ImageOps.autocontrast(ImageOps.grayscale(im), cutoff=2)\n a = np.array(g).astype(int)\n yy_, xx_ = np.mgrid[0:a.shape[0], 0:a.shape[1]]\n dots = ((xx_ % 7 - 3) ** 2 + (yy_ % 7 - 3) ** 2) <= 3\n out = np.full(a.shape, 255, np.uint8)\n out[(a < 205) & dots] = 0\n out[a < 120] = 0\n return Image.fromarray(out).convert('RGB')\n\n# ------------------------------------------------------------ manga: v1 touch, different characters per panel\ndef manga_cast_strip():\n c = Image.new('RGB', (W, H), (250, 250, 248)); d = ImageDraw.Draw(c)\n for y in range(0, H, 16):\n for x in range(0, W, 16):\n dist = abs(x - W / 2) / (W / 2); r = 3.2 * max(0, dist - 0.35) / 0.65\n if r > 0.4: d.ellipse((x - r, y - r, x + r, y + r), fill=(200, 200, 198))\n mg = lambda n, b: Image.open(SITE + f'c_manga{n}.png').convert('RGB').crop(b)\n panels = [\n (frame(1).crop((0, 730, 380, 1080)), 'none', (0.5, 0.15)), # Hakobu\n (mg(1, (290, 0, 545, 330)), 'surprise', (0.5, 0.1)), # nurse\n (mg(0, (330, 30, 561, 330)), 'sweat', (0.5, 0.1)), # engineer\n (mg(2, (170, 0, 400, 270)), 'joy', (0.5, 0.5)), # arm robot with a face\n ]\n pw, ph, gap = 290, 300, 18\n x = (W - (4 * pw + 3 * gap)) // 2; y = (BAND[1] + BAND[3]) // 2 - ph // 2\n for i, (im, fx, cen) in enumerate(panels):\n big = ImageOps.fit(im, (pw, ph), Image.LANCZOS, centering=cen)\n panel = to_manga(big) if i == 0 else ink_manga(big)\n pd = ImageDraw.Draw(panel)\n if fx == 'surprise':\n for a in range(0, 360, 9):\n ca, sa = math.cos(math.radians(a)), math.sin(math.radians(a))\n pd.line((pw / 2 + ca * 165, ph * 0.3 + sa * 165, pw / 2 + ca * 260, ph * 0.3 + sa * 260), fill=INK, width=2)\n pd.polygon([(30, 20), (48, 20), (43, 70), (35, 70)], fill=INK); pd.ellipse((32, 78, 46, 92), fill=INK)\n if fx == 'sweat':\n pd.polygon([(40, 34), (28, 64), (52, 64)], fill=(255, 255, 255), outline=INK)\n pd.ellipse((26, 52, 54, 80), fill=(255, 255, 255), outline=INK, width=2)\n if fx == 'joy':\n for sx, sy, s in ((40, 40, 18), (255, 60, 14), (60, 120, 10)):\n pd.polygon([(sx, sy - s), (sx + s * 0.25, sy - s * 0.25), (sx + s, sy), (sx + s * 0.25, sy + s * 0.25),\n (sx, sy + s), (sx - s * 0.25, sy + s * 0.25), (sx - s, sy), (sx - s * 0.25, sy - s * 0.25)], fill=INK)\n c.paste(panel, (x, y))\n d.rectangle((x - 4, y - 4, x + pw + 3, y + ph + 3), outline=INK, width=5)\n x += pw + gap\n x0 = (W - (4 * pw + 3 * gap)) // 2\n for k in range(24):\n yy_ = y + 10 + k * (ph - 20) / 23; ln = 90 + (k * 37 % 5) * 45\n d.line((x + 20, yy_, x + 20 + ln, yy_), fill=INK, width=3)\n d.line((x0 - 40 - ln, yy_, x0 - 40, yy_), fill=INK, width=3)\n return c\n\nitems = [('illust_flat_single', corridor_flat)]\nfor name, fn in items:\n im = fn()\n im.save(OUT + f'hakobu_banner_{name}_2048x1152.jpg', quality=92, subsampling=0)\n im.crop(BAND).save(OUT + f'_band_{name}.png')\n print(name)</code></pre><h2 id=\"index_NP8gCgQ3\">スクリプト以外に実行したコマンド</h2><h3 id=\"index_VpcG8_8h\">サイトの画面を保存したPDFから画像を取り出し、ページを縦につないだ</h3><pre><code class=\"language-bash\">cd /home/claude/banner/site && pdfimages -j \"/root/.claude/uploads/6f54b9ec-7b51-53dd-a180-2e8bdb7969c6/f328e762-screencapture-adaptdesign-jp-pixelstomotors-2026-10-12-07_46_26.pdf\" pg && convert pg-000.jpg pg-001.jpg pg-002.jpg -append full.jpg && identify full.jpg && for i in 0 1 2 3 4; do convert full.jpg -crop 2728x2100+0+$((i*2100)) -resize 1100x prev$i.jpg; done; ls</code></pre><h3 id=\"index_naTwarcA\">つないだ画像から、サムネイルや4コマを切り出した</h3><pre><code class=\"language-python\">from PIL import Image\nim=Image.open('full.jpg')\nk=1/0.4032\ndef c(name, x0,y0,x1,y1,page):\n box=(int(x0*k),int(page*2100+y0*k),int(x1*k),int(page*2100+y1*k))\n im.crop(box).save(f'c_{name}.png'); print(name, box, (box[2]-box[0],box[3]-box[1]))\nc('hero',670,86,936,283,0)\nfor i,(a,b) in enumerate([(62,288),(312,538),(562,788),(812,1038)]):\n c(f'manga{i}',a,512,b,728,0)\nc('drug',62,158,535,481,1)\nc('lab',565,158,1038,481,1)\nc('imgrec_top',62,598,535,846,1)\nc('deep_top',565,598,1038,846,1)\nc('sim',62,245,535,533,2)\nc('flow',565,210,1038,533,2)\nc('rec_study',62,0,368,116,3); c('rec_micro',397,0,703,116,3); c('rec_say',731,0,1038,116,3)\nc('bowl',62,254,368,425,3); c('gpu',397,254,703,425,3); c('film',731,254,1038,425,3)\nc('panels324',62,605,368,775,3); c('panels323',397,605,703,775,3); c('who03',731,605,1038,775,3)\nc('inside',62,65,368,236,4); c('panels320',397,65,703,236,4); c('panels319',731,65,1038,236,4)\nc('issues',620,488,900,612,4)</code></pre><h3 id=\"index_egF7Khp0\">試作動画から、ハコブくんの表情のコマを書き出した</h3><pre><code class=\"language-bash\">cd /home/claude/banner/vid && V=\"/root/.claude/uploads/6f54b9ec-7b51-53dd-a180-2e8bdb7969c6/d2aa7563-hakobu_demo_upper.mp4\"; ffmpeg -v error -y -i \"$V\" -vf \"fps=3,crop=400:340:0:740\" h_%03d.png; ls h_*.png | wc -l; montage h_*.png -tile 10x -geometry 200x170+2+2 -label '%t' -pointsize 10 hsheet.jpg</code></pre><pre><code class=\"language-bash\">cd /home/claude/banner/vid && V=\"/root/.claude/uploads/6f54b9ec-7b51-53dd-a180-2e8bdb7969c6/d2aa7563-hakobu_demo_upper.mp4\"; for t in 0 3.0; do ffmpeg -v error -y -ss $t -i \"$V\" -frames:v 1 fr_$t.png; done</code></pre><pre><code class=\"language-bash\">cd /home/claude/banner/vid && V=\"/root/.claude/uploads/6f54b9ec-7b51-53dd-a180-2e8bdb7969c6/d2aa7563-hakobu_demo_upper.mp4\"; mkdir -p fx && for i in 1 4 8 9 15 24; do t=$(python3 -c \"print(($i-1)/3)\"); ffmpeg -v error -y -ss $t -i \"$V\" -frames:v 1 fx/e_$i.png; done; ls fx</code></pre><h3 id=\"index_hy_pvXuR\">動画のコマで、ハコブくんの範囲とディスプレイの位置を測った</h3><pre><code class=\"language-python\">from PIL import Image\nimport numpy as np\nim=Image.open('fr_0.png').convert('RGB')\na=np.array(im).astype(int)\nbg=np.array([245,245,240])\nreg=a[700:1080,0:420]\ndist=np.abs(reg-bg).sum(-1)\nys,xs=np.where(dist>18)\nprint('bbox',xs.min(),xs.max(),ys.min()+700,ys.max()+700)\nprint('corner color',a[720,5],a[1075,410])</code></pre><pre><code class=\"language-python\">from PIL import Image; import numpy as np\na=np.array(Image.open('fr_0.png').convert('RGB')).astype(int)[700:1080,0:420]\nys,xs=np.where(a.sum(-1)<90); print(xs.min(),xs.max(),ys.min()+700,ys.max()+700)</code></pre><h3 id=\"index_rLSvvOLQ\">三面図の3つの図の範囲、リングの中心と半径、口の位置を測った</h3><pre><code class=\"language-python\">from PIL import Image\nimport numpy as np\na=np.array(Image.open('src.png').convert('L')).astype(int)\n# column/row darkness profile to find view bboxes\ndark=a<120\ncols=dark[380:900].sum(0); rows=dark[:, :].sum(1)\nruns=[];inrun=False\nfor x,v in enumerate(cols):\n if v>3 and not inrun: s=x;inrun=True\n if v<=3 and inrun: runs.append((s,x));inrun=False\nprint([r for r in runs if r[1]-r[0]>20])\nfor (x0,x1) in [r for r in runs if r[1]-r[0]>20]:\n ys=np.where(dark[:,x0:x1].sum(1)>2)[0]; ys=ys[(ys>300)&(ys<1000)]; print(x0,x1,ys.min(),ys.max())\n# ring: find dark circle near side center</code></pre><pre><code class=\"language-python\">from PIL import Image\nimport numpy as np\na=np.array(Image.open('src.png').convert('L')).astype(float)\nbest=None\nfor cx in range(540,568,2):\n for cy in range(655,690,2):\n yy,xx=np.mgrid[cy-80:cy+80,cx-80:cx+80]; r=np.hypot(xx-cx,yy-cy); sub=a[cy-80:cy+80,cx-80:cx+80]\n prof=[sub[(r>=k)&(r<k+1)].mean() for k in range(30,75)]\n score=np.std(prof)\n sharp=sorted(prof)[:2]\n s=sum(sharp)\n if best is None or s<best[0]: best=(s,cx,cy,prof)\ns,cx,cy,prof=best; print(cx,cy); print([int(p) for p in prof])\n# mouth gray pixels\nsub=a[480:515,175:230]; ys,xs=np.where((sub>70)&(sub<200)); print('mouth', xs.min()+175,xs.max()+175,ys.min()+480,ys.max()+480)</code></pre><h3 id=\"index_kGmWoZfd\">2回目の全案を 2048×1152px で書き出した(1回目の写真は文字を外して作り直した)</h3><pre><code class=\"language-bash\">cd /home/claude/banner && python3 - <<'EOF'\nsrc=open('make.py').read()\nsrc=src[:src.index(\"import os\\nos.makedirs\")]\nexec(src)\ndef arm_clean():\n bg = Image.new('RGB', (W, H), MAT)\n s=0.74; subj=((1282+2650)/2, 1790)\n layer,_=place(bg, photo, s, subj, (1280, 730))\n ox=1280-subj[0]*s; oy=730-subj[1]*s\n mb=(ox+MAT_BOX[0]*s+70, oy+MAT_BOX[1]*s+70, ox+MAT_BOX[2]*s-70, oy+MAT_BOX[3]*s-70)\n bg.paste(ImageEnhance.Brightness(layer).enhance(0.95),(0,0),rect_mask(mb,60))\n return bg\nim=arm_clean(); im.save('/home/claude/banner/deliver/pixelstomotors_banner_A_arm_photo.jpg',quality=92,subsampling=0)\nEOF\nmkdir -p final_2048 && cd deliver && for f in pixelstomotors_banner_*.jpg; do n=${f%.jpg}; convert \"$f\" -resize 2048x1152! -quality 92 -sampling-factor 4:4:4 ../final_2048/${n}_2048x1152.jpg; done; cd ../final_2048 && identify -format \"%f %wx%h %b\\n\" *.jpg</code></pre><p></p><h2 id=\"index_dxzTRtXR\">実行ログ</h2><h3 id=\"index_6qQln2AW\">1回目の書き出し(make.py)</h3><pre><code>A_grasp 464 KB\nB_pixels_to_motors 341 KB\nC_see_decide_move 461 KB</code></pre><h3 id=\"index_fJsloe7H\">2回目の書き出し(make2.py)</h3><pre><code>B_archive_wall 769 KB\nC_mono_split 315 KB\nD_sim_halftone 1300 KB\nE_hakobu_manga 908 KB\nF_dot_matrix 669 KB\nG_hakobu_photo 170 KB</code></pre><h3 id=\"index_gljFeQAL\">サイトの画面から切り出した範囲</h3><pre><code>hero (1661, 213, 2321, 701) (660, 488)\nmanga0 (153, 1269, 714, 1805) (561, 536)\nmanga1 (773, 1269, 1334, 1805) (561, 536)\nmanga2 (1393, 1269, 1954, 1805) (561, 536)\nmanga3 (2013, 1269, 2574, 1805) (561, 536)\ndrug (153, 2491, 1326, 3292) (1173, 801)\nlab (1401, 2491, 2574, 3292) (1173, 801)\nimgrec_top (153, 3583, 1326, 4198) (1173, 615)\ndeep_top (1401, 3583, 2574, 4198) (1173, 615)\nsim (153, 4807, 1326, 5521) (1173, 714)\nflow (1401, 4720, 2574, 5521) (1173, 801)\nrec_study (153, 6300, 912, 6587) (759, 287)\nrec_micro (984, 6300, 1743, 6587) (759, 287)\nrec_say (1812, 6300, 2574, 6587) (762, 287)\nbowl (153, 6929, 912, 7354) (759, 425)\ngpu (984, 6929, 1743, 7354) (759, 425)\nfilm (1812, 6929, 2574, 7354) (762, 425)\npanels324 (153, 7800, 912, 8222) (759, 422)\npanels323 (984, 7800, 1743, 8222) (759, 422)\nwho03 (1812, 7800, 2574, 8222) (762, 422)\ninside (153, 8561, 912, 8985) (759, 424)\npanels320 (984, 8561, 1743, 8985) (759, 424)\npanels319 (1812, 8561, 2574, 8985) (762, 424)\nissues (1537, 9610, 2232, 9917) (695, 307)</code></pre><h3 id=\"index_JyBXovfG\">試作動画の情報と、ハコブくんの範囲</h3><pre><code>1920,1080,30/1,13.266667\n0/0,13.266000</code></pre><pre><code>bbox 29 351 766 1079\ncorner color [244 245 237] [244 245 237]</code></pre><pre><code>155 326 787 1022</code></pre><h3 id=\"index_GC_7inZM\">三面図の測定</h3><pre><code>[(70, 331), (396, 698), (749, 1012)]\n70 331 415 874\n396 698 419 874\n749 1012 420 875</code></pre><pre><code>554 663\n[250, 250, 249, 248, 247, 246, 245, 244, 242, 241, 240, 239, 238, 237, 235, 235, 234, 232, 225, 188, 140, 122, 140, 173, 179, 182, 188, 189, 177, 181, 174, 131, 114, 155, 194, 219, 227, 228, 229, 230, 230, 231, 232, 232, 233]\nmouth 184 215 490 505</code></pre><h3 id=\"index_htfRwQhT\">2048×1152px への書き出し</h3><pre><code>pixelstomotors_banner_A_arm_photo_2048x1152.jpg 2048x1152 478891B\npixelstomotors_banner_B_archive_wall_2048x1152.jpg 2048x1152 522786B\npixelstomotors_banner_C_mono_split_2048x1152.jpg 2048x1152 182721B\npixelstomotors_banner_D_sim_halftone_2048x1152.jpg 2048x1152 506840B\npixelstomotors_banner_E_hakobu_manga_2048x1152.jpg 2048x1152 506732B\npixelstomotors_banner_F_dot_matrix_2048x1152.jpg 2048x1152 481321B\npixelstomotors_banner_G_hakobu_photo_2048x1152.jpg 2048x1152 96050B</code></pre><h3 id=\"index_l3SpavP3\">3回目〜決定版の書き出し</h3><pre><code>1_illustration\n2_manga\n3_realistic\n4_photo\n5_graphic</code></pre><pre><code>I1_threeview\nI2_corridor\nM1_manga_page\nM2_cast</code></pre><pre><code>illust_flat\nillust_line\nmanga_cast</code></pre><pre><code>illust_flat_single</code></pre><p></p><h2 id=\"index_QUNQPzA5\">参照した情報</h2><ul><li><p><a href=\"https://support.google.com/youtube/answer/10456525?hl=ja\" data-has-link=\"true\">YouTube ヘルプ:チャンネルのブランディングを管理する</a></p></li><li><p><a href=\"https://support.google.com/youtube/answer/9481328?hl=ja\" data-has-link=\"true\">YouTube ヘルプ:チャンネルの権限</a></p></li><li><p><a href=\"/\" data-has-link=\"true\">Pixels to Motors</a></p></li></ul>", "toc": [{"id": "index__Ynzo3Cp", "text": "YouTube用のアカウントは、Google Workspaceのエイリアスではなく新しいユーザーにした"}, {"id": "index_8rfkxb9m", "text": "管理コンソールには、普段使いのアカウントでは入れなかった"}, {"id": "index_SZwLHwTg", "text": "「このユーザーは既に存在しています」の原因は、3月に付けたエイリアスだった"}, {"id": "index_EnRPO5Pq", "text": "作ろうとしたチャンネルのハンドルは、自分の既存チャンネルが使っていた"}, {"id": "index_04xwKQv5", "text": "既存チャンネルは、新しいアカウントを「管理者」として招待して引き継いだ"}, {"id": "index_6tAUffjk", "text": "チャンネルの説明文は、フィジカルAIを先頭に置いて書き直した"}, {"id": "index_GNMYvKmk", "text": "バナーは、どの端末でも切れない中央の範囲を基準に作った"}, {"id": "index_BpOCllXq", "text": "バナーの1回目は、実機の写真に文字を入れた3案だった"}, {"id": "index_KWVQksqt", "text": "2回目は、サイトの画像から6つの方向を出した"}, {"id": "index_wfU0AzRw", "text": "アイコンは、動画で話しているハコブくんの1コマから作った"}, {"id": "index_BZlJ8lML", "text": "バナーの3回目は、ハコブくんだけで5つのタッチを作った"}, {"id": "index_aoA9mNR9", "text": "5回目で、ハコブくんを背景と同じタッチで描き直した"}, {"id": "index_8PXH6omo", "text": "チャンネル名は「フィジカルAI解説|Pixels to Motors」を候補にした"}, {"id": "index_mtwyUJht", "text": "チャンネル名の変更と、アイコン・4コマの作り直しが残っている"}, {"id": "index_LGOmRd2E", "text": "制作スクリプトの全文"}, {"id": "index_NP8gCgQ3", "text": "スクリプト以外に実行したコマンド"}, {"id": "index_dxzTRtXR", "text": "実行ログ"}, {"id": "index_QUNQPzA5", "text": "参照した情報"}]}
\ No newline at end of file
diff --git a/src/data/static.json b/src/data/static.json
index 9f34bac..de3a71e 100644
--- a/src/data/static.json
+++ b/src/data/static.json
@@ -60,6 +60,26 @@
"type": "embed",
"html": "<div class=\"embed\"><iframe src=\"/embeds/series1-3.html\" sandbox=\"allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-presentation\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div>",
"height": null
+ },
+ {
+ "type": "text",
+ "html": "FigJam/フロー図",
+ "link": null
+ },
+ {
+ "type": "iframe",
+ "html": "<iframe style=\"border: 1px solid rgba(0, 0, 0, 0.1);\" width=\"800\" height=\"450\" src=\"https://embed.figma.com/board/fFRdzGMRwQGF7Y36dCmB1m/%E5%9F%BA%E6%9C%AC%E3%83%95%E3%83%AD%E3%83%BC%E5%9B%B3?node-id=0-1&embed-host=share\" allowfullscreen></iframe>",
+ "height": 800
+ },
+ {
+ "type": "text",
+ "html": "スプレッドシート/ログ & リファレンス",
+ "link": null
+ },
+ {
+ "type": "embed",
+ "html": "<div class=\"embed\"><iframe src=\"/embeds/series4-1.html\" sandbox=\"allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-presentation\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div>",
+ "height": null
}
]
},
@@ -115,38 +135,13 @@
"blocks": [
{
"type": "text",
- "html": "Robotics Design Workflow | ロボティクスデザイン実務フロー",
+ "html": "Behind the Solo Lab | ひとり研究所の裏側",
"link": null
},
{
"type": "text",
- "html": "FigJam/フロー図",
- "link": null
- },
- {
- "type": "iframe",
- "html": "<iframe style=\"border: 1px solid rgba(0, 0, 0, 0.1);\" width=\"800\" height=\"450\" src=\"https://embed.figma.com/board/fFRdzGMRwQGF7Y36dCmB1m/%E5%9F%BA%E6%9C%AC%E3%83%95%E3%83%AD%E3%83%BC%E5%9B%B3?node-id=0-1&embed-host=share\" allowfullscreen></iframe>",
- "height": 800
- },
- {
- "type": "text",
- "html": "スプレッドシート/ログ & リファレンス",
- "link": null
- },
- {
- "type": "embed",
- "html": "<div class=\"embed\"><iframe src=\"/embeds/series4-1.html\" sandbox=\"allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-presentation\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div>",
- "height": null
- },
- {
- "type": "text",
- "html": "Webサイト/フィジカルAIが社会実装される前に解くべき課題",
- "link": null
- },
- {
- "type": "embed",
- "html": "<div class=\"embed\"><iframe src=\"/embeds/series4-2.html\" sandbox=\"allow-scripts allow-popups allow-popups-to-escape-sandbox\" loading=\"lazy\" title=\"図解\" data-embed allowfullscreen></iframe></div>",
- "height": null
+ "html": "課題地図/フィジカルAIが社会実装される前に解くべき課題(pixelstomotors.com/issues)",
+ "link": "/issues"
}
]
},
diff --git a/src/lib/records.ts b/src/lib/records.ts
index 94c905e..9a164b6 100644
--- a/src/lib/records.ts
+++ b/src/lib/records.ts
@@ -33,7 +33,7 @@ export const SERIES_PAGES: Record<string, { series: string; title: string; pageT
series1: { series: 'HRI', title: 'Drug Delivery Robot | 薬剤搬送ロボットハコブくん', pageTitle: 'Series1 | 薬剤搬送ロボット', limit: 12 },
series2: { series: 'R2R2R', title: 'R2R2R | デジタルツイン学習-R2R2R(Real to Render to Real)', pageTitle: 'Series2 | R2R2R', limit: 12 },
series3: { series: 'UV-C農業ロボット', title: 'UV-C Agricultural Robot | UV-C農業ロボット', pageTitle: 'Series3 | UV-C農業ロボット', limit: 20 },
- series4: { series: 'ロボティクスデザイン', title: 'Robotics Design Workflow | ロボティクスデザイン実務フロー', pageTitle: 'Series4 | ロボティクスデザイン実務フロー', limit: 12,
+ series4: { series: 'ひとり研究所の裏側', title: 'Behind the Solo Lab | ひとり研究所の裏側', pageTitle: 'Series4 | ひとり研究所の裏側', limit: 12,
intro: 'フィジカルAIの研究をひとりで続けるために、AIと一緒に作ってきた仕組みの記録です。サイトの移行、動画の制作、記録の整理など、研究の外側の作業をどう回しているかを残しています。' },
series5: { series: '考察記事', title: 'Deep Dive | 考察記事', pageTitle: 'Series5 | 考察記事', limit: 12 },
series6: { series: 'フィジカルAI研究室', title: 'Physical AI Lab | フィジカルAI研究室', pageTitle: 'Series6 | フィジカルAI研究室', limit: 12 },
diff --git a/src/pages/[series].astro b/src/pages/[series].astro
index d60b6f3..cbdcf6c 100644
--- a/src/pages/[series].astro
+++ b/src/pages/[series].astro
@@ -28,7 +28,7 @@ const jsonLd = {
<h1 class="sec-h s-title">{conf.title}</h1>
{conf.intro && <p class="s-intro">{conf.intro}</p>}
{blocks.map((b) => (
- b.type === 'text' ? (b.link ? <a class="s-label" href={b.link} target="_blank" rel="noopener" set:html={b.html} /> : <p class="s-label" set:html={b.html} />)
+ b.type === 'text' ? (b.link ? <a class="s-label" href={b.link} target={b.link.startsWith('/') ? undefined : '_blank'} rel={b.link.startsWith('/') ? undefined : 'noopener'} set:html={b.html} /> : <p class="s-label" set:html={b.html} />)
: b.type === 'iframe' ? <div class="s-frame" style={`height:${b.height}px`} set:html={b.html} />
: b.type === 'embed' ? <div class="s-embed" set:html={b.html} />
: b.type === 'img' ? (b.link ? <a class="s-img" href={b.link} target="_blank" rel="noopener"><img src={media(b.src)} alt={b.alt || conf.title} loading="lazy" /></a> : <div class="s-img"><img src={media(b.src)} alt={b.alt || conf.title} loading="lazy" /></div>)
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 90de929..2134701 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -26,7 +26,7 @@ const series = [
desc: 'Essays that go beyond the build log — examining why things work, what they mean, and where the field is heading.' },
{ href: '/series2', img: 's-1920x1318_v-frms_0d775000-730b-4ac2-90b5-d814a9f53fc6.webp', en: 'R2R2R', ja: 'デジタルツイン学習-R2R2R(Real to Render to Real)',
desc: 'A hands-on project proving that robot arm motion can be designed without a robotics background — using only an iPhone for 3D scanning and Unity for simulation.' },
- { href: '/series4', img: 's-4480x3680_2e3487ec-2968-4b89-8bca-7062b48f6093.webp', en: 'Robotics Design Workflow', ja: 'ロボティクスデザイン実務フロー', desc: '' },
+ { href: '/series4', img: '/og/LOG339.jpg', en: 'Behind the Solo Lab', ja: 'ひとり研究所の裏側', desc: 'How a one-person Physical AI lab keeps running — the site, the videos and the records behind the research.' },
];
const records = await allCards();
import { SITE, SITE_NAME, AUTHOR, PUBLISHER } from '../lib/seo';
@@ -65,7 +65,7 @@ const jsonLd = [
<div class="grid2">
{series.map((s) => (
<a class="scard" href={s.href}>
- <img src={media(`/media/${s.img}`)} alt={`${s.en}|${s.ja}`} loading="lazy" />
+ <img src={s.img.startsWith('/') ? s.img : media(`/media/${s.img}`)} alt={`${s.en}|${s.ja}`} loading="lazy" />
<div class="sc-meta">
<div class="sc-line"><span class="sc-en">{s.en}</span><span class="sc-ja">{s.ja}</span></div>
{s.desc && <p class="sc-desc">{s.desc}</p>}
「ひとり研究所の裏側」のカードの画像を新しく作った
トップのシリーズのカードには、最初は LOG339 のサムネイルを仮に当てていました。そのあと「カテゴリのサムネイルについても新しくしてください」と頼まれ、このシリーズ用の画像を作っています。
1枚目は、手前に黒い板とアームの線画を置き、その奥に点線の板を3枚重ねて、サイト・動画・記録という裏側の仕事を表した絵でした。これは「微妙なデザイン」と言われ、作り直しています。
2回目は、フィジカルAI研究室や画像認識のカードと同じ白黒の線画に、縞と網点を合わせる描き方にそろえて、3案を並べました。
| 案 | 絵 |
|---|---|
| A 舞台袖 | 縞の幕が開いて、裏の机にノートPCとカチンコがある |
| B 半開きのドア | 黒い部屋のドアが少しだけ開き、隙間から site・videos・records・ledger・issues map の一覧と小さなアームがのぞく |
| C 机を真上から | 左の黒い面からアームが伸び、机の上にノートPC・カチンコ・SSD・カップが並ぶ |
選ばれたのは B です。「裏側」と、名前を選んだときの「ひっそり」の両方に合い、一覧でカードが小さくなっても形がつぶれにくい案です。カードは横3:縦2で切り抜かれるので、画像も 3:2(1800×1200)で書き出しています。元は HTML(tools/series_cards/series4.html)で、tools/series_cards/render.py で PNG に書き出します。
$ python3 tools/series_cards/render.py series4
/home/claude/p2m-site/public/site/series4-behind-the-solo-lab-door.png
$ npm run build >/dev/null 2>&1; echo build=$?; grep -o 'src="/site/series4-behind-the-solo-lab-door.png"' dist/index.html
build=0
src="/site/series4-behind-the-solo-lab-door.png"

commit 30e6173 ― 1枚目のカードの画像の変更の全量(PNG を除く・あとで B 案に替えた)
commit 30e61736c9e06ffcd111c89198df470fff58b0af
Author: Claude <noreply@anthropic.com>
Date: Sat Oct 17 14:22:31 2026 +0900
「ひとり研究所の裏側」のカードの画像を新しく作る
手前に研究(黒い板とアーム)、奥に点線の板で裏側の仕事(サイト・動画・記録)を重ねた。
元は tools/series_cards/series4.html。tools/series_cards/render.py で public/site/ に PNG で書き出す。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8PyTPCaRx7Mg89h22HK6a
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 2134701..f27653d 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -26,7 +26,7 @@ const series = [
desc: 'Essays that go beyond the build log — examining why things work, what they mean, and where the field is heading.' },
{ href: '/series2', img: 's-1920x1318_v-frms_0d775000-730b-4ac2-90b5-d814a9f53fc6.webp', en: 'R2R2R', ja: 'デジタルツイン学習-R2R2R(Real to Render to Real)',
desc: 'A hands-on project proving that robot arm motion can be designed without a robotics background — using only an iPhone for 3D scanning and Unity for simulation.' },
- { href: '/series4', img: '/og/LOG339.jpg', en: 'Behind the Solo Lab', ja: 'ひとり研究所の裏側', desc: 'How a one-person Physical AI lab keeps running — the site, the videos and the records behind the research.' },
+ { href: '/series4', img: '/site/series4-behind-the-solo-lab.png', en: 'Behind the Solo Lab', ja: 'ひとり研究所の裏側', desc: 'How a one-person Physical AI lab keeps running — the site, the videos and the records behind the research.' },
];
const records = await allCards();
import { SITE, SITE_NAME, AUTHOR, PUBLISHER } from '../lib/seo';
diff --git a/tools/series_cards/render.py b/tools/series_cards/render.py
new file mode 100644
index 0000000..2775ce7
--- /dev/null
+++ b/tools/series_cards/render.py
@@ -0,0 +1,26 @@
+"""トップのシリーズのカードの画像を、HTML から PNG に書き出す(3:2・1500×1000 を 2 倍で)。
+Claude のクラウドの作業環境で動かす(Mac には Playwright がない)。
+使い方: python3 tools/series_cards/render.py series4
+"""
+import asyncio
+import sys
+from pathlib import Path
+from playwright.async_api import async_playwright
+
+NAMES = {'series4': 'series4-behind-the-solo-lab.png'}
+
+
+async def main(key):
+ here = Path(__file__).resolve().parent
+ out = here.parent.parent / 'public/site' / NAMES[key]
+ async with async_playwright() as p:
+ b = await p.chromium.launch()
+ pg = await b.new_page(viewport={'width': 1500, 'height': 1000}, device_scale_factor=1.2)
+ await pg.goto((here / f'{key}.html').as_uri())
+ await pg.wait_for_timeout(1500)
+ await pg.screenshot(path=str(out), clip={'x': 0, 'y': 0, 'width': 1500, 'height': 1000})
+ await b.close()
+ print(out)
+
+
+asyncio.run(main(sys.argv[1]))
diff --git a/tools/series_cards/series4.html b/tools/series_cards/series4.html
new file mode 100644
index 0000000..13e2edc
--- /dev/null
+++ b/tools/series_cards/series4.html
@@ -0,0 +1,59 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Behind the Solo Lab</title>
+<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;700&display=swap" rel="stylesheet">
+<style>
+ html,body { margin:0; background:#f5f5f0; }
+ svg { display:block; width:1500px; height:1000px; }
+</style>
+</head>
+<body>
+<!-- トップのシリーズのカード(3:2)。tools/series_cards/render.py で public/site/series4-behind-the-solo-lab.png に書き出す -->
+<svg viewBox="0 0 1500 1000" xmlns="http://www.w3.org/2000/svg" font-family="'IBM Plex Mono', monospace">
+ <defs>
+ <pattern id="hatch" width="8" height="8" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><rect width="8" height="8" fill="#f5f5f0"/><rect width="1.6" height="8" fill="#0d0d0d" opacity=".35"/></pattern>
+ </defs>
+ <rect width="1500" height="1000" fill="#f5f5f0"/>
+
+ <!-- 奥の段(裏側の仕事):点線の板が重なって奥へ下がる -->
+ <g fill="#f5f5f0" stroke="#9a9a96" stroke-width="1.6" stroke-dasharray="7 7">
+ <rect x="770" y="150" width="520" height="330"/>
+ </g>
+ <text x="790" y="186" font-size="17" letter-spacing="4" fill="#0d0d0d" opacity=".5">RECORDS · LOG 1–347</text>
+ <g fill="#f5f5f0" stroke="#7a7a76" stroke-width="1.6" stroke-dasharray="7 7">
+ <rect x="690" y="230" width="520" height="330"/>
+ </g>
+ <text x="710" y="266" font-size="17" letter-spacing="4" fill="#0d0d0d" opacity=".62">VIDEOS · WHOSE TURN</text>
+ <g fill="#f5f5f0" stroke="#4a4a47" stroke-width="1.6" stroke-dasharray="7 7">
+ <rect x="610" y="310" width="520" height="330"/>
+ </g>
+ <text x="630" y="346" font-size="17" letter-spacing="4" fill="#0d0d0d" opacity=".8">SITE · ONE PUSH</text>
+
+ <!-- 手前の板(表に見える研究) -->
+ <rect x="530" y="390" width="520" height="330" fill="#0d0d0d"/>
+ <text x="556" y="430" font-size="17" letter-spacing="4" fill="#f5f5f0" opacity=".7">FRONT · PHYSICAL AI LAB</text>
+ <!-- 机とアームの小さな線画 -->
+ <g stroke="#f5f5f0" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round">
+ <line x1="600" y1="670" x2="980" y2="670"/>
+ <rect x="690" y="640" width="60" height="30" fill="#f5f5f0"/>
+ <path d="M720 640 L760 560 L850 590 L880 620"/>
+ <path d="M880 620 L900 612 M880 620 L894 638"/>
+ <rect x="900" y="642" width="26" height="28" fill="#f5f5f0"/>
+ </g>
+ <circle cx="760" cy="560" r="8" fill="#0d0d0d" stroke="#f5f5f0" stroke-width="4"/>
+ <circle cx="850" cy="590" r="8" fill="#0d0d0d" stroke="#f5f5f0" stroke-width="4"/>
+
+ <!-- 裏へ回る細い矢印 -->
+ <path d="M1050 555 C 1140 555, 1180 520, 1205 470" fill="none" stroke="#0d0d0d" stroke-width="1.8"/>
+ <path d="M1196 474 L1207 462 L1213 478" fill="none" stroke="#0d0d0d" stroke-width="1.8"/>
+
+ <!-- 見出し(控えめ) -->
+ <text x="110" y="200" font-size="18" letter-spacing="6" fill="#0d0d0d" opacity=".7">SERIES · BACKSTAGE</text>
+ <text x="110" y="790" font-size="64" font-weight="700" fill="#0d0d0d">BEHIND THE</text>
+ <text x="110" y="866" font-size="64" font-weight="700" fill="#0d0d0d">SOLO LAB.</text>
+ <rect x="110" y="900" width="420" height="10" fill="url(#hatch)" stroke="#0d0d0d" stroke-width="1.2"/>
+</svg>
+</body>
+</html>
commit 4cb0589 ― カードを B 案(半開きのドア)に替えた変更の全量(PNG を除く)
commit 4cb058948086746b585360786ae9f6e3fb40aa39
Author: Claude <noreply@anthropic.com>
Date: Sat Oct 17 14:35:37 2026 +0900
「ひとり研究所の裏側」のカードを半開きのドアの絵(B 案)に替える
1枚目(手前の板と点線の板)は「微妙」とのことで作り直した。3案から B を選んでもらった。
古い画像(series4-behind-the-solo-lab.png)は消し、新しい名前で置いた(キャッシュに古い絵が残らないように)。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R8PyTPCaRx7Mg89h22HK6a
diff --git a/src/pages/index.astro b/src/pages/index.astro
index f27653d..29d175c 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -26,7 +26,7 @@ const series = [
desc: 'Essays that go beyond the build log — examining why things work, what they mean, and where the field is heading.' },
{ href: '/series2', img: 's-1920x1318_v-frms_0d775000-730b-4ac2-90b5-d814a9f53fc6.webp', en: 'R2R2R', ja: 'デジタルツイン学習-R2R2R(Real to Render to Real)',
desc: 'A hands-on project proving that robot arm motion can be designed without a robotics background — using only an iPhone for 3D scanning and Unity for simulation.' },
- { href: '/series4', img: '/site/series4-behind-the-solo-lab.png', en: 'Behind the Solo Lab', ja: 'ひとり研究所の裏側', desc: 'How a one-person Physical AI lab keeps running — the site, the videos and the records behind the research.' },
+ { href: '/series4', img: '/site/series4-behind-the-solo-lab-door.png', en: 'Behind the Solo Lab', ja: 'ひとり研究所の裏側', desc: 'How a one-person Physical AI lab keeps running — the site, the videos and the records behind the research.' },
];
const records = await allCards();
import { SITE, SITE_NAME, AUTHOR, PUBLISHER } from '../lib/seo';
diff --git a/tools/series_cards/render.py b/tools/series_cards/render.py
index 2775ce7..cfb84e9 100644
--- a/tools/series_cards/render.py
+++ b/tools/series_cards/render.py
@@ -7,7 +7,7 @@ import sys
from pathlib import Path
from playwright.async_api import async_playwright
-NAMES = {'series4': 'series4-behind-the-solo-lab.png'}
+NAMES = {'series4': 'series4-behind-the-solo-lab-door.png'}
async def main(key):
diff --git a/tools/series_cards/series4.html b/tools/series_cards/series4.html
index 13e2edc..ff3b8a3 100644
--- a/tools/series_cards/series4.html
+++ b/tools/series_cards/series4.html
@@ -1,59 +1,12 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8">
-<title>Behind the Solo Lab</title>
+<!doctype html><html><head><meta charset="utf-8">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;700&display=swap" rel="stylesheet">
-<style>
- html,body { margin:0; background:#f5f5f0; }
- svg { display:block; width:1500px; height:1000px; }
-</style>
-</head>
-<body>
-<!-- トップのシリーズのカード(3:2)。tools/series_cards/render.py で public/site/series4-behind-the-solo-lab.png に書き出す -->
+<style>html,body{margin:0;background:#f5f5f0}svg{display:block;width:1500px;height:1000px}</style></head><body>
+<!-- 「ひとり研究所の裏側」のカード(3:2・半開きのドア)。tools/series_cards/render.py で public/site/series4-behind-the-solo-lab-door.png に書き出す -->
<svg viewBox="0 0 1500 1000" xmlns="http://www.w3.org/2000/svg" font-family="'IBM Plex Mono', monospace">
- <defs>
- <pattern id="hatch" width="8" height="8" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><rect width="8" height="8" fill="#f5f5f0"/><rect width="1.6" height="8" fill="#0d0d0d" opacity=".35"/></pattern>
- </defs>
- <rect width="1500" height="1000" fill="#f5f5f0"/>
-
- <!-- 奥の段(裏側の仕事):点線の板が重なって奥へ下がる -->
- <g fill="#f5f5f0" stroke="#9a9a96" stroke-width="1.6" stroke-dasharray="7 7">
- <rect x="770" y="150" width="520" height="330"/>
- </g>
- <text x="790" y="186" font-size="17" letter-spacing="4" fill="#0d0d0d" opacity=".5">RECORDS · LOG 1–347</text>
- <g fill="#f5f5f0" stroke="#7a7a76" stroke-width="1.6" stroke-dasharray="7 7">
- <rect x="690" y="230" width="520" height="330"/>
- </g>
- <text x="710" y="266" font-size="17" letter-spacing="4" fill="#0d0d0d" opacity=".62">VIDEOS · WHOSE TURN</text>
- <g fill="#f5f5f0" stroke="#4a4a47" stroke-width="1.6" stroke-dasharray="7 7">
- <rect x="610" y="310" width="520" height="330"/>
- </g>
- <text x="630" y="346" font-size="17" letter-spacing="4" fill="#0d0d0d" opacity=".8">SITE · ONE PUSH</text>
-
- <!-- 手前の板(表に見える研究) -->
- <rect x="530" y="390" width="520" height="330" fill="#0d0d0d"/>
- <text x="556" y="430" font-size="17" letter-spacing="4" fill="#f5f5f0" opacity=".7">FRONT · PHYSICAL AI LAB</text>
- <!-- 机とアームの小さな線画 -->
- <g stroke="#f5f5f0" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round">
- <line x1="600" y1="670" x2="980" y2="670"/>
- <rect x="690" y="640" width="60" height="30" fill="#f5f5f0"/>
- <path d="M720 640 L760 560 L850 590 L880 620"/>
- <path d="M880 620 L900 612 M880 620 L894 638"/>
- <rect x="900" y="642" width="26" height="28" fill="#f5f5f0"/>
- </g>
- <circle cx="760" cy="560" r="8" fill="#0d0d0d" stroke="#f5f5f0" stroke-width="4"/>
- <circle cx="850" cy="590" r="8" fill="#0d0d0d" stroke="#f5f5f0" stroke-width="4"/>
-
- <!-- 裏へ回る細い矢印 -->
- <path d="M1050 555 C 1140 555, 1180 520, 1205 470" fill="none" stroke="#0d0d0d" stroke-width="1.8"/>
- <path d="M1196 474 L1207 462 L1213 478" fill="none" stroke="#0d0d0d" stroke-width="1.8"/>
-
- <!-- 見出し(控えめ) -->
- <text x="110" y="200" font-size="18" letter-spacing="6" fill="#0d0d0d" opacity=".7">SERIES · BACKSTAGE</text>
- <text x="110" y="790" font-size="64" font-weight="700" fill="#0d0d0d">BEHIND THE</text>
- <text x="110" y="866" font-size="64" font-weight="700" fill="#0d0d0d">SOLO LAB.</text>
- <rect x="110" y="900" width="420" height="10" fill="url(#hatch)" stroke="#0d0d0d" stroke-width="1.2"/>
-</svg>
-</body>
-</html>
+<defs>
+<pattern id="stripe" width="14" height="14" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><rect width="14" height="14" fill="#f5f5f0"/><rect width="5" height="14" fill="#0d0d0d"/></pattern>
+<pattern id="dots" width="16" height="16" patternUnits="userSpaceOnUse"><circle cx="4" cy="4" r="2.2" fill="#0d0d0d"/></pattern>
+<pattern id="dotsw" width="16" height="16" patternUnits="userSpaceOnUse"><circle cx="4" cy="4" r="2.2" fill="#f5f5f0"/></pattern>
+<pattern id="vfold" width="40" height="10" patternUnits="userSpaceOnUse"><rect width="40" height="10" fill="#0d0d0d"/><rect x="26" width="5" height="10" fill="#f5f5f0" opacity=".85"/><rect x="33" width="2" height="10" fill="#f5f5f0" opacity=".5"/></pattern>
+</defs>
+<rect width="1500" height="1000" fill="#0d0d0d"/><rect x="80" y="80" width="140" height="140" fill="url(#dotsw)" opacity=".5"/><rect x="560" y="140" width="380" height="760" fill="none" stroke="#f5f5f0" stroke-width="3"/><polygon points="560,140 760,140 1500,1000 560,900" fill="#f5f5f0" opacity=".08"/><polygon points="560,140 760,140 760,900 560,900" fill="#f5f5f0"/><polygon points="760,140 940,90 940,950 760,900" fill="url(#stripe)" stroke="#f5f5f0" stroke-width="3"/><circle cx="905" cy="530" r="9" fill="#0d0d0d" stroke="#f5f5f0" stroke-width="3"/><text x="590" y="260" font-size="22" fill="#0d0d0d">site</text><line x1="590" y1="272" x2="730" y2="272" stroke="#0d0d0d" stroke-width="1" opacity=".35"/><text x="590" y="304" font-size="22" fill="#0d0d0d">videos</text><line x1="590" y1="316" x2="730" y2="316" stroke="#0d0d0d" stroke-width="1" opacity=".35"/><text x="590" y="348" font-size="22" fill="#0d0d0d">records</text><line x1="590" y1="360" x2="730" y2="360" stroke="#0d0d0d" stroke-width="1" opacity=".35"/><text x="590" y="392" font-size="22" fill="#0d0d0d">ledger</text><line x1="590" y1="404" x2="730" y2="404" stroke="#0d0d0d" stroke-width="1" opacity=".35"/><text x="590" y="436" font-size="22" fill="#0d0d0d">issues map</text><line x1="590" y1="448" x2="730" y2="448" stroke="#0d0d0d" stroke-width="1" opacity=".35"/><g stroke="#0d0d0d" stroke-width="4" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="585" y1="862" x2="740" y2="862"/><rect x="605" y="842" width="34" height="20" fill="#0d0d0d"/><path d="M622 842 L650 790 L705 808 L718 832"/><path d="M718 832 L730 826 M718 832 L726 846"/></g><circle cx="650" cy="790" r="6" fill="#f5f5f0" stroke="#0d0d0d" stroke-width="4"/><circle cx="705" cy="808" r="6" fill="#f5f5f0" stroke="#0d0d0d" stroke-width="4"/><text x="80" y="820" font-size="44" letter-spacing="2" fill="#f5f5f0" opacity="1">BEHIND THE</text><text x="80" y="876" font-size="44" letter-spacing="2" fill="#f5f5f0" opacity="1">SOLO LAB</text><text x="1010" y="180" font-size="16" letter-spacing="4" fill="#f5f5f0" opacity="0.6">THE DOOR IS</text><text x="1010" y="206" font-size="16" letter-spacing="4" fill="#f5f5f0" opacity="0.6">ALWAYS AJAR</text></svg></body></html>
\ No newline at end of file
