Transcripciones (/anfibios)
Herramienta solo-superadmin para subir audios de llamadas, transcribirlos
automáticamente con Deepgram (pre-recorded + diarización) y promover los
transcripts útiles a la memoria del copiloto (flywheel memory_embeddings).
Vive en /anfibios/transcriptions, junto a Prompts y Ajustes.
El procesamiento usa una cola RabbitMQ que corre localmente (docker-compose). En
producción no hay RABBITMQ_URL, así que la subida de audios responde 503 — el
módulo está pensado para armar dataset de entrenamiento desde la máquina del dev, no
para producción. El resto de Aura funciona igual sin RabbitMQ.
Flujo end-to-end
-
Subida: se aceptan audios sueltos (varios a la vez) o un archivo comprimido
.tar/.tar.gz/.zipcon audios adentro — el backend lo descomprime en memoria (archive.util.ts), filtra los audios y trata cada uno como una subida individual. Por cada audio inserta una filapendingy publica un mensaje a RabbitMQ con los bytes en el cuerpo. El audio no se guarda en disco ni en object storage. Responde al toque. -
Transcripción: el consumer procesa los jobs (
prefetch= concurrencia, default 1 = uno a la vez). Llama a Deepgramlisten.v1.media.transcribeFileconpunctuate,smart_format,utterances, keyterm/keywords boosting del vocabulario del dominio, y separación de hablantes (ver abajo). Descarta el buffer. En error transitorio reintenta (hastaTRANSCRIPTION_MAX_ATTEMPTS) y luego manda el job a la DLQaura.transcriptions.dlq. -
Separación de hablantes (híbrida):
- Si el audio es estéreo (una pata por canal), usa
multichannel→ separación acústica perfecta (canal = hablante). - Si es mono y Deepgram no logra diarizar (
diarizedevuelve un solo speaker, habitual en audio telefónico a 8kHz), cae a atribución por LLM: gpt-4o-mini clasifica cada segmento ya transcripto comooperador/clientepor contexto. La segmentación y los timestamps siguen siendo de Deepgram (precisos); el LLM solo pone la etiqueta de rol.
- Si el audio es estéreo (una pata por canal), usa
-
Análisis de valor: el LLM (
LLM_ADAPTER) clasifica si la llamada es material útil de entrenamiento — una consulta real atendida — vs basura (contestador, corte, número equivocado). Devuelve{util, motivo, tipo_consulta}, que se guardan en la fila. Atajo barato: transcripts de menos de 10 palabras se marcanutil=falsesin llamar al LLM. -
Revisión: el front hace polling mientras haya trabajo en curso; muestra el análisis de valor y el transcript por hablante (rol si fue atribuido por LLM, o "Hablante N" si fue acústico). Los hablantes se pueden corregir a mano.
-
Promoción: al abrir el diálogo, el LLM detecta todas las consultas distintas de la llamada y redacta una lección por cada una (
consulta → respuesta correcta) — endpointGET /:id/suggest-lessons. El superadmin revisa/edita/borra/agrega y confirma; cada lección se guarda como una fila enmemory_embeddingsvíaLearningService.learnFromLesson. Después aparecen vía RAG en próximas llamadas.Por qué varias filas y no una: el RAG matchea por similitud contra el embedding del
customerComplaint. Una fila por consulta puntual matchea mucho mejor que una fila "gigante" con varios temas mezclados (embedding borroso).promotedLessonIdguarda el CSV de ids generados (sirve de marcador y de conteo en la UI).Forma de los campos: la
consultase redacta en primera persona, como la diría el cliente (no en tercera persona ni resumen), porque es lo que se compara contra la frase real del cliente en vivo — mismo estilo que el flywheel de feedback. Larespuestase redacta accionable (guía para el copiloto), ya que se inyecta como contexto y no se embebe.category = 'feedback_approved': las lecciones se guardan con esa categoría, que NO es una etiqueta libre sino un campo de control que leeproduction.service: las trata como caso validado por supervisor (umbral más alto, sin framing de "error previo"), igual que un feedback marcado "used". No hay error del bot, así quewrongAnswerBotva vacío. El tema de la llamada queda enqueryType(análisis de valor), que el flywheel no consume.
Componentes
| Pieza | Ubicación |
|---|---|
| Módulo | Aura-reloaded/src/transcription/ |
| Tabla | audio_transcriptions (migraciones drizzle/0013_*, 0014_*) |
| Cola (RabbitMQ local) | transcription.queue.ts (durable + DLQ + reintentos) |
| Orquestación | transcription.service.ts (publica jobs / consumer processJob) |
| STT pre-recorded | DeepgramService.transcribeFile (en audio-fork/deepgram.service.ts) |
| Atribución de hablantes (LLM) | transcription/speaker-attribution.service.ts (usa LLM_ADAPTER) |
| Análisis de valor (LLM) | transcription/call-analysis.service.ts (usa LLM_ADAPTER) |
| Ingesta al flywheel | MemoryModule → LearningService |
| Frontend | aura-front/src/components/copilot/transcriptions-panel.tsx |
Configuración
DEEPGRAM_API_KEY— igual que la transcripción en vivo (env, nunca en la UI).RABBITMQ_URL— broker local (docker compose up -d rabbitmq). Sin ella, la subida da 503.TRANSCRIPTION_CONCURRENCY(default 1) yTRANSCRIPTION_MAX_ATTEMPTS(default 3).- El modelo (
nova-3/nova-2), idioma y el glosariodeepgram.keytermsse editan en vivo desde Ajustes runtime → grupo Transcripción. - La atribución de hablantes reutiliza el LLM ya configurado (
LLM_PROVIDER/LLM_MODEL, por defectogpt-4o-mini) — no requiere config aparte. - Los prompts de los tres pasos con LLM (atribución de hablantes, análisis de valor y
sugerencia de lecciones) son editables desde el frontend en
/anfibios/prompts(keystranscription.*), con versionado — mismo sistema que el prompt base del agente. Los servicios leen el system prompt activo víaPromptsService.getActiveContent; la parte dinámica (transcript, formato JSON) queda en código.
Infra: solo RabbitMQ local (docker-compose). Sin Redis, sin object storage, y sin broker en la nube (el módulo no corre en producción).
Footguns
keytermes solo Nova-3. Con Nova-2 el glosario se envía comokeywords(el servicio lo conmuta según el modelo). Si cambiás a Nova-2 y el boosting "no anda", es esperable que el comportamiento del glosario difiera.- El audio no se persiste en disco. Viaja en el mensaje de RabbitMQ y se descarta tras transcribir. No hay re-transcripción ni reproducción posterior; para eso haría falta object storage (no está hoy).
- Solo local. Sin
RABBITMQ_URL(producción) la cola queda deshabilitada y la subida responde 503. El broker se levanta condocker compose up -d rabbitmq. - Un reinicio del backend NO pierde los jobs. Los mensajes son durables: RabbitMQ redelivera los no-ackeados y el consumer los retoma (por eso no hay "orphan cleanup").
- Procesamiento secuencial por defecto (
prefetch=1); subíTRANSCRIPTION_CONCURRENCYpara procesar en paralelo. El front refleja el estado de cada uno. - La atribución por LLM es inferencia, no diarización acústica. En audio mono, Deepgram no separa las voces; el LLM adivina el rol por contexto. Es bueno en diálogos claros pero puede errar (ej. si el operador y el cliente dicen frases ambiguas). Si el audio viene estéreo, la separación es acústica y exacta.
- La promoción es curada, no automática. El transcript crudo NO se inyecta al RAG:
learnFromLessonembebe sobrecustomerComplaint, así que un humano arma la lección. La extracción automática con LLM (clasificador + armado de lección) es trabajo futuro.