配膳ロボットのシミュレーションを表紙で伝える ― サムネイル37本を Unity の店に

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

サービスロボット導入シミュレーションのシリーズ(pixelstomotors.com/series8 )の記事サムネイル37枚を、白黒の英字の見出しを組んだ表紙から、Unity で動かしたファミレスの画像だけの表紙に替えた。 記事一覧のカードを見ただけで、配膳ロボットとスタッフと客が動く3Dの店のシミュレーションの記録だと分かるようにするためだ。並べ方は8種類(1枚の寄り・左右・上下の帯・縦の列・格子・大1枚と小2枚・引きに寄りの枠・店を丸ごと)を回ごとに変え、画面の幅で3列・2列・1列に変わるどの並びでも、上下左右の隣に同じ並べ方が来ないようにした。

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

  • シリーズのページを開いた人が、記事を開く前に、ロボット(白い筒)・スタッフ(青い筒)・客(黄色い筒)が動く店の様子を見られる。前は、37本のうち32本が英語の見出しと図を組んだ白黒の表紙で、トップページのシリーズのカード(Unity の店の画像)とつながって見えなかった
  • にらみ合い・下げ膳・大型店のように、その回の話の場面に寄せた表紙がある。たとえば2台のロボットが向かい合った回(LOG434)は、ロボットの通った跡の記録から2台がいちばん近づいた時刻を探し、その場面に寄った枠を重ねた
  • 37枚は、スクリプト1本(make.py)で作り直せる。2コアの CPU で37枚を通しで作って約40〜56秒。前の表紙は1枚ずつ HTML で組んでいて、作るのにかかった時間は測っていない
  • 依頼(17:54)から37枚の公開(18:19)まで25分。そのあと、並べ方の重なりを直した6枚を 18:35 に公開した

サムネイルは、記事一覧のカードの上に出る絵のこと。サービスロボット導入シミュレーションは、配膳ロボットを店に入れる前に、待ち時間・人数・止まる場所・費用を試算する記録で、時間と台数はサイトの試算で、店の中の動きは Unity(ゲームを作る道具で、ここではファミレスを3Dで建ててロボットと人を動かす画面)で扱っている。ところが記事一覧のカードは、ほかのシリーズと同じ、英字の見出しと図を組んだ白黒の表紙が並んでいた。

替える前のシリーズのページの一覧。LOG434・426・423・418・415・414 の表紙が、白黒の地に英字の見出し(TWO ROBOTS. NOBODY YIELDS. など)と図を組んだ形で並んでいる

依頼は、トップページのシリーズのカードの画像と一緒に届いた。

トップページのサービスロボット導入シミュレーションのカード。Unity で建てたファミレスを斜め上から見た画像を、背景から切り抜いて明るい灰色の地に置いている

サービスロボット導入シミュレーション | Service Robot Simulationの記事のサムネイルをTOPのようにシミュレーションの画像を使ったサムネイルに全部変更(TOPのように切り抜きで使う必要はないです。上下左右に画像並べたり、アップにしたり同じようなサムネイルが並ばないように工夫してください)

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

順やったことなぜ結果
1
Unity の録画から、静止画を56枚切り出した
表紙はシミュレーションの画像だけで作るため
斜めの引き・寄り・真上から・下げ膳・大型店の画像がそろった
2
背景を明るい灰色に置き換え、ロボットとスタッフの位置を色で見つけて寄せる仕組みを作った
寄った画像が、厨房の壁や空いた床ばかりになったため
寄りの表紙が、ロボットのいる場面に寄るようになった
3
37枚を8種類の並べ方で作って公開した
同じようなサムネイルを並べないため
1枚の寄りが12枚あり、3列の並びで隣り合う所が6組あった
4
6枚の並べ方を変え、2列・1列の並びも調べた
サイトの一覧は、画面の幅で2列・1列にもなるため
どの並びでも、隣に同じ並べ方が来なくなった

このあと、サムネイルの決まりが、シリーズごとに違うものになった。記事化の途中で、次の指示を受けた。

サムネイルのルールがシリーズごとに変わりました。そのためのルールづくりを確認してください。

前のサムネイルルールは、ひとり研究所の裏側 | Behind the Solo Labのみに適用です。

そこで、記事の決まり(article_rules.md)のサムネイルの節を、シリーズごとの表に書き直した。この記事の最後の節に、その表を載せている。

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

作業ログ ― Unity の録画から、表紙に使う静止画を56枚切り出した

表紙の素材は、Unity で回したシミュレーションの録画から取った。録画は KIOXIA(外付けの SSD)の service_sim/unity/ に、夜に回した回ごとのフォルダで入っている。4K(3840×2160)で斜め上から撮った引きの録画、真上から撮った録画、問題の場所に寄る試し撮り、大型店の録画、下げ膳の録画がある。

静止画は、ffmpeg で切り出した。ffmpeg は、動画の変換や切り出しをする道具で、ここでは録画の決まった秒数の1コマを、そのままの大きさの JPEG にするのに使った。Mac の上で切り出し、iCloud の サービスロボット導入シミュレーション/thumb_data/ に置いた(録画そのものは重いので、Mac から動かさない)。

素材の名前何の録画か大きさ枚数
top_p0〜p5・top_p3_200/700
6店を真上から撮った録画2560×1726〜20808
obl_p0〜p5_l30/r30
6店を60°見下ろし・左右30°から撮った引き
3840×216012
close_*
③の店の通路・受け渡し口・入口に寄った試し撮り
1920×10807
bus_*下げ膳の録画の引き3840×21605
large_*
大型店⑧⑨の引きと、問題の場所に寄った画面
3840×2160 など10
seq_*同じ録画の別の時刻3840×2160 など6
face_*
ロボット2台がいちばん近づいた時刻(下の節)
3840×21608
切り出した静止画56枚の一覧。真上から見た6店、斜めから見た店、通路に寄った画面、大型店、問題の場所に丸を付けた画面などが並ぶ

大型店の「寄り」の画面(large_p6_p01b_focus など)は、字幕と丸の印が入った 1280×720 の動画から切り出したもので、表紙に使うには小さく、字幕も入っている。表紙には使わなかった。

作業ログ ― Unity の背景を、トップのカードと同じ明るい灰色に置き換えた

Unity の録画の背景は、茶色がかった灰色(RGB 103, 98, 94)の一色で、店の外に広く出ている。このまま並べると、どのカードも外側が暗い茶色になり、トップのカードの明るい灰色の地とも合わない。

そこで、背景の色に近く、画面の端につながっている所だけを、トップのカードと同じ明るい灰色(#eceef2)に置き換えた。端につながっている所だけにしたのは、店の中の灰色(壁の上面やキッチンの屋根)まで消さないためだ。暗い版の表紙3枚(店を丸ごと見せる2枚と、格子の1枚)は、墨色(#111111)に置き換えた。

ねらい:トップのカードとシリーズのページの表紙を、同じ地の色でつなげる。 変わったこと:表紙の外側が、茶色がかった灰色から、トップのカードと同じ明るい灰色になった。 見込める効果:トップのカードから /series8 に来た人が、同じシミュレーションの続きの記事だと見て分かる(見込み。確かめる数字はまだない)。

作業ログ ― 寄りの表紙が厨房の壁と床ばかりになり、ロボットの位置に寄せる形にした

最初は、店の範囲の中の位置(左から何割・上から何割)を回ごとに指定して切り出した。37枚を並べてみると、寄りの表紙の多くが、灰色の厨房の屋根か、だれもいない床で埋まっていた。配膳ロボットは受け渡し口(厨房の前)の近くにいることが多く、そこに寄ると厨房の大きな屋根が画面の半分を占める。

1回目と公開した形の比べ。LOG434 は、1回目は寄りの枠が厨房の屋根だけを映していたのが、公開した形ではロボット2台と卓を映している。LOG366 は、1回目は屋根と空いた床だったのが、ロボット2台と卓と客の画面になった。LOG435 は、1回目は同じ卓の帯が3段続いていたのが、真上から見たロボットとスタッフの格子になった

そこで、画面の中のロボットとスタッフの位置を、色で見つけて寄せる形にした。Unity の配膳ロボットは白い筒、スタッフは青い筒で描かれている。白に近い色(3色とも 200 より明るく、差が 14 より小さい)と、青(青が 150 より大きく、赤が 80・緑が 120 より小さい)のかたまりを探し、大きい順に並べる。表紙の指定に 'r0'(いちばん大きいロボット)・'rr'(ロボット2台の中間)・'s0'(いちばん大きいスタッフ)と書くと、その位置を中心に切り出す。

ロボット2台がぴったり並ぶと、白いかたまりが1つにつながることがある。その場合は 'r0' で寄せた。

ねらい:寄りの表紙に、必ずロボットかスタッフが入るようにする。 変わったこと:寄りの表紙の中心を、手で決めた位置から、画面の中で見つけたロボット・スタッフの位置に変えた。 見込める効果:表紙を見ただけで、配膳ロボットの記録だと分かる(見込み。並びは公開した一覧の画面で確かめた)。

作業ログ ― ロボット2台がいちばん近づいた時刻を、通った跡の記録から探した

にらみ合いの回(LOG434)や、止まり合いの回の表紙には、ロボット2台が近づいている場面がほしい。録画を見て探すと時間がかかるので、Unity が書き出したロボットの通った跡の記録(robot_track.csv)を使った。この記録には、5秒ごとに、ロボットごとの位置(x, z)と、そのとき何をしていたか(受け渡し口へ・卓へ・待機 など)が入っている。

2台とも待機していない時刻のうち、2台の間がいちばん近い時刻を探し、録画の何秒目にあたるかを *_frames.csv(録画の1コマごとの、シミュレーションの中の時刻)で調べた(near.py)。6店の結果は次のとおり。

$ for p in 0 1 2 3 4 5; do python3 $HOME/scan/near.py $(ls -d 20261103_rec_focus2_cs62_fam_p${p}*) ; done
0.52 8500.0 ['往復おわり', '往復おわり'] video_s 566.7
0.52 8505.0 ['往復おわり', '往復おわり'] video_s 567.0
0.52 8510.0 ['往復おわり', '往復おわり'] video_s 567.3
0.52 8515.0 ['往復おわり', '往復おわり'] video_s 567.7
0.52 8520.0 ['往復おわり', '往復おわり'] video_s 568.0
20261103_rec_focus2_cs62_fam_p0_r2_s8_passon_sbret_ek_post/recordings/20261103_rec_focus2_cs62_fam_p0_r2_s8_passon_sbret_ek_post_wide_l30.mp4 30.0 20440
0.43 5230.0 ['受け渡し口へ', '卓へ'] video_s 348.7
0.43 5235.0 ['受け渡し口へ', '卓へ'] video_s 349.0
0.44 5240.0 ['受け渡し口へ', '卓へ'] video_s 349.3
0.44 5245.0 ['受け渡し口へ', '卓へ'] video_s 349.7
0.47 2395.0 ['受け渡し口へ', '卓へ'] video_s 159.7
20261103_rec_focus2_cs62_fam_p1_r2_s7_passon_sbret_ek_post/recordings/20261103_rec_focus2_cs62_fam_p1_r2_s7_passon_sbret_ek_post_wide_l30.mp4 30.0 23648
0.46 6120.0 ['受け渡し口へ', '卓へ'] video_s 408.0
0.46 6125.0 ['受け渡し口へ', '卓へ'] video_s 408.3
0.47 6130.0 ['受け渡し口へ', '卓へ'] video_s 408.7
0.5 4305.0 ['卓へ', '卓へ'] video_s 287.0
0.5 4310.0 ['卓へ', '卓へ'] video_s 287.3
20261103_rec_focus2_cs62_fam_p2_r2_s1_passon_sbret_ek_post/recordings/20261103_rec_focus2_cs62_fam_p2_r2_s1_passon_sbret_ek_post_wide_l30.mp4 30.0 20075
0.4 7395.0 ['卓へ', '往復おわり'] video_s 493.0
0.47 9605.0 ['卓へ', '卓へ'] video_s 640.3
0.47 9610.0 ['卓へ', '卓へ'] video_s 640.7
0.47 9615.0 ['卓へ', '卓へ'] video_s 641.0
0.53 3410.0 ['卓へ', '卓へ'] video_s 227.3
20261103_rec_focus2_cs62_fam_p3_r2_s1_passon_sbret_ek_post/recordings/20261103_rec_focus2_cs62_fam_p3_r2_s1_passon_sbret_ek_post_wide_l30.mp4 30.0 19355
0.47 4495.0 ['受け渡し口へ', '卓へ'] video_s 299.7
0.58 6915.0 ['卓へ', '受け渡し口へ'] video_s 461.0
0.59 1570.0 ['卓へ', '受け渡し口へ'] video_s 104.7
0.6 6720.0 ['受け渡し口へ', '卓へ'] video_s 448.0
0.6 7140.0 ['受け渡し口へ', '卓へ'] video_s 476.0
20261103_rec_focus2_cs62_fam_p4_r2_s1_passon_sbret_ek_post/recordings/20261103_rec_focus2_cs62_fam_p4_r2_s1_passon_sbret_ek_post_wide_l30.mp4 30.0 21394
0.43 7895.0 ['受け渡し口へ', '卓へ'] video_s 526.3
0.43 7900.0 ['受け渡し口へ', '卓へ'] video_s 526.7
0.47 7905.0 ['受け渡し口へ', '卓へ'] video_s 527.0
0.5 9970.0 ['受け渡し口へ', '卓へ'] video_s 664.7
0.51 3325.0 ['受け渡し口へ', '卓へ'] video_s 221.7
20261103_rec_focus2_cs62_fam_p5_r2_s7_passon_sbret_ek_post/recordings/20261103_rec_focus2_cs62_fam_p5_r2_s7_passon_sbret_ek_post_wide_l30.mp4 30.0 24782

この中から、2台が卓や通路の近くにいる時刻を選んで切り出した。

素材の名前店録画の秒2台の間(m)2台がしていたこと
face_p1_l30_349・face_p1_r30_349
p1349.00.43受け渡し口へ・卓へ
face_p1_l30_159
p1159.70.47受け渡し口へ・卓へ
face_p2_l30_408
p2408.30.46受け渡し口へ・卓へ
face_p3_r30_640
p3640.50.47卓へ・卓へ
face_p4_l30_299
p4299.70.47受け渡し口へ・卓へ
face_p4_r30_461
p4461.00.58卓へ・受け渡し口へ
face_p5_r30_526
p5526.50.43受け渡し口へ・卓へ

録画の秒は、1コマごとの時刻の表から計算した値で、0.5秒ほどずれることがある。表紙では、切り出した画面の中のロボットの位置で寄せ直しているので、ずれは表紙に出ない。

作業ログ ― 37枚を8種類の並べ方に振り分けた

並べ方は、次の8種類にした。どれも文字は入れていない。

並べ方中身使った回の例
1枚の寄り
1枚を全面に。ロボットや客のいる所に寄る
LOG440(下げ膳の回のロボットと卓)
左右
2枚を左右に。比べる回は同じ場面の左30°と右30°
LOG404(動画で見比べる回)
上下の帯2枚を上下の帯に
LOG456(大型店の受け渡し口と通路)
縦の列3枚か5枚の縦の列
LOG356(5つの現場の回は5店)
格子2×2・3×2・4×3
LOG360(6店の平面図の回は6店の真上から)
大1+小2大きい1枚と小さい2枚
LOG426(動画が詰まる所に寄る回)
引き+寄りの枠
店の引きに、ロボットに寄った枠を重ねる
LOG434(にらみ合いの回)
店を丸ごと
トップのカードと同じく、店全体を地の上に
LOG357(Unity にファミレスを建てた回)

記事の中身に合わせられる回は、合わせた。道の線の回(LOG374)は、真上から見た録画にロボットの道の線が写っている場面、6店の平面図の回(LOG360)は6店の真上から、大型店の回は大型店の録画にした。

公開したあと、並べ方ごとに数えて、記事一覧と同じ3列の並びで上下左右の隣を調べた。1枚の寄りが12枚あり、次の6組が隣り合っていた。

並び隣り合っていた2枚並べ方
3列
LOG456 と LOG440(上下)
1枚の寄り
3列
LOG440 と LOG435(左右)
1枚の寄り
3列
LOG384 と LOG379(上下)
1枚の寄り
3列
LOG379 と LOG374(左右)
1枚の寄り
3列
LOG374 と LOG366(上下)
1枚の寄り
3列
LOG359 と LOG353(上下)
1枚の寄り

LOG456 と LOG384 を上下の帯、LOG374 を左右、LOG353 を大1+小2、LOG435 を引き+寄りの枠に変えた。続けて、サイトの一覧は画面の幅で2列(1199px 以下)と1列にも変わるので、その並びも調べた。2列では LOG415 と LOG409(どちらも1枚の寄り)、1列では LOG435 と LOG434(どちらも引き+寄りの枠)が隣り合っていたので、LOG409 を縦の列、LOG435 を格子に変えた。これで、1列・2列・3列のどれでも、隣に同じ並べ方が来ない形になった。LOG466(別の作業のときに、同じ作り方で足した38本目)を先頭に入れても、隣り合いは出なかった。

ねらい:一覧のどの幅でも、同じ形の表紙を隣に並べない。 変わったこと:1枚の寄りを12枚から6枚に減らし、ほかの並べ方に振り分けた。1列・2列・3列で、隣に同じ並べ方が来た所が0組になった。 見込める効果:スマホ(1列)やタブレット(2列)で見ても、同じような表紙が続かない(見込み。3列と2列の画面は公開後に撮って確かめた)。

並べ方ごとの回を、図にした。列の数のボタンで3列・2列・1列を、並べ方のボタンで、その形の回を切り替えられる。

作り直した37枚を、記事一覧と同じ新しい順に3列で並べた一覧。斜めの店の引き、真上から見た平面、ロボットに寄った画面、墨色の地に店を置いた表紙などが混ざっている

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

作り直した後のシリーズのページの一覧の上の段。LOG466 は左右、LOG465 は縦の列、LOG456 は上下の帯、LOG452 は店を丸ごと、LOG444 は左右、LOG440 は1枚の寄り
一覧の下の段。LOG435 は真上から見た格子、LOG434 は引きに寄りの枠、LOG426 は大1+小2、LOG423 は左右、LOG418 は4×3の格子、LOG415 は真上から見た1枚

作業ログ ― 表紙は画像(webp)にし、サムネイルの HTML は画像を見せるだけにした

前の表紙は、記事ごとの HTML(public/thumbs/LOG{番号}.html)に図を組んだものだった。今回の表紙は、make.py で 1920×1080 の PNG を書き出し、publish.py で webp にして public/thumbs/img/LOG{番号}.webp に置き、HTML はその画像を全面に見せるだけの形に書き換えた。画像認識のシリーズ(LOG472)と同じ入れ方にした。

表紙が画像(cover)だった5本(LOG364・374・382・404・414)は、記事の頭の cover を新しい webp に向けた。前の画像は R2 の /media/LOG{番号}_cover.jpg に残っている。前の HTML の表紙は git の履歴に残っているので、戻すときは commit d669632 の前の版を取り出す。

表紙が重くなったことも書いておく。HTML だった32本で比べると、前の HTML は合わせて 1,067,970 バイト(1本あたり約33KB)、新しい HTML と webp は合わせて 2,859,476 バイト(1本あたり約89KB)で、約2.7倍になった。一覧のカードは画像を読み込むので、一覧ページの重さはそのぶん増える。画面での表示の速さは測っていない。

作業ログ ― サムネイルの決まりを、シリーズごとの表に書き直した

同じ日に、ほかのシリーズのサムネイルもそれぞれ作り直していた(R2R2R は LOG470、フィジカルAI研究室は LOG471、画像認識は LOG472、薬剤搬送ロボットは LOG473)。記事の決まり(article_rules.md)のサムネイルの節には、まだ「配色モノクロ・英字の見出し」の1つの決まりしかなく、作り直したシリーズの形と食い違っていた。

指示を受けて、決まりを3つに分けた。

  1. 全シリーズ共通:16:9・最大 1920×1080、LOG 番号や題名を焼き込まない、同じようなサムネイルは2枚くらいまで、斜めに置くなら寄せて余白を出さない、本人が写った写真は使わない
  2. シリーズごとの表:表紙の形・文字・作り方と入れ方・見本の記事
  3. 前の決まり(モノクロ・英字の見出し・質の基準)は、ひとり研究所の裏側だけに当てはめる節に移した

決める途中で、2つを選んでもらった。記事に載せた動画のコマを優先する決まり(LOG470 で決めた)は、全シリーズ共通にせず、R2R2R だけの決まりにした(回答は「それぞれのシリーズのルールに沿ってサムネイルは作成」)。画像で作るシリーズの入れ方(R2 の画像を cover に書くか、webp と入れ物の HTML か)も、そろえずにシリーズごとのままにした(回答は「シリーズによってサムネイルルールを作成」)。

書き直したシリーズごとの表は次のとおり。

シリーズ(ページ)表紙の形文字作り方・入れ方見本
ひとり研究所の裏側(/series4)
モノクロ+英字の見出し英字の見出し
LOG{NNN}_thumbnail.html
R2R2R(/series2)
動画の1コマ → 実機の写真 → 図解の図、の順
なし
webp を R2 に入れ、記事の頭に cover
LOG470
薬剤搬送ロボット(/series1)
記事の画像・4コマの扉絵・図解を全面に
なし
webp を R2 に入れ、記事の頭に cover
LOG473
考察記事(/series5)問いの文を3段階の強弱で組む問いの文
deep_dive_thumb.py
/series5 の一覧
フィジカルAI研究室(/series6)
実データの線と点。紙色・墨・赤・青
小さなラベルだけlab_thumb.pyLOG471
画像認識・物体検出(/series7)
白黒の実写+測った数字1つ+差し色1つ
小さなラベルと数字
vision_thumbs/make.py
LOG472
導入シミュレーション(/series8)
Unity の店の画像。背景はトップと同じ明るい灰色。並べ方は回ごとに変える
なし
sim_thumbs/make.py
LOG474
UV-C農業ロボット(/series3)
2本だけ。次の記事を書くときに決める
―――

あわせて、p2m-site の README にあった「GPU を使った回のサムネイルは、GPU ラックの写真を使う」という1行を、「サムネイルの決まりはシリーズごと。正は article_rules.md」に直した(GPU ラックの写真の形は、同じ日に使わないと決めていた)。

この記事はひとり研究所の裏側のシリーズなので、この記事の表紙は、前の決まりのとおり、モノクロと英字の見出しで作った。

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

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

ファイルの一覧(1本)

01_make.py.txt ― make.py(全文)― 38枚の表紙を作る・9,342字

使った素材の場所

  • 録画:KIOXIA の /Volumes/KIOXIA/r2r2r-data/service_sim/unity/(読むだけ)
  • 切り出した静止画:iCloud の デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/thumb_data/(56枚)
  • 作り方のコード:p2m-site の tools/sim_thumbs/make.py・publish.py
  • 作った表紙:p2m-site の public/thumbs/img/LOG{番号}.webp、SNS 用の画像は public/og/LOG{番号}.jpg

extract_frames_1.sh ― 録画から静止画38枚を切り出す(Mac で動かした)

cd $HOME/mnt/KIOXIA/r2r2r-data/service_sim/unity && OUT="$HOME/mnt/PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/thumb_data"; mkdir -p "$OUT"; 
ex(){ ffmpeg -v error -y -ss $2 -i "$1" -frames:v 1 -q:v 2 "$OUT/$3.jpg"; }
for p in 0 1 2 3 4 5; do f=$(ls 20261031_rec_cs52_fam_p${p}_r2_s*_passon_sbret_ek_post/recordings/*whole.mp4); ex $f 420 top_p${p}; done
for p in 0 1 2 3 4 5; do for s in l30 r30; do f=$(ls 20261103_rec_focus2_cs62_fam_p${p}_r2_s*/recordings/*wide_${s}.mp4); ex $f 380 obl_p${p}_${s}; done; done
for k in aisle16_l30 aisle16_r30 dish16_l30 dish16_r30 dish20_l30 door14_r30 door18_r30; do ex 20261102_rec_oblique4_cs56_fam_p1_r2_s7_passon_sbret_ek_post/recordings/*_${k}.mp4 12 close_${k}; done
for p in 2 4 5; do f=$(ls 20261105_rec92_cs78_fam_p${p}_r2_s*_rescue_bus/recordings/*wide_l30.mp4); ex $f 600 bus_p${p}_l30; f=$(ls 20261105_rec92_cs78_fam_p${p}_r2_s*_rescue_bus/recordings/*wide_r30.mp4); ex $f 750 bus_p${p}_r30; done
ex rec_large_20261105/large_p6/serve/20261105_rec94_cs79_large_p6_r2_s7_passon_sbret_ek_post_rescue_wide_l30.mp4 500 large_p6_l30
ex rec_large_20261105/large_p6/serve/20261105_rec94_cs79_large_p6_r2_s7_passon_sbret_ek_post_rescue_wide_r30.mp4 800 large_p6_r30
f=$(find rec_large_20261105/large_p7 -name "*wide_l30.mp4" | grep -v panel | head -1); ex $f 500 large_p7_l30
f=$(find rec_large_20261105/large_p7 -name "*wide_r30.mp4" | grep -v panel | head -1); ex $f 800 large_p7_r30
for k in p01b_focus p02b_focus p03b_focus; do ex rec_large_20261105/large_p6/serve/focus_v1/$k.mp4 3 large_p6_$k; done
ex rec_base8_20261108/slow_std/compare_serve_bus_large_p6_std.mp4 30 cmp_large
ls "$OUT" | wc -l; du -sh "$OUT"
38
11M	/sessions/rcw-01levexutryzmmzaz9rd38ne/mnt/PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/thumb_data

extract_frames_2.sh ― 同じ録画の別の時刻を10枚足す

cd $HOME/mnt/KIOXIA/r2r2r-data/service_sim/unity && OUT="$HOME/mnt/PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/thumb_data"; ex(){ ffmpeg -v error -y -ss $2 -i "$1" -frames:v 1 -q:v 2 "$OUT/$3.jpg"; }
f=$(ls 20261103_rec_focus2_cs62_fam_p1_r2_s7*/recordings/*wide_r30.mp4); for t in 150 260 520; do ex $f $t seq_p1_r30_$t; done
f=$(ls 20261102_rec_oblique4_cs56_fam_p1_r2_s7_passon_sbret_ek_post/recordings/*aisle16_l30.mp4); for t in 3 9 17; do ex $f $t seq_aisle_$t; done
f=$(ls 20261031_rec_cs52_fam_p3_r2_s1_passon_sbret_ek_post/recordings/*whole.mp4); for t in 200 700; do ex $f $t top_p3_$t; done
f=rec_large_20261105/large_p6/serve/20261105_rec94_cs79_large_p6_r2_s7_passon_sbret_ek_post_rescue_wide_r30.mp4; for t in 300 1000; do ex $f $t large_p6_r30_$t; done
ls "$OUT" | wc -l
48

near.py ― ロボット2台がいちばん近づいた時刻を探す

import csv, sys, glob, math, subprocess, collections
d=sys.argv[1]; vid=sorted(glob.glob(d+'/recordings/*wide_l30.mp4'))[0]
fr=[float(r['t_s']) for r in csv.DictReader(open(vid[:-4]+'_frames.csv'))]
fps=float(eval(subprocess.check_output(['ffprobe','-v','error','-select_streams','v','-show_entries','stream=r_frame_rate','-of','csv=p=0',vid]).decode().strip()))
by=collections.defaultdict(dict)
for r in csv.DictReader(open(d+'/robot_track.csv')):
    by[float(r['t_s'])][r['robot_id']]=(float(r['x']),float(r['z']),r['phase'])
best=[]
for t,rs in by.items():
    v=list(rs.values())
    if len(v)<2: continue
    if any(p=='待機' for _,_,p in v): continue
    dd=math.hypot(v[0][0]-v[1][0],v[0][1]-v[1][1])
    best.append((dd,t,[p for *_,p in v]))
best.sort()
import bisect
for dd,t,ph in best[:5]:
    i=bisect.bisect_left(fr,t); print(round(dd,2),t,ph,'video_s',round(i/fps,1))
print(vid, fps, len(fr))

extract_frames_3.sh ― 2台が近づいた時刻を8枚切り出す

cd $HOME/mnt/KIOXIA/r2r2r-data/service_sim/unity && OUT="$HOME/mnt/PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/thumb_data"; ex(){ ffmpeg -v error -y -ss $2 -i "$1" -frames:v 1 -q:v 2 "$OUT/$3.jpg"; }
for spec in "1 349.0 l30" "1 349.0 r30" "2 408.3 l30" "3 640.5 r30" "4 299.7 l30" "5 526.5 r30" "1 159.7 l30" "4 461.0 r30"; do set -- $spec; f=$(ls 20261103_rec_focus2_cs62_fam_p$1*/recordings/*wide_$3.mp4); ex $f $2 face_p$1_$3_${2%.*}; done; ls "$OUT" | grep face
face_p1_l30_159.jpg
face_p1_l30_349.jpg
face_p1_r30_349.jpg
face_p2_l30_408.jpg
face_p3_r30_640.jpg
face_p4_l30_299.jpg
face_p4_r30_461.jpg
face_p5_r30_526.jpg

make.py(全文)― 38枚の表紙を作る

LOG466 の1行は、別の作業のときに足したもの。

01_make.py.txt9,342字・180行・python
"""サービスロボット導入シミュレーション(/series8)の記事サムネイル37本を作る(2026-11-08)。
TOP のカードと同じく、Unity で動かした店の画像を主役にする。文字は入れない。
並べ方(1枚・左右・上下・3列・格子・大小の組み合わせ・切り抜き・真上から)は回ごとに変え、一覧(LOG番号の大きい順)で同じ形が続かないようにする。
Unity の背景(茶色がかった灰色)は、TOP のカードと同じ明るい灰色に置き換える(暗い版は墨色)。

素材は KIOXIA の service_sim/unity の録画から ffmpeg で切り出した静止画(iCloud の
「サービスロボット導入シミュレーション/thumb_data/」。切り出しの時刻はファイル名と make.py の SRC のコメント)。
Mac 連携で /mnt/user-data/uploads/ に写してから動かす。
別のタブで開く保存

make.py を動かした出力

(LOG466 を足す前に、37枚を通しで作り直したときの出力。2コアの CPU で 56.3 秒だった。最初に動かしたときは 38.0 秒)

$ python3 make.py
LOG 465
LOG 456
LOG 452
LOG 444
LOG 440
LOG 435
LOG 434
LOG 426
LOG 423
LOG 418
LOG 415
LOG 414
LOG 409
LOG 405
LOG 404
LOG 402
LOG 400
LOG 398
LOG 397
LOG 393
LOG 391
LOG 384
LOG 383
LOG 382
LOG 379
LOG 374
LOG 370
LOG 367
LOG 366
LOG 364
LOG 362
LOG 360
LOG 359
LOG 357
LOG 356
LOG 353
LOG 351

publish.py(全文)― webp にして、サムネイルの HTML を書き換える

"""make.py の out/LOG{N}.png を webp にして public/thumbs/img/ に置き、記事のサムネイルの HTML を、その画像を見せるだけの形に書き換える(2026-11-08)。
表紙が画像(cover)だった5本(LOG364・374・382・404・414)は、記事の頭の cover を /thumbs/img/LOG{N}.webp に向ける(前の絵は R2 の /media/LOG{N}_cover.jpg に残っている)。
前の HTML のサムネイルは git の履歴に残っている。書き換えたあと tools/render_og.py で SNS 用の画像も作り直す。"""
import os, re
from PIL import Image
from make import JOBS
SITE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
HERE = os.path.dirname(os.path.abspath(__file__))
COVER = [364, 374, 382, 404, 414]
HTML = '''<!DOCTYPE html>
<!-- サービスロボット導入シミュレーションのサムネイル。画像は tools/sim_thumbs/make.py で作る(2026-11-08) -->
<html lang="ja"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>*{{margin:0;padding:0}}html,body{{background:#eceef2;height:100%;overflow:hidden}}img{{display:block;width:100%;height:100%;object-fit:cover}}</style>
</head><body><img src="img/LOG{n}.webp" alt=""></body></html>
'''
os.makedirs(f'{SITE}/public/thumbs/img', exist_ok=True)
for n in JOBS:
    Image.open(f'{HERE}/out/LOG{n}.png').convert('RGB').save(f'{SITE}/public/thumbs/img/LOG{n}.webp', quality=86)
    open(f'{SITE}/public/thumbs/LOG{n}.html', 'w').write(HTML.format(n=n))
    if n in COVER:
        p = f'{SITE}/src/content/records/LOG{n}.md'
        s = open(p).read()
        s2 = re.sub(r'^cover: ".*"$', f'cover: "/thumbs/img/LOG{n}.webp"', s, count=1, flags=re.M)
        if s2 != s: open(p, 'w').write(s2)
    print(n)

SNS 用の画像を書き出した出力

$ python3 tools/render_og.py LOG465 LOG456 LOG452 LOG444 LOG440 LOG435 LOG434 LOG426 LOG423 LOG418 LOG415 LOG414 LOG409 LOG405 LOG404 LOG402 LOG400 LOG398 LOG397 LOG393 LOG391 LOG384 LOG383 LOG382 LOG379 LOG374 LOG370 LOG367 LOG366 LOG364 LOG362 LOG360 LOG359 LOG357 LOG356 LOG353 LOG351
書き出し 37 件 → public/og/
$ python3 tools/render_og.py LOG456 LOG435 LOG409 LOG384 LOG374 LOG353
書き出し 6 件 → public/og/

shot_series8.js(全文)― 公開した一覧の画面を撮る

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/series8?z=1');await p.waitForTimeout(1500);
await p.addStyleTag({content:'*{scroll-behavior:auto!important}'});
const cards=await p.$$('.rcard');
const y0=await p.evaluate(e=>e.getBoundingClientRect().top+window.scrollY,cards[0]);
for(const [i,y] of [[6,y0-90],[7,y0-90+770]]){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+`/LOG474_0${i}_series8_after_${i==6?'upper':'lower'}.png`});}
console.log(cards.length);await b.close();})();

ビルドと公開

$ npm run build
18:18:52 [@astrojs/sitemap] `sitemap-index.xml` created at `dist`
18:18:52 [build] 836 page(s) built in 24.02s
18:18:52 [build] Complete!
$ git log --format="%h %ci %s"
42c855f 2026-11-08 18:35:04 +0900 series8: 同じ並べ方が隣り合う6本を別の並べ方に(1・2・3列のどれでも隣に同じ形が来ないように)
54dc0e0 2026-11-08 18:31:06 +0900 README: サムネイルの決まりはシリーズごと(正は article_rules.md)、GPUラックの写真の形は使わない
a0ee2b4 2026-11-08 18:19:55 +0900 series8 のサムネイルの作業中の PNG を git の対象から外す
d669632 2026-11-08 18:19:19 +0900 series8: サムネイル37本を Unity のシミュレーションの画像に差し替え(並べ方は回ごとに変える)