Reporte técnico interno — Nomadic

Cómo entrenamos la voz de Esteban

27 horas de audio real, 19.483 clips, 5 modelos evaluados, 6 fases de tuning A/B y un fine-tuning de 23 horas sobre una GPU casera. Este es el proceso end-to-end para que cualquier persona del equipo pueda replicarlo con otra voz.

Modelo activo · esteban-v1 Tecnología · XTTS v2 fine-tuned (Coqui TTS) Período · Abril – Mayo 2026 Hardware · RTX 2060 SUPER (8 GB VRAM)

1. Resumen ejecutivo (TL;DR)

19.483
Clips de audio
~27 h
Audio real entrenado
159
Videos procesados
16.562
Steps de training
22.9 h
De training, 0 crashes
5
Modelos evaluados
6
Fases de tuning A/B
~U$S 0
Costo de cómputo (GPU local)

El modelo activo se llama esteban-v1. Es un fine-tune de XTTS v2 (Coqui TTS) sobre 19.483 clips de audio real de Esteban, extraídos automáticamente de 159 videos de Google Drive con un pipeline de diarización + identificación por embedding de voz. Vive en eom/brain/voice/ y se usa hoy a través del skill /eom --voz, que dispara los 12 agentes eom-* de Claude Code (DM, Slack, email, coaching, etc.).

El audio final suena conciso, con ritmo natural, con menos pausas artificiales que el modelo previo de 622 clips, y captura el timbre y la cadencia argentina de Esteban. Toda la inferencia es local — sin API, sin costo por llamada.

Output final · esteban-v1 (sample «conversacional»)
Texto: "Lo que necesitamos es que el equipo entienda que esto no es solo un proyecto más..."

2. Objetivo del proyecto

Crear una voz clonada de Esteban que pudiera ser usada por los agentes eom-* de Claude Code para devolver audio cuando Esteban redactara mensajes con /eom --voz, o leyera contenido con /eom --voz --leer.

Tres requisitos no negociables:

  • Calidad indistinguible en samples cortos (≤30s). El audio tenía que servir para mandar mensajes reales al equipo y a clientes, no para una demo.
  • Inferencia local. Sin enviar audio a APIs externas (privacidad + cero costo marginal).
  • Replicable. El mismo pipeline tenía que poder usarse para clonar la voz de otra persona del equipo más adelante.

Restricciones de hardware: una RTX 2060 SUPER de 8 GB VRAM. Eso forzó decisiones durante el entrenamiento (batch size pequeño, gradient accumulation, float32 en vez de mixed precision por bugs específicos de XTTS v2).

3. Cronología del proceso

Marzo 2026

Exploración inicial

Investigamos opciones de voice cloning open-source y zero-shot. Criterios: español rioplatense, inferencia local, fine-tunable, calidad. Shortlist: XTTS v2, OpenVoice V2, CosyVoice 2, Qwen3-TTS, RVC.

Marzo 2026

Bench de modelos zero-shot

Probamos los 5 candidatos con un mismo clip de referencia (15-30s de Esteban hablando solo, micrófono profesional, del workshop «framework 2026»). Generamos el mismo texto con todos para comparar timbre y naturalidad. Resultado: ninguno capturó la voz específica de Esteban en zero-shot.

Marzo – Abril 2026

Primer fine-tune: 622 clips

Grabamos reuniones de Google Meet y cortamos manualmente clips de 3-15s. Transcripción y fine-tune sobre XTTS v2 base. Salió un modelo funcional pero con voz ligeramente aguda y poca variedad tonal — un techo claro por falta de datos.

Abril 2026

Pipeline de extracción masiva

Construimos un pipeline automático para sacar la voz de Esteban de 159 videos de Google Drive: descarga vía Drive API → diarización con pyannote 3.1 → identificación por embedding wespeaker (threshold cosine ≥0.65) → clips WAV de 3-15s. Salida: 18.861 clips nuevos.

Abril 2026

Transcripción

faster-whisper large-v3 sobre los 18.866 clips. 1 hora en GPU. Aceptamos 18.861 (99,97%) con avg_logprob = -0.172 (excelente).

Abril 2026 (23 horas)

Fine-tune v2: 19.483 clips

2 epochs, batch 2 × grad_accum 4 = batch efectivo 8, learning rate 5e-6, float32. 16.562 steps, 22.9 horas de training continuo, cero crashes (el watchdog con auto-restart no se usó). Loss final: 0.93 (inicio: 1.06).

Abril – Mayo 2026

Tuning A/B en 6 fases

Sin re-entrenar: probamos clips de referencia, multi-ref, parámetros de inferencia (temperature, top_p, length_penalty), pitch correction, EQ, y splitting de texto. Ganador final: resultados2025_0000.wav como referencia, temperature=0.6, top_p=0.75.

Mayo 2026

Productivización

El modelo se integró al skill /eom y a los 12 agentes eom-* de Claude Code. Inferencia con infer-xtts-ft.py que auto-detecta el último checkpoint.

4. Tecnologías evaluadas y descartadas

Antes de elegir XTTS v2 fine-tuned, evaluamos cinco arquitecturas alternativas. Todas ellas siguen instaladas en eom/brain/voice/ para referencia y para que cualquiera pueda volver a probarlas. Los scripts de test (test-*.py) generan un mismo texto de prueba con cada modelo.

Modelo Tipo Por qué se descartó Script
XTTS v2 Argentine
(marianbasti)
Zero-shot con acento argentino Buena base de español rioplatense, pero sin fine-tuning no captura la voz específica de Esteban. Confirmó que el camino era fine-tunear XTTS v2 base. test-xtts-arg.py
OpenVoice V2
(MyShell)
Conversión de tono (MeloTTS → ToneColorConverter) Pipeline complejo de 2 pasos. Calidad inferior, voz "robótica", artefactos en la conversión de tono. No competitivo. test-openvoice.py
CosyVoice 2
(Alibaba, 0.5B params)
Zero-shot multilingüe Buena calidad general pero no se acerca al timbre con sólo 15s de referencia. Sin fine-tuning soportado out-of-the-box en español. test-cosyvoice.py
Qwen3-TTS
(Alibaba, 0.6B base)
Zero-shot con alignment de texto Requiere texto de referencia alineado a un audio corto, pipeline frágil. Calidad inferior al fine-tune de XTTS y mucho menos flexible. test-qwen3.py
RVC
(via Applio, 100-200 epochs)
Voice-to-voice conversion (Edge TTS → Esteban) Pipeline en 2 pasos: generar TTS base con otra voz y convertirla. Latencia alta, artefactos en la conversión, no aporta sobre XTTS fine-tuned. Útil sólo si ya tenés un TTS base bueno y querés convertirlo. train-rvc.py
infer-rvc.py

Escuchá los outputs descartados

Todos los modelos descartados generaron el mismo texto: "Bueno, lo que necesitamos es que el equipo entienda que esto no es solo un proyecto más. Es la base de todo lo que viene después."

XTTS v2 Argentine (zero-shot)
Pre-entrenado en argentino, sin fine-tune sobre la voz de Esteban
OpenVoice V2 (tone conversion)
MeloTTS español + ToneColorConverter
CosyVoice 2 (zero-shot)
Alibaba CosyVoice2-0.5B, prompt de 15s
RVC — 50 epochs
Edge TTS base convertido a la voz de Esteban con RVC entrenado
RVC — 100 epochs
Más entrenamiento. Más overfit, no mejora cualitativa.
Edge TTS base (referencia de RVC)
Voz neutra usada como input antes de la conversión RVC
Para qué dejarlos vivos. Cada modelo descartado sigue instalable desde eom/brain/voice/{CosyVoice,OpenVoice,Applio}/. Si en el futuro sale una versión mejor, los scripts de test sirven como benchmark inmediato sin tener que volver a investigar la integración.

5. La tecnología elegida: XTTS v2

¿Qué es XTTS v2?

XTTS v2 (Cross-lingual Text-to-Speech v2) es un modelo multilingüe open-source desarrollado por Coqui. Soporta 17 idiomas, incluido español. Su arquitectura combina:

  • Un encoder GPT-style que predice tokens de audio discretos a partir de tokens de texto.
  • Un decoder DVAE que convierte esos tokens en mel-spectrogramas.
  • Un vocoder HiFi-GAN que sintetiza audio waveform a 24 kHz.

El speaker conditioning se hace con un clip de referencia (3-30s) del cual se extraen latentes de hablante. Por defecto soporta zero-shot, pero acepta fine-tuning sobre el encoder GPT para capturar una voz específica.

Por qué XTTS v2 ganó

CriterioXTTS v2Resto
Fine-tuning soportadoSí, GPT encoder. Pipeline maduro (Coqui Trainer).RVC sí pero distinto modelo; resto: zero-shot only.
Calidad en españolExcelente. Hay fine-tune argentino preentrenado disponible.CosyVoice: ok. OpenVoice: artefactos. Qwen3: frágil.
Inferencia localSí, RTX 2060 SUPER alcanza (~1.5x realtime).Todos sí, pero con peor calidad.
VRAM para training8 GB alcanza con batch=2 + grad_accum=4.RVC: 8 GB ok. Resto: no se fine-tunea.
Documentación + comunidadMadura. Bug en mixed precision conocido y reportado.Variable.

La arquitectura del fine-tune

No se reentrena todo el modelo. Sólo se fine-tunea el GPT encoder (la parte que aprende la prosodia y la entonación). El vocoder y el DVAE quedan congelados — son agnósticos al hablante.

Se parte del checkpoint base de XTTS v2 (~1.8 GB) y se sigue entrenando sobre el dataset propio. El resultado es un best_model.pth de ~5.3 GB que contiene el GPT encoder ajustado.

6. El dataset: pipeline de extracción

Sin datos no hay fine-tune. El primer modelo con 622 clips dio un resultado funcional pero limitado. Para llegar a calidad real necesitábamos un orden de magnitud más. Construimos un pipeline automatizado para extraer voz de Esteban de 159 videos de Google Drive sin tener que cortar manualmente.

6.1 Selección de fuentes (las 5 prioridades P0–P5)

Prio. Criterio Videos Con Esteban Clips Min. audio
P0Videos específicos por ID (workshop solo, mic profesional)1132626
P1Carpeta de recordings personales de Esteban34286.059498
P2Títulos con "Esteban /" o "/ Esteban" (1:1 meetings)4375663
P3Títulos con "Nomadic L10" (leadership meetings)73429.577847
P5Títulos con "Escuela CX" (clases internas)47312.306173
Total efectivo15910519.0241.607

P4 (titulos con "SEO CX-driven") resultó ser duplicado de P1. P6 (todo lo demás operativo) se descartó: el corpus ya era más que suficiente y P6 tiene videos con muchos speakers donde Esteban habla poco — la relación señal/ruido bajaba.

6.2 Pipeline técnico

El script principal es nmc-dev/scripts/extract-esteban-audio.py. Hace cuatro cosas en cadena:

  1. Descarga del video desde Google Drive vía Drive API (con service account + domain-wide delegation, Esteban como subject).
  2. Diarización con pyannote/speaker-diarization-3.1: identifica cuántos speakers hay y en qué timestamps habla cada uno.
  3. Identificación: para cada speaker, se genera un embedding de voz con wespeaker-voxceleb-resnet34-LM y se compara por similitud coseno contra un embedding de referencia de Esteban (promediado sobre 622 clips originales). Threshold: 0.65.
  4. Extracción: cuando un segmento es Esteban y dura entre 3 y 15 segundos, se exporta como WAV a 22.050 Hz mono.

El estado del pipeline se persiste en extracted-clips/_progress.json — fuente de verdad — y permite resumir cualquier corrida. El reporte de presencia se genera en _presencia-esteban.md.

Referencias de voz "limpia" (las 3 sesiones P0)

Estos son los audios de Esteban hablando solo, con micrófono, que sirvieron de semilla para el embedding y para el tuning posterior:

resultados2025 (abr 2025, 55 min)
Workshop solo · mic profesional · el clip ganador del tuning final salió de acá
framework2026 (dic 2025, 30 min)
Workshop framework · mic profesional
estructura2025 (jun 2025, 20 min)
Presentación de estructura · mic profesional

6.3 Transcripción

XTTS necesita pares audio | texto. Transcribimos los 18.866 clips con faster-whisper large-v3 en GPU.

  • Aceptados: 18.861 (99,97%)
  • Filtrados: 5 clips con no_speech_prob > 0.7 o texto < 4 chars.
  • Calidad media: avg_logprob = -0.172 (whisper considera bueno todo lo que esté arriba de -1).
  • Tiempo: ~1 hora total en GPU.

El merge final entre los 622 clips originales y los 18.861 nuevos vive en xtts-dataset/metadata.csv con formato Coqui:

audio_file|text|speaker_name
wavs/resultados2025_0000.wav|Bueno equipo, hoy quiero compartirles...|esteban
extracted/P3/L10-2025-09-15_0014.wav|Lo que necesitamos es que el equipo entienda...|esteban
...

7. El entrenamiento en detalle

7.1 Hiperparámetros

ParámetroValorPor qué
Epochs2Con 19.483 clips, 2 epochs equivalen a ver cada clip 2 veces. El modelo viejo (622 clips) usó 6 epochs. Más epochs con este corpus = overfit.
Batch size2 (efectivo 8)2 es el máximo que entra en 8 GB VRAM. Compensamos con grad_accum_steps=4 para batch efectivo de 8.
Learning rate5e-6Default Coqui. Conservador, evita catastrophic forgetting del modelo base.
Max audio length13 sCubre clips de hasta 12s sin rechazos. Más largo desperdicia VRAM.
Precisionfloat32Mixed precision causa loss=NaN en XTTS v2. Bug conocido. Float32 es el único modo estable.
OptimizerAdamWbetas=(0.9, 0.96), weight_decay=1e-2, eps=1e-8.
LR schedulerMultiStepLRγ=0.5 en milestones 50k×18, 150k×18, 300k×18.
Save policyCada 1000 steps, retener 3~5.3 GB por checkpoint, no podemos guardar todos.
num_loader_workers0En Windows, >0 dispara RecursionError con multiprocessing.

7.2 Hardware y duración

  • GPU: RTX 2060 SUPER (8 GB VRAM, ~7 TFLOPS).
  • Duración total: 22.9 horas continuas.
  • Steps: 16.562 (2 epochs × ~8.281 steps/epoch).
  • Step time medio: 5,8 s.
  • Loss final (train): 0.93 — bajó desde 1.06 al inicio.
  • Crashes: 0. El watchdog con auto-restart desde último checkpoint nunca tuvo que activarse.

7.3 Fixes que tuvimos que hacer al training loop

Fix 1: Recursión infinita en el Dataset de XTTS

El XTTSDataset.__getitem__ original llama a self[1] cuando un sample falla. Si el item 1 también falla (texto muy largo, audio corrupto), entra en recursión infinita y crashea con RecursionError.

El fix es un monkey-patch que reemplaza el __getitem__ con una búsqueda lineal hasta 500 intentos, ignorando samples ya fallados. Vive en finetune-xtts.py líneas 248-301.

Fix 2: mixed_precision rompe el loss

Si activás mixed_precision=True (FP16) en GPTTrainerConfig, después de algunos cientos de steps el loss se vuelve NaN y no hay forma de recuperarlo. Es un bug conocido de XTTS v2 con FP16. Solución: float32 puro. Cuesta más VRAM y un poco más de tiempo, pero el modelo converge.

Fix 3: resume desde último checkpoint

Si el training se corta (corte de luz, crash de driver, OOM), no querés perder 12 horas. El script busca automáticamente el último best_model.pth o checkpoint_*.pth en xtts-finetune-output/run_v2/ y resume desde ahí sin tener que pasar argumentos.

7.4 Loss curve (resumen del log de training)

# Inicio (step 0)
loss: 1.06    loss_text_ce: 0.024    loss_mel_ce: 4.08

# Step 1.400 (1ª epoch)
loss: 0.85    loss_text_ce: 0.023    loss_mel_ce: 3.44

# Step 8.000 (2ª epoch comienza)
loss: 0.94    loss_text_ce: 0.022    loss_mel_ce: 3.45

# Step final (~16.562)
loss: 0.93    loss_text_ce: 0.022    loss_mel_ce: 3.50
Observación. El loss bajó rápido en la primera epoch y se estabilizó. Una tercera epoch probablemente habría llevado a overfit (loss de eval subiría). Por eso cortamos en 2 — más datos > más epochs.

8. Tuning de inferencia (6 fases A/B)

Una vez entrenado el modelo, los parámetros de inferencia mueven mucho la calidad percibida sin re-entrenar nada. Corrimos 6 fases A/B sobre el mismo modelo esteban-v1. El script tune-voice.py automatiza P1, P2 y P3.

Fase 1 · Elección del clip de referencia

XTTS v2 necesita un clip de speaker (3-30s) en cada inferencia. Probamos 9 clips: 3 sesiones × 3 segmentos (inicio, medio, final) cada una. Ganador: resultados2025_0000.wav (mejor tono, voz más relajada).

Ref: resultados2025_0000 (GANADOR)
Segmento de inicio del workshop "Resultados 2025"
Ref: framework2026_0000
Inicio del workshop "Framework 2026"
Ref: estructura2025_0000
Inicio de "Estructura 2025"

Fase 2 · Multi-referencia

XTTS acepta varios clips a la vez (promedia los embeddings). Probamos combos de 3 a 5 clips. Conclusión: multi-ref no superó al single-ref ganador. Más estable pero menos "personal" — el promedio limaba los rasgos distintivos de Esteban. Descartado.

Fase 3 · Parámetros de inferencia

8 variaciones de temperature, top_p y length_penalty. Ganador final: temp=0.6, top_p=0.75, lp=1.15 ("natural").

Presettemperaturetop_plength_penaltyVeredicto
baseline0.750.851.0OK pero un poco "ansioso", overshoot
temp_050.50.851.0Demasiado monótono
conservador0.50.71.1Robotico
natural ✓0.60.751.15Equilibrio: variedad sin overshoot
P3 · baseline (defaults)
P3 · temp=0.5
P3 · conservador
P3 · natural (GANADOR)
temp=0.6 · top_p=0.75 · lp=1.15

Fase 4 · Pitch correction

El audio del modelo a veces sonaba apenas más agudo que la voz real de Esteban. Probamos bajar el pitch -1 y -2 semitonos con ffmpeg rubberband:

P4 · Sin pitch correction
P4 · -1 semitono
P4 · -2 semitonos

-1 semitono ayuda levemente, -2 ya suena artificial. Descartado por complejidad operativa (cada inferencia requeriría un pase extra de ffmpeg). Decisión basada en feedback explícito de Esteban: «no modificar la voz — adaptar canal es sólo pausas, no pitch ni speed».

Fase 5 · EQ paramétrico

3 niveles de EQ (boost 3 kHz / dip 1,5 kHz) para "abrir" la voz. Mejora marginal, no justifica el post-proceso. Descartado por el mismo principio.

Fase 6 · Splitting de texto largo

Para textos largos, hay dos opciones: dejar que XTTS haga el split automático (enable_text_splitting=True) o hacerlo manualmente y concatenar con pausas. Auto-splitting de XTTS ganó: mejor flujo, sin cortes abruptos. Manual splitting (con pausas insertadas) se usa sólo cuando hace falta control fino sobre la respiración entre oraciones — caso --leer para textos largos.

9. A/B: modelo viejo (622 clips) vs esteban-v1 (19.483 clips)

Para confirmar que el corpus expandido valía las 23 horas de GPU, generamos el mismo set de frases con los dos modelos. Cinco estilos para cubrir el rango expresivo de Esteban:

Modelo viejo
622 clips · 6 epochs
esteban-v1
19.483 clips · 2 epochs
Conversacional"Lo que necesitamos es que el equipo entienda..."
VIEJO
esteban-v1
Técnico / explicativo"El tema de la migración SEO es complejo..."
VIEJO
esteban-v1
Entusiasta"Estoy muy contento con los resultados..."
VIEJO
esteban-v1
Reflexivo"A veces hay que parar, respirar..."
VIEJO
esteban-v1
Directivo / corto"Necesito que me mandes el reporte..."
VIEJO
esteban-v1
Veredicto. esteban-v1 suena significativamente mejor: audio más conciso, menos pausas artificiales, mejor ritmo natural, más variedad tonal en los estilos «reflexivo» y «entusiasta». El salto de 622 → 19.483 clips justifica las 23 horas de GPU.

Más samples del modelo final

Conversacional
Directivo
Entusiasta
Reflexivo
Largo (~30s, varias oraciones)

10. Uso actual en producción

10.1 Cómo se invoca el modelo

El modelo está integrado al skill /eom de Claude Code. Tres formas principales:

ComandoQué hace
/eom "consulta"Esteban piensa y responde en texto. No genera audio.
/eom --voz "consulta"Igual al anterior + genera WAV con la voz de Esteban.
/eom --voz --leer "texto"TTS puro: convierte el texto tal cual a audio, sin reescribir.

10.2 Los 12 agentes eom-*

El skill /eom tiene flags de canal que disparan agentes específicos. Cada agente sabe cómo redactar para su contexto, y todos pueden ser combinados con --voz:

FlagAgentePara qué canal
--slackeom-slackCanal público de Slack
--dmeom-dmDM privado
--email-eseom-email-esEmail en español
--email-eneom-email-enEmail en inglés
--feedback-positivoeom-feedback-positivoReconocimiento al equipo
--feedback-correctivoeom-feedback-correctivoFeedback negativo
--coachingeom-coachingCoaching 1:1
--negociacioneom-negociacionNegociación comercial
--oral-clienteeom-oral-clienteReuniones con clientes
--oral-equipoeom-oral-equipoReuniones con equipo
--instrucciones-iaeom-instrucciones-iaInstrucciones a IA
(sin flag)eom-generalPensamiento general sin canal específico

10.3 Inferencia (script de producción)

Toda generación de audio pasa por infer-xtts-ft.py:

cd C:/Users/seoli/Desktop/cursorgit/eom/brain/voice
.venv311/Scripts/python.exe infer-xtts-ft.py \
  --text "Hola equipo, les paso el update..." \
  --output output/eom-20260511-001.wav

Lo que hace internamente:

  1. Busca el último checkpoint en xtts-finetune-output/run_v2/ automáticamente.
  2. Carga el clip de referencia resultados2025_0000.wav.
  3. Si el texto es largo, lo splitea en oraciones <200 chars.
  4. Genera cada chunk con temperature=0.6, top_p=0.75, repetition_penalty=2.0.
  5. Trimea silencios y concatena con pausas de 250 ms entre oraciones.
  6. Exporta WAV a 24 kHz.

11. Dónde está cada cosa (estructura de repos)

Todo el trabajo vive en el repo personal de Esteban (eom/) y el repo de Nomadic (nmc-dev/). El equipo tiene acceso a nmc-dev/; eom/ es privado, pero los scripts de extracción viven en nmc-dev/ y son los que importan para replicar.

Mapa completo de archivos

RutaRepoQué hay ahí
eom/brain/voice/MODELO-ESTEBAN-V1.md eom (privado) Documento técnico fuente. Es la base de este reporte.
eom/brain/voice/finetune-xtts.py eom Script de fine-tuning con resume, Telegram notify, dataset monkey-patch.
eom/brain/voice/infer-xtts-ft.py eom Script de inferencia productivo. Auto-detecta checkpoint y referencia.
eom/brain/voice/tune-voice.py eom Tuning A/B automatizado (P1, P2, P3).
eom/brain/voice/test-{cosyvoice,openvoice,qwen3,xtts-arg}.py eom Benchmarks de modelos descartados. Cada uno genera el mismo texto.
eom/brain/voice/train-rvc.py · infer-rvc.py eom Pipeline RVC (Applio) descartado.
eom/brain/voice/xtts-dataset/ eom Dataset Coqui: wavs/ (622 originales) + metadata.csv (19.483 filas).
eom/brain/voice/xtts-finetune-output/run_v2/ eom El modelo: best_model.pth (5.3 GB), config.json, vocab.json.
eom/brain/voice/extracted-clips/{P0,P1,P2,P3,P5}/ eom 18.866 clips WAV extraídos por el pipeline.
eom/brain/voice/comparacion/ eom Todos los audios de prueba: modelos descartados, tuning P1-P6, A/B viejo vs nuevo.
eom/brain/voice/{CosyVoice,OpenVoice,Applio}/ eom Repos clonados de los modelos descartados, con sus checkpoints. Listos para volver a probar.
nmc-dev/scripts/extract-esteban-audio.py nmc-dev (compartido) Pipeline de extracción de audio. Descarga Drive → diarización pyannote → identificación wespeaker → clips WAV.
eom/brain/voice/transcribe-clips.py eom Transcripción batch con faster-whisper large-v3.
eom/brain/voice/build-metadata.py eom Merge entre metadata original (622) y transcripciones nuevas (18.861).
.claude/skills/eom/SKILL.md cursorgit (raíz) Skill /eom que orquesta canal + voz.
~/.claude/agents/eom-*.md ~/.claude (user-global) Los 12 agentes eom-*.
Acceso al equipo: el modelo entrenado (best_model.pth), los clips, y los audios de comparación viven en eom/ que es privado de Esteban. Para replicar el proceso con otra voz no hace falta acceso a eom/: alcanza con los scripts de nmc-dev/scripts/extract-*-audio.py + los scripts de training (que pueden replicarse a partir de este documento).

12. Receta para replicar con otra voz

Si querés clonar la voz de otra persona del equipo (ej: un líder al que quieras poder citar en audio), seguí estos pasos. Toma ~30 horas de cómputo total (extracción + transcripción + fine-tune) sobre la misma GPU.

Paso 0 · Pre-requisitos

  • GPU NVIDIA con >=8 GB VRAM (RTX 2060 SUPER alcanza).
  • Python 3.11 + venv. Instalar: pip install TTS[all] trainer faster-whisper pyannote.audio wespeaker-python torchaudio
  • HuggingFace token con licencias aceptadas para pyannote/speaker-diarization-3.1 y XTTS v2.
  • Service account de Google con domain-wide delegation + scope drive.readonly.

Paso 1 · Conseguir 1-3 referencias "limpias"

Buscá 1-3 audios de la persona hablando sola con micrófono decente y ≥10 min de duración. Workshops, presentaciones, monólogos. Sin ruido de fondo, sin otra gente. Estos audios cumplen dos funciones: generar el embedding de referencia y servir como clip de referencia en inferencia.

Paso 2 · Generar el embedding de referencia

Cortá los audios limpios en clips de 5-10 segundos (puede ser manual o con un script basado en VAD). Generá un embedding promedio con wespeaker:

from wespeaker import load_model
model = load_model('english')  # también sirve para español
embeddings = [model.extract_embedding(clip) for clip in clips]
avg_embedding = np.mean(embeddings, axis=0)
np.save('persona-embedding.npy', avg_embedding)

Paso 3 · Pipeline de extracción masiva

Adaptá nmc-dev/scripts/extract-esteban-audio.py cambiando:

  • EXPLICIT_VIDEO_IDS y EXPLICIT_FOLDER_IDS → IDs de Drive de la persona objetivo.
  • SA_SUBJECT → email de la persona (para que el SA pueda leer sus drives).
  • EMBEDDINGS_FILE → tu nuevo embedding.
  • Threshold de similitud: empezá en 0.65. Si tenés pocos clips, bajá a 0.60. Si tenés muchos clips de baja calidad, subí a 0.70.

Apuntá a 15.000-20.000 clips de 3-15 segundos para tener calidad alta. Menos de 5.000 clips dará resultados similares al modelo viejo de Esteban (funcional pero mediocre).

Paso 4 · Transcripción

Pasá los clips por transcribe-clips.py. Filtrá clips con no_speech_prob > 0.7 o texto < 4 chars. Generá el metadata.csv en formato Coqui (audio_file|text|speaker_name).

Paso 5 · Fine-tuning

Copiá finetune-xtts.py y ajustá las constantes:

NUM_EPOCHS = 2           # con 15-20k clips
BATCH_SIZE = 2           # NO subir si tenés 8 GB VRAM
GRAD_ACCUM_STEPS = 4
MAX_AUDIO_LENGTH_SEC = 13
LANGUAGE = "es"          # o "en", "pt", etc
LEARNING_RATE = 5e-6
NUM_LOADER_WORKERS = 0   # Windows

Lanzá python finetune-xtts.py. Esperá ~24 hs. El script tiene resume automático: si se corta, relanzar y sigue.

No actives mixed precision. mixed_precision=True causa NaN loss en XTTS v2. Es un bug conocido. Float32 siempre.

Paso 6 · Tuning

Una vez entrenado el modelo, corré tune-voice.py --prueba all para generar variantes de referencia, multi-ref y parámetros. Escuchá y elegí. El punto de partida razonable es temperature=0.6, top_p=0.75, length_penalty=1.15 — el ganador de Esteban — pero cada voz puede preferir otra combinación.

Paso 7 · Integración

Si querés exponerlo como agente Claude Code:

  1. Copiá infer-xtts-ft.py apuntándolo al nuevo checkpoint.
  2. Creá un agente en ~/.claude/agents/persona-X.md con la voz y tono de esa persona (estilo, vocabulario, valores).
  3. Creá un skill persona-X que combine el agente + invoque infer-xtts-ft.py con la flag --voz.

Checklist final antes de declarar la voz "lista"

  • ✅ A/B test con >=5 frases de distintos estilos vs un modelo previo (o vs Edge TTS si no hay previo).
  • ✅ Sample largo (30+ segundos) para verificar coherencia en textos extensos.
  • ✅ Validación humana de la propia persona (¿se reconoce a sí misma? ¿le incomoda?).
  • ✅ Documento MODELO-PERSONA-V1.md con dataset, hiperparámetros, ganadores de tuning, y comparativa A/B.
Esfuerzo realista. Para una segunda voz, todo el código ya existe. El cuello de botella es conseguir el corpus (acceso a Drive de la persona, videos donde aparezca hablando solo o suficientemente diferenciado). El training en sí son ~24 hs de GPU desatendidas.

13. Calibración de identificación por voz (corte 0,65)

El dataset de entrenamiento sale de videos de reuniones donde Esteban aparece. Para extraer solo su voz hay que identificar qué hablante de la diarización es él, comparando el embedding de voz (wespeaker, similitud coseno) de cada cluster contra una referencia. El problema: un umbral que parece validado puede no estarlo, porque el set de validación nunca vio la zona gris.

El hallazgo. El umbral previo de 0,38 —"validado con precisión 1.0 en 36 videos"— atribuía a Esteban más de 500 videos ajenos de un corpus de 953. Su set de validación tenía los negativos en sim 0,15–0,37: nunca vio la zona 0,38–0,60, donde wespeaker da ~0,40 entre dos voces masculinas distintas de timbre parecido (piso de ruido).

La recalibración con ground-truth real (el oído del dueño de la voz) llevó el corte a 0,65 —precisión poblacional 0,99— a un costo acotado (~30 min de escucha + ~50 descargas). El método es replicable para cualquier voz con corpus de referencia.

Las 6 etapas

  1. Triangulación gratis — sin descargar nada, cruzar cada video contra señales independientes del embedding: ¿la persona era attendee en el calendario esa fecha? ¿hay más de un cluster etiquetado como ella (señal de diarización mixta)? Salida: lista de sospechosos.
  2. Muestra de escucha estratificada — ~50 videos (≈6 por banda de 0,05 en la zona gris 0,35–0,70 + anclas de sim alta como control + falsos conocidos). Descargar solo esos, recortar ~25 s del segmento más largo del cluster atribuido.
  3. Escucha ciega (~30 min, la persona) — reproductor con orden aleatorio y la similitud oculta (para no sesgar el oído): botones ÉL / NO ÉL / DUDOSO, export a CSV.
  4. Curva real y corte — cruzar ground-truth × similitud. Clave: la muestra es estratificada, así que se calcula la curva poblacional (ponderada por banda), no la cruda. Se elige el corte por su precisión/recall reales.
  5. Detección de diarización mixta — el modo de falla que el umbral no ve: un cluster SPEAKER_XX con más de una voz (la diarización mezcló personas). El promedio por-cluster diluye la señal. Se valida y filtra por segmento.
  6. Aplicación en dos carriles — videos "limpios" (un solo cluster, se extraen directo) y "mixtos" (se filtran los segmentos de voz ajena antes de extraer).
Resultado. De 908 videos atribuidos con el 0,38 a 349 validados con el corte 0,65 (precisión 0,99 / recall 0,92). La Gaceta y otros falsos positivos confirmados a oído quedaron fuera; las anclas de voz real, dentro. Método y scripts documentados en eom/scripts/extract-video/audit_threshold/METODO.md, replicables para otra voz.