# 第2回「配膳ロボットで1人減らせるか」の背景の録画(試算 v2.7 のページをローカルで開いて録る) # 使い方: p2m-site/public/sim で python3 -m http.server 8765 を動かし、xvfb-run -s '-screen 0 1920x1400x24' python3 rec2.py [名前 ...] # 第1回の src/rec_背景の録画.py を写して書き換えた(2026-10-25)。ページは p2m-site public/sim/serving-robot.html(md5 88adf2b2・v2.7) import asyncio, sys, os, subprocess from playwright.async_api import async_playwright URL = 'http://localhost:8765/serving-robot.html' CUR = """(() => { if (document.getElementById('fakecur')) return; const c=document.createElement('div'); c.id='fakecur'; c.style.cssText='position:fixed;left:960px;top:700px;width:34px;height:34px;margin:-17px 0 0 -17px;border-radius:50%;background:rgba(240,90,40,.35);border:3px solid #F05A28;z-index:99999;pointer-events:none;transition:left .7s ease, top .7s ease, transform .15s'; document.body.appendChild(c);})()""" async def cur_to(pg, loc): await loc.scroll_into_view_if_needed() bb = await loc.bounding_box(); x, y = bb['x']+bb['width']/2, bb['y']+bb['height']/2 await pg.evaluate(f"(()=>{{const c=document.getElementById('fakecur');c.style.left='{x}px';c.style.top='{y}px'}})()") await pg.wait_for_timeout(800) async def click(pg, loc, wait=1400): await cur_to(pg, loc) await pg.evaluate("(()=>{const c=document.getElementById('fakecur');c.style.transform='scale(.7)';setTimeout(()=>c.style.transform='',150)})()") await loc.click(); await pg.wait_for_timeout(wait) async def scroll_to(pg, y, ms=1200): await pg.evaluate(f"window.scrollTo({{top:{y},behavior:'smooth'}})"); await pg.wait_for_timeout(ms) async def ytext(pg, text): return await pg.evaluate(f"(()=>{{const w=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT);let n;while(n=w.nextNode()){{if(n.textContent.trim().startsWith({text!r})){{const r=n.parentElement.getBoundingClientRect();return r.top+window.scrollY}}}}return 0}})()") async def seg(pg, sid, v, wait=1500): inp = pg.locator(f'#{sid} input[value="{v}"]') if await inp.count(): await click(pg, pg.locator(f'#{sid} label[for="' + (await inp.get_attribute('id')) + '"]'), wait) else: await click(pg, pg.locator(f'#{sid} button.more'), 900) await click(pg, pg.locator(f'#{sid}-panel button[data-v="{v}"]'), wait) async def nth(pg, sid, i, wait=1500): await click(pg, pg.locator(f'#{sid} label').nth(i), wait) async def mode(pg, i, wait=1500): await click(pg, pg.locator(f'#modes label[for="mode-{i}"]'), wait) RS, ST = 'seg-robots', 'seg-staff' RES = 600 async def clip(name, fn): os.makedirs('out', exist_ok=True) async with async_playwright() as p: b = await p.chromium.launch(headless=False, args=['--window-position=0,0','--window-size=1920,1300','--hide-scrollbars','--force-device-scale-factor=1']) ctx = await b.new_context(no_viewport=True); pg = await ctx.new_page() await pg.goto(URL, wait_until='networkidle') await pg.add_style_tag(content='html{scroll-behavior:auto} ::-webkit-scrollbar{display:none}') await pg.evaluate(CUR); await pg.wait_for_timeout(1200) top = await pg.evaluate('window.outerHeight-window.innerHeight'); ih = await pg.evaluate('window.innerHeight') if ih < 1080: raise SystemExit(f'inner {ih}') pre = getattr(fn, 'pre', None) if pre: await pre(pg) rec = subprocess.Popen(['ffmpeg','-v','error','-y','-f','x11grab','-framerate','30','-video_size','1920x1080','-draw_mouse','0','-i',os.environ.get('DISPLAY',':99')+f'.0+0,{top}', '-c:v','libx264','-preset','veryfast','-crf','14','-pix_fmt','yuv420p',f'out/{name}.mp4'], stdin=subprocess.PIPE) await pg.wait_for_timeout(700); await fn(pg); rec.communicate(b'q') await ctx.close(); await b.close(); print('done', name, flush=True) def pre_scroll(y): async def f(pg): await pg.evaluate(f"window.scrollTo(0,{y})"); await pg.wait_for_timeout(600) return f async def c_entry2(pg): # 3人・0台 → 2人・2台(料理を運ぶだけ) await seg(pg, RS, 0, 2200); await seg(pg, ST, 2, 1600); await seg(pg, RS, 2, 3000) c_entry2.pre = pre_scroll(RES) async def c_bus(pg): # 2人・2台:まとめた皿も運ぶ await seg(pg, ST, 2, 1500); await mode(pg, 1, 3500) c_bus.pre = pre_scroll(RES) async def c_patrol6(pg): # 2人・食事中に集めて回る:1→6台 await seg(pg, ST, 2, 1200); await mode(pg, 2, 1500) for n in (1, 2, 3, 4, 5, 6): await seg(pg, RS, n, 1700) await pg.wait_for_timeout(1200) c_patrol6.pre = pre_scroll(RES) async def c_crowdp(pg): # 混雑・集めて回る・3台:2人 → 3人 await nth(pg, 'seg-arr', 2, 1500); await mode(pg, 2, 1500); await seg(pg, RS, 3, 1500) await seg(pg, ST, 2, 2600); await seg(pg, ST, 3, 2600) c_crowdp.pre = pre_scroll(RES) async def c_cost(pg): # 費用の欄:集めて回る・3台 → 月3万円 → 4時間 → 買う・補助金 await mode(pg, 2, 1000); await seg(pg, RS, 3, 1000) y = await pg.evaluate("document.getElementById('cost').getBoundingClientRect().top+window.scrollY") await scroll_to(pg, y-20, 2200); await pg.wait_for_timeout(3500) await nth(pg, 'seg-cprice', 0, 3200); await nth(pg, 'seg-cprice', 1, 1500) await nth(pg, 'seg-chours', 1, 3200); await nth(pg, 'seg-chours', 0, 1500) await nth(pg, 'seg-cpay', 1, 2000); await nth(pg, 'seg-csub', 1, 3500) await scroll_to(pg, y-20, 1500); await pg.wait_for_timeout(2500) async def c_cut(pg): # 「スタッフを1人減らして」の比べ:集めて回る・3人 → ロボット2台・3台 await mode(pg, 2, 1200) y = await pg.evaluate("document.getElementById('ctable').getBoundingClientRect().top+window.scrollY") await scroll_to(pg, y-260, 2000) await nth(pg, 'seg-crob', 1, 2500); await nth(pg, 'seg-crob', 2, 3500) CL = dict(entry2=c_entry2, bus=c_bus, patrol6=c_patrol6, crowdp=c_crowdp, cut=c_cut, cost=c_cost) async def main(): for k in (sys.argv[1:] or CL): await clip('sim_'+k, CL[k]) asyncio.run(main())