Contenido
- Resumen ejecutivo (TL;DR)
- Objetivo del proyecto
- Cronología del proceso
- Tecnologías evaluadas y descartadas
- La tecnología elegida: XTTS v2
- El dataset: pipeline de extracción
- El entrenamiento en detalle
- Tuning de inferencia (6 fases A/B)
- A/B: modelo viejo vs esteban-v1
- Uso actual en producción
- Dónde está cada cosa (estructura de repos)
- Receta para replicar con otra voz
- Calibración de identificación por voz (corte 0,65)
1. Resumen ejecutivo (TL;DR)
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.
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
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.
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.
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.
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.
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).
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).
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.
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.pyinfer-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."
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ó
| Criterio | XTTS v2 | Resto |
|---|---|---|
| Fine-tuning soportado | Sí, GPT encoder. Pipeline maduro (Coqui Trainer). | RVC sí pero distinto modelo; resto: zero-shot only. |
| Calidad en español | Excelente. Hay fine-tune argentino preentrenado disponible. | CosyVoice: ok. OpenVoice: artefactos. Qwen3: frágil. |
| Inferencia local | Sí, RTX 2060 SUPER alcanza (~1.5x realtime). | Todos sí, pero con peor calidad. |
| VRAM para training | 8 GB alcanza con batch=2 + grad_accum=4. | RVC: 8 GB ok. Resto: no se fine-tunea. |
| Documentación + comunidad | Madura. 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 |
|---|---|---|---|---|---|
| P0 | Videos específicos por ID (workshop solo, mic profesional) | 1 | 1 | 326 | 26 |
| P1 | Carpeta de recordings personales de Esteban | 34 | 28 | 6.059 | 498 |
| P2 | Títulos con "Esteban /" o "/ Esteban" (1:1 meetings) | 4 | 3 | 756 | 63 |
| P3 | Títulos con "Nomadic L10" (leadership meetings) | 73 | 42 | 9.577 | 847 |
| P5 | Títulos con "Escuela CX" (clases internas) | 47 | 31 | 2.306 | 173 |
| Total efectivo | 159 | 105 | 19.024 | 1.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:
- Descarga del video desde Google Drive vía Drive API (con service account + domain-wide delegation, Esteban como subject).
- Diarización con
pyannote/speaker-diarization-3.1: identifica cuántos speakers hay y en qué timestamps habla cada uno. - Identificación: para cada speaker, se genera un embedding de
voz con
wespeaker-voxceleb-resnet34-LMy se compara por similitud coseno contra un embedding de referencia de Esteban (promediado sobre 622 clips originales). Threshold: 0.65. - 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:
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.7o 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ámetro | Valor | Por qué |
|---|---|---|
| Epochs | 2 | Con 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 size | 2 (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 rate | 5e-6 | Default Coqui. Conservador, evita catastrophic forgetting del modelo base. |
| Max audio length | 13 s | Cubre clips de hasta 12s sin rechazos. Más largo desperdicia VRAM. |
| Precision | float32 | Mixed precision causa loss=NaN en XTTS v2. Bug conocido. Float32 es el único modo estable. |
| Optimizer | AdamW | betas=(0.9, 0.96), weight_decay=1e-2, eps=1e-8. |
| LR scheduler | MultiStepLR | γ=0.5 en milestones 50k×18, 150k×18, 300k×18. |
| Save policy | Cada 1000 steps, retener 3 | ~5.3 GB por checkpoint, no podemos guardar todos. |
| num_loader_workers | 0 | En 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
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).
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").
| Preset | temperature | top_p | length_penalty | Veredicto |
|---|---|---|---|---|
| baseline | 0.75 | 0.85 | 1.0 | OK pero un poco "ansioso", overshoot |
| temp_05 | 0.5 | 0.85 | 1.0 | Demasiado monótono |
| conservador | 0.5 | 0.7 | 1.1 | Robotico |
| natural ✓ | 0.6 | 0.75 | 1.15 | Equilibrio: variedad sin overshoot |
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:
-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:
622 clips · 6 epochs
19.483 clips · 2 epochs
Más samples del modelo final
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:
| Comando | Qué 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:
| Flag | Agente | Para qué canal |
|---|---|---|
--slack | eom-slack | Canal público de Slack |
--dm | eom-dm | DM privado |
--email-es | eom-email-es | Email en español |
--email-en | eom-email-en | Email en inglés |
--feedback-positivo | eom-feedback-positivo | Reconocimiento al equipo |
--feedback-correctivo | eom-feedback-correctivo | Feedback negativo |
--coaching | eom-coaching | Coaching 1:1 |
--negociacion | eom-negociacion | Negociación comercial |
--oral-cliente | eom-oral-cliente | Reuniones con clientes |
--oral-equipo | eom-oral-equipo | Reuniones con equipo |
--instrucciones-ia | eom-instrucciones-ia | Instrucciones a IA |
| (sin flag) | eom-general | Pensamiento 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:
- Busca el último checkpoint en
xtts-finetune-output/run_v2/automáticamente. - Carga el clip de referencia
resultados2025_0000.wav. - Si el texto es largo, lo splitea en oraciones <200 chars.
- Genera cada chunk con
temperature=0.6, top_p=0.75, repetition_penalty=2.0. - Trimea silencios y concatena con pausas de 250 ms entre oraciones.
- 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
| Ruta | Repo | Qué 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-*. |
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.1y 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_IDSyEXPLICIT_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.
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:
- Copiá
infer-xtts-ft.pyapuntándolo al nuevo checkpoint. - Creá un agente en
~/.claude/agents/persona-X.mdcon la voz y tono de esa persona (estilo, vocabulario, valores). - Creá un skill
persona-Xque combine el agente + invoqueinfer-xtts-ft.pycon 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.mdcon dataset, hiperparámetros, ganadores de tuning, y comparativa A/B.
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.
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
- 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.
- 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.
- 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.
- 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.
- Detección de diarización mixta — el modo de falla que el umbral no ve: un
cluster
SPEAKER_XXcon más de una voz (la diarización mezcló personas). El promedio por-cluster diluye la señal. Se valida y filtra por segmento. - 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).
eom/scripts/extract-video/audit_threshold/METODO.md,
replicables para otra voz.