tools/qa/tour_shots.py: one deterministic run capturing the release set into docs/shots/release_v3/ (+ contact.html), grouped by the flags each shot needs, all seed 20261990: - venue_night, district_posters, queue_night (John's HERO — now with A's flipped frontage poster VISIBLE by the door), gig_interior (the 4-piece + real instruments + a roster crowd — 2 punters carried through), rain_verandah, tram_stop - settled discipline (ledger #5): every shot measured after the async instrument GLBs / poster textures / chunk stream land (shoot-twice), from a clean per-group load; the queue is driven LIVE (explicit admits, not rAF auto-drain which automation throttles) The hero confirms ledger #1: the frontage poster reads from the street for the first time. Interior settles 35k->182k tris (draws 56 <= 350 gate). All street shots <=300 draws / <=200k tris. 0 page errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
edc0c479c1
commit
6a35b29352
1
docs/shots/release_v3/contact.html
Normal file
1
docs/shots/release_v3/contact.html
Normal file
@ -0,0 +1 @@
|
||||
<!doctype html><meta charset="utf-8"><title>PROCITY v3.0 — release tour</title><style>body{background:#14110c;color:#f4efe6;font:14px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;margin:24px}h1{font-weight:700}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(420px,1fr));gap:18px}figure{margin:0;background:#1d1810;border-radius:10px;overflow:hidden}img{width:100%;display:block}figcaption{padding:8px 12px;color:#cdc4b2}b{color:#ffd9a0}</style><h1>PROCITY v3.0 — release money-shot tour <small>· seed 20261990 · settled measurements</small></h1><div class="grid"><figure><img src="venue_night.png" alt="venue_night" loading="lazy"><figcaption><b>venue_night</b><br>121 draws · 51,899 tris · queue 2 · 14 posters</figcaption></figure><figure><img src="district_posters.png" alt="district_posters" loading="lazy"><figcaption><b>district_posters</b><br>186 draws · 134,068 tris · queue 2 · 14 posters</figcaption></figure><figure><img src="queue_night.png" alt="queue_night" loading="lazy"><figcaption><b>queue_night</b><br>121 draws · 61,738 tris · queue 2 · 14 posters</figcaption></figure><figure><img src="gig_interior.png" alt="gig_interior" loading="lazy"><figcaption><b>gig_interior</b><br>The Exchange Hotel · Screaming Utes · 4-piece · crowd 8 (2 from roster) · 56 draws · 35,216→182,657 tris (pre-load→settled)</figcaption></figure><figure><img src="rain_verandah.png" alt="rain_verandah" loading="lazy"><figcaption><b>rain_verandah</b><br>116 draws · 51,643 tris</figcaption></figure><figure><img src="tram_stop.png" alt="tram_stop" loading="lazy"><figcaption><b>tram_stop</b><br>105 draws · 172,187 tris</figcaption></figure></div>
|
||||
BIN
docs/shots/release_v3/district_posters.png
Normal file
BIN
docs/shots/release_v3/district_posters.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
BIN
docs/shots/release_v3/gig_interior.png
Normal file
BIN
docs/shots/release_v3/gig_interior.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 946 KiB |
BIN
docs/shots/release_v3/queue_night.png
Normal file
BIN
docs/shots/release_v3/queue_night.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 357 KiB |
BIN
docs/shots/release_v3/rain_verandah.png
Normal file
BIN
docs/shots/release_v3/rain_verandah.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
BIN
docs/shots/release_v3/tram_stop.png
Normal file
BIN
docs/shots/release_v3/tram_stop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 860 KiB |
BIN
docs/shots/release_v3/venue_night.png
Normal file
BIN
docs/shots/release_v3/venue_night.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 330 KiB |
176
tools/qa/tour_shots.py
Normal file
176
tools/qa/tour_shots.py
Normal file
@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PROCITY Lane F — the v3.0 release money-shot tour (round 15 · ?gigs=1). One deterministic run.
|
||||
|
||||
Captures the release set into docs/shots/release_v3/ (+ a contact.html), grouped by the flags each shot
|
||||
needs, all from seed 20261990 (deterministic, reproducible — never hand-cropped):
|
||||
• venue_night — lit pub frontage + streetlamp pools + posters at night (?gigs=1)
|
||||
• district_posters — the spine at night: poster poles + lit frontages receding (?gigs=1)
|
||||
• queue_night — JOHN'S HERO: close on the pub door at 'on', queue + marquee + A's now-visible poster
|
||||
• gig_interior — the 4-piece with real instruments + a roster crowd (continuity) (?gigs=1)
|
||||
• rain_verandah — a wet night outside the lit video-shop window (?weather=rain&winmap=1)
|
||||
• tram_stop — the bus shelter the tram pauses at (?tram=1)
|
||||
|
||||
Discipline (ROUND15 ledger #5 + B's note): every shot is measured SETTLED — after the async instrument
|
||||
GLBs / poster textures / chunk stream land (shoot-twice), from a clean per-group load (no orphan-chunk
|
||||
inflation); the queue is driven LIVE (rAF is throttled in automation, so we run the animate loop + admit
|
||||
explicitly rather than trust auto-drain).
|
||||
|
||||
Run: tools/.venv/bin/python tools/qa/tour_shots.py [--seed N]
|
||||
Needs the Playwright venv + Lane B's ?dbg=1 hook (incl. queue_night). Writes full-frame 1280x720 PNGs.
|
||||
"""
|
||||
import sys, time, socket, subprocess, pathlib
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent.parent
|
||||
PORT = 8130
|
||||
HOST = f'http://127.0.0.1:{PORT}'
|
||||
SEED = 20261990
|
||||
OUTDIR = ROOT / 'docs' / 'shots' / 'release_v3'
|
||||
if '--seed' in sys.argv:
|
||||
SEED = int(sys.argv[sys.argv.index('--seed') + 1])
|
||||
|
||||
# shot groups — each is one clean page load with the flags that shot needs (deterministic, same seed).
|
||||
GROUPS = [
|
||||
{'flags': 'gigs=1', 'street': ['venue_night', 'district_posters', 'queue_night'], 'interior': True},
|
||||
{'flags': 'weather=rain&tram=1&winmap=1', 'street': ['rain_verandah', 'tram_stop'], 'interior': False},
|
||||
]
|
||||
|
||||
# settle + re-pose a street bookmark once its live/async content (queue, instrument GLBs, poster textures,
|
||||
# streamed chunks) has landed, then report SETTLED stats.
|
||||
STREET_JS = r"""
|
||||
async (name) => {
|
||||
const P = window.PROCITY, D = window.DBG;
|
||||
D.shot(name); // initial pose + segment (2 renders)
|
||||
for (let i = 0; i < 8; i++) await new Promise(r => requestAnimationFrame(r)); // animate loop: queue spawns
|
||||
const q = (P.gigs && P.venueQueues) ? P.venueQueues.get(P.gigs.venueShopIds[0]) : null;
|
||||
await new Promise(r => setTimeout(r, 1400)); // async GLB / poster / chunk settle
|
||||
const stat = D.shot(name); // RE-POSE now that everything is loaded → the settled frame
|
||||
const queue = (P.gigs && P.queueCountOf) ? P.queueCountOf(P.gigs.venueShopIds[0]) : null;
|
||||
const posters = (P.plan.posters || []).length;
|
||||
return { name, draws: stat.drawCalls, tris: stat.tris, queue, posters };
|
||||
}
|
||||
"""
|
||||
|
||||
INTERIOR_JS = r"""
|
||||
async () => {
|
||||
const P = window.PROCITY, D = window.DBG, THREE = P.THREE;
|
||||
const id = P.gigs.venueShopIds[0];
|
||||
D.setSegment(5);
|
||||
// drive the queue live + admit a couple so the interior crowd carries roster identities (continuity)
|
||||
for (let i = 0; i < 8; i++) await new Promise(r => requestAnimationFrame(r));
|
||||
const q = P.venueQueues && P.venueQueues.get(id);
|
||||
if (q) for (let i = 0; i < 3 && q.count() > 0; i++) q.admitOne();
|
||||
D.enterShop(id); await new Promise(r => setTimeout(r, 900));
|
||||
const room = P.interiorMode.current; if (!room) return { ok: false };
|
||||
const st = room.stage;
|
||||
P.camera.position.set(st.x + 0.2, 1.6, room.dims.D / 2 - 1.2);
|
||||
P.camera.lookAt(new THREE.Vector3(st.x, 1.35, st.z));
|
||||
// shoot-twice / settled discipline (ledger #5): immediate vs after the instrument GLBs land
|
||||
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
|
||||
const trisImmediate = P.renderer.info.render.triangles;
|
||||
await new Promise(r => setTimeout(r, 1600));
|
||||
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
|
||||
const info = P.interiorMode.crewInfo;
|
||||
return { ok: true, draws: P.renderer.info.render.calls, trisImmediate, trisSettled: P.renderer.info.render.triangles,
|
||||
band: info ? info.band : 0, crowd: info ? info.crowd : 0, fromRoster: info ? info.fromRoster : 0,
|
||||
venue: (P.plan.shops.find(s => s.id === id) || {}).name, bandName: P.gigs.bandNameOf(id) };
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def port_up(port):
|
||||
with socket.socket() as s:
|
||||
s.settimeout(0.4)
|
||||
return s.connect_ex(('127.0.0.1', port)) == 0
|
||||
|
||||
|
||||
def ensure_server():
|
||||
if port_up(PORT):
|
||||
return None
|
||||
proc = subprocess.Popen(['python3', '-m', 'http.server', str(PORT)], cwd=str(ROOT / 'web'),
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
for _ in range(50):
|
||||
if port_up(PORT): return proc
|
||||
time.sleep(0.1)
|
||||
proc.terminate(); sys.exit(f'could not start http.server on :{PORT}')
|
||||
|
||||
|
||||
def boot(pg, flags):
|
||||
pg.goto(f'{HOST}/index.html?seed={SEED}&dbg=1&{flags}')
|
||||
pg.wait_for_function('window.DBG && window.DBG.ready === true', timeout=25000)
|
||||
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
|
||||
try:
|
||||
pg.wait_for_function('() => window.PROCITY && (!window.PROCITY.fleet || window.PROCITY.fleet.ready)', timeout=15000)
|
||||
except Exception:
|
||||
print(' ! fleet never readied — shooting placeholders')
|
||||
|
||||
|
||||
def contact_html(shots):
|
||||
cards = []
|
||||
for s in shots:
|
||||
cap = s['caption']
|
||||
cards.append(
|
||||
f'<figure><img src="{s["file"]}" alt="{s["name"]}" loading="lazy">'
|
||||
f'<figcaption><b>{s["name"]}</b><br>{cap}</figcaption></figure>')
|
||||
return (
|
||||
'<!doctype html><meta charset="utf-8"><title>PROCITY v3.0 — release tour</title>'
|
||||
'<style>body{background:#14110c;color:#f4efe6;font:14px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;margin:24px}'
|
||||
'h1{font-weight:700}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(420px,1fr));gap:18px}'
|
||||
'figure{margin:0;background:#1d1810;border-radius:10px;overflow:hidden}img{width:100%;display:block}'
|
||||
'figcaption{padding:8px 12px;color:#cdc4b2}b{color:#ffd9a0}</style>'
|
||||
f'<h1>PROCITY v3.0 — release money-shot tour <small>· seed {SEED} · settled measurements</small></h1>'
|
||||
f'<div class="grid">{"".join(cards)}</div>')
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
except ImportError:
|
||||
sys.exit('playwright not installed — tools/.venv/bin/python -m playwright install chromium')
|
||||
|
||||
print(f"\033[1mv3.0 RELEASE MONEY-SHOT TOUR\033[0m (seed {SEED} → docs/shots/release_v3/)")
|
||||
OUTDIR.mkdir(parents=True, exist_ok=True)
|
||||
srv = ensure_server()
|
||||
shots, errs_total = [], 0
|
||||
try:
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch()
|
||||
for grp in GROUPS:
|
||||
pg = b.new_page(viewport={'width': 1280, 'height': 720})
|
||||
errs = []
|
||||
pg.on('pageerror', lambda e: errs.append(str(e)))
|
||||
boot(pg, grp['flags'])
|
||||
for name in grp['street']:
|
||||
stat = pg.evaluate(STREET_JS, name)
|
||||
out = OUTDIR / f'{name}.png'
|
||||
pg.screenshot(path=str(out))
|
||||
cap = f"{stat['draws']} draws · {stat['tris']:,} tris"
|
||||
if stat.get('queue') is not None: cap += f" · queue {stat['queue']}"
|
||||
if name in ('district_posters', 'venue_night', 'queue_night'): cap += f" · {stat['posters']} posters"
|
||||
shots.append({'name': name, 'file': f'{name}.png', 'caption': cap})
|
||||
print(f" \033[32m●\033[0m {name}: {cap}")
|
||||
if grp['interior']:
|
||||
info = pg.evaluate(INTERIOR_JS)
|
||||
if info.get('ok'):
|
||||
out = OUTDIR / 'gig_interior.png'
|
||||
pg.screenshot(path=str(out))
|
||||
cap = (f"{info['venue']} · {info['bandName']} · {info['band']}-piece · crowd {info['crowd']} "
|
||||
f"({info['fromRoster']} from roster) · {info['draws']} draws · "
|
||||
f"{info['trisImmediate']:,}→{info['trisSettled']:,} tris (pre-load→settled)")
|
||||
shots.append({'name': 'gig_interior', 'file': 'gig_interior.png', 'caption': cap})
|
||||
print(f" \033[32m●\033[0m gig_interior: {cap}")
|
||||
else:
|
||||
print(" \033[31m✗\033[0m gig_interior: could not stage")
|
||||
errs_total += len(errs)
|
||||
if errs: print(f" ! {len(errs)} page error(s) in [{grp['flags']}]: {errs[0][:100]}")
|
||||
pg.close()
|
||||
b.close()
|
||||
finally:
|
||||
if srv: srv.terminate()
|
||||
|
||||
(OUTDIR / 'contact.html').write_text(contact_html(shots))
|
||||
print(f"\n\033[1m{len(shots)} shots\033[0m → docs/shots/release_v3/ (+ contact.html) · {errs_total} page errors")
|
||||
sys.exit(1 if errs_total or len(shots) < 6 else 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user