Polish RoadScore overlay states and document the demo

This commit is contained in:
firestar5683
2026-09-18 22:37:46 -07:00
parent 90bd992ced
commit e8eae47837
6 changed files with 296 additions and 28 deletions
+43
View File
@@ -0,0 +1,43 @@
# RoadScore demo
RoadScore makes an adaptive soundtrack from road context. The native demo shows a recorded drive in the normal comma UI while ACE-Step generates new musical passages on Chestnut. Prism is the prepared musical profile. RoadScore supplies the native port, musical continuation, buffering, quality checks, immediate gestures and synchronization; ACE-Step is a pretrained model, not a model trained by this project.
The conductor receives only road messages already delivered by replay. It requests future passages and uses immediate, source-derived gestures for short road events. Generation takes time: the buffer lets the current score continue while the next passage is being made. A curve gesture does not imply a complete new neural composition in that instant.
## Reading the overlay
The compact lower-left panel leaves the existing camera, lane and path display in place. Its rows show RoadScore's activity, profile, section, compute backend and buffered music.
| Display | Meaning |
| --- | --- |
| Prism / Aurora | Reported prepared musical profile. Prism is the primary candidate. |
| INTENT: VERSE > PRECHORUS | Current section conditioning and the next scheduled section, when provided by the runtime. These are musical intentions, not verified structural labels. |
| ACE · CHESTNUT | Reported composer and compute backend. This is not an independent GPU health probe. |
| PREPARING | The status does not yet report playback readiness. Cold preparation is separate from warm generation time. |
| READY | The runtime reports ready and no degraded condition is present. This does not certify listening quality or live-driving readiness. |
| GENERATING | A fresh passage is in flight. The elapsed time is job time, not an estimated countdown. Buffered music may continue playing. |
| DEGRADED | The runtime reports a failure, quality issue or accepted-music hold. This label takes priority even if a generation job is still in flight. |
| Holding accepted music | Previously accepted music is being reused. It is not fresh generation. |
| BUFFER 64s | Reported queued playback time. It can include accepted-music holds; it is not a fresh-music counter. `--` means unavailable. |
| STORED SCORE · NO COMPUTE | Archived score playback. It must not be presented as fresh Chestnut generation. |
Native text uses ASCII labels and measured truncation to avoid unsupported glyphs or text spilling outside the panel. The middle dot in the backend label is drawn geometrically. Long section labels end with `...`; full source values remain in the existing status/audit JSON.
## Presenter explanation
“Prism is the musical identity. The recorded road messages guide the score as they arrive. ACE generates future passages locally on Chestnut, while the buffer keeps playback moving. Short gestures can react sooner than a new generated passage. READY means the runtime is ready; GENERATING means a new passage is being made. If generation cannot keep up or a quality check fails, DEGRADED makes that visible, and the system can reuse accepted music.”
Use [EVENT.md](EVENT.md) for measured event claims. The recorded 45 W native pass was muted and does not establish physical audio, Bluetooth, live driving or modeld coexistence. Historical performance reports describe a different bench. Operational commands belong in [README.md](README.md#run-on-the-event-comma) and are for the agent/operator who owns the hardware.
## Offline UI review
From the repository root, with a Python environment containing Pillow:
```sh
python3 -m unittest discover -s roadscore/tests -v
python3 roadscore/tools/preview_overlay.py --output roadscore/results/ui-polish/overlay-states.png
```
The preview renders six synthetic states with the repository's Inter bitmap font atlas: preparation, ready, generating, accepted-music hold, failed composer and stored score. It uses CPU image drawing and does not open a window, contact a device, start a worker, run replay or open an audio stream. It is a layout review, not a native-rendering or hardware-validation claim. The output stays in ignored `results/`.
The active native overlay is `prototype/overlay.py`; the pure presentation helper is `prototype/overlay_view.py`. `prototype/index.html` and `prototype/native_display.py` are older excerpt-specific bench interfaces, not the normal onroad demo. The historical style selector is not an ACE profile selector. Keep those older entrypoints distinct when presenting the current demo.
+13 -1
View File
@@ -10,11 +10,23 @@ recorded cereal → semantic road state → section requests → ACE on Chestnut
Only already-delivered messages enter musical decisions. The ordinary replay engine owns route resolution, camera video, path/lane display, and playback timing. Existing generated music may be held when fresh material is unavailable; holds are recorded and are not counted as new generation.
## Demo and source guide
- [Demo explanation and overlay legend](DEMO.md): what the audience sees and what each state means.
- [Event evidence](EVENT.md): measurements and preserved failures on the event hardware; read the latest checkpoint.
- [Status history](STATUS.md): event summary followed by explicitly historical pre-event results.
- `prototype/overlay.py` and `prototype/overlay_view.py`: native overlay integration and read-only presentation.
- `prototype/`: score runtime, composition, gestures, transport and archival code.
- `tools/`: development and event utilities; [offline UI preview](DEMO.md#offline-ui-review) needs no device.
- `tests/`: isolated UI tests. Existing runtime tests remain beside their modules and in `experiments/`.
- `experiments/`, `chestnut_music/`, `chestnut_stable_audio/`: research and historical backend work.
- `routes/`, `results/`, model assets: private local data, excluded from Git.
## Event status
Source lives in this repository on the `RoadScore` branch and on the event comma at `/data/openpilot/roadscore`. `/data/roadscore` is a compatibility symlink. See [EVENT.md](EVENT.md) for current measurements and preserved failures.
The official Chestnut example passed. Unrestricted decoder runs lost GPU access; a 30 W cap passed 40 repeated decodes and full Prism preparation. At that cap, warm continuation takes approximately 44s per 28s of new music (RTF 1.57). A 45 W comparison is underway. **The event native-replay acceptance gate is not yet complete.** Live/modeld coexistence and physical speaker/Bluetooth validation remain pending.
The latest recorded checkpoint in [EVENT.md](EVENT.md#first-clean-event-native-replay--45-w) reports a complete 254.1-second muted native Prism replay at an explicitly selected 45 W: six accepted fresh jobs, no holds, no underflows and no worker failure. This is one instrumented pass, not a production-reliability claim. Live/modeld coexistence and physical speaker/Bluetooth validation remain pending. Earlier 30 W results and failed runs are preserved in that report.
## Run on the event comma
+5 -27
View File
@@ -1,12 +1,13 @@
"""Optional external overlay on the existing normal UI; no public source modifications."""
import json,time,os
from overlay_view import draw_panel
from pathlib import Path
def install():
if os.environ.get('ROADSCORE_OVERLAY')!='1':return
import pyray as rl
from openpilot.system.ui.lib.application import gui_app,FontWeight
original=gui_app.render;last=0.;state={};icons={};frames=0;captured=False;capture_ready_since=None
original=gui_app.render;last=0.;state={};frames=0;captured=False;capture_ready_since=None
path=Path(os.environ['ROADSCORE_STATUS_FILE'])
def draw():
nonlocal last,state,frames,captured,capture_ready_since
@@ -15,36 +16,13 @@ def install():
try:state=json.loads(path.read_text())
except (OSError,ValueError):pass
last=now
readiness=state.get('readiness','Preparing');ready=readiness=='READY'
extra=bool(state.get('gesture_active') or state.get('gesture_queued') or state.get('turn_signal_music'));height=126 if extra else 104
x=12;y=max(12,gui_app.height-height-12);width=min(320,gui_app.width*.55)
rl.draw_rectangle_rounded(rl.Rectangle(x,y,width,height),.12,8,rl.Color(15,22,31,220))
key='crossed' if state.get('compute')=='none' else ('green' if ready else 'loading')
if key not in icons:
try:icons[key]=gui_app.texture('icons_mici/egpu_'+key+'.png',24,18)
except Exception:icons[key]=None
if icons[key]:rl.draw_texture(icons[key],x+10,y+10,rl.WHITE)
font=gui_app.font(FontWeight.NORMAL)
lines=[f"RoadScore / {state.get('style','Preparing')}",state.get('section',readiness)]
if state.get('next_section'):lines[1]+=' > '+state['next_section']
lead=state.get('lead');detail=f'Curve {lead:.1f}s / ' if isinstance(lead,(int,float)) and lead>0 else ''
detail+=f"{readiness} / Buffer {(state.get('buffered') or 0):.0f}s"
elapsed=state.get('generation_elapsed_seconds')
backend='ACE / CHESTNUT' if state.get('composer')=='ace' else str(state.get('composer','LOCAL')).upper()
activity='GENERATING' if state.get('job_inflight') else readiness
if state.get('job_inflight') and isinstance(elapsed,(int,float)):activity+=f' {elapsed:.1f}s'
lines.append(f'{backend} / {activity}')
lines.append(detail)
if extra:
active=state.get('gesture_active') or [];queued=state.get('gesture_queued') or []
gesture='Signal percussion' if state.get('turn_signal_music') else (active[0] if active else ('Queued '+queued[0]['kind'] if queued else ''))
lines.append(str(gesture).replace('_',' '))
for i,line in enumerate(lines):rl.draw_text_ex(font,str(line),rl.Vector2(x+(42 if i==0 else 10),y+10+i*22),13,0,rl.WHITE)
view=draw_panel(rl,gui_app.font(FontWeight.NORMAL),state,gui_app.width,gui_app.height)
ready=view['ready']
frames+=1
if frames==1:path.with_name('ui_capture_origin.json').write_text(json.dumps({'wall':time.monotonic(),'frame':0}))
if os.environ.get('ROADSCORE_CAPTURE_TIMING')=='1':
with path.with_name('ui_frames.jsonl').open('a') as audit:audit.write(json.dumps({'frame':frames-1,'wall':time.monotonic(),'state':state})+'\n')
if frames%60==1:path.with_name('overlay_status.json').write_text(json.dumps({'frames':frames,'native_gpu_icon':bool(icons[key]),'state':state}))
if frames%60==1:path.with_name('overlay_status.json').write_text(json.dumps({'frames':frames,'native_gpu_icon':False,'presentation':view,'state':state}))
from openpilot.selfdrive.ui.ui_state import ui_state
if ready and ui_state.started and capture_ready_since is None:capture_ready_since=now
if capture_ready_since is not None and now-capture_ready_since>=5 and ui_state.started and not captured and os.environ.get("ROADSCORE_OVERLAY_CAPTURE"):
+105
View File
@@ -0,0 +1,105 @@
"""Read-only presentation of RoadScore status; no runtime or device dependencies."""
import math
import unicodedata
def display_text(value):
"""The native bitmap font reliably supports ASCII; draw separators as shapes."""
text = str(value or '').replace('→', ' > ').replace('·', ' / ').replace('—', '-').replace('–', '-')
return ' '.join(unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode().split())
def seconds(value):
if isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value):
return max(0., float(value))
return None
def overlay_view(state):
readiness = str(state.get('readiness') or 'PREPARING').upper()
degraded = readiness == 'DEGRADED' or any(state.get(k) for k in ('worker_failed', 'holding_accepted_music', 'quality_failures'))
activity = 'DEGRADED' if degraded else ('GENERATING' if state.get('job_inflight') else readiness)
if activity not in ('READY', 'GENERATING', 'DEGRADED'):
activity = 'PREPARING'
stored = state.get('compute') == 'none'
composer = display_text(state.get('composer')).upper()
backend = ('STORED SCORE', 'NO COMPUTE') if stored else ((composer, 'CHESTNUT') if composer in ('ACE', 'SA3') else ('LOCAL SCORE', ''))
profile = state.get('style') or str(state.get('profile') or 'Preparing').title()
section = display_text(state.get('section') or 'Waiting for score').upper()
section = section.removesuffix(' / CONTINUOUS')
if state.get('next_section'):
section += ' > ' + display_text(state['next_section']).upper()
if state.get('form_labels_are_intent'):
section = 'INTENT: ' + section
elapsed = seconds(state.get('generation_elapsed_seconds'))
note = ''
if state.get('holding_accepted_music'):
note = 'Holding accepted music'
elif state.get('worker_failed'):
note = 'Composer unavailable'
elif degraded:
note = 'Quality check / reserve in use'
elif state.get('job_inflight'):
note = 'New passage' + (f' / {elapsed:.1f}s elapsed' if elapsed is not None else '')
event = ''
if state.get('turn_signal_music'):
event = 'Signal percussion'
elif state.get('gesture_active'):
event = display_text(state['gesture_active'][0]).replace('_', ' ')
elif state.get('gesture_queued'):
event = 'Queued ' + display_text(state['gesture_queued'][0].get('kind')).replace('_', ' ')
elif (lead := seconds(state.get('lead'))) is not None and lead > 0:
event = f'Curve in {lead:.1f}s'
if event:
note = f'{note} / {event}' if note else event
return dict(profile=display_text(profile), section=section, backend=backend, activity=activity,
buffered=seconds(state.get('buffered')), note=note,
ready=readiness == 'READY' and not degraded, stored=stored)
def fit_text(text, max_width, measure):
"""Fit with the actual font metrics, using a supported ellipsis."""
if measure(text) <= max_width + .01:
return text
while text and measure(text + '...') > max_width + .01:
text = text[:-1]
return text.rstrip() + '...' if text else ''
def draw_panel(rl, font, state, screen_width, screen_height):
view = overlay_view(state)
width = min(340, screen_width - 24)
height = 146 if view['note'] else 126
x, y = 12, max(12, screen_height - height - 12)
color = {'READY': (123, 229, 193), 'GENERATING': (131, 192, 255),
'DEGRADED': (255, 197, 112), 'PREPARING': (180, 190, 204)}[view['activity']]
accent = rl.Color(*color, 255)
muted = rl.Color(180, 190, 204, 255)
rl.draw_rectangle_rounded(rl.Rectangle(x, y, width, height), .12, 8, rl.Color(15, 22, 31, 235))
def text(label, left, top, size=13, tint=rl.WHITE, available=None):
available = width - left - 12 if available is None else available
label = fit_text(label, available, lambda s: rl.measure_text_ex(font, s, size, 0).x)
rl.draw_text_ex(font, label, rl.Vector2(x + left, y + top), size, 0, tint)
badge_width = rl.measure_text_ex(font, view['activity'], 11, 0).x + 16
rl.draw_rectangle_rounded(rl.Rectangle(x + width - badge_width - 10, y + 10, badge_width, 22), .4, 8, rl.Color(*color, 28))
text(view['activity'], width - badge_width - 2, 15, 11, accent)
text('RoadScore', 12, 12, 17, available=width - badge_width - 34)
text(view['profile'], 12, 39, 17)
text(view['section'], 12, 62, 12, muted)
left, right = view['backend']
text(left, 12, 83, 11, muted)
if right:
offset = 12 + rl.measure_text_ex(font, left, 11, 0).x
# A geometric middle dot avoids missing-glyph boxes in the native font.
rl.draw_circle(int(x + offset + 7), int(y + 89), 1.5, muted)
text(right, offset + 15, 83, 11, muted)
buffer = view['buffered']
text('BUFFER', 12, 105, 11, muted)
label = '--' if buffer is None else f'{buffer:.0f}s'
label_width = rl.measure_text_ex(font, label, 13, 0).x
text(label, width - label_width - 12, 103, 13, accent)
if view['note']:
text(view['note'], 12, 126, 11, accent)
return view
+51
View File
@@ -0,0 +1,51 @@
"""Presentation contracts only; these tests never import the runtime or device tools."""
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'prototype'))
from overlay_view import display_text, fit_text, overlay_view
class OverlayTests(unittest.TestCase):
def test_degradation_takes_priority_over_generation(self):
for reason in ('worker_failed', 'holding_accepted_music', 'quality_failures'):
with self.subTest(reason=reason):
view = overlay_view(dict(readiness='READY', job_inflight=True, **{reason: True}))
self.assertEqual(view['activity'], 'DEGRADED')
self.assertFalse(view['ready'])
self.assertEqual(overlay_view(dict(readiness='DEGRADED', job_inflight=True))['activity'], 'DEGRADED')
def test_active_generation_preserves_playback_readiness(self):
view = overlay_view(dict(readiness='READY', job_inflight=True, generation_elapsed_seconds=12.3))
self.assertEqual(view['activity'], 'GENERATING')
self.assertTrue(view['ready'])
self.assertIn('12.3s', view['note'])
def test_missing_buffer_is_not_zero(self):
for value in (None, float('nan'), float('inf'), '12', True):
self.assertIsNone(overlay_view({'buffered': value})['buffered'])
self.assertEqual(overlay_view({'buffered': -1})['buffered'], 0)
self.assertEqual(overlay_view({})['activity'], 'PREPARING')
def test_stored_score_never_claims_chestnut_compute(self):
view = overlay_view(dict(composer='ace', compute='none', readiness='READY'))
self.assertEqual(view['backend'], ('STORED SCORE', 'NO COMPUTE'))
def test_profile_and_section_intent(self):
view = overlay_view(dict(profile='prism', composer='ace', section='VERSE / CONTINUOUS',
next_section='PRECHORUS', form_labels_are_intent=True))
self.assertEqual(view['profile'], 'Prism')
self.assertEqual(view['section'], 'INTENT: VERSE > PRECHORUS')
self.assertEqual(view['backend'], ('ACE', 'CHESTNUT'))
def test_native_labels_and_truncation_use_supported_characters(self):
self.assertEqual(display_text('Prism · VERSE → CHORUS — café'), 'Prism / VERSE > CHORUS - cafe')
result = fit_text('A long section label', 10, len)
self.assertLessEqual(len(result), 10)
self.assertTrue(result.endswith('...'))
self.assertEqual(fit_text('long', 2, len), '')
if __name__ == '__main__':
unittest.main()
+79
View File
@@ -0,0 +1,79 @@
"""Render synthetic overlay states with the repo's font atlas, entirely on CPU.
Requires Pillow. No device, network, audio, replay, worker, or window is opened.
"""
import argparse
import re
import sys
from pathlib import Path
from types import SimpleNamespace
from PIL import Image, ImageDraw
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / 'roadscore/prototype'))
from overlay_view import draw_panel
class Canvas:
WHITE = (255, 255, 255, 255)
Color = staticmethod(lambda *rgba: rgba)
Rectangle = staticmethod(lambda x, y, w, h: (x, y, x + w, y + h))
Vector2 = staticmethod(lambda x, y: SimpleNamespace(x=x, y=y))
def __init__(self, image, font_path):
self.image = image
self.atlas = Image.open(font_path.with_suffix('.png')).convert('RGBA')
lines = font_path.read_text().splitlines()
self.base = abs(int(re.search(r'size=(-?\d+)', lines[0])[1]))
self.glyphs = {}
for line in lines:
if line.startswith('char '):
glyph = {key: int(value) for key, value in re.findall(r'(\w+)=(-?\d+)', line)}
self.glyphs[glyph['id']] = glyph
def measure_text_ex(self, font, text, size, spacing):
return SimpleNamespace(x=sum(self.glyphs[ord(c)]['xadvance'] for c in text) * size / self.base)
def draw_text_ex(self, font, text, pos, size, spacing, color):
scale = size / self.base
for char in text:
g = self.glyphs[ord(char)]
if g['width'] and g['height']:
mask = self.atlas.crop((g['x'], g['y'], g['x'] + g['width'], g['y'] + g['height'])).getchannel('A')
mask = mask.resize((max(1, round(g['width'] * scale)), max(1, round(g['height'] * scale))))
ink = Image.new('RGBA', mask.size, color)
self.image.paste(ink, (round(pos.x + g['xoffset'] * scale), round(pos.y + g['yoffset'] * scale)), mask)
pos.x += g['xadvance'] * scale
def draw_rectangle_rounded(self, rect, roundness, segments, color):
layer = Image.new('RGBA', self.image.size)
ImageDraw.Draw(layer).rounded_rectangle(rect, radius=6, fill=color)
self.image.alpha_composite(layer)
def draw_circle(self, x, y, radius, color):
ImageDraw.Draw(self.image).ellipse((x-radius, y-radius, x+radius, y+radius), fill=color)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
base = dict(composer='ace', style='Prism', readiness='READY', section='VERSE / CONTINUOUS',
next_section='PRECHORUS', form_labels_are_intent=True, buffered=64)
cases = [{}, base, dict(base, job_inflight=True, generation_elapsed_seconds=23.4, turn_signal_music=True),
dict(base, readiness='DEGRADED', holding_accepted_music=True, job_inflight=True, buffered=18),
dict(base, worker_failed=True, buffered=0),
dict(readiness='READY', style='Stored score', section='ARCHIVED SCORE', compute='none', buffered=92)]
board = Image.new('RGBA', (728, 564), '#080e16')
for index, state in enumerate(cases):
panel = Image.new('RGBA', (364, 188), '#080e16')
draw_panel(Canvas(panel, ROOT / 'selfdrive/assets/fonts/Inter-Medium.fnt'), None, state, 364, 176)
board.paste(panel, ((index % 2) * 364, (index // 2) * 188))
args.output.parent.mkdir(parents=True, exist_ok=True)
board.convert('RGB').save(args.output)
print(args.output)
if __name__ == '__main__':
main()