# 第1回「何台入れたら店は変わる」の背景の録画(試算 v2.7 のページをローカルで開いて録る) # 使い方: simulation/v2_7 で python3 -m http.server 8765 を動かし、xvfb-run -s '-screen 0 1920x1400x24' python3 rec_背景の録画.py [名前 ...] # 「配膳ロボットで1人減らせるか」の src/rec_背景の録画.py を写して書き換えた(2026-10-24) import asyncio, sys, os, subprocess from playwright.async_api import async_playwright URL = 'http://localhost:8765/serving-robot.v27.html' # iCloud simulation/v2_7/serving-robot.v27.html(p2m-site public/sim/serving-robot.html と同じ md5 88adf2b2) 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 mark_text(pg, text): r = 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();if(r.top>0&&r.top<1080)return [r.left+r.width/2,r.top+r.height/2]}}}}return null}})()") if r: await pg.evaluate(f"(()=>{{const c=document.getElementById('fakecur');c.style.left='{r[0]}px';c.style.top='{r[1]+40}px'}})()"); await pg.wait_for_timeout(900) 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 segi(pg, sid, i, wait=1500): # 何番目のボタン(混み具合など値が名前でないもの) await click(pg, pg.locator(f'#{sid} label[for="{sid[4:] if False else ""}"]') if False else pg.locator(f'#{sid} label').nth(i), wait) async def store(pg, i, wait=1500): await click(pg, pg.locator(f'#stores label[for="store-{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_open(pg): # 標準ファミレス・3人:0→1→2→3台 await pg.wait_for_timeout(1200); await scroll_to(pg, RES, 1500) for n in (0, 1, 2, 3): await seg(pg, RS, n, 1800) await pg.wait_for_timeout(1500) async def c_more(pg): # 3人:3→4→5→6台 for n in (3, 4, 5, 6): await seg(pg, RS, n, 1700) await pg.wait_for_timeout(1500) c_more.pre = pre_scroll(RES) async def c_one(pg): # 3人・1台:料理が届くまで 2分12秒 await seg(pg, RS, 1, 1800); await mark_text(pg, '料理ができてから届くまで'); await pg.wait_for_timeout(3500) await seg(pg, RS, 2, 1800); await mark_text(pg, '料理ができてから届くまで'); await pg.wait_for_timeout(2500) c_one.pre = pre_scroll(RES) async def c_table(pg): # 「ロボットの台数を変えると」の比べ y = await ytext(pg, 'ロボットの台数を変えると'); await scroll_to(pg, y-80, 2200); await pg.wait_for_timeout(5000) async def c_two(pg): # 2人:0→1→2→3→6台 await seg(pg, ST, 2, 1800) for n in (1, 2, 3, 6): await seg(pg, RS, n, 1600) await pg.wait_for_timeout(1500) c_two.pre = pre_scroll(RES) async def c_crowd(pg): # 混雑・3人:0→1台 await segi(pg, 'seg-arr', 2, 1800); await seg(pg, RS, 0, 1800); await seg(pg, RS, 1, 2500) c_crowd.pre = pre_scroll(RES) async def c_buffet(pg): # 焼肉食べ放題・2人:0→1台 await store(pg, 4, 1500); await scroll_to(pg, RES, 1200); await seg(pg, ST, 2, 1500); await seg(pg, RS, 0, 1600); await seg(pg, RS, 1, 2500) async def c_hotel(pg): # 洗い場が遠い店・2人:0→1台 await store(pg, 3, 1500); await scroll_to(pg, RES, 1200); await seg(pg, ST, 2, 1500); await seg(pg, RS, 0, 1600); await seg(pg, RS, 1, 2500) async def c_patrol(pg): # 2人・食事中に集めて回る・3台 await seg(pg, ST, 2, 1500); await mode(pg, 2, 1800); await seg(pg, RS, 3, 3000) c_patrol.pre = pre_scroll(RES) CL = dict(open=c_open, more=c_more, one=c_one, table=c_table, two=c_two, crowd=c_crowd, buffet=c_buffet, hotel=c_hotel, patrol=c_patrol) async def main(): for k in (sys.argv[1:] or CL): await clip('sim_'+k, CL[k]) asyncio.run(main())