# -*- coding: utf-8 -*- # 動画「ハコブくんの声と絵は、どう作っているのか」(シリーズ「ひとりとAIのチーム」4回目・描く係と声の係)の場面 PATTERN = 2 # 画面パターン2(大文字型・video_rules.md 4-4c) MOVE = True # 画面型の動き(4-4d)を強弱を付けて使う(2026-11-01 エイイチが選んだ) # 2026-11-01: 「AIに昨日の続きをさせる」の scenes.py の形を写して書いた # 強弱(video_rules.md 4-4d): # 中: manga(縦長の並び)・vd_text(頼んだ文章)・size(チェック項目) # 強(11場面・続けない): hook_koma(腕が3本のコマ)・question・body(体の絵)・fail_arm(直す前/直した後)・ledger(22 と数え上げ)・ # listen_old(前の声の動画)・cast(10 個)・read_table(207 と数え上げ)・quiz_ans・mitate_job・answer2 # 背景の「画面」は、記録のファイルをエディタの見た目で描いて下へ送った映像(make_bg.py。中身は記録のまま) # マンガの絵は生成AI(Gemini)で描いた物。直す前の絵(腕が3本)は 第121話/生成/第121話_P3_20261030_114254.jpg import os as _os, sys as _sys _here = _os.path.dirname(_os.path.abspath(__file__)) for _c in (_os.environ.get("P2M_BASE", ""), _os.path.join(_here, "..", "base"), _os.path.join(_here, "..", "_制作ベース")): if _c and _os.path.exists(_os.path.join(_c, "scene_kit.py")): _sys.path.insert(0, _c); break from scene_kit import * from motion_kit import use_motion, CAP import json as _json, base64 as _b64 from scene_kit import _p2page # --- 場面ごとの、文の始まりの秒(声の長さから)。「車は作れるのにロボは作れない」の scenes.py から写した --- _idx = _json.load(open(_os.path.join(_here, 'tts/index.json'))) if _os.path.exists(_os.path.join(_here, 'tts/index.json')) else [] _LINES = {} for _o in _idx: _d = _LINES.setdefault(_o['scene'], []) _t0 = (_d[-1][0] + _d[-1][1] + _d[-1][2]) if _d else 0.0 _d.append((_t0, _o['dur'], _o['pause'])) def at(scene, k=0, f=0.0): L = _LINES.get(scene) if not L: return 0.3 k = min(k, len(L) - 1) return round(L[k][0] + L[k][1] * f, 2) def scene_len(scene): L = _LINES.get(scene) return (L[-1][0] + L[-1][1] + L[-1][2]) if L else 4.0 FUDA_CSS = """ .fuda{position:absolute;left:140px;top:96px;display:flex;align-items:center;gap:14px;font-size:30px;font-weight:700;color:#0D0D0D} .fuda .k{padding:6px 18px;border-radius:999px;font-size:28px;font-weight:900} .fuda .k.ei{background:#C4471F;color:#fff} """ def fuda(who): return f'
エイイチ本人{who}
' # --- エイイチ(ドット絵・右下の小窓)。video_rules.md 1-4 --- import ei_talk as _talk EI = {k: (_talk.base_uri(k), _talk.base_uri(k, True)) for k in ('udegumi', 'unazuku', 'yubisasu', 'hirameku')} import importlib.util as _ilu _sp = _ilu.spec_from_file_location('_narr_here', _os.path.join(_here, 'narration.py')) _nm = _ilu.module_from_spec(_sp); _sp.loader.exec_module(_nm) _EIL = getattr(_nm, 'EI_LINES', []) _EI_SPEAK = {} _cnt = {} for _o in _idx: _k = _cnt.get(_o['scene'], 0); _cnt[_o['scene']] = _k + 1 if _o['i'] in _EIL: _EI_SPEAK.setdefault(_o['scene'], []).append((_k, _o['i'])) EI_CSS = """ .eiw{position:absolute;right:10px;bottom:0px;width:420px;height:420px;overflow:hidden} .eiw img{position:absolute;left:0;top:8px;width:420px;height:420px;image-rendering:pixelated} """ def _blinks(start, end): t, out = start, [] while t < end - 0.4: out.append(round(t, 2)); t += 3.3 return out def ei_wipe(pose, scene, pop=None): L = scene_len(scene) + 1 spans, mouth, blinks = [], [], [] for k, i in _EI_SPEAK.get(scene, []): t0 = at(scene, k, 0.0) sq, end = _talk.mouth_seq(i, t0) spans.append((t0, end)); mouth += sq blinks += [(b, 0.13) for b in _talk.blink_times(t0, end)] mouth.sort() blinks += [(b, 0.15) for b in _blinks((pop or 0) + 1.3, scene_len(scene)) if not any(a0 - 0.2 <= b <= a1 + 0.2 for a0, a1 in spans)] ev = [] for t, l in mouth: if pose in _talk.MOUTH: ev.append((t, f'')) e = _talk.eye_patches(pose) for b, d in blinks: if e: ev.append((b, f'')) ev.append((round(b + d, 2), f'')) ev.sort(key=lambda x: x[0]) inner = f'' + ''.join(h for _, h in ev) a = f' data-a="pop" data-t="{pop}" data-d="0.35"' if pop is not None else '' return f'
{inner}
' def add(page_html, *parts): return page_html.replace('', ''.join(parts) + '', 1) EI_BIG = """""" T1 = "1絵" T2 = "2声" T3 = "3読み" T4 = "4エイイチの見立て" BMS = dict(file="src/bg_mistakes.mp4", dim=0.5, slow=1.3) # 過去の間違いのリスト BCK = dict(file="src/bg_check.mp4", dim=0.5, slow=1.3) # チェック項目 A(体) BVC = dict(file="src/bg_voice.mp4", dim=0.5, slow=1.3) # 声の頼んだ文章 BRD = dict(file="src/bg_read.mp4", dim=0.5, slow=1.3) # 読みの直しのリスト BMG = dict(file="src/bg_manga.mp4", dim=0.55, slow=1.0) # 縦長マンガの確認用の並び OLD = dict(file="src/hakobu_old_wide.mp4", dim=0.8) # サイトのトップの前の動画(前の声) S = {} # 0 フック S["hook_koma"] = p2_word(['腕を', '数えてみて'], kicker="このマンガのコマ", kc="muted", size=170) S["hook_voice"] = p2_word(['この声も', 'AIで作った'], kicker="いま話している、ハコブくんの声", kc="muted", size=150) S["hook_q"] = p2_quiz(['AIの絵と声は、', 'ボタン1つで出る?'], pill="そう思いませんか", size=130) # 2 今日の問い S["question"] = p2_quiz(['ハコブくんの声と絵は、', 'どう作っているのか'], pill="今日の問い", size=120) S["series"] = p2_word(['ひとりとAIの', 'チーム'], kicker="シリーズ 4回目", kc="muted", size=170, sub="今日は「描く係」と「声の係」") S["scope"] = p2_word(['1人の、', '1つのやり方'], kicker="今日の範囲:このチャンネルの作り方", kc="muted", size=150, sub="AIの製品の比べ合いではない") # 3 結論と立場 S["answer"] = p2_pair("AI", "人", da="候補を出す・確かめる", db="最後に選んで通す", ca="blue", cb="red", mid="→", kicker="先に結論", size=200) S["stance"] = p2_word(['手間は', '減った?'], kicker="人が選んで確かめるなら", kc="muted", size=170, sub="減った所と、減っていない所がある") # 4 目次 S["toc"] = p2_list([("1", "絵を、どう描く"), ("2", "声を、どう選んだ"), ("3", "読み間違いを、どう直す"), ("4", "エイイチの見立て")], kicker="今日は3つ+見立て", size=90) # 5 節1 絵 S["c1"] = chapter2("1つ目", "絵", "AIに描かせて、リストと人の目で確かめる") S["body"] = p2_word(['体は', 'AIの絵'], kicker="画面の左のハコブくん", kc="muted", size=170, sub="画像を作るAIで作った絵から、切り抜いた", tag=T1) S["face"] = p2_pair("8つ", "4段階", da="表情", db="口の開き(声の大きさで)", ca="blue", cb="blue", mid="/", kicker="顔はコードで描く", size=200, tag=T1) S["manga"] = p2_num("99", "話", kicker="4コママンガ『ハコブくん』(日本語版・10月半ばまで)", sub="1話目は 2025年12月", tag=T1) S["manga_flow"] = p2_list([("Claude", "指示の文を書く"), ("Gemini", "1コマずつ描く"), ("AI", "リストと見比べる"), ("人", "全部見て通す")], kicker="いまの作り方", size=80, tag=T1) S["manga_why"] = p2_word(['4コマを1枚で', '描かせない'], kicker="1枚に4つの構図を頼むと", kc="muted", size=140, sub="どこかが崩れる", tag=T1) S["fail"] = p2_word(['今日の', '失敗'], kicker="冒頭の、あのコマ", kc="muted", size=190, tag=T1) S["fail_arm"] = p2_pair("腕が3本", "2本に", da="第121話・3コマ目(直す前)", db="描き直した後", ca="red", cb="blue", mid="→", kicker="見つけたのは、人", size=150, tag=T1, sub="第131話でも、大城さんの腕が3本") S["fail_fix"] = p2_word(['腕は2本、', '数えること'], kicker="人が出るコマには、毎回指示に入れた", kc="muted", size=150, sub="確かめるリストにも、腕・手・脚を数える行", tag=T1) S["ledger"] = p2_num("22", "行", kicker="過去の間違いのリスト(10/30〜31・11/1 に数えた)", sub="輪に穴・腕が生えた・扉が浮いた", tag=T1) S["size"] = p2_word(['数字を書いても', '大きさは守らない'], kicker="いちばん手こずったのは、大きさ", kc="muted", size=120, sub="頭と足の位置の線を渡し、描いた後に比を測る", tag=T1) S["c1_sum"] = p2_pair("AIが描く", "人が通す", da="リストで確かめる", db="最後は人の目", ca="blue", cb="red", mid="→", kicker="1つ目のまとめ", size=150, tag=T1) # 6 節2 声 S["c2"] = chapter2("2つ目", "声", "文章で頼んで候補を作り、聞き比べる") S["vd"] = p2_word(['文章で頼むと', '声ができる'], kicker="ElevenLabs のボイスデザイン", kc="muted", size=150, tag=T2) S["vd_text"] = p2_pair("6通り", "3つずつ", da="頼んだ文章", db="1つの文章から出る声", ca="blue", cb="blue", mid="×", kicker="候補を作る", size=170, tag=T2) S["why"] = p2_word(['正確でも', '見た目に合わない'], kicker="最初の、落ち着いた30代の声", kc="muted", size=140, sub="決めたのは、6つ目の文章の2つ目の声", tag=T2) S["model"] = p2_word(['モデルも', '聞き比べた'], kicker="1本目の動画の冒頭14文で", kc="muted", size=160, tag=T2) S["listen_old"] = p2_word(['前の', 'ハコブくんの声'], kicker="サイトのトップの動画", kc="muted", size=150, tag=T2) S["listen_now"] = p2_pair("前の声", "いまの声", da="高い、キャラクターらしい声", db="ボイスデザインで作り直した", ca="", cb="blue", mid="→", kicker="聞き比べると", size=150, tag=T2) S["cast"] = p2_num("10", "個まで", kicker="いまのプランで持てる声", sub="足りないので、出ない話どうしで貸し借り", tag=T2) S["c2_sum"] = p2_pair("AIが作る", "耳で決める", da="候補", db="聞いた人", ca="blue", cb="red", mid="→", kicker="2つ目のまとめ", size=150, tag=T2) # 7 節3 読み S["c3"] = chapter2("3つ目", "読み", "見つけるたびに、直しのリストに足す") S["read_table"] = p2_num("207", "組", kicker="読みの直しのリスト(37本の台本・11/1 に数えた)", sub="台本の言葉 → 読み方", tag=T3) S["read_ex"] = p2_list([("描く", "かく → えがく"), ("係", "カタカナで渡す"), ("名前", "漢字で渡す")], kicker="直した例", size=96, tag=T3) S["quiz"] = p2_quiz(['読みが2つある字。', '毎回同じに読む?'], pill="考えてみてください", size=120, tag=T3) S["quiz_ans"] = p2_word(['同じとは', '限らない'], kicker="答え", kc="muted", size=170, sub="「節」を「せつ」と「ふし」に読み分けた → 言い換える", tag=T3) S["try"] = p2_word(['最後は', '通しで聞く'], kicker="試し読みで読めても", kc="muted", size=160, sub="全文では読み違えることがある", tag=T3) S["less_more"] = p2_pair("していない", "残っている", da="声の録音・一から描く", db="選ぶ・確かめる", ca="blue", cb="red", mid="/", kicker="手間は減ったのか", size=140, tag=T3) S["c3_sum"] = p2_word(['リストに足して、', '同じ所でつまずかない'], kicker="3つ目のまとめ", kc="muted", size=120, tag=T3) # 8 見立て S["mitate"] = chapter2("最後に", "見立て", "エイイチの考え") # --- エイイチの登場と声(2026-11-01 確認用1回目の指示「おれの登場とセリフ(音声)がない」)--- _o, _c = EI['udegumi'] _bl = ''.join(f'' for b in _blinks(1.6, scene_len('ei_intro'))) S["ei_intro"] = page(EI_BIG + f'
{_bl}
エイイチこの番組を作っている人
', tag=T1) S["honne_check"] = add(p2_word(['気がつくと、', 'デザインが変わる'], kicker="作っている本人に聞いた", kc="muted", size=140, sub="だから、気が抜けない", tag=T1), fuda("確認の大変さ"), ei_wipe('udegumi', 'honne_check', pop=0.1)) S["listen"] = add(p2_word(['声は', '何度か試した'], kicker="実は", kc="muted", size=170, sub="最初は、もう少し高いキャラクターっぽい声", tag=T2), fuda("声の決め方"), ei_wipe('hirameku', 'listen', pop=at('listen', 1, 0.0))) S["mitate_fear"] = add(p2_word(['怖いと思うことは、', 'ないな'], kicker="AIで声と絵を作ることに、怖さは?", kc="muted", size=140, tag=T4), fuda("エイイチの見立て"), ei_wipe('udegumi', 'mitate_fear', pop=0.1)) S["mitate_job"] = add(p2_word(['選ぶ仕事も、', 'なくなるかも'], kicker="デザイナーとして", kc="muted", size=150, sub="できないと思った仕事も、すぐ置き換わった", tag=T4), fuda("エイイチの見立て"), ei_wipe('yubisasu', 'mitate_job', pop=0.1)) # 9 答え S["answer2"] = p2_word(['最後に通すのは、', 'いまは人'], kicker="問いへの答え", kc="muted", size=150, sub="AIは候補を出し、AIも確かめる") S["question_next"] = p2_quiz(['その候補、', '誰が選んだ?'], pill="AIの絵や声を見たら", size=150) S["series_next"] = p2_word(['つかめたかを', 'AIが判定'], kicker="次回", kc="muted", size=150, sub="ロボットに戻ります") # 動画シーンに重ねるテロップ(透過) O = { "title": overlay('
フィジカルAI解説
案内役:ハコブくん(AI音声)
'), "title_eiichi": overlay('
エイイチ
デザイナー/ロボティクスの研究・開発
'), "outro": overlay('
相談は概要欄の連絡先から
高評価・チャンネル登録/コメントはエイイチが全部読んでいます
'), } # 実機の映像を流す場面 (映像, 開始秒, 透かし)。開始秒 None は前の場面の続き VIDEO = { "title": ("src/day124_crop.mp4", 4.5, False), "title_eiichi": ("src/day124_crop.mp4", None, False), "outro": ("src/day124_crop.mp4", 18.0, False), } B4 = 'src/manga/p121_P3_before.jpg' A4 = 'src/manga/p121_P3_after.png' def _f(p): return 'file://' + _os.path.abspath(_os.path.join(_here, p)) # 動きのある場面(motion_kit.py が src/motion_{場面}.mp4 に書き出す) MOTION = { "hook_koma": dict(type="html", html=page( f'' '
1
' '
2
' '
3
' '
マンガの絵は生成AI(Gemini)で作成・第121話の直す前の絵
')), "question": dict(type="video", bg=dict(BMG, ss=0, blur=6), card=dict(pill="今日の問い", big="ハコブくんの声と絵は、
どう作っているのか", size=110, at=0.15)), "body": dict(type="html", html=page( f'' f'' '
体はAIの絵、顔はコード
', tag=T1)), "manga": dict(type="video", bg=dict(BMG, ss=0), cap="縦長の4コマの確認用の並び(絵は生成AI)", card=dict(kicker="4コママンガ『ハコブくん』日本語版", kicker_color="#ddd", big='99話', sub="1話目は 2025年12月(10月半ばまで)", size=200, at=(0, 0.6))), "fail_arm": dict(type="html", html=page( f'' f'' '
直す前:腕が3本
' '
描き直した後
' '
第121話の3コマ目(絵は生成AIで作成)
', tag=T1)), "ledger": dict(type="video", bg=dict(BMS, ss=0), cap="チェック項目と過去の間違い.md(過去の間違いのリスト)", card=dict(kicker="過去の間違いのリスト(10/30〜31)", kicker_color="#ddd", big='0行', sub="輪に穴・腕が生えた・扉が浮いた", size=220, at=(0, 0.2))), "size": dict(type="video", bg=dict(BCK, ss=0), cap="チェック項目と過去の間違い.md(A. 体)", card=dict(kicker="いちばん手こずったのは、大きさ", kicker_color="#ddd", big='数字を書いても
大きさは守らない', sub="頭と足の位置の線を渡し、描いた後に比を測る", size=110, at=(0, 0.3))), "vd_text": dict(type="video", bg=dict(BVC, ss=0), cap="ハコブくんの声_候補.md(頼んだ文章の日本語訳)", card=dict(kicker="候補を作る", kicker_color="#ddd", big='文章6通り × 声3つずつ', size=110, at=(0, 0.2))), "listen_old": dict(type="video", bg=dict(OLD, ss=0), cap="サイトのトップで流していた、前のハコブくんの動画", card=dict(kicker="前のハコブくんの声", kicker_color="#ddd", big='聞いてみてください', size=110, at=(0, 0.1))), "cast": dict(type="count", counts=[("10", 10, 0.2, (0, 0.8))]), "read_table": dict(type="video", bg=dict(BRD, ss=0), cap="37本の台本の読みの直し(ELEVEN_READ・重複なし)", card=dict(kicker="読みの直しのリスト(11/1 に数えた)", kicker_color="#ddd", big='0組', size=220, at=(0, 0.2))), "quiz_ans": dict(type="slam", at=0.05), "ei_intro": dict(type="html", html=S["ei_intro"]), "honne_check": dict(type="html", html=S["honne_check"]), "listen": dict(type="html", html=S["listen"]), "mitate_fear": dict(type="html", html=S["mitate_fear"]), "mitate_job": dict(type="html", html=S["mitate_job"]), "answer2": dict(type="slam", at=(1, 0.1)), } use_motion(MOTION, VIDEO, O, _here, MOVE)