Skip to main content

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.

Herramienta solo-local

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

  1. Subida: se aceptan audios sueltos (varios a la vez) o un archivo comprimido .tar/.tar.gz/.zip con 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 fila pending y 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.

  2. Transcripción: el consumer procesa los jobs (prefetch = concurrencia, default 1 = uno a la vez). Llama a Deepgram listen.v1.media.transcribeFile con punctuate, smart_format, utterances, keyterm/keywords boosting del vocabulario del dominio, y separación de hablantes (ver abajo). Descarta el buffer. En error transitorio reintenta (hasta TRANSCRIPTION_MAX_ATTEMPTS) y luego manda el job a la DLQ aura.transcriptions.dlq.

  3. 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 (diarize devuelve un solo speaker, habitual en audio telefónico a 8kHz), cae a atribución por LLM: gpt-4o-mini clasifica cada segmento ya transcripto como operador/cliente por contexto. La segmentación y los timestamps siguen siendo de Deepgram (precisos); el LLM solo pone la etiqueta de rol.
  4. 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 marcan util=false sin llamar al LLM.

  5. 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.

  6. 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) — endpoint GET /:id/suggest-lessons. El superadmin revisa/edita/borra/agrega y confirma; cada lección se guarda como una fila en memory_embeddings vía LearningService.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). promotedLessonId guarda el CSV de ids generados (sirve de marcador y de conteo en la UI).

    Forma de los campos: la consulta se 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. La respuesta se 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 lee production.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í que wrongAnswerBot va vacío. El tema de la llamada queda en queryType (análisis de valor), que el flywheel no consume.

Componentes

PiezaUbicación
MóduloAura-reloaded/src/transcription/
Tablaaudio_transcriptions (migraciones drizzle/0013_*, 0014_*)
Cola (RabbitMQ local)transcription.queue.ts (durable + DLQ + reintentos)
Orquestacióntranscription.service.ts (publica jobs / consumer processJob)
STT pre-recordedDeepgramService.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 flywheelMemoryModuleLearningService
Frontendaura-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) y TRANSCRIPTION_MAX_ATTEMPTS (default 3).
  • El modelo (nova-3/nova-2), idioma y el glosario deepgram.keyterms se editan en vivo desde Ajustes runtime → grupo Transcripción.
  • La atribución de hablantes reutiliza el LLM ya configurado (LLM_PROVIDER/LLM_MODEL, por defecto gpt-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 (keys transcription.*), con versionado — mismo sistema que el prompt base del agente. Los servicios leen el system prompt activo vía PromptsService.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

  • keyterm es solo Nova-3. Con Nova-2 el glosario se envía como keywords (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 con docker 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_CONCURRENCY para 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: learnFromLesson embebe sobre customerComplaint, así que un humano arma la lección. La extracción automática con LLM (clasificador + armado de lección) es trabajo futuro.