4コママンガ『ハコブくん』のコマに動きを足した ― 次は描く前に構図を決める
この記事の素材(コード・ログ・データ 7ファイル・まとめてダウンロードあり)arrow_downwardAIで読む ChatGPT・Claude などで、この記事に質問できます expand_more
Gemini の画像生成 API で描く4コママンガ『ハコブくん』は、コマごとに寄り・引き・カメラの高さ・向きを変えるよう指示すると、同じ大きさの人とロボットが4コマ並ぶ単調さが消えた。 この形で第134〜137話の4話を描き、pixelstomotors.com の日本語版と英語版、YouTube ショート4本、SNS の予約までを1日で出しました。
この作業で変わったことは3つです。
- 読む人にとって:1話の中に、顔の大写し、床すれすれから見上げる絵、人の肩越しに奥を見る絵が入り、めくるたびに見え方が変わる
- 作る側にとって:これまで描くたびに形が変わっていた、後ろ向きのハコブくんと、何コマにも出る配膳車を、参照の絵を添えて固定した。次の話から、同じ直しを繰り返さずに済む見込み
- 費用:4話で Gemini に描かせた絵は156枚、1枚約10円で約1,500円。第134話は組み直しで41枚になり、ふだんの1話(20枚前後)の約2倍かかった。作業時間は、前を測っていないので比べられない
『ハコブくん』は、病院で薬や物を運ぶロボット・ハコブくんと、看護師さんたちの4コママンガです。ロボットが現場に入ったときに起きるすれ違いを、実際の病院の調査をもとに毎話1つ描いています。前の記録(LOG413)で、1コマを縦長(縦横比 4:5)の1枚ずつで描き、横にめくって読む形にしました。絵は Google の Gemini の画像生成 API(文章と参考の絵を渡すと絵を返す仕組み。使っているのは gemini-3.1-flash-image)で描いています(LOG410)。
困っていたのは、絵がきれいでも、4コマがどれも同じに見えることでした。人もロボットも中くらいの大きさで、目の高さから横に並べて撮ったような絵が4つ続く。マンガで言うカメラワーク(コマごとに、どこから・どれくらい寄って・どの高さから見せるか)がないのです。AI にコマの中身だけを書いて渡すと、いちばん無難な構図で返ってきます。

そこで、コマごとにショットの大きさ(顔だけか、全身か)、カメラの位置、カメラの高さを、数字で書いて渡すようにしました。高さは、人の目の高さ120〜150cm だけでなく、ハコブくんの目の高さ90cm、床すれすれの30〜60cm も使います。
やったことの全体は次のとおりです。
| やったこと | なぜ | 結果 |
|---|---|---|
第134話を試しに描き、カメラワークを組み直した | 4コマが同じ大きさ・同じ高さで単調だった | 寄り・引き・高さ・向きをコマごとに変え、効果線を入れた |
後ろ向きのハコブくんの参照の絵と、背中の形の説明を足した | 背中が描くたびに違う形だった | 背中が三面図どおりになった |
配膳車の三面図を作り、毎コマ添えた(第135話) | 配膳車の形がコマごとに変わった | 4コマとも同じ形の配膳車になった |
床のレーンの線をプログラムで引き直した(第136話) | 線の色・本数・幅がコマごとにずれた | 4本の黒い線と広めの間で、2コマ目と3コマ目がそろった |
3コマ目のセリフを変えた(第136話) | 話の流れと絵が合っていなかった | 正面で鉢合わせて、だから、どっちに来るのよ! にした |
4話をサイトに載せ、英語版と表紙を作った | 読める場所を増やす | 日本語4話・英語4話を公開した |
YouTube ショートを4本作った | マンガを動画でも届ける | 冒頭の動きを話ごとに作り、音が出ない不具合を直した |
| SNS を予約した | 新しい話を知らせる | 日本語は 11/28〜12/1、英語は 1/27〜1/29 |
チェック項目に、コマの動きの節を足した | 同じ指摘を繰り返さない | 4コマを並べて見る5つの確かめを足した |
ここから下は作業ログです。使ったコード・決まり・出力は、あとで同じ手順を追えるように全文を載せています。効果の欄は、数字があるものは測った値、ないものは見込みです。
作業ログ ― 第134話を描き、4コマのカメラワークを組み直した
この記事のコード・ログ・データは、1ファイルずつ下に置いています。全文はリンク先にあります。
ファイルの一覧(7本)
01_sec01.py.txt ― カメラワークの指示:コマごとにショット・位置・高さを書く / 第134話の spec.py・9,254字
02_sec02.txt ― vbuild.py の変更点(変える前の控えとの差分)・4,623字
03_vbuild.py.txt ― vbuild.py(変更後の全文)・49,107字
04_spec.py.txt ― 第135話の spec.py・7,478字
05_sec05.py.txt ― 1コマ目:構図はそのまま、大城さんだけを横へ動かす / lanes136.py(1コマ目の線を後から引く形。試したが使わなかった)・3,133字
06_2_spec.py.txt ― 3コマ目:セリフを変え、2コマ目と同じレーンの線を重ねる / 第136話の spec.py・8,910字
07_spec.py.txt ― 第137話の spec.py・6,283字
プロットと、1回目の第134話
話のもとは、Google スプレッドシートの『ハコブくん』のプロットの表です。第134〜183話の50話が、未着手のまま入っていました。まず第134話だけを試しに描きました。話は、細い廊下で、急ぎの点滴を持った高杉さんの前を、ハコブくんがゆっくり進み、抜こうとするとハコブくんも同じほうへ寄ってしまう、というもの。もとにしたのは、オールボー大学の病院のロボットの調査(ゆっくり動くロボットを追い越す余地がなく、職員の仕事が遅れる)です。
1回目の4コマは、上の画像の左のとおりです。絵の間違いはほとんどありませんでしたが、spec のカメラの指示は、4コマとも全身が入る引き(3〜4m 離れて、人とロボットを同じ距離に並べる)で、高さは 120・150・120・120cm でした。どのコマも、同じ大きさの人とロボットが横に並ぶ絵になります。
カメラワークの指示:コマごとにショット・位置・高さを書く
描く指示は、話ごとの spec.py に書きます。spec.py は、コマごとに出てくる人・ロボット・セリフ・構図を並べた Python のファイルで、vbuild.py(コマの指示文を組み立てて Gemini に渡す道具)がこれを読みます(LOG410)。今回、各コマに camera の項目を足し、次の4つを必ず書くようにしました。
| 項目 | 書く中身 | 第134話の1コマ目の例 |
|---|---|---|
| SHOT SIZE | 何がどれくらいの大きさで写るか | ハコブくんの上半身を手前に大きく、高杉さんは後ろに腰から上 |
| CAMERA POSITION | カメラがどこにあるか | ハコブくんの左前、約1.2m |
| CAMERA HEIGHT | カメラの高さと向き | 60cm、少し見上げる |
| FRAME CROP | どこで切るか | ハコブくんの車輪は下の端で切る |
第134話の4コマは、次のように組み直しました。
| コマ | ショット | カメラの高さ | 効果線 |
|---|---|---|---|
| 1 | ハコブくんを手前に大きく、後ろで詰まる高杉さん | 60cm(低い所から見上げる) | ゆっくり進む小さな波線 |
| 2 | 高杉さんの肩越し(肩なめ:手前の人の肩や頭を大きく入れ、その奥を見せる構図)に、後ろ向きのハコブくん | 150cm(高杉さんの目の高さ) | 横へ動くスピード線 |
| 3 | ハコブくんの頭を手前に、見上げた高杉さんを大きく | 90cm(ハコブくんの目の高さ) | 集中線 |
| 4 | 手前を通り抜ける高杉さん、奥で脇へ寄ったハコブくん | 140cm | なし |
- ねらい:隣り合うコマで、大きさと高さを必ず変え、めくる理由を作る
- 変わったこと:4コマのカメラの高さが 120・150・120・120cm から 60・150・90・140cm に、全コマ全身の引きから、手前に大きく・肩なめ・寄り・手前と奥の重ねになった。寄りのコマ(3コマ目)と肩なめ(2コマ目)が入った
- 見込める効果:スワイプで読むとき、1枚ずつの見え方が違うので最後までめくってもらえる見込み。めくられた割合は、サイトの記録で確かめる
4話それぞれの4コマで、カメラの高さとショットの大きさがどう違うかを、下の図で切り替えて見られます。
第134話の spec.py
EP, TITLE = 134, "オ先ニドウゾ"
# v2(2026-11-05):エイイチ「全体にコマに動きがなく単調」→ カメラワークを組み直した
# P1 低い位置・ハコブくんを手前に大きく/P2 肩なめ・後ろ姿・効果線/P3 ハコブくん目線から見上げる寄り・集中線/P4 手前を通り抜ける高杉さんと奥のハコブくん
IV = ("IV BAG IN HER HAND:", ["Takasugi carries ONE small clear IV drip bag, held by its top hanger in one hand. The bag is plain with NO text, NO label",
"she holds nothing else"])
PANELS = [
# 起:のろのろ進むハコブくんと、その後ろで詰まる高杉さん。ハコブくんを手前に大きく、低い所から
dict(chars=["高杉", "ハコブ"], mouth="NORMAL", no_yline=True, full=[],後ろ向きのハコブくんを、背中の参照の絵で固定した
組み直した第134話の2コマ目は、後ろ向きのハコブくんです。描かせると、頭の後ろに黒い格子が付いたり、背中に何もなかったりと、背中が毎回違う形になりました。これまでも後ろ向きは描くたびに違っていました。
原因は、AI に渡していた三面図(前・横・後ろを並べた設定の絵)では、後ろの絵が小さく、背中の部品が読み取りにくかったことです。そこで、後ろ姿だけを大きくした キャラ/hakobu_back.png を作り、コマの項目に back=True と書くと、この絵を添えて、背中の形を文字でも説明するようにしました。説明に書いたのは、上のドームの切り込み、3つのスイッチの箱、右に取っ手のある扉、下の光の帯と通気口です。
あわせて、ハコブくんの顔に寄るコマで、頭が別のテレビの箱のように描かれる問題(第135話の1回目)が出たので、頭の形の決まり(画面は体の上の丸い部分にはめ込まれていて、首も箱もない)も足しました。
- ねらい:何コマにも出る形を、文字ではなく絵で固定する
- 変わったこと:後ろ向きのコマでは、背中の参照の絵を必ず添えるようにした
- 見込める効果:後ろ向きのハコブくんを描き直す回数が減る見込み。次の話から、後ろ向きのコマの描き直しの枚数で確かめる
vbuild.py の変更点(変える前の控えとの差分)
--- 指示/控え/vbuild_20261105_before_back.py
+++ 道具/vbuild.py
@@ -98,6 +98,10 @@
- the round ring on each side face is a thin flat circular light strip printed flush on the flat white side panel, exactly as on the reference sheet's side view
- the area INSIDE the ring is the same flat white side panel as outside it — NO hole, NO opening, NO speaker cone, NO lens, NO dot or circle at its centre, NO concentric rings, NO depth or shading inside
+HAKOBUKUN HEAD SHAPE — EVEN IN CLOSE-UPS (2026-11-05):
+- the screen is set FLUSH into the front of the narrower rounded upper tower of the body, framed by a thin even border, exactly as on the front view of the sheet. It is NOT a separate box, NOT a TV set, NOT a thick protruding bezel, and there is NO neckvbuild.py(変更後の全文)
#!/usr/bin/env python3
"""縦長4コマ(1コマ=4:5の1枚)のプロンプトを、縦長版のプロジェクト指示の雛形どおりに組み、Gemini で描き、仕上げる(2026-10-30)
使い方(第NN話のフォルダに spec.py を置いてから):
python3 vbuild.py NN prompts # 第NN話_P{n}_プロンプト.txt と 小窓 を書く
python3 vbuild.py NN gen [1 2 3 4 W3] # 描く(省略で全部。並べて同時に頼む)
python3 vbuild.py NN finish # 各コマの最新の絵(または spec の PICK)を 仕上げ/ へ(緑→白・1080×1350・小窓を重ねる)
python3 vbuild.py NN sheet # 4枚を並べた 確認用.jpg(小さめ)高杉さんの背中の名札を、近くの網点を写して消した
第134話の2コマ目では、後ろから見た高杉さんの袖に、名札が付いてしまいました。名札は胸に付けるもので、背中にはありません。描き直すと構図が変わるので、仕上げの段階で消すことにしました。
vbuild.py の仕上げ(緑の背景を白にして 1080×1350 にする段階)に CLONE を足しました。CLONE={コマ:[((元の左,上,右,下),(写す先の左,上))]} と書くと、近くの網点(マンガの灰色の点の模様)を切り取って、消したい所に貼ります。白で塗るだけ(これまでの PATCH)だと、網点の服の上に白い四角が残るためです。第136話の2コマ目でも、大城さんの背中の名札を同じ方法で消しました。
第135話:配膳車の三面図と、頭の形の決まり
第135話は、細い廊下でハコブくんが急に止まって経路の計算を始め、後ろで配膳車を押す寺島さんが止められる話です。もとにしたのは ACM interactions の病院のロボットの記事(ロボットが廊下で止まって経路を計算し直し、温かい料理の台車がすれ違えず待たされた)です。
1回目は、配膳車の形がコマごとに変わり(扉の数、取っ手、色)、1コマ目のハコブくんの頭が別の箱のように描かれ、構成も単調でした。直したのは3つです。
- 配膳車の三面図
キャラ/配膳車.jpgを作り、spec のextra_refsで毎コマ添えた - 上の頭の形の決まりを
vbuild.pyに足した - カメラワークを組み直した:1コマ目は配膳車の取っ手と湯気を手前に大きく、2コマ目は寺島さんの顔の大写しで画面を10度傾け集中線、3コマ目は床から30cm で見上げる、4コマ目はハコブくん目線100cm で広いホール
第135話の spec.py
EP, TITLE = 135, "考えるのは広い所で"
# v2(2026-11-05):エイイチ「配膳台の形が変わる・1コマ目のハコブくんのデザインが違う・単調すぎる」
# 配膳車は キャラ/配膳車.jpg を毎コマ添える。カメラ:P1 配膳車の取っ手と湯気を手前に大きく/P2 寺島さんの顔の大写し・傾けた構図・集中線/P3 床すれすれの低い位置から見上げる/P4 ハコブくんの寄りと、遠くで親指を立てる槙原さん
CART_REF = [("the hospital meal cart — copy its shape exactly in every view (tall enclosed cabinet, two front doors, push handle bar on one short end, four casters)", "キャラ/配膳車.jpg")]
CART = ("MEAL CART", ["the hospital meal cart from the attached cart reference sheet — the SAME cart in every picture: a tall enclosed cabinet with two front doors, a push-handle bar on one short end, four casters. Copy it exactly. NO text, NO logo",
"it is about 1.4 m tall and as wide as Hakobukun"])
PANELS = [
# 起:手前に配膳車の取っ手と湯気(温かいご飯)、その先の廊下でハコブくんが止まる第136話:床のレーンの線と、大城さんの位置と、3コマ目のセリフ
第136話は、病院の地下通路の床に、歩く人と台車の通り道を分ける線が引いてあるのに、ハコブくんだけがその間を走ってくる話です。もとにしたのはデンマークの病院の地下通路での調査(線があるのにロボットがそこに留まらず、職員はロボットがどちら側か分からなかった)です。この話は、床の線が話の中心なので、線の描き方で4回直しました。
| 回 | 指摘 | 直し方 |
|---|---|---|
| 1 | 床のマークが黒だったり白だったりする。マークが並びすぎ | 線とマークは黒一色、1つのレーンにマーク1つ、と spec に書いて描き直した |
| 2 | 1コマ目は左の線が1本ない。2コマ目は2本のレーンの間を空ける。2コマ目のセリフは大城さん | 1コマ目は線を描かせず、 lanes136.py で後から引く形を試した。2コマ目は間の空いた絵を選び、セリフを大城さんにした |
| 3 | 1コマ目は構図を変えず、大城さんが真ん中に動けばいい。真ん中とは、人のレーンとハコブくんのレーンの間ではなく、歩く人のレーンの真ん中 | 元の絵のまま、 move136.py で大城さんだけを横へ動かした |
| 4 | 3コマ目の流れがおかしい。大城さんの私もよ! がよくない。3コマ目のレーンは2コマ目の幅に合わせ、間をもう少し広げる | 3コマ目を正面の鉢合わせにし、セリフを変え、 lanes_p3.py で2コマ目と同じ形の線を重ねた |
1コマ目:構図はそのまま、大城さんだけを横へ動かす
move136.py は、絵の中の太くて長い黒いかたまりを床の線として見つけ、直線を当てはめます。大城さんは、顔を含むかたまりとして切り抜き、腰の高さで、2本目と3本目の線のちょうど間に来るように横へずらします。ずらしたあとに見えてしまう、大城さんが隠していた床の線は、当てはめた直線で引き直します。ずらした量は右へ36ピクセルでした。

move136.py
"""第136話 1コマ目:構図はそのまま(071045)で、大城さんだけを歩く人のレーンの真ん中へ横に動かす(2026-11-05 エイイチ「大城が真ん中に動けばいいだけ。構図は変えないで」)
太くて長い黒=床の線として直線を当てはめ、大城さんが隠していた所も含めて引き直す。大城さんは顔を含むかたまりとして切り抜いて横にずらす。"""
import sys, numpy as np
from PIL import Image, ImageDraw
from scipy import ndimage
src, out = sys.argv[1:3]
im = Image.open(src).convert("RGB"); W, H = im.size
a = np.asarray(im).astype(int); g = a.mean(2)
green = (a[..., 1] - (a[..., 0] + a[..., 2]) / 2) > 40
thick = ndimage.binary_opening(g < 70, structure=np.ones((11, 11)))
lab, n = ndimage.label(thick)
lines = []
for k, (sy, sx) in enumerate(ndimage.find_objects(lab), 1):
if sy.stop - sy.start > 0.25 * H: # 縦に長い太い黒=床の線
ys, xs = np.nonzero(lab == k)
m, c = np.polyfit(ys, xs, 1)
wid = (lab == k).sum() / (sy.stop - sy.start)
lines.append((c + m * H, m, c, wid, sy.start, sy.stop))
lines.sort()
linemask = np.isin(lab, [k for k, (sy, sx) in enumerate(ndimage.find_objects(lab), 1) if sy.stop - sy.start > 0.25 * H])
linemask = ndimage.binary_dilation(linemask, iterations=2) & (g < 120)
# 大城さん:線を除いた緑でないかたまりのうち、顔(左上の明るい肌)を含むもの
fg = ~green & ~linemask
lab2, n2 = ndimage.label(fg)
top = lab2[:int(0.25 * H), :int(0.5 * W)]
k0 = np.bincount(top[top > 0]).argmax()
fig = ndimage.binary_fill_holes(lab2 == k0) & ~green
ys, xs = np.nonzero(fig); foot = ys.max()
fx = xs[ys > foot - 0.04 * H].mean()
def lx(L, y): return L[1] * y + L[2]
L0, L1 = lines[1], lines[2] # 2026-11-05 エイイチ「真ん中は人のレーンとハコブくんのレーンの間」→ 2本目と3本目の線の間
yb = int(ys.min() + 0.45 * (foot - ys.min())) # 腰の高さ
bx = xs[np.abs(ys - yb) < 6].mean()
cx = (lx(L0, yb) + lx(L1, yb)) / 2
dx = int(round(cx - bx))
b = a.copy(); b[ndimage.binary_dilation(fig, iterations=2)] = (0, 177, 64)
lay = Image.new("L", (W, H), 0); d = ImageDraw.Draw(lay)
for L in lines:
d.line([(lx(L, 0), 0), (lx(L, H), H)], fill=255, width=int(round(L[3])))
lay = (np.asarray(lay) > 128) & ndimage.binary_dilation(fig, iterations=4)
gb = (b[..., 1] - (b[..., 0] + b[..., 2]) / 2) > 40
b[lay & gb] = (0, 0, 0)
B = Image.fromarray(b.astype(np.uint8))
f = Image.fromarray(a.astype(np.uint8)); mk = Image.fromarray((fig * 255).astype(np.uint8))
T = (1, 0, -dx, 0, 1, 0)
B.paste(f.transform((W, H), Image.AFFINE, T), (0, 0), mk.transform((W, H), Image.AFFINE, T))
B.save(out, quality=95)
print("線", len(lines), [(int(l[0]), round(l[3])) for l in lines], "足もと", int(fx), int(foot), "→ レーンの真ん中", int(cx), "ずらす", dx, "大城さんの画素", int(fig.sum()))
lanes136.py(1コマ目の線を後から引く形。試したが使わなかった)
"""第136話 1コマ目の床の線を、絵の上に正確に描く(2026-11-05)
エイイチ「左の線が1本ない・大城さんもレーンの真ん中を通る」→ 線は絵のモデルに任せず、遠近の計算で4本引く。
大城さんの絵(床に何もない版)を切り抜いて、歩く人のレーンの真ん中に立たせる。マークは別の絵(071045)から切り出した黒い形。"""
import sys, numpy as np
from PIL import Image, ImageDraw
from scipy import ndimage
src, picsrc, out = sys.argv[1:4]
im = Image.open(src).convert("RGB"); W, H = im.size3コマ目:セリフを変え、2コマ目と同じレーンの線を重ねる
1回目の3コマ目は、大城さんとハコブくんが横に並んで同じ向きによけ、大城さんが私もよ! と言う絵でした。2コマ目でハコブくんは向こうから2本のレーンの間を走ってきているので、3コマ目は正面から鉢合わせになるはずです。私もよ! は、何に対しての返事なのかが分かりません。
3コマ目を、肩越しに正面で鉢合わせる絵にし、ハコブくんのヨケテイマスに対して大城さんが、だから、どっちに来るのよ! と言う形にしました。プロットの表の3コマ目のセリフも、同じ文に書き換えました。
- ねらい:2コマ目の、どっち側なの? という問いを、3コマ目で鉢合わせという結果につなげる
- 変わったこと:3コマ目のセリフが、私もよ! から、だから、どっちに来るのよ! に変わった
- 見込める効果:4コマ目の、どっち側か分かれば、よけ方も分かる、という結論が、3コマ目の困りごとからそのまま読める見込み
3コマ目は床なしで描かせ、lanes_p3.py で線を重ねました。線の消える点をハコブくんの体の後ろに置き、2本のレーンの間の真ん中を、ハコブくんの車輪の真ん中に合わせます。間の幅は、下の端でハコブくんの幅の約1.4倍にしました。線は奥ほど細くし、人とロボットの後ろ(緑の背景の所)にだけ描きます。

lanes_p3.py
"""第136話 3コマ目:床なしで描いた絵に、2コマ目と同じ形の床の線(4本・黒・同じ太さの決まり)とマークを重ねる(2026-11-05 エイイチ「2コマ目のレーンの幅と合わせて・間をもう少し広げる」)
消える点はハコブくんの真上。2本のレーンの間(広め)の真ん中を、ハコブくんの車輪の真ん中に合わせる。線は人とロボットの後ろ(緑のところだけ)に描く。"""
import sys, numpy as np
from PIL import Image, ImageDraw
from scipy import ndimage
src, out = sys.argv[1:3]
im = Image.open(src).convert("RGB"); W, H = im.size
a = np.asarray(im).astype(int)
green = (a[..., 1] - (a[..., 0] + a[..., 2]) / 2) > 40
fg = ndimage.binary_opening(~green, iterations=2)
lab, n = ndimage.label(fg)
# ハコブくん:右半分の、床に届く大きなかたまり
best = None
for k, (sy, sx) in enumerate(ndimage.find_objects(lab), 1):
cx = (sx.start + sx.stop) / 2
if cx > 0.5 * W and sy.stop > 0.5 * H and (best is None or (lab == k).sum() > best[1]): best = (k, (lab == k).sum(), sy, sx)
k, _, sy, sx = best
ys, xs = np.nonzero(lab == k); wy = ys.max(); hx = xs[ys > wy - 0.03 * H].mean()
vx, vy = hx, 0.42 * H # 消える点はハコブくんの体の後ろ(線がハコブくんの頭の上に出ない)
LW, GAP = 0.85 * W, 0.75 * W # 間はハコブくんの幅の約1.4倍(車輪の高さで) # 下の端でのレーンの幅と、間の幅(2コマ目より広め)
gc = hx # 間の真ん中(消える点の真下なので、どの高さでもハコブくんの下)
XB = [gc - GAP / 2 - LW, gc - GAP / 2, gc + GAP / 2, gc + GAP / 2 + LW]
def X(x, y): return vx + (x - vx) * (y - vy) / (H - vy)
lay = Image.new("L", (W, H), 0); d = ImageDraw.Draw(lay)
for x in XB:
for i in range(80):
y0 = vy + (H - vy) * (0.06 + i / 80 * 0.94); y1 = vy + (H - vy) * (0.06 + (i + 1) / 80 * 0.94) + 1
w0 = 4 + 44 * (y0 - vy) / (H - vy); w1 = 4 + 44 * (y1 - vy) / (H - vy)
d.polygon([(X(x, y0) - w0 / 2, y0), (X(x, y0) + w0 / 2, y0), (X(x, y1) + w1 / 2, y1), (X(x, y1) - w1 / 2, y1)], fill=255)
def icon(name, S=400):
m = Image.new("L", (S, S), 0); g = ImageDraw.Draw(m); L = lambda pts, w: g.line(pts, fill=255, width=w, joint="curve")
if name == "walk":
g.ellipse((175, 10, 245, 80), fill=255); L([(200, 100), (175, 230)], 52)
L([(195, 120), (120, 185), (80, 160)], 34); L([(195, 120), (260, 175), (300, 210)], 34)
L([(175, 225), (120, 300), (60, 380)], 40); L([(175, 225), (240, 300), (270, 385)], 40)
else:
L([(20, 40), (80, 40), (130, 280), (360, 280)], 26)
g.polygon([(110, 110), (380, 110), (350, 250), (135, 250)], fill=255)
g.ellipse((120, 300, 190, 370), fill=255); g.ellipse((300, 300, 370, 370), fill=255)
return m.crop(m.getbbox())
def put(m, lane, y, frac):
lw = (XB[lane + 1] - XB[lane]) * (y - vy) / (H - vy); r = lw * frac / m.width
m = m.resize((max(1, int(m.width * r)), max(1, int(m.height * r * 0.6))))
c = X((XB[lane] + XB[lane + 1]) / 2, y)
lay.paste(255, (int(c - m.width / 2), int(y - m.height / 2)), m)
put(icon("walk"), 0, 0.93 * H, 0.22)
put(icon("cart"), 2, 0.56 * H, 0.40)
L = np.asarray(lay) > 128
b = a.copy(); b[L & green] = (0, 0, 0)
Image.fromarray(b.astype(np.uint8)).save(out, quality=95)
print("ハコブくんの車輪の真ん中", int(hx), int(wy), "下の端の線", [int(x) for x in XB])
第136話の spec.py
EP, TITLE = 136, "どっち側なの"
# 2026-11-05 カメラワーク:P1 床すれすれから2本の線と大城さん/P2 大城さんの肩なめで、真ん中を来るハコブくん/P3 真横の引きで右へ左へ(効果線)/P4 槙原さんのタブレットの手元を前景に、線どおりにすれ違う2人
LANES = ["two long parallel lanes painted on the floor, receding into the distance. EACH lane has its OWN two border lines — so there are exactly FOUR thick SOLID BLACK lines in total, all equally thick: the left lane's left and right borders, and the right lane's left and right borders",
"between the two lanes there is a clear GAP of bare floor, about half a lane wide, with NO line inside it — the lanes do NOT share a middle line",
"inside each lane there is only ONE large pictogram, printed in SOLID BLACK ink: a WALKING PERSON in the LEFT lane, a WHEELED CART in the RIGHT lane. Exactly ONE pictogram per lane in the whole picture — NOT a repeated row, NO second copy further away",
"every line and pictogram on the floor is SOLID BLACK — NOT white, NOT outlined, NOT grey. NO letters, NO arrows, NO words",
"the lanes are the ONLY thing drawn on the floor; everything else stays flat green"]
# P1 の床の線は描かせず、lanes136.py で後から描く(線の本数・位置・大城さんがレーンの真ん中を歩くことを正確にするため)4コマ目のレーンは、直す前の絵のままです。並べて見て、通してよいとなりました。
第137話:毎朝同じ時刻に詰まる廊下
第137話は、朝の交代の時間、ナースステーション前の廊下で申し送りが始まり、ハコブくんがワゴンの列の前で動けなくなる話です。もとにしたのは、病院に搬送ロボットを入れた事例の報告(シフト交代の時間に廊下で詰まった)です。カメラワークを最初から spec に書いて描いたので、この話は各コマ3枚ずつ、計12枚で決まりました。
第137話の spec.py
EP, TITLE = 137, "毎朝ここで"
# 2026-11-05 カメラワーク:P1 引きの全景(申し送りの2人と、奥から来るハコブくん)/P2 ハコブくんの背中なめで、よける2人と並ぶワゴン/P3 高杉さんの顔に寄る・手前にハコブくんの画面の端/P4 白石さんの腰上を手前に、遠回りして走り去るハコブくん
WAGONS = ("WAGONS", ["a row of three plain hospital wagons (small carts with shelves and casters) parked one after another along the corridor, blocking the way. NO text, NO labels"])
PANELS = [
dict(chars=["大城", "高杉", "ハコブ"], mouth="NORMAL", no_yline=True,
prop=("CLIPBOARDS", ["Oshiro and Takasugi each hold one plain clipboard with blank paper. NO text, NO writing on any paper"]),
hscale_text=["Oshiro and Takasugi stand together in the middle ground at the RIGHT, full body: heads at about 32%, shoes at about 74% of the image height (Takasugi slightly shorter)",
"Hakobukun is far away at the LEFT, about 10 m behind them, small: its top at about 50% and wheels at about 64% of the image height"],4話の仕上がりと、サイト・英語版・表紙

4話を pixelstomotors.com のマンガのページに載せました。日本語のページは src/data/manga/ep{N}.json、英語のページは src/data/english/ep{N}.json に、題名・説明・各コマの説明・デザインノート(その話で起きたことが、現実のロボットの設計のどこにつながるかを3段で書いた文)を書きます。デザインノートには、もとにした調査へのリンクを付けました。
| 話 | 日本語の題名 | 英語の題名 | ページ |
|---|---|---|---|
| 134 | ゆっくり走っても、安全とは限らない | Slow Isn’t Always Safe | |
| 135 | 止まる場所が、人の仕事を止める | Where It Stops Matters | |
| 136 | ロボットは、どっち側を通るのか | Which Side Is the Robot On? | |
| 137 | 毎朝同じ時刻に詰まる廊下 | The Hallway That Jams Every Morning |
p2m-site への commit は、第134話が 59abdc9、第135〜137話が af85a70 です。
表紙
表紙は、見せ場のコマから吹き出しを消し、題名の一言を大きく重ねて作ります(LOG413)。吹き出しを消すのは Gemini の編集に任せていますが、第136話は消すときに床の物を足されたので、吹き出しを白く塗る形にしました。題名の字は Noto Sans CJK の Black、96px、色 #242424 です。
英語版
英語版は、描き直さずに、吹き出しの中の日本語を消して英語を入れる 道具/英語版/en_manga.py で作ります(LOG416)。話ごとの指定は JSON で、seed は各吹き出しの中の1点です。第134話の3コマ目の吹き出しは、コマの端に触れていて吹き出しとして見つけられなかったので、手書き風の文字(signs の hand)にしました。
英語版の指定 ep134〜137.json
// ep134.json
{"ep": 134, "cover_line": "BUT I'M RUNNING LATE!", "panels": [{"bubbles": [{"seed": [481, 164], "text": "Um, I'm kind of\nin a hurry here…"}], "signs": []}, {"bubbles": [], "signs": []}, {"bubbles": [{"seed": [24, 642], "text": "I am driving\nsafely."}], "signs": [{"box": [684, 44, 1050, 232], "text": "Safe, sure. But I'm the one running late!", "style": "hand"}]}, {"bubbles": [{"seed": [88, 209], "text": "After you."}, {"seed": [721, 197], "text": "Now that's\nthe safest."}], "signs": []}]}
// ep135.json
{"ep": 135, "cover_line": "THE FOOD'S GETTING COLD!", "panels": [{"bubbles": [{"seed": [321, 149], "text": "Recalculating\nroute…"}], "signs": []}, {"bubbles": [{"seed": [39, 229], "text": "You're stopping HERE?\nThe food's\ngetting cold!"}, {"seed": [62, 732], "text": "Calculating."}], "signs": []}, {"bubbles": [{"seed": [74, 204], "text": "Calculation\ncomplete."}, {"seed": [587, 150], "text": "And I was stuck\nthe whole time, too."}], "signs": []}, {"bubbles": [{"seed": [545, 237], "text": "Better to think\nsomewhere roomy."}], "signs": []}]}
// ep136.json
{"ep": 136, "cover_line": "WHICH WAY ARE YOU GOING?!", "panels": [{"bubbles": [], "signs": []}, {"bubbles": [{"seed": [90, 195], "text": "Wait, which side\nare you on?"}], "signs": []}, {"bubbles": [{"seed": [63, 175], "text": "So which way\nare you going?!"}, {"seed": [685, 183], "text": "I am dodging."}], "signs": []}, {"bubbles": [{"seed": [43, 168], "text": "Know the side,\nknow how\nto dodge."}, {"seed": [679, 161], "text": "I am on\nthe cart side."}], "signs": []}]}
// ep137.json
{"ep": 137, "cover_line": "EVERY MORNING, I STOP HERE.", "panels": [{"bubbles": [], "signs": []}, {"bubbles": [{"seed": [109, 186], "text": "Coming\nthrough."}], "signs": []}, {"bubbles": [{"seed": [453, 197], "text": "Every morning,\nit's like this."}, {"seed": [27, 677], "text": "Every morning,\nI stop here."}], "signs": []}, {"bubbles": [{"seed": [561, 196], "text": "At shift change,\nI take the\nlong way."}], "signs": []}]}
YouTube ショート4本:冒頭の動きと、最初の数秒の無音
マンガは、縦長のコマを順に見せ、声と効果音を付けた YouTube ショートにもしています(LOG408)。声は ElevenLabs(文章を読み上げる音声合成のサービス)で、Mac の tts_eleven.py から作ります。動画を組み立てる道具一式は、クラウドの作業環境が新しくなっていたので、KIOXIA に置いた _取り込み_20261030/w30.tgz から作り直しました。
冒頭の約3.6秒は、話ごとに違う動きをするハコブくんのアニメです。第134話はのろのろ進んでから気づき、脇へ寄っておじぎ。第135〜137話は motion13x.py に3つ足しました。
| 話 | 冒頭の動き |
|---|---|
| 134 | のろのろ → 気づく → 脇へ寄っておじぎ |
| 135 | 急に止まって 経路ヲ計算中 → 長い計算 → 完了でにっこり |
| 136 | 右へ左へ、よけるように揺れる(ドッチ?) |
| 137 | 走ってきて 8:30 交代中 で止まり、しょんぼり → 気づいて遠回り |
冒頭の効果音が鳴らなかった原因
書き出したショートを見ると、最初の数秒に音がありませんでした。原因は、作り直した作業環境で、効果音のファイルが読めていなかったことです。 組み立てる mkshort.py は、動き出すとすぐに spec のフォルダへ移り(os.chdir)、効果音を spec からの相対の住所 se/se_rev.mp3 で読みます。作り直した環境では、各話のフォルダに se がなく、読めないまま、止まらずに音なしで書き出していました。
各話のフォルダに se へのリンクを置き、冒頭0秒にも走る音を足して4本を作り直しました。冒頭1秒の音量を ffmpeg の volumedetect で測った結果です。
$ ls -la */se
lrwxrwxrwx 1 root root 8 Nov 5 17:09 第134話_オ先ニドウゾ/se -> ../../se
lrwxrwxrwx 1 root root 8 Nov 5 17:09 第135話_考えるのは広い所で/se -> ../../se
lrwxrwxrwx 1 root root 8 Nov 5 17:09 第136話_どっち側なの/se -> ../../se
lrwxrwxrwx 1 root root 8 Nov 5 17:09 第137話_毎朝ここで/se -> ../../se
$ for d in 第13[4-7]話_*; do f="$d/out_$d.mp4"; echo "== $f"; ffprobe -v error -show_entries format=duration -of csv=p=0 "$f"; ffmpeg -hide_banner -t 1.0 -i "$f" -af volumedetect -vn -f null - 2>&1 | grep -E "mean_volume|max_volume"; done
== 第134話_オ先ニドウゾ/out_第134話_オ先ニドウゾ.mp4
23.033333
[Parsed_volumedetect_0 @ 0x559bbe53e500] mean_volume: -17.5 dB
[Parsed_volumedetect_0 @ 0x559bbe53e500] max_volume: -2.1 dB
== 第135話_考えるのは広い所で/out_第135話_考えるのは広い所で.mp4
22.900000
[Parsed_volumedetect_0 @ 0x55886a99aa00] mean_volume: -16.7 dB
[Parsed_volumedetect_0 @ 0x55886a99aa00] max_volume: -1.2 dB
== 第136話_どっち側なの/out_第136話_どっち側なの.mp4
22.300000
[Parsed_volumedetect_0 @ 0x5591377dfac0] mean_volume: -17.8 dB
[Parsed_volumedetect_0 @ 0x5591377dfac0] max_volume: -1.1 dB
== 第137話_毎朝ここで/out_第137話_毎朝ここで.mp4
20.566667
[Parsed_volumedetect_0 @ 0x55591a75eec0] mean_volume: -16.7 dB
[Parsed_volumedetect_0 @ 0x55591a75eec0] max_volume: -1.3 dB
4本とも20〜23秒で、冒頭1秒から音が出ています。書き出した mp4 と、題名・説明の txt は、KIOXIA の videos/_ショート専用/_未アップ/第13X話_*/ に置きました。
motion13x.py(第135〜137話の冒頭の動き)
"""第135〜137話の冒頭の動き(2026-11-05)。話ごとに違う動き。
135 急停止して「経路ヲ計算中」→ 長い計算 → 「完了」でにっこり
136 右へ左へ、よけるように揺れる(「ドッチ?」)
137 走ってきて「8:30 交代」で止まり、しょんぼり → 気づいて遠回り(左へ走り去る)"""
import sys
import numpy as np
import motion30, motion134
import hakobu_motion as M
from hakobu_motion import T, ease, _approach
class Recalc135:
dur = 3.6; empty = True
def motion(self, t):
if t < 0.7: return dict(dx=260 * (1 - ease(t / 0.7)), dy=-5 * abs(np.sin(2 * np.pi * 4 * t)))
v = t - 0.7; k = np.exp(-v * 10) * np.cos(2 * np.pi * 5 * v)
return dict(sx=1 + 0.05 * k, sy=1 - 0.05 * k, rot=-3 * k)
def face(self, t):
if t < 0.7: return ("open", "open"), None, "mid"
if t < 2.8:
dots = "・" * (1 + int((t - 0.7) * 3) % 3)
return T("経路ヲ", "計算中" + dots)
return ("happy", "happy"), None, "closed"
def ring(self, t): return 1.0 if 0.7 <= t < 2.8 and int(t * 6) % 2 == 0 else 0.6
def blur(self, t): return t < 0.7
def dim(self, t): return 1.0
def cam(self, t): return (0, 0)
class Sway136:
dur = 3.6; empty = True
def motion(self, t):
if t < 0.9: return _approach(t, 0.9, 230)
v = t - 0.9
x = 90 * np.sin(2 * np.pi * 0.9 * v) * min(1, v / 0.3)
return dict(dx=x, rot=-2.5 * np.cos(2 * np.pi * 0.9 * v) * min(1, v / 0.3))
def face(self, t):
if t < 0.9: return ("open", "open"), None, "mid"
if t < 2.6: return T("ドッチ?")
return ("big", "big"), None, "wave" if int(t * 9) % 2 else "wave2"
def ring(self, t): return 0.6
def blur(self, t): return t < 0.9
def dim(self, t): return 1.0
def cam(self, t): return (0, 0)
class Shift137:
dur = 3.8; empty = True
def motion(self, t):
if t < 0.9: return _approach(t, 0.9, 230)
if t < 2.6:
u = ease((t - 1.6) / 0.6) if t > 1.6 else 0
return dict(dy=14 * u, rot=3 * u, sy=1 - 0.03 * u)
v = t - 2.6
return dict(dx=-420 * ease(v / 0.9), dy=-4 * abs(np.sin(2 * np.pi * 3 * v)), rot=-1.5)
def face(self, t):
if t < 0.9: return ("open", "open"), None, "mid"
if t < 1.6: return T("8:30", "交代中")
if t < 2.6: return ("down", "down"), "trouble", "frown"
return ("happy", "happy"), None, "closed"
def ring(self, t): return 0.6
def blur(self, t): return t < 0.9 or t >= 2.6
def dim(self, t): return 1.0
def cam(self, t): return (0, 0)
M.PATTERNS["ep135"] = Recalc135; M.PATTERNS["ep136"] = Sway136; M.PATTERNS["ep137"] = Shift137
if __name__ == "__main__":
for arg in sys.argv[1:]:
nm, _, kv = arg.partition(":")
pat = M.PATTERNS[nm]()
for item in filter(None, kv.split(",")):
k, v = item.split("=", 1)
if k in motion30.STR: setattr(pat, k, v)
else: setattr(pat, k, float(v))
M.render(pat, f"ハコブくん_{nm}.mp4")
motion134.py(第134話の冒頭の動き)
"""第134話の冒頭の動き(2026-11-05):ノロノロの安全運転 → 後ろの気配に気づく → スッと脇へ寄って、どうぞのおじぎ"""
import sys
import numpy as np
import motion30
import hakobu_motion as M
from hakobu_motion import T, ease
class Yield134:
dur = 3.6
empty = True
SLOW, NOTICE, SIDE = 2.0, 2.0, 2.45
def motion(self, t):
if t < self.SLOW: # 右からノロノロ(歩くより遅い)
u = t / self.SLOW
return dict(dx=250 * (1 - u), dy=-2 * abs(np.sin(2 * np.pi * 1.2 * t)), rot=0.4 * np.sin(2 * np.pi * 1.2 * t))
if t < self.SIDE: # 後ろの気配に気づいて、びくっ
v = t - self.NOTICE
k = np.exp(-v * 9)
return dict(dy=-14 * k * abs(np.sin(2 * np.pi * 3 * v)), sy=1 + 0.03 * k)
v = t - self.SIDE # スッと左の脇へ寄って止まり、おじぎ
x = -150 * ease(v / 0.35)
nod = 0.05 * max(0.0, np.sin(2 * np.pi * 1.2 * (v - 0.45))) if 0.45 < v < 0.9 else 0
return dict(dx=x, sy=1 - nod, dy=6 * nod / 0.05)
def face(self, t):
if t < self.SLOW: return T("安全", "運転中")
if t < self.SIDE: return ("big", "big"), None, "o"
return ("happy", "happy"), None, "closed"
def ring(self, t): return 0.6
def blur(self, t): return self.SIDE <= t < self.SIDE + 0.35
def dim(self, t): return 1.0
def cam(self, t): return (0, 0)
M.PATTERNS["ep134"] = Yield134
if __name__ == "__main__":
for arg in sys.argv[1:]:
nm, _, kv = arg.partition(":")
pat = M.PATTERNS[nm]()
for item in filter(None, kv.split(",")):
k, v = item.split("=", 1)
if k in motion30.STR: setattr(pat, k, v)
else: setattr(pat, k, float(v))
M.render(pat, f"ハコブくん_{nm}.mp4")
spec_ep134.py
import os, sys
sys.path.insert(0, os.environ.get("W24", os.path.expanduser("~/w")) + "/specs"); from common24 import *
import common24; common24.BGMAP[134] = "第63話.png"
HERE = os.path.dirname(os.path.abspath(__file__)); d = lambda l, e=2.0: vlen(HERE, l["v"], e)
NAME, EP = "第134話_オ先ニドウゾ", 134
HEAD = ["安全運転なのに", "なぜ怒られる?"]
COMIC = C + "ep134.jpg" # 縦長のコマ(1080×1350)を P1〜P4 の順に積んだ画像
P1, P2, P3, P4 = [(0, 1350 * i, 1080, 1350 * (i + 1)) for i in range(4)]
DUR = 3.6
MOTION = mot("ep134", EP) # 冒頭の動き(motion134.py:ノロノロ→気づく→脇へ寄っておじぎ)
SCENES = [
dict(kind="op", video=anim(EP, "ep134"), dur=DUR, lines=[], se=[dict(at=0.05, f="se/se_rev.mp3", db=-14), dict(at=2.0, f="se/se_pikon.mp3", db=-10), dict(at=2.45, f="se/se_dash.mp3", db=-12)]),
dict(kind="panel", crop=P1, fit=False, lines=[ns("高杉", 1, "ちょっと|【急いでる】んだけど…")]),
dict(kind="panel", crop=P2, fit=False, min=2.2, se=[dict(at=0.3, f="se/se_dash.mp3", db=-12), dict(at=0.55, f="se/se_dash.mp3", db=-12)], words=[W_("右へ…", 0.1, 300, 240, 110, "orange", -6), W_("こっちも右!?", 0.9, 560, 380, 90, "orange", 4)]),
dict(kind="panel", crop=P3, fit=False, lines=[ns("高杉", 2, "安全運転なのは分かるけど、|【私が遅れてる】!"), hk(1, "ワタシハ、|【安全ニ走ッテイマス】")]),
dict(kind="panel", crop=P4, fit=False, lines=[hk(2, "【オ先ニドウゾ】"), ns("高杉", 3, "それが|【一番安全】")]),
end(("遅く走るだけでは", 74, "ink", 0.0), ("安全にならない", 74, "ink", 0.25), ("譲る場所", 120, "orange", 0.9), ("を作る", 120, "orange", 0.9), ("(追い越せる場所と、脇へ寄る動き)", 34, "gray", 1.8)),
]
spec_ep135.py
import os, sys
sys.path.insert(0, os.environ.get("W24", os.path.expanduser("~/w")) + "/specs"); from common24 import *
import common24
common24.BGMAP[135] = "第48話.png"
HERE = os.path.dirname(os.path.abspath(__file__))
NAME, EP = "第135話_考えるのは広い所で", 135
HEAD = ["ロボットが止まると", "ご飯が冷める?"]
COMIC = C + "ep135.jpg"
P1, P2, P3, P4 = [(0, 1350 * i, 1080, 1350 * (i + 1)) for i in range(4)]
DUR = 3.6
MOTION = mot("ep135", EP) # motion13x.py:急停止→経路ヲ計算中→完了でにっこり
SCENES = [
dict(kind="op", video=anim(EP, "ep135"), dur=DUR, lines=[], se=[dict(at=0.0, f="se/se_dash.mp3", db=-12), dict(at=0.7, f="se/se_skid.wav", db=-10), dict(at=2.8, f="se/se_pikon.mp3", db=-10)]),
dict(kind="panel", crop=P1, fit=False, lines=[hk(1, "【経路ヲ計算中】…")]),
dict(kind="panel", crop=P2, fit=False, lines=[ns("寺島", 1, "ここで止まるの?|【ご飯が冷めちゃう】"), hk(2, "計算中デス")]),
dict(kind="panel", crop=P3, fit=False, lines=[hk(3, "計算ガ、|終ワリマシタ"), ns("寺島", 2, "終わるまで、|【私も止まってた】のよ")]),
dict(kind="panel", crop=P4, fit=False, lines=[hk(4, "考エルノハ、|【広イ所デ】")]),
end(("迷って止まるなら", 74, "ink", 0.0), ("人の通り道の", 74, "ink", 0.25), ("外で", 120, "orange", 0.9), ("(狭い所に入る前に、広い所で道を決める)", 34, "gray", 1.8)),
]
spec_ep136.py
import os, sys
sys.path.insert(0, os.environ.get("W24", os.path.expanduser("~/w")) + "/specs"); from common24 import *
import common24
common24.BGMAP[136] = "第86話.png"
HERE = os.path.dirname(os.path.abspath(__file__))
NAME, EP = "第136話_どっち側なの", 136
HEAD = ["ロボットは", "どっち側を通る?"]
COMIC = C + "ep136.jpg"
P1, P2, P3, P4 = [(0, 1350 * i, 1080, 1350 * (i + 1)) for i in range(4)]
DUR = 3.6
MOTION = mot("ep136", EP) # motion13x.py:右へ左へ揺れて「ドッチ?」
SCENES = [
dict(kind="op", video=anim(EP, "ep136"), dur=DUR, lines=[], se=[dict(at=0.0, f="se/se_dash.mp3", db=-12), dict(at=1.2, f="se/se_yusa.wav", db=-10), dict(at=2.3, f="se/se_yusa.wav", db=-10)]),
dict(kind="panel", crop=P1, fit=False, min=1.8, words=[W_("人の線", 0.2, 230, 1500, 80, "orange", 0), W_("台車の線", 0.5, 820, 1500, 80, "orange", 0)]),
dict(kind="panel", crop=P2, fit=False, lines=[ns("大城", 1, "えっ、あなたは|【どっち側】なの?")]),
dict(kind="panel", crop=P3, fit=False, lines=[hk(1, "ヨケテイマス"), ns("大城", 2, "だから、|【どっちに来るのよ】!")]),
dict(kind="panel", crop=P4, fit=False, lines=[ns("大城", 3, "どっち側か分かれば、|【よけ方も分かる】"), hk(2, "ワタシハ、|【台車ノ側】デス")]),
end(("ロボットにも", 74, "ink", 0.0), ("決まった", 74, "ink", 0.25), ("通る側", 120, "orange", 0.9), ("(施設の交通ルールの中で、見える形に)", 34, "gray", 1.8)),
]
spec_ep137.py
import os, sys
sys.path.insert(0, os.environ.get("W24", os.path.expanduser("~/w")) + "/specs"); from common24 import *
import common24
common24.BGMAP[137] = "第24話.png"
HERE = os.path.dirname(os.path.abspath(__file__))
NAME, EP = "第137話_毎朝ここで", 137
HEAD = ["毎朝同じ時刻に", "止まるロボット"]
COMIC = C + "ep137.jpg"
P1, P2, P3, P4 = [(0, 1350 * i, 1080, 1350 * (i + 1)) for i in range(4)]
DUR = 3.8
MOTION = mot("ep137", EP) # motion13x.py:「8:30 交代中」でしょんぼり→遠回りへ走り去る
SCENES = [
dict(kind="op", video=anim(EP, "ep137"), dur=DUR, lines=[], se=[dict(at=0.0, f="se/se_dash.mp3", db=-12), dict(at=0.9, f="se/se_pikon.mp3", db=-10), dict(at=2.6, f="se/se_dash.mp3", db=-12)]),
dict(kind="panel", crop=P1, fit=False, min=1.8, words=[W_("朝の交代", 0.1, 540, 1500, 100, "orange", 0)]),
dict(kind="panel", crop=P2, fit=False, lines=[hk(1, "【通リマス】")]),
dict(kind="panel", crop=P3, fit=False, lines=[ns("高杉", 1, "この時間は、|【毎朝こうなる】のよ"), hk(2, "毎朝、|【ココデ止マッテ】イマス")]),
dict(kind="panel", crop=P4, fit=False, lines=[hk(3, "交代ノ時間ハ、|【遠回リ】シマス")]),
end(("ロボットの予定に", 74, "ink", 0.0), ("病院の", 74, "ink", 0.25), ("時刻表", 120, "orange", 0.9), ("(毎日混む時刻を、先に入れておく)", 34, "gray", 1.8)),
]
SNS の予約:日本語は 11/28 から、英語は 1/27 から
マンガの投稿文は r2r2r-content の sns/MANGA_ep{N}_sns_posts.json に置き、予定表 sns/schedule.json に入れると、GitHub Actions が時刻に投稿します。日本語は空いている次の枠、英語は空いている次の平日の米国東部時間 11:00 か 15:00 です(英語は、これまでの話を順に出している列の後ろに入ります)。
| 話 | 日本語の X・LinkedIn(日本時間) | 英語の X(米国東部時間) |
|---|---|---|
| 134 | 11/28 19:30 | 1/27 15:00 |
| 135 | 11/29 19:30 | 1/28 11:00 |
| 136 | 11/30 20:00 | 1/28 15:00 |
| 137 | 12/1 20:00 | 1/29 11:00 |
r2r2r-content への commit は c3d6068 と 28e6d00 です。ショートの X の文の型(SHORT_ep13X の x_template)も同じときに置きました。
Gemini に描かせた枚数と費用
各話の 生成/ フォルダにある、Gemini が返した絵の数を数えました(プログラムで加工した絵は除く)。費用は1枚約10円で見積もった値です。
| 話 | 描いた枚数 | 費用の見積もり | 枚数が増えた理由 |
|---|---|---|---|
| 134 | 41枚 | 約410円 | 1回目を描いたあと、カメラワークを組み直して描き直した |
| 135 | 29枚 | 約290円 | 配膳車の形と構成を直して描き直した |
| 136 | 74枚 | 約740円 | 床の線を4回直した |
| 137 | 12枚 | 約120円 | 最初からカメラワークを書いて描いた |
| 計 | 156枚 | 約1,560円 |
ふだんの1話は20枚前後です。第137話のように、描く前に spec でカメラワークを決めておけば、1話12枚で収まることがありました。逆に、第136話のように話の中心になる小道具(床の線)が絵の中でずれると、描き直しが増えます。線のように形が決まっている物は、描かせずにプログラムで引くほうが早い、というのが第136話から分かったことです。
チェック項目と、過去の間違いの台帳に足したこと
マンガを見せる前に確かめるチェック項目(指示/チェック項目と過去の間違い.md)に、コマの動きの節 H と、ショートの音の確かめを足しました。同じ内容を、このプロジェクトの資料 claude/manga_check_and_mistakes.md にも入れています。
### F. 話と絵
- [ ] プロットの場面になっている(誰が・何をしている・小道具)。めくる前のコマでオチを見せていない
- [ ] 小道具が変でない(頭の上に扉が浮く、など。扉は床から立つ)
- [ ] **何コマにも出る小道具(配膳車・ワゴン・床の線など)は、同じ話の中で形と色がそろっている。** 形がぶれる物は三面図を作って `キャラ/` に置き、spec の `extra_refs` で毎コマ添える(例 `キャラ/配膳車.jpg`)
- [ ] 床の線・マークは黒で統一し、数を絞る(1つのレーンにマーク1つ)。白抜きや、マークがずらっと並ぶ絵にしない
- [ ] ガイドの白い枠や名前の札が絵に写り込んでいない
### H. コマの動き(4コマを並べて見る・2026-11-05)
- [ ] 4コマが同じ大きさ・同じ高さ・横並びの棒立ちになっていない。隣り合うコマでショットサイズとカメラの位置を変える(`指示/カメラワーク規定_縦長版.md`)
- [ ] 1話の中に、大胆な寄り(顔・手元・画面)か、手前に人やロボットを大きく切って入れる重ね(肩なめ・前景)が1コマ以上ある
- [ ] カメラの高さを変えている(人の目線150cm だけで通さない。ハコブくん目線90〜100cm・低い60cm など)
- [ ] 動き・感情の山のコマに、意図のある効果線(スピード線・集中線・縦線)が入っている
- [ ] 大きさを比べないコマ(手前と奥の重ね)は `no_yline=True` にして、hscale_text で画像の上の位置を書く。並び図の比は、同じ床・同じ距離に立つコマで守る
### G. ショート(書き出したあと)
- [ ] 冒頭の動くハコブくんは話ごとに違う動き(同じ動きを並べない)
- [ ] 声の読み・抑揚が変でない(怪しい語は --try で聞き比べる)
- [ ] 30秒以内・字幕は1行14字・2行まで
- [ ] 冒頭(最初の3秒)から音が出ている。効果音は spec のフォルダからの相対の `se/…` で読むので、作業環境では各話のフォルダに `se` のリンクを置く(ないと音なしで書き出されても止まらない)。書き出したら ffmpeg の volumedetect で冒頭の音量を測る
## 過去の間違い(2026-11-05 に足した行)
| 日付 | 話・コマ | 間違い | 見つけた人 | 直し方(プロンプト・作り方) |
|---|---|---|---|---|
| 2026-11-05 | 第134〜137話のショート | 冒頭の効果音が鳴らず、最初の数秒が無音 | エイイチ | 作り直した作業環境で、話のフォルダに se のリンクがなく効果音が読めていなかった。リンクを置き、冒頭0秒にも走る音を足して作り直した |
| 2026-11-05 | 第135話(1回目) | 配膳車の形がコマごとに違う/1コマ目のハコブくんの頭が別の箱のよう/構成が単調 | エイイチ | 配膳車の三面図(キャラ/配膳車.jpg)を作って毎コマ添えた。vbuild に HAKOBUKUN HEAD SHAPE の節を足した。顔の大写し・傾けた構図・床から見上げる・手前の取っ手と湯気、で組み直した |
| 2026-11-05 | 第136話 P1・P2・P4(1回目) | 床のマークが黒と白でばらばら・並びすぎ | エイイチ | 線とマークは黒一色、1つのレーンにマーク1つ、と spec の LANES に書いた |
| 2026-11-05 | 第134話 全コマ(1回目) | コマに動きがなく単調(4コマとも中くらいの引き・横並び・人の目線の高さ) | エイイチ | カメラワーク規定どおり、コマごとに寄り・引き・高さ・向きを変え、前景の重ねと効果線を入れて組み直した。チェック項目 H を足した |
| 2026-11-05 | 第134話 P2(1回目) | 後ろ向きのハコブくんの背中が三面図と違う(ドームに黒い格子など)。いつも違う形で描かれていた | エイイチ | hakobu_back.png を添える `back=True` と、背中の形を書いた HAKOBU_BACK の節を vbuild に足した |
| 2026-11-05 | 第134話 P2 | 後ろから見た高杉さんの袖に名札が付いた | Claude | 仕上げで近くの網点を写して消す `CLONE` を vbuild に足した |
次にやること:描く前に、spec でカメラワークを決める
第137話は、描く前に spec でコマごとのショット・位置・高さを決めたので、描き直しが少なく済みました。次の話からは、プロットから spec を書く段階で、4コマのカメラの高さとショットの大きさを先に並べ、隣り合うコマで同じにならないかを見てから描きます。床の線のように形の決まった物は、最初からプログラムで引く前提で spec に書きます。プロットの表には、まだ第138〜183話の46話が残っています。