# 薬剤搬送ロボットの記事を表紙で伝える ― サムネイル9本を4コマと記事の画像に

- 記録: LOG473（ひとり研究所の裏側）
- 公開: 2026/11/8
- 記事のページ: https://pixelstomotors.com/record/drug-delivery-robot-thumbnails-manga-and-article-images
- サイト: Pixels to Motors（ADAPT DESIGN / 島津 栄一） https://pixelstomotors.com/
- 要約: 薬剤搬送ロボットのシリーズの記事サムネイル9枚を、白黒の英字の表紙から、4コマの絵・記事の画像・記事の図解に替え、札の名前を HRI から薬剤搬送ロボットにした。同じ見た目が続かないよう、同じ種類は2〜3枚までにした。

> AI が読むための記事の全文です（コード・実行ログを含む）。引用するときは上の「記事のページ」の住所を使ってください。

---

**薬剤搬送ロボットのシリーズ（pixelstomotors.com/series1 ）の記事サムネイル9枚を、白黒の英字の見出しを組んだ表紙から、4コママンガ『ハコブくん』の絵・記事に載せた画像・記事の図解に替え、記事の札の名前を「HRI」から「薬剤搬送ロボット」にした。** 同じシリーズの古い記事は、絵や画面を全面に置いた文字のない表紙で並んでいて、新しい9本だけが別のシリーズのように見えていたためだ。替えるときは、同じ種類の表紙が一覧で続かないよう、同じ種類は2〜3枚までに抑えた。

替えたことで、次のことが変わった。

- シリーズのページを開いた人が、9本の記事を、ほかの回と同じ「絵と画面の表紙」の並びの中で見られる。前は、9本だけが英字の見出しの表紙で、古い記事とは別のシリーズに見えていた
- 記事の札が「HRI」から「薬剤搬送ロボット」になり、57本の記事のカードで、何のシリーズの記事かが日本語で分かる。HRI（Human-Robot Interaction、人とロボットの関わり）という言葉を知らない人には、札の意味が伝わっていなかった
- 9枚は、スクリプト1本（make_hri_covers.py）で作り直せる。2コアの CPU で、素材の取得を含めて約10秒、取得済みなら約7秒。前の表紙を作るのにかかった時間は測っていない

サムネイルは、記事一覧のカードの上に出る絵のこと。薬剤搬送ロボットのシリーズは、病院で薬を運ぶロボット「ハコブくん」を題材に、人とロボットの関わりを設計してきた記録で、古い回（LOG1〜LOG171）の表紙は、ハコブくんのイラスト、画面の設計図、Unity の場面などを全面に置いた画像だった。ところが、7月以降に書いた9本（LOG267〜LOG322）だけは、ほかのシリーズと同じ、英字の見出しと図を組んだ白黒の表紙になっていた。

![作り直す前のシリーズのページの記録の一覧。LOG322・321・311・308・307・305 の表紙が、白黒の地に英字の見出しを組んだ形で並んでいる。札は HRI](https://pixelstomotors.com/media/LOG473_01_series1_before_upper.png)

![作り直す前の一覧の下の段。LOG303・269・267 の表紙も、白黒の英字の見出しと図の形](https://pixelstomotors.com/media/LOG473_02_series1_before_lower.png)

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

| 順 | やったこと | なぜ | 結果 |
|---|---|---|---|
| 1 | 9本の表紙を、記事の画像と、記事の図解を斜めに置いた画像に替えた。札を「薬剤搬送ロボット」にした | 古い回の表紙は、文字のない全面の画像だった | 斜めの図解が5枚、Fusion の暗い画面が3枚になり、似た表紙が並んだ |
| 2 | 斜めの図解を2枚に絞って寄せ、3枚を4コマの扉絵に替えた。LOG308 はハコブくんの三面図にした | 「同じようなサムネイルはNG」「2個くらいならOK」「斜めならもっとアップに」と指示を受けた | 三面図は NG になった |
| 3 | LOG308 を、記事の中の Fusion の画面（最初に出てきた形）にした | 3つの案を出したが、記事の中の画面を使うよう指示を受けた | 9枚を公開した |

この表紙の替え方には、4つの考えがある。

1. 素材は、その記事に載っているものか、同じ世界の4コマから取る。描き起こさない
2. 同じ種類の表紙は、一覧で2〜3枚までにする。カードは3列で並ぶので、同じ種類が続くと、どれも同じに見える
3. 斜めに置く図解は、紙の外の余白を見せない。余白が見えると、画面の写真ではなく、置いた紙に見える
4. 表紙に見出しの文字を入れない。古い回と同じく、絵と画面に語らせる

ここから下は、作業の記録になる。


## 作業ログ ― 薬剤搬送ロボットの古い記事の表紙を確かめた

依頼は次のとおり。

> 薬剤搬送ロボットハコブくん | Drug Delivery Robotページのこの白黒サムネイルを他のHRI記事のサムネイルのデザインに合わせてください。

> HRIというカテゴリータグを「薬剤搬送ロボット」に変更

サイトのデータを見ると、シリーズの値が HRI の記事は57本あった。そのうち、表紙が HTML（`/thumbs/r-*.html`）なのは9本だけで、残りの48本は、Studio の時代に作った画像の表紙（`cover`）だった。48本の表紙を並べて見た。

![古い回の表紙48枚を並べた一覧。ハコブくんのイラスト、4コマの扉絵、Figma の画面、色の見本、Unity の場面、FigJam の流れ図などが、文字のない全面の画像で並んでいる](https://pixelstomotors.com/media/LOG473_03_old_hri_covers.jpg)

古い回の表紙は、どれも文字を入れていない。ハコブくんのイラスト、斜めに置いた画面の設計図、Unity の場面のように、その回で作ったものが全面に出ている。9本の表紙もこの形にそろえることにした。

9本の記事のうち、記事の中に画像があるのは4本だった。Autodesk Fusion（3D CAD のソフト）の回3本には Fusion の画面、アバターの回の LOG322 にはロボット「ピカリくん」の三面図がある。残りの5本（LOG267・269・303・305・321）は、記事の中に画像がなく、HTML の図解だけだった。iCloud の作業フォルダと、サイトのアーカイブ（site_archive）も探したが、この5本はもともと HTML の表紙で始まっていて、画像の表紙は見つからなかった。

そこで、画像のない5本は、記事の図解をブラウザで描いて、斜めに置いた画像にした。古い回にも、Figma の画面を斜めに置いた表紙がある。

画像は、Cloudflare R2 に置く。[Cloudflare R2](https://www.cloudflare.com/developer-platform/products/r2/) は、ファイルを置いておくクラウドの置き場所で、このサイトの記事の画像と動画はすべてここにある（GitHub には入れない）。記事の図解は、[Playwright](https://playwright.dev/)（ブラウザをプログラムから動かす道具）でページを開き、画面を撮って画像にした。


## 作業ログ ― 1回目：斜めの図解が5枚並び、NG になった

1回目は、記事の画像がある4本は記事の画像を切り出し、ない5本は図解を斜め -16° に回して置いた。札の値も HRI から薬剤搬送ロボットに替えて公開した。

公開した一覧を見て、次の指示を受けた。

> この辺の同じようなサムネイルはNGです

> 2個くらいならOK

![1回目に公開した一覧。LOG308・307 は Fusion の暗い画面、LOG305・303・269・267 は白っぽい図解を斜めに置いた画像で、似た表紙が6枚続いている](https://pixelstomotors.com/media/LOG473_04_round1_ng.png)

斜めの図解が5枚、Fusion の暗い画面が3枚になっていた。1枚ずつ見れば記事の中身だが、一覧では、白っぽい斜めの紙と、暗い画面が続いて、同じ種類の表紙が並んで見える。

ねらい：同じ種類の表紙を2〜3枚までにして、一覧で同じ見た目が続かないようにする。
変わったこと：斜めの図解を5枚から2枚に、残りの3枚を4コマの扉絵にした。
見込める効果：シリーズのページで、カードの並びが古い回と同じ「いろいろな絵と画面」の並びになる（見込み。並びは公開後のページの画面で確かめた）。

続けて、次の指示を受けた。

> 斜めならもっとアップにして余白がないようにしてください

斜めにした図解は、紙の外の灰色が四隅に見えていた。残す2枚（LOG303・305）は、倍率を 1.7〜1.9 から 3.0〜3.3 に上げ、灰色が出ないところまで寄せた。


## 作業ログ ― 4コママンガの扉絵から、記事の中身に近い3枚を選んだ

画像のない3本（LOG267・269・321）は、4コママンガ『ハコブくん』の扉絵から選んだ。4コマは同じ病院の世界の話で、扉絵は 1080×1350 の縦長の絵になっている。143話分の扉絵を並べて選んだ。

![4コマの扉絵143枚の一覧。ハコブくんと看護師、患者、アバターのピカリくんが白黒の漫画の絵で描かれている](https://pixelstomotors.com/media/LOG473_05_manga_eyecatches.jpg)

| 記事 | 記事の中身 | 選んだ扉絵 |
|---|---|---|
| LOG267 | 病院の夜の搬送をデジタルツインで組む | 第100話「夜の運び方」：夜の廊下を走るハコブくん |
| LOG269 | 予測と実際の差を見て、判断を直す仕組み | 第80話「過去最高の配送件数」：タブレットを見る人と、「過去最高デス」と言うハコブくん |
| LOG321 | 遠くにいる人が操るアバター | 第110話「すぐ戻ります」：VR のゴーグルで操縦する人 |

縦長の絵から 16:9 の帯を切ると、題名の文字か、人の顔のどちらかが切れやすい。題名の文字を外し、顔が入る高さを1枚ずつ探した（第80話は上から330px、第110話は300px、第100話は560px）。

LOG308 は、記事の中にあるハコブくんの三面図に替えた。暗い Fusion の画面を3枚から2枚に減らすためだった。


## 作業ログ ― LOG308 の表紙を、記事の中の Fusion の画面にした

2回目を公開すると、LOG308 について次の指示を受けた。

> これはNG。他の案を出して

![NG になった LOG308 の表紙。白い地に、ハコブくんを前・横・後ろから描いた三面図](https://pixelstomotors.com/media/LOG473_06_log308_threeview_ng.png)

三面図は、古い回の最初の表紙（LOG1）とほとんど同じ絵で、しかも、Fusion で形にしようとした「元の絵」だった。記事は「Claude の指示だけでは形にならなかった」話なので、元の絵では中身が伝わらない。

3つの案を作った。

![LOG308 の表紙の3案。A は4コマ第55話の、画面に「同一指示3回目：確認シマス」と出たハコブくん。B は廊下でハコブくんとベッドがすれ違う寸法図。C は車椅子の人と並ぶ寸法図](https://pixelstomotors.com/media/LOG473_07_log308_options.png)

これに対して、記事のページの画面写真と一緒に、次の指示を受けた。

> この辺使いなよ

![LOG308 の記事のページ。「出てきたのは、角の丸い箱に穴が開いたものだった。イラストのハコブくんとは似ていない」の文の下に、板2枚と箱と青い面が並んだ Fusion の画面がある](https://pixelstomotors.com/media/LOG473_08_log308_article_fusion.png)

記事で最初に Fusion に出てきた形（板2枚・箱・青い面）の画面で、記事の「指示だけでは形にならなかった」がそのまま見える。ウィンドウの中の作業画面だけを、形が真ん中に来るように切り出した（中心 1400, 880・幅 1500px）。Fusion の暗い画面は3枚（LOG307・308・311）になったが、3枚とも形が違う（板・ばらばらの部品・ボディ）。

表紙の素材の種類を、回ごとに図にした。上のボタンで、前・1回目・公開した形を切り替えられる。

[図解: LOG473_cover_kinds.html](https://pixelstomotors.com/embeds/LOG473_cover_kinds.html)

作り直した後のシリーズのページは次のとおり。

![作り直した後のシリーズのページの記録の一覧。LOG322 はピカリくんの三面図、LOG321 は VR で操縦する人の4コマ、LOG311 は Fusion のボディ、LOG308 は Fusion の板と箱、LOG307 は Fusion の板、LOG305 は寄せた図解。札は薬剤搬送ロボット](https://pixelstomotors.com/media/LOG473_09_series1_after_upper.png)

![一覧の下の段。LOG303 は寄せた色のトークンの図解、LOG269 はタブレットを見る人とハコブくんの4コマ、LOG267 は夜の廊下を走るハコブくんの4コマ](https://pixelstomotors.com/media/LOG473_10_series1_after_lower.png)


## 作業ログ ― 札の名前を HRI から薬剤搬送ロボットにした

記事の札に出る文字は、記事のデータのシリーズの値そのものになっている。そこで、値を HRI から「薬剤搬送ロボット」に替えた。替えた所は次のとおり。

| 場所 | 中身 | 数 |
|---|---|---|
| src/data/records/*.json | Studio から移した記事1本ずつのデータの `series` | 57本 |
| src/data/cards.json | 記事一覧のカードのデータの `series` | 57件 |
| src/lib/records.ts | /series1 のページが、どのシリーズの値の記事を並べるか（`SERIES_PAGES`） | 1か所 |
| src/components/MangaPage.astro | 4コマのページの下に出す「関連する記録」の値 | 1か所 |

ページの紹介文の「人とロボットの関わり（HRI）」のような、文の中の HRI はそのままにした。過去の記事のコードやログの中に出てくる `series: 'HRI'`（LOG344・347・351・460 など）も、その時点の記録なので替えていない。

前の HTML の表紙（public/thumbs/r-*.html）は消さずに残した。データの `cover` を `null` に戻し、`thumb` を前の値に戻せば、前の表紙に戻る。


## 作業ログ ― 表紙を作って入れる手順

[ファイル一式: https://pixelstomotors.com/files/LOG473/index.json](https://pixelstomotors.com/files/LOG473/index.json)

### 使った素材の場所

- 記事の画像：pixelstomotors.com/media/ の4枚（Fusion の画面3枚・ピカリくんの三面図）
- 4コマの扉絵：p2m-site の src/data/manga/ep100.json・ep80.json・ep110.json の `cover`
- 記事の図解：p2m-site の public/embeds/figma-design-token-consolidation-CwxN66LU.html・does-a-delivery-robot-help-nurses-t7lznGbR.html
- 作った表紙：iCloud の `デジタルツイン学習R2R2R/HRI_サムネイル_20261108/`（R2 の `hri-thumb-LOG{番号}.webp`。作り直した回は `-v2`・`-v3` を付けた新しい名前で入れた。upload_media.py は同じ名前の上書きをしないため）

### make_hri_covers.py（全文）― 9枚の表紙を作る

作業の途中では、1枚ずつ値を変えて切り出した。公開した9枚の値を、このスクリプト1本にまとめた。動かし直すと、LOG307・311・322 は公開した画像とバイト単位で同じものができた。

```python
"""薬剤搬送ロボット（/series1）の記事サムネイル9枚を作る（LOG473）。

前は HTML のサムネイル（白黒・英字の見出し）だった9本を、記事の画像・4コマの絵・記事の図解から作った
1920x1080 の webp に替える。素材はすべて公開中のサイト（pixelstomotors.com/media/ と p2m-site の public/embeds/）から取る。

使い方（p2m-site のフォルダで動かす）:
  python3 make_hri_covers.py <出力フォルダ>
斜めに置く図解（LOG303・LOG305）は、同じフォルダの tilt.js（Playwright）で描く。
"""
import json
import os
import subprocess
import sys
import urllib.request
from pathlib import Path

from PIL import Image

OUT = Path(sys.argv[1] if len(sys.argv) > 1 else 'hri_covers')
SRC = OUT / '_src'
HERE = Path(__file__).resolve().parent
SITE = 'https://pixelstomotors.com'

# 記事の画像（Fusion の画面・三面図）
MEDIA = {
    'plate': '/media/s-2642x1806_v-frms_webp_3b3bd195-b7fb-4c65-9229-a6eb4f907f01.png',      # LOG307 40x40x5mm の板
    'parts': '/media/s-2642x1806_v-frms_2fc0ae98-d942-4304-bb8a-09f002fd36e2.webp',          # LOG308 角の丸い箱に穴が開いた最初の形
    'solid': '/media/s-2720x1894_v-frms_webp_6738acd9-b438-4ced-bfe0-a65c630a5b69.png',      # LOG311 ボディ
    'pikari': '/media/s-1376x768_v-fms_1f4bba08-2627-4dd8-b02a-ca45b3e44374.webp',           # LOG322 ピカリくんの三面図
}
# 4コマの扉絵（1080x1350）
MANGA = {'ep100': None, 'ep80': None, 'ep110': None}


def fetch(url, dst):
    if not dst.exists():
        req = urllib.request.Request(url, headers={'User-Agent': 'p2m-covers'})
        dst.write_bytes(urllib.request.urlopen(req, timeout=60).read())
    return dst


def crop_center(src, cx, cy, w, dst):
    """中心 (cx, cy)・幅 w の 16:9 を切り出して 1920x1080 にする"""
    im = Image.open(src).convert('RGB')
    h = w * 9 / 16
    im.crop((int(cx - w / 2), int(cy - h / 2), int(cx + w / 2), int(cy + h / 2))).resize((1920, 1080), Image.LANCZOS).save(dst, quality=85)


def crop_band(src, y, dst):
    """縦長の扉絵から、上から y の位置で横幅いっぱいの 16:9 の帯を切り出す"""
    im = Image.open(src).convert('RGB')
    W = im.size[0]
    h = int(W * 9 / 16)
    im.crop((0, y, W, y + h)).resize((1920, 1080), Image.LANCZOS).save(dst, quality=85)


def main():
    OUT.mkdir(parents=True, exist_ok=True)
    SRC.mkdir(exist_ok=True)
    src = {k: fetch(SITE + v, SRC / Path(v).name) for k, v in MEDIA.items()}
    for ep in MANGA:
        d = json.load(open(f'src/data/manga/{ep}.json', encoding='utf-8'))
        src[ep] = fetch(SITE + d['cover'], SRC / f'{ep}.webp')

    # 記事の Fusion の画面：ウィンドウの中の作業画面だけを、形が真ん中に来るように切る
    crop_center(src['plate'], 1320, 900, 1200, OUT / 'hri-thumb-LOG307.webp')
    crop_center(src['parts'], 1400, 880, 1500, OUT / 'hri-thumb-LOG308-v3.webp')
    crop_center(src['solid'], 1560, 1040, 2050, OUT / 'hri-thumb-LOG311.webp')
    # ピカリくんの三面図：右端の余白を落として 16:9 に
    Image.open(src['pikari']).convert('RGB').crop((0, 0, 1365, 768)).resize((1920, 1080), Image.LANCZOS).save(OUT / 'hri-thumb-LOG322.webp', quality=85)
    # 4コマの扉絵：題名の文字を外し、顔が切れない高さで帯にする
    crop_band(src['ep100'], 560, OUT / 'hri-thumb-LOG267-v2.webp')   # 第100話 夜の運び方
    crop_band(src['ep80'], 330, OUT / 'hri-thumb-LOG269-v2.webp')    # 第80話 過去最高の配送件数
    crop_band(src['ep110'], 300, OUT / 'hri-thumb-LOG321-v2.webp')   # 第110話 すぐ戻ります

    # 記事の図解を斜めに置く（回転 -16°・余白が出ない倍率まで寄せる）
    jobs = [
        ['figma-design-token-consolidation-CwxN66LU.html', 'design-token', -16, 3.3, 380, 380],
        ['does-a-delivery-robot-help-nurses-t7lznGbR.html', 'four-assumptions', -16, 3.0, 330, 600],
    ]
    tmp = OUT / '_tilt'
    (tmp / 'out').mkdir(parents=True, exist_ok=True)
    subprocess.run(['node', str(HERE / 'tilt.js'), json.dumps(jobs)], check=True, env={**os.environ, 'S': str(tmp)})
    for name, log in [('design-token', 303), ('four-assumptions', 305)]:
        Image.open(tmp / 'out' / f'{name}.png').convert('RGB').save(OUT / f'hri-thumb-LOG{log}-v2.webp', quality=85)

    for f in sorted(OUT.glob('*.webp')):
        print(f.name, Image.open(f).size, f.stat().st_size, 'bytes')


if __name__ == '__main__':
    main()
```

### tilt.js（全文）― 記事の図解を斜めに置いて撮る

```javascript
const {chromium}=require('playwright');const fs=require('fs');
const S=process.env.S;const jobs=JSON.parse(process.argv[2]);
(async()=>{const b=await chromium.launch();const p=await b.newPage({viewport:{width:1920,height:1080},deviceScaleFactor:1});
for(const [f,out,rot,sc,tx,ty] of jobs){
 const html=`<html><body style="margin:0;width:1920px;height:1080px;overflow:hidden;background:#e6e6e6">
 <iframe src="file://${process.cwd()}/public/embeds/${f}" style="position:absolute;left:320px;top:40px;width:1280px;height:1000px;border:0;background:#fff;box-shadow:0 30px 60px rgba(0,0,0,.18);transform:translate(${tx}px,${ty}px) rotate(${rot}deg) scale(${sc});transform-origin:center"></iframe></body></html>`;
 fs.writeFileSync(S+'/w.html',html);await p.goto('file://'+S+'/w.html');await p.waitForTimeout(1200);
 await p.screenshot({path:S+'/out/'+out+'.png'});}
await b.close();})();
```

### make_hri_covers.py を動かした出力

```
$ cd p2m-site && python3 make_hri_covers.py hri_covers
hri-thumb-LOG267-v2.webp (1920, 1080) 187264 bytes
hri-thumb-LOG269-v2.webp (1920, 1080) 173130 bytes
hri-thumb-LOG303-v2.webp (1920, 1080) 38134 bytes
hri-thumb-LOG305-v2.webp (1920, 1080) 80708 bytes
hri-thumb-LOG307.webp (1920, 1080) 63254 bytes
hri-thumb-LOG308-v3.webp (1920, 1080) 190294 bytes
hri-thumb-LOG311.webp (1920, 1080) 96758 bytes
hri-thumb-LOG321-v2.webp (1920, 1080) 269894 bytes
hri-thumb-LOG322.webp (1920, 1080) 198886 bytes
```

### R2 に入れたときの出力（Mac で動かした）

1回目（9枚）:

```
$ cd "$HOME/mnt/PixelstoMotors/デジタルツイン学習R2R2R/HRI_サムネイル_20261108" && python3 _tool/upload_media.py .
入れる 9・入っている 0・名前の重なり 0・合計 0.8MB
  9/9
おわり
```

2回目（作り直した6枚）:

```
入れる 6・入っている 9・名前の重なり 0・合計 0.8MB
  6/6
おわり
```

3回目（LOG308）:

```
入れる 1・入っている 15・名前の重なり 0・合計 0.2MB
  1/1
おわり
```

### update_series1.py（全文）― 札の名前と表紙の欄を書き換える

実際には3回に分けて書き換えた（commit 84070ae・60a93b3・39501d2）。このスクリプトは、その3回を合わせた最後の形を1回で作る。書き換える前の commit で動かし、公開中の60ファイルと1文字も違わないことを確かめた。

```python
"""/series1（薬剤搬送ロボット）の記事データを書き換える（LOG473）。

1. シリーズの値 HRI を 薬剤搬送ロボット に変える（記事の札に出る文字。records の json・cards.json・
   src/lib/records.ts の SERIES_PAGES・4コマのページの関連記録）
2. 9本のサムネイルを HTML（/thumbs/r-*.html）から画像（/media/hri-thumb-LOG{N}*.webp）に替える
   （cover に画像を入れ、thumb を null にする。前の HTML は public/thumbs/ に残す）

p2m-site のフォルダで動かす。実際には、作業の途中で3回に分けて書き換えた
（84070ae・60a93b3・39501d2）。このスクリプトは、その3回を合わせた最後の形を1回で作る。
"""
import glob
import json
import re

COVERS = {
    'hospital-operations-digital-twin-system-design': 'hri-thumb-LOG267-v2.webp',
    'predicted-vs-actual-correction-loop': 'hri-thumb-LOG269-v2.webp',
    'figma-design-token-consolidation': 'hri-thumb-LOG303-v2.webp',
    'does-a-delivery-robot-help-nurses': 'hri-thumb-LOG305-v2.webp',
    'autodesk-fusion-mcp-setup': 'hri-thumb-LOG307.webp',
    'claude-instructions-hit-a-wall': 'hri-thumb-LOG308-v3.webp',
    'from-drawing-to-solid': 'hri-thumb-LOG311.webp',
    'avatar-gitai': 'hri-thumb-LOG321-v2.webp',
    'avatar-gitai-pikari': 'hri-thumb-LOG322.webp',
}
OLD, NEW = 'HRI', '薬剤搬送ロボット'


def records():
    n = 0
    for f in glob.glob('src/data/records/*.json'):
        s = open(f, encoding='utf-8').read()
        d = json.loads(s)
        if d.get('series') != OLD:
            continue
        s2 = s.replace(f'"series": "{OLD}"', f'"series": "{NEW}"', 1)
        if d['slug'] in COVERS:
            old_t = json.dumps(d['thumb'], ensure_ascii=False)
            s2 = s2.replace('"cover": null, "thumb": ' + old_t, f'"cover": "/media/{COVERS[d["slug"]]}", "thumb": null', 1)
            assert COVERS[d['slug']] in s2, f
        json.loads(s2)
        open(f, 'w', encoding='utf-8').write(s2)
        n += 1
    print('records', n)


def cards():
    t = open('src/data/cards.json', encoding='utf-8').read()
    print('cards', t.count(f'"series": "{OLD}"'))
    t = t.replace(f'"series": "{OLD}"', f'"series": "{NEW}"')
    for slug, name in COVERS.items():
        pat = re.compile(r'("slug": "%s",\n"cover": )null(,\n"thumb": )"/thumbs/r-%s\.html"' % (re.escape(slug), re.escape(slug)))
        t, k = pat.subn(r'\1"/media/%s"\2null' % name, t)
        assert k == 1, (slug, k)
    json.loads(t)
    open('src/data/cards.json', 'w', encoding='utf-8').write(t)


def code():
    for f, a, b in [
        ('src/lib/records.ts', "series1: { series: 'HRI'", "series1: { series: '薬剤搬送ロボット'"),
        ('src/components/MangaPage.astro', "seriesCards('HRI')", "seriesCards('薬剤搬送ロボット')"),
        ('src/components/MangaPage.astro', 'Studio と同じく HRI の新しい6件', 'Studio と同じく 薬剤搬送ロボット（series1）の新しい6件'),
    ]:
        t = open(f, encoding='utf-8').read()
        assert a in t, (f, a)
        open(f, 'w', encoding='utf-8').write(t.replace(a, b))


if __name__ == '__main__':
    records()
    cards()
    code()
```

```
$ python3 update_series1.py
records 57
cards 57
```

### shot_series1.js（全文）― 公開した一覧の画面を撮る

```javascript
const {chromium}=require('playwright');
(async()=>{const b=await chromium.launch();const p=await b.newPage({viewport:{width:1440,height:1000},deviceScaleFactor:2});
await p.goto('https://pixelstomotors.com/series1?z=1');await p.waitForTimeout(1500);
await p.addStyleTag({content:'*{scroll-behavior:auto!important}'});
const cards=await p.$$('.rcard');
await cards[0].scrollIntoViewIfNeeded();
const y0=await p.evaluate(e=>e.getBoundingClientRect().top+window.scrollY,cards[0]);
for(const [i,y] of [[9,y0-90],[10,y0-90+520]]){await p.evaluate(v=>window.scrollTo(0,v),y);await p.waitForTimeout(2500);await p.evaluate(()=>{for(const e of document.querySelectorAll('body *')){const c=getComputedStyle(e);if(c.position==='fixed'&&!e.closest('header'))e.style.display='none'}});
await p.screenshot({path:process.env.W+`/LOG473_${String(i).padStart(2,'0')}_series1_after_${i==9?'upper':'lower'}.png`});}
await b.close();})();
```

### ビルドと公開

```
$ npm run build
17:43:28 [@astrojs/sitemap] `sitemap-index.xml` created at `dist`
17:43:28 [build] 835 page(s) built in 30.48s
17:43:28 [build] Complete!
$ git log --oneline -3
39501d2 LOG308のサムネイルを記事の Fusion の画面（板・箱・青い面）に差し替え
60a93b3 series1: 似たサムネイルを差し替え（斜めの図解は2本に絞り余白なしに、残りは4コマの絵・三面図）
84070ae series1: 白黒サムネイル9本を他のHRI記事と同じ画像の形に、タグ「HRI」を「薬剤搬送ロボット」に
```
