kb://products/persona-radio-bridgestable2026-05-22

Persona ↔ Radio integration — sentence-level music bridge

personaradiointegrationbridgewebsocket

Persona ↔ Radio integration

End-to-end BGM (background-music) channel selection for a streamer's agent. The streamer picks a radio channel in Hub → persona-server spawns a per-agent WebSocket bridge to radio.blal.pronow_playing frames land on the PersonaEvent bus → the live page renders «currently playing …» without ever opening a second WS to radio.

Shipped 2026-05-22 in three coordinated commits across three repos.

The full loop

[Hub UI]                      [Persona server]                [Radio service]
                                                              radio.blal.pro
streamer picks
"Lo-fi 24/7"   ──PATCH──►   agent_overrides handler
                                bgm_source = "radio:lo-fi-24-7"
                                    │
                                    ▼
                            reconcile_bridge():
                              spawn tokio task
                                    │
                                    ▼
                            connect_async  ─────WSS─────►   /v1/radio/channels/
                              (1s→30s backoff)               lo-fi-24-7/subscribe
                                    │
                                    │◄─── now_playing frame ───
                                    ▼
                          PersonaEvent::Custom {
                            topic: "radio.now_playing",
                            payload: { title, artist, audio_url, ... }
                          }
                                    │
                                    ▼  (existing live-page SSE/WS
                                       subscribes once per room)
                          ▼
[Live page]
   "🎶 Amber Lo-fi · OpenAlice CDN"

What lives where

LayerRepoModuleOwns
Music distributionopenalice-radiosrc/handlers/ws.rs, src/seed.rs4 shared channels (lo-fi-24-7, synthwave-24-7, jazz-cafe, ambient-spacy) + 1Hz scheduler + WS fan-out + tracks CRUD + Lyria 3 share pipeline (planned). Endpoint: wss://radio.blal.pro/v1/radio/channels/:slug/subscribe
Music originopenalice-cdnassets/audio/bgm/*.mp3CC0 public-domain placeholders bundled from Free Music Archive (HoliznaCC0 etc.) — see ATTRIBUTIONS.md. Future: Lyria-generated own tracks
Bridge engineopenalice-personaserver/src/radio_bridge.rsconnect_async + JSON frame parse + exponential-backoff reconnect (1s/2s/5s/10s/30s cap) + Drop-aborts-task BridgeHandle. Gated behind ws-bridge cargo feature, default on
Bridge wiringopenalice-personaserver/src/agent_overrides.rs, server/src/state.rs, server/src/boot.rsAppState.bridge_registry: Arc<Mutex<HashMap<(Uuid, Uuid), BridgeHandle>>>. PATCH transitions spawn/drop bridges. Startup-restore re-spawns existing bridges so a restart doesn't drop live subscriptions
Picker UIopenalice-appsrc/app/account/agents/[id]/settings/page.tsx5-card grid (Тишина + 4 channels) + live now-playing strip + roadmap placeholder + PATCH to persona. Alice voice copy throughout

Wire shape

WS frame received from radio:

{
  "channel_id": "...",
  "track_id": "...",
  "title": "Amber Lo-fi",
  "artist": "OpenAlice CDN",
  "audio_url": "https://cdn.blal.pro/audio/bgm/lofi-amber.mp3",
  "duration_ms": 180000,
  "started_at": "2026-05-22T10:39:10.322812+00:00",
  "position_ms_at_start": 0
}

Re-emitted on PersonaEvent bus under topic radio.now_playing — exact payload preserved, so the live-page consumer (existing SSE subscriber on the agent's default room) reads it without needing to parse a second protocol.

Format conventions

  • bgm_source column on agent_overrides: NULL (silent) or "radio:<slug>" (regex radio:[a-z0-9-]+). Invalid slug → HTTP 400.
  • Future extensions reserved: "file:<url>" (custom upload), "lyria:<id>" (in-room Lyria generation).
  • Slug formats live in radio's seed.rs — current canonical four: lo-fi-24-7, synthwave-24-7, jazz-cafe, ambient-spacy.

Non-trivial design decisions

  • Why bridge per (tenant, agent) and not per channel? A single agent's bgm_source is the unit of intent. Two agents picking the same channel could share a WS connection, but the bookkeeping savings don't justify the lifecycle complexity. Per-agent bridges keep abort semantics trivial and isolate failure modes.
  • Why startup-restore? Persona-server restarts (deploy, OOM, manual) shouldn't drop live radio subscriptions. boot::spawn_radio_bridge_restore scans agent_overrides with non-null bgm_source and re-spawns bridges before the HTTP listener opens.
  • Why exponential backoff to 30s? Radio service uptime is high but not perfect; bare reconnect would spam logs on outage. 30s cap means ~120 retries/hour worst case, which is light.
  • Why not let live page subscribe to radio directly? Two reasons: (1) the live page already subscribes once per room to PersonaEvent — adding a second WS doubles handshake + connection-state surface for no benefit; (2) the persona side may want to react to track changes (announce on chat, emote, etc.) — easier when frames flow through its event bus.

Sentence-level vs frame-level emotion (relevant context for TTS)

Music streaming is frame-on-change, not chunked. Radio emits one now_playing frame per track transition; persona forwards immediately. This is the inverse of TTS streaming (where sentence-level chunks are the correct grain — see radio_bridge.rs comments and the TTS chunker in tts_stream.rs). Music = whole-frame events; voice = sentence chunks. Different rhythms, both correct for their domain.

Verification

End-to-end verified 2026-05-22 10:39 UTC: DB UPDATE → restart → boot restore → connect_async → connected slug=lo-fi-24-7 → 180 s "Amber Lo-fi" track snapshot returned by /v1/radio/channels/lo-fi-24-7/ now-playing. 558 persona-server tests + 674 hub tests green; zero regressions. Bridge feature is default = ["ws-bridge"] — no env flag needed.

Open / future

  • Hub UI v2: custom track upload (bgm_source = "file:<url>")
  • In-room Lyria 3 generation (bgm_source = "lyria:<prompt-id>"), draws from openalice-radio's already-built Lyria-share pipeline
  • Cross-agent channel sharing (one bridge → many agents' rooms)
  • Persona reacting in-chat to track changes («ой, я люблю эту»)