4コママンガ『ハコブくん』を Gemini の API で描く ― 次は話を選ぶだけでショートまで

4コママンガ『ハコブくん』を Gemini の API で描く ― 次は話を選ぶだけでショートまで

この記事の素材(コード・ログ・データ 9ファイル・まとめてダウンロードあり)

4コママンガ『ハコブくん』の縦長のコマは、Google の Gemini API(画像を描くモデル Nano Banana 2)で、1コマ約11円・20秒前後で描けました。参照画像の前に「この絵は高杉さん」のような名前の札を付けて送ると、キャラの取り違えも直りました。 第61話「押していいのか問題」は、4コマの絵から、声、22.9秒の YouTube ショート、X の予約まで、Claude との1つの会話の中で作れています。

『ハコブくん』は、病院で薬を運ぶロボット・ハコブくんと看護師さんたちの4コママンガです。これまで絵は、Gemini のアプリ(ブラウザやスマホで使うもの)に、1コマずつ手で頼んでいました。Claude が書いたプロンプト(絵を頼む文)をコピーし、キャラの三面図(正面・後ろ・横から描いた設定画)を添えて送り、できた絵を保存する。4コマで4回、描き直しがあればその分だけ、この手の作業が増えます。

API(プログラムから AI に頼むための窓口)を使うと、この「コピーして送って保存する」を、プログラムに任せられます。ただ、アプリと同じ絵柄で描けるのか、1枚いくらかかるのかは、試すまで分かりませんでした。

試して分かったことを先に並べます。

確かめたこと結果
縦長(4:5)で返ってくるか
928×1152 で返る。比は 0.806 で、4:5(0.8)とほぼ同じ
1コマの費用
約11円(入力 約3,000〜4,800 トークン・出力の画像 1,120 トークン)
1コマの時間12〜35秒
絵柄
サイトに出しているマンガの見え方に近い。ハコブくんの形・トーン・字の書き方もそろう
うまくいかなかったこと
看護師さんの髪型の取り違え(ボブとお団子)、セリフの字の重なり・抜け、ボタンの形の違い
効いた直し方
参照画像に名前の札を付けて送る・最初に描けたコマを部品の見本にして添える
第61話の4枚にかかった費用
描き直しを含めて13枚・約144円

絵のほかにも、同じ流れで手作業を減らしました。

  • 声:ショートの声は、ElevenLabs(文章を人の声で読み上げるサービス)の鍵を毎回ターミナルに入れて作っていました。鍵をファイルに置き、会話の中から作れるようにしました
  • LinkedIn:解説動画を LinkedIn に手で上げていたのをやめ、YouTube の公開と同じ時刻に、YouTube のリンク付きで自動で出すようにしました
  • データの置き場所:キャラの図・指示・話ごとのデータが1つのフォルダに混ざっていたので、分けました
Gemini の API で描き、背景を白にして仕上げた第61話の4コマ。袋を引っかけたハコブくん、停止ボタンの前で指が止まる高杉さん、無言で押す大城さん、3人の会話のオチ

この記事の効果の書き方について断っておきます。費用・時間・トークン数は、API の返事に入っていた実際の値です。「手作業が減る」は見込みで、次の話を作るときに、どれだけ人の手が要ったかで確かめます。

作業ログ ― Gemini の API で4コマを描く準備

Gemini のアプリではなく API を使うと決めた理由

縦長の4コマを作る会話(LOG408 の続き)で、まず「アプリの代わりに何が使えるか」を比べました。

方法できることこのマンガに向かない理由
Gemini の API
プロンプトとキャラの画像と縦長の下地を、スクリプトから送れる
費用がかかる(アプリの契約とは別)
Figma の連携の画像生成
Gemini の画像モデルを選べる
文章しか渡せず、キャラの画像を添えられない。顔がそろわない
ブラウザの自動操作
アプリの Gemini をそのまま使える
遅く、画面が変わると止まりやすい

キャラの顔をそろえるには、三面図を毎回添える必要があります。それができるのは API だけでした。

Gemini API と Google AI Studio は何か

Gemini API は、Google の AI の Gemini を、プログラムから呼び出すための窓口です。画像を描くモデルもここから使えます。鍵(API キー)を作るのと支払いの設定は、Google AI Studio という Google のページで行います。

この記事で使う言葉意味
API キー(鍵)
プログラムが「誰が頼んでいるか」を Google に示すための文字列。人に見せない。KIOXIA(外付けのドライブ)のファイルに置き、スクリプトは読むだけにした
Nano Banana 2
Gemini の画像を描くモデルの呼び名。API では gemini-3.1-flash-image という名前
トークン
AI が文や画像を数える単位。費用はトークンの数で決まる
無料枠
支払いの設定をしなくても使える範囲。画像のモデルは無料枠では使えなかった
前払い(クレジット)
Google AI Studio の支払いは、先にお金を入れて、使った分が引かれる形だった

このプロジェクトでは、Claude(この会話の AI)がプロンプトを書き、Mac の上のスクリプトが Gemini API に送り、返ってきた絵を Claude が見て直す、という分担にしました。

API の鍵を作って KIOXIA に置いた手順

鍵は Google AI Studio の「API キー」の画面で作りました。初めてなので、プロジェクト(鍵や支払いをまとめる単位)も、その場で「p2m-manga」として作りました。

鍵は、ターミナルで次の1行を動かして、コピーした鍵をそのままファイルに書き込む方法で置きます。テキストエディットで保存すると書式や拡張子が変わりやすいためです。

pbpaste > /Volumes/KIOXIA/gemini_api_key.txt

pbpaste は「いまコピーしてあるもの」を出す命令です。ここで2回つまずきました。

  1. この1行をチャットからコピーしたとき、先にコピーしてあった鍵が消え、ファイルの中身がコマンドの文字そのものになった
  2. やり直しで、↑キーで出したコマンドの後ろに鍵が続けて貼られ、鍵の文字が入った名前のファイルができた

2回目は、ファイルの中身(鍵そのもの)を正しい名前のファイルに写し、間違ってできたファイルは消してもらいました。確かめるときは鍵の中身を表示せず、長さと文字の種類だけを見ます。

f="$HOME/mnt/KIOXIA/gemini_api_key.txt"; ls -la "$f" | awk '{print $5, $6, $7, $8}'; wc -c < "$f"; head -c 4 "$f" | grep -q '^AIza' && echo "先頭はAIza" || echo "先頭がAIzaではない"; tr -d '\n' < "$f" | grep -qE '^[A-Za-z0-9_-]+$' && echo "余計な文字なし" || echo "余計な文字あり"; wc -l < "$f"; P="$HOME/mnt/PixelstoMotors/ハコブくん/4コママンガ作成データ/縦長試作_20261030/第61話_P1_プロンプト.txt"; wc -c < "$P"
44 Oct 30 05:03
44
先頭がAIzaではない
余計な文字あり
0
0
f="$HOME/mnt/KIOXIA/gemini_api_key.txt"; sed -E 's/[A-Za-z]/a/g; s/[0-9]/9/g' "$f"; echo; grep -c "pbpaste\|Volumes" "$f"
aaaaaaa > /aaaaaaa/aaaaaa/aaaaaa_aaa_aaa.aaa
1

英字を a、数字を 9 に置き換えて形だけを見ると、中身がコマンドの文字だと分かります。もう1つ、確かめ方も間違っていました。今の Google の鍵は AIza ではなく AQ. で始まる形でした。

2回目の直しで使ったスクリプトです。鍵の文字は表示しません。

cd "$HOME/mnt/KIOXIA" && python3 - <<'EOF'
import os
base="gemini_api_key.txt"
odd=[n for n in os.listdir('.') if n.startswith(base) and n!=base]
assert len(odd)==1, len(odd)
odd=odd[0]
suffix=odd[len(base):]
content=open(odd,encoding='utf-8').read().strip()
print("中身と名前の後ろが同じ:", content==suffix, "長さ:", len(content), "先頭3文字の形:", "AQ." if content.startswith("AQ.") else "ちがう")
open(base,'w',encoding='utf-8').write(content)
os.makedirs("_to_delete",exist_ok=True)
os.rename(odd,"_to_delete/gemini_key_mistake.txt")
print("書き直した長さ:", os.path.getsize(base))
print("残り:", [n for n in os.listdir('.') if 'gemini' in n])
EOF
中身と名前の後ろが同じ: True 長さ: 53 先頭3文字の形: AQ.
書き直した長さ: 53
残り: ['gemini_api_key.txt']

途中でフォルダの一覧を出したとき、名前に入っていた鍵の文字が一度画面に出ました。この記事には載せていません。

  • ねらい:鍵を人に見せずに、スクリプトから使える場所に置く
  • 変わったこと:鍵は KIOXIA の決まった名前のファイルに1つだけ。スクリプトは読むだけで、記事・ログ・JSON には出さない
  • 見込める効果:鍵を貼る手作業が、最初の1回で終わる。ElevenLabs の鍵も同じ形にした(後の節)

作業ログ ― Claude Code で第61話の1コマ目を試した

この記事のコード・ログ・データは、1ファイルずつ下に置いています。全文はリンク先にあります。

ファイルの一覧(9本)

01_ude_Code_gemini_panel.py_20261030_33_Gemini_API___.md ― Claude Code の報告と gemini_panel.py / 20261030_33_Gemini画像API試し_報告.md・6,686字

02_ude_Code_gemini_panel.py_gemini_panel.py.txt ― Claude Code の報告と gemini_panel.py / gemini_panel.py(Claude Code が作ったスクリプト)・5,639字

03_sec03.py.txt ― gemini_gen_labeled.py:画像の前に名前の札を付けて送る / gemini_gen_labeled.py・3,241字

04_sec03.txt ― 使った4枚のプロンプト / 第61話_P1_プロンプト_v4.txt・10,720字

05_sec03.txt ― 使った4枚のプロンプト / 第61話_P2_プロンプト_v2.txt・10,050字

06_sec03.txt ― 使った4枚のプロンプト / 第61話_P3_プロンプト_v2.txt・9,314字

07_sec03.txt ― 使った4枚のプロンプト / 第61話_P4_プロンプト_v5.txt・11,900字

08_sec03.md ― 縦長の差分指示に足した学び / 縦長の差分指示.md・3,380字

09_LinkedIn_YouTube.txt ― LinkedIn の本編の動画を、YouTube のリンクで自動で出すようにした / LinkedIn を YouTube のリンクにした変更(差分)・5,895字

Claude Code に渡した依頼

最初の試しは、Mac の Claude Code(Mac の上でコードを書いて動かす Claude。LOG390 の依頼箱で依頼を受け渡している)に頼みました。依頼箱では別の長い仕事(12時間まで)が動いていたので、依頼は手で Claude Code に渡しています。

20261030_33_Geminiの画像APIで第61話P1を1枚試す.md

---
作業フォルダ: /Users/shimazueiichi/r2r2r-scripts
上限時間: 45
---
# 依頼:Gemini の画像の API で、ハコブくん縦長4コマ 第61話の Panel 1 を1枚だけ生成して確かめる(2026-10-30)

## ねらい
今はアプリの Gemini に1コマずつ手で頼んでいる。API で同じ頼み方(プロンプト+キャラの画像+縦長の下地)ができるかを、1枚で確かめる。

## 置き場所(作業フォルダの外は、下の2か所の読み書きだけ)
- 作成データ:`~/Library/Mobile Documents/com~apple~CloudDocs/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/ハコブくん/4コママンガ作成データ/`(以下「作成データ」)
- API の鍵:`/Volumes/KIOXIA/gemini_api_key.txt`(読むだけ。鍵の文字をログ・報告・コード・git に出さない。環境変数にも書き残さない)

## やること
1. 最初に、KIOXIA がつながっているか、鍵のファイルがあるかを確かめる。どちらかがなければ、何もせずに止めて報告する
2. スクリプト `~/r2r2r-scripts/manga/gemini_panel.py` を作る
   - 鍵はファイルから読むだけ
   - 引数:プロンプトのファイル・添付する画像(複数)・出力先・モデル名
   - Google の公式の Python の部品(google-genai)を使う。なければ入れる
   - 縦横比は API の指定で 4:5、大きさは 1K を指定する(指定の項目名は Google の公式の資料で確かめる)
   - 返ってきた画像と、返事の中身(使ったモデル名・トークン数などの使用量。画像の中身は除く)を JSON で保存する
3. モデルを決める:鍵で使えるモデルの一覧を取り、画像を作れるモデルを並べる。アプリの Gemini が今使っている画像のモデルと同じものを、Google の公式の資料で確かめて選ぶ(軽い版 Lite は選ばない)。決めた理由を報告に書く
4. 生成は **1回だけ**。プロンプトは `作成データ/縦長試作_20261030/第61話_P1_プロンプト.txt` の全文をそのまま渡す(書き換えない・足さない)。空なら止めて報告する
5. 添付する画像(この順):
   - `作成データ/canvas_4x5_1024x1280.png`(縦横比の下地)
   - `作成データ/ハコブくん.png`(リファレンス台帳の「ハコブくんが出る全コマ」)
   - 高杉さんは画面に入らない(声だけ)ので添付しない
   - プロンプトの冒頭の「USE ATTACHED REFERENCES」の一覧(ハコブくん.png と、縦長の下地)と食い違ったら、生成せずに止めて報告する
6. 出力:`作成データ/縦長試作_20261030/API試し/` に `第61話_P1_api_{モデル名}_{日時}.png` と同じ名前の `.json`
7. 失敗したとき(無料枠では使えない・支払いの設定がない、など)は、やり直さずに、Google からの返事の文をそのまま報告に書く
8. スクリプトだけを git に個別に add して commit する(鍵と画像は入れない)

## 確かめて報告すること
1. **大きさ**:返ってきた画像の縦横の画素数と、比(4:5 にどれだけ近いか)
2. **絵柄**:画像は人が見るので、出力先のパスを書くだけでよい。気づいた崩れ(脚や靴の残像・口が線になる・停止ボタン以外の部品が足されている など)があれば一言
3. **費用**:返事の使用量(入力・出力のトークン数)と、Google の公式の料金のページの単価から、1枚の費用を出す。**円で書く**(ドルは補足。換算に使ったレートと料金ページの URL も書く)
4. 使ったモデル名・かかった時間

## 決めてほしいこと(ここで止める)
- 2枚目以降の生成・Panel 2〜4 の生成はしない。続けるかはエイイチが決める

1回目は「無料枠の上限が0」で断られた

1回目は、絵が返ってきませんでした。Google の返事は次のとおりです。

429 RESOURCE_EXHAUSTED … Quota exceeded for metric: generate_content_free_tier_requests,
limit: 0, model: gemini-3.1-flash-image

画像のモデルは、無料枠では1枚も使えません。 料金のページでも、このモデルの無料枠は「Not available」でした。Google AI Studio の「課金」から支払いを設定し、前払いで 3,200円を入れました。残りが減っても自動では足されない設定(オートチャージ オフ)にしています。

Claude Code の報告と gemini_panel.py

支払いの設定のあと、同じコマンドでもう一度描かせました。Claude Code の報告の全文です(1回目の報告に、Panel 2〜4 の追記が続きます)。

20261030_33_Gemini画像API試し_報告.md

# 報告:Gemini の画像 API で第61話 P1 を1枚試す(2026-10-30)

## 結論
**生成できなかった。** API への依頼は1回だけ送ったが、Google から「無料枠ではこのモデルの上限が 0」という返事が来て断られた(429 RESOURCE_EXHAUSTED)。依頼のとおり、やり直していない。
画像の API は**支払いの設定(有料の枠)がないと使えない**。料金のページにも、このモデルの無料枠は「Not available」と書いてある。
スクリプトはできていて、依頼の中身(プロンプト・添付・4:5・1K の指定)も確かめた。支払いの設定をすれば、同じコマンドをもう一度動かすだけで試せる。

## やったこと
別のタブで開く保存

gemini_panel.py(Claude Code が作ったスクリプト)

#!/usr/bin/env python3
"""Gemini の画像 API でマンガの1コマを1枚だけ生成する。

使い方:
  python3 manga/gemini_panel.py --prompt P.txt --image canvas.png --image hakobu.png \
      --out-dir OUT --model gemini-3.1-flash-image --name 第61話_P1

- 鍵は /Volumes/KIOXIA/gemini_api_key.txt から読むだけ (表示・保存しない)
別のタブで開く保存

1コマ目の絵とサイトのマンガを見比べた

返ってきた1コマ目は 928×1152 でした。頼んだ 1024×1280 より1割ほど小さく、比はほぼ 4:5 です。中身は JPEG で、名前だけ .png になっていたので、2枚目からは拡張子を中身に合わせました。

絵を見て、最初は「カメラが高い」と指摘しました。プロンプトでは「カメラは約50cm・上面を見せない」と頼んでいたのに、ハコブくんの上面が見えていたからです。ところが、サイトに出しているマンガ(第61話・第50話・第24話の公開版)を見直すと、ハコブくんは上面が少し見える高さで描くのがふつうでした。指摘は取り下げ、縦長の指示とカメラワーク規定のほうを直しました。

cd "$HOME/mnt/PixelstoMotors/ハコブくん/4コママンガ作成データ"; python3 - <<'EOF'
p1="プロジェクトのファイル_md_20261030/カメラワーク規定_縦長版.md"
p2="マンガのプロジェクト指示_縦長版_20261030.md"
p3="縦長試作_20261030/縦長の差分指示.md"
s=open(p1,encoding='utf-8').read()
old="- ハコブくんを上から見下ろすショットは禁止(既存ルール)。見下ろす高さにするのは、人物だけを撮るコマに限る\n"
new=old+"- ハコブくん(1150mm)を人の目線(約150cm)から撮ると、上面が少し見える。これがサイトのマンガのふつうの見え方で、見下ろすショットには当たらない。ハコブくんが出るコマの高さは、まず人の目線を基本にする\n- 低い位置(約50cm)は、コマの中身で選ぶ(床・足元・引きずる物を大きく見せる、など)。上面を見せたくない理由がないときは使わない\n"
assert s.count(old)==1; open(p1,'w',encoding='utf-8').write(s.replace(old,new))
s=open(p2,encoding='utf-8').read()
old="- ハコブくんの「あおり・俯瞰なし」は変えない。高さの差は人物の仕草(見上げる・見下ろす)で出す\n"
new=old+"- ハコブくんが出るコマのカメラは、人の目線(約150cm・上面が少し見える)を基本にする。これはサイトのマンガのふつうの見え方で、俯瞰には当たらない。低い位置(約50cm)は、床・足元を見せるなどコマの中身で選ぶときだけにする\n"
assert s.count(old)==1; open(p2,'w',encoding='utf-8').write(s.replace(old,new))
s=open(p3,encoding='utf-8').read().rstrip('\n')+"\n"
s+="""
## 試作で分かったこと(第61話・Gemini の API・2026-10-30)

- **カメラの高さ**:Panel 1 を「低め(約50cm)・上面を見せない」で頼んだが、上面が少し見える高さで描かれた。サイトのマンガもこの見え方がふつう(第61話・第50話・第24話の公開版で確認)。ハコブくんが出るコマは人の目線(約150cm)を基本にし、低い位置はコマの中身で選ぶときだけにする(カメラワーク規定とプロジェクト指示に書き足した)
"""
open(p3,'w',encoding='utf-8').write(s)
print("ok")
EOF

プロンプトを作る場所を、この会話に移すと決めた

ここまでプロンプトは、claude.ai の別のプロジェクト(縦長のマンガのプロンプトを作るプロジェクト)で作り、テキストに貼って保存していました。プロンプトの決まり(縦長の指示・カメラワーク規定・リファレンス台帳)は、どれも Mac のファイルにあります。この会話で読めば、プロンプト作りから生成・確かめまで1つの会話でつながります。第61話の Panel 2〜4 だけは前のプロジェクトで作り、次の話からはこの会話(開発ログのプロジェクト)で作ると決めました。

  • ねらい:プロンプトを貼って保存する手間と、決まりの写しが2か所でずれることをなくす
  • 変わったこと:決まりの正は Mac のファイル1つ。プロンプト・生成・確かめを同じ会話で行う
  • 見込める効果:別のプロジェクトにファイルを入れ直す作業がなくなる(第61話では、カメラの決まりを直したあとに1回入れ直した)

作業ログ ― 参照画像に名前の札を付けて、取り違えを直した

Claude Code が描いた Panel 2〜4 は、髪型を取り違えた

Panel 2〜4 も Claude Code に1回ずつ描かせました。三面図と見比べると、合っていないところがはっきりありました。

  • P2・P4 の高杉さんが長い髪で描かれた。三面図はあご丈の黒いボブ
  • P3 で押す人がボブで描かれた。押すのはお団子の大城さん
  • P4 のハコブくんが、画面が横に付いた箱の形に崩れた。セリフも「どこにも書/書いてないのよ」と「書」が2回出た

原因の1つは、プロンプトの見分けの言葉でした。縦長の指示では、高杉さんの見分けの言葉を hair worn down, no glasses(髪をおろす・メガネなし)と決めていました。アプリでは添付の三面図が勝ってボブになっていたようですが、API では「おろす」を長い髪と読みました。もう1つは、画像を並べて送るだけで、どの画像が誰の三面図かを伝えていなかったことです。

gemini_gen_labeled.py:画像の前に名前の札を付けて送る

API では、画像の前に文を挟めます。そこで、画像ごとに「Reference image — Nurse Takasugi, short chin-length black bob…」のような札を付けて送るスクリプトを、こちらで書きました。Mac の作業用の窓(Linux)から Google の API に直接届いたので、Claude Code を通さずに動かしています。

gemini_gen_labeled.py

#!/usr/bin/env python3
"""Gemini の画像の API で、名前の札を付けた参照画像と一緒に1コマを生成する(第61話の描き直し用・2026-10-30)
使い方: python3 gemini_gen_labeled.py <プロンプト.txt> <出力の頭> <札=画像のパス> ...
鍵は KIOXIA/gemini_api_key.txt から読むだけ(表示しない)"""
import sys, json, base64, time, urllib.request, mimetypes, os, datetime
MODEL = "gemini-3.1-flash-image"
KEY_PATHS = [os.path.expanduser("~/mnt/KIOXIA/gemini_api_key.txt"), "/Volumes/KIOXIA/gemini_api_key.txt"]
key = next(open(p).read().strip() for p in KEY_PATHS if os.path.exists(p))
prompt_path, out_head, refs = sys.argv[1], sys.argv[2], sys.argv[3:]
parts = []
for r in refs:
    label, path = r.split("=", 1)
    mime = mimetypes.guess_type(path)[0] or "image/png"
    parts.append({"text": f"Reference image — {label}:"})
    parts.append({"inlineData": {"mimeType": mime, "data": base64.b64encode(open(path, "rb").read()).decode()}})
parts.append({"text": open(prompt_path, encoding="utf-8").read()})
body = {"contents": [{"role": "user", "parts": parts}],
        "generationConfig": {"responseModalities": ["IMAGE"], "imageConfig": {"aspectRatio": "4:5", "imageSize": "1K"}}}
req = urllib.request.Request(f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL}:generateContent",
        data=json.dumps(body).encode(), headers={"Content-Type": "application/json", "x-goog-api-key": key})
t = time.time()
try:
    res = json.load(urllib.request.urlopen(req, timeout=170))
except urllib.error.HTTPError as e:
    print("失敗", e.code, e.read().decode()[:800]); sys.exit(1)
el = time.time() - t
stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
img = None
for c in res.get("candidates", []):
    for p in c.get("content", {}).get("parts", []):
        if "inlineData" in p:
            img = p["inlineData"]
ext = ".jpg" if img and "jpeg" in img["mimeType"] else ".png"
base = f"{out_head}_{MODEL}_{stamp}"
if img:
    open(base + ext, "wb").write(base64.b64decode(img["data"]))
for c in res.get("candidates", []):
    for p in c.get("content", {}).get("parts", []):
        p.pop("inlineData", None)
meta = {"model": MODEL, "elapsed_s": round(el, 2), "prompt": prompt_path, "refs": refs, "response": res}
json.dump(meta, open(base + ".json", "w", encoding="utf-8"), ensure_ascii=False, indent=1)
u = res.get("usageMetadata", {})
print(os.path.basename(base + ext) if img else "画像なし", f"{el:.1f}秒", "入力", u.get("promptTokenCount"), "出力", u.get("candidatesTokenCount"), res.get("candidates", [{}])[0].get("finishReason"))

プロンプトは、見分けの言葉と、髪の違いだけを書いた独立の節(WHO IS WHO)を足して作り直しました。

03_sec03.py.txt3,241字・40行・bash
cd "$HOME/mnt/PixelstoMotors/ハコブくん/4コママンガ作成データ/縦長試作_20261030"; python3 - <<'EOF'
T_LIST="- Nurse Takasugi — SHORT chin-length straight black BOB with bangs, no glasses (her labelled reference sheet)"
O_LIST="- Nurse Oshiro — black hair pulled back tight into a BUN at the back of the head, NOT a bob (her labelled reference sheet)"
HAIR_BLOCK="""WHO IS WHO — HAIR IS THE KEY, NEVER SWAP:
- every reference image is labelled with the character's name. Match each character ONLY to her own labelled sheet
- Takasugi: SHORT chin-length black BOB, the hair ends at the jaw line. Her hair NEVER reaches her shoulders. NO long hair, NO ponytail, NO bun
- Oshiro: hair pulled back into a BUN at the back of her head, forehead and ears visible. NEVER a bob, NEVER hair hanging down
"""
別のタブで開く保存
2 10208
3 9412
4 11855

3枚を同時に描かせたときのコマンドと出力です。札の文は、三面図の要点(髪型・メガネ)と、並び図の身長だけにしました。

D="$HOME/mnt/PixelstoMotors/ハコブくん/4コママンガ作成データ"; V="$D/縦長試作_20261030"; cd "$V/API試し"
C="blank 4:5 canvas, it only sets the shape of the picture, draw nothing from it=$D/canvas_4x5_1024x1280.png"
T="Nurse Takasugi, short chin-length black bob, no glasses (front, back, side)=$D/高杉綾-女性看護師.png"
O="Nurse Oshiro, hair in a bun at the back (front, back, side)=$D/大城愛名-女性看護師.png"
H="Hakobukun robot orthographic sheet (front, side, rear)=$D/ハコブくん.png"
L="lineup sheet of relative heights, labels under each figure: ハコブくん 115cm, 高杉 155cm, 大城 165cm=$D/並び図_主要キャラ.png"
python3 gemini_gen_labeled.py "$V/第61話_P2_プロンプト_v2.txt" "第61話_P2_v2" "$C" "$T" "$H" "$L" &
python3 gemini_gen_labeled.py "$V/第61話_P3_プロンプト_v2.txt" "第61話_P3_v2" "$C" "$T" "$O" "$H" "$L" &
python3 gemini_gen_labeled.py "$V/第61話_P4_プロンプト_v2.txt" "第61話_P4_v2" "$C" "$T" "$O" "$H" "$L" &
wait
第61話_P4_v2_gemini-3.1-flash-image_20261030_055115.jpg 21.8秒 入力 4341 出力 1763 STOP
第61話_P2_v2_gemini-3.1-flash-image_20261030_055120.jpg 26.3秒 入力 3639 出力 1702 STOP
第61話_P3_v2_gemini-3.1-flash-image_20261030_055129.jpg 35.1秒 入力 3765 出力 1587 STOP

3枚とも、三面図どおりの髪型になりました。 P4 のハコブくんの形と、セリフの字の重なりも直りました。

名前の札を付ける前と後の比べ。P2 は長い髪からボブに、P3 は押す人がボブからお団子の大城さんに、P4 は箱の形に崩れたハコブくんと重なった字が直った
  • ねらい:どの参照画像が誰の三面図かを、Gemini に取り違えさせない
  • 変わったこと:画像の前に、名前と見分けの要点を書いた札を付けて送る。プロンプトには髪の違いだけの節を置く
  • 見込める効果:P2〜P4 の3枚で、髪型の取り違えが3枚から0枚になった(直す前は Claude Code のスクリプトで札なし、直した後は札あり。どちらも1回ずつ描いた結果)

三面図にない停止ボタンは、最初に描けたコマを見本にした

第61話には、ハコブくんの三面図にない部品(非常停止のボタン)が出てきます。ところが、コマごとに形が変わりました。P2・P3 は「白い台に黒い平たいふた」、P1 は灰色のキノコ形、P4 はキノコ形やボタンなし。

そこで、P2 で描けたボタンを切り抜いて「停止ボタンの見本」と札を付け、ほかのコマにも添えました。これで P1・P4 もほかのコマと同じ形になりました。三面図にない部品は、最初に描けたコマを見本にして、残りのコマに添えるとそろいます。

cd "$HOME/mnt/PixelstoMotors/ハコブくん/4コママンガ作成データ/縦長試作_20261030"; python3 - <<'EOF'
from PIL import Image
im=Image.open("API試し/第61話_P2_v2_gemini-3.1-flash-image_20261030_055120.jpg")
c=im.crop((200,500,420,640)); c=c.resize((c.width*3,c.height*3),Image.LANCZOS); c.save("API試し/停止ボタンの見本_P2v2より.png")
EOF

1コマ目は4回描いた(しっぽ・背中の小窓・袋の位置・ボタン)

1コマ目は、直すたびに別のところがずれて、4回描きました。

回直したこと次に見つかったこと
1回目(Claude Code)
—
吹き出しのしっぽがハコブくんを向き、ハコブくんがしゃべっているように見える。吹き出しが横いっぱい
2回目
しっぽは吹き出しの右上から、右上の角の外へ。幅は画像の約44%
背中の小窓に顔(目と口)が出た
3回目
背中の小窓は何も映らない暗い窓
袋がハコブくんの横(車輪の脇)に付いた。ボタンが灰色のキノコ形
4回目
袋は進む向きの真後ろ。背面の通気口の下の真ん中に結び目が挟まる。ボタンの見本を添えた
使った絵

袋の位置は「真後ろ」と書くだけでは伝わらず、「背面(小窓・扉・黒い通気口の面)の下の真ん中」と、面の名前で書くと直りました。

1コマ目を4回描いた絵。しっぽが下を向いた1回目、背中の小窓に顔が出た2回目、袋が横に付いてボタンが灰色の3回目、袋が真後ろでボタンがそろった4回目

4枚目もボタンの形がほかと違ったので2回描き直し、1回目は「押セマス」が「押セス」と1字抜けました。行の分け方と「セとスの間にマ」を書くと直りました。セリフは毎回1字ずつ見比べています。

生成した13枚の時間と費用

費用は、返事の使用量(入力・出力のトークン数)と、Google の料金のページの単価(入力 $0.50・出力の画像 $60・出力の残り $3。どれも100万トークンあたり)で計算し、1ドル=156.23円で円にしました。

ファイル描いた人時間入力出力費用
P1_api(1回目)Claude Code———
0円(無料枠の上限0で断られた)
P1_api(2回目)Claude Code13.1秒2,8551,64411.0円
P2_apiClaude Code18.2秒3,4431,68111.0円
P3_apiClaude Code19.4秒3,4781,57211.0円
P4_apiClaude Code13.6秒3,8671,86511.1円
P2_v2Claude(札あり)26.3秒3,6391,70211.1円
P3_v2Claude(札あり)35.1秒3,7651,58711.0円
P4_v2Claude(札あり)21.8秒4,3411,76311.1円
P1_v2Claude(札あり)12.0秒3,0801,53910.9円
P4_v3Claude(札あり)20.5秒4,3961,97911.2円
P1_v3Claude(札あり)19.9秒3,1271,63411.0円
P1_v4
Claude(札あり・ボタンの見本)
16.3秒3,5081,60911.0円
P4_v4
Claude(札あり・ボタンの見本)
23.7秒4,6971,58011.1円
P4_v5
Claude(札あり・ボタンの見本)
21.1秒4,7561,83911.2円
合計13枚・143.7円

出力のうち画像の分は、どの回も 1,120 トークンでした。添付の画像を増やすと入力のトークンが増えますが、費用の大半は出力の画像の分(1枚 $0.0672)なので、1枚の費用はほとんど変わりません。

使った4枚のプロンプト

第61話_P1_プロンプト_v4.txt

04_sec03.txt10,720字・114行・text
black and white manga panel artwork on a flat chroma-key green background, single panel, no panel border, no frame line
the artwork bleeds to all four edges of the image

USE ATTACHED REFERENCES — match face, hair, body proportions, robot shape, line weight, and screentone density EXACTLY. Do not stylize beyond reference.
- Hakobukun — ハコブくん.png
- the blank 4:5 canvas image only sets the shape of the picture — draw nothing from it

HAKOBUKUN — TEXT-FREE MACHINE, HIGHEST PRIORITY:
別のタブで開く保存

第61話_P2_プロンプト_v2.txt

05_sec03.txt10,050字・114行・text
black and white manga panel artwork on a flat chroma-key green background, single panel, no panel border, no frame line
the artwork bleeds to all four edges of the image

USE ATTACHED REFERENCES — match face, hair, body proportions, robot shape, line weight, and screentone density EXACTLY. Do not stylize beyond reference.
- Nurse Takasugi — SHORT chin-length straight black BOB with bangs, no glasses (her labelled reference sheet)
- Hakobukun (its labelled orthographic sheet: front, side, rear)
- the lineup sheet shows their relative heights
別のタブで開く保存

第61話_P3_プロンプト_v2.txt

06_sec03.txt9,314字・104行・text
black and white manga panel artwork on a flat chroma-key green background, single panel, no panel border, no frame line
the artwork bleeds to all four edges of the image

USE ATTACHED REFERENCES — match face, hair, body proportions, robot shape, line weight, and screentone density EXACTLY. Do not stylize beyond reference.
- Nurse Takasugi — SHORT chin-length straight black BOB with bangs, no glasses (her labelled reference sheet)
- Nurse Oshiro — black hair pulled back tight into a BUN at the back of the head, NOT a bob (her labelled reference sheet)
- Hakobukun (its labelled orthographic sheet: front, side, rear)
- the lineup sheet shows their relative heights
別のタブで開く保存

第61話_P4_プロンプト_v5.txt

07_sec03.txt11,900字・126行・text
black and white manga panel artwork on a flat chroma-key green background, single panel, no panel border, no frame line
the artwork bleeds to all four edges of the image

USE ATTACHED REFERENCES — match face, hair, body proportions, robot shape, line weight, and screentone density EXACTLY. Do not stylize beyond reference.
- Nurse Takasugi — SHORT chin-length straight black BOB with bangs, no glasses (her labelled reference sheet)
- Nurse Oshiro — black hair pulled back tight into a BUN at the back of the head, NOT a bob (her labelled reference sheet)
- Hakobukun (its labelled orthographic sheet: front, side, rear)
- the lineup sheet shows their relative heights
別のタブで開く保存

縦長の差分指示に足した学び

直し方は、縦長の差分指示(縦長の4コマを作るときの決まりの追記)に1つずつ書き足しています。全文です。

縦長の差分指示.md

08_sec03.md3,380字・80行・markdown
# 縦長4コマ(ショート用の試作)— 差分の指示

この指示は、プロジェクト指示に対する**今回だけの上書き**です。ここに書いていない決まり(キャラ・ハコブくん・ピカリくん・背景の緑・吹き出し・文字・禁止事項)は、プロジェクト指示と v4.0 のままです。

## 目的

1コマ=縦長1枚(4:5)の4枚で、YouTube ショートでめくって見せる試作。サイトと SNS には出さない。
別のタブで開く保存

縦長の指示の、高杉さんの見分けの言葉も変えました。

cd "$HOME/mnt/PixelstoMotors/ハコブくん/4コママンガ作成データ"; python3 - <<'EOF'
p="マンガのプロジェクト指示_縦長版_20261030.md"
s=open(p,encoding='utf-8').read()
old="/ 高杉=`hair worn down, no glasses` /"
assert s.count(old)==1
s=s.replace(old,"/ 高杉=`short chin-length black bob, no glasses`(三面図はあご丈のボブ。`hair worn down` と書くと API では長い髪になった)/")
open(p,'w',encoding='utf-8').write(s)
EOF

作業ログ ― 背景を白にして 1080×1350 に仕上げた

Gemini には、背景を一色の緑(クロマキー)で描かせています。緑の強さから「どれだけ背景か」を出して白に置き換え、1080×1350 に書き出すスクリプトです。JPEG のむらで背景にうっすら灰色が残ったので、ほぼ背景の画素は真っ白にそろえる1行を足しています。

仕上げ_緑を白に_1080x1350.py

"""縦長4コマの仕上げ:緑の背景を白に置き換え(緑の強さから背景の割合を出す・にじみなし)、1080×1350 に書き出す"""
import sys, numpy as np
from PIL import Image
def finish(src, dst):
    a = np.asarray(Image.open(src).convert("RGB")).astype(np.float64)
    h, w, _ = a.shape
    k = 24
    corners = np.concatenate([a[:k,:k].reshape(-1,3), a[:k,-k:].reshape(-1,3), a[-k:,:k].reshape(-1,3), a[-k:,-k:].reshape(-1,3)])
    bg = np.median(corners, axis=0)
    m = (a[...,0] + a[...,2]) / 2; mg = (bg[0] + bg[2]) / 2
    alpha = np.clip((a[...,1] - m) / (bg[1] - mg), 0, 1)
    v = np.where(alpha < 0.999, (m - alpha * mg) / np.maximum(1 - alpha, 1e-3), 255)
    v = np.clip(v, 0, 255)
    out = alpha * 255 + (1 - alpha) * v
    out = np.where(alpha > 0.85, 255, out)  # ほぼ背景の画素は真っ白にそろえる(JPEG のむら消し)
    img = Image.fromarray(np.repeat(out[..., None], 3, axis=2).round().astype(np.uint8))
    s = max(1080 / w, 1350 / h)
    img = img.resize((round(w * s), round(h * s)), Image.LANCZOS)
    l = (img.width - 1080) // 2; t = (img.height - 1350) // 2
    img = img.crop((l, t, l + 1080, t + 1350))
    img.save(dst)
    r = np.asarray(img).astype(int)
    green = int(((r[...,1] - np.maximum(r[...,0], r[...,2])) > 12).sum())
    print(dst, img.size, "背景の色", bg.round().astype(int).tolist(), "緑の残り画素", green)
for src, dst in zip(sys.argv[1::2], sys.argv[2::2]):
    finish(src, dst)
仕上げ/第61話_P1_1080x1350.png (1080, 1350) 背景の色 [8, 167, 75] 緑の残り画素 0
仕上げ/第61話_P2_1080x1350.png (1080, 1350) 背景の色 [8, 170, 73] 緑の残り画素 0
仕上げ/第61話_P3_1080x1350.png (1080, 1350) 背景の色 [9, 181, 55] 緑の残り画素 0
仕上げ/第61話_P4_1080x1350.png (1080, 1350) 背景の色 [13, 163, 77] 緑の残り画素 0

作業ログ ― 4コマのデータの置き場所を分けた

キャラの三面図37枚・縦横比の下地・指示・プロット・試作が、4コママンガ作成データ/ の直下に混ざっていました。放っておくと、どこに何があるか分からなくなるので、種類ごとに分けました。一番上のフォルダの名前は、過去の記事や台帳が書いているので変えていません。

フォルダ中身
キャラ/
三面図・並び図・ロボットの図(リファレンス台帳の37枚)
下地/縦横比の下地 canvas_
指示/
縦長版・横長版の指示、カメラワーク規定(縦長版)、リファレンス台帳、縦長の差分指示。古いものは 指示/控え/
道具/
gemini_gen_labeled.py、仕上げ_緑を白に_1080x1350.py
縦長/第NN話/
その話の TSV・プロンプト・生成/(API が返した絵と記録)・仕上げ/(1080×1350)
横長_完成版_2026-10-19_20/
横長の完成版
プロット/まとめて作ったプロット
set -e; cd "$HOME/mnt/PixelstoMotors/ハコブくん/4コママンガ作成データ"
mkdir -p キャラ 下地 指示 道具 縦長 プロット
mv 縦長試作_20261030 縦長/第61話
mkdir -p 縦長/第13話; mv 縦長/第61話/第13話_縦長.tsv 縦長/第13話/
mv 縦長/第61話/縦長の差分指示.md 指示/
mv 縦長/第61話/仕上げ/仕上げ_緑を白に_1080x1350.py 道具/
mv 縦長/第61話/API試し/gemini_gen_labeled.py 道具/
mv 縦長/第61話/API試し 縦長/第61話/生成
mv 完成版_2026-10-19_20 横長_完成版_2026-10-19_20
mv canvas_*.png 下地/
mv プロット_124-133話_法律と運用.* プロット/
mv マンガのプロジェクト指示_縦長版_20261030.md マンガのプロジェクト指示_横長版_20261030.md 指示/
mv プロジェクトのファイル_md_20261030 指示/控え
mv 指示/控え/カメラワーク規定_縦長版.md 指示/控え/ハコブくん_リファレンス台帳.md 指示/
for f in *.png *.jpeg; do mv "$f" キャラ/; done
mv "$HOME/mnt/PixelstoMotors/依頼箱/確認待ち/20261030_33_Geminiの画像APIで第61話P1を1枚試す.md" "$HOME/mnt/PixelstoMotors/依頼箱/済み/"
  • ねらい:キャラの図・決まり・話ごとのデータを、探さずに見つけられるようにする
  • 変わったこと:直下のファイル48個を、7つのフォルダに分けた。新しい話は 縦長/第NN話/ に作る
  • 見込める効果:プロンプトを作るときに読むファイル(指示/)と添える画像(キャラ/・下地/)の場所が決まり、スクリプトに書く道のりも短くなる

作業ログ ― 第61話の縦長ショートを作った

ショートの流れ:冒頭2.4秒のあと、4枚ともめくる

YouTube ショートは、マンガのショートの型(冒頭の動くハコブくん → コマ → 叩きつけ → まとめ)で組みました。第13話(LOG408)は冒頭が1・2コマ目の中身を受け持ち、コマは2枚だけ使いました。第61話は冒頭を2.4秒に縮め、縦長の4枚をすべてめくります。

場面長さ中身
冒頭2.4秒
廊下で、袋を引っかけたままのハコブくんが「お届け中です」とご機嫌に進む
P12.8秒
高杉さん「ちょっと、引っかかってる!」。「ズルズル…」の擬音
P22.8秒
高杉さん「これ、私が押していいやつ…?」。オレンジの「?」
P31.7秒
「バチン!」の叩きつけ・ドーン・画面が揺れる
P48.8秒
高杉さん・ハコブくん・大城さんの3つのセリフ
まとめ4.4秒
ボタンに手が届く/でも、/押していいかは/別の話
第61話のショートの5つの場面。冒頭の袋を引きずるハコブくん、1コマ目と「ズルズル…」、3コマ目の「バチン!」、4コマ目のハコブくんの水色の字幕、まとめの画面

冒頭の動き「引きずり」を足した

冒頭は、コードでハコブくんを動かす仕組み(hakobu_motion.py)に、新しい動き「引きずり」を足しました。袋は絵ではなく、その場でコードで描いています(布らしいでこぼこの輪郭・床の影・結び目・スピード線)。

hakobu_motion.py に足した Drag61

class Drag61:           # 第61話: 袋を引っかけたまま、気づかずにご機嫌で進む(「お届け中です」)
    dur = 2.6
    empty = True
    sub = 3
    X0, X1 = 0.0, -240.0          # 右から入って、止まらずに左へ進み続ける(袋が画面に入る位置)
    H0, H1 = 0.55, 1.6             # 「お届け中です」を言う間(声に合わせて上書き)
    def _x(self, t): return self.X0 + (self.X1 - self.X0) * (t / self.dur)
    def motion(self, t):
        return dict(dx=self._x(t), dy=-5 * abs(np.sin(2 * np.pi * 3.2 * t)), rot=-1.5 + 1.0 * np.sin(2 * np.pi * 1.6 * t), sx=1.02, sy=0.99)
    def face(self, t):
        if self.H0 <= t < self.H1:
            return ("open", "open"), None, ("mid" if int(t * 9) % 2 else "small")
        return ("open", "open"), None, "closed"
    def ring(self, t): return 0.75
    def blur(self, t): return True
    def dim(self, t): return 1.0
    def fx(self, t, frame, mo):
        x = RIGHT + mo.get("dx", 0)                  # 体の右下の角(進む向きの後ろ)
        yb = 905 + mo.get("dy", 0) * 0.3
        img = np.ascontiguousarray(frame.astype(np.uint8))
        wob = 4 * np.sin(2 * np.pi * 5.5 * t)
        cx, cy = int(x + 130), int(yb - 74 + wob * 0.4)    # 袋の真ん中
        # 引きずった跡のスピード線(袋の後ろ=右へ)
        for k, (oy, ln) in enumerate([(-8, 70), (8, 95), (22, 60), (34, 80)]):
            ph = (t * 600 + k * 37) % 40
            x0 = int(cx + 85 + ph); cv2.line(img, (x0, cy + 30 + oy), (x0 + ln, cy + 30 + oy), (40, 40, 40), 3, cv2.LINE_AA)
        # 結び目からロボットの下のふちへ続くひも
        kx, ky = int(x + 30), int(yb - 58)
        cv2.line(img, (int(x - 6), int(yb - 6)), (kx, ky), (20, 20, 20), 5, cv2.LINE_AA)
        # 袋(白い布・ふくらんで横倒し)
        axes = (94, 60)
        cv2.ellipse(img, (cx + 6, cy + 56), (100, 14), 0, 0, 360, (150, 140, 130), -1, cv2.LINE_AA)      # 床の影
        th = np.linspace(0, 2 * np.pi, 48, endpoint=False)          # 布らしいでこぼこの輪郭
        rr = 1 + 0.06 * np.sin(3 * th + 1.3) + 0.04 * np.sin(5 * th + t * 6)
        body = np.stack([cx + axes[0] * rr * np.cos(th), cy + axes[1] * rr * np.sin(th)], 1).astype(np.int32)
        cv2.fillPoly(img, [body], (246, 246, 242), cv2.LINE_AA)
        cv2.ellipse(img, (cx + 6, cy + 14), (int(axes[0] * 0.85), int(axes[1] * 0.6)), wob, 0, 180, (214, 212, 206), -1, cv2.LINE_AA)   # 下の陰
        cv2.polylines(img, [body], True, (15, 15, 15), 4, cv2.LINE_AA)
        for a0, a1, r in [(200, 250, 0.55), (280, 330, 0.6), (120, 160, 0.5)]:     # 布のしわ
            cv2.ellipse(img, (cx + 8, cy + 4), (int(axes[0] * r), int(axes[1] * r)), wob, a0, a1, (60, 60, 60), 3, cv2.LINE_AA)
        # 口を縛った結び目(左=ロボットの側)
        pts = np.array([[cx - 90, cy - 16], [kx + 4, ky - 18], [kx - 6, ky + 2], [kx + 6, ky + 20], [cx - 90, cy + 18]], np.int32)
        cv2.fillPoly(img, [pts], (246, 246, 242), cv2.LINE_AA); cv2.polylines(img, [pts], True, (15, 15, 15), 4, cv2.LINE_AA)
        cv2.circle(img, (int(cx - 86), cy), 10, (15, 15, 15), -1, cv2.LINE_AA)
        return img.astype(np.float32)

PATTERNS.update({"引きずり": Drag61})

声を作る .sh と、組み立ての台本

第61話の声.sh

#!/bin/bash
# 第61話(縦長4コマを4枚ともめくるショート・2026-10-30)の声を作る。先に read -s ELEVENLABS_API_KEY && export ELEVENLABS_API_KEY
# できている声はとばすので、何度動かしてもよい
cd "$(dirname "$0")"
B=../_制作ベース
HK="gWJpc8whMBqnF7Y0kc9f"
id() { python3 -c "import json;print(json.load(open('../$B/声の一覧.json')).get('$1',''))"; }
mk() { # mk 人 本数 文
  local who="$1" n="$2" txt="$3" v
  if [ "$who" = "ハコブ" ]; then v=$HK; else v="$(id "$who")"; fi
  [ -f "voice_eleven/$who/試し_$(printf %02d $n).mp3" ] && return
  python3 ../$B/tts_eleven.py --voice "$v" --name "$who" --try "$txt" || echo "  ! $who で止まりました"
}
echo "== 第61話_押していい"; cd "第61話_押していい"
[ -f narration.py ] || printf "# ショート専用。声は --try で作る(_ショート専用/第61話の声.sh)\nSEGS = []\n" > narration.py
# --try は1回ごとに 試し_01 から番号を振り直すので、1人ぶんを「|」でつないで1回で作る(本数=その人の文の数)
mk "高杉" 3 "ちょっと、引っかかってる!|これ、私が押していいやつ…?|押していいって、どこにも書いてないのよ"
mk "ハコブ" 2 "お届け中です。|非常停止、誰でも押せます!以後、明記します。"
mk "大城" 1 "うんね。"
echo "できあがり: $(ls voice_eleven/*/試し_*.mp3 2>/dev/null | wc -l | tr -d ' ') 本(6本そろえば完成)"

spec_ep61.py(第61話のショートの台本)

import os, sys
sys.path.insert(0, os.environ.get("W24", os.path.expanduser("~/w")) + "/specs"); from common24 import *
import common24; common24.BGMAP[61] = "第86話.png"          # 病棟の廊下(第86話の背景を借りる)
HERE = os.path.dirname(os.path.abspath(__file__)); d = lambda l, e=2.0: vlen(HERE, l["v"], e)
NAME, EP = "第61話_押していい", 61
HEAD = ["非常停止ボタン、", "押していいのは誰?"]
COMIC = C + "ep61.png"                      # 縦長のコマ(1080×1350)を上から P1〜P4 の順に積んだ画像
P1, P2, P3, P4 = [(0, 1350 * i, 1080, 1350 * (i + 1)) for i in range(4)]

# 冒頭:袋を引っかけたまま、ご機嫌で進むハコブくん「お届け中です」
l0 = hk(1, "お届け中デス", at=0.35)
H0 = l0["at"]; H1 = H0 + d(l0, 1.2)
DUR = max(2.4, H1 + 0.7)
MOTION = mot("引きずり", EP, H0=H0, H1=H1, dur=DUR)

SCENES = [
  dict(kind="op", video=anim(EP, "引きずり"), dur=DUR, lines=[l0],
       se=[dict(at=0.0, f="se/候補_A_ズザーッ.m4a", db=-16)]),
  dict(kind="panel", crop=P1, fit=False, min=2.0, lines=[ns("高杉", 1, "ちょっと、|【引っかかってる】!")],
       words=[W_("ズルズル…", 0.2, 300, 845, 84, "ink", -8)]),
  dict(kind="panel", crop=P2, fit=False, min=2.4, lines=[ns("高杉", 2, "これ、【私が】|【押していい】やつ…?")],
       words=[W_("?", 1.3, 990, 470, 180, "orange", 12)]),
  dict(kind="panel", crop=P3, fit=False, min=1.7,
       slam=dict(text="バチン!", at=0.35, y=190, size=170, color="orange"),
       se=[dict(at=0.35, f="se/se_don.mp3", db=-6)]),
  dict(kind="panel", crop=P4, fit=False, min=4.0,
       lines=[ns("高杉", 3, "【押していい】って、|どこにも【書いてない】のよ"),
              hk(2, "非常停止、|【誰デモ押セマス】!||以後、【明記シマス】"),
              ns("大城", 1, "【うんね】", gap=0.1)]),
  end(("ボタンに手が届く", 74, "ink", 0.0), ("でも、", 74, "ink", 0.25),
      ("押していいかは", 120, "orange", 0.9), ("別の話", 120, "orange", 0.9),
      ("(「誰でも押せる」と書いておく)", 34, "gray", 1.8)),
]
できあがり: out_第61話_押していい.mp4 22.9 秒

字幕の色は「ハコブくんは水色と白、ほかの人は白とオレンジ」にそろえた

字幕の強める語をオレンジにする工夫を入れたあと、色の決まりを1つ足しました。

誰の字幕地の字強める語
ハコブくん水色白
ほかの人白オレンジ

途中で、色を解説動画と同じ暗いオレンジ・水色1色ずつにそろえ、黒いふちを細くしてみましたが、見た目がいまいちで、前の明るい色に戻しました。明るい色は、黒いふちの上で目立たせるためのものでした。残したのは、誰の字幕かで色を分ける決まりだけです。ショートを組む仕組み(マンガショート_mkshort.py)の変更です。

マンガショート_mkshort.py の変更(差分)

--- "kit/_\345\210\266\344\275\234\343\203\231\343\203\274\343\202\271/\343\203\236\343\203\263\343\202\254\343\202\267\343\203\247\343\203\274\343\203\210_mkshort.py"	2026-10-30 11:17:54.000000000 +0900
+++ w/mkshort.py	2026-10-30 15:29:41.220011576 +0900
@@ -17,6 +17,7 @@
 
 W, H, FPS, SR = 1080, 1920, 30, 48000
 PAPER = (245, 245, 240); INK = (13, 13, 13); GRAY = (136, 136, 136); ORANGE = (196, 71, 31); BLUE = (120, 200, 255)
+SUB_OR = (255, 140, 90)   # 字幕の強める語のオレンジ(黒いふちの上で目立つ明るさ)
 COL = {"ink": INK, "orange": ORANGE, "gray": GRAY}
 FONT = "/usr/share/fonts/opentype/noto/NotoSansCJK-Black.ttc"
 F = lambda size: ImageFont.truetype(FONT, size, index=0)
@@ -224,15 +225,17 @@
 import functools
 @functools.lru_cache(maxsize=None)
 def subtitle(txt, who, small):
+    # 字幕の色: ハコブくんは水色の字で強める語は白、ほかの人は白い字で強める語はオレンジ(2026-10-30 エイイチの指示。色を増やさない)
+    hl = (255, 255, 255) if who == "hk" else SUB_OR
     if small:
-        return text_img(txt, 30, fill=BLUE if who == "hk" else (255, 255, 255), stroke=5, stroke_fill=INK)
+        return text_img(txt, 30, fill=BLUE if who == "hk" else (255, 255, 255), stroke=5, stroke_fill=INK, hl=hl)
     rows = txt.split("|")                 # 「|」で2行に分ける(1行は13字まで)
     plain = lambda r: len(r.replace("【", "").replace("】", ""))
     if len(rows) == 1 and plain(txt) > 13:     # 長いときは真ん中に近い「、」「。」で自動で折る
         cands = [i + 1 for i, ch in enumerate(txt[:-1]) if ch in "、。!?"]
         if cands:
             k = min(cands, key=lambda i: abs(plain(txt[:i]) - plain(txt) / 2)); rows = [txt[:k], txt[k:]]
-    ims = [text_img(r, 66, fill=BLUE if who == "hk" else (255, 255, 255), stroke=11, stroke_fill=INK, hl=(255, 140, 90)) for r in rows]
+    ims = [text_img(r, 66, fill=BLUE if who == "hk" else (255, 255, 255), stroke=11, stroke_fill=INK, hl=hl) for r in rows]
     if len(ims) == 1: return ims[0]
     out = Image.new("RGBA", (max(i.width for i in ims), sum(i.height - 14 for i in ims) + 14), (0, 0, 0, 0))
     y = 0

作業ログ ― ElevenLabs の声と LinkedIn の投稿を自動にした

ElevenLabs は何か・鍵をファイルに置いた

ElevenLabs は、文章を人の声で読み上げるサービスです。看護師さんたちとハコブくんの声は、ElevenLabs で作った声(Voice ID で呼び分ける)で読み上げています。これまでは、声を作るたびに鍵をターミナルに入れてもらっていました。Gemini と同じく鍵を KIOXIA の elevenlabs_api_key.txt に置き、読み上げのスクリプトが、環境変数がなければこのファイルを読むようにしました。

ここでも、鍵がファイル名の後ろに付いたファイルができました。鍵の文字を表示せずに、中身を正しい名前のファイルへ写しています。

cd "$HOME/mnt/KIOXIA" && python3 - <<'EOF'
import os
base="elevenlabs_api_key.txt"
c=[n for n in os.listdir('.') if n.startswith(base)]
print("見つかった数:", len(c), "正しい名前のファイル:", base in c)
odd=[n for n in c if n!=base]
for n in odd:
    suf=n[len(base):]; body=open(n,encoding='utf-8',errors='replace').read().strip()
    print("名前の後ろの長さ:", len(suf), "中身の長さ:", len(body), "中身と名前の後ろが同じ:", body==suf, "中身が英数字だけ:", body.replace('_','').replace('-','').isalnum())
EOF
見つかった数: 1 正しい名前のファイル: False
名前の後ろの長さ: 51 中身の長さ: 51 中身と名前の後ろが同じ: True 中身が英数字だけ: True

鍵で読み上げを1回だけ試すと、声の一覧や使った量は「権限がない」で断られ、読み上げはできました。読み上げだけの権限の鍵です。

voices?page_size=1 ng 401 {"detail":{"type":"authentication_error","code":"unauthorized","message":"The API key you used is missing the permission voices_read to execute this operation.","status":"missing_permissions","request
models ng 401 {"detail":{"type":"authentication_error","code":"unauthorized","message":"The API key you used is missing the permission models_read to execute this operation.","status":"missing_permissions","request
ok 11327 バイト

tts_eleven.py と sound_eleven.py に足した行です。

key = os.environ.get('ELEVENLABS_API_KEY')
if not key:   # 2026-10-30 エイイチが鍵を KIOXIA に置いた(Claude が会話から声を作れるように)
    for _kp in ('/Volumes/KIOXIA/elevenlabs_api_key.txt', os.path.expanduser('~/mnt/KIOXIA/elevenlabs_api_key.txt')):
        if os.path.exists(_kp): key = open(_kp).read().strip(); break
  • ねらい:ショートの声を作るたびに、鍵を入れてもらう手間をなくす
  • 変わったこと:鍵は KIOXIA のファイル。声は会話の中から作る。新しい声をデザインするときだけ、今までどおりターミナルで作業する
  • 見込める効果:1話あたり1回のターミナルの作業がなくなる(第61話では声の6本を作るのに1回あった)

LinkedIn の本編の動画を、YouTube のリンクで自動で出すようにした

解説動画(本編)は、YouTube の公開予約に合わせて X に自動で出し、LinkedIn には動画を直接アップロードして手で出していました。この手作業が面倒なので、LinkedIn も YouTube のリンクのカード(YouTube のサムネイル付き)で自動で出すようにしました。決めたのは次の2つです。

決めたこと中身
対象
本編だけ。ショートは LinkedIn に出さない
時刻
YouTube の公開と同じ時刻。研究の記録の 17:00 の枠とは別

ただ、LinkedIn では、外のサイトへのリンクの投稿は、動画を直接上げるより広がりにくいと言われています(公式の発表ではありません)。そこで、ここぞという回は、これまでどおり LinkedIn に動画を直接上げることにしました。その回は json に "linkedin_manual": true を書くと、自動の予約から外れます。

公開前の本編14本の json に、作業フォルダの投稿文(投稿文_X_LinkedIn.txt)から日本語の LinkedIn の本文とコメントを入れました。

cd /home/claude/r2r2r-content && python3 - <<'EOF'
import json, glob, os, re
from datetime import datetime, timezone
now=datetime.now(timezone.utc)
CJK=re.compile(r'[぀-ヿ一-鿿]')
def parse(path):
    L=open(path,encoding='utf-8').read().split("\n")
    heads=[i for i,l in enumerate(L) if l.startswith("■")]
    def sec(pred):
        for k,i in enumerate(heads):
            if pred(L[i]):
                j=heads[k+1] if k+1<len(heads) else len(L)
                return [l for l in L[i+1:j] if not re.fullmatch(r"[─━]+",l.strip())]
        return None
    body=sec(lambda h: h.startswith("■ LinkedIn") and "コメント" not in h)
    com=sec(lambda h: h.startswith("■ LinkedIn") and "コメント" in h)
    b="\n".join(body)
    if "――――" in b: b=b.split("――――",1)[1]
    b=b.strip().replace("【URL】","{url}")
    c=""
    if com:
        paras=[p.strip() for p in "\n".join(com).split("\n\n") if p.strip()]
        c="\n\n".join(p for p in paras if CJK.search(p)).replace("【URL】","{url}")
    return b,c
files={os.path.basename(f)[:-4]:f for f in glob.glob('/home/claude/li/*.txt')}
for p in sorted(glob.glob('sns/VIDEO_*_sns_posts.json')):
    d=json.load(open(p,encoding='utf-8')); m=d['meta']
    pa=m.get('publish_at')
    if pa and datetime.fromisoformat(pa)<now: continue
    key=next((k for k in files if m['title'].startswith(k)),None)
    if not key: print("文なし",p); continue
    b,c=parse(files[key])
    d['posts']['linkedin_template']=b
    if c: d['posts']['linkedin_comment_template']=c
    d['note']=re.sub(r"LinkedIn は動画を直接アップロードする決まりなので入れない","LinkedIn も YouTube のリンクのカードで自動で出す(2026-10-30 から。linkedin_template の {url} が動画の URL に替わる)",d.get('note',''))
    json.dump(d,open(p,'w',encoding='utf-8'),ensure_ascii=False,indent=2); open(p,'a').write("\n")
    print(os.path.basename(p), pa, len(b), "字 / コメント", len(c), "字", "| 本文に{url}" if "{url}" in b else "")
EOF
VIDEO_ai-reviews-ai_sns_posts.json 2026-11-01T19:30:00+09:00 663 字 / コメント 120 字 
VIDEO_ai-yesterday_sns_posts.json 2026-11-04T19:30:00+09:00 700 字 / コメント 125 字 
VIDEO_ghost-cyborg-body_sns_posts.json 2026-11-12T19:15:00+09:00 767 字 / コメント 216 字 
VIDEO_ghost-remote-body_sns_posts.json 2026-11-10T19:00:00+09:00 857 字 / コメント 168 字 
VIDEO_haro_sns_posts.json 2026-10-30T19:30:00+09:00 702 字 / コメント 166 字 
VIDEO_kddi-elyza-robotics_sns_posts.json None 863 字 / コメント 161 字 
VIDEO_patlabor_sns_posts.json 2026-11-11T19:30:00+09:00 733 字 / コメント 145 字 
VIDEO_serving-robots-how-many_sns_posts.json 2026-11-09T19:30:00+09:00 1020 字 / コメント 155 字 
VIDEO_serving-robots-one-less_sns_posts.json 2026-11-16T19:30:00+09:00 1117 字 / コメント 198 字 
VIDEO_softbank-pudu-no1_sns_posts.json None 783 字 / コメント 110 字 
VIDEO_solo-ai-team_sns_posts.json 2026-10-31T19:30:00+09:00 638 字 / コメント 99 字 
VIDEO_tesla-optimus-ramp_sns_posts.json 2026-11-02T19:30:00+09:00 668 字 / コメント 85 字 
VIDEO_votoms-at_sns_posts.json 2026-11-06T19:15:00+09:00 796 字 / コメント 140 字 
VIDEO_world-record-and-laundry_sns_posts.json 2026-11-23T19:30:00+09:00 614 字 / コメント 98 字

同期のスクリプト(sync_youtube_shorts.py)・投稿のスクリプト(sns_autopost.py)・カレンダーのスクリプト(calendar_plan.py)の変更です。

LinkedIn を YouTube のリンクにした変更(差分)

09_LinkedIn_YouTube.txt5,895字・106行・diff
diff --git a/tools/calendar_plan.py b/tools/calendar_plan.py
index 66b6e2a..d79753a 100644
--- a/tools/calendar_plan.py
+++ b/tools/calendar_plan.py
@@ -17,7 +17,7 @@
 予定の見分け方(key)。予定の説明の最後の行に「p2m-key: {key}」を入れる
   x:{id} / linkedin:{id}            予定表の1行(id は schedule.json の id)
   yt:{youtube_id}                   YouTube の公開
別のタブで開く保存

直接上げる回を自動の予約から外す変更(差分)

diff --git a/tools/sync_youtube_shorts.py b/tools/sync_youtube_shorts.py
index 79d98aa..fa59e92 100644
--- a/tools/sync_youtube_shorts.py
+++ b/tools/sync_youtube_shorts.py
@@ -103,7 +103,13 @@ def main():
             changes.append(f"時刻を直した {sid} {entry['post_at']} → {post_at}")
             entry["post_at"] = post_at
         # 本編の LinkedIn(2026-10-30 から): YouTube の公開と同じ時刻。同じ時刻の LinkedIn の予約があれば30分ずつ後ろへ
-        if is_main and posts.get("linkedin_template"):
+        if is_main and meta.get("linkedin_manual"):
+            # エイイチが LinkedIn に動画を直接上げる回(2026-10-30「ポイントポイントで直接あげる」)。自動の予約はまだ出ていなければ外す
+            lid = sid[:-len("-x")] + "-linkedin"
+            if lid not in done_ids and any(e["id"] == lid for e in sched["posts"]):
+                sched["posts"] = [e for e in sched["posts"] if e["id"] != lid]
+                changes.append(f"外した {lid}(LinkedIn は直接上げる回)")
+        elif is_main and posts.get("linkedin_template"):
             lid = sid[:-len("-x")] + "-linkedin"
             lentry = next((e for e in sched["posts"] if e["id"] == lid), None)
             if not (lentry and lid in done_ids) and (lentry or when >= now):

同期を動かした結果です。

足した VIDEO_ai-reviews-ai-linkedin 2026-11-01T19:30:00+09:00 AIの間違いは、誰が見つけるのか ― 別のAIに疑わせたら、
足した VIDEO_ai-yesterday-linkedin 2026-11-04T19:30:00+09:00 AIに、昨日の続きをさせるには ― 新しい会話のAIのために
足した VIDEO_ghost-cyborg-body-linkedin 2026-11-12T19:15:00+09:00 攻殻機動隊の義体は、いまの技術でどこまで作れるのか ― 体は
足した VIDEO_ghost-remote-body-linkedin 2026-11-10T19:00:00+09:00 攻殻機動隊を思わせる遠隔ボディは、もう作れるのか ― 中身に
足した VIDEO_haro-linkedin 2026-10-30T19:30:00+09:00 ハロは、もう作れるのか ― 話す・転がる・段差を上る。1台に
足した VIDEO_kddi-elyza-robotics-x 2026-11-06T20:30:00+09:00 KDDIとソフトバンクはフィジカルAIでもライバルか 300
足した VIDEO_kddi-elyza-robotics-linkedin 2026-11-06T19:30:00+09:00 KDDIとソフトバンクはフィジカルAIでもライバルか 300
足した VIDEO_patlabor-linkedin 2026-11-11T19:30:00+09:00 パトレイバーは、もう作れるのか ― 人型の重機は線路で働いて
足した VIDEO_serving-robots-how-many-linkedin 2026-11-09T19:30:00+09:00 配膳ロボット1台だと、料理が遅くなる? ファミレスで0〜6台
足した VIDEO_serving-robots-one-less-linkedin 2026-11-16T19:30:00+09:00 配膳ロボットは「0.5人分」? ファミレスで人件費とレンタル
足した VIDEO_softbank-pudu-no1-x 2026-11-05T20:30:00+09:00 猫の配膳ロボット、売上世界一はソフトバンクかPuduか
足した VIDEO_softbank-pudu-no1-linkedin 2026-11-05T19:30:00+09:00 猫の配膳ロボット、売上世界一はソフトバンクかPuduか
足した VIDEO_solo-ai-team-linkedin 2026-10-31T19:30:00+09:00 ひとりとAIで、研究と発信を続けるには ― AIに任せる仕事
足した VIDEO_tesla-optimus-ramp-linkedin 2026-11-02T19:30:00+09:00 テスラは人型ロボットをなぜすぐ作れないのか。決算説明会から3
足した VIDEO_world-record-and-laundry-linkedin 2026-11-23T19:30:00+09:00 走るのは人間超え、たたむのはまだ。人型ロボットの謎を3つの手
  • ねらい:LinkedIn に動画を手で上げる作業をなくす
  • 変わったこと:本編は、YouTube で公開予約するだけで、X と LinkedIn の両方に、公開と同じ時刻で出る。ここぞという回だけ、手で直接上げる
  • 見込める効果:本編1本ごとの LinkedIn の手作業がなくなる(予約済みの13本と、まだ予約していない1本の分)。リンクの投稿で反応が落ちるかは、LinkedIn の分析の書き出しで、直接上げていた回と比べて確かめる

次は、話を選ぶだけで4コマからショートまで

第61話で、4コマの絵・仕上げ・声・ショート・X の予約まで、どれも会話の中から動かせるようになりました。人の手が残っているのは、話を選ぶこと、できた絵とショートを見て直しを言うこと、YouTube に上げて公開予約することです。次の話は、この会話のプロジェクトで TSV(話のプロット)を受け取り、プロンプト作りから始めます。どれだけ人の手が要ったかを、次の話で数えます。