feat(lumi-web): notas de voz end-to-end + deploy del demo#17
Conversation
… 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>
Reviewer's GuideAdds 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 flowsequenceDiagram
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)
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 4 issues, and left some high level feedback:
- The MIME→file-suffix mapping is duplicated in both
azure_whisper.pyandfaster_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 iffetchfails; it would be safer to guard uses ofr.transcript/r.replybehind 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 usinguv/pipwith a lockfile or constraints derived frompyproject.tomlinstead.
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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| 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 { |
There was a problem hiding this comment.
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.
| 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" |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| > (`/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). |
There was a problem hiding this comment.
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]."
| > (`/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). |
There was a problem hiding this comment.
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
Transcriberport 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.
| 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) |
| api_key = os.environ.get("AZURE_OPENAI_API_KEY") | ||
| if api_key is not None: | ||
| client = AzureOpenAI(api_key=api_key, **kwargs) | ||
| else: |
| ## 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). |
There was a problem hiding this comment.
💡 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".
|
@copilot resolve the merge conflicts in this pull request merge |
Conflictos resueltos: hice merge de |
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
mainresolviendo el conflicto enDockerfile.Changes
Dockerfile,.dockerignore, extras opcionales de voz).origin/maincon resolución quirúrgica del conflicto add/add enDockerfile(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,ruffno disponibles).Related
Actualización de mantenimiento del PR: sincronización con
mainy resolución de conflictos de merge.