4コママンガ『ハコブくん』のコマに動きを足した ― 次は描く前に構図を決める

4コママンガ『ハコブくん』のコマに動きを足した ― 次は描く前に構図を決める

この記事の素材(コード・ログ・データ 7ファイル・まとめてダウンロードあり)
AIで読む ChatGPT・Claude などで、この記事に質問できます

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 にコマの中身だけを書いて渡すと、いちばん無難な構図で返ってきます。

第134話の1回目と組み直し後の4コマ。1回目は4コマとも同じ引きで人とロボットが横に並ぶ。組み直し後はロボットを手前に大きく置き、肩越し・見上げる寄り・集中線が入っている

そこで、コマごとにショットの大きさ(顔だけか、全身か)、カメラの位置、カメラの高さを、数字で書いて渡すようにしました。高さは、人の目の高さ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

01_sec01.py.txt9,254字・77行・python
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 の変更点(変える前の控えとの差分)

02_sec02.txt4,623字・54行・text
--- 指示/控え/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 neck
別のタブで開く保存

vbuild.py(変更後の全文)

03_vbuild.py.txt49,107字・688行・python
#!/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つです。

  1. 配膳車の三面図 キャラ/配膳車.jpg を作り、spec の extra_refs で毎コマ添えた
  2. 上の頭の形の決まりを vbuild.py に足した
  3. カメラワークを組み直した:1コマ目は配膳車の取っ手と湯気を手前に大きく、2コマ目は寺島さんの顔の大写しで画面を10度傾け集中線、3コマ目は床から30cm で見上げる、4コマ目はハコブくん目線100cm で広いホール

第135話の spec.py

04_spec.py.txt7,478字・62行・python
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ピクセルでした。

第136話1コマ目。左が Gemini が描いた元の絵、右が大城さんだけを歩く人のレーンの真ん中へずらした絵。構図と線はそのまま

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コマ目の線を後から引く形。試したが使わなかった)

05_sec05.py.txt3,133字・53行・python
"""第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.size
別のタブで開く保存

3コマ目:セリフを変え、2コマ目と同じレーンの線を重ねる

1回目の3コマ目は、大城さんとハコブくんが横に並んで同じ向きによけ、大城さんが私もよ! と言う絵でした。2コマ目でハコブくんは向こうから2本のレーンの間を走ってきているので、3コマ目は正面から鉢合わせになるはずです。私もよ! は、何に対しての返事なのかが分かりません。

3コマ目を、肩越しに正面で鉢合わせる絵にし、ハコブくんのヨケテイマスに対して大城さんが、だから、どっちに来るのよ! と言う形にしました。プロットの表の3コマ目のセリフも、同じ文に書き換えました。

  • ねらい:2コマ目の、どっち側なの? という問いを、3コマ目で鉢合わせという結果につなげる
  • 変わったこと:3コマ目のセリフが、私もよ! から、だから、どっちに来るのよ! に変わった
  • 見込める効果:4コマ目の、どっち側か分かれば、よけ方も分かる、という結論が、3コマ目の困りごとからそのまま読める見込み

3コマ目は床なしで描かせ、lanes_p3.py で線を重ねました。線の消える点をハコブくんの体の後ろに置き、2本のレーンの間の真ん中を、ハコブくんの車輪の真ん中に合わせます。間の幅は、下の端でハコブくんの幅の約1.4倍にしました。線は奥ほど細くし、人とロボットの後ろ(緑の背景の所)にだけ描きます。

第136話の2コマ目と3コマ目。3コマ目は床なしで描いた絵に、2コマ目と同じ形の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

06_2_spec.py.txt8,910字・64行・python
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

07_spec.py.txt6,283字・53行・python
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話の仕上がりと、サイト・英語版・表紙

仕上げた第134〜137話の4コマ。上から第134話、第135話、第136話、第137話

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(米国東部時間)
13411/28 19:301/27 15:00
13511/29 19:301/28 11:00
13611/30 20:001/28 15:00
13712/1 20:001/29 11:00

r2r2r-content への commit は c3d6068 と 28e6d00 です。ショートの X の文の型(SHORT_ep13X の x_template)も同じときに置きました。

Gemini に描かせた枚数と費用

各話の 生成/ フォルダにある、Gemini が返した絵の数を数えました(プログラムで加工した絵は除く)。費用は1枚約10円で見積もった値です。

話描いた枚数費用の見積もり枚数が増えた理由
13441枚約410円
1回目を描いたあと、カメラワークを組み直して描き直した
13529枚約290円
配膳車の形と構成を直して描き直した
13674枚約740円床の線を4回直した
13712枚約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話が残っています。