---
title: "4コママンガ『ハコブくん』を Gemini API のバッチで描く ― 5話の下書きが3分で届いた"
slug: hakobu-4koma-gemini-batch
date: 2026-11-05
day: 448
meta_description: "4コママンガ『ハコブくん』の絵は、Gemini API のバッチ（まとめて頼む半額の頼み方）で5話40枚を頼むと3分ほどで全部返り、36枚をそのまま使えた。第138〜142話を日本語・英語・ショートまで出した。"
tags: [4コママンガ, ハコブくん, 縦長マンガ, 画像生成AI, Gemini, バッチ処理, YouTubeショート, AI活用]
ai_consult: true
series: "ひとり研究所の裏側"
---

**4コママンガ『ハコブくん』の絵を Google の Gemini API のバッチ（何枚もの頼みを1つにまとめて送る、半額の頼み方）で頼むと、5話・40枚の下書きが約3分で全部返り、そのうち36枚をそのまま使えた。** 描き直しが要ったのは4コマだけで、それは今までどおり1枚ずつ頼んで直しました。

この形に変えて、変わったことは3つです。

- 費用：今回の5話で Gemini に描かせた絵は67枚、見積もりで約460円（1話あたり約90円）。前の第134〜137話は4話で156枚・約1,500円（1話あたり約390円）だった。下書きを半額のバッチに回したことと、1コマの候補を3〜5枚から2枚に減らしたことの両方が効いている
- 待ち時間：バッチは「最大24時間で返す」と案内されているが、40枚が約3分で返った。夜に頼んで朝に見る前提だったのが、その場で選べる
- 手間：描き直しの指示は、5話をまとめて見てから出せる。何話ぶんの下書きでも、頼むのは1つのコマンドで済む

『ハコブくん』は、病院で薬や物を運ぶロボット・ハコブくんと、看護師さんたちの4コママンガです。実際の病院でロボットを使ったときに起きたことを、調べた報告をもとに毎話1つ描いています（pixelstomotors.com/manga ）。絵は1コマずつ、縦長（縦横比 4:5）で [Gemini の画像生成 API](https://ai.google.dev/gemini-api/docs/image-generation)（文章と参考の絵を渡すと絵を返す仕組み）に描かせています。

困っていたのは費用でした。正確な絵は、だいたい一発では上がってきません。腕が3本になる、ロボットの大きさが変わる、セリフの字が崩れる。そのたびに描き直すので、前の4話では1話あたり30〜70枚描かせていました。描き直しは避けられない。それなら、最初の下書きを安く大量に作り、だめなコマだけを直す。そのために使ったのが、Gemini API の[バッチモード](https://ai.google.dev/gemini-api/docs/batch-mode)です。

【画像】LOG448_01_ep141_batch_candidates.jpg 第141話の下書き。上下の段が同じコマの2枚の候補で、バッチで一度に返ってきたもの

やったことの全体は次のとおりです。

| やったこと | なぜ | 結果 |
|---|---|---|
| バッチで頼む道具 vbatch.py を作った | 下書きを半額で、何話もまとめて頼むため | 1コマ1枚の試しで、ふつうの頼み方と同じ絵柄になった |
| 第138〜142話の指示（spec）を、プロットからカメラワーク付きで書いた | 5話を一度に頼むため | 5話・20コマ |
| 1コマ2枚・40枚をバッチで頼んだ | 候補を2枚に減らして費用を下げる | 約3分で40枚返り、36枚を使えた |
| 描き直しは1枚ずつ（ふつうの頼み方） | 直しはすぐ見たい | 第140話3コマ目・第141話3・4コマ目を直した |
| サイト（日本語・英語）・表紙・ショート5本・SNS の予約 | いつもの公開の流れ | 5話とも公開。ショートは YouTube にアップロード待ち |

【図解】LOG448_batch_cost.html

効果の数字について断っておきます。枚数・トークン数・返ってくるまでの時間は、API の返事と記録に残った実際の値です。円の額は、1枚約11円（ふつうの頼み方の実績・LOG410）と、バッチはその半額という Google の案内から出した見積もりで、次の請求で確かめます。


## 作業ログ ― Gemini API のバッチで下書きを頼む道具を作った

### Gemini API のバッチモードは何か

[バッチモード](https://ai.google.dev/gemini-api/docs/batch-mode)は、たくさんの頼みを1つのファイルにまとめて送り、あとでまとめて結果を受け取る頼み方です。すぐに返事が要らない代わりに、料金がふつうの頼み方の半分になります。

| この記事で使う言葉 | 意味 |
|---|---|
| ふつうの頼み方 | 1枚ずつ頼んで、その場で絵が返る（generateContent）。今までのやり方 |
| バッチ | 頼みを JSONL（1行に1つの頼みを書いたファイル）にまとめて送る。返りは最大24時間、48時間たつと取り消される |
| Files API | 絵やファイルを先に Google に上げておき、頼みの中では住所（fileUri）だけで指す仕組み。上げたファイルは48時間で消える |
| 下書き | バッチで頼んだ1コマ2枚の候補。ここから選び、だめなコマだけ描き直す |

マンガの1コマを頼むときは、プロンプトの文と一緒に、キャラの設定画（三面図）や大きさの並び図など、参考の絵を何枚も付けます。ふつうの頼み方では絵を毎回中に埋め込んで送っていましたが、バッチで40枚分を埋め込むとファイルが重くなります。そこで、参考の絵は Files API に一度だけ上げて、44時間のうちは同じ絵を使い回すようにしました。

### 1コマの試しで、ふつうの頼み方と同じ絵になるかを確かめた

最初に、第137話の1コマ目だけをバッチで頼みました。左がふつうの頼み方で前に描いた絵、右がバッチの絵です。参考の絵が効いているか（キャラの髪型・服・ロボットの形がそろうか）を見ました。入力のトークンのうち絵の分は、ふつうの頼み方と同じ1,290でした。参考の絵が同じように読まれています。

```
$ python3 vbatch.py submit 137 -n 1 -p 1 && sleep 20 && python3 vbatch.py status
第137話 プロンプトを作った
頼んだ: batches/6pew97xg467pd0ru7671caw3oj2m1218r4qz 枚数 1 （第137話・1コマ1枚）
20261105_090929 batches/6pew97xg467pd0ru7671caw3oj2m1218r4qz 第137話 1 枚 BATCH_STATE_RUNNING {"requestCount": "1", "pendingRequestCount": "1"}

（約1分後）
20261105_090929 batches/6pew97xg467pd0ru7671caw3oj2m1218r4qz 第137話 1 枚 BATCH_STATE_SUCCEEDED {"requestCount": "1", "successfulRequestCount": "1"}

$ python3 vbatch.py fetch
batches/6pew97xg467pd0ru7671caw3oj2m1218r4qz 回収 1 枚・画像なし 0 ・入力トークン 3465 ・出力トークン 1860
```

```
ふつうの頼み方 第137話_P1_20261105_064605.json {"promptTokenCount": 3342, "candidatesTokenCount": 1615, "totalTokenCount": 4957, "promptTokensDetails": [{"modality": "TEXT", "tokenCount": 2052}, {"modality": "IMAGE", "tokenCount": 1290}], "candidatesTokensDetails": [{"modality": "IMAGE", "tokenCount": 1120}], "serviceTier": "standard"}
バッチ         第137話_P1_20261105_091231b1.json {"promptTokenCount": 3465, "candidatesTokenCount": 1860, "totalTokenCount": 5325, "promptTokensDetails": [{"modality": "TEXT", "tokenCount": 2175}, {"modality": "IMAGE", "tokenCount": 1290}], "candidatesTokensDetails": [{"modality": "IMAGE", "tokenCount": 1120}], "serviceTier": "SERVICE_TIER_STANDARD"}
```

（時刻は Mac の中の作業環境の時計で、日本時間より9時間遅れています。以下の記録も同じです。）

### vbatch.py（バッチで頼む・状態を見る・回収する道具）

置き場所は iCloud の `ハコブくん/4コママンガ作成データ/道具/vbatch.py`。プロンプトの組み立ては、今までの vbuild.py をそのまま使います。回収した絵は、ふつうの頼み方と同じ名前の形（`第138話_P1_{時刻}b1.jpg`）で各話の 生成/ に置くので、大きさを測る・選ぶ・仕上げる道具はそのまま動きます。

```python
#!/usr/bin/env python3
"""縦長4コマを Gemini API のバッチで頼む（2026-11-05 エイイチ「Gemini 代を節約。バッチで多めの下書きを何話も一気に頼んでから修正」）

使い方（道具/ で動かす）:
  python3 vbatch.py submit 138 139 140 [-n 2] [-p 1 3]   … 話ごとに prompts を作り、1コマ n 枚をまとめて頼む（-p でコマを絞る）
  python3 vbatch.py status                                 … 頼んだバッチの状態
  python3 vbatch.py fetch                                  … 終わったバッチを回収し、各話の 生成/ に置く（ふつうの gen と同じ名前の形）
直しは今までどおり vbuild.py の gen（-n 1）でその場で頼む。

- 料金はふつうの頼み方の約半分。返りは最大24時間（48時間で失効）
- 参照画像は Files API に上げて fileUri で指す（同じ絵は48時間のうち使い回す。記録は _batch/files.json）
- 頼んだ中身と結果の記録は 道具/_batch/ に残す
"""
import sys, os, json, time, datetime, mimetypes, urllib.request, urllib.error, base64, glob
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import vbuild as V

API = "https://generativelanguage.googleapis.com"
BD = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_batch")
os.makedirs(BD, exist_ok=True)
FILES = BD + "/files.json"
JOBS = BD + "/jobs.json"
KEY = next(open(p).read().strip() for p in V.KEYS if os.path.exists(p))

def call(method, url, body=None, headers=None, raw=False, timeout=300):
    h = {"x-goog-api-key": KEY}
    h.update(headers or {})
    data = body if isinstance(body, (bytes, type(None))) else json.dumps(body).encode()
    if data is not None and "Content-Type" not in h: h["Content-Type"] = "application/json"
    req = urllib.request.Request(url, data=data, headers=h, method=method)
    for attempt in range(4):
        try:
            r = urllib.request.urlopen(req, timeout=timeout)
            return r if raw else json.load(r)
        except urllib.error.HTTPError as e:
            msg = e.read().decode()[:1500]
            if e.code in (429, 500, 503) and attempt < 3:
                time.sleep(10 * (attempt + 1)); continue
            raise SystemExit("失敗 %s %s\n%s" % (e.code, url.split("?")[0], msg))

def upload(path, mime, name):
    """Files API に上げる（resumable）。files/xxx の名前と uri を返す"""
    data = open(path, "rb").read()
    r = call("POST", API + "/upload/v1beta/files", {"file": {"display_name": name[:120]}}, raw=True,
             headers={"X-Goog-Upload-Protocol": "resumable", "X-Goog-Upload-Command": "start",
                      "X-Goog-Upload-Header-Content-Length": str(len(data)), "X-Goog-Upload-Header-Content-Type": mime})
    up = r.headers["X-Goog-Upload-URL"]
    info = call("POST", up, data, headers={"Content-Length": str(len(data)), "X-Goog-Upload-Offset": "0",
                                           "X-Goog-Upload-Command": "upload, finalize", "Content-Type": mime})
    return info["file"]

def ref_uri(path, cache):
    """参照画像の fileUri。中身が同じで上げてから44時間以内なら使い回す"""
    st = os.stat(path); k = os.path.abspath(path)
    c = cache.get(k)
    if c and c["mtime"] == st.st_mtime and time.time() - c["at"] < 44 * 3600:
        return c["uri"], c["mime"]
    mime = mimetypes.guess_type(path)[0] or "image/png"
    f = upload(path, mime, os.path.basename(path))
    cache[k] = {"uri": f["uri"], "name": f["name"], "mime": mime, "mtime": st.st_mtime, "at": time.time()}
    json.dump(cache, open(FILES, "w"), ensure_ascii=False, indent=0)
    return f["uri"], mime

def make_prompts(ep, m):
    d = V.epdir(ep); os.makedirs(d + "/生成", exist_ok=True)
    for i, p in enumerate(m.PANELS, 1):
        p["_guide"] = d + "/生成/_guide_P%d.png" % i
        txt, refs = V.build(p, ep)
        open(d + "/第%d話_P%d_プロンプト.txt" % (ep, i), "w", encoding="utf-8").write(txt)
        json.dump(refs, open(d + "/第%d話_P%d_refs.json" % (ep, i), "w"), ensure_ascii=False, indent=0)
    for k, w in getattr(m, "WINDOWS", {}).items():
        txt, refs = V.build_window(w)
        open(d + "/第%d話_P%d_小窓_プロンプト.txt" % (ep, k), "w", encoding="utf-8").write(txt)
        json.dump(refs, open(d + "/第%d話_P%d小窓_refs.json" % (ep, k), "w"), ensure_ascii=False, indent=0)

def submit(eps, n, which):
    cache = json.load(open(FILES)) if os.path.exists(FILES) else {}
    stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
    lines, index = [], {}
    for ep in eps:
        m = V.load(ep); make_prompts(ep, m)
        for prompt_path, out_head, refs, aspect in V.jobs(ep, m, which):
            parts = []
            for label, path in refs:
                uri, mime = ref_uri(path, cache)
                parts.append({"text": "Reference image — %s:" % label})
                parts.append({"fileData": {"mimeType": mime, "fileUri": uri}})
            parts.append({"text": open(prompt_path, encoding="utf-8").read()})
            req = {"contents": [{"role": "user", "parts": parts}],
                   "generationConfig": {"responseModalities": ["IMAGE"], "imageConfig": {"aspectRatio": aspect, "imageSize": "1K"}}}
            for c in range(1, n + 1):
                key = "%s#%d" % (os.path.relpath(out_head, V.BASE), c)
                lines.append(json.dumps({"key": key, "request": req}, ensure_ascii=False))
                index[key] = {"prompt": prompt_path, "refs": ["%s=%s" % (a, b) for a, b in refs]}
        print("第%d話 プロンプトを作った" % ep)
    jl = BD + "/%s_input.jsonl" % stamp
    open(jl, "w", encoding="utf-8").write("\n".join(lines) + "\n")
    f = upload(jl, "application/jsonl", "manga_batch_%s" % stamp)
    res = call("POST", API + "/v1beta/models/%s:batchGenerateContent" % V.MODEL,
               {"batch": {"display_name": "manga_%s" % stamp, "input_config": {"file_name": f["name"]}}})
    name = res.get("name") or res.get("metadata", {}).get("name")
    jobs = json.load(open(JOBS)) if os.path.exists(JOBS) else []
    jobs.append({"name": name, "stamp": stamp, "eps": eps, "n": n, "count": len(lines), "input": jl, "index": index, "fetched": False})
    json.dump(jobs, open(JOBS, "w"), ensure_ascii=False, indent=1)
    print("頼んだ:", name, "枚数", len(lines), "（第%s話・1コマ%d枚）" % ("・".join(map(str, eps)), n))

def get(name):
    return call("GET", API + "/v1beta/" + name)

def state_of(s):
    md = s.get("metadata", {})
    return md.get("state") or s.get("state"), md.get("batchStats") or s.get("batchStats") or {}

def status():
    jobs = json.load(open(JOBS)) if os.path.exists(JOBS) else []
    for j in jobs:
        if j.get("fetched"): continue
        st, stats = state_of(get(j["name"]))
        print(j["stamp"], j["name"], "第%s話" % "・".join(map(str, j["eps"])), j["count"], "枚", st, json.dumps(stats, ensure_ascii=False))

def fetch():
    jobs = json.load(open(JOBS)) if os.path.exists(JOBS) else []
    for j in jobs:
        if j.get("fetched"): continue
        s = get(j["name"]); st, stats = state_of(s)
        if st not in ("JOB_STATE_SUCCEEDED", "BATCH_STATE_SUCCEEDED"):
            print(j["name"], st, "まだ回収できない"); continue
        resp = s.get("response", {})
        rf = resp.get("responsesFile") or s.get("metadata", {}).get("output", {}).get("responsesFile") or s.get("dest", {}).get("fileName")
        out = BD + "/%s_output.jsonl" % j["stamp"]
        r = call("GET", API + "/download/v1beta/%s:download?alt=media" % rf, raw=True, timeout=600)
        open(out, "wb").write(r.read())
        ok = ng = 0; tok_in = tok_out = 0
        fstamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        for line in open(out, encoding="utf-8"):
            if not line.strip(): continue
            o = json.loads(line); key = o.get("key", "")
            head, c = key.rsplit("#", 1)
            out_head = os.path.join(V.BASE, head)
            res = o.get("response") or {}
            img = None
            for cd in res.get("candidates", []):
                for p in cd.get("content", {}).get("parts", []):
                    if "inlineData" in p: img = p["inlineData"]
            base = "%s_%sb%s" % (out_head, fstamp, c)     # 例 第138話_P1_20261106_070000b1.jpg（latest・measure の _2* に合う）
            u = res.get("usageMetadata", {}); tok_in += u.get("promptTokenCount", 0); tok_out += u.get("candidatesTokenCount", 0)
            if img:
                ext = ".jpg" if "jpeg" in img["mimeType"] else ".png"
                open(base + ext, "wb").write(base64.b64decode(img["data"])); ok += 1
            else:
                ng += 1; print("画像なし", key, json.dumps(o.get("error") or res.get("candidates", [{}])[0].get("finishReason"), ensure_ascii=False)[:300])
            for cd in res.get("candidates", []):
                for p in cd.get("content", {}).get("parts", []): p.pop("inlineData", None)
            meta = dict(j["index"].get(key, {}), model=V.MODEL, batch=j["name"], response=res, error=o.get("error"))
            json.dump(meta, open(base + ".json", "w", encoding="utf-8"), ensure_ascii=False, indent=1)
        j["fetched"] = True; j["result"] = {"ok": ok, "ng": ng, "input_tokens": tok_in, "output_tokens": tok_out, "fetched_at": fstamp}
        json.dump(jobs, open(JOBS, "w"), ensure_ascii=False, indent=1)
        print(j["name"], "回収", ok, "枚・画像なし", ng, "・入力トークン", tok_in, "・出力トークン", tok_out)

if __name__ == "__main__":
    a = sys.argv[1:]
    if not a: print(__doc__); sys.exit()
    cmd, rest = a[0], a[1:]
    if cmd == "submit":
        n = 2; which = []
        if "-n" in rest: k = rest.index("-n"); n = int(rest[k + 1]); rest = rest[:k] + rest[k + 2:]
        if "-p" in rest: k = rest.index("-p"); which = rest[k + 1:]; rest = rest[:k]
        submit([int(x) for x in rest], n, which)
    elif cmd == "status": status()
    elif cmd == "fetch": fetch()
```

- ねらい：描き直しが避けられない前提で、最初の下書きを半額で何話もまとめて作る
- 変わったこと：1コマの候補を3〜5枚から2枚に減らし、下書きはバッチ、直しはふつうの頼み方、と分けた。vbuild.py の gen も、1コマ2枚が既定になった（描き直しは `-n 1`）
- 見込める効果：1話あたりの Gemini 代が、前の4話の約390円から約90円になった（見積もり）。次の請求で、バッチの分が本当に半額だったかを確かめる


## 第138〜142話の5話を、1つのバッチで40枚頼んだ

### プロットとカメラワーク

5話のプロットは、スプレッドシートの「4コママンガプロット」にある第138〜142話です。どれも、実際の病院でロボットを使ったときの報告をもとにしています。spec（各コマの人物・カメラ・吹き出し・場面を書いた指示）は、前の LOG447 で決めたカメラワークの決まりどおり、コマごとにショットの大きさ・カメラの位置・高さ（cm）を書きました。

| 話 | 題 | もとにした報告 | 1話のカメラ |
|---|---|---|---|
| 138 | 柱のわきで待つ | 神奈川県の病院での案内ロボットの実証（受付前の混雑で動けず、出発位置を変えた） | ハコブくん目線の背中なめ → 肩なめと横切る人の影 → 槙原さんの顔に寄る → 低い位置から柱のわき |
| 139 | 扉の向こう | トヨタの病院の搬送ロボットの開発（扉の向こうから来る物を前もって知れない） | 背中なめの扉 → 低い位置の横から鼻先 → 岩田さんの顔を手前に → 引きで扉の上のカメラ |
| 140 | 紙くず2つ | 米NBCニュースの報道（紙くず2枚を壁ぎわまで寄ってよけた） | 床すれすれの紙くず → 寺島さんの背中なめ → ハコブくんの画面を手前に → ハコブくん目線 |
| 141 | 隅っこ | Proof News の報道（エレベーターで職員を隅に押し込めた） | 背中なめのかご → 低い斜めから回る → 外の廊下から戸口 → 扉の脇 |
| 142 | ガラスの壁 | KQED の報道と神奈川県の実証（ガラス扉にぶつかる・ガラス壁の近くを走らない設定） | 引きのガラスの部屋 → 部屋の中から肩なめ → タブレットに寄る → 角の引き |

#### 第138話の spec.py（柱のわきで待つ）

```python
EP, TITLE = 138, "柱のわきで待つ"
# 2026-11-05 カメラワーク：P1 ハコブくん目線で見上げる森田さん（ハコブくんの頭なめ）／P2 森田さんの肩なめ、間を横切る人の影／P3 槙原さんの顔に寄る（頭をかく）／P4 低い位置から、柱のわきから出てくるハコブくんと森田さん
NOFRONT = "NOT a front-facing lineup. NOBODY faces the viewer directly."
PILLAR = ("LOBBY PILLAR", ["one plain thick round lobby column standing on the floor, plain white with light screentone. NO text, NO signs, NO posters on it"])
NOBADGE = ["Mr. Morita is a patient: he wears NO lanyard, NO name badge, NO ID card"]
PANELS = [
 dict(chars=["森田茂", "ハコブ"], mouth="NORMAL", no_yline=True, back=True, costume=NOBADGE,
      hscale_text=["Hakobukun is in the near foreground at the bottom-left, seen from BEHIND, cut by the left and bottom edges: x 0–42%, its top at about 60% of the image height",
                   "Mr. Morita stands about 1 m beyond it, knee-up, leaning slightly forward to talk to the robot: his head at about 24% of the image height, cut at the knees by the bottom edge",
                   "even in the foreground, Hakobukun's top edge stays BELOW the pit of Mr. Morita's stomach — his chest, shoulders and head are clearly ABOVE the robot. Hakobukun is only 115 cm tall, he is 163 cm"],
      camera=["SHOT SIZE: Hakobukun's back in the foreground; Mr. Morita knee-up", "CAMERA POSITION: just behind Hakobukun's rear-left corner, Hakobukun's point of view", "CAMERA HEIGHT: 100 cm, looking slightly UP at Mr. Morita", NOFRONT],
      band="4% and 20%",
      comp=["Mr. Morita at about 62% of the image width, body turned 30° to camera-left, smiling and looking down at the robot, one hand raised a little as he asks",
            "the bubble 「検査室まで頼めるかね」 at the upper left, its tail down to Mr. Morita"],
      bubbles=[("Mr. Morita", "検査室まで頼めるかね", ["検査室まで", "頼めるかね"])],
      scene="Morning outpatient lobby, right in front of the main reception (not drawn). Hakobukun waits for its next guiding job. Elderly Mr. Morita walks up and politely asks it to take him to the examination room."),
 dict(chars=["森田茂", "ハコブ"], mouth="TROUBLED", no_yline=True,
      hscale_text=["Mr. Morita is in the near foreground at the RIGHT, waist-up, seen from his rear three-quarter, cut by the right and bottom edges: x 60–100%, his head at about 30% of the image height",
                   "Hakobukun is about 3 m away at the LEFT, facing three-quarter toward the camera: its top at about 44% and wheels at about 72% of the image height",
                   "between them, two featureless grey silhouettes of passers-by stride across from right to left, cut by the frame (only torsos and legs), blurred with speed lines. They have NO faces and are NOT characters"],
      camera=["SHOT SIZE: over-the-shoulder — Mr. Morita's back in the foreground, Hakobukun full body beyond", "CAMERA POSITION: behind Mr. Morita's right shoulder", "CAMERA HEIGHT: 150 cm", NOFRONT],
      band="3% and 22%",
      comp=["Hakobukun at about 30% of the image width, stopped, its screen showing a troubled face",
            "two bubbles: 「出発デキマセン」 at the upper left, tail down to Hakobukun; 「わしはもう、ずっと待っとるが」 at the upper right, tail down to Mr. Morita"],
      bubbles=[("Hakobukun", "出発デキマセン", ["出発デキマセン"]), ("Mr. Morita", "わしはもう、ずっと待っとるが", ["わしはもう、", "ずっと待っとるが"])],
      extra=[("EFFECT:", ["horizontal speed lines on the passing silhouettes only. A small stuck mark: three short lines bursting from the top of Hakobukun. NO letters"])],
      scene="Every time Hakobukun tries to set off, someone walks across in front of it, and it stops again. Mr. Morita, still waiting behind, grumbles that he has been waiting forever."),
 dict(chars=["槙原"], mouth="NORMAL", no_yline=True,
      hscale_text=["Makihara is LARGE in a close shot: head and shoulders fill the right two-thirds, x 30–95%, his face between 30% and 58% of the image height"],
      camera=["SHOT SIZE: close-up of Makihara's face and shoulders", "CAMERA POSITION: front-left of Makihara, 1 m away", "CAMERA HEIGHT: 130 cm, looking slightly up", NOFRONT],
      band="4% and 24%",
      comp=["Makihara, body turned 40° to camera-right, scratching the back of his head with one hand, an awkward embarrassed half-smile, one drop of sweat at his temple; he wears his black-rimmed glasses",
            "the bubble 「一番人が通る所に、待たせてたんだ…」 at the upper left, its tail to Makihara"],
      bubbles=[("Makihara", "一番人が通る所に、待たせてたんだ…", ["一番人が通る所に、", "待たせてたんだ…"])],
      extra=[("EFFECT:", ["a few soft vertical gloom lines behind his head. NO letters"])],
      scene="Engineer Makihara comes over to check, sees the crowd at the reception, and realises his own mistake: he made the robot wait in the busiest spot of the lobby."),
 dict(chars=["森田茂", "ハコブ"], mouth="HAPPY", no_yline=True, prop=PILLAR, costume=NOBADGE,
      hscale_text=["Hakobukun is in the near foreground at the LEFT, coming out from behind the pillar, three-quarter front view: x 8–50%, its top at about 34% and wheels at about 86% of the image height",
                   "Mr. Morita stands about 2 m beyond it at the RIGHT, full body, pleased: his head at about 22% and shoes at about 74% of the image height",
                   "the round pillar stands at the far left, cut by the left edge, x 0–14%, from the top to the bottom of the picture"],
      camera=["SHOT SIZE: Hakobukun large in the foreground; Mr. Morita full body beyond", "CAMERA POSITION: low, front-left of Hakobukun", "CAMERA HEIGHT: 80 cm", NOFRONT],
      band="3% and 18%",
      comp=["Mr. Morita at about 66% of the image width, body turned 30° to camera-left, smiling and following the robot",
            "the bubble 「森田サン、コチラデス」 at the upper middle, its tail down to Hakobukun"],
      bubbles=[("Hakobukun", "森田サン、コチラデス", ["森田サン、", "コチラデス"])],
      extra=[("EFFECT:", ["light speed lines trailing from behind the pillar to Hakobukun, and a small happy note mark above it. NO letters"])],
      scene="Hakobukun's waiting spot has been moved to beside a lobby pillar, out of the stream of people. When guidance is requested from the reception desk screen, Hakobukun rolls out from beside the pillar and leads Mr. Morita."),
]

# 2026-11-05 バッチの下書き（1コマ2枚）から選んだ。140 P3・141 P4 はふつうの頼み方で描き直した
PICK = {1: '第138話_P1_20261105_091906b1.jpg', 2: '第138話_P2_20261105_091906b1.jpg', 3: '第138話_P3_20261105_091906b1.jpg', 4: '第138話_P4_20261105_091906b2.jpg'}
```

#### 第139話の spec.py（扉の向こう）

```python
EP, TITLE = 139, "扉の向こう"
# 2026-11-05 カメラワーク：P1 ハコブくんの背中なめ、開き始める扉／P2 低い位置の横から、鼻先で向かい合うハコブくんとベッド／P3 岩田さんの顔を手前に、奥に高杉さんと止まったハコブくん／P4 引き：扉の上のカメラと、脇で待つハコブくん、通るベッド
NOFRONT = "NOT a front-facing lineup. NOBODY faces the viewer directly."
DOORS = ("DOUBLE DOORS", ["a pair of large plain hospital swinging double doors, each with a small round window (empty, no faces seen through it). NO text, NO signs"])
BED = "a plain hospital bed on casters with a white sheet and a pillow, Mr. Iwata lying on it under a blanket, his upper body slightly raised. NO text on the bed"
PANELS = [
 dict(chars=["ハコブ"], mouth="NORMAL", no_yline=True, back=True, prop=DOORS,
      hscale_text=["Hakobukun is in the near foreground at the bottom-left, seen from BEHIND, cut by the left and bottom edges: x 0–48%, its top at about 46% of the image height",
                   "the double doors stand about 1.5 m beyond it, filling the upper middle: x 26–92%, y 6–80%"],
      camera=["SHOT SIZE: Hakobukun's back in the foreground; the double doors beyond", "CAMERA POSITION: just behind Hakobukun's rear-left corner", "CAMERA HEIGHT: 110 cm"],
      nofront=False,
      comp=["the two doors are just starting to swing open toward the viewer, a narrow bright gap down the middle",
            "there is NO speech bubble in this picture"],
      sfx=[("ウィーン", "drawn at the upper right beside the door gap, at about 8–18% of the image height and 60–88% of the image width")],
      scene="The double doors leading to the operating wing. Hakobukun arrives right in front of them, and the automatic doors begin to open."),
 dict(chars=["高杉", "岩田", "ハコブ"], mouth="STOPPED", no_yline=True, not_standing=["岩田"],
      prop=("HOSPITAL BED", [BED]),
      hscale_text=["Hakobukun is in the near foreground at the LEFT, side-front three-quarter view, cut by the left and bottom edges: x 0–40%, its top at about 40% of the image height",
                   "the foot end of the bed is only 30 cm from Hakobukun's screen, coming from the RIGHT; Mr. Iwata lies on it, his upper body at x 45–80%, y 40–60%",
                   "Takasugi pushes the bed from its head end at the far right, waist-up above the bed: her head at about 26% of the image height, x 70–92%"],
      camera=["SHOT SIZE: side view of the robot and the bed nose to nose", "CAMERA POSITION: side-front, between them", "CAMERA HEIGHT: 70 cm, looking slightly up", NOFRONT],
      band="3% and 20%",
      comp=["Takasugi startled, eyes wide, both hands on the bed's head rail; Mr. Iwata blinking in surprise",
            "the bubble 「わっ！」 at the upper right, its tail down to Takasugi"],
      bubbles=[("Takasugi", "わっ！", ["わっ！"])],
      extra=[("EFFECT:", ["a burst of short shock lines between the robot's screen and the foot of the bed. NO letters"])],
      scene="Through the opened doors, Takasugi comes out pushing a bed with Mr. Iwata on it, and ends up nose to nose with Hakobukun."),
 dict(chars=["岩田", "高杉", "ハコブ"], mouth="TROUBLED", no_yline=True, not_standing=["岩田"],
      prop=("HOSPITAL BED", [BED]),
      hscale_text=["Mr. Iwata's face and shoulders are LARGE in the near foreground at the LEFT, lying on his pillow, head turned toward the camera at three-quarter: x 0–55%, y 50–95%",
                   "Takasugi stands behind the bed's head end at the right, chest-up, gripping the rail: her head at about 28% of the image height, x 58–85%",
                   "Hakobukun is about 2 m away beyond the foot of the bed, in the middle: its top at about 30% and its bottom at about 56% of the image height, x 18–42%"],
      camera=["SHOT SIZE: Mr. Iwata's face close in the foreground; Takasugi and Hakobukun beyond", "CAMERA POSITION: beside the head of the bed", "CAMERA HEIGHT: 150 cm", NOFRONT],
      band="3% and 22%",
      comp=["Mr. Iwata deadpan, eyes half-closed, looking sideways at the robot; Takasugi looking around for space behind her",
            "two bubbles: 「…どっちが下がるの？」 at the upper right, tail down to Mr. Iwata; 「扉ノ向コウハ、見エマセンデシタ」 at the upper left above Hakobukun, tail down to Hakobukun"],
      bubbles=[("Mr. Iwata", "…どっちが下がるの？", ["…どっちが", "下がるの？"]), ("Hakobukun", "扉ノ向コウハ、見エマセンデシタ", ["扉ノ向コウハ、", "見エマセンデシタ"])],
      extra=[("EFFECT:", ["a few drifting dots (silence ellipsis) between the bed and the robot. NO letters"])],
      scene="Hakobukun stops and looks for a place to back into, but neither the bed nor the robot can move. Mr. Iwata, flat on his back, asks who is going to back up. Hakobukun explains it could not see beyond the door."),
 dict(chars=["高杉", "岩田", "ハコブ"], mouth="HAPPY", no_yline=True, not_standing=["岩田"],
      prop=("DOORS, CAMERA AND BED", ["the same pair of plain double doors, now open, with one small dome camera mounted on the wall just above the door frame, three small curved signal arcs from the camera toward Hakobukun",
                                      BED, "NO text anywhere"]),
      hscale_text=["the open double doors fill the upper half: x 10–80%, y 4–62%; the small dome camera sits above the frame at about x 45%, y 6%",
                   "Hakobukun waits against the wall at the RIGHT, beside the doors, three-quarter front view: its top at about 46% and wheels at about 78% of the image height, x 66–92%",
                   "Takasugi pushes the bed through the doorway at the left-centre: her head at about 26% of the image height; the bed passes from x 8% to 60%, y 52–82%"],
      camera=["SHOT SIZE: wide establishing shot of the doorway", "CAMERA POSITION: front-left, a few metres back", "CAMERA HEIGHT: 150 cm", NOFRONT],
      band="3% and 18%",
      comp=["Takasugi relaxed, nodding to the robot as she passes; Mr. Iwata gives a small wave from the bed",
            "the bubble 「向コウカラ、ベッドガ来マス。待チマス」 at the upper right, its tail down to Hakobukun"],
      bubbles=[("Hakobukun", "向コウカラ、ベッドガ来マス。待チマス", ["向コウカラ、", "ベッドガ来マス。", "待チマス"])],
      scene="A camera has been installed above the door, sending a view of the far side to Hakobukun. When a bed comes, Hakobukun now waits beside the door, and Takasugi wheels Mr. Iwata through without a stop."),
]

# 2026-11-05 バッチの下書き（1コマ2枚）から選んだ。140 P3・141 P4 はふつうの頼み方で描き直した
PICK = {1: '第139話_P1_20261105_091906b1.jpg', 2: '第139話_P2_20261105_091906b1.jpg', 3: '第139話_P3_20261105_091906b2.jpg', 4: '第139話_P4_20261105_091906b1.jpg'}
```

#### 第140話の spec.py（紙くず2つ）

```python
EP, TITLE = 140, "紙くず2つ"
# 2026-11-05 カメラワーク：P1 床すれすれ、手前に大きな紙くず2つ・奥で止まるハコブくん／P2 寺島さんの背中なめ、壁ぎわを大回りするハコブくん／P3 ハコブくんの画面を手前に大きく、紙くずを拾ってゴミ箱へ投げる寺島さん／P4 ハコブくん目線、腰に手の寺島さん
NOFRONT = "NOT a front-facing lineup. NOBODY faces the viewer directly."
PAPER = "two small crumpled balls of white paper, each about the size of a fist, plain, NO writing"
PANELS = [
 dict(chars=["ハコブ"], mouth="STOPPED", no_yline=True,
      prop=("PAPER BALLS", [PAPER + ", LARGE in the foreground at the bottom, x 20–70%, y 72–96%"]),
      hscale_text=["Hakobukun is about 1.5 m beyond the paper balls, three-quarter front view: its top at about 22% and wheels at about 70% of the image height, x 30–74%"],
      extra=[("HAKOBUKUN HEAD:", ["its screen is set FLUSH into the front of its rounded upper tower, exactly as on the orthographic sheet — the screen does NOT stick out, it is NOT a separate box, and there is NO neck"]), ("EFFECT:", ["a sharp exclamation burst (three short lines) above Hakobukun. NO letters"])],
      camera=["SHOT SIZE: the two paper balls huge in the foreground; Hakobukun full body beyond", "CAMERA POSITION: on the floor, in front-right of the paper", "CAMERA HEIGHT: 20 cm, looking up", NOFRONT],
      band="3% and 18%",
      comp=["Hakobukun stopped dead, its screen staring at the paper",
            "the bubble 「障害物ヲ、検知シマシタ」 at the upper right, its tail down to Hakobukun"],
      bubbles=[("Hakobukun", "障害物ヲ、検知シマシタ", ["障害物ヲ、", "検知シマシタ"])],
      scene="An evening corridor. Two crumpled balls of paper lie on the floor. Hakobukun stops right in front of them and announces it has detected an obstacle."),
 dict(chars=["寺島", "ハコブ"], mouth="TROUBLED", no_yline=True,
      prop=("PAPER BALLS", [PAPER + ", tiny on the floor in the middle distance"]),
      hscale_text=["Terashima is in the near foreground at the RIGHT, waist-up, seen from her rear three-quarter, cut by the right and bottom edges: x 58–100%, her head at about 28% of the image height",
                   "Hakobukun is about 3 m away at the LEFT, squeezed against the corridor wall (a plain vertical wall edge at the far left), creeping in a wide arc around the two tiny paper balls: its top at about 42% and wheels at about 68% of the image height, x 6–36%"],
      camera=["SHOT SIZE: over-the-shoulder — Terashima's back in the foreground, Hakobukun and the paper beyond", "CAMERA POSITION: behind Terashima's left shoulder", "CAMERA HEIGHT: 150 cm", NOFRONT],
      band="3% and 20%",
      comp=["the paper balls at about 46% of the image width, y 66–70%, far from the robot; Terashima's head turned 20° toward camera-left, deadpan, she wears her round glasses",
            "the bubble 「…紙よ、それ」 at the upper middle, its tail down to Terashima"],
      bubbles=[("Terashima", "…紙よ、それ", ["…紙よ、それ"])],
      sfx=[("ジリジリ", "small, next to Hakobukun's wheels, at about 70–78% of the image height and 20–40% of the image width")],
      extra=[("EFFECT:", ["tiny short motion strokes behind Hakobukun showing it inching along. NO path line on the floor"])],
      scene="Hakobukun creeps right up to the wall, inch by inch, to make a huge detour around the paper. Terashima, waiting behind it, says flatly that it is just paper."),
 dict(chars=["寺島", "ハコブ"], mouth="STOPPED", no_yline=True,
      prop=("PAPER AND BIN", ["one crumpled paper ball flying in an arc from Terashima's hand into a plain small trash bin; the other ball is in her other hand. NO text on the bin"]),
      hscale_text=["Hakobukun's screen-head is LARGE in the near foreground at the bottom-left, cut by the left and bottom edges: x 0–46%, y 56–100%, its blank flat-mouthed face visible",
                   "Terashima stands about 1.5 m beyond it at the right, knee-up, bending a little: her head at about 26% of the image height, x 50–86%",
                   "the trash bin at about x 30–42%, y 50–62%, beyond the robot's head"],
      camera=["SHOT SIZE: Hakobukun's screen close in the foreground; Terashima knee-up beyond", "CAMERA POSITION: just beside Hakobukun's screen, slightly behind", "CAMERA HEIGHT: 110 cm", NOFRONT],
      band="3% and 22%",
      comp=["two bubbles: 「拾ったほうが早いわ」 at the upper right, well inside the picture with at least 8% of the width empty to its right, tail down to Terashima; 「何デアルカ、分カリマセンデシタ」 at the upper left, tail down to Hakobukun"],
      bubbles=[("Terashima", "拾ったほうが早いわ", ["拾ったほうが", "早いわ"]), ("Hakobukun", "何デアルカ、分カリマセンデシタ", ["何デアルカ、", "分カリマセンデシタ"])],
      extra=[("EFFECT:", ["a dotted arc line following the flying paper ball into the bin. NO letters"])],
      scene="Terashima picks up the paper balls and tosses them into the trash bin, remarking that picking them up is faster. Hakobukun admits it could not tell what the things were."),
 dict(chars=["寺島", "ハコブ"], mouth="HAPPY", no_yline=True,
      prop=("PAPER BALL", ["one small crumpled ball of white paper on the floor between them, plain, NO writing"]),
      hscale_text=["Hakobukun is in the near foreground at the LEFT, seen from its rear three-quarter so its screen faces Terashima, cut by the left and bottom edges: x 0–40%, its top at about 52% of the image height",
                   "Terashima stands about 1.5 m beyond it, full body: her head at about 20% and shoes at about 82% of the image height, x 48–80%",
                   "even in the foreground, Hakobukun's top edge stays BELOW the level of Terashima's chest — her shoulders and head are clearly ABOVE the robot. Hakobukun is only 115 cm tall, she is 160 cm"],
      camera=["SHOT SIZE: Hakobukun's shoulder in the foreground; Terashima full body", "CAMERA POSITION: behind Hakobukun's left side", "CAMERA HEIGHT: 100 cm, Hakobukun's point of view, looking up at Terashima", NOFRONT],
      back=True,
      band="3% and 20%",
      comp=["Terashima, one hand on her hip, smiling with a little shrug, about to bend for the paper; the paper ball at about 46% of the image width, y 84–88%",
            "two bubbles: 「紙カモシレマセン。確カメテクダサイ」 at the upper left, tail down to Hakobukun; 「最初からそう言えば、拾うわよ」 at the upper right, tail down to Terashima"],
      bubbles=[("Hakobukun", "紙カモシレマセン。確カメテクダサイ", ["紙カモシレマセン。", "確カメテクダサイ"]), ("Terashima", "最初からそう言えば、拾うわよ", ["最初からそう言えば、", "拾うわよ"])],
      scene="After a settings change, Hakobukun now stops before anything it cannot identify and asks a nearby person. Terashima laughs: if it had said so from the start, she would have picked it up."),
]

# 2026-11-05 バッチの下書き（1コマ2枚）から選んだ。140 P3・141 P4 はふつうの頼み方で描き直した
PICK = {1: '第140話_P1_20261105_091906b2.jpg', 2: '第140話_P2_20261105_091906b1.jpg', 3: '第140話_P3_20261105_092019.jpg', 4: '第140話_P4_20261105_091906b1.jpg'}
```

#### 第141話の spec.py（隅っこ）

```python
EP, TITLE = 141, "隅っこ"
# 2026-11-05 カメラワーク：P1 ハコブくんの背中なめ、かごの奥に立つ高杉さん／P2 低い位置の斜めから、その場で回るハコブくんと隅に押し込まれる高杉さん／P3 外の廊下から、開いた扉をふさぐハコブくん（高杉さんは肩ごしにのぞく）／P4 扉の外で待つハコブくんを手前に、閉まりかける扉の中の高杉さん
NOFRONT = "NOT a front-facing lineup. NOBODY faces the viewer directly."
CAR = "a small plain hospital elevator car: plain back wall and side walls with light screentone, a handrail on the back wall, a simple button panel with blank round buttons. NO numbers, NO text, NO floor display"
PANELS = [
 dict(chars=["高杉", "ハコブ"], mouth="NORMAL", no_yline=True, back=True, prop=("ELEVATOR", [CAR, "the elevator doors are open, their edges at the left and right edges of the picture"]),
      hscale_text=["Hakobukun is in the near foreground at the bottom-left, seen from BEHIND as it rolls in, cut by the left and bottom edges: x 0–46%, its top at about 50% of the image height",
                   "Takasugi stands at the back wall of the car, about 1.5 m beyond it, at the right, full body: her head at about 22% and shoes at about 78% of the image height, x 56–80%"],
      camera=["SHOT SIZE: Hakobukun's back in the foreground; Takasugi full body at the back of the car", "CAMERA POSITION: just behind Hakobukun, in the open doorway", "CAMERA HEIGHT: 130 cm", NOFRONT],
      comp=["Takasugi, body turned 30° to camera-left, holding a plain clipboard, glancing at the robot coming in",
            "there is NO speech bubble in this picture"],
      scene="Inside an elevator. Takasugi is already standing at the back. Hakobukun rolls in and heads for its usual spot."),
 dict(chars=["高杉", "ハコブ"], mouth="NORMAL", no_yline=True, prop=("ELEVATOR", [CAR]),
      hscale_text=["Hakobukun is LARGE in the near foreground at the left-centre, turning on the spot, three-quarter view: x 4–62%, its top at about 30% and wheels at about 92% of the image height",
                   "Takasugi is squeezed into the back right corner of the car beyond it, knee-up: her head at about 18% of the image height, x 62–90%"],
      camera=["SHOT SIZE: Hakobukun large in the foreground; Takasugi pressed into the corner beyond", "CAMERA POSITION: low, front-left corner of the car", "CAMERA HEIGHT: 70 cm, looking up", NOFRONT],
      band="3% and 16%",
      comp=["Takasugi pressed flat against the walls with both hands up, clipboard held to her chest, alarmed",
            "the bubble 「ちょっと、ちょっと！」 at the upper right, its tail down to Takasugi"],
      bubbles=[("Takasugi", "ちょっと、ちょっと！", ["ちょっと、", "ちょっと！"])],
      extra=[("EFFECT:", ["curved rotation motion lines around the base of Hakobukun showing it spinning in place, and a few short panic lines around Takasugi. NO letters"])],
      scene="To turn around, Hakobukun spins on the spot. Its body sweeps across the tiny car and Takasugi is pushed into the far corner."),
 dict(chars=["高杉", "ハコブ"], mouth="NORMAL", no_yline=True,
      prop=("ELEVATOR", [CAR, "seen from the hall: the elevator doors are open, but the door opening is NARROW — only a little wider than Hakobukun"]),
      # 2026-11-05 エイイチ「これだとハコブくんは高杉さんの邪魔には見えない」→ ハコブくんが戸口の幅いっぱいをふさぎ、高杉さんはその真後ろで出られない。大きさはみぞおちの高さで書く
      hscale_text=["Hakobukun stands IN the doorway, centred, three-quarter front view, facing out into the hall: its body fills the WHOLE width of the door opening, wall to wall, NO gap on either side to walk through",
                   "Hakobukun: its top at about 46% and wheels at about 88% of the image height",
                   "Takasugi stands directly BEHIND Hakobukun, only half a step back, trapped inside the car: her lower body is hidden behind the robot; her head at about 24% of the image height",
                   "Hakobukun's top edge reaches only the pit of Takasugi's stomach — her chest, shoulders and head are clearly ABOVE the robot. It is NOT as tall as her chest or shoulders"],
      camera=["SHOT SIZE: the doorway — Hakobukun full body blocking it, Takasugi waist-up behind it", "CAMERA POSITION: outside in the hall, slightly front-right of the doorway", "CAMERA HEIGHT: 150 cm"],
      band="3% and 20%",
      comp=["the door frame edges at about 18% and 82% of the image width; Hakobukun fills between them",
            "Takasugi at about 55% of the image width behind the robot, leaning forward over its top, both hands pressed on the top of the robot, desperate, looking out at the hall she cannot reach",
            "two bubbles: 「降りたいの！」 at the upper right, tail down to Takasugi; 「ワタシハ、次ノ階デス」 at the upper left, tail down to Hakobukun"],
      bubbles=[("Takasugi", "降りたいの！", ["降りたいの！"]), ("Hakobukun", "ワタシハ、次ノ階デス", ["ワタシハ、", "次ノ階デス"])],
      extra=[("EFFECT:", ["a few sweat drops flying from Takasugi. NO letters"])],
      scene="The doors open at Takasugi's floor, but Hakobukun is parked right in the doorway, filling it completely, and she is stuck behind it. Hakobukun calmly says its floor is the next one."),
 dict(chars=["高杉", "ハコブ"], mouth="HAPPY", no_yline=True,
      prop=("ELEVATOR", [CAR, "seen from the hall: the elevator doors are fully open, the door frame around the picture's middle"]),
      # 2026-11-05 エイイチ「このセリフだとこの構成おかしくない？」→ 扉の前を空けて止まる形に変更。3コマ目（戸口をふさぐ）と同じ向きで比べられるようにする
      hscale_text=["Hakobukun is parked INSIDE the car, pulled tight against the LEFT side wall, three-quarter front view: x 20–46%, its top at about 46% and wheels at about 80% of the image height",
                   "the right half of the doorway is clearly EMPTY — a wide open path from the car to the hall",
                   "Takasugi walks out through that open path at the right, full body, stepping into the hall toward camera-right: her head at about 24% and shoes at about 84% of the image height, x 52–76%",
                   "Hakobukun's top edge reaches only the pit of Takasugi's stomach — her chest, shoulders and head are clearly ABOVE the robot"],
      camera=["SHOT SIZE: the doorway — both full body", "CAMERA POSITION: outside in the hall, slightly front-right of the doorway (the same side as in the previous panel)", "CAMERA HEIGHT: 150 cm"],
      band="3% and 20%",
      comp=["the door frame edges at about 16% and 84% of the image width",
            "Takasugi relaxed, a small smile, glancing back at the robot as she walks out easily",
            "two bubbles: 「扉ノ前ハ、空ケテオキマス」 at the upper left, tail down to Hakobukun; 「それでいいの」 at the upper right, tail down to Takasugi"],
      bubbles=[("Hakobukun", "扉ノ前ハ、空ケテオキマス", ["扉ノ前ハ、", "空ケテオキマス"]), ("Takasugi", "それでいいの", ["それでいいの"])],
      extra=[("EFFECT:", ["a light motion line behind Takasugi's step. NO letters"])],
      scene="A new riding rule: when people are aboard, Hakobukun stops against the side wall and keeps the space in front of the doors free. At her floor, Takasugi walks straight out, and nods that this is how it should be."),
]

# 2026-11-05 バッチの下書き（1コマ2枚）から選んだ。140 P3・141 P4 はふつうの頼み方で描き直した
PICK = {1: '第141話_P1_20261105_091906b1.jpg', 2: '第141話_P2_20261105_091906b2.jpg', 3: '第141話_P3_20261105_092900.jpg', 4: '第141話_P4_20261105_093212.jpg'}
```

#### 第142話の spec.py（ガラスの壁）

```python
EP, TITLE = 142, "ガラスの壁"
# 2026-11-05 カメラワーク：P1 引き：ガラス張りの面談室と、廊下を来るハコブくん／P2 部屋の中から、大城さんの肩なめ、ガラスにコツンのハコブくん／P3 槙原さんのタブレットの画面と顔に寄る（下から）／P4 高い引き：ガラスから離れて角を曲がるハコブくん
NOFRONT = "NOT a front-facing lineup. NOBODY faces the viewer directly."
GLASS = "a floor-to-ceiling clear glass partition, shown ONLY by thin vertical frame lines and a few diagonal white shine streaks. NO text, NO stickers, NO signs on the glass"
PANELS = [
 dict(chars=["大城", "ハコブ"], mouth="NORMAL", no_yline=True, prop=("GLASS ROOM", [GLASS + "; behind it, a small meeting room with one plain table", "Oshiro stands BESIDE the table (never behind it), full body, arranging plain blank papers. NO writing"]),
      hscale_text=["the glass partition runs diagonally from the upper left to the right, x 20–100%",
                   "Oshiro is inside the room beyond the glass at the right, full body: her head at about 22% and shoes at about 66% of the image height, x 62–80%",
                   "Hakobukun is in the corridor in the foreground at the LEFT, three-quarter front view, rolling toward the corner: its top at about 40% and wheels at about 80% of the image height, x 6–40%"],
      camera=["SHOT SIZE: establishing wide shot of the corridor and the glass room", "CAMERA POSITION: front-left, down the corridor", "CAMERA HEIGHT: 150 cm", NOFRONT],
      comp=["there is NO speech bubble in this picture"],
      scene="A newly built consultation room. The whole wall between it and the corridor is glass. Inside, Oshiro is tidying documents. Hakobukun comes rolling down the corridor."),
 dict(chars=["大城", "ハコブ"], mouth="STOPPED", no_yline=True, prop=("GLASS", [GLASS + " between Oshiro and the robot"]),
      hscale_text=["Oshiro is in the near foreground at the RIGHT, inside the room, chest-up, turning her head over her shoulder: x 56–100%, her head at about 30% of the image height, cut by the right and bottom edges",
                   "Hakobukun is just beyond the glass, about 1.5 m away at the left, its screen pressed against the glass at an angle: its top at about 28% and wheels at about 76% of the image height, x 8–50%"],
      camera=["SHOT SIZE: over-the-shoulder from inside the room — Oshiro chest-up in the foreground, Hakobukun beyond the glass", "CAMERA POSITION: behind Oshiro's right shoulder", "CAMERA HEIGHT: 130 cm", NOFRONT],
      band="3% and 18%",
      comp=["Oshiro surprised, eyes wide, holding a few blank papers",
            "the bubble 「…何モ、アリマセンデシタ」 at the upper left, its tail down to Hakobukun"],
      bubbles=[("Hakobukun", "…何モ、アリマセンデシタ", ["…何モ、", "アリマセンデシタ"])],
      sfx=[("コツン", "just above the point where the robot's screen touches the glass, at about 22–30% of the image height and 26–46% of the image width")],
      extra=[("EFFECT:", ["a small impact star with short lines where the screen meets the glass. NO letters"])],
      scene="Trying to turn the corner, Hakobukun bumps into the glass wall with a tap, and reports that there was nothing there. Inside, Oshiro turns around."),
 dict(chars=["槙原"], mouth="NORMAL", no_yline=True,
      prop=("TABLET", ["Makihara holds a plain tablet toward the camera. Its screen shows a simple top-down map: a scatter of small dots tracing the walls, and a clear GAP with no dots where the glass wall is. NO text, NO numbers"]),
      hscale_text=["the tablet is LARGE in the lower foreground: x 6–64%, y 52–94%",
                   "Makihara is just behind it, chest-up, looking at the screen: his head at about 30% of the image height, x 40–86%"],
      camera=["SHOT SIZE: close shot of the tablet and Makihara's face", "CAMERA POSITION: low, front-left of Makihara", "CAMERA HEIGHT: 110 cm, looking up", NOFRONT],
      band="3% and 24%",
      comp=["Makihara serious and thoughtful, pointing at the gap on the screen with one finger; he wears his black-rimmed glasses",
            "the bubble 「センサーにはガラスが映ってない。ハコブくんには、ここに壁がないように見えてたんだ」 at the upper part, its tail down to Makihara"],
      bubbles=[("Makihara", "センサーにはガラスが映ってない。ハコブくんには、ここに壁がないように見えてたんだ", ["センサーには", "ガラスが映ってない。", "ハコブくんには、ここに", "壁がないように見えてたんだ"])],
      scene="Engineer Makihara checks the robot's records on a tablet. The sensor saw no glass at all; to Hakobukun, there simply was no wall there."),
 dict(chars=["大城", "ハコブ"], mouth="HAPPY", no_yline=True, prop=("GLASS ROOM", [GLASS + "; the small meeting room behind it"]),
      hscale_text=["the glass partition runs from the upper right down toward the left, x 0–70%",
                   "Hakobukun is in the corridor at the right-centre, rounding the corner in a wide curve well away from the glass, three-quarter front view: its top at about 38% and wheels at about 64% of the image height, x 44–70%",
                   "Oshiro is inside beyond the glass at the left, waist-up: her head at about 26% of the image height, x 10–34%"],
      camera=["SHOT SIZE: wide shot of the corner", "CAMERA POSITION: front-right, at the corner", "CAMERA HEIGHT: 160 cm", NOFRONT],
      band="3% and 18%",
      comp=["Oshiro smiling, giving a small wave through the glass",
            "the bubble 「見エナイ壁ハ、地図デ覚エマス」 at the upper right, its tail down to Hakobukun"],
      bubbles=[("Hakobukun", "見エナイ壁ハ、地図デ覚エマス", ["見エナイ壁ハ、", "地図デ覚エマス"])],
      extra=[("EFFECT:", ["a light curved motion line behind Hakobukun showing its wide turn, keeping its distance from the glass. NO line on the floor"])],
      scene="A no-drive strip along the glass wall has been added to the robot's map (not drawn). Now Hakobukun turns the corner a little away from the glass, and Oshiro waves from inside."),
]

# 2026-11-05 バッチの下書き（1コマ2枚）から選んだ。140 P3・141 P4 はふつうの頼み方で描き直した
PICK = {1: '第142話_P1_20261105_091906b1.jpg', 2: '第142話_P2_20261105_091906b2.jpg', 3: '第142話_P3_20261105_091906b1.jpg', 4: '第142話_P4_20261105_091906b2.jpg'}
```

第138話と第140話の spec の、ハコブくんの大きさ・頭の形・吹き出しの位置を足した行は、あとで描き直そうとして足し、描き直しをやめたあとも残ったものです（下の「描き直しをやめたバッチ」）。選んだ絵（PICK）は、足す前の spec で描いたバッチの下書きです。

### 40枚のバッチは約3分で全部返った

```
$ python3 vbatch.py submit 138 139 140 141 142 -n 2 && sleep 5 && python3 vbatch.py status
第138話 プロンプトを作った
第139話 プロンプトを作った
第140話 プロンプトを作った
第141話 プロンプトを作った
第142話 プロンプトを作った
頼んだ: batches/265eg1lnyjrmekthe44y8lbypu6uxxotz4zz 枚数 40 （第138・139・140・141・142話・1コマ2枚）
20261105_091459 batches/265eg1lnyjrmekthe44y8lbypu6uxxotz4zz 第138・139・140・141・142話 40 枚 BATCH_STATE_RUNNING {"requestCount": "40", "pendingRequestCount": "40"}

（約3分後）
20261105_091459 batches/265eg1lnyjrmekthe44y8lbypu6uxxotz4zz 第138・139・140・141・142話 40 枚 BATCH_STATE_SUCCEEDED {"requestCount": "40", "successfulRequestCount": "40"}

$ python3 vbatch.py fetch
batches/265eg1lnyjrmekthe44y8lbypu6uxxotz4zz 回収 40 枚・画像なし 0 ・入力トークン 133576 ・出力トークン 67213
```

**バッチは最大24時間と案内されているのに、40枚が約3分で返りました。** 夜に頼んで朝に選ぶつもりでしたが、その場で選べます。混み具合で変わるはずなので、いつも3分で返るとは限りません。

### 2枚の候補から選んだ結果（20コマ中、描き直しは4コマ）

2枚を並べて、チェック項目（体の数・大きさ・ロボットの形・字の1字ずつ・台本にない『』）で選びました。2枚とも使えなかったのは次の2コマです。

| コマ | 2枚の候補で起きていたこと | 直し方 |
|---|---|---|
| 第140話 3コマ目 | 1枚は両方の吹き出しに台本にない『』、もう1枚は寺島さんの吹き出しに意味のない字（拾着に構後ら） | ふつうの頼み方で2枚描き直した |
| 第141話 4コマ目 | 2枚とも「乗リマス」が「乗りマス」（カタカナがひらがなに） | ふつうの頼み方で2枚描き直した（あとで場面ごと変えた） |

```
$ python3 vbuild.py 140 gen 3 -n 2 && python3 vbuild.py 141 gen 4 -n 2
頼む枚数 2 （1コマ 2 枚）
第140話_P3_20261105_092016.jpg 19秒
第140話_P3_20261105_092019.jpg 22秒
頼む枚数 2 （1コマ 2 枚）
第141話_P4_20261105_092041.jpg 22秒
第141話_P4_20261105_092040.jpg 20秒
```

ほかに、縦書きになった吹き出しが何コマかあります。チェック項目では軽い問題として通す決まりなので、そのまま使いました。

【画像】LOG448_02_ep138_142_panels.jpg 仕上げた第138〜142話の4コマ。上から第138話（柱のわきで待つ）、第139話（扉の向こう）、第140話（紙くず2つ）、第141話（隅っこ）、第142話（ガラスの壁）


## 第141話の3・4コマ目を、見て直した

### 3コマ目：ハコブくんが大きすぎ、直すと今度は邪魔に見えなかった

並べた絵を見て、次の指摘がありました。

> ハコブくんが大きすぎる？

高杉さんは155cm、ハコブくんは115cmなので、ハコブくんは高杉さんの約74%（上の端がみぞおち）が正しい大きさです。下書きの絵では約87%あり、肩の近くまで届いていました。原因は spec の書き方で、2人が同じ床に並んで立つコマなのに、並び図どおりに大きさを決める仕組み（vbuild.py の yline）を切って、文章で位置を書くだけにしていました。yline に戻して描き直すと、比は約81%になりました。ところが、その絵を見て次の指摘がありました。

> これだとハコブくんは高杉さんの邪魔には見えないね

大きさを比べやすくしようとして2人を横に並べたので、高杉さんがハコブくんの横をすっと通れそうに見えていたのです。最後は、ハコブくんを戸口の幅いっぱいに置き、高杉さんをその真後ろ（半歩奥）に立たせました。大きさは「ハコブくんの上の端が高杉さんのみぞおち」と文で書いて合わせています。

【画像】LOG448_03_ep141_p3_three_versions.jpg 第141話3コマ目の3つの版。左がバッチの下書き（ハコブくんが大きすぎる）、中が1回目の直し（大きさは合ったが横を通れそう）、右が採用した版（戸口をふさぎ、高杉さんは後ろで出られない）

- ねらい：めくる前のコマで、ハコブくんが邪魔をしていることが一目で分かるようにする
- 変わったこと：大きさを合わせる構図（横に並べる）から、話を見せる構図（戸口をふさぐ）に変え、大きさは体の部位で書いた
- 見込める効果：ふさぐ・はさまる話のコマで、同じ直しを繰り返さない（チェック項目の台帳に、ふさぐ話の置き方として1行足した）

### 4コマ目：セリフと構図が合っていなかった

> 4コマ目このセリフだとこの構成おかしくない？

もとの4コマ目は、高杉さんが開いたかごの中に立っていて、ハコブくんが外で「次ニ、乗リマス」と言う絵でした。これでは、次のかごを待つことも、「それでいいの」が何に向けた言葉かも伝わりません。セリフを変えて、3コマ目と同じ廊下側から撮る構図にしました。ハコブくんはかごの左の壁にぴったり寄って扉の前の右半分を空け、高杉さんはそこを通って降ります。セリフは「扉ノ前ハ、空ケテオキマス」と「それでいいの」です。スプレッドシートの第141話の4コマ目も同じ中身に書き換えました。

【画像】LOG448_04_ep141_p4_before_after.jpg 第141話4コマ目の前と後。左は高杉さんがかごの中に立つだけの絵、右はハコブくんが壁に寄って扉の前を空け、高杉さんが降りていく絵

### 描き直しをやめたバッチ（10枚）

ほかの4話についても、自分のチェックで気になった所（ハコブくんが手前で大きく見える・画面が胴から飛び出す・吹き出しが端で切れる・患者さんが名札を下げている）を挙げて、直すかを聞きました。返事は「他はOK」で、これは直さなくてよいという意味でした。直すという意味に読み違えて、第138話の2コマと第140話の3コマをバッチで頼んでしまい、取り消そうとした時点で10枚とも描き終わっていました。この10枚（約55円）は取り込まず、使っていません。

```
batches/cv6yfgklt9812g24xkzaexy0v7xkmi985mbx BATCH_STATE_SUCCEEDED
batches/8r6ale5emqpg4yq83rpao86wuhslyaijccyr BATCH_STATE_SUCCEEDED
138 PICK を戻した
140 PICK を戻した
```

### Gemini に描かせた枚数と費用の見積もり

| 頼んだもの | 頼み方 | 枚数 | 見積もり |
|---|---|---|---|
| 1コマの試し（第137話） | バッチ | 1 | 約6円 |
| 5話の下書き | バッチ | 40 | 約220円 |
| 描き直し（第140話3、第141話3・4） | ふつう | 10 | 約110円 |
| 表紙の吹き出し消し | ふつう | 6 | 約66円 |
| 描き直しをやめた分 | バッチ | 10 | 約55円 |
| 計 | | 67 | 約460円 |

1話あたり約90円です。前の第134〜137話は4話で156枚・約1,500円（1話あたり約390円）でした。


## サイト・英語版・表紙・ショートに出した

### 表紙

表紙は、見せ場のコマから吹き出しを消し、題名を重ねて作ります（LOG413）。吹き出しは Gemini の編集で消しています。第138話は、消すときの指示に「小窓の中の吹き出しも消す」という一文があったため、小窓のないコマに Gemini が小窓を描き足しました。その一文を抜いた指示で描き直しています。

【画像】LOG448_05_covers.jpg 第138〜142話の表紙。吹き出しを消したコマの上に、第N話と題名を重ねている

#### cover_edit_latest.py（表紙用に吹き出しを消す道具。PICK を第138〜142話にした）

```python
"""縦長4コマの表紙用：選んだコマから吹き出しと字を消す（Gemini の API・2026-10-31）"""
import sys, os, concurrent.futures
sys.path.insert(0, os.path.expanduser('~/mnt/PixelstoMotors/ハコブくん/4コママンガ作成データ/道具'))
import vbuild as V
PICK = {138:2,139:2,140:1,141:2,142:2}
NOEDIT = {133,128,116,111,101,99,76}
PROMPT = """Edit this manga panel. Remove EVERY speech bubble and thought bubble completely, including their tails and all the text inside them (ALSO the speech bubble inside the small square inset window in the corner: erase that bubble entirely so the inset shows only the person in the room). Where a bubble was, redraw what would be behind it: plain white background, or the continuation of the wall, hair, body or object that the bubble covered. Keep sound-effect lettering drawn directly on the art (like バチン, カラン, ジリリリ) as it is. Keep EVERYTHING else exactly the same: same characters, faces, eyes, poses, sizes, positions, clothes, robots, screentone, line style, black-and-white manga look, white background, 4:5 framing. Do not add any new text, bubble, border, inset window, portrait or object. Where the bubble was, leave plain white background (and continue the thin vertical line if it passed behind the bubble)."""
here = os.path.dirname(os.path.abspath(__file__))
open(here + '/cover_edit/_prompt.txt', 'w', encoding='utf-8').write(PROMPT)
eps = [int(a) for a in sys.argv[1:]] or [e for e in PICK if e not in NOEDIT]
def job(e):
    src = os.path.expanduser(f'~/mnt/PixelstoMotors/ハコブくん/4コママンガ作成データ/縦長/第{e}話/仕上げ/第{e}話_P{PICK[e]}_1080x1350.png')
    return V.gen_one(here + '/cover_edit/_prompt.txt', here + f'/cover_edit/ep{e}_P{PICK[e]}', [("the manga panel to edit", src)], "4:5")
with concurrent.futures.ThreadPoolExecutor(6) as ex:
    for r in ex.map(job, eps): print(r)
```

#### 第138話だけ使った、吹き出しを消す指示（_prompt138.txt）

```text
Edit this manga panel. Remove BOTH speech bubbles completely, including their tails and all the text inside them. Where a bubble was, leave plain white background. Keep EVERYTHING else exactly the same: same characters, faces, poses, sizes, positions, clothes, robot, the grey passer-by silhouettes, the small stuck mark lines above the robot, screentone, line style, black-and-white manga look, white background, 4:5 framing. Do NOT add anything: no inset window, no picture-in-picture, no frame, no new person, no text.
```

### 英語版

英語版は描き直さず、吹き出しの中の日本語を消して英語を組みます（LOG416 の en_manga.py）。`seed` は各吹き出しの中の1点で、今回は白い閉じた領域を探すプログラムで座標を出してから、どの吹き出しかを見て当てました。第140話3コマ目の寺島さんの吹き出しは、コマの右の端で切れていて吹き出しとして見つけられないので、四角を指定して手書き風の字（`signs` の `hand`）で入れています。

【画像】LOG448_06_english.jpg 第138〜142話の英語版。各段の左4つが英語にしたコマ、右端が英語版の表紙

#### 英語版の指定 ep138.json

```json
{"ep": 138, "cover_line": "I'VE BEEN WAITING FOREVER.", "panels": [{"bubbles": [{"seed": [293, 201], "text": "Could you take me\nto the exam room?"}], "signs": []}, {"bubbles": [{"seed": [270, 190], "text": "Unable to\ndepart."}, {"seed": [820, 171], "text": "I've been waiting\nhere forever."}], "signs": []}, {"bubbles": [{"seed": [228, 336], "text": "I parked it\nwhere the most\npeople walk…"}], "signs": []}, {"bubbles": [{"seed": [573, 233], "text": "Mr. Morita,\nthis way."}], "signs": []}]}
```
#### 英語版の指定 ep139.json

```json
{"ep": 139, "cover_line": "WHO'S BACKING UP?", "panels": [{"bubbles": [], "signs": []}, {"bubbles": [{"seed": [753, 202], "text": "Whoa!"}], "signs": []}, {"bubbles": [{"seed": [915, 188], "text": "…So who's\nbacking up?"}, {"seed": [457, 251], "text": "I could not see\nbehind\nthe door."}], "signs": []}, {"bubbles": [{"seed": [835, 272], "text": "A bed is coming\nthrough.\nI will wait."}], "signs": []}]}
```
#### 英語版の指定 ep140.json

```json
{"ep": 140, "cover_line": "IT'S PAPER.", "panels": [{"bubbles": [{"seed": [818, 195], "text": "Obstacle\ndetected."}], "signs": []}, {"bubbles": [{"seed": [572, 239], "text": "…It's paper."}], "signs": []}, {"bubbles": [{"seed": [324, 245], "text": "I could not tell\nwhat it was."}], "signs": [{"box": [880, 30, 1075, 330], "text": "Faster to just pick it up.", "style": "hand"}]}, {"bubbles": [{"seed": [313, 159], "text": "It may be paper.\nPlease check."}, {"seed": [878, 147], "text": "Just say so,\nand I'll\npick it up."}], "signs": []}]}
```
#### 英語版の指定 ep141.json

```json
{"ep": 141, "cover_line": "I WANT TO GET OFF!", "panels": [{"bubbles": [], "signs": []}, {"bubbles": [{"seed": [771, 185], "text": "Hey, hey,\nhey!"}], "signs": []}, {"bubbles": [{"seed": [262, 322], "text": "My floor is\nthe next one."}, {"seed": [819, 150], "text": "I want to get off!"}], "signs": []}, {"bubbles": [{"seed": [279, 165], "text": "I will keep\nthe door clear."}, {"seed": [839, 143], "text": "That's\nthe way."}], "signs": []}]}
```
#### 英語版の指定 ep142.json

```json
{"ep": 142, "cover_line": "…THERE WAS NOTHING THERE.", "panels": [{"bubbles": [], "signs": []}, {"bubbles": [{"seed": [278, 154], "text": "…There was\nnothing there."}], "signs": []}, {"bubbles": [{"seed": [541, 210], "text": "The sensor doesn't see glass.\nTo Hakobukun, there was\nno wall here at all."}], "signs": []}, {"bubbles": [{"seed": [848, 160], "text": "Walls I can't see,\nI keep on the map."}], "signs": []}]}
```

| 話 | 日本語のページの題 | 英語のページの題 |
|---|---|---|
| 138 | 人が集まる場所は、ロボットの待ち場所に向かない | Busy Spots Make Bad Robot Stops |
| 139 | ロボットに見えない所は、建物の目を借りる | Borrowing the Building's Eyes |
| 140 | 分からない物の前では、粘らずに人に聞く | When in Doubt, Ask a Human |
| 141 | 狭い箱の中では、人の逃げ道を残す | Leave People a Way Out |
| 142 | センサーが見落とす物は、地図で守る | Mapping the Walls Robots Can't See |

ページは pixelstomotors.com/manga/ep138 〜 ep142 と pixelstomotors.com/english-4-koma/ep138 〜 ep142 です。p2m-site への commit は `4a0ae71` です。

```
$ timeout 170 python3 upload_media.py media_ep138_142
入れる 25・入っている 0・名前の重なり 0・合計 4.5MB
  25/25
おわり
$ for e in 138 139 140 141 142; do curl ... /manga/ep$e ... /english-4-koma/ep$e; done
138 200 200
139 200 200
140 200 200
141 200 200
142 200 200
```

### YouTube ショート5本：冒頭の動き

マンガは、縦長のコマを順に見せ、声と効果音を付けた YouTube ショートにもしています。声は [ElevenLabs](https://elevenlabs.io/)（文章を人の声で読み上げるサービス）で作ります。冒頭の約3.6〜3.8秒は話ごとに違う動きをするハコブくんのアニメで、5つとも新しく作りました。

| 話 | 冒頭の動き | 声（借りた声） |
|---|---|---|
| 138 | 出ようとしてガクッと2回止まり、しょんぼり → 左の脇へスッと寄る | 森田さん（田中先生の声） |
| 139 | 走ってきて、のけぞって「！？」→ じりじり下がる | 岩田さん（小芝先生の声） |
| 140 | 「障害物？」→ 小刻みにじりじり大回り →「？」 | 寺島さん |
| 141 | その場でくるくる回り（横に縮んで裏返る）→ 目が回る →「次ノ階デス」 | 高杉さん |
| 142 | 勢いよく走ってきて見えない壁にゴツン → 跳ね返る →「…何モ？」 | 槙原さん |

#### motion138x.py（第138〜142話の冒頭の動き）

```python
"""第138〜142話の冒頭の動き（2026-11-05）。話ごとに違う動き。
138 出ようとして、ガクッと止まる（2回）→ しょんぼり → 左の脇へスッと寄る（柱のわき）
139 走ってきて、見えない物に鼻先でぶつかりそうになり、のけぞって止まる →「！？」→ じりじり下がる
140 止まって「障害物？」→ じりじり横へ大回り（小刻みに）→「？」
141 その場でくるくる回る（横に縮んで裏返る）→ 目が回る →「次ノ階デス」
142 勢いよく走ってきて、見えない壁にゴツン → 跳ね返って揺れる →「…何モ？」"""
import sys
import numpy as np
import motion30
import hakobu_motion as M
from hakobu_motion import T, ease, _approach

class Stall138:
    dur = 3.8; empty = True
    def motion(self, t):
        for t0 in (0.5, 1.4):                      # 前へ出かけて、ガクッ
            if t0 <= t < t0 + 0.7:
                v = t - t0
                if v < 0.18: return dict(dx=-40 * ease(v / 0.18), rot=-2)
                k = np.exp(-(v - 0.18) * 10) * np.cos(2 * np.pi * 6 * (v - 0.18))
                return dict(dx=-40, rot=4 * k, sy=1 - 0.04 * k)
        if t < 1.4: return dict(dx=-40) if t >= 1.2 else {}
        if t < 2.6: return dict(dx=-40, dy=8 * ease(min(1, (t - 2.1) / 0.3)) if t > 2.1 else 0)
        v = t - 2.6
        return dict(dx=-40 - 300 * ease(min(1, v / 0.6)), dy=-4 * abs(np.sin(2 * np.pi * 3 * v)))
    def face(self, t):
        if t < 0.5: return T("案内", "待機中")
        if t < 2.1: return ("big", "big"), None, "o"
        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 >= 2.6
    def dim(self, t): return 1.0
    def cam(self, t): return (0, 0)

class Door139:
    dur = 3.6; empty = True
    def motion(self, t):
        if t < 0.8: return dict(dx=240 * (1 - ease(t / 0.8)), dy=-5 * abs(np.sin(2 * np.pi * 4 * t)))
        if t < 1.5:                                 # のけぞって止まる
            v = t - 0.8; k = np.exp(-v * 7) * np.cos(2 * np.pi * 4 * v)
            return dict(rot=7 * k, dx=12 * (1 - np.exp(-v * 9)))
        v = t - 1.5                                 # じりじり下がる
        return dict(dx=12 + 120 * ease(min(1, v / 1.6)) + 3 * np.sin(2 * np.pi * 5 * v), rot=0.8 * np.sin(2 * np.pi * 5 * v))
    def face(self, t):
        if t < 0.8: return ("open", "open"), None, "mid"
        if t < 1.5: return T("！？")
        return ("down", "down"), "trouble", "wave" if int(t * 8) % 2 else "wave2"
    def ring(self, t): return 0.6
    def blur(self, t): return t < 0.8
    def dim(self, t): return 1.0
    def cam(self, t): return (0, 0)

class Creep140:
    dur = 3.8; empty = True
    def motion(self, t):
        if t < 0.8: return _approach(t, 0.8, 200)
        if t < 1.5: return {}
        v = t - 1.5                                 # 小刻みに、じりじり大回り（左へ・少し傾く）
        steps = np.floor(v * 5) + ease((v * 5) % 1)
        x = -min(steps * 22, 230)
        return dict(dx=x, rot=-2.5 * min(1, v / 0.4), dy=-2 * abs(np.sin(2 * np.pi * 5 * v)))
    def face(self, t):
        if t < 0.8: return ("open", "open"), None, "mid"
        if t < 1.5: return T("障害物？")
        if t < 3.0: return ("dot", "dot"), "trouble", "wave" if int(t * 6) % 2 else "wave2"
        return T("？")
    def ring(self, t): return 0.6
    def blur(self, t): return t < 0.8
    def dim(self, t): return 1.0
    def cam(self, t): return (0, 0)

class Spin141:
    dur = 3.8; empty = True
    def motion(self, t):
        if t < 0.8: return _approach(t, 0.8, 230)
        if t < 2.4:                                 # その場でくるくる（横に縮んで裏返る）
            v = t - 0.8; ph = 2 * np.pi * 1.4 * v
            c = np.cos(ph)
            return dict(sx=max(0.08, abs(c)), flip=c < 0, dy=-3 * abs(np.sin(ph)))
        v = t - 2.4                                 # 目が回って、ふらふら
        return dict(rot=4 * np.sin(2 * np.pi * 1.6 * v) * np.exp(-v * 1.2), dx=6 * np.sin(2 * np.pi * 1.6 * v) * np.exp(-v * 1.2))
    def face(self, t):
        if t < 0.8: return ("open", "open"), None, "mid"
        if t < 2.4: return ("open", "open"), None, "closed"
        if t < 3.0: return ("x", "x"), None, "wave" if int(t * 8) % 2 else "wave2"
        return T("次ノ階", "デス")
    def ring(self, t): return 0.6
    def blur(self, t): return 0.8 <= t < 2.4
    def dim(self, t): return 1.0
    def cam(self, t): return (0, 0)

class Bonk142:
    dur = 3.6; empty = True
    HIT = 0.6
    def motion(self, t):
        if t < self.HIT: return dict(dx=330 * (1 - t / self.HIT), dy=-6 * abs(np.sin(2 * np.pi * 5 * t)), rot=-3, sx=1.05, sy=0.97)
        v = t - self.HIT                            # ゴツン：つぶれて跳ね返り、揺れる
        k = np.exp(-v * 5) * np.cos(2 * np.pi * 3.5 * v)
        return dict(dx=40 * (1 - np.exp(-v * 8)), sx=1 - 0.08 * k, sy=1 + 0.05 * k, rot=6 * k)
    def face(self, t):
        if t < self.HIT: return ("open", "open"), None, "mid"
        if t < 1.6: return ("x", "x"), None, "o"
        return T("…何モ？")
    def ring(self, t): return 1.0 if self.HIT <= t < 1.0 else 0.6
    def blur(self, t): return t < self.HIT
    def dim(self, t): return 1.0
    def cam(self, t):
        v = t - self.HIT
        return (int(10 * np.exp(-v * 9) * np.sin(2 * np.pi * 20 * v)), 0) if 0 <= v < 0.4 else (0, 0)

M.PATTERNS.update(ep138=Stall138, ep139=Door139, ep140=Creep140, ep141=Spin141, ep142=Bonk142)
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_ep138.py（第138話_柱のわきで待つ）

```python
import os, sys
sys.path.insert(0, os.environ.get("W24", os.path.expanduser("~/w")) + "/specs"); from common24 import *
import common24
common24.BGMAP[138] = "第82話.png"
HERE = os.path.dirname(os.path.abspath(__file__))
NAME, EP = "第138話_柱のわきで待つ", 138
HEAD = ['受付の前で', '出発できない']
COMIC = C + "ep138.jpg"
P1, P2, P3, P4 = [(0, 1350 * i, 1080, 1350 * (i + 1)) for i in range(4)]
DUR = 3.8
MOTION = mot("ep138", EP)                  # motion138x.py
SCENES = [
  dict(kind="op", video=anim(EP, "ep138"), dur=DUR, lines=[], se=[dict(at=0.5, f="se/se_pikon.mp3", db=-12), dict(at=1.4, f="se/se_pikon.mp3", db=-12), dict(at=2.6, f="se/se_dash.mp3", db=-12)]),
  dict(kind="panel", crop=P1, fit=False, lines=[ns("森田茂", 1, "検査室まで|【頼める】かね")]),
  dict(kind="panel", crop=P2, fit=False, lines=[hk(1, "【出発デキマセン】"), ns("森田茂", 2, "わしはもう、|【ずっと待っとる】が")], se=[dict(at=0.2, f="se/se_dash.mp3", db=-14)]),
  dict(kind="panel", crop=P3, fit=False, lines=[ns("槙原", 1, "一番人が通る所に、|【待たせてた】んだ…")]),
  dict(kind="panel", crop=P4, fit=False, lines=[hk(2, "森田サン、|【コチラデス】")]),
  end(("人が一番集まる場所は", 74, "ink", 0.0), ("ロボットの", 74, "ink", 0.25), ("待ち場所", 120, "orange", 0.9), ("に向かない", 120, "orange", 0.9), ("（呼ぶ場所と待つ場所を分ける）", 34, "gray", 1.8)),
]
```

#### spec_ep139.py（第139話_扉の向こう）

```python
import os, sys
sys.path.insert(0, os.environ.get("W24", os.path.expanduser("~/w")) + "/specs"); from common24 import *
import common24
common24.BGMAP[139] = "第63話.png"
HERE = os.path.dirname(os.path.abspath(__file__))
NAME, EP = "第139話_扉の向こう", 139
HEAD = ['扉を開けたら', '目の前にベッド']
COMIC = C + "ep139.jpg"
P1, P2, P3, P4 = [(0, 1350 * i, 1080, 1350 * (i + 1)) for i in range(4)]
DUR = 3.6
MOTION = mot("ep139", EP)                  # motion138x.py
SCENES = [
  dict(kind="op", video=anim(EP, "ep139"), dur=DUR, lines=[], se=[dict(at=0.0, f="se/se_dash.mp3", db=-12), dict(at=0.8, f="se/se_pikon.mp3", db=-10)]),
  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, min=1.6, lines=[ns("高杉", 1, "【わっ！】")]),
  dict(kind="panel", crop=P3, fit=False, lines=[ns("岩田", 1, "…【どっちが下がる】の？"), hk(1, "扉ノ向コウハ、|【見エマセンデシタ】")]),
  dict(kind="panel", crop=P4, fit=False, lines=[hk(2, "向コウカラ、ベッドガ来マス。|【待チマス】")]),
  end(("見えない所は", 74, "ink", 0.0), ("建物の", 120, "orange", 0.9), ("目を借りる", 120, "orange", 0.9), ("（扉の向こうをカメラで見る）", 34, "gray", 1.8)),
]
```

#### spec_ep140.py（第140話_紙くず2つ）

```python
import os, sys
sys.path.insert(0, os.environ.get("W24", os.path.expanduser("~/w")) + "/specs"); from common24 import *
import common24
common24.BGMAP[140] = "第48話.png"
HERE = os.path.dirname(os.path.abspath(__file__))
NAME, EP = "第140話_紙くず2つ", 140
HEAD = ['紙くず2つに', '大回りする']
COMIC = C + "ep140.jpg"
P1, P2, P3, P4 = [(0, 1350 * i, 1080, 1350 * (i + 1)) for i in range(4)]
DUR = 3.8
MOTION = mot("ep140", EP)                  # motion138x.py
SCENES = [
  dict(kind="op", video=anim(EP, "ep140"), dur=DUR, lines=[], se=[dict(at=0.0, f="se/se_dash.mp3", db=-12), dict(at=0.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, "…【紙よ】、それ")]),
  dict(kind="panel", crop=P3, fit=False, lines=[ns("寺島", 2, "【拾ったほうが早い】わ"), hk(2, "何デアルカ、|【分カリマセンデシタ】")]),
  dict(kind="panel", crop=P4, fit=False, lines=[hk(3, "紙カモシレマセン。|【確カメテクダサイ】"), ns("寺島", 3, "最初からそう言えば、|【拾う】わよ")]),
  end(("分からない物の前では", 74, "ink", 0.0), ("粘らずに", 74, "ink", 0.25), ("人に聞く", 120, "orange", 0.9), ("（用心しすぎも遅れになる）", 34, "gray", 1.8)),
]
```

#### spec_ep141.py（第141話_隅っこ）

```python
import os, sys
sys.path.insert(0, os.environ.get("W24", os.path.expanduser("~/w")) + "/specs"); from common24 import *
import common24
common24.BGMAP[141] = "第83話.png"
HERE = os.path.dirname(os.path.abspath(__file__))
NAME, EP = "第141話_隅っこ", 141
HEAD = ['エレベーターで', '隅に押し込む']
COMIC = C + "ep141.jpg"
P1, P2, P3, P4 = [(0, 1350 * i, 1080, 1350 * (i + 1)) for i in range(4)]
DUR = 3.8
MOTION = mot("ep141", EP)                  # motion138x.py
SCENES = [
  dict(kind="op", video=anim(EP, "ep141"), dur=DUR, lines=[], se=[dict(at=0.0, f="se/se_dash.mp3", db=-12), dict(at=0.9, f="se/se_dash.mp3", db=-14), dict(at=3.0, f="se/se_pikon.mp3", db=-10)]),
  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=[ns("高杉", 1, "【ちょっと、ちょっと！】")]),
  dict(kind="panel", crop=P3, fit=False, lines=[ns("高杉", 2, "【降りたいの！】"), hk(1, "ワタシハ、|【次ノ階】デス")]),
  dict(kind="panel", crop=P4, fit=False, lines=[hk(2, "扉ノ前ハ、|【空ケテオキマス】"), ns("高杉_納得", 1, "そうそう、|【それでいいの】")]),
  end(("狭い箱の中では", 74, "ink", 0.0), ("人の", 74, "ink", 0.25), ("逃げ道", 120, "orange", 0.9), ("を残す", 120, "orange", 0.9), ("（扉の前を空けて止まる）", 34, "gray", 1.8)),
]
```

#### spec_ep142.py（第142話_ガラスの壁）

```python
import os, sys
sys.path.insert(0, os.environ.get("W24", os.path.expanduser("~/w")) + "/specs"); from common24 import *
import common24
common24.BGMAP[142] = "第72話.png"
HERE = os.path.dirname(os.path.abspath(__file__))
NAME, EP = "第142話_ガラスの壁", 142
HEAD = ['何もないのに', 'ぶつかった']
COMIC = C + "ep142.jpg"
P1, P2, P3, P4 = [(0, 1350 * i, 1080, 1350 * (i + 1)) for i in range(4)]
DUR = 3.6
MOTION = mot("ep142", EP)                  # motion138x.py
SCENES = [
  dict(kind="op", video=anim(EP, "ep142"), dur=DUR, lines=[], se=[dict(at=0.0, f="se/se_dash.mp3", db=-12), dict(at=0.6, f="se/se_don.mp3", db=-8)]),
  dict(kind="panel", crop=P1, fit=False, min=1.8, words=[W_("ガラスの面談室", 0.1, 540, 1500, 96, "orange", 0)]),
  dict(kind="panel", crop=P2, fit=False, lines=[hk(1, "…【何モ、アリマセンデシタ】")]),
  dict(kind="panel", crop=P3, fit=False, lines=[ns("槙原", 1, "センサーには|【ガラスが映ってない】||ハコブくんには、ここに|【壁がない】ように見えてたんだ")]),
  dict(kind="panel", crop=P4, fit=False, lines=[hk(2, "見エナイ壁ハ、|【地図デ覚エマス】")]),
  end(("センサーが", 74, "ink", 0.0), ("見落とす物は", 74, "ink", 0.25), ("地図", 120, "orange", 0.9), ("で守る", 120, "orange", 0.9), ("（ガラスの壁ぎわを走らない）", 34, "gray", 1.8)),
]
```

```
$ for d in ep/第1[34]*; do python3 mkshort.py $d/spec_ep*.py; done
== 第138話_柱のわきで待つ
できあがり: out_第138話_柱のわきで待つ.mp4 21.4 秒
== 第139話_扉の向こう
できあがり: out_第139話_扉の向こう.mp4 19.6 秒
== 第140話_紙くず2つ
できあがり: out_第140話_紙くず2つ.mp4 23.4 秒
== 第141話_隅っこ
できあがり: out_第141話_隅っこ.mp4 19.8 秒
== 第142話_ガラスの壁
できあがり: out_第142話_ガラスの壁.mp4 22.2 秒

冒頭1秒の音量（効果音が鳴っているか）
第138話_柱のわきで待つ 21.4 mean_volume: -21.8 dB
第139話_扉の向こう 19.6 mean_volume: -16.1 dB
第140話_紙くず2つ 23.4 mean_volume: -16.1 dB
第141話_隅っこ 19.8 mean_volume: -16.4 dB
第142話_ガラスの壁 22.2 mean_volume: -12.8 dB
```

書き出したあと、各ショートのコマを15枚ずつ並べて見ました。第142話は、絵に描かれたコツンの上に字幕のコツンも出して二重になっていたので、字幕のほうを消して書き出し直しています。

【画像】LOG448_07_short_frames.jpg 第138話のショートのコマ。冒頭で出ようとしては止まるハコブくん、4コマ、最後のまとめの字

### 第141話の「それでいいの」が疑問形に聞こえた

> エレベーターの動画の最後のセリフ「それでいいの」のイントネーションが変
> これだと疑問系の発音だね。あの場面は納得する感じのそれでいいの」にしないと
> 言葉尻を下げる感じかな

1回目は、区切り・うなずき・言い切りの3通り（「それで、いいの。」「うん、それでいいの。」「それでいいの！」）を作りましたが、どれも疑問形に聞こえました。2回目に、言葉尻を下げる読みとして次の4通りを作り、1つ目を使うことにしました。

```
$ python3 ../../_制作ベース/tts_eleven.py --voice 2zABg81whjrIKypr7XTd --name 高杉_納得 --try "そうそう、それでいいの。｜うん、それでいいのよ。｜それでいいのよ。｜[satisfied] それでいいの。"
試し_01 そうそう、それでいいの。
試し_02 うん、それでいいのよ。
試し_03 それでいいのよ。
試し_04 [satisfied] それでいいの。
できあがり: voice_eleven/高杉_納得  使った文字数 約50字
```

**納得・言い切りのセリフは、「。」で止めて、前に「そうそう、」のようなうなずきを足すと、語尾が下がって聞こえました。** 字幕も声に合わせて「そうそう、それでいいの」にしています（マンガの吹き出しは「それでいいの」のまま）。チェック項目の台帳にも、この直し方を1行足しました。

書き出した mp4 と題名・説明の txt は、KIOXIA の `videos/_ショート専用/_未アップ/第138〜142話_*/` に置きました。


## SNS の予約と、本編3本の投稿文

### マンガ5話の予約

| 話 | 日本語の X・LinkedIn（日本時間） | 英語の X（米国東部時間） |
|---|---|---|
| 138 | 12/2 20:00 | 2/1 11:00 |
| 139 | 12/3 20:00 | 2/2 11:00 |
| 140 | 12/4 20:00 | 2/3 11:00 |
| 141 | 12/5 19:30 | 2/4 11:00 |
| 142 | 12/6 19:30 | 2/5 11:00 |

マンガの投稿文は r2r2r-content の `sns/MANGA_ep138〜142_sns_posts.json`、ショートの X の型は `sns/SHORT_ep138〜142_sns_posts.json` です（commit `ea80544`）。

### 予約が抜けていた本編3本

YouTube で公開予約したショートを確かめたとき、本編3本の X と LinkedIn の予約がないことに気づきました。投稿文の json がなかったためです。3本とも作って、YouTube の公開時刻に合わせて予約しました（commit `b6e1c09`）。

| 本編 | X | LinkedIn |
|---|---|---|
| ハコブくんの声と絵は、どう作っているのか | 11/13 20:30 | 11/13 20:30 |
| ロボットの頭脳は、脳のように分かれるのか | 11/18 20:00 | 11/18 19:30 |
| 配膳ロボットが店に広がった理由は「賢さ」じゃなかった | 11/20 20:00 | 11/20 19:30 |

```
$ python3 tools/sync_youtube_shorts.py --videos youtube_videos.json
足した VIDEO_hakobu-voice-art-x 2026-11-13T20:30:00+09:00 ハコブくんの声と絵は、どう作っているのか ― キャラクターの
足した VIDEO_hakobu-voice-art-linkedin 2026-11-13T20:30:00+09:00 ハコブくんの声と絵は、どう作っているのか ― キャラクターの
足した VIDEO_robot-brain-divides-x 2026-11-18T20:00:00+09:00 ロボットの頭脳は、脳のように分かれるのか｜大きなAIは大脳、
足した VIDEO_robot-brain-divides-linkedin 2026-11-18T19:30:00+09:00 ロボットの頭脳は、脳のように分かれるのか｜大きなAIは大脳、
足した VIDEO_serving-robot-last-mile-x 2026-11-20T20:00:00+09:00 配膳ロボットが店に広がった理由は「賢さ」じゃなかった
足した VIDEO_serving-robot-last-mile-linkedin 2026-11-20T19:30:00+09:00 配膳ロボットが店に広がった理由は「賢さ」じゃなかった
```

#### VIDEO_hakobu-voice-art_sns_posts.json

```json
{
  "meta": {
    "kind": "youtube_video",
    "title": "ハコブくんの声と絵",
    "youtube_title": "ハコブくんの声と絵は、どう作っているのか ― キャラクターの声と絵を、AIで作って分かったこと",
    "title_match": [
      "ハコブくん",
      "声と絵"
    ],
    "seconds": 671,
    "url": "https://youtu.be/ZT3AJ6aL-_k",
    "youtube_id": "ZT3AJ6aL-_k",
    "publish_at": "2026-11-13T20:30:00+09:00"
  },
  "posts": {
    "x_template": "AIが描いたマンガのコマ、看護師さんの腕が3本あった。見つけたのは人の目なんだよね。\nハコブくんの声も絵もAIの候補から選んでるけど、気を抜くとデザインが変わってる（笑）\n#生成AI #ロボット\n{url}",
    "linkedin_template": "このマンガのコマ、腕を数えてみてください。1、2、3。\n\n描いたのは画像を作るAI。3本目に気づいたのは、私でした。\n\nこのチャンネルの案内役ハコブくん（病院で物を運ぶ小さなロボット）は、絵も声もAIで作っています。ただ、ボタン1つではありません。\n・絵：Claude が1コマずつ指示の文を書き、Gemini が1コマずつ描き、AIがチェックのリストと見比べ、最後に私が全部のコマを見て通す。過去の間違いのリストは、2日で22行になりました。ハコブくんの横の輪に穴、ハコブくんに腕が生える、扉が頭の上に浮く……\n・声：ElevenLabs のボイスデザインで説明の文章を6通り書き、1つから3つずつ出た声を聞き比べて決めた。いちばん落ち着いて正確な声は、ハコブくんの見た目に合いませんでした\n・読み：読み間違いを見つけるたびに、直しのリストに足す。37本の台本で207組になりました\n\n減ったのは、声を録る・一から描く手間。減っていないのは、選ぶことと確かめること。むしろ確かめる所は、リストになって増えています。\n\n選ぶ仕事は、人に残るのか。正直、分かりません。ただ今は、腕の数を数えているのは人です。\n\n動画：{url}\n\nYouTube のほかの動画: https://www.youtube.com/@PixelstoMotors\n\n#生成AI #ロボット",
    "linkedin_comment_template": "補足：22行・207組は、2026年11月1日に数えた数です。マンガの絵は生成AI（Gemini）、声は AI 音声（ElevenLabs）で作っています。目次と、扱う範囲の断りは YouTube の概要欄にあります。",
    "x": "AIが描いたマンガのコマ、看護師さんの腕が3本あった。見つけたのは人の目なんだよね。\nハコブくんの声も絵もAIの候補から選んでるけど、気を抜くとデザインが変わってる（笑）\n#生成AI #ロボット\nhttps://youtu.be/ZT3AJ6aL-_k",
    "linkedin": "このマンガのコマ、腕を数えてみてください。1、2、3。\n\n描いたのは画像を作るAI。3本目に気づいたのは、私でした。\n\nこのチャンネルの案内役ハコブくん（病院で物を運ぶ小さなロボット）は、絵も声もAIで作っています。ただ、ボタン1つではありません。\n・絵：Claude が1コマずつ指示の文を書き、Gemini が1コマずつ描き、AIがチェックのリストと見比べ、最後に私が全部のコマを見て通す。過去の間違いのリストは、2日で22行になりました。ハコブくんの横の輪に穴、ハコブくんに腕が生える、扉が頭の上に浮く……\n・声：ElevenLabs のボイスデザインで説明の文章を6通り書き、1つから3つずつ出た声を聞き比べて決めた。いちばん落ち着いて正確な声は、ハコブくんの見た目に合いませんでした\n・読み：読み間違いを見つけるたびに、直しのリストに足す。37本の台本で207組になりました\n\n減ったのは、声を録る・一から描く手間。減っていないのは、選ぶことと確かめること。むしろ確かめる所は、リストになって増えています。\n\n選ぶ仕事は、人に残るのか。正直、分かりません。ただ今は、腕の数を数えているのは人です。\n\n動画：https://youtu.be/ZT3AJ6aL-_k\n\nYouTube のほかの動画: https://www.youtube.com/@PixelstoMotors\n\n#生成AI #ロボット",
    "linkedin_comment": "補足：22行・207組は、2026年11月1日に数えた数です。マンガの絵は生成AI（Gemini）、声は AI 音声（ElevenLabs）で作っています。目次と、扱う範囲の断りは YouTube の概要欄にあります。"
  },
  "note": "YouTube の本編の X と LinkedIn。url と posts.x・posts.linkedin は tools/sync_youtube_shorts.py が YouTube の公開予約を見て入れる。ひとりとAIのチーム 4回目。文は iCloud の 動画/ハコブくんの声と絵/投稿文_X_LinkedIn.txt（LinkedIn は日本語の部分）"
}
```
#### VIDEO_robot-brain-divides_sns_posts.json

```json
{
  "meta": {
    "kind": "youtube_video",
    "title": "頭脳は脳のように分かれる",
    "youtube_title": "ロボットの頭脳は、脳のように分かれるのか｜大きなAIは大脳、専用AIは小脳",
    "title_match": [
      "頭脳",
      "脳"
    ],
    "seconds": 426,
    "url": "https://youtu.be/TOUyy-o-f0s",
    "youtube_id": "TOUyy-o-f0s",
    "publish_at": "2026-11-18T19:30:00+09:00"
  },
  "posts": {
    "x_template": "ロボットを動かすAIが1回考えるのに、専用のAIは0.05秒、大きなAIは18.6秒。\n人の脳みたいに、大脳と小脳に分かれていくのかも。ハコブくんと壁打ちした考察回だよ。\n#フィジカルAI #ロボット\n{url}",
    "linkedin_template": "ロボットを動かすAIが1回考えるのに、専用のAIは0.05秒。何にでも使える大きなAIは18.6秒でした。\n\n速さで比べると、370倍ほどの差です。大きなAIが賢くなれば、ロボット専用の頭脳はいらなくなる、という声もあります。でも、コップを落としかけた手を0.05秒で戻すのは、考える頭とは別の仕事です。\n\n動画では、案内役のハコブくんと一緒に、\n・人の脳が大脳・小脳・反射に分かれているように、ロボットのAIも分かれていくのか\n・小脳を育てるのは何か（失敗した所の手直し）\n・手直しの記憶を、どう整理して学ばせるか\nを考えました。答えを伝える回ではなく、考えている途中をそのまま見せる考察回です。数字と考えた道すじの全文はサイトの記事にあります。\n\n動画：{url}\n\nYouTube のほかの動画: https://www.youtube.com/@PixelstoMotors\n\n#フィジカルAI #ロボット #AI",
    "linkedin_comment_template": "補足：0.05秒・18.6秒は NVIDIA の Astranomy（2026年10月14日）の値です。数字の出どころは概要欄に、考えた道すじは記事にあります https://pixelstomotors.com/record/robot-ai-divides-like-brain",
    "x": "ロボットを動かすAIが1回考えるのに、専用のAIは0.05秒、大きなAIは18.6秒。\n人の脳みたいに、大脳と小脳に分かれていくのかも。ハコブくんと壁打ちした考察回だよ。\n#フィジカルAI #ロボット\nhttps://youtu.be/TOUyy-o-f0s",
    "linkedin": "ロボットを動かすAIが1回考えるのに、専用のAIは0.05秒。何にでも使える大きなAIは18.6秒でした。\n\n速さで比べると、370倍ほどの差です。大きなAIが賢くなれば、ロボット専用の頭脳はいらなくなる、という声もあります。でも、コップを落としかけた手を0.05秒で戻すのは、考える頭とは別の仕事です。\n\n動画では、案内役のハコブくんと一緒に、\n・人の脳が大脳・小脳・反射に分かれているように、ロボットのAIも分かれていくのか\n・小脳を育てるのは何か（失敗した所の手直し）\n・手直しの記憶を、どう整理して学ばせるか\nを考えました。答えを伝える回ではなく、考えている途中をそのまま見せる考察回です。数字と考えた道すじの全文はサイトの記事にあります。\n\n動画：https://youtu.be/TOUyy-o-f0s\n\nYouTube のほかの動画: https://www.youtube.com/@PixelstoMotors\n\n#フィジカルAI #ロボット #AI",
    "linkedin_comment": "補足：0.05秒・18.6秒は NVIDIA の Astranomy（2026年10月14日）の値です。数字の出どころは概要欄に、考えた道すじは記事にあります https://pixelstomotors.com/record/robot-ai-divides-like-brain"
  },
  "note": "YouTube の本編の X と LinkedIn。url と posts.x・posts.linkedin は tools/sync_youtube_shorts.py が YouTube の公開予約を見て入れる。考察回「壁打ちの型」の1本目（もとは LOG424）。投稿文の txt がないため、概要欄_下書き.md から作った（2026-11-05）"
}
```
#### VIDEO_serving-robot-last-mile_sns_posts.json

```json
{
  "meta": {
    "kind": "youtube_video",
    "title": "現場で扱えるか",
    "youtube_title": "配膳ロボットが店に広がった理由は「賢さ」じゃなかった",
    "title_match": [
      "配膳ロボット",
      "賢さ"
    ],
    "seconds": 516,
    "url": "https://youtu.be/yn7tePEfPec",
    "youtube_id": "yn7tePEfPec",
    "publish_at": "2026-11-20T19:30:00+09:00"
  },
  "posts": {
    "x_template": "成功率99%でも、1時間に600回働けば6回呼ばれる。\n配膳ロボットが店に広がったのは、賢さより「止まったとき店の人が戻せるか」だったんだよね。ハコブくんと壁打ちした考察回だよ。\n#配膳ロボット #ロボット\n{url}",
    "linkedin_template": "成功率99%のロボットでも、1時間に600回働けば、10分に1回は人が呼ばれます。\n\n配膳ロボットが店に広がったのは、賢さではありませんでした。いちばん難しい「料理をテーブルに置く」はお客さんに任せ、止まってもボタン1つで店のスタッフさんが戻せる。\n\n動画では、ハコブくんと一緒に、\n・配膳ロボットが先に広がった条件\n・止まった理由が分かるか、その場の人が戻せるか、次の動きが読めるか\n・足りなくなるのは、作る人より、店に入れて回す人\n・最後の壁、下げ膳と調理場の扉\nを考えました。考えた道すじの全文はサイトの記事にあります。\n\n動画：{url}\n\nYouTube のほかの動画: https://www.youtube.com/@PixelstoMotors\n\n#配膳ロボット #フィジカルAI #ロボット",
    "linkedin_comment_template": "補足：考えた道すじの全文は記事にあります https://pixelstomotors.com/record/service-robot-last-mile",
    "x": "成功率99%でも、1時間に600回働けば6回呼ばれる。\n配膳ロボットが店に広がったのは、賢さより「止まったとき店の人が戻せるか」だったんだよね。ハコブくんと壁打ちした考察回だよ。\n#配膳ロボット #ロボット\nhttps://youtu.be/yn7tePEfPec",
    "linkedin": "成功率99%のロボットでも、1時間に600回働けば、10分に1回は人が呼ばれます。\n\n配膳ロボットが店に広がったのは、賢さではありませんでした。いちばん難しい「料理をテーブルに置く」はお客さんに任せ、止まってもボタン1つで店のスタッフさんが戻せる。\n\n動画では、ハコブくんと一緒に、\n・配膳ロボットが先に広がった条件\n・止まった理由が分かるか、その場の人が戻せるか、次の動きが読めるか\n・足りなくなるのは、作る人より、店に入れて回す人\n・最後の壁、下げ膳と調理場の扉\nを考えました。考えた道すじの全文はサイトの記事にあります。\n\n動画：https://youtu.be/yn7tePEfPec\n\nYouTube のほかの動画: https://www.youtube.com/@PixelstoMotors\n\n#配膳ロボット #フィジカルAI #ロボット",
    "linkedin_comment": "補足：考えた道すじの全文は記事にあります https://pixelstomotors.com/record/service-robot-last-mile"
  },
  "note": "YouTube の本編の X と LinkedIn。url と posts.x・posts.linkedin は tools/sync_youtube_shorts.py が YouTube の公開予約を見て入れる。考察回。文は KIOXIA の videos/現場で扱えるか/アップロード用/投稿文_X_LinkedIn.txt（LinkedIn は日本語の部分）"
}
```


## 次の話も、下書きはバッチで何話もまとめて頼む

5話の下書きが3分で返ったので、次は10話ぶんを1回のバッチで頼むこともできます。ただし、バッチの下書きが36枚使えたのは、spec にカメラワークと大きさを先に書いていたからです。並んで立つコマで大きさの仕組みを切らないこと、ふさぐ・はさまる話のコマは、大きさではなく話を見せる構図にすること。この2つを spec を書く段階で守れば、描き直しはさらに減る見込みです。

出典
- [Gemini API Batch Mode | Google AI for Developers](https://ai.google.dev/gemini-api/docs/batch-mode)
- [Gemini API 画像生成 | Google AI for Developers](https://ai.google.dev/gemini-api/docs/image-generation)
- [神奈川県 ロボットの実証（院内誘導）](https://www.pref.kanagawa.jp/documents/71550/robohon3-iryou-innaiyuudou.pdf)
- [トヨタ フロンティアリサーチ](https://global.toyota/jp/mobility/frontier-research/40390261.html)
- [NBC News: Robots gaining traction in hospitals](https://www.nbcnews.com/health/health-news/robots-gaining-traction-hospitals-flna1c9475445)
- [Proof News: Moxi](https://www.proofnews.org/moxi/)
- [KQED: Hospital uses robots instead of people to cart things around](https://www.kqed.org/futureofyou/121043/hospital-uses-robots-instead-of-people-to-cart-things-around)
