# -*- coding: utf-8 -*-
# 動画「配膳ロボット、何台入れたら店は変わる?」(シリーズ「配膳ロボット導入シミュレーション」第1回)の場面
PATTERN = 2 # 画面パターン2(大文字型・video_rules.md 4-4c)
MOVE = True # 画面型の動き(4-4d)を強弱を付けて使う(2026-10-24 エイイチが選んだ)
# 2026-10-24: 「配膳ロボットで1人減らせるか」の scenes.py の形を写して書いた
# 背景の映像は、この動画のために新しく録った(試算 v2.7 のページを、p2m-site の public/sim/serving-robot.html からローカルで開いて録画。src/rec_背景の録画.py)
# src/sim_*.mp4 試算ページ(open: 3人で0→3台・more: 3→6台・one: 1台の料理・table: 台数の比べ・two: 2人で0→6台・crowd: 混雑で0→1台・
# buffet: 焼肉食べ放題2人で0→1台・hotel: 洗い場が遠い店2人で0→1台・patrol: 2人で集めて回る3台)
# 2026-10-24 台本4版(codex 1回目)に合わせて文言を直した
# src/unity_*.mp4 Unity の仮のファミレス(「配膳ロボットで1人減らせるか」で切り出したもの)。自己紹介・問い・アウトロの背景(シリーズの後の回の予告を兼ねる)
# 強弱(video_rules.md 4-4d):
# 中: setup・entry_flat・util・patrol・hotel(試算ページを流す、棒を足す)
# 強(10場面・続けない): hook・question・answer・entry・dish_ans・two_b・crowd・buffet・mitate1・answer2
# 色: オレンジ(red)=悪くなる・人に残る所、青(blue)=ロボット・良くなる所
import os as _os, sys as _sys
_here = _os.path.dirname(_os.path.abspath(__file__))
for _c in (_os.environ.get("P2M_BASE", ""), _os.path.join(_here, "..", "base"), _os.path.join(_here, "..", "_制作ベース")):
if _c and _os.path.exists(_os.path.join(_c, "scene_kit.py")):
_sys.path.insert(0, _c); break
from scene_kit import *
from scene_kit import _p2page
from motion_kit import use_motion, CAP
T1 = "13人のまま増やす"
T2 = "21台だと遅い?"
T3 = "3減らせる台数"
T4 = "4エイイチの見立て"
SIM = lambda n, **k: dict(dict(file=f"src/sim_{n}.mp4", dim=0.55, slow=1.0), **k) # 試算ページの録画
UO = dict(file="src/unity_oblique.mp4", dim=0.55, slow=1.2) # Unity の店(斜め)
UT = dict(file="src/unity_top.mp4", dim=0.5, slow=1.2) # Unity の店(真上)
CAP_SIM = "試算ページ(pixelstomotors.com/series8・試作 v2.7)"
CAP_U = "Unity の仮のファミレス(このシリーズの後の回で扱う)"
SRC_SIM = "数字:試算 v2.7(各条件40回の平均・仮の前提)"
_IDX = []
def _idx():
"""声の長さから割り付けた文の一覧(timing.layout)。重いので、tts/index.json と narration.py が同じなら作業フォルダの _timing_cache.pkl を使う"""
if _IDX:
return _IDX[0]
import hashlib, pickle
from timing import load, layout
key = ""
try:
key = hashlib.md5(open(_os.path.join(_here, "tts/index.json"), "rb").read() + open(_os.path.join(_here, "narration.py"), "rb").read()).hexdigest()
cp = _os.path.join(_here, "_timing_cache.pkl")
if _os.path.exists(cp):
k, idx = pickle.load(open(cp, "rb"))
if k == key:
_IDX.append(idx); return idx
except Exception:
pass
idx = load(); layout(idx); _IDX.append(idx)
try:
pickle.dump((key, idx), open(_os.path.join(_here, "_timing_cache.pkl"), "wb"))
except Exception:
pass
return idx
def _when(scene, k, f):
"""場面の頭から、その場面の k 番目の文の読み上げの f 割の所までの秒(声を取り込む前は 0)"""
try:
from timing import FPS
lines, fr = [], 0
for o in _idx():
if o['scene'] == scene:
lines.append((fr, o['speech'])); fr += o['frames']
a, sp = lines[min(k, len(lines) - 1)]
return (a + sp * f) / FPS
except Exception:
return 0.0
def bars(vals, labels, unit="分", kicker="", sub="", tag=None, colors=None, times=None, sub_t=None, vmax=None, src=SRC_SIM, fmt="{:.1f}"):
"""台数ごとの棒(縦)。ハコブくんの区画(左下)を避けるため、図は左を空けて右寄りに置く。
times: 棒ごとに出す秒(None なら最初から出ている=静止画)/sub_t: 下の一言を出す秒"""
vmax = vmax or max(vals) * 1.08
H = 340
cols = []
for i, (v, l) in enumerate(zip(vals, labels)):
c = (colors or {}).get(i, "#9A9A94")
h = max(6, int(H * v / vmax))
ta = f' data-a="pop" data-t="{times[i]:.2f}" data-d="0.4"' if times else ""
cols.append(f'
'
f'
{fmt.format(v)}{unit}
'
f'
')
lab = "".join(f'{l}
' for l in labels)
k = f'{kicker}
' if kicker else ""
st = f' data-a="up" data-t="{sub_t:.2f}"' if sub_t is not None else ""
sb = f'{sub}
' if sub else ""
body = (f'{k}'
f'
{"".join(cols)}
'
f'
{lab}
{sb}
')
return _p2page(body, tag, src)
ENTRY3 = [8.6, 4.0, 3.2, 2.9, 3.9, 3.5, 3.3] # 3人・席に着くまで(分)0〜6台
ENTRY2 = [28.3, 16.9, 14.3, 14.2, 14.9, 16.1, 15.4] # 2人・席に着くまで(分)0〜6台
NL = ["0台", "1台", "2台", "3台", "4台", "5台", "6台"]
B, R_ = "#1A6C98", "#C4471F"
S = {}
# 0 フック
S["hook"] = p2_word(['約2,100店に', '約3,000台'], kicker="すかいらーくグループ(2022年末までに)", kc="muted", size=130, sub="単純に割ると、1店あたり平均で約1.4台", src="出典:すかいらーくHD「歴史・沿革」(2022年12月)")
S["hook_q"] = p2_quiz(['増やすほど、', '店は良くなる?'], pill="2台より3台、3台より6台", size=150)
S["hook_try"] = p2_word(['0台から6台まで', '1台ずつ'], kicker="ファミレスの混む2時間を、パソコンの中で", kc="muted", size=140, sub="ある台数から先は変わらない・1台だと料理が遅い")
# 2 今日の問い
S["question"] = p2_quiz(['何台入れたら、', '店は変わる?'], pill="今日の問い", size=150)
S["define"] = p2_list([("1", "席に着くまで"), ("2", "料理が届くまで"), ("3", "人を減らせるか")], kicker="見るのは3つ", size=110)
S["define2"] = p2_num("10", "分", kicker="席は空いているのに、片付けや案内を待たせた時間(平均)", sub="これ以上で「店が回らない」。比べるためにエイイチが決めた線・満席の待ちは入れない")
S["scope"] = p2_word(['パソコンの中の', '試算'], kicker="シリーズ1回目・今日の範囲", kc="muted", size=170, sub="空の店から2時間・数字の多くは仮の値・兼務や休憩は入れていない")
# 3 結論と立場
S["answer"] = p2_word(['2台目まで', '大きく変わる'], kicker="先に結論:3人のまま・料理を運ぶだけ・席に着くまでの時間", kc="muted", size=150, sub="3台目から先は、大きくは縮まない")
S["answer_two"] = p2_word(['要る人数は', '変わらない'], kicker="普通の混み具合・料理を運ぶだけなら、試した6台まで", kc="muted", size=150, sub="3人を2人にすると、店は回らない")
S["stance"] = p2_word(['意味がない、', 'ではない'], kicker="先に言っておくと", kc="muted", size=150, sub="人に足せば席に着くまでは縮む・店や混み具合で、1台で1人少なくても線を下回る")
# 4 目次
S["toc"] = p2_list([("1", "3人のまま増やす"), ("2", "1台だと遅い?"), ("3", "減らせる台数"), ("4", "エイイチの見立て")], kicker="今日は3つ+見立て", size=96)
# 5 節1
S["c1"] = chapter2("1つ目", "増やすと", "3人のまま、0台から6台へ")
S["setup"] = p2_pair("35卓", "1時間35組", da="標準ファミレス", db="普通の混み具合", ca="", cb="", mid="/", kicker="試算の店", size=150, tag=T1, sub="ロボットは料理を運ぶだけ・1往復で料理を3つまで")
S["simpy"] = p2_word(['出来事を', '時間の順に'], kicker="SimPy(Python の道具)", kc="muted", size=160, tag=T1, sub="1つの条件を40回計算した平均")
S["speed"] = p2_pair("0.5m/秒", "1.0m/秒", da="ロボット", db="人が歩く", ca="blue", cb="", mid="/", kicker="仮に置いた速さ", size=150, tag=T1, sub="どちらも仮の値(店で測ったものではない)")
S["entry"] = bars(ENTRY3[:3] + [0] * 0, NL[:3], kicker="店に来てから席に着くまで(3人)", tag=T1, colors={0: "#9A9A94", 1: B, 2: B}, vmax=9.5)
S["entry_flat"] = bars(ENTRY3, NL, kicker="店に来てから席に着くまで(3人)", tag=T1, colors={0: "#9A9A94", 1: B, 2: B, 3: B, 4: B, 5: B, 6: B}, vmax=9.5, sub="3台目から先は、3分台(4台の延びがばらつきかは、平均だけでは分からない)")
S["why1"] = p2_word(['人の手が', '片付けと案内へ'], kicker="料理を運ぶ仕事が、ロボットに移った", kc="muted", size=140, tag=T1)
S["why1b"] = p2_pair("7.4分", "2.3分", da="ロボットなし", db="2台", ca="", cb="blue", mid="→", kicker="席は空いているのに待たせた時間", size=170, tag=T1, src=SRC_SIM)
S["why1c"] = p2_word(['下げる・拭く・案内は', '人の仕事'], kicker="人の仕事を待つ時間が残る(今回の作業時間と順番では)", kc="muted", size=120, tag=T1)
S["util"] = p2_num("2割", "ちょっと", kicker="6台のとき、1台が仕事でふさがっていた時間(平均)", sub="お客さんが料理を取るのを待つ時間も含む", tag=T1, src=SRC_SIM)
S["walk"] = p2_pair("3.9km", "2.3km", da="ロボットなし", db="2台", ca="", cb="blue", mid="→", kicker="スタッフ1人が歩く距離(最後の片付けまで)", size=170, tag=T1, sub="約4割減・3台目から先は変わらない", src=SRC_SIM)
S["c1_end"] = p2_word(['大きく変わったのは', '2台目まで'], kicker="席に着くまで・歩く距離", kc="muted", size=150, tag=T1, sub="料理を早く届けるなら、3台目にも効き目")
# 6 節2
S["c2"] = chapter2("2つ目", "1台だと", "料理が、かえって遅くなる")
S["dish"] = p2_num("58", "秒", kicker="料理ができてから席に届くまで(3人・ロボットなし)", tag=T2, src=SRC_SIM)
S["dish_q"] = p2_quiz(['1台入れたら、', 'この58秒は?'], pill="考えてみてください", size=150, tag=T2)
S["dish_ans"] = p2_pair("58秒", "2分12秒", da="ロボットなし", db="1台", ca="", cb="red", mid="→", kicker="料理ができてから卓に着くまで", size=160, tag=T2, sub="倍以上に延びた", src=SRC_SIM)
S["dish_more"] = p2_pair("約1分", "39秒", da="2台", db="3台", ca="", cb="blue", mid="→", kicker="ロボットなしより早く届いたのは、3台目から", size=170, tag=T2, src=SRC_SIM)
S["rule"] = p2_pair("まずロボット", "2分で人も", da="料理ができたら", db="ロボットが来なければ", ca="blue", cb="", mid="→", kicker="この試算の運び方の決まり(エイイチが決めた)", size=120, tag=T2)
S["rule2"] = p2_word(['料理が', 'ロボットの帰りを待つ'], kicker="1台だと、往復の間に次の料理ができる", kc="muted", size=130, tag=T2, sub="1台は、時間の約4分の3が仕事でふさがっていた")
S["fair"] = p2_pair("まずロボット", "先に空いた方", da="今の試算", db="前の版の試算", ca="blue", cb="", mid="/", kicker="どちらがいいかは、別に比べる必要がある", size=100, tag=T2, sub="前の版は、人が先に運び、ロボットの出番が減った")
S["honne1"] = p2_word(['意外。でも', '限界点もよく分かった'], kicker="この結果を見たエイイチの感想", kc="red", size=120, tag=T2, sub="分かったのは、この試算の条件での限界(製品の性能は測っていない)")
S["c2_end"] = p2_pair("台数", "決まり", ca="blue", cb="blue", mid="+", kicker="何台入れるかは、セットで決める", size=190, tag=T2)
# 7 節3
S["c3"] = chapter2("3つ目", "減らせる台数", "普通の日・料理を運ぶだけなら、6台までになかった")
S["two"] = p2_num("28.3", "分", kicker="スタッフ2人・ロボットなし:席に着くまで", sub="店が回らない", tag=T3, src=SRC_SIM)
S["two_b"] = bars(ENTRY2, NL, kicker="スタッフ2人:席に着くまで", tag=T3, colors={i: R_ for i in range(7)}, vmax=30, sub="どれも「店が回らない」")
S["two_c"] = p2_word(['下げる・拭く・案内は', '2人のまま'], kicker="運ぶ仕事はロボットへ。でも", kc="muted", size=120, tag=T3)
S["honne2"] = p2_word(['ロボットに', '任せたかった仕事'], kicker="昔ファミレスで働いていたエイイチに聞いた", kc="red", size=140, tag=T3)
S["honne2_list"] = p2_list([("1", "皿を下げる"), ("2", "洗い場まで運ぶ"), ("3", "シルバーを拭く")], kicker="エイイチの答え(「地味に大変」)", size=110, tag=T3)
S["honne2_c"] = p2_word(['どれも', '任せていない'], kicker="今日の試算のロボットには", kc="muted", size=160, tag=T3)
S["patrol"] = p2_word(['2人+3台で', '回る'], kicker="食事中に席を回って、皿を集める使い方", kc="muted", size=160, tag=T3, sub="この話は、次の回で", src=SRC_SIM)
S["crowd"] = p2_pair("4人", "3人", da="ロボットなし", db="1台", ca="", cb="blue", mid="→", kicker="混雑の日(1時間に45組):店が回る最少の人数", size=190, tag=T3, src=SRC_SIM)
S["crowd_note"] = p2_pair("45秒", "2分17秒", da="4人・ロボットなし", db="3人+1台", ca="", cb="red", mid="→", kicker="混雑の日:料理が卓に着くまで(席に着くまでも 11.4分 → 17.4分)", size=170, tag=T3, sub="同じサービスのまま減らせた、ではない", src=SRC_SIM)
S["other"] = p2_list([("・", "小型店"), ("・", "大型店"), ("・", "洗い場が遠い店"), ("・", "焼肉食べ放題")], kicker="ほかの店の種類(来客数も食事の長さも、店ごとに別の設定)", size=96, tag=T3)
S["buffet"] = p2_pair("約27.5分", "9.8分", da="2人・ロボットなし", db="2人+1台", ca="red", cb="blue", mid="→", kicker="焼肉の食べ放題:席に着くまで", size=160, tag=T3, sub="線は下回る・料理の待ちは 1分17秒 → 3分26秒", src=SRC_SIM)
S["hotel"] = p2_pair("線の上", "線の下", da="2人・ロボットなし", db="2人+1台", ca="red", cb="blue", mid="→", kicker="洗い場が遠い店", size=150, tag=T3, sub="席に着くまでは、3人のときの2.9分から6.0分に", src=SRC_SIM)
S["large"] = p2_pair("6人", "5人", da="0台・1台", db="2台から", ca="", cb="blue", mid="→", kicker="大型店(60卓):店が回る最少の人数", size=190, tag=T3, src=SRC_SIM)
S["c3_end"] = p2_word(['近い店の数字は', '自分の店の答えではない'], kicker="何台で何が変わるかは、店と混み具合ごとに違う", kc="muted", size=110, tag=T3)
# 8 見立て
S["mitate"] = chapter2("最後に", "見立て", "エイイチの考え")
S["mitate1"] = p2_word(['入れるだけでは', '良くならないことも'], kicker="エイイチの見立て", kc="red", size=130, sub="そこには、専門家の知見が要る", tag=T4)
S["mitate2"] = p2_list([("1", "何台入れるか"), ("2", "誰が先に運ぶか"), ("3", "何を任せるか")], kicker="今日の比べで、答えを変えたもの", size=100, tag=T4)
S["limits"] = p2_word(['時間と数だけの', '計算'], kicker="フェアに言うと", kc="muted", size=150, tag=T4, sub="止まり方は仮・ロボットどうしの邪魔、載せる人の時間は入れていない・今日の店は図面なし")
S["next"] = p2_list([("次", "人は減らせる? いくら?"), ("その後", "図面から3Dの店へ")], kicker="次の回から", size=96, tag=T4)
# 9 答え
S["answer2"] = p2_word(['2台目まで', '8.6分 → 3.2分'], kicker="問いへの答え:3人のまま・席に着くまで", kc="muted", size=130, sub="3台目から先は、大きくは縮まない(40回の平均・仮の前提)")
S["answer2b"] = p2_word(['台数より', '店と決まり'], kicker="何台で変わるか", kc="muted", size=160, sub="店や混み具合で、1台で1人少なくても線を下回る(待ちは延びる)")
S["question_next"] = p2_quiz(['何台いて、', '誰が運んでいる?'], pill="次に店に行ったら", size=150)
S["try"] = p2_word(['試算は', 'サイトで試せる'], kicker="pixelstomotors.com/series8", kc="muted", size=150, sub="リンクは概要欄に")
# 動画シーンに重ねるテロップ(透過)
O = {
"title": overlay('
フィジカルAI解説
案内役:ハコブくん(AI音声)
'),
"title_eiichi": overlay('
エイイチ
デザイナー/ロボティクスの研究・開発
'),
"outro": overlay('
相談は概要欄の連絡先から
高評価・チャンネル登録/コメントはエイイチが全部読んでいます
'),
}
# 映像を流す場面 (映像, 開始秒, 透かし)。開始秒 None は前の場面の続き
VIDEO = {
"title": ("src/unity_oblique.mp4", 2.0, False),
"title_eiichi": ("src/unity_oblique.mp4", None, False),
"outro": ("src/unity_top.mp4", 4.0, False),
}
# 動きのある場面(motion_kit.py が src/motion_{場面}.mp4 に書き出す)
# 2026-10-24 エイイチの指示:「背景にシミュレーションの動画を長く使う(今までより圧倒的に増やす)」「テキストは背景をカードタイプで」
# → 自己紹介とアウトロのほかは、すべての場面で、シミュレーションの映像(Unity の店・試算ページ・試算のコード)を背景に流し、
# その上に黒いカード(白い文字・青の強調)を置く。同じ映像は続く場面でつなげて流す(場面ごとに頭へ戻さない)
# 上の S(紙の図)は、動きを書き出す前の確かめ用に残している
import subprocess as _sp
# 背景の映像の置き場所(重いので KIOXIA。iCloud には置かない)
_BG_CANDS = [_os.environ.get("P2M_BG", ""),
_os.path.expanduser("~/mnt/r2r2r-data/videos/何台入れたら店は変わる/_取り込み/背景"),
"/Volumes/KIOXIA/r2r2r-data/videos/何台入れたら店は変わる/_取り込み/背景"]
BGDIR = next((c for c in _BG_CANDS if c and _os.path.isdir(c)), _BG_CANDS[1])
# 映像の名前 → (ファイル, 何の映像かの札)
U_OLD = "Unity の仮のファミレス(初期の版・シリーズの後の回で扱う)"
U_NEW = "Unity で建てたファミレス(図面のある店③・シリーズの後の回で扱う)"
BG = {
"u357o": (f"{BGDIR}/LOG357_v1_stage2_oblique.mp4", U_OLD),
"u357t": (f"{BGDIR}/LOG357_v2_stage2_top.mp4", U_OLD),
"u364a": (f"{BGDIR}/LOG364_v3_run3_people_and_robots.mp4", U_NEW),
"u364b": (f"{BGDIR}/LOG364_v4_run4_robots_back_and_forth.mp4", U_NEW),
"u364c": (f"{BGDIR}/LOG364_v5_run5_return_lane.mp4", U_NEW),
"four": (f"{BGDIR}/four-views.mp4", U_NEW),
"obl": (f"{BGDIR}/fv-oblique.mp4", U_NEW),
"busy": (f"{BGDIR}/top-busy.mp4", U_NEW),
"pass": (f"{BGDIR}/top-pass.mp4", U_NEW),
"term": (f"{BGDIR}/terminal.mp4", "試算のコード(SimPy)を回しているところ"),
}
for _k in ("open", "more", "one", "table", "two", "crowd", "buffet", "hotel", "patrol"):
BG["s_" + _k] = (_os.path.join(_here, f"src/sim_{_k}.mp4"), CAP_SIM)
_DUR = {}
def _clip_len(f):
if f not in _DUR:
try:
_DUR[f] = float(_sp.check_output(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", f]).decode().strip())
except Exception:
_DUR[f] = 9999.0
return _DUR[f]
_START = {}
def _scene_start(scene):
"""動画の頭から、その場面の始まりまでの秒(声を取り込む前は 0)"""
try:
from timing import FPS
if not _START:
fr = 0
for o in _idx():
_START.setdefault(o["scene"], fr / FPS); fr += o["frames"]
return _START.get(scene, 0.0)
except Exception:
return 0.0
# 背景の割り当て:(映像の名前, 頭の秒, この映像でつなげて流す場面)
RUNS = [
("u357o", 2, ["hook", "hook_q", "hook_try"]),
("four", 0, ["question", "define", "define2"]),
("u364a", 0, ["scope", "answer", "answer_two", "stance", "toc"]),
("busy", 0, ["c1", "setup"]),
("term", 0, ["simpy"]),
("pass", 0, ["speed"]),
("s_open", 0, ["entry"]),
("s_more", 0, ["entry_flat"]),
("u364c", 0, ["why1", "why1b", "why1c", "util", "walk", "c1_end"]),
("pass", 4, ["c2"]),
("s_one", 0, ["dish_ans", "dish_more"]),
("u364b", 0, ["rule", "rule2", "fair", "honne1", "c2_end"]),
("four", 8, ["c3"]),
("s_two", 0, ["two", "two_b", "two_c"]),
("u357t", 0, ["honne2", "honne2_list", "honne2_c"]),
("s_patrol", 0, ["patrol"]),
("s_crowd", 0, ["crowd", "crowd_note"]),
("s_table", 0, ["other"]),
("s_buffet", 0, ["buffet"]),
("s_hotel", 0, ["hotel"]),
("u364a", 30, ["large", "c3_end"]),
("obl", 0, ["mitate", "mitate1"]),
("u364c", 50, ["mitate2", "limits", "next"]),
("u364a", 50, ["answer2", "answer2b", "question_next"]),
("s_open", 0, ["try"]),
]
def _bg(scene):
for key, ss0, scs in RUNS:
if scene in scs:
f, cap = BG[key]
el = _scene_start(scene) - _scene_start(scs[0])
ss = (ss0 + el) % max(1.0, _clip_len(f) - 0.5)
# 試算ページの録画は、操作が終わったら最後のコマで止まっていてよいので、頭に戻さない
if key.startswith("s_"):
ss = min(ss0 + el, max(0.0, _clip_len(f) - 0.5))
return dict(file=f, ss=round(ss, 2), dim=(0.5 if key.startswith("s_") else 0.62), slow=1.0), cap
return None, ""
# ---- カード(黒い板に白い文字・強調は青)----
HL, WARN = "#C4471F", "#62BFF5" # 基本はオレンジ(元の #C4471F)、追加の差し色に水色(2026-10-24 エイイチの指示)
CARD_CSS = f"""
.cw{{position:absolute;left:0;right:0;top:150px;bottom:300px;display:flex;align-items:center;justify-content:center}}
.cd{{background:rgba(8,8,8,.86);border-radius:30px;padding:46px 84px 52px;text-align:center;color:#fff;max-width:1560px;box-shadow:0 10px 40px rgba(0,0,0,.35)}}
.cd .k{{font-size:42px;font-weight:700;color:#BDBDBD;margin-bottom:22px;letter-spacing:.03em}}
.cd .b{{font-weight:900;line-height:1.16;letter-spacing:-.01em}}
.cd .s{{font-size:44px;font-weight:700;color:#D0D0D0;margin-top:26px;line-height:1.35}}
.cd .hl{{color:{HL}}} .cd .wn{{color:{WARN}}}
.cd .pill{{display:inline-block;font-size:38px;font-weight:900;padding:8px 28px;border-radius:999px;background:#F5F5F0;color:#0D0D0D;margin-bottom:28px}}
.cd .pr{{display:flex;align-items:center;justify-content:center;gap:60px}}
.cd .pr .w{{font-weight:900;line-height:1.1}}
.cd .pr .d{{font-size:42px;font-weight:700;color:#BDBDBD;margin-top:16px}}
.cd .pr .m{{font-size:96px;font-weight:900;color:#8A8A8A}}
.cd .li{{text-align:left;display:inline-block}}
.cd .li div{{font-weight:900;margin:8px 0}}
.cd .li span{{color:{HL};margin-right:30px}}
.cap{{position:absolute;left:96px;top:64px;background:rgba(8,8,8,.72);color:#fff;font-size:30px;font-weight:700;padding:12px 24px;border-radius:10px}}
.tg{{position:absolute;right:56px;top:64px;background:rgba(8,8,8,.72);color:#fff;font-size:30px;font-weight:900;padding:12px 24px;border-radius:10px}}
.tg b{{color:{HL};margin-right:12px}}
.sr{{position:absolute;right:56px;top:126px;color:#eee;font-size:24px;font-weight:700;text-shadow:0 1px 4px #000}}
"""
_COL = {"": "#fff", "blue": HL, "red": WARN, "muted": "#BDBDBD"}
def _wrap(inner, cap="", tag=None, src="", pop=0.05, big_at=None):
"""カード1枚の重ね絵(透過)。pop: カードが出る秒。big_at: 大きい文字を叩きつける秒(強の場面)"""
if big_at is not None:
inner = inner.replace('class="b"', f'class="b" data-a="slam" data-t="{big_at:.2f}" data-d="0.45"', 1)
c = f'{cap}
' if cap else ""
tg = f'{tag}
' if tag else ""
sr = f'{src}
' if src else ""
body = f'{c}{tg}{sr}'
return overlay(body)
def _hl(s):
return s.replace('', '').replace('', '')
def C_word(lines, kicker="", sub="", size=130, kc=None):
k = f'{kicker}
' if kicker else ""
s = f'{sub}
' if sub else ""
return f'{k}{_hl("
".join(lines))}
{s}'
def C_num(num, unit="", kicker="", sub="", size=230):
k = f'{kicker}
' if kicker else ""
s = f'{sub}
' if sub else ""
u = f'{unit}' if unit else ""
return f'{k}{num}{u}
{s}'
def C_pair(a, b, da="", db="", ca="", cb="blue", mid="→", kicker="", sub="", size=140):
k = f'{kicker}
' if kicker else ""
s = f'{sub}
' if sub else ""
side = lambda w, d, c: f'{w}
' + (f'
{d}
' if d else "") + "
"
return f'{k}{side(a, da, ca)}
{mid}
{side(b, db, cb)}
{s}'
def C_list(items, kicker="", size=96):
k = f'{kicker}
' if kicker else ""
li = "".join(f'{n}{x}
' for n, x in items)
return f'{k}{li}
'
def C_quiz(lines, pill, size=140):
return f'{pill}
{"
".join(lines)}
'
def C_chap(no, word, sub):
return f'{no}
{word}
{sub}
'
def C_bars(vals, labels, kicker="", colors=None, times=None, vmax=None, sub="", sub_t=None):
vmax = vmax or max(vals) * 1.08; Hh = 205; cols = []
for i, (v, l) in enumerate(zip(vals, labels)):
c = (colors or {}).get(i, "#9A9A94"); h = max(6, int(Hh * v / vmax))
ta = f' data-a="pop" data-t="{times[i]:.2f}" data-d="0.4"' if times else ""
cols.append(f'')
lab = "".join(f'{l}
' for l in labels)
k = f'{kicker}
' if kicker else ""
st = f' data-a="up" data-t="{sub_t:.2f}"' if sub_t is not None else ""
s = f'{sub}
' if sub else ""
return (f'{k}{"".join(cols)}
'
f'{lab}
{s}')
# 場面ごとのカード:(中身, 強の場面なら大きい文字を叩きつける時((文, 割合)・秒), 章の札, 出典)
_e = [_when("entry", 0, f) for f in (0.3, 0.55, 0.8)]
_f = [_when("entry_flat", 0, f) for f in (0.0, 0.0, 0.0, 0.1, 0.35, 0.5, 0.65)]
_t2 = [_when("two_b", 0, f) for f in (0.0, 0.1, 0.25, 0.45, 0.5, 0.55, 0.6)]
GRAY, BL, OR = "#9A9A94", HL, WARN
CARDS = {
"hook": (C_word(['約2,100店に', '約3,000台'], "すかいらーくグループ(2022年12月に導入完了)", "単純に割ると、1店あたり平均で約1.4台"), (1, 0.05), None, "出典:すかいらーくHD「歴史・沿革」"),
"hook_q": (C_quiz(['増やすほど、', '店は良くなる?'], "2台より3台、3台より6台"), None, None, ""),
"hook_try": (C_word(['0台から6台まで', '1台ずつ'], "ファミレスの混む2時間を、パソコンの中で", "ある台数から先は縮まない・1台だと料理が遅い"), None, None, ""),
"question": (C_quiz(['何台入れたら、', '店は変わる?'], "今日の問い", 150), 0.1, None, ""),
"define": (C_list([("1", "席に着くまで"), ("2", "料理が卓に着くまで"), ("3", "人を減らせるか")], "見るのは3つ"), None, None, ""),
"define2": (C_num("10", "分", "席は空いているのに、片付けや案内を待たせた時間(平均)", "これ以上で「店が回らない」:比べるためにエイイチが決めた線"), None, None, ""),
"scope": (C_word(['パソコンの中の', '試算'], "シリーズ1回目・今日の範囲", "空の店から2時間・数字の多くは仮の値・兼務や休憩は入れていない", 150), None, None, ""),
"answer": (C_word(['2台目まで', '大きく縮む'], "先に結論:3人のまま・料理を運ぶだけ・席に着くまで", "3台目から先は、大きくは縮まない", 150), (0, 0.6), None, SRC_SIM),
"answer_two": (C_word(['要る人数は', '変わらない'], "普通の混み具合・料理を運ぶだけなら、試した6台まで", "3人を2人にすると、店は回らない", 150), None, None, SRC_SIM),
"stance": (C_word(['意味がない、', 'ではない'], "先に言っておくと", "人に足せば、席に着くまでは縮む", 150), None, None, ""),
"toc": (C_list([("1", "3人のまま増やす"), ("2", "1台だと遅い?"), ("3", "減らせる台数"), ("4", "エイイチの見立て")], "今日は3つ+見立て", 90), None, None, ""),
"c1": (C_chap("1つ目", "増やすと", "3人のまま、0台から6台へ"), None, None, ""),
"setup": (C_pair("35卓", "1時間35組", "標準ファミレス", "普通の混み具合", "", "", "/", "試算の店", "ロボットは料理を運ぶだけ・1往復で料理を3つまで", 130), None, T1, ""),
"simpy": (C_word(['出来事を', '時間の順に'], "SimPy(Python の道具)", "1つの条件を40回計算した平均", 140), None, T1, ""),
"speed": (C_pair("0.5m/秒", "1.0m/秒", "ロボット", "人が歩く", "blue", "", "/", "仮に置いた速さ", "どちらも仮の値(店で測ったものではない)", 130), None, T1, ""),
"entry": (C_bars(ENTRY3[:3], NL[:3], "店に来てから席に着くまで(3人)", {0: GRAY, 1: BL, 2: BL}, _e, 9.5), None, T1, SRC_SIM),
"entry_flat": (C_bars(ENTRY3, NL, "店に来てから席に着くまで(3人)", {0: GRAY, **{i: BL for i in range(1, 7)}}, _f, 9.5,
"3台目から先は3分台(4台の延びは、ばらつきか分からない)", _when("entry_flat", 1, 0.1)), None, T1, SRC_SIM),
"why1": (C_word(['人の手が', '片付けと案内へ'], "料理を運ぶ仕事が、ロボットに移った", "", 130), None, T1, ""),
"why1b": (C_pair("7.4分", "2.3分", "ロボットなし", "2台", "", "blue", "→", "席は空いているのに待たせた時間", "", 160), None, T1, SRC_SIM),
"why1c": (C_word(['下げる・拭く・案内は', '人の仕事'], "人の仕事を待つ時間が残る(今回の作業時間と順番では)", "", 115), None, T1, ""),
"util": (C_num("2割", "ちょっと", "6台のとき、1台が仕事でふさがっていた時間(平均)", "お客さんが料理を取るのを待つ時間も含む"), None, T1, SRC_SIM),
"walk": (C_pair("3.9km", "2.3km", "ロボットなし", "2台", "", "blue", "→", "スタッフ1人が歩く距離(最後の片付けまで)", "約4割減・3台目から先は変わらない", 160), None, T1, SRC_SIM),
"c1_end": (C_word(['大きく変わったのは', '2台目まで'], "席に着くまで・歩く距離", "料理を早く届けるなら、3台目にも効き目", 140), None, T1, ""),
"c2": (C_chap("2つ目", "1台だと", "料理が、かえって遅くなる"), None, None, ""),
"dish_ans": (C_pair("58秒", "2分12秒", "ロボットなし", "1台", "", "red", "→", "料理ができてから卓に着くまで", "倍以上に延びた", 150), (0, 0.45), T2, SRC_SIM),
"dish_more": (C_pair("約1分", "39秒", "2台", "3台", "", "blue", "→", "ロボットなしより早く届いたのは、3台目から", "", 160), None, T2, SRC_SIM),
"rule": (C_pair("まずロボット", "2分で人も", "料理ができたら", "ロボットが来なければ", "blue", "", "→", "この試算の運び方の決まり(エイイチが決めた)", "", 110), None, T2, ""),
"rule2": (C_word(['料理が', 'ロボットの帰りを待つ'], "1台だと、往復の間に次の料理ができる", "1台は、時間の約4分の3が仕事でふさがっていた", 120), None, T2, SRC_SIM),
"fair": (C_pair("まずロボット", "先に空いた方", "今の試算", "前の版の試算", "blue", "", "/", "どちらがいいかは、別に比べる必要がある", "前の版は、人が先に運び、ロボットの出番が減った", 100), None, T2, ""),
"honne1": (C_word(['意外。でも', '限界点もよく分かった'], "この結果を見たエイイチの感想", "分かったのは、この試算の条件での限界(製品の性能は測っていない)", 110, "red"), None, T2, ""),
"c2_end": (C_pair("台数", "決まり", "", "", "blue", "blue", "+", "何台入れるかは、セットで決める", "", 180), None, T2, ""),
"c3": (C_chap("3つ目", "減らせる台数", "普通の日・料理を運ぶだけなら、6台までになかった"), None, None, ""),
"two": (C_num("28.3", "分", "スタッフ2人・ロボットなし:席に着くまで", "店が回らない"), None, T3, SRC_SIM),
"two_b": (C_bars(ENTRY2, NL, "スタッフ2人:席に着くまで", {i: OR for i in range(7)}, _t2, 30, "どれも「店が回らない」", _when("two_b", 0, 0.8)), (0, 0.0), T3, SRC_SIM),
"two_c": (C_word(['下げる・拭く・案内は', '2人のまま'], "運ぶ仕事はロボットへ。でも", "", 115), None, T3, ""),
"honne2": (C_word(['ロボットに', '任せたかった仕事'], "昔ファミレスで働いていたエイイチに聞いた", "", 130, "red"), None, T3, ""),
"honne2_list": (C_list([("1", "皿を下げる"), ("2", "洗い場まで運ぶ"), ("3", "シルバーを拭く")], "エイイチの答え(「地味に大変」)", 100), None, T3, ""),
"honne2_c": (C_word(['どれも', '任せていない'], "今日の試算のロボットには", "", 150), None, T3, ""),
"patrol": (C_word(['2人+3台で', '線を下回る'], "食事中に席を回って、お客さんに皿を載せてもらう使い方", "お客さんが一定の量を載せてくれる前提・この話は次の回で", 140), None, T3, SRC_SIM),
"crowd": (C_pair("4人", "3人", "ロボットなし", "1台", "", "blue", "→", "混雑の日(1時間に45組):線を下回る最少の人数", "", 180), (0, 0.55), T3, SRC_SIM),
"crowd_note": (C_pair("45秒", "2分17秒", "4人・ロボットなし", "3人+1台", "", "red", "→", "料理が卓に着くまで(席に着くまでも 11.4分 → 17.4分)", "同じサービスのまま減らせた、ではない", 150), None, T3, SRC_SIM),
"other": (C_list([("・", "小型店"), ("・", "大型店"), ("・", "洗い場が遠い店"), ("・", "焼肉食べ放題")], "ほかの店の種類(来客数も食事の長さも、店ごとに別の設定)", 90), None, T3, ""),
"buffet": (C_pair("約27分半", "9.8分", "2人・ロボットなし", "2人+1台", "red", "blue", "→", "焼肉の食べ放題:席に着くまで", "線は下回る・料理の待ちは 1分17秒 → 3分26秒", 140), (0, 0.6), T3, SRC_SIM),
"hotel": (C_pair("2.9分", "6.0分", "3人・ロボットなし", "2人+1台", "", "red", "→", "洗い場が遠い店:席に着くまで", "2人+1台でも、線は下回る", 150), None, T3, SRC_SIM),
"large": (C_pair("6人", "5人", "0台・1台", "2台から", "", "blue", "→", "大型店(60卓):線を下回る最少の人数", "", 180), None, T3, SRC_SIM),
"c3_end": (C_word(['近い店の数字は', '自分の店の答えではない'], "何台で何が変わるかは、店と混み具合ごとに違う", "", 105), None, T3, ""),
"mitate": (C_chap("最後に", "見立て", "エイイチの考え"), None, None, ""),
"mitate1": (C_word(['入れるだけでは', '良くならないことも'], "エイイチの見立て", "そこには、専門家の知見が要る", 125, "red"), (0, 0.1), T4, ""),
"mitate2": (C_list([("1", "何台入れるか"), ("2", "誰が先に運ぶか"), ("3", "何を任せるか")], "今日の比べで、答えを変えたもの", 96), None, T4, ""),
"limits": (C_word(['時間と数だけの', '計算'], "フェアに言うと", "止まり方は仮・ロボットどうしの邪魔は入れていない", 140), None, T4, ""),
"next": (C_list([("次", "人は減らせる? いくら?"), ("その後", "図面から3Dの店へ")], "次の回から", 92), None, T4, ""),
"answer2": (C_word(['2台目まで', '8.6分 → 3.2分'], "問いへの答え:3人のまま・席に着くまで", "3台目から先は、大きくは縮まない(40回の平均・仮の前提)", 130), (1, 0.3), None, SRC_SIM),
"answer2b": (C_word(['台数より', '店と決まり'], "何台で変わるか", "店や混み具合で、1台で1人少なくても線を下回る(待ちは延びる)", 150), None, None, ""),
"question_next": (C_quiz(['何台いて、', '誰が運んでいる?'], "次に店に行ったら", 140), None, None, ""),
"try": (C_word(['試算は', 'サイトで試せる'], "pixelstomotors.com/series8", "リンクは概要欄に", 140), None, None, ""),
}
MOTION = {}
for _name, (_inner, _big, _tag, _src) in CARDS.items():
_b, _cap = _bg(_name)
_ba = None
if _big is not None:
_ba = _big if isinstance(_big, (int, float)) else _when(_name, _big[0], _big[1])
MOTION[_name] = dict(type="html", bg=_b, html=_wrap(_inner, cap=_cap, tag=_tag, src=_src, big_at=_ba))
use_motion(MOTION, VIDEO, O, _here, MOVE)