12 KiB
Changelog — s2s-client (Azurian Vocero)
Todos los cambios relevantes del proyecto están documentados en este archivo. Formato basado en Keep a Changelog.
[2026-09-07]
Añadido — Soporte episode_id (inject de episodio podcast)
Nuevo parámetro query string ?episode_id= junto a ?phoneNumber= que
habilita la carga y la inyección automática de un episodio podcast como
contexto PRIMARIO en el prompt LLM.
URL de ejemplo:
/?phoneNumber=%2B447432477433&episode_id=episode-19569f77-a06e-5ce5-a52f-6299dbd67d03
Archivos modificados:
-
server.py(server.py):- Añadida la constante
PODCAST_EPISODE_HINT(L426-L429) para el hint semántico en el prompt. - Añadida la función
_resolve_episode_context(raw_value)(L475-L504): normaliza la respuesta API (dict/list/str, posible wrapper{"result": …}), la serializa en JSON (tope 24.000 chars) y la envuelve con los marcadores<<<PODCAST_EPISODE_BEGIN/END>>>. - Añadida la función asíncrona
_fetch_podcast_episode(episode_id)(L1010-L1078): GET directo a
https://aquachile.azurianlab.com/api/v1/podcast/episodes/{episode_id}
(timeout 15s, follow redirects, manejo de errores tolerante — nunca rompe el flujo, devuelveNoneen caso de fallo). - Extendida
_augment_instructions_with_profile(...)(L1137-L1181) con el nuevo parámetroepisode_context_text; el orden de inject ahora es (más específico → más genérico): episode → profile → reports → summary. - Extendida
_patch_client_session_update_with_profile(...)(L1184-L1306) con soporte aepisode_context_texty logging dedicado[INJECT-EPISODE]/[DEBUG-EPISODE]. - WebSocket connect handler (L1791-L1959):
- lee
episode_iddesde query params junto aphoneNumber; - ejecuta fetch episodio + resolve ANTES de profile/reports/summary;
- guarda
injected_episode_contexteinject_episode_iden elsession_ctxcompartido.
- lee
_client_to_upstream(L2649-L2661): pasactx_episode_contextal patch desession.update.- Endpoint
POST /api/reasoning:- extendido el modelo
ReasoningRequestcon el campoepisode_id: str | None = None(L1309-L1323); - en el handler (L1411-L1577) fetch episodio independiente de phone;
inject_episode_textse hace prepend sobreuser_context_parts(primero de todos);- reenvío a NODE_RED / OPENAI con logging extendido (
episodeIdLen).
- extendido el modelo
- Añadida la constante
-
main.js(main.js):- Lectura desde query string:
const voceroEpisodeId = pageParams.get("episode_id")(L676-L680). fetchOpenAiReasoningAnswer(L1337-L1364): campoepisode_idañadido al body POST/api/reasoning, logKey yconsole.infoextendidos.- Construcción URL WebSocket (L2323-L2342):
episode_idañadido a query params tanto con la APIURL.searchParamscomo en el fallback string (concat con&).
- Lectura desde query string:
Añadido — S2S_START_SESSION_MODE: 3 modos de inicio de sesión
Variable de entorno leída por el servidor y propagada al cliente vía GET /api/config.startSessionMode.
Objetivo: controlar si y cómo se enciende automáticamente el micrófono y si el modelo saluda
primero (flujo similar al wake-word). Valores soportados:
| Valor | Descripción |
|---|---|
| (vacío) / default | Inicio manual: solo tap sobre el Orb / botón Start |
TALK |
Después de 2s overlay a pantalla completa "Toca en cualquier lugar para empezar" (policy safe para Safari/iOS getUserMedia). Click/touch/tecla dentro de la user gesture → doStart(). El cliente WS inyecta bootstrap "hola vocero" + requestResponse(). |
TALKFIRST |
Después de 2s intenta doStart() DIRECTAMENTE SIN click, precargando pendingWakeWordConfirmationText = "hola vocero" (misma variable de la wake-word real). Si los permisos de mic ya están "allow always" funciona sin click. Si falla (policy Safari/iOS) → fallback sobre el mismo unlocker de TALK con mensaje "permiso micrófono". |
Archivos modificados:
-
server.py(server.py):- Lectura env:
S2S_START_SESSION_MODE = os.environ.get(...),.strip().upper()(L150). - Banner
[STARTUP]extendido con el campostartSessionMode=(L196-L212). - Endpoint
GET /api/config→ nuevo campostartSessionMode+ log[INTERNAL] /api/config(L1383-L1400).
- Lectura env:
-
index.html(index.html):- Añadido nuevo overlay
#talk-unlockera pantalla completa (z-index 9999),hiddenpor defecto, con tarjeta "Preparando Vocero… / Toca en cualquier lugar para empezar a hablar" + spinner degradado.
- Añadido nuevo overlay
-
style.css(style.css):- Clases
.talk-unlocker,.talk-unlocker-hint,.talk-unlocker-spinner,.talk-unlocker-title,.talk-unlocker-sub+ animaciones fade-in y pop-up de tarjeta.
- Clases
-
main.js(main.js):- Variable global
startSessionMode = ""+ comentario (L628-L632). - Parse dentro de
fetchConfig()→ uppercase, log enreportDebug("B",…)(L2309-L2320). - Paso a cliente WS →
startWithResponse: startSessionMode === "TALK"(L2683-L2692). - Bloque scheduling auto-start (L2976-L3083):
bootstrapWithAutoStart():await fetchConfig()→maybeScheduleAutoStart();setTimeout(2000 ms)warmup para finalizar fetch de episode/profile/reports;showTalkUnlocker(fallbackHint): gestiona labels personalizadas en caso de fallback TALKFIRST;tryTalkFirstDirect()(TALKFIRST): setpendingWakeWordConfirmationText="hola vocero"+doStart()+ try/catch con teardown y fallback unlocker;- listener unlocker click/touchstart/keydown →
startTalkFromGesture()concreateResumedAudioContext()dentro de la gesture (para iOS silent-free).
onClientStatus()(L2878-L2932) añadidohideTalkUnlocker()enconnecting/queued/your-turn/connected.
- Variable global
-
ws/s2s-ws-client.js(s2s-ws-client.js):- Constructor: nuevas propiedades
_startWithResponse,_startWithResponseText(default"hola vocero"),_initialResponseSentguardia one-shot (L163-L178). - Handler
session.updated(L688-L713): Solo paraTALK(no TALKFIRST, que en cambio usapendingWakeWordConfirmationText):this.sendUserText("hola vocero") + this.requestResponse()— mismo code path identico de la wake-word. requestResponse/_createResponseNowrestaurados limpios (solo payload opcionalimage; ningún shapedresponse.create).
- Constructor: nuevas propiedades
Modificado — #reports-sidebar-toggle visible solo con ?debug=true
El botón "reports" en la topbar (toggle del panel lateral reports) ahora es
data-debug-only="true" — por lo tanto solo se muestra si la URL contiene
?debug=true, de manera análoga al cluster .topbar-right.
Archivos modificados:
index.html→#reports-sidebar-toggle(index.html): añadidodata-debug-only="true".main.js→applyDebugOnlyVisibilityGate()(L48-L109): extendido para iterar todos los elementos[data-debug-only="true"](no solo.topbar-right); línea 1227:reportsSidebarToggle.hidden = falsecondicionado aqs.get("debug") === "true"(L1227-L1246) + log rojo🚫 FORZADO HIDDENcuando falta debug=true.style.css→ regla de seguridad ya presente:[data-debug-only]:not([data-debug-enabled]) { display:none !important; visibility:hidden; opacity:0 }(L532-L537).
[2026-09-04]
Modificado — Chat siempre visible + integración seamless
Reorganizado el layout para mostrar el chat textual de forma permanente e integrado visualmente con el Orb en la parte superior del stage.
- Eliminado botón toggle chat en la topbar (
#chat-btn) y relativo overlay. - Stage con el Orb en la parte superior; nuevo
#chat-panela continuación (historial- form input fijo sobre el footer sticky).
- Actualizada lógica en
main.jsyui/chat.jspara el envío de texto medianteclient.sendUserTexty auto-scroll sin controles de visibilidad del panel. - Versión banner deploy: 2026-09-03 14:40.
Resuelto — Estabilidad integración chat mobile
- Error
Missing element: #chat-btnresuelto con selectores non-strict (document.querySelector) para elementos opcionales eliminados. - Refactorización estética "seamless": chat sin marcos, sombras ni fondos
(
background:transparent,box-shadow:none). - Layout mobile:
- padding incrementado en
#app(56px) y.chat-history(96px) para evitar superposiciones con el footer sticky; - márgenes fijos en las burbujas:
margin-right:22px(Assistant),margin-left:6px(User).
- padding incrementado en
- Auto-scroll streaming: implementado
_scrollToBottomcon cadena de retry múltiples (requestAnimationFrame+setTimeout) para compensar el line-wrap asíncrono durante la llegada de los tokens;overflow-anchor:none+scroll-behavior:auto !important.
Añadido — Avatar usuario con iniciales + modal de perfil
- Sustituido el texto del nombre de usuario en el header por avatar circular 40×40px (gradient de marca) conteniendo las iniciales calculadas (ej. "AV").
- Nueva
<dialog id="user-profile-dialog">enindex.htmlque se abre al click sobre el avatar: muestra email, teléfono, rol, empresa, con soporte a shapes anidadas del payload de perfil. - Cierre del modal: tecla ESC, click fuera de la tarjeta, botón
×dedicado.
[Notas de formato]
- Tope campo prompt episodio: 24.000 caracteres (transcript largos).
- Tope profile / summary: 1.600 caracteres cada uno.
- Tope reports list: 12.000 caracteres.
- Orden inject prompt LLM (WS session.update + reasoning):
- Episodio podcast actual (PRIMARIO)
- Perfil llamador (identidad)
- Lista reports / scripts podcast programados
- Summary última conversación
- Todos los bloques están marcados con
<<<NOMBRE_BLOQUE_BEGIN/END>>>para inspección de logs.