Voces sintéticas para NPCs: TTS para juegos
ElevenLabs, Coqui XTTS, Piper, Bark. Cómo elegir TTS según calidad, latencia y presupuesto. Pipeline pre-generado vs runtime para diálogos de NPCs.
Publicado: · Por Juanjo "Banyo" López
0. Introducción
Doblar 5000 líneas de NPC con actores reales cuesta tiempo, dinero y agenda. Un actor de doblaje cobra por sesión, necesita estudio, retomas y un director que valide. Para el indie con presupuesto contado, o para el juego cuyos NPCs improvisan en runtime (LLM-driven), ese pipeline simplemente no encaja.
Los TTS (text-to-speech) modernos cambiaron el cálculo. Servicios como ElevenLabs sintetizan voces que pasan el filtro del oído casual en condiciones controladas, y modelos open source como Coqui XTTS, Piper o Bark corren en tu máquina sin pagar por carácter. La elección depende del caso: pre-generar 5000 líneas fijas es un problema; hacer que un NPC LLM hable en vivo es otro distinto.
Este tutorial te muestra cómo elegir TTS, cómo armar un pipeline pre-generado para diálogo fijo, y qué cambia cuando lo necesitas en runtime para un NPC que improvisa con un LLM.
En este tutorial vas a ver:
- Qué TTS sirve para cada caso (cinematic vs barks vs runtime).
- Pipeline pre-generación desde spreadsheet a
AudioClipindexado. - Cómo se sincroniza el audio sintético con lip-sync.
- Trampas legales con voice cloning y disclosures de plataformas.
1. Demo
2. Concepto y arquitectura
Un TTS sintetiza audio de voz a partir de texto. Hay servicios cloud (ElevenLabs, OpenAI tts) con calidad cinematográfica y costo por carácter; y modelos open source (Coqui XTTS, Piper, Bark) ejecutables localmente sin costo por uso pero con calidad variable. La elección depende de calidad necesaria, presupuesto y latencia.
La arquitectura mínima de un sistema de voz para NPC tiene tres piezas: el cliente (Unity), el motor TTS (cloud o local) y el almacén de audio (archivos en disco o AudioClips en memoria). El flujo es siempre el mismo:
El texto entra al motor TTS (cloud o local), salen bytes de audio, se decodifican a AudioClip y suenan por el AudioSource de Unity.
Lo que cambia entre proyectos es cuándo ocurre cada paso: en build-time (pre-gen) o en runtime.
2.1 ¿Qué TTS sirve para cada caso?
No hay un “mejor TTS”. Hay trade-offs entre calidad, latencia, costo y dónde corre. Esta tabla resume los cuatro motores más usados a inicios de 2026:
| TTS | Calidad | Latencia | Costo | Hosted |
|---|---|---|---|---|
| ElevenLabs | Excelente, casi cinematográfica | 300-800 ms | $$ por carácter | Cloud |
| OpenAI tts | Muy buena, neutra | 400-1000 ms | $ por carácter | Cloud |
| Coqui XTTS v2 | Buena, voice cloning sólido | 1-3 s en CPU | $0 | Local o self-host |
| Piper | OK, robótica en frases largas | < 500 ms en CPU | $0 | Local |
| Bark | Muy buena, lenta, no controlable | 5-15 s | $0 | Local con GPU |
Regla práctica:
- Cinemáticas, protagonista, narrador: ElevenLabs. La diferencia de prosodia se nota en frases largas.
- NPCs secundarios, barks de combate, vendedores: OpenAI tts o Coqui XTTS si quieres open source.
- Juego sin internet, sin costo por uso, calidad aceptable: Piper. Es el “good enough” gratis.
- Voice cloning para mantener coherencia entre líneas: Coqui XTTS o ElevenLabs (con consent).
2.2 Voice cloning: ¿cuándo conviene?
Voice cloning es generar nuevas líneas con la voz de un hablante a partir de una muestra. Con 1-2 minutos de audio limpio puedes clonar una voz en ElevenLabs o Coqui XTTS. Útil cuando quieres que todas las líneas de un personaje suenen al mismo actor, o cuando combinas líneas pre-grabadas con líneas generadas en runtime.
Cuidado: clonar la voz de alguien sin permiso explícito te lleva a un terreno legal feo y, en algunos casos, criminal. ElevenLabs exige consent verificado para clonar voces no preset. Piensa en voice cloning como en un sample de música: necesitas el contrato, no solo el archivo.
2.3 Pre-generado vs runtime: ¿qué uso?
Hay dos formas de meter TTS en un juego, y casi todos los proyectos terminan combinándolas.
Pre-generado significa que sintetizas todas las líneas en build-time (o en un paso de pipeline antes del build), guardas los .ogg resultantes en Assets/Audio/Generated/ y en runtime solo cargas AudioClips. Cero latencia, cero dependencia de red, costo controlado (lo gastas una vez y no escala con jugadores).
Runtime significa que el juego llama al TTS mientras el jugador está jugando. Lo necesitas cuando el texto se decide en vivo: NPCs LLM que improvisan, sistemas dinámicos de barks contextuales, juegos AI-driven. Pagas por jugador y por sesión, añades latencia, dependes de la red.
Regla simple: si el texto está fijado en el repositorio, pre-genera. Si lo genera un LLM en runtime, runtime.
2.4 ¿Cómo se sincroniza el audio con lip-sync?
Generar la voz es la mitad del trabajo. Si el NPC abre la boca como un pez fuera del agua mientras escuchas la línea, rompe la inmersión. Dos enfoques:
- Phoneme detection clásico: analizas el audio (FFT + clasificador) para detectar fonemas y mapeas cada uno a un viseme (la forma de la boca). Funciona en cualquier audio sin metadata. Algo manual de tunear.
- Modelos AI dedicados: Oculus LipSync (gratis para Quest), NVIDIA Audio2Face, JALI. Toman el audio y generan blendshapes directamente. Resultado mejor con menos trabajo, pero atan tu pipeline a una librería específica.
Para indies, Oculus LipSync es la opción pragmática: gratis, ligero, suficiente.
2.5 Pipeline de pre-generación
El pipeline típico para diálogo fijo se ve así:
Pre-gen ejecuta el TTS en build-time desde un CSV y deja .ogg en disco. Runtime ejecuta el TTS por petición del LLM mientras el jugador habla con el NPC.
Beneficios: idempotente (vuelves a generar solo las líneas que cambiaron por hash del texto), versionable (los .ogg van al repo o a un LFS), barato (lo pagas una sola vez).
2.6 Calidad subjetiva: ¿qué nota el oído?
Comparar TTS no es comparar “claridad”. Todos los motores modernos pronuncian palabras inteligiblemente. Lo que separa a uno de otro es:
- Prosodia: la entonación, el subir y bajar natural de la voz en una frase. ElevenLabs gana aquí por un margen amplio.
- Pausas: dónde respira la voz. Piper tiende a leer todo como un solo bloque sin aire.
- Énfasis contextual: detectar que “no” en “no quiero” debe sonar más fuerte. Solo los modelos top lo hacen bien.
- Emoción: ElevenLabs y Bark pueden modular alegría/tristeza; Piper no.
Si tu juego es narrativo y vive de los personajes, escatimar aquí se nota. Si tus NPCs sueltan barks de 4 palabras (“¡cuidado!”, “¡por aquí!”), cualquier motor sirve.
2.7 Runtime + streaming: ¿cómo bajo la latencia?
En un NPC LLM, la respuesta del modelo tarda 2-4 segundos. Si encima esperas a que el TTS sintetice el bloque completo de texto antes de reproducir nada, sumas otros 2 segundos. Resultado: 4-6 segundos de silencio tras hablar al NPC. Inaceptable.
La técnica clave es streaming TTS: en cuanto el LLM va emitiendo tokens, los pasas al TTS, que devuelve audio mientras siga llegando texto. Empiezas a reproducir el primer chunk de audio antes de que termine la generación completa. ElevenLabs lo soporta nativamente; OpenAI tts también; Piper y Coqui requieren montar tu propio servidor con streaming.
Otra optimización: pre-generar las frases más comunes (“entiendo”, “déjame pensar”, “claro”) y reproducirlas como bridge mientras el TTS principal sintetiza el resto.
2.8 Voice rights y disclosures: ¿qué necesito legalmente?
Esto no es un consejo legal, pero sí lo que vas a encontrar en la práctica:
- Voice cloning: necesitas consent explícito y firmado del hablante. ElevenLabs te lo exige técnicamente para voces no preset. Sin contrato, no clones.
- Voces preset: ElevenLabs y OpenAI ofrecen voces que ellos licencian. Su EULA permite uso comercial dentro de los términos del plan. Léelo igual.
- Disclosure en plataformas: desde 2023, Steam exige declarar si tu juego usa contenido generado por IA en el formulario de submission. Apple y Google tienen políticas similares. No es opcional ocultarlo: te bajan el juego.
- Voces de personajes famosos: ni se te ocurra. Aunque técnicamente puedas clonar a un actor conocido, el right of publicity te alcanza incluso sin que el resultado sea idéntico al original.
3. Pseudocódigo
function generateVoiceLines(lines: Dict<DialogId, String>, voiceId: String, tts: TtsClient) -> Dict<DialogId, AudioFile>
output = {}
for (id, text) in lines
# idempotencia: salta si el hash del texto coincide con el cache
if cache.has(id) and cache.hash(id) == hash(text)
output[id] = cache.path(id)
continue
audioBytes = tts.synthesize(text, voiceId, format="ogg", sampleRate=24000)
path = "Assets/Audio/Generated/" + id + ".ogg"
writeFile(path, audioBytes)
cache.update(id, hash(text), path)
output[id] = path
return output
function speakInRuntime(text: String, voiceId: String, audioSource: AudioSource, tts: TtsClient)
audioBytes = tts.synthesize(text, voiceId)
clip = decodeToAudioClip(audioBytes)
audioSource.clip = clip
audioSource.Play()
function speakStreaming(textStream: Stream<String>, voiceId: String, audioSource: AudioSource, tts: TtsClient)
audioStream = tts.synthesizeStream(textStream, voiceId)
for chunk in audioStream
clip = decodeToAudioClip(chunk)
audioSource.PlayOneShotQueued(clip)
Tres ideas: pre-gen cachea por hash para no pagar la misma línea dos veces, runtime básico bloquea hasta tener el audio completo, y streaming reproduce chunks conforme llegan.
4. Implementación en Unity / C#
Cliente mínimo con HttpClient apuntando a ElevenLabs. El mismo patrón sirve para Coqui XTTS o Piper apuntando a un servidor local (http://localhost:5000/synthesize). El editor script itera un CSV y genera los AudioClip en batch.
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class TtsClient {
static readonly HttpClient http = new();
readonly string apiKey;
readonly string baseUrl;
public TtsClient(string apiKey, string baseUrl = "https://api.elevenlabs.io/v1/text-to-speech") {
this.apiKey = apiKey;
this.baseUrl = baseUrl;
}
public async Task<byte[]> SynthesizeAsync(string text, string voiceId) {
var body = $"{{\"text\":{JsonEscape(text)},\"model_id\":\"eleven_multilingual_v2\"}}";
var req = new HttpRequestMessage(HttpMethod.Post, $"{baseUrl}/{voiceId}");
req.Headers.Add("xi-api-key", apiKey);
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
req.Content.Headers.ContentType = new("application/json");
var resp = await http.SendAsync(req);
resp.EnsureSuccessStatusCode();
return await resp.Content.ReadAsByteArrayAsync();
}
static string JsonEscape(string s) => "\"" + s.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
}
#if UNITY_EDITOR
public static class TtsBatchGenerator {
const string OutputDir = "Assets/Audio/Generated";
[MenuItem("Tools/TTS/Generate Voice Lines")]
public static async void GenerateAll() {
var csv = File.ReadAllLines("Assets/Data/dialog_lines.csv");
var client = new TtsClient(Environment.GetEnvironmentVariable("ELEVENLABS_API_KEY"));
Directory.CreateDirectory(OutputDir);
for (int i = 1; i < csv.Length; i++) {
var cols = csv[i].Split(',');
var (id, voiceId, text) = (cols[0], cols[1], cols[2]);
var path = $"{OutputDir}/{id}.mp3";
if (File.Exists(path) && SameHash(path, text)) continue;
var bytes = await client.SynthesizeAsync(text, voiceId);
File.WriteAllBytes(path, bytes);
WriteHashMeta(path, text);
Debug.Log($"Generated {id} ({bytes.Length} bytes)");
}
AssetDatabase.Refresh();
}
static bool SameHash(string path, string text) {
var metaPath = path + ".hash";
return File.Exists(metaPath) && File.ReadAllText(metaPath) == Hash(text);
}
static void WriteHashMeta(string path, string text) => File.WriteAllText(path + ".hash", Hash(text));
static string Hash(string s) => Convert.ToBase64String(System.Security.Cryptography.SHA1.HashData(Encoding.UTF8.GetBytes(s)));
}
#endif
5. En otros engines
- Godot:
HTTPRequestnode para el POST. Decodificas los bytes conAudioStreamMP3.new()oAudioStreamOggVorbis.load_from_buffer()y los asignas a unAudioStreamPlayer. - Unreal:
FHttpModule::Get().CreateRequest()para la llamada. Los bytes se cargan en unUSoundWaveruntime con laRuntimeAudioImporterplugin (gratis en marketplace). - JavaScript / web:
fetchal endpoint del TTS, luegonew Audio(URL.createObjectURL(blob))oAudioContext.decodeAudioData()para algo más controlable.
La parte que cambia es el decoder de audio. La llamada HTTP y la elección de motor TTS son idénticas.
6. Quiz
Pon a prueba lo que entendiste
Responde una por una. La explicación aparece al elegir, correcta o no.
Tu NPC LLM responde tras 3 segundos, luego el TTS tarda 2 segundos más antes de reproducir audio. ¿Qué optimizas primero?
¿Por qué Piper NO da la misma calidad que ElevenLabs en frases largas?
Quieres clonar la voz de un actor de doblaje para usar en tu juego. ¿Qué necesitas legalmente antes de subir nada?
Tu juego tiene 5000 líneas de diálogo fijo escritas en un spreadsheet. ¿Pre-generación o runtime?