MultiVoice

Publicado el 29 de septiembre de 2026

Traducción de voz en vivo en Minecraft

En mi servidor se junta gente que habla español, inglés, portugués y japonés. El chat de voz funcionaba, pero la mitad de las conversaciones se perdían por el idioma. Así que monté un sistema que escucha lo que dices, lo escribe y lo traduce al idioma de cada quien, y lo muestra en un bocadillo sobre tu cabeza.

Todo corre en mis propias máquinas, sin APIs de pago, y la pieza que más trabajo hace es una VPS con un procesador de 2010.

Resumen

  • Qué hace: transcribe la voz de proximidad de Plasmo Voice y la traduce al idioma de Minecraft de cada jugador. El texto aparece en un bocadillo sobre la cabeza del que habla.
  • Cómo: el servidor de Minecraft manda una copia del audio a un servicio aparte en Python (Vosk para reconocer, OPUS-MT sobre CTranslate2 para traducir) y devuelve el texto por un paquete nuevo del protocolo.
  • Latencia real: el texto provisional aparece mientras hablas; la frase final llega ~0,7 s después de callar, y la traducción ~0,2 s más tarde.
  • Coste: 0 € en APIs. Una VPS de 8 vCPU aguanta 8–10 personas hablando a la vez con el modelo bueno.
  • Lo que no salió bien: traducir al japonés con modelos libres pequeños es inservible, y sin un glosario de jerga el traductor convierte “creeper” en cosas impublicables.

El camino de una frase

Hablas ──▶ Servidor de Minecraft ──▶ Servicio de voz (otra máquina)
                 │                        │  Opus → 16 kHz → Vosk
                 │                        │  texto final → traducción
                 ◀────── texto ───────────┘
                 │
                 └──▶ Bocadillo sobre tu cabeza, en el idioma de cada oyente

Tres decisiones marcaron el diseño:

1. El audio se copia en el servidor, no en los clientes. El servidor de Minecraft ya recibe la voz de todos y tiene la clave para descifrarla, así que abre una sola conexión con el servicio de voz y le manda el audio Opus tal cual, sin decodificar. Unos 30 kbps por persona hablando, en vez de los 256 kbps que costaría enviar audio sin comprimir.

2. El reconocimiento vive fuera de la JVM. Se come varios núcleos y no quiero que le robe CPU al hilo principal de Minecraft, que es lo que produce tirones en el juego. Además puedo reiniciar el servicio o cambiar de motor sin tocar el servidor.

3. Si nadie lo va a leer, no se transcribe. Solo se manda audio si hay alguien en rango con los subtítulos activados. Es el ahorro más grande de todos: la mayor parte del tiempo, el sistema no consume nada.

Reconocimiento: medir antes de elegir

La VPS es un Xeon X5675 de 2010: 8 vCPU, sin AVX, sin GPU. Eso descarta Whisper de entrada, así que fui con Vosk (Kaldi), que es streaming de verdad y da texto provisional mientras hablas.

Antes de escribir una línea del mod, hice un banco de pruebas que simula N jugadores hablando a la vez en tiempo real y mide cuánto se retrasa el procesado. Resultados con el modelo grande de español:

Streams a la vezNúcleos usadosRetraso p95Veredicto
20,5571 msOK
82,6299 msOK
103,91129 msOK
126,90794 msSe retrasa

Dos cosas aprendí aquí:

  • El coste por stream crece con la carga. 12 streams deberían pedir 3,6 núcleos y pedían 6,9. No era el proveedor robando CPU (vmstat daba steal 0), sino la memoria lenta de un procesador de hace quince años.
  • Un solo proceso con hilos rinde mejor que varios procesos, porque todos comparten la misma copia del modelo en memoria: 2,3 GB en vez de 14 GB.

También probé a afinar el modelo grande. Quitarle el reordenado neuronal final (rnnlm) bajó el coste un 27 % casi sin perder calidad. Tocar los parámetros de búsqueda del decodificador no cambió nada: el coste está en la red neuronal acústica, y sin AVX no hay ajuste que lo arregle.

Traducción: lo barato funciona

Para traducir uso OPUS-MT (los modelos de Helsinki-NLP) convertidos a CTranslate2 con cuantización int8. Son modelos de 79 MB por par de idiomas, y CTranslate2 solo exige SSE4.1, así que funcionan en el Xeon viejo.

Métrica (en la VPS)es→enen→es
Tiempo por frase (p50 / p95)197 / 245 ms191 / 222 ms
Frases por segundo, 2 hilos9,810,9
Frases por segundo, 4 hilos17,518,4

Fue la sorpresa agradable del proyecto: el procesador antiguo traduce casi tan rápido como mi PC moderno, porque el modelo es pequeño.

Tres detalles de diseño que importan más que el modelo:

  • Se traduce una vez por idioma, no por oyente. Si diez personas escuchan y tres idiomas distintos hay entre ellas, son tres traducciones, no diez.
  • Solo se traducen las frases finales. Traducir el texto provisional que se corrige palabra a palabra multiplicaría el coste y haría parpadear el subtítulo.
  • Caché de las últimas frases. En un juego se repite mucho “vamos”, “espera”, “ya voy”.

Idiomas que no encajan en la cuadrícula

Cubrir varios idiomas con modelos por pares tiene un problema evidente: con N idiomas necesitas N×(N−1) modelos. Con 4 idiomas son 12; con 20 serían 380.

La solución clásica es el puente por inglés: si no hay modelo directo, traduzco al inglés y del inglés al destino. Mi enrutador prueba primero el modelo directo y solo encadena si hace falta. Cuesta el doble de tiempo (~1,1 s en vez de ~0,5 s) y pierde algo de fidelidad, pero evita mantener cientos de modelos.

Ahí aparecieron dos casos interesantes.

Japonés: reconocer sí, traducir no

Del japonés hacia fuera funciona bien: “危ないクリーパーが後ろにいる” salió como “¡Cuidado! Creper está detrás de mí”.

Hacia dentro fue un desastre. El único modelo libre de inglés→japonés de Helsinki está entrenado con textos bíblicos, y “watch out there is a creeper behind you” se convirtió en algo que hablaba de buitres congregándose. Inservible.

Decisión: un jugador japonés puede hablar y todos le leen traducido, pero él lee las frases de los demás en el idioma original. Si no hay ruta de traducción, el sistema simplemente manda el texto original; no hay que configurar nada.

Portugués: dos dialectos, un modelo

Para portugués sí existe un modelo moderno (opus-mt-tc-big-en-pt) que acepta una marca de dialecto: >>pob<< para brasileño y >>por<< para europeo. Lo integré detectando automáticamente, en el vocabulario del modelo, si acepta esas marcas.

El resultado honesto: la marca apenas cambia el texto. Pedí portugués europeo y seguí recibiendo “ônibus” y “trem” (brasileño) en lugar de “autocarro” y “comboio”. El mecanismo queda montado por si aparecen modelos mejores, pero hoy no promete lo que no cumple.

El cliente conserva la región solo para los idiomas donde importa: es_mx y es_es son ambos “es”, pero pt_br y pt_pt viajan enteros. Y entre esos dos dialectos no se traduce: se leen igual.

Y la jerga lo rompe todo

La peor traducción del proyecto fue portugués→español, pasando por inglés:

“olha que tens um creeper atrás de ti” → “Mira, tienes una ramera detrás de ti”

También “spawn” → “engendro” y “nether” → “parte inferior”. La traducción automática general no conoce el vocabulario del juego, y el puente por inglés multiplica el destrozo. La solución es un glosario de términos protegidos que no se traducen nunca. Es lo siguiente en mi lista, y el ejemplo perfecto de que estos sistemas fallan justo en las palabras que más se usan en su contexto.

El idioma, sin menús

Un detalle del que estoy contento: no hay selector de idioma. El cliente envía el idioma que tiene puesto en Minecraft, y con eso el servidor asume en qué idioma hablas y a cuál traducirte. Si tú juegas en español y otro en inglés, cada uno lee al otro en el suyo sin tocar una sola opción.

Las únicas opciones que añadí son “ver bocadillos” y “permitir que transcriban mi voz”, porque transcribir la voz de la gente es tratar datos personales y eso se pregunta, no se asume. Por defecto no se guarda nada en disco.

Qué haría con más presupuesto

El diseño por pares de idiomas tiene un techo claro. A partir de unos seis idiomas, lo razonable es una GPU con un único modelo multilingüe (Whisper o similar) en lugar de un modelo por idioma: pasas de 20 modelos y 30 GB de RAM a dos modelos y mejor calidad en todos los idiomas a la vez. El código ya tiene la interfaz de motores separada, así que sería añadir una clase, no rehacerlo.

Mientras tanto, el sistema hace lo que prometía: la gente se entiende.

Lo que me llevo

  1. Medir antes de construir. El banco de pruebas que hice antes de tocar el mod decidió el modelo, el número de personas simultáneas y hasta qué parte del modelo desactivar.
  2. La latencia cero no existe, y conviene decirlo. Nadie transcribe una palabra antes de que termine de decirse. El objetivo realista era “que se sienta instantáneo”, y eso sí se consigue con texto provisional mientras hablas.
  3. Degradar, no romper. Si el servicio de voz se cae, el chat de voz sigue igual; solo dejan de salir subtítulos. Si no hay CPU, la frase se salta. Si no hay traducción, se ve el original.
  4. Los fallos interesantes están en los bordes: un modelo entrenado con la Biblia, una marca de dialecto que no hace nada, y “creeper” convertido en un insulto.