Skip to content

feat(lumi-web): notas de voz end-to-end + deploy del demo#17

Merged
R0SEWT merged 18 commits into
mainfrom
feat/lumi-product-foundation
Jun 13, 2026
Merged

feat(lumi-web): notas de voz end-to-end + deploy del demo#17
R0SEWT merged 18 commits into
mainfrom
feat/lumi-product-foundation

Conversation

@R0SEWT

@R0SEWT R0SEWT commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Summary

Implementa soporte end-to-end de notas de voz en el demo web de Lumi (UI + API + puerto de transcripción con adapters), junto con documentación y artefactos de deploy; además incorpora el merge más reciente de main resolviendo el conflicto en Dockerfile.

Changes

  • Añade flujo de notas de voz en el demo web (muestras predefinidas y grabación real).
  • Introduce el puerto de transcripción y adapters (canned, Azure Whisper y faster-whisper local opcional).
  • Extiende la API FastAPI para listar/reproducir notas de voz y subir audio con validaciones básicas.
  • Agrega pruebas para adapters/transcripción y endpoints de voz, incluyendo límites/validaciones.
  • Documenta arquitectura/configuración de voice notes y despliegue del demo en Azure App Service.
  • Agrega artefactos de build/deploy del demo (Dockerfile, .dockerignore, extras opcionales de voz).
  • Merge de origin/main con resolución quirúrgica del conflicto add/add en Dockerfile (manteniendo dependencias runtime necesarias del demo).

Testing

En esta sesión no se re-ejecutaron tests/lint porque el entorno no tenía herramientas instaladas (uv, pytest, ruff no disponibles).

Related

Actualización de mantenimiento del PR: sincronización con main y resolución de conflictos de merge.

R0SEWT-CGIAR and others added 16 commits June 12, 2026 20:28
… patrones

Interfaz web para mostrar Lumi, sobre el ConversationRouter existente:

- Detector determinista de patrones longitudinales (domain/pattern_detection.py):
  empareja exposición(D) → molestia(D+1/D+2), emite REPEATED_AFTER/COINCIDES_WITH/
  MISSING_INFO vía CandidatePattern.build (plantillas aprobadas, sin lenguaje causal).
  Cableado en service.build_clinician_report (antes candidate_patterns=()).
- Bootstrap reutilizable (api/bootstrap.py): build_runtime, load_env (.env),
  build_ai_adapter; DemoClock mutable (adapters/system.py). cli.py refactorizado.
- Conversación natural: texto sin "/" → check-in (IA Azure o nota); surface del
  patrón nuevo en el chat con copia aprobada (router.py, ai_mapping.map_ai_observations).
- Backend FastAPI (api/web.py): /, /api/snapshot, /api/message, /api/reset,
  /api/report. Snapshot ensamblado desde build_clinician_report + export.
- Seed multinoche (api/seed.py): plan v1, adherencia, patrón jabón→molestia,
  remedio no prescrito, evento de seguridad CONTACT_CLINICIAN, transcript.
- SPA de dos columnas (api/static/index.html): chat + panel clínico en vivo.
- Extra opcional `web` (fastapi/uvicorn/python-dotenv) + script lumi-web.

Tests: pattern_detection, seed, web_api, router NL. 86 passed (non-slow).
Verificado en navegador: estado sembrado, semáforo verde→amber en vivo, patrones.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The new tests/lumi/test_web_api.py imports starlette/fastapi from the optional
`web` extra, which CI did not install (only `--extra azure`), failing collection.

- CI now syncs `--extra azure --extra web`.
- test_web_api skips gracefully (importorskip) when the web extra is absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a generated hexagonal architecture diagram and documents how to regenerate
it; expands the README product boundary and hard rules, and links the diagram
from docs/ARCHITECTURE.md.

- scripts/render_architecture.py renders docs/diagrams/lumi_architecture.png via
  mingrammer/diagrams (imports nothing from src/, so no TensorFlow/mic/acoustic
  pulled into the render). Requires Graphviz + the `diagrams` dev dependency.
- Edge legend: solid = implemented today; dashed = accepted production target;
  dotted = cross-cutting identity/secrets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rediseña la columna de chat del demo para que se lea como WhatsApp real:
header de contacto (avatar + estado "en línea" + acciones), burbujas con
cola, doble-check azul, hora flotante que el texto rodea, indicador
"escribiendo…" y lienzo con doodle. Composer redondeado estilo WA.

Ignora artefactos locales (.playwright-mcp/, lumi-wa-*.png) usados para
la verificación visual del demo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(report): render clínico HTML/PDF desde ClinicianReport (WeasyPrint + Jinja2)

Movement 3 / Fase 4: el modelo de reporte ya era puro y completo; faltaba el
render. Se añade adapters/reports/pdf.py con dos etapas testeables por separado:

- render_clinician_report_html: ClinicianReport -> HTML determinista (Jinja2,
  autoescaped). Solo necesita Jinja2 (dep core liviana).
- render_clinician_report_pdf: HTML -> PDF (WeasyPrint, import perezoso tras el
  extra [report]; el core y el render HTML nunca dependen del extra).

Secciones de PRODUCT.md Movement 3: plan activo + versión, evolución, adherencia,
patrones "a validar" (no diagnóstico), no-prescritos en sección neutral aparte,
fotos por fecha, cobertura, disclaimer. Sin lenguaje causal: todo valor dinámico
viene de records ya validados (CandidatePattern.rendered se filtra en build).

Tests: contenido HTML (secciones, separación de no-prescritos, disclaimer, sin
lenguaje causal, determinismo) sin libs de sistema; bytes PDF válidos con el
extra (skip si WeasyPrint no está instalado).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(report): autoescape forzado + test de lenguaje causal efectivo (review)

- pdf.py: select_autoescape sobre un template from_string (sin nombre) dejaba el
  autoescaping OFF (hallazgo de seguridad de Sourcery). Se fuerza autoescape=True.
- test_pdf.py: el assert de lenguaje causal era un no-op para "diagnostic"
  (token inglés) y permitía cualquier ocurrencia. Reescrito: stems causales
  reales ausentes, y el stem "diagn" solo permitido dentro de las dos frases
  de seguridad negadas.
- reports/__init__.py: docstring corregido — el renderer markdown es pure-Python,
  no usa Jinja2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(lumi-hip) (#12)

* feat(lumi): fecha efectiva del evento desde expresiones relativas es (lumi-hip)

El detector de patrones fechaba cada observación/mención con recorded_at (la
fecha del mensaje). Cuando el cuidador dice "anteayer le di la crema", el evento
ocurrió antes, lo que distorsiona los lags de coincidencia/repetición — el
"magic moment". Resuelto de forma determinista, sin LLM:

- domain/relative_dates.py: resolve_relative_date(texto, fecha_ref) -> date|None
  puro stdlib. Cubre hoy/ayer/anteayer (y antier/antes de ayer)/hace N días
  (dígito o palabra)/el <día> pasado. Devuelve None fuera del vocabulario; nunca
  fecha en el futuro. "anoche"/"anteanoche" NO desplazan el día a propósito (una
  nota nocturna es sobre su propia noche), lo que mantiene el seed intacto.
- Observation y TreatmentMention ganan effective_date: date|None = None. Nunca
  pisa provenance, que sigue siendo el registro de auditoría.
- record_checkin/record_treatment_mention fijan effective_date desde el texto,
  relativo a recorded_at.date(). El detector usa effective_date or recorded_at.
- 24 casos deterministas (offline) + 3 de integración.

Decisión: parseo determinista en vez de LLM, alineado con la aceptación del
bead ("sin dependencia de LLM") y la regla dura "model output is an untrusted
proposal; deterministic code validates and persists state". La fecha alimenta
una señal clínica y debe ser reproducible y auditable.

Closes lumi-hip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(lumi): honor effective event dates end to end

* docs(bd): align relative-date design with stdlib

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(eval): harness de seguridad Fase 3 (golden set es-PE, offline + live gated)

El modelo emite una propuesta estructurada NO confiable; el código determinista
(ai_mapping + safety.policy) es lo que sostiene los guardarraíles. Este harness
fija esas garantías contra un golden set en español peruano que cubre los
escenarios de la Fase 3: remedios tradicionales, ansiedad, mensajes incompletos,
fuente de tratamiento ambigua y red flags.

Capa offline (default/CI, sin modelo ni libs de sistema):
- Mapeo de plan: ítems ambiguos/sin fuente nunca entran al plan (van a
  follow-up); no-prescrito se preserva (jamás recordatorios); nada se
  auto-confirma.
- Política red-flag: disposición es función pura de señales tipadas; el modelo
  no puede suprimir ni inventar escalada; gana la regla de mayor rango; casos
  borde no sobre-escalan.
- Lenguaje: plantillas/copys sin lenguaje causal; inyección causal rechazada.
- Test de cobertura contra encogimiento silencioso del set.

Capa live (gated, -m live_model + LUMI_EVAL_LIVE=1): manda el texto crudo al
adapter real y asserta que los MISMOS invariantes sobreviven a la salida del
modelo; nunca fija extracción exacta.

Archivos nuevos solamente (cero conflicto con voz/fechas). 20 passed offline.
EVAL_SET_VERSION = es-PE-v1 (alinea con azure_openai.py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(eval): enforce live golden boundaries

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mi-8ko) (#14)

* feat(voice): transcripción de notas de voz como puerto media→texto (lumi-8ko)

Un puerto Transcriber (audio→texto) cuyo resultado entra al flujo de check-in
existente como input NO confiable, igual que un mensaje escrito — el router no
cambia y no se tocan invariantes de seguridad. Adapter real faster-whisper
(CTranslate2, sin PyTorch) detrás del extra [voice]; adapter canned
determinista para el demo en vivo y los tests. El core nunca importa la dep
pesada (import perezoso en __init__ + boundary test que lo garantiza).

Demo WhatsApp: chips 🎤 de notas de voz que renderizan una burbuja con onda +
duración y la transcripción debajo, y disparan el check-in (con IA activa el
panel clínico reacciona; offline igual queda registrado).

- ports/transcription.py: VoiceClip, Transcript, Transcriber
- adapters/media/{canned,faster_whisper}.py
- api/voice_samples.py + endpoints /api/voice y /api/voice/samples
- pyproject: extra [voice] = faster-whisper
- tests: canned + integración web + boundary (rápidos); adapter real (slow, skip sin el extra)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(voice): Azure OpenAI Whisper como motor real de transcripción (lumi-8ko)

Tercer adapter detrás del puerto Transcriber: AzureWhisperTranscriber
reutiliza el mismo surface v1 y credenciales que el extractor (Entra ID /
managed identity por defecto, AZURE_OPENAI_API_KEY como fallback local).
build_transcriber prioriza Azure -> faster-whisper local ([voice], opt-in
LUMI_VOICE_LOCAL) -> canned no-op, y queda gateado por use_ai para que tests
y demos offline nunca construyan un cliente Azure. Los imports openai/
azure-identity son perezosos (el core nunca los importa).

Separación de paths de voz:
- /api/voice (muestras): el transcript se resuelve directo del fixture, sin
  pasar por el motor -> los chips del demo funcionan en escenario sin importar
  qué engine (si alguno) esté cableado. Arregla la regresión donde las
  muestras salían vacías al cambiar build_transcriber a canned vacío.
- /api/voice/upload (audio real): base64 JSON -> Transcriber (Azure cuando hay
  deployment) -> mismo flujo de check-in no confiable que un mensaje escrito.
  El audio se transcribe en el borde y se descarta; nunca se persiste.

Demo WhatsApp: botón 🎤 de grabación real con MediaRecorder -> /api/voice/upload,
con feature-detect (se oculta si el navegador no graba) y fallback honesto
"configura Azure Whisper" cuando no hay motor.

Tests: adapter Azure con cliente fake (sin red), endpoint de upload con
transcriber inyectado + validación de base64/vacío. .env.example documenta
AZURE_OPENAI_TRANSCRIBE_DEPLOYMENT / LUMI_VOICE_LANGUAGE / LUMI_VOICE_LOCAL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(voice): whisper en Azure usa el path clásico (AzureOpenAI), no /openai/v1

Verificado contra el deployment real: whisper se sirve en el path
deployment-scoped /openai/deployments/{dep}/audio/transcriptions?api-version=...,
NO en el surface /openai/v1 que usa el extractor de chat. El cliente OpenAI v1
daba DeploymentNotFound. El adapter ahora usa el cliente AzureOpenAI con
api-version (default 2024-06-01, AZURE_OPENAI_TRANSCRIBE_API_VERSION para
override) y azure_endpoint (se strippea el sufijo /openai/v1 si viene).
Credencial: api-key o azure_ad_token_provider (Entra ID), igual que antes.

Probado end-to-end con un clip es-PE real: transcripción exacta en 2.4s, y el
flujo /api/voice/upload completo (audio -> whisper -> check-in -> respuesta).
.env.example deja AZURE_OPENAI_TRANSCRIBE_DEPLOYMENT=whisper activo (deployment
creado en rg-team-09) y documenta el api-version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(voice): VOICE_NOTES.md + Dockerfile/.dockerignore para deploy del demo

VOICE_NOTES.md documenta el puerto Transcriber, los 3 adapters (canned / Azure
Whisper / faster-whisper) y su selección en build_transcriber, los endpoints
/api/voice (muestras=fixture) y /api/voice/upload (audio real), el gotcha del
path clásico de whisper (AzureOpenAI, no /openai/v1) y el estado del deploy
(App Service; Container Apps bloqueado por Microsoft.App sin registrar; auth por
API key como app setting, no managed identity, por ser Contributor).

Dockerfile mínimo (solo deps de Lumi [web]+[azure], sin TensorFlow) + .dockerignore
para construir la imagen del demo. Archivos nuevos; no tocan módulos existentes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ndation

# Conflicts:
#	.env.example
#	pyproject.toml
#	src/lumi/api/bootstrap.py
#	src/lumi/api/static/index.html
#	src/lumi/api/web.py
#	tests/lumi/test_boundaries.py
El endpoint decodificaba el base64 completo sin límite -> un payload grande
podía agotar memoria. Se rechaza (413) ANTES de decodificar si el string base64
excede el equivalente a 25 MB (límite de archivo de los motores de transcripción),
con un segundo check sobre los bytes decodificados. Test del 413 añadido.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 13, 2026 18:00
@sourcery-ai

sourcery-ai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds end-to-end voice note support to the Lumi web demo (UI, HTTP API, transcription port + adapters, and tests), wires Azure/OpenAI Whisper and optional faster-whisper as engines behind a new Transcriber port, and documents voice notes plus the Azure App Service demo deploy, including a minimal Docker image setup.

Sequence diagram for POST /api/voice/upload flow

sequenceDiagram
    actor Caregiver
    participant Browser
    participant FastAPIApp as FastAPI_app
    participant DemoRuntime as DemoRuntime_state
    participant Transcriber
    participant AzureOpenAI

    Caregiver->>Browser: click recBtn
    Browser->>Browser: toggleRecording()
    Browser->>Browser: uploadVoice(blob, duration_s)
    Browser->>FastAPIApp: POST /api/voice/upload
    FastAPIApp->>FastAPIApp: voice_upload(VoiceUploadIn)
    FastAPIApp->>FastAPIApp: base64.b64decode(audio_b64)
    FastAPIApp->>DemoRuntime: state.runtime.transcriber
    FastAPIApp->>Transcriber: transcribe(VoiceClip)
    Transcriber->>AzureOpenAI: audio.transcriptions.create(...)
    AzureOpenAI-->>Transcriber: text
    Transcriber-->>FastAPIApp: Transcript
    FastAPIApp->>DemoRuntime: _voice_turn(text, _fmt_duration)
    DemoRuntime-->>FastAPIApp: {transcript, reply, snapshot}
    FastAPIApp-->>Browser: JSON response
    Browser->>Browser: update voiceBubble()
    Browser->>Browser: renderPanel(snapshot)
Loading

File-Level Changes

Change Details Files
Implement voice note UX in the demo web UI, including sample chips and in-browser recording that call new voice APIs.
  • Extend chat HTML/CSS with voice-note specific chips, bubbles, waveform visuals, and recording button states.
  • Add client-side logic to list sample voice notes, send scripted samples via /api/voice, and render transcript plus Lumi reply.
  • Integrate MediaRecorder-based audio capture, base64 upload to /api/voice/upload, and appropriate fallbacks when recording is unsupported or transcription is not configured.
src/lumi/api/static/index.html
Expose server-side HTTP endpoints to serve sample voice notes, replay canned samples, and handle real audio uploads via a Transcriber abstraction.
  • Define new Pydantic models for voice note input and uploads, including size limits and base64 validation for audio payloads.
  • Add helper utilities to format voice durations and to record a transcribed voice turn into the conversation transcript and route it through the normal check-in path.
  • Implement /api/voice/samples, /api/voice, and /api/voice/upload routes using scripted fixtures and the runtime transcriber, including edge validation and empty-transcript behaviour.
  • Wire a Transcriber instance into DemoRuntime and ensure it is available on the app state.
src/lumi/api/web.py
src/lumi/api/bootstrap.py
src/lumi/api/voice_samples.py
Introduce a speech-to-text port and concrete adapters for canned, Azure Whisper, and faster-whisper-based local transcription.
  • Define a Transcriber protocol plus VoiceClip and Transcript dataclasses as the speech-to-text port, keeping dependencies stdlib-only.
  • Add a deterministic CannedTranscriber that resolves transcripts from an id->text map for demos and fast tests.
  • Implement AzureWhisperTranscriber using AzureOpenAI on the classic deployment-scoped audio path, with environment-driven settings and lazy imports for openai/azure-identity.
  • Implement FasterWhisperTranscriber using faster-whisper (CTranslate2), mapping MIME types to file suffixes and running offline transcription via a temp file.
  • Provide a build_transcriber factory that prefers Azure, then optional local faster-whisper, then falls back to canned; gate construction on the use_ai flag so tests/offline demos avoid Azure clients.
src/lumi/ports/transcription.py
src/lumi/adapters/media/__init__.py
src/lumi/adapters/media/canned.py
src/lumi/adapters/media/azure_whisper.py
src/lumi/adapters/media/faster_whisper.py
src/lumi/api/bootstrap.py
Add comprehensive tests around transcription port, adapters, voice APIs, and core boundary guarantees.
  • Test the canned transcriber and demo voice-note fixtures, including id uniqueness and duration formatting.
  • Test AzureWhisperTranscriber behaviour with a fake client: argument wiring, MIME-to-filename mapping, env var parsing, endpoint normalisation, and non-storage of API keys.
  • Test FasterWhisperTranscriber with a slow, optional integration that generates synthetic audio and asserts a well-formed Transcript when faster-whisper is installed.
  • Test the new voice HTTP endpoints end-to-end using TestClient, including sample listing, scripted and uploaded flows, empty-engine behaviour, and base64/size validation errors.
  • Extend the boundary test suite to assert that no heavy voice dependencies (faster_whisper/ctranslate2/torch) are imported at module level anywhere in the lumi package.
tests/lumi/media/test_transcription.py
tests/lumi/media/test_azure_whisper.py
tests/lumi/media/test_faster_whisper_slow.py
tests/lumi/test_voice_api.py
tests/lumi/test_boundaries.py
Document the voice note architecture and the live Azure App Service demo deploy, and add a minimal Docker image and optional voice extra.
  • Add a detailed VOICE_NOTES.md describing the Transcriber port, adapters, Azure Whisper path gotcha, configuration, and testing strategy.
  • Extend README with sections for Voice notes and Live demo (deploy), including Azure CLI snippets to create the Whisper deployment, build/redeploy the container, and tear down resources, plus RBAC-driven design decisions.
  • Introduce a Dockerfile for a slim web demo image that installs only Lumi runtime dependencies ([web]+[azure]) and runs the FastAPI app.
  • Add a [voice] extra in pyproject.toml for faster-whisper and ensure the heavy dep stays out of the core install.
  • Add a .dockerignore and update sample env as part of the deploy/docs setup.
README.md
docs/VOICE_NOTES.md
Dockerfile
pyproject.toml
.dockerignore
.env.example

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues, and left some high level feedback:

  • The MIME→file-suffix mapping is duplicated in both azure_whisper.py and faster_whisper.py; consider centralizing this mapping in a shared helper to keep behavior consistent and avoid drift.
  • In the frontend voice-note handlers (sendVoice / uploadVoice), the response object is assumed to exist even if fetch fails; it would be safer to guard uses of r.transcript/r.reply behind a successful fetch check to avoid runtime errors.
  • The Dockerfile hard-codes Python dependencies rather than installing from pyproject.toml, which can lead to version skew; consider using uv/pip with a lockfile or constraints derived from pyproject.toml instead.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The MIME→file-suffix mapping is duplicated in both `azure_whisper.py` and `faster_whisper.py`; consider centralizing this mapping in a shared helper to keep behavior consistent and avoid drift.
- In the frontend voice-note handlers (`sendVoice` / `uploadVoice`), the response object is assumed to exist even if `fetch` fails; it would be safer to guard uses of `r.transcript`/`r.reply` behind a successful fetch check to avoid runtime errors.
- The Dockerfile hard-codes Python dependencies rather than installing from `pyproject.toml`, which can lead to version skew; consider using `uv`/`pip` with a lockfile or constraints derived from `pyproject.toml` instead.

## Individual Comments

### Comment 1
<location path="src/lumi/api/static/index.html" line_range="378-387" />
<code_context>
+    `<span class="meta">${fmtTime(when)} <span class="ticks">✓✓</span></span>`;
+  return b;
+}
+async function sendVoice(id, duration) {
+  const stream = $("stream");
+  const vb = voiceBubble(duration, new Date());
+  stream.appendChild(vb);
+  stream.scrollTop = stream.scrollHeight;
+  const typing = typingBubble();
+  stream.appendChild(typing);
+  stream.scrollTop = stream.scrollHeight;
+  let r;
+  try {
+    r = await fetch("/api/voice", {
+      method: "POST", headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({ id }),
+    }).then((x) => x.json());
+  } finally {
+    typing.remove();
+  }
+  vb.querySelector(".vtrans").innerHTML =
+    `<b>📝 transcripción</b>${escapeHtml(r.transcript || "—")}`;
+  stream.appendChild(bubble({ role: "lumi", text: r.reply }, new Date()));
</code_context>
<issue_to_address>
**issue:** Guard against failed /api/voice requests before dereferencing the response object.

If `fetch("/api/voice")` fails, `r` stays `undefined` and the `vb.querySelector(".vtrans").innerHTML = ... r.transcript ...` line in `finally` will throw, breaking the UI after a failed request.

Consider aligning with `uploadVoice` by initializing `r` to `null`, handling errors in the `catch` (e.g. set an error message in `.vtrans` and return), and only dereferencing `r.transcript` after the `try/catch/finally`:

```js
let r = null;
try {
  r = await fetch(...).then((x) => x.json());
} catch (e) {
  vb.querySelector(".vtrans").innerHTML =
    `<b>📝</b> <i>no se pudo transcribir</i>`;
  return;
} finally {
  typing.remove();
}

const t = (r && r.transcript) || "";
// ...
```

This avoids throwing on failure and lets the bubble show a graceful error message.
</issue_to_address>

### Comment 2
<location path="src/lumi/adapters/media/azure_whisper.py" line_range="43-52" />
<code_context>
+    use_api_key: bool = False
</code_context>
<issue_to_address>
**nitpick:** Remove or use the unused `use_api_key` field to avoid configuration confusion.

`use_api_key` is never read; the logic only checks `AZURE_OPENAI_API_KEY`. This is misleading because toggling `use_api_key` has no effect. Please either remove the field or wire it into `AzureWhisperTranscriber.__init__` (e.g., to force Entra ID even when an API key is present) so the configuration accurately reflects behavior.
</issue_to_address>

### Comment 3
<location path="tests/lumi/test_voice_api.py" line_range="83-90" />
<code_context>
+    assert client.post("/api/voice/upload", json={"audio_b64": ""}).status_code == 422
+
+
+def test_upload_rejects_oversize_audio(client):
+    # An over-limit base64 string is rejected (413) BEFORE it is decoded, so a
+    # huge payload can't exhaust memory at the decode step.
+    from lumi.api.web import _MAX_AUDIO_B64_LEN
+
+    oversize = "A" * (_MAX_AUDIO_B64_LEN + 4)
+    r = client.post("/api/voice/upload", json={"audio_b64": oversize})
+    assert r.status_code == 413
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test for the oversize-audio path *after* base64 decoding

There’s also a decoded-size guard in `/api/voice/upload` (`len(data) > _MAX_AUDIO_BYTES`) that we don’t currently exercise. Please add a test that sends a base64 string shorter than `_MAX_AUDIO_B64_LEN` but which decodes to more than `_MAX_AUDIO_BYTES`, and assert a 413 response to cover that branch.
</issue_to_address>

### Comment 4
<location path="README.md" line_range="95-98" />
<code_context>
+> ⚠️ Whisper on Azure is served on the **classic deployment-scoped path**
+> (`/openai/deployments/{deployment}/audio/transcriptions?api-version=…`), **not**
+> the `/openai/v1` surface the chat extractor uses — so its adapter drives the
+> `AzureOpenAI` client. Full details, config and the deploy gotcha in
+> [`docs/VOICE_NOTES.md`](docs/VOICE_NOTES.md).
+
</code_context>
<issue_to_address>
**nitpick (typo):** Sentence seems grammatically incomplete; consider adding a verb for clarity.

"Full details, config and the deploy gotcha in [`docs/VOICE_NOTES.md`]" reads incomplete without a verb. Consider wording like "Full details, config, and the deploy gotcha are in [`docs/VOICE_NOTES.md`]."

```suggestion
> (`/openai/deployments/{deployment}/audio/transcriptions?api-version=…`), **not**
> the `/openai/v1` surface the chat extractor uses — so its adapter drives the
> `AzureOpenAI` client. Full details, config, and the deploy gotcha are in
> [`docs/VOICE_NOTES.md`](docs/VOICE_NOTES.md).
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +378 to +387
async function sendVoice(id, duration) {
const stream = $("stream");
const vb = voiceBubble(duration, new Date());
stream.appendChild(vb);
stream.scrollTop = stream.scrollHeight;
const typing = typingBubble();
stream.appendChild(typing);
stream.scrollTop = stream.scrollHeight;
let r;
try {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Guard against failed /api/voice requests before dereferencing the response object.

If fetch("/api/voice") fails, r stays undefined and the vb.querySelector(".vtrans").innerHTML = ... r.transcript ... line in finally will throw, breaking the UI after a failed request.

Consider aligning with uploadVoice by initializing r to null, handling errors in the catch (e.g. set an error message in .vtrans and return), and only dereferencing r.transcript after the try/catch/finally:

let r = null;
try {
  r = await fetch(...).then((x) => x.json());
} catch (e) {
  vb.querySelector(".vtrans").innerHTML =
    `<b>📝</b> <i>no se pudo transcribir</i>`;
  return;
} finally {
  typing.remove();
}

const t = (r && r.transcript) || "";
// ...

This avoids throwing on failure and lets the bubble show a graceful error message.

Comment on lines +43 to +52
use_api_key: bool = False
language: str = "es"
api_version: str = "2024-06-01"
timeout_seconds: float = 30.0
max_retries: int = 2

@classmethod
def from_env(cls) -> "AzureWhisperSettings":
endpoint = os.environ.get("AZURE_AI_ENDPOINT") or os.environ.get(
"AZURE_OPENAI_ENDPOINT"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Remove or use the unused use_api_key field to avoid configuration confusion.

use_api_key is never read; the logic only checks AZURE_OPENAI_API_KEY. This is misleading because toggling use_api_key has no effect. Please either remove the field or wire it into AzureWhisperTranscriber.__init__ (e.g., to force Entra ID even when an API key is present) so the configuration accurately reflects behavior.

Comment on lines +83 to +90
def test_upload_rejects_oversize_audio(client):
# An over-limit base64 string is rejected (413) BEFORE it is decoded, so a
# huge payload can't exhaust memory at the decode step.
from lumi.api.web import _MAX_AUDIO_B64_LEN

oversize = "A" * (_MAX_AUDIO_B64_LEN + 4)
r = client.post("/api/voice/upload", json={"audio_b64": oversize})
assert r.status_code == 413

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a test for the oversize-audio path after base64 decoding

There’s also a decoded-size guard in /api/voice/upload (len(data) > _MAX_AUDIO_BYTES) that we don’t currently exercise. Please add a test that sends a base64 string shorter than _MAX_AUDIO_B64_LEN but which decodes to more than _MAX_AUDIO_BYTES, and assert a 413 response to cover that branch.

Comment thread README.md
Comment on lines +95 to +98
> (`/openai/deployments/{deployment}/audio/transcriptions?api-version=…`), **not**
> the `/openai/v1` surface the chat extractor uses — so its adapter drives the
> `AzureOpenAI` client. Full details, config and the deploy gotcha in
> [`docs/VOICE_NOTES.md`](docs/VOICE_NOTES.md).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (typo): Sentence seems grammatically incomplete; consider adding a verb for clarity.

"Full details, config and the deploy gotcha in [docs/VOICE_NOTES.md]" reads incomplete without a verb. Consider wording like "Full details, config, and the deploy gotcha are in [docs/VOICE_NOTES.md]."

Suggested change
> (`/openai/deployments/{deployment}/audio/transcriptions?api-version=…`), **not**
> the `/openai/v1` surface the chat extractor uses — so its adapter drives the
> `AzureOpenAI` client. Full details, config and the deploy gotcha in
> [`docs/VOICE_NOTES.md`](docs/VOICE_NOTES.md).
> (`/openai/deployments/{deployment}/audio/transcriptions?api-version=…`), **not**
> the `/openai/v1` surface the chat extractor uses — so its adapter drives the
> `AzureOpenAI` client. Full details, config, and the deploy gotcha are in
> [`docs/VOICE_NOTES.md`](docs/VOICE_NOTES.md).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds end-to-end voice note support to the Lumi demo (HTTP endpoints + web UI), introduces a speech-to-text port with multiple adapters (canned/demo, Azure Whisper, faster-whisper), and documents voice notes plus an Azure App Service deployment path.

Changes:

  • Add a Transcriber port with adapters for Azure OpenAI Whisper, local faster-whisper, and a canned/demo transcriber.
  • Extend the FastAPI demo with voice-note endpoints and update the static demo UI to send scripted and recorded voice notes.
  • Add tests, docs (VOICE_NOTES.md), and deployment artifacts (Dockerfile, env example, .dockerignore).

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/lumi/test_voice_api.py Adds API-level tests for voice sample listing, scripted voice routing, upload validation, and no-engine behavior.
tests/lumi/test_boundaries.py Adds an AST-based boundary test to ensure heavy voice deps aren’t imported at module level.
tests/lumi/media/test_transcription.py Tests the transcription port types and canned/demo transcript behavior.
tests/lumi/media/test_faster_whisper_slow.py Adds a slow integration smoke test for the faster-whisper adapter.
tests/lumi/media/test_azure_whisper.py Adds a fake-client unit test suite for the Azure Whisper adapter and env settings.
src/lumi/ports/transcription.py Introduces VoiceClip, Transcript, and the Transcriber protocol as the STT port.
src/lumi/api/web.py Adds /api/voice/samples, /api/voice, and /api/voice/upload plus payload bounds and decoding/validation.
src/lumi/api/voice_samples.py Adds scripted demo voice-note fixtures and a transcript map.
src/lumi/api/static/index.html Updates the demo UI to show voice-note chips and record/upload audio via MediaRecorder.
src/lumi/api/bootstrap.py Wires a new build_transcriber() into the demo runtime with Azure→local→no-op selection.
src/lumi/adapters/media/faster_whisper.py Adds a local faster-whisper adapter with deferred import and temp-file transcription.
src/lumi/adapters/media/canned.py Adds a deterministic canned transcriber for demo/tests.
src/lumi/adapters/media/azure_whisper.py Adds an Azure OpenAI Whisper adapter and settings loader with endpoint normalization.
src/lumi/adapters/media/init.py Adds a package marker/docstring for media adapters.
README.md Documents voice notes and the live demo deployment commands/decisions.
pyproject.toml Adds the optional [voice] extra for faster-whisper.
docs/VOICE_NOTES.md Adds detailed architecture/config docs for voice notes and Azure Whisper “classic path” gotcha.
Dockerfile Adds a minimal container image for running the Lumi web demo with only web + azure deps.
.env.example Adds env vars for enabling transcription deployments and language selection.
.dockerignore Restricts Docker build context to keep the demo image build small.
.beads/issues.jsonl Updates the project’s issue tracking data file with new entries.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/lumi/api/web.py
if len(data) > _MAX_AUDIO_BYTES:
raise HTTPException(status_code=413, detail="audio demasiado grande")
with state.lock:
clip = VoiceClip(data=data, mime=body.mime, duration_s=body.duration_s)
Comment on lines +97 to +100
api_key = os.environ.get("AZURE_OPENAI_API_KEY")
if api_key is not None:
client = AzureOpenAI(api_key=api_key, **kwargs)
else:
Comment thread README.md
Comment on lines +84 to +92
## Voice notes

A voice note is just another way to author a check-in: audio is transcribed **at
the edge** and the resulting text flows into the *same* untrusted extraction /
check-in path as a typed message — the conversation core never sees audio. Behind
the `Transcriber` port sit three swappable adapters: a deterministic canned one
(demo/tests), **Azure OpenAI Whisper** (the real engine), and a local
faster-whisper fallback (`[voice]` extra). The demo exposes `/api/voice` (scripted
samples) and `/api/voice/upload` (real recorded audio).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 250b104678

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .dockerignore
Comment thread src/lumi/adapters/media/azure_whisper.py
@R0SEWT

R0SEWT commented Jun 13, 2026

Copy link
Copy Markdown
Owner Author

@copilot resolve the merge conflicts in this pull request merge

Copilot AI commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request merge

Conflictos resueltos: hice merge de origin/main en esta rama y resolví el conflicto en Dockerfile en el commit 9c4030c.

Copilot AI changed the title docs(readme): notas de voz + deploy del demo feat(lumi-web): notas de voz end-to-end + deploy del demo Jun 13, 2026
@R0SEWT
R0SEWT merged commit 6803ccd into main Jun 13, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants