Files
vocero-client/s2s-client/ARCHITECTURE.md
T

14 KiB

Demo S2S — schema di funzionamento

Documento orientativo del modulo demo/ (UI + proxy S2S + reasoning). Non sostituisce il codice: serve a trovare il proprio posto quando si apre il progetto.


1. Cosa c'è in demo/

Il modulo fa tre cose contemporaneamente:

Ruolo Cosa fa Entry point
HTTP server statico + API Serve l'UI, offre endpoint REST, gira un WebSocket proxy server.py
Voice UI (browser) Orba, mic, transcriptioni, audio playback, pannelli index.html + main.js
Reasoning hook Inoltra transcript utente a un motore di ragionamento esterno (NODE_RED o OpenAI) server.py /api/reasoning

La configurazione (URL backend S2S, tipo di reasoning, chiavi API) viene presa dal file .env nella root del progetto.


2. Architettura in un diagramma

                 ┌──────────────────────────────────────────────────┐
                 │                 BROWSER (demo UI)                │
                 │  [index.html] → [main.js] (ESM type=module)      │
                 │       │                      ▲                   │
                 │       │ audio + WS events    │ transcript chunks │
                 │       ▼                      │                   │
                 │  ┌──────────────────────────────────────────┐   │
                 │  │  s2s-ws-client.js  →  WebSocket /v1/realtime │  │
                 │  │   + codec.js + orb-visualizer.js          │   │
                 │  └──────────────────────────────────────────┘   │
                 └─────────────────────────┬────────────────────────┘
                                           │  WebSocket /v1/realtime
                                           ▼
                   ┌───────────────────────────────────────────────┐
                   │              server.py  (FastAPI)              │
                   │                                               │
                   │  ┌──────────────────────────────────────┐     │
                   │  │  realtime_proxy  ← TUNNEL → upstream │     │
                   │  │  (WS relay puro, copia byte-per-byte)│     │
                   │  │  _client_to_upstream ─┐              │     │
                   │  │  _upstream_to_client ─┤ session_ctx  │     │
                   │  │  session_ctx{user,id} │ shared dict  │     │
                   │  └───────────────────────┴──────────────┘     │
                   │                                               │
                   │   endpoint REST:                             │
                   │     GET  /                      → index.html  │
                   │     GET  /api/config            → env flags   │
                   │     POST /api/reasoning         → NODE_RED /  │
                   │                                            OPENAI  │
                   │     POST /api/auth/*            → Aquachile   │
                   │                                               │
                   └──────┬──────────────────────────────┬─────────┘
                          │ POST /api/reasoning          │ fire-and-forget
                          ▼                              ▼
              ┌──────────────────────┐     ┌───────────────────────────────┐
              │  Node-RED (port 1881)│     │ Node-RED (port 1881)          │
              │  POST /api/reasoning │     │ POST /api/log/system-response │
              │  log user transcript │     │ log assistant TTS + pairing   │
              └──────────────────────┘     └───────────────────────────────┘
                                           ^
                                           │ forward 4 eventi realtime:
                                           │  audio_transcript.done
                                           │  content_part.done
                                           │  output_item.done role=assistant
                                           │  conversation.item.created role=assistant

📝 Nota importante sul flusso AUDIO con l'attuale .env

Con .env attuale che ha S2S_PROXY_TARGET=ws://ai-server:12345/v1/realtime:

✅ Sì, TUTTO l'AUDIO utente (PCM16 16kHz mono, eventi input_audio_buffer.append) arriva a ws://ai-server:12345/v1/realtime — ma non direttamente: passa SEMPRE per il tunnel 1:1 WebSocket di server.py (_client_to_upstream), che copia byte-per-byte senza modifiche.

✅ Allo stesso modo l'AUDIO risposta (TTS, response.output_audio.delta) ritorna da ai-server:12345 → tunnel _upstream_to_client → browser → playback nativo.

❌ Cosa invece NON va a ai-server:12345 (canale HTTP separato parallelo, non tocca l'audio):

  • Il transcript STT viene mandato via HTTP a http://localhost:1881/api/reasoning (Node-RED, hook LOG_ONLY oggi)
  • Il testo risposta TTS viene mandato via HTTP a http://localhost:1881/api/log/system-response (Node-RED, forward fire-and-forget)
  • Chiamate auth / profilo Aquachile verso aquachile.azurianlab.com

Questi 3 flussi REST sono aggiuntivi e non alterano minimamente il tunnel audio: browser ↔ server.py ↔ ai-server:12345 rimane il percorso esclusivo per voce e risposta.


3. Cose importanti di server.py

File → ruoli

  • / → StaticFast StaticFiles su demo/ (index.html + assets)
  • @app.middleware http no_cache_static_assets → Cache-Control: no-store per .js/.html/.css/.mjs/.wasm per sconfiggere cache browser durante sviluppo
  • @app.get /api/config → manda al browser variabili env (S2S_REASONING_TYPE, S2S_PROXY_TARGET, auth, features). Il client decida da sé cosa mostrare in UI.
  • @app.post /api/reasoning (L658-L834) → cuore del reasoning. Due modalità, scelta da env S2S_REASONING_TYPE:
    • Modalità A — NODE_RED: inoltra POST {NODE_RED_URL}/api/reasoning timeout 30s. Accetta log_only=true + answer="" (Fase 1).
    • Modalità B — OPENAI: chiamata diretta POST $OPENAI_BASE_URL/chat/completions (Fasi successive).
  • @app.websocket /v1/realtime (L874-L980) → tunnel S2S nativo. Non traduce messaggi: copia + registra + forwarda. 2 task paralleli:
    • _client_to_upstream(ws, upstream, client_ip, session_ctx): browser → AI server
    • _upstream_to_client(ws, upstream, client_ip, session_ctx): AI server → browser
    • session_ctx = {last_user_transcript, last_item_id} dizionario mutabile condiviso per accoppiare input↔output
  • async def _forward_agent_text_to_nodered (L837-L873) → helper fire-and-forget. Tutti errori → logger.debug (non spezza il tunnel WS).
  • _upstream_to_client hook eventi (L1305-L1500): per ogni evento output agente avvia asyncio.create_task(_forward_agent_text_to_nodered(...)) → non blocca lo streaming audio.
  • Sidecar Python locali: auth.py (Aquachile profile+user+phone), limiter.py (rate limiter).

Ordine di chiamata in un turno vocale

  1. Browser clicca orb → ws /v1/realtime → realtime_proxy() accetta
  2. Browser manda session.update → input_audio_buffer.append in loop
  3. Upstream genera conversation.item.input_audio_transcription.completed → server cattura → session_ctx["last_user_transcript"] = transcript
  4. _upstream_to_client vede transcript STT completo → server invoca in parallelo POST /api/reasoning con quel transcript
  5. Upstream manda response.output_audio.delta audio chunks → tunnelati diretti al browser → playback nativo
  6. Upstream manda response.output_audio_transcript.done → server cattura → create_task(_forward_agent_text_to_nodered({tts, source_event, user_transcript, …})) → Node-RED SYSTEM_RESPONSE log
  7. Audio finisce → turno chiuso

4. Cose importanti del lato client (main.js)

Moduli ESM importati (tutti con ?v=VERSIONE per cache-bust)

main.js
 ├─ dom.js              → helper DOM (create, qs, bubble, ecc.)
 ├─ chat.js             → pannello messaggi + appendAssistantText
 ├─ account.js          → pannello auth + profilo utente
 └─ ws/s2s-ws-client.js
      ├─ codec.js               → codec pcm + base64
      └─ orb-visualizer.js      → animazione orba su eventi volume/state

Funzioni chiave

Funzione Ruolo
fetchOpenAiReasoningAnswer(transcript) POST /api/reasoning, ritorna {answer, log_only, raw}
maybeRunOpenAiReasoning(transcript, itemId) guardia "ho già processato questo turn?" (processedReasoningTurns). Se result.log_only === true → early return, zero side-effect. Altrimenti branch standard (Fasi successive).
cancelActiveResponse() Fasi successive: ferma risposta corrente prima di inject custom
appendAssistantText(answer) / sendUserText(renderMarker)+requestResponse Fasi successive: sostituisce testo risposta backend nativo
speechSynthesis.speak(utterance) Fasi successive: TTS browser per answer custom, invece del backend nativo

Regole per non rompere Fase 1

  • Se S2S_REASONING_TYPE=NODE_RED e Node-RED torna log_only=true → il client non tocca nulla. Zero cancelActiveResponse, zero speechSynthesis.speak, zero conversation.item.create manuali. La voce è quella del backend S2S nativo.
  • Cache busting triplo: index.html meta tag → Cache-Control no-store middleware → query-string ?v=20260825003 su ogni import.

5. Node-RED (Fase 1 — LOG_ONLY)

Risiede in s2s-reasoning-engine/flows.json. Espone 2 endpoint completamente indipendenti:

Endpoint Payload Output Node-RED
POST /api/reasoning {transcript, language, phoneNumber, …} [S2S NODE_RED LOG_ONLY] ts=… lang=… phone=… transcript="..."
POST /api/log/system-response {tts, source_event, user_transcript, role, item_id, …} [S2S NODE_RED SYSTEM_RESPONSE] ts=… phone=… user="..." tts="..."

Entrambi ritornano 200 {log_only:true, intent:"..."}.


6. Worklets (non toccare)

Nota: i worklets vengono caricati via AudioWorklet.addModule(), che usa script-context non-ESM. Non aggiungere export function a questi file: rompono il caricamento.


7. Variabili ambiente che comandano tutto

Nome Valore tipico oggi Cosa fa
S2S_REASONING_TYPE NODE_RED (Fase 1) / OPENAI (Fasi succ.) / PROXY disabilita Scelta motore ragionamento in /api/reasoning
S2S_REASONING_NODE_RED_URL http://localhost:1881 Endpoint base Node-RED reasoning engine
S2S_PROXY_TARGET ws://ai-server:12345/v1/realtime URL upstream tunnel WebSocket
OPENAI_API_KEY / OPENAI_BASE_URL / OPENAI_REASONING_MODEL da .env Usati in Modalità B OPENAI
S2S_AGENT_PROFILE / S2S_USER_PHONE_AUTH_URL / S2S_TOOL_EXECUTION_MCP_URL aquachile.azurianlab.com/api/v1/… URL provider esterni Aquachile
LOG_LEVEL INFO / DEBUG Livello uvicorn

8. Dove toccare per…

Obiettivo File da aprire
Cambiare colori tema orb / UI style.css :root + DESIGN.md
Aggiungere un pannello in UI index.html markup + main.js render + nuovo modulo in ui/
Aggiungere un endpoint REST server.py, dopo le route esistenti
Nuova logica Node-RED Fase 2 flows.json → Node-RED editor → export
Passare da LOG_ONLY a produzione di answer Flip in flows.json log_only=false + riempire answer, il branch result.log_only del client si spegne automaticamente
Debugging SyntaxError browser Unexpected token 'export' 1) Controlla syntax AST server.py (se crasha → 500 → browser riceve HTML invece di main.js) 2) Hard reload DevTools ✓ Disable cache 3) node --check main.js e import