Ignitor Docs ← getignitor.com
🇺🇸 EN 🇪🇸 ES

Editor de Voces#

El Editor de Voces hornea actuación de voz local (text-to-speech) para las líneas habladas del juego, y genera las señales de forma de boca (lip-sync) que mueven la animación de un personaje mientras habla. Lee/escribe un manifiesto de voz por proyecto (voice/voice_manifest.json) que declara una voz por personaje y un conjunto de líneas de voz (qué línea, hablada por qué personaje, con qué overrides de parámetros por línea). El manifiesto no guarda el texto hablado: el id de una línea de voz es su lid, así que las palabras son un registro de strings.json y el manifiesto sólo lo apunta; la generación en sí corre a través de un modelo local (VoxCPM2) mediante un pipeline de Python offline (tools/voicegen/), invocado desde el editor a través de endpoints de job del dev server. Nada de esto se envía en el runtime del juego — solo los WAV resultantes y los archivos JSON de lip-sync sí.

Llegas a él desde el Hub (con el dev server corriendo). La columna izquierda VOICE tiene la configuración de voz del personaje seleccionado; la columna derecha VOICE LINES lista las líneas de ese personaje, ya sea como una lista compacta paginada o, al expandir una, una única línea enfocada con su texto, tomas, y controles de lip-sync. El texto se muestra en sólo lectura — ver más abajo.

La pantalla del Editor de Voz — el manifiesto de voz con sus personajes, líneas de voz y datos de lip-sync.
Editor de Voz — manifiesto de voz con personajes y líneas de voz.

Qué se voicea#

El editor no inventa un guión aparte — importa el texto hablado del propio juego vía 📥 Import from game, así que el texto de una línea de voz ES la línea que el juego realmente muestra, sin doble autoría:

  • strings.json (la tabla de strings de content-i18n, core/strings.js) es la fuente primaria — pero solo sus líneas habladas, no cada fila de la tabla: strings.json también carga nombres de hotspot e ítem, descripciones de sala, texto de GUI, etiquetas de verbo, mensajes de sistema del motor y texto de logros, y ninguno de esos es algo que un personaje diría en voz alta. parseNameLid(), el mismo parser de direcciones que la tabla de strings usa en todos lados, clasifica cada lid según de qué es overlay; solo su tipo 'line' — un SAY/NPCSAY — se vuelve candidata, con clave por el propio lid como id de línea. Es la misma tabla en la que escriben el Editor de Diálogos y el Editor de Cutscenes cuando autoras un token SAY:#lid / NPCSAY:#lid — mira sus docs para cómo se acuña y resuelve un lid. Como el id de línea es el lid, la ruta del WAV horneado (.../<char>/<lid>/<lang>/baked.wav) se puede derivar del lid solo; voicePathForLid() de core/strings.js es el único constructor puro que llaman tanto el editor como el runtime.
  • Los árboles de diálogo también aportan la línea de NPC de cada nodo. El id sale de la dirección de la línea, no de su texto — dialogVoiceLid(dialogId, nodeId), con forma dlg_<dialogo>_<nodo> — así que reescribir el nodo después no huerfaniza el clip horneado. Y es el mismo lid con el que la tabla de strings guarda las traducciones de ese nodo (el literal del archivo de diálogo sigue siendo la fuente del locale por defecto): una sola dirección keyea el texto en pantalla, sus traducciones y su clip de voz. Las líneas de opción del jugador no se importan (el hablante sería el personaje jugable que esté activo, algo que el importador no puede resolver).
  • Los tokens SAY:/NPCSAY: literales heredados que todavía viven directamente en un archivo de sala sin migrar se toman como respaldo, deduplicados por personaje+texto y con un id slugificado.

El modal de import muestra cada línea candidata con su personaje hablante y texto, atenúa las que ya tienes (emparejadas por lid o por personaje+texto), y te deja seleccionar-todo/ninguno antes de agregarlas. Las líneas agregadas son filas normales que puedes renombrar, borrar, o agregar a mano (+ Line) — importar es una comodidad, no el único camino de entrada.

Acá el texto es de sólo lectura#

Los cuadros de texto por idioma de una línea enfocada muestran las palabras de la línea; no las autoran. Como el id de una línea de voz es su lid, el texto es un registro del strings.json del proyecto, y se edita donde se autora el contenido: en el juego (el token SAY: mismo) y en el Editor de Traducciones para los demás idiomas.

Antes era un campo editable que escribía en el manifiesto, o sea que las mismas palabras vivían en dos archivos sin nada que las mantuviera en sincro — y los dos alimentan consumidores distintos: el pipeline tools/voicegen/ hornea el WAV desde uno mientras el motor renderiza el subtítulo desde el otro. Una edición de cualquier lado y el jugador escucha una línea y lee otra. Una línea que todavía carga su propio texto es heredada o sin lidificar; el cuadro lo aclara, y el pipeline la sigue leyendo por una rama de back-compat.

Configuración de voz por personaje#

Cada personaje del juego (de characters.json) aparece automáticamente como una tarjeta de voz; también puedes agregar voces extra (+ Extra voice) para NPCs que no son personajes jugables del juego, con un id libremente editable. Cada voz tiene:

  • SourceVoice Design (un prompt de texto que describe la voz deseada, sin necesidad de audio — VoxCPM2 sintetiza un hablante a partir de la descripción) o Human (clonada desde un WAV de referencia).
  • Para Human, un WAV de referencia — grabado en vivo por el micrófono del navegador (con preview de escuchar-antes-de-guardar) o importado desde un archivo — más un style prompt opcional que ajusta emoción/ritmo al sintetizar sin cambiar el timbre clonado.
  • Parámetros de voz compartidos — knobs rotativos para cfg_value e inference_timesteps, más una seed (vacía = aleatoria en cada horneado, o fíjala para reproducibilidad con el 🎲 de reroll). El conjunto de knobs en sí no está hardcodeado en el editor: tanto él como generate.py leen el mismo tools/voicegen/param_spec.json, así que agregar un knob es un cambio de spec, no una reescritura del editor.
  • Una caja de voice preview (con gate de venv) sintetiza una frase ad-hoc con la configuración actual del personaje y la reproduce al toque — audita un ajuste antes de comprometerlo a un horneado real.

Los overrides por línea (en la vista de línea enfocada) dejan que una línea específica sobrescriba cualquiera de esos mismos knobs; un knob atenuado significa que está heredando el default del personaje, y doble-clic en un knob de override de línea borra el override en vez de solo resetear su valor.

Generación: el pipeline TTS local#

Hornear invoca tools/voicegen/generate.py, que debe correr bajo un venv de Python dedicado (importa torch/voxcpm — nunca cargado por core/, shell/, ni el propio dev_server.py). El editor nunca ejecuta directamente una ruta de venv; hace POST a /api/voice/generate, que arranca un job en background, y el editor sondea /api/voice/generate-status una vez por segundo hasta que reporta terminado. Dos puntos de entrada:

  • ⟳ Regenerate (por línea/idioma enfocado) — fuerza una toma nueva.
  • Bake character — hornea en lote cada línea del personaje seleccionado, saltando cualquier línea cuya toma existente ya coincida con el hash actual de texto + parámetros + referencia (así que re-correrlo no cuesta nada una vez que todo está al día).

La primera vez que se intenta un horneado, window.ensureModelConsent('voxcpm2', …) (tools/_ai-consent.js) lo condiciona: el modelo es una descarga de Hugging Face de varios GB, así que se avisa al usuario una vez antes de bajarlo — llamadas siguientes chequean localStorage y una consulta al caché server-side /api/model-status, y solo vuelven a preguntar si ninguno dice que sí. El id/label/tamaño del modelo vienen del manifiesto compartido tools/ai-models.json, no de strings hardcodeados — el gate de MusicGen del Editor de Audio usa el mismo mecanismo idéntico.

Las tomas no son destructivas#

Cada (re)generación agrega una toma numerada (take_01.wav, take_02.wav, …) en vez de sobrescribir; un takes.json por línea registra los parámetros/texto/hash de cada toma y cuál está baked (canónica — el archivo que el lip-sync y el runtime realmente reproducen). La vista de línea enfocada lista cada toma con un botón de play y un control tipo radio de set as baked, así que puedes auditar alternativas y elegir una favorita sin perder las demás. Una toma también puede pasar por 🎚 Voice FX, que abre el pedalboard completo del Editor de Audio en un modal sobre esa toma puntual y postea de vuelta una toma nueva (nunca muta la original) cuando guardas ahí.

Badges de estado#

Cada línea/idioma muestra uno de: missing (todavía sin tomas), takes (hay tomas, ninguna canónica), baked (el hash de la toma canónica coincide con el texto/parámetros actuales — al día), o stale (existe una toma canónica pero su hash ya no coincide — el manifiesto cambió desde que se generó esa toma). El badge se calcula con el MISMO hash que usa generate.py para decidir si hace falta hornear, así que el estado del editor y la decisión de "nada que hornear" del CLI nunca pueden discrepar.

Horneado de señales de lip-sync#

👄 Bake lip-sync (por línea, o Bake lip-sync (all) para todo el personaje) es una acción separada de la generación de voz, ofrecida independientemente del venv de VoxCPM2 — todo lo que necesita es un baked.wav ya horneado y el modelo de voz de Rhubarb Lip Sync. Ese modelo (unos 70 MB) no viene en el instalador: el primer horneado te pregunta si lo descarga, te muestra el porcentaje mientras lo hace, y a partir de ahí queda cacheado y cada horneado posterior corre sin conexión. Si declinas, simplemente no se hornea — nada se descarga a tus espaldas. Y si prefieres poner tu propia copia de Rhubarb, apunta Hub Config a ella y esa ruta manda. Corre síncronamente a través de /api/voice/lipsync (Rhubarb es suficientemente rápido como para que la respuesta HTTP SEA el resultado, sin sondeo), escribiendo lipsync.json al lado del WAV: { dur, rec, shapes, cues:[{t, shape}, …] }, una señal por cada forma de boca Preston-Blair (AH, X = reposo), con tiempos en segundos contra baked.wav. Las líneas en inglés usan el reconocedor asistido-por-texto pocketSphinx de Rhubarb (alimentado con el propio texto de la línea como pista de diálogo para más precisión); cualquier otro idioma cae al reconocedor fonético, agnóstico de idioma. La vista de línea enfocada renderiza las señales horneadas como una tira de línea de tiempo de solo lectura y proporcional (un segmento coloreado por señal, ancho ∝ duración) para que puedas chequear un horneado a ojo sin abrir el runtime.

En runtime, shell/voice.js es quien realmente consume esto: cada vez que empieza a reproducirse una línea SAY/NPCSAY voiceada, busca de forma perezosa el lipsync.json de esa línea y apunta un único objetivo global de lip-sync (VOICE.lipsyncActive) al actor que habla. En cada cuadro, visemeFrame(shape, frameCount) mapea la forma de la señal activa a un valor de apertura (0 cerrado/reposo, hasta 1 bien abierto) y elige el cuadro correspondiente de los que tenga la animación _talk del actor — funcionan tanto 2 cuadros (cerrado/abierto) como hasta 9 (uno por forma). Sin señales, voces mudeadas, o clip ya terminado, todo cae de vuelta a la animación genérica de habla, así que una línea sin lip-sync igual anima, solo que sin precisión de forma de boca.

Desactualización (stale) y regeneración#

Ambos horneados usan un hash de texto + parámetros efectivos + source/design/ref + mtime del archivo de referencia. Edita el texto de una línea (o el prompt de diseño, style prompt, parámetros, o clip de referencia de un personaje) y su badge pasa de baked a stale — pero el baked.wav y el lipsync.json existentes quedan intactos y siguen reproduciéndose en el juego exactamente igual que antes. Nada fuerza un re-horneado; stale es una advertencia, no un bloqueo. Regenerar la voz (⟳ Regenerate / Bake character) produce una toma nueva contra el hash nuevo y, si la marcas baked, un baked.wav nuevo — pero el lip-sync no se re-hornea automáticamente. Vuelve a correr 👄 Bake lip-sync para esa línea después, o la línea de tiempo de señales queda cableada a como sonaba el baked.wav anterior.

Flujo de trabajo#

  1. 📥 Import from game para traer líneas candidatas desde strings.json (lids de SAY/NPCSAY) y árboles de diálogo; selecciona las que quieres y agrégalas.
  2. Elige un personaje en la toolbar, configura su Source (prompt de Design o referencia Human + style opcional), y ajusta los voice params compartidos.
  3. Usa Voice preview para auditar la configuración con una frase descartable antes de gastar un horneado real.
  4. Bake character (o ⟳ Regenerate una línea enfocada puntual) — espera el gate de consentimiento la primera corrida, después el sondeo del job.
  5. Revisa las takes por línea, escucha, y elige cuál set as baked si la primera no es la mejor.
  6. 👄 Bake lip-sync (por línea o para todo el personaje) una vez que la toma de voz que quieres sea la canónica.
  7. 💾 Save el manifiesto. Todo lo anterior (regen, set-baked, bake de lip-sync) lee el manifiesto desde disco, así que una edición sin guardar bloquea esas acciones hasta que guardes primero. Si alguien más (o una corrida por CLI) escribió el manifiesto desde que lo cargaste, Save muestra un aviso de conflicto — conservar tus cambios locales o sobrescribir los del otro, nunca se resuelve solo — y un servidor inalcanzable da un error en vez de quedarse colgado.

La voz y el lip-sync desactualizados pueden desalinearse en silencio del texto. Editar el texto de una línea SAY después de haberla voiceado no invalida el audio ni sus señales de boca — solo cambia un badge. Si te salteas la regeneración, el personaje va a seguir diciendo la línea vieja (correctamente lip-sincronizada consigo misma) mientras el subtítulo en pantalla muestra el texto nuevo, editado. Y si regeneras la voz pero te olvidas de re-hornear el lip-sync, obtienes un clip nuevo que suena correcto pero mueve la boca con señales cronometradas contra la grabación anterior — normalmente sigue siendo mirable, pero ya no coincide con precisión. Trata los badges stale como una lista de pendientes, no como un detalle visual.