From 71bbdb089f6f37d15c029601c28f440537b46480 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Sun, 20 Sep 2026 11:06:58 -0700 Subject: [PATCH] Launch saved paired demo with actual screen mirror and preserve signal cues --- roadscore/onroad | 7 + roadscore/prototype/app.py | 1 + roadscore/prototype/curve_build_drop.py | 7 +- roadscore/prototype/curve_reaction.py | 3 +- roadscore/prototype/demo_session.py | 3 +- roadscore/prototype/mac_showcase.py | 17 +- .../prototype/native_prepared_showcase.py | 11 +- roadscore/prototype/paired_showcase.html | 16 ++ roadscore/prototype/paired_showcase.py | 148 ++++++++++++++++++ roadscore/prototype/prepared_core.py | 3 +- roadscore/prototype/test_paired_showcase.py | 45 ++++++ roadscore/prototype/test_prepared_core.py | 8 +- starpilot/system/the_galaxy/roadscore.py | 21 ++- .../the_galaxy/tests/test_roadscore_demo.py | 19 +++ 14 files changed, 287 insertions(+), 22 deletions(-) create mode 100644 roadscore/prototype/paired_showcase.html create mode 100644 roadscore/prototype/paired_showcase.py create mode 100644 roadscore/prototype/test_paired_showcase.py diff --git a/roadscore/onroad b/roadscore/onroad index a36888f068..c696224c2f 100755 --- a/roadscore/onroad +++ b/roadscore/onroad @@ -1,6 +1,13 @@ #!/usr/bin/env bash set -euo pipefail for argument in "$@"; do + if [[ "$argument" == "--demo" ]]; then + if [ -f /TICI ]; then + export PYTHONPATH="/data/roadscore-feasibility/venv/lib/python3.12/site-packages${PYTHONPATH:+:$PYTHONPATH}" + exec /usr/local/venv/bin/python "$(dirname "$0")/prototype/native_prepared_showcase.py" "$@" + fi + exec python3 "$(dirname "$0")/prototype/paired_showcase.py" "$@" + fi if [[ "$argument" == "--prepared-showcase" ]]; then exec python3 "$(dirname "$0")/prototype/mac_showcase.py" "$@" fi diff --git a/roadscore/prototype/app.py b/roadscore/prototype/app.py index cc083622b0..2dd62d3f8f 100644 --- a/roadscore/prototype/app.py +++ b/roadscore/prototype/app.py @@ -243,6 +243,7 @@ def callback(out,n,ti,status): curve_fresh=road_model_valid and 0<=callback_wall-command_wall<=.5 motion_blocked=bool(motion is not None and motion.stopped and motion.fresh) curve_state=event_state.get('demo_curve_state',event_state) + curve_state={**curve_state,'manual_signal_priority':demo_signal_mode in ('left','right') and signal_fresh} curve_grid=None if curve_state.get('demo_build_drop') and rhythm_timeline is not None: target_frame=frames-n+round((curve_state['predicted_peak']-source_time)*rate) diff --git a/roadscore/prototype/curve_build_drop.py b/roadscore/prototype/curve_build_drop.py index 345e465aef..a460d6c0d4 100644 --- a/roadscore/prototype/curve_build_drop.py +++ b/roadscore/prototype/curve_build_drop.py @@ -25,7 +25,7 @@ class CurveBuildDrop: self.status=dict(rendered_stage='bypass',rendered_active=False,added_delay_samples=0) def process(self, pcm, start_frame, grid, build_start_frame, payoff_frame, *, enabled=False, - source_fresh=True, blocked=False): + source_fresh=True, blocked=False, allow_roll=True): """Same frames, no input mutation. Caller supplies and authorizes the schedule. Disabling a fully bypassed instance is bit exact; cancelling an active effect @@ -56,8 +56,9 @@ class CurveBuildDrop: if not np.any(strength): self.tail=np.zeros((0,2),np.float32);outputs.append(x);continue frames=self.indices[:n]+first + if not allow_roll:self.tail=np.zeros((0,2),np.float32) roll=np.zeros_like(x);take=min(n,len(self.tail));roll[:take]+=self.tail[:take];self.tail=self.tail[take:].copy() - if requested and firstbuild: + if allow_roll and requested and firstbuild: beat=self.rate*60/grid.bpm;origin=grid.beat_phase*self.rate # Eighth notes establish the motif; the final four beats use sixteenths. boundary=max(build,payoff-4*beat) @@ -107,7 +108,7 @@ class CurveBuildDrop: if self.strength==0 and not valid:self.schedule=None result=pcm else:result=np.concatenate(outputs) if len(outputs)>1 else outputs[0] - self.status=dict(enabled=bool(enabled),input_fresh=bool(source_fresh),blocked=bool(blocked),grid_usable=bool(grid.usable), + self.status=dict(enabled=bool(enabled),input_fresh=bool(source_fresh),blocked=bool(blocked),grid_usable=bool(grid.usable),roll_allowed=bool(allow_roll), rendered_stage=stage,rendered_active=any_active,rendered_strength=self.strength,rendered_roll_peak=max(peaks,default=0.), rendered_cut_gain_min=cut_min,planned_build_frame=None if self.schedule is None else self.schedule[0], planned_payoff_frame=None if self.schedule is None else self.schedule[1],actual_payoff_frame=self.actual_payoff, diff --git a/roadscore/prototype/curve_reaction.py b/roadscore/prototype/curve_reaction.py index bcb54e6ed3..356217ff33 100644 --- a/roadscore/prototype/curve_reaction.py +++ b/roadscore/prototype/curve_reaction.py @@ -49,7 +49,8 @@ class CurveReaction: result=self._filter(pcm,start_frame,filtered_state,source_fresh,blocked,self.impact_payoff if armed else None) if self.impact is not None: result=self.impact.process(result,start_frame,self.grid,self.impact_start,self.impact_payoff, - enabled=armed,source_fresh=source_fresh,blocked=blocked) + enabled=armed,source_fresh=source_fresh,blocked=blocked, + allow_roll=not state.get('manual_signal_priority',False)) if not valid: self.impact_activation=None;self.impact_payoff=None;self.impact_start=None;self.impact_grid=None return result diff --git a/roadscore/prototype/demo_session.py b/roadscore/prototype/demo_session.py index a12b19719d..7b09211977 100644 --- a/roadscore/prototype/demo_session.py +++ b/roadscore/prototype/demo_session.py @@ -60,7 +60,8 @@ class DemoSession: result['ready_session_id'] = ready.get('session_id') if result['prepared'] else None state = read(out/'status.json') result['presentation_session_id'] = state.get('presentation_session_id') if running else None - result['failure'] = read(out/'failure.json').get('error') + result['failure'] = read(out/'demo_failure.json').get('error') + result['complete'] = read(out/'demo_complete.json').get('complete') is True return result def start(self, data): diff --git a/roadscore/prototype/mac_showcase.py b/roadscore/prototype/mac_showcase.py index 356559087b..d466dd6573 100644 --- a/roadscore/prototype/mac_showcase.py +++ b/roadscore/prototype/mac_showcase.py @@ -159,8 +159,10 @@ def audio_worker(a): from demo_engagement import DemoEngagement from operator_output import PresentationDelay from prepared_core import load_archive, initial_frame, PreparedPresentation + from prepared_clock import PreparedClock audio, rate, meta = load_archive(a.score_archive, a.route) processor = PreparedPresentation(a.score_archive, rate) + playback_clock = PreparedClock(rate) session = os.environ['ROADSCORE_SHOWCASE_SESSION'] controls = DemoEngagement(a.out, session, 'replay') delay = PresentationDelay(a.presentation_root or a.out,output_provider=lambda: {'id':a.output_identity} if a.output_identity else None) @@ -192,14 +194,12 @@ def audio_worker(a): if anchor is None or done:return now=time.monotonic();dac=now+float(ti.outputBufferDacTime-ti.currentTime) expected=initial_frame(meta,anchor[0],dac-anchor[1],rate) - if position is None:position=expected;first_frame=position - start=position;position+=n - max_drift=max(max_drift,abs(start-expected)/rate) if status:flags+=1 - chunk=np.zeros((n,2),np.float32) - lo=max(0,-start);hi=min(n,len(audio)-start) - if hi>lo:chunk[lo:hi]=audio[start+lo:start+hi] try: + chunk,clock_info=playback_clock.render(audio,expected,n) + start=clock_info['source_frame'];position=clock_info['next_source_frame'] + if first_frame is None:first_frame=start + max_drift=max(max_drift,abs(clock_info['pre_error_frames'])/rate) wet,cues=processor.process(chunk,start,state,controls.selection) rendered=dict(sequence=start,callback_wall=now,dac_wall=dac,cues=cues) if not a.muted:out[:]=wet @@ -241,6 +241,7 @@ def audio_worker(a): if anchor is not None and state['route_t'] is not None and np.isfinite(state['route_t']): snapshot.update(route_t=float(state['route_t']),elapsed=max(0,(position or 0)/rate), source_model_ns=int(sm.logMonoTime['modelV2'])) + snapshot['prepared_clock']=playback_clock.snapshot() snapshot=delay.apply(snapshot,rendered) # The command API must remain available during initial DAC lead-in. snapshot.setdefault('engagement_presentation',{'enabled':True}) @@ -257,9 +258,9 @@ def audio_worker(a): finally:server.server_close() elif forwarder is not None:forwarder.close() finally:trace.close() - write_json(a.out/'prepared_summary.json',dict(generation_invoked=False,source=str(a.score_archive),first_source_frame=first_frame,last_source_frame=position,sample_rate=rate,portaudio_flags=flags,max_clock_error_seconds=max_drift,callback_errors=errors,muted=a.muted,session_id=session,manual_scope='isolated replay display and presentation only')) + write_json(a.out/'prepared_summary.json',dict(generation_invoked=False,source=str(a.score_archive),first_source_frame=first_frame,last_source_frame=position,sample_rate=rate,portaudio_flags=flags,max_clock_error_seconds=max_drift,prepared_clock=playback_clock.snapshot(),callback_errors=errors,muted=a.muted,session_id=session,manual_scope='isolated replay display and presentation only')) if errors:raise RuntimeError(errors[0]) - if max_drift>.05:raise RuntimeError('Prepared audio clock drift exceeded 50 ms') + if playback_clock.snapshot()['max_post_error_seconds']>.05:raise RuntimeError('Prepared audio clock drift remained above 50 ms after recovery') def apply_showcase_config(a, config): diff --git a/roadscore/prototype/native_prepared_showcase.py b/roadscore/prototype/native_prepared_showcase.py index 13302b085c..f6aec4777d 100644 --- a/roadscore/prototype/native_prepared_showcase.py +++ b/roadscore/prototype/native_prepared_showcase.py @@ -74,6 +74,7 @@ def native_environment(project, root, out, session, inherited=None): env.update(PYTHONDONTWRITEBYTECODE='1',OPENPILOT_PREFIX='roadscore_replay',BASEDIR=str(project),PWD=str(project),NOBOARD='1',SIMULATION='1',SKIP_FW_QUERY='1',BIG='0',ROADSCORE_PREPARED_SHOWCASE='1',ROADSCORE_SHOWCASE_SESSION=session,ROADSCORE_REPLAY_UI_CONTROLS='1',ROADSCORE_CLEAN_DEMO_UI='1',ROADSCORE_OVERLAY='1',ROADSCORE_STATUS_FILE=str(out/'status.json'),ROADSCORE_UI_AUDIT=str(out/'ui_audit.jsonl'),ROADSCORE_OVERLAY_CAPTURE=str(out/'overlay.png'),ROADSCORE_AUDIO_DRAIN_FILE=str(out/'audio_drained.json'),ROADSCORE_PRESENTATION_POLICY='conservative-v4',SP_ALLOW_DESKTOP_FAKE_WIFI='0',SP_ALLOW_DESKTOP_FAKE_BLUETOOTH='0',SP_ONROAD_NAV_DEMO='0',SP_ONROAD_CEM_DEMO='0') paths=[HERE,project,project/'starpilot/third_party',*project.glob('*_repo'),Path('/data/roadscore-feasibility/venv/lib/python3.12/site-packages')] env['PYTHONPATH']=':'.join(map(str,paths)) + env['ROADSCORE_MIRROR_DIR']=str(out/'mirror') return env @@ -112,11 +113,11 @@ def main(): from route_favorites import resolve_favorite route,_=resolve_favorite(a.route,root/'routes/favorites.json') if a.score_archive is None: - registry=json.loads((root/'assets/prepared_showcase.json').read_text()) - entry=registry.get('routes',{}).get(a.route,registry) - if entry.get('route')!=route:raise ValueError('No matching prepared showcase registered') - a.score_archive=Path(entry['archive']) - if a.curve_plan is None and entry.get('curve_plan'):a.curve_plan=Path(entry['curve_plan']) + from demo_catalog import entry + selected=entry(root,a.route) + if selected['route']!=route:raise ValueError('No matching prepared showcase registered') + a.score_archive=Path(selected['archive']) + if a.curve_plan is None and selected.get('curve_plan'):a.curve_plan=Path(selected['curve_plan']) archive=a.score_archive.resolve(strict=True) from prepared_core import load_archive audio,rate,meta=load_archive(archive,route) diff --git a/roadscore/prototype/paired_showcase.html b/roadscore/prototype/paired_showcase.html new file mode 100644 index 0000000000..e0d89b3997 --- /dev/null +++ b/roadscore/prototype/paired_showcase.html @@ -0,0 +1,16 @@ + +RoadScore demo + +

RoadScore

Connecting to the comma…
+
Live mirror of the comma replay
Preparing saved replay…
+
+
Open Galaxy
+

Saved soundtrack · live presentation controls · actual comma screen
+ diff --git a/roadscore/prototype/paired_showcase.py b/roadscore/prototype/paired_showcase.py new file mode 100644 index 0000000000..7d334dd186 --- /dev/null +++ b/roadscore/prototype/paired_showcase.py @@ -0,0 +1,148 @@ +"""Launch one saved native replay and mirror its actual UI on the Mac.""" +import argparse +import json +import os +from pathlib import Path +import signal +import subprocess +import threading +import time +import uuid +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from urllib.parse import urlencode +from urllib.request import Request, build_opener, ProxyHandler + +from paired_demo_controls import GalaxyPeer, _NoRedirect, _http_json + +HERE = Path(__file__).resolve().parent + + +class DemoPeer: + def __init__(self, url, *, transport=_http_json): + self.peer = GalaxyPeer(url, allow_lan_http=True) + self.transport = transport + + def call(self, action, data=None): + return self.transport('GET' if action == 'status' else 'POST', + self.peer.base_url+'/api/roadscore/'+action, data, + {'Accept':'application/json','Content-Type':'application/json','Cache-Control':'no-store'}, 4.) + + def frame(self, request_id): + url=self.peer.base_url+'/api/roadscore/demo_frame?'+urlencode({'request_id':request_id}) + request=Request(url,headers={'Accept':'image/jpeg','Cache-Control':'no-store'}) + with build_opener(ProxyHandler({}),_NoRedirect()).open(request,timeout=2.) as response: + if response.headers.get_content_type() != 'image/jpeg':raise ValueError('Expected a live replay image') + frame=response.read(1500001) + if len(frame)>1500000:raise ValueError('Replay frame is too large') + return frame, {key:response.headers.get(key,'') for key in ('X-RoadScore-Frame','X-RoadScore-Age-Ms')} + + +def control_server(peer, request_id, port, state): + class Handler(BaseHTTPRequestHandler): + def log_message(self,*args):pass + def send(self, body, kind='application/json', code=200, headers=None): + self.send_response(code) + self.send_header('Content-Type',kind);self.send_header('Cache-Control','no-store') + self.send_header('Content-Length',str(len(body))) + for name,value in (headers or {}).items():self.send_header(name,value) + self.end_headers() + try:self.wfile.write(body) + except (BrokenPipeError,ConnectionResetError):pass + def json(self,value,code=200):self.send(json.dumps(value).encode(),code=code) + def do_GET(self): + if self.path == '/':self.send((HERE/'paired_showcase.html').read_bytes(),'text/html; charset=utf-8');return + if self.path == '/status': + try:self.json({'phase':state['phase'],'peer':peer.call('status'),'galaxy_url':peer.peer.base_url+'/mobile/#/roadscore','muted':state['muted']}) + except Exception as error:self.json({'phase':state['phase'],'error':str(error)},503) + return + if self.path.startswith('/frame?'): + try: + frame,headers=peer.frame(request_id);self.send(frame,'image/jpeg',headers=headers) + except Exception:self.json({'error':'Waiting for a fresh comma replay frame'},503) + return + self.send_error(404) + def do_POST(self): + if self.path!='/control':self.send_error(404);return + expected={f'http://127.0.0.1:{self.server.server_port}',f'http://localhost:{self.server.server_port}'} + if ('http://'+self.headers.get('Host','') not in expected + or self.headers.get('Origin') not in expected + or self.headers.get('Sec-Fetch-Site','same-origin') != 'same-origin'): + self.json({'error':'Open controls from this local demo page'},403);return + try: + length=int(self.headers.get('Content-Length','0')) + if not 0deadline:raise TimeoutError('Saved replay did not prepare within 90 seconds; no model is being compiled') + time.sleep(.25) + peer.call('demo_play',{'request_id':request_id,'session_id':ready['ready_session_id']});released=True + state['phase']='Playing saved replay' + print('Playing on comma; this Mac mirrors its actual UI. Galaxy controls both views.',flush=True) + started=time.monotonic() + while time.monotonic()-started=3.5:curve=dict(kind='curve',phase='neutral',amount=0.,activation=None) state=dict(active=True,signal_on=True,fresh=True,car_fresh=True,model_fresh=True, speed=10.,alert_key='',alert_meaningful=False,curve=curve,route_t=now) - wet,_=engine.process(core[frame:frame+960],frame,state,('engaged','left')) + wet,cues=engine.process(core[frame:frame+960],frame,state,('engaged','left')) + if .51500000:return jsonify(error='Invalid replay image'),503 + response=Response(data,mimetype='image/jpeg') + response.headers.update({'Cache-Control':'no-store','X-RoadScore-Frame':str(frame.get('frame_id','')),'X-RoadScore-Age-Ms':str(round(age*1000,1))}) + return response + @app.route('/api/roadscore/status') def roadscore_status(): response = jsonify(operator.status(real_offroad())) diff --git a/starpilot/system/the_galaxy/tests/test_roadscore_demo.py b/starpilot/system/the_galaxy/tests/test_roadscore_demo.py index 9d76c03792..55402b5601 100644 --- a/starpilot/system/the_galaxy/tests/test_roadscore_demo.py +++ b/starpilot/system/the_galaxy/tests/test_roadscore_demo.py @@ -70,5 +70,24 @@ class DemoTests(unittest.TestCase): self.assertEqual(state['backend'],'Prepared local audio') self.assertEqual(state['demo']['playhead']['route_t'],12.) self.assertGreaterEqual(state['demo']['status_age_seconds'],0.) + def test_mirror_frame_requires_fresh_owned_session_and_offroad(self): + from flask import Flask + from unittest.mock import patch + import operator_output + app=Flask(__name__) + folder=self.run/'mirror';folder.mkdir() + (folder/'latest.jpg').write_bytes(b'\xff\xd8fixture\xff\xd9') + frame={'session_id':'ready-session','frame_id':1,'captured_wall':time.monotonic()} + (folder/'frame.json').write_text(json.dumps(frame)) + with patch.object(module,'Operator') as factory,patch.object(operator_output,'real_offroad',return_value=True) as parked: + factory.return_value.demo_controller.return_value.status.return_value={'running':True,'request_id':'owned-request','ready_session_id':'ready-session','out':str(self.run)} + module.register(app,None);client=app.test_client() + self.assertEqual(client.get('/api/roadscore/demo_frame?request_id=wrong').status_code,409) + response=client.get('/api/roadscore/demo_frame?request_id=owned-request') + self.assertEqual(response.status_code,200);self.assertEqual(response.mimetype,'image/jpeg') + frame['captured_wall']=time.monotonic()-2;(folder/'frame.json').write_text(json.dumps(frame)) + self.assertEqual(client.get('/api/roadscore/demo_frame?request_id=owned-request').status_code,503) + parked.return_value=False + self.assertEqual(client.get('/api/roadscore/demo_frame?request_id=owned-request').status_code,409) if __name__=='__main__':unittest.main()