@ing.jorgeu/wargaming-overlay 2.5.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/PUBLISHING.md +9 -1
  2. package/README.md +324 -11
  3. package/THIRD_PARTY_ASSETS.md +8 -0
  4. package/battle_reader.py +4 -1
  5. package/cli.js +27 -7
  6. package/game-store.js +2 -1
  7. package/package.json +19 -6
  8. package/public/army-list.css +28 -0
  9. package/public/army-list.html +1 -0
  10. package/public/army-list.js +106 -0
  11. package/public/control.html +2 -2
  12. package/public/control.js +7 -1
  13. package/public/detachments.css +2 -2
  14. package/public/remote-lists-control.js +21 -0
  15. package/public/roster-view.js +12 -0
  16. package/public/rosters-control.js +82 -0
  17. package/public/style.css +7 -0
  18. package/remote-lists/.env.example +16 -0
  19. package/remote-lists/backend.js +70 -0
  20. package/remote-lists/demo.js +4 -0
  21. package/remote-lists/dev.js +2 -0
  22. package/remote-lists/examples/demo-list.txt +10 -0
  23. package/remote-lists/importer.js +60 -0
  24. package/remote-lists/local.js +55 -0
  25. package/remote-lists/public/player.css +7 -0
  26. package/remote-lists/public/player.html +1 -0
  27. package/remote-lists/public/player.js +13 -0
  28. package/remote-lists/references.js +39 -0
  29. package/remote-lists/start.js +10 -0
  30. package/rosters.js +46 -0
  31. package/scripts/setup-kokoro.js +2 -0
  32. package/scripts/tts-demo.js +0 -1
  33. package/server.js +46 -4
  34. package/tts/kokoro-assets.js +27 -0
  35. package/tts/kokoro-engine.js +31 -0
  36. package/tts/kokoro-worker.js +13 -0
  37. package/tts/licenses/espeak-ng-COPYING +674 -0
  38. package/tts/playback.js +28 -0
  39. package/tts/speech.js +55 -3
  40. package/tts/voice-command.js +14 -0
  41. package/tts/wav.js +6 -0
  42. package/tts/windows-playback.js +14 -0
  43. package/youtube/index.js +5 -2
  44. package/youtube/live-chat.js +5 -3
  45. package/youtube/poll-chat.js +15 -0
  46. package/youtube/service.js +18 -9
package/PUBLISHING.md CHANGED
@@ -6,7 +6,7 @@ Desde la raíz del repositorio:
6
6
 
7
7
  ```sh
8
8
  npm ci
9
- npm version 2.5.0 --no-git-tag-version
9
+ npm version minor --no-git-tag-version
10
10
  npm test
11
11
  npm pack --dry-run
12
12
  npm login
@@ -26,3 +26,11 @@ npm view @ing.jorgeu/wargaming-overlay version
26
26
  `files` limita el paquete a código, interfaz y documentación. No incluye Excel de investigación, credenciales OAuth, estados de partidas, pruebas ni workflows. Las dependencias de gRPC se instalan por npm; no hay scripts de instalación propios. ADB se instala únicamente al invocar `setup --install-adb`.
27
27
 
28
28
  Las pruebas automatizadas cubren el transporte gRPC local, OAuth simulado y continuidad del overlay ante errores. El acceso real a la API del canal ya se verificó; la recepción de mensajes de un directo y el cambio físico de salida de audio siguen requiriendo una prueba real. Windows/Linux no usan el TTS nativo de macOS.
29
+
30
+ ## Backend de listas y validación del release
31
+
32
+ El paquete incluye el backend opcional de listas, la página adaptable para jugadores y la plantilla `remote-lists/.env.example`. Publicar en npm no despliega ese backend: se ejecuta por separado con `npm run start:remote-lists`. El overlay lo utiliza únicamente al iniciarse con `--remote-lists`.
33
+
34
+ Antes del release, sigue las pruebas locales del README: primero el flujo demo sin gasto y después, si tienes credenciales, una importación real. `npm test` valida el flujo con un LLM simulado; no acredita por sí solo la calidad ni el coste de respuestas reales. Revisa en `npm pack --dry-run` que se incluyan `remote-lists` y `public/remote-lists-control.js`, y que no haya `.env.dev`, credenciales, datos de partidas ni cachés.
35
+
36
+ Para desplegar el backend utiliza Node 22+, una sola réplica, HTTPS y un volumen persistente indicado por `REMOTE_LISTS_DATA_DIR`. Configura `OPENAI_API_KEY`, una credencial propia del manager y `REMOTE_LISTS_DEMO=false` en las variables del hosting. La caché de referencias de 11.ª caduca a los siete días. La configuración local y el presupuesto persistente no se publican con el paquete.
package/README.md CHANGED
@@ -25,7 +25,7 @@ El paquete está publicado en [npm como @ing.jorgeu/wargaming-overlay](https://w
25
25
 
26
26
  | Dependencia | Requisito y función | Descarga oficial |
27
27
  | --- | --- | --- |
28
- | Node.js y npm | Node 20 o posterior; preferir una versión LTS mantenida. Ejecuta el CLI y servidor. npm viene con las instalaciones habituales de Node. | [Node.js](https://nodejs.org/en/download/) |
28
+ | Node.js y npm | Node 22 o posterior; preferir una versión LTS mantenida. Ejecuta el CLI y servidor. npm viene con las instalaciones habituales de Node. | [Node.js](https://nodejs.org/en/download/) |
29
29
  | Python | Python 3.8 o posterior; preferir una versión mantenida. Interpreta el XML de Android. | [Python](https://www.python.org/downloads/) |
30
30
  | ADB | Android SDK Platform-Tools. Comunica el equipo con el teléfono. | [Platform-Tools](https://developer.android.com/tools/releases/platform-tools) |
31
31
  | OBS Studio | Fuente Navegador disponible. Muestra el overlay sobre el vídeo. | [OBS Studio](https://obsproject.com/download) |
@@ -372,15 +372,18 @@ Instala las dependencias con `npm ci` (o `npm install` si actualizas dependencia
372
372
  | `npm start` | Inicia el backend desde este checkout. |
373
373
  | `npm start -- --port 8766 --interval 5` | Pasa opciones al backend; el separador `--` es necesario. |
374
374
  | `npm test` | Ejecuta las pruebas automatizadas; no necesita el canal ni reproduce audio. |
375
- | `npm run test-tts` | Reproduce tres mensajes de prueba por la salida predeterminada de macOS. |
375
+ | `wargaming-overlay --test-tts "Hola"` | Lee un texto con la configuración TTS y termina; no inicia overlay, YouTube ni ADB. |
376
+ | `npm run test-tts` | Reproduce tres mensajes de prueba por la salida predeterminada del sistema. |
376
377
  | `npm run facts:coverage` | Informa la cobertura real de curiosidades por facción. |
377
378
  | `npm run facts:coverage -- --require-four-hours` | Devuelve código 1 si alguna facción no llega a 120 curiosidades. |
378
379
  | `npm run prepublishOnly` | Ejecuta las pruebas previas a publicar; npm lo invoca automáticamente en `npm publish`. |
379
380
  | `npm pack --dry-run` | Revisa los archivos que incluirá el paquete sin publicarlo. |
380
381
 
381
- Para instalar o actualizar el comando global: `npm install -g @ing.jorgeu/wargaming-overlay`. Para una versión concreta: `npm install -g @ing.jorgeu/wargaming-overlay@2.5.0`. Usa `wargaming-overlay --version` para comprobar qué instalación ejecuta tu terminal.
382
+ Para instalar o actualizar el comando global: `npm install -g @ing.jorgeu/wargaming-overlay`. Para una versión concreta: `npm install -g @ing.jorgeu/wargaming-overlay@3.1.0`. Usa `wargaming-overlay --version` para comprobar qué instalación ejecuta tu terminal.
382
383
 
383
- ### Activar y desactivar la voz de YouTube (macOS)
384
+ ### Activar y desactivar la voz de YouTube
385
+
386
+ El TTS está **desactivado por defecto**. Sin `YOUTUBE_CHAT_TTS_ENABLED=true`, el overlay no inicia el chat, la cola de voz ni Kokoro. También puedes deshabilitarlo con `"enabled": false` en `youtube-tts.json`.
384
387
 
385
388
  ```sh
386
389
  # Desde el repositorio
@@ -393,7 +396,7 @@ YOUTUBE_CHAT_TTS_ENABLED=true wargaming-overlay start
393
396
  YOUTUBE_CHAT_TTS_ENABLED=false wargaming-overlay start
394
397
  ```
395
398
 
396
- La variable se aplica a ese inicio. También puedes exportarla en tu terminal; para quitar un valor exportado, usa `unset YOUTUBE_CHAT_TTS_ENABLED`. La configuración opcional `youtube-tts.json`, los pasos OAuth y las limitaciones están en [YouTube Live → voz local](#youtube-live--voz-local-macos). `Ctrl+C` detiene lector, chat y voz; los cambios de código/configuración requieren reiniciar.
399
+ La variable se aplica a ese inicio. También puedes exportarla en tu terminal; para quitar un valor exportado, usa `unset YOUTUBE_CHAT_TTS_ENABLED`. La configuración opcional `youtube-tts.json`, los pasos OAuth y las limitaciones están en [YouTube Live → voz local](#youtube-live--voz-local-con-kokoro). `Ctrl+C` detiene lector, chat y voz; los cambios de código/configuración requieren reiniciar.
397
400
 
398
401
  Para publicar una release, consulta [PUBLISHING.md](PUBLISHING.md): `npm version VERSION --no-git-tag-version`, `npm login`, `npm whoami`, `npm publish --dry-run` y `npm publish --access public`. La publicación requiere acceso a la cuenta propietaria y una versión no publicada.
399
402
 
@@ -567,7 +570,7 @@ npm pack
567
570
 
568
571
  `npm pack` genera el `.tgz` instalable. `npm pack --dry-run` lista el contenido sin crear ese archivo. No hay un paso de compilación ni dependencias npm que descargar para ejecutar las pruebas; estas sí necesitan Python.
569
572
 
570
- La matriz de `.github/workflows/test.yml` está preparada para Node 20/22/24 en Windows, macOS y Linux. Las pruebas cubren diagnósticos, rutas, selección de instaladores, parser y estado guardado. No sustituyen las pruebas físicas de USB ni de emisión con OBS.
573
+ La matriz de `.github/workflows/test.yml` está preparada para Node 22/24 en Windows, macOS y Linux. Las pruebas cubren diagnósticos, rutas, selección de instaladores, parser y estado guardado. No sustituyen las pruebas físicas de USB ni de emisión con OBS.
571
574
 
572
575
  Consulta [PUBLISHING.md](PUBLISHING.md) para autenticarte y publicar. `npm publish` sube el paquete realmente; no es un paso necesario para usarlo localmente. El scope debe ser tuyo o de una organización en la que tengas permiso. No se han incluido credenciales en el proyecto.
573
576
 
@@ -671,9 +674,9 @@ Activa la depuración inalámbrica y empareja el computador con el teléfono en
671
674
 
672
675
  Para elegir otro teléfono, inicia con `--serial SERIAL` usando su identificador de `adb devices -l`. Si hay varios teléfonos diferentes y ninguno seleccionado, el lector pide seleccionar uno. El panel distingue conexión USB, Wi-Fi y espera de una ronda legible. Al enviar Tabletop Battles al fondo se conserva el último marcador; al volver a la ronda, la lectura se reanuda. Durante una reconexión puede haber una pausa de unos segundos.
673
676
 
674
- ## YouTube Live → voz local (macOS)
677
+ ## YouTube Live → voz local con Kokoro
675
678
 
676
- Función opcional del mismo backend, independiente de OBS y del teléfono. Usa OAuth de solo lectura, descubre el directo activo de la cuenta autorizada y recibe el chat por `liveChatMessages.streamList` (gRPC). La voz sale por el dispositivo predeterminado de macOS; volumen y salida se cambian en macOS. No usa TTS externo ni IA.
679
+ Función opcional del mismo backend, independiente de OBS y del teléfono. Usa OAuth de solo lectura, descubre el directo activo de la cuenta autorizada y recibe el chat por `liveChatMessages.streamList` (gRPC). La voz neural Kokoro se genera localmente desde Node mediante ONNX. Sale por el dispositivo predeterminado del sistema; volumen y salida se cambian en los ajustes de audio. No necesita Python, Homebrew ni servicios TTS externos.
677
680
 
678
681
  ### Primera autorización
679
682
 
@@ -698,11 +701,11 @@ YOUTUBE_CHAT_TTS_ENABLED=true npm start
698
701
  npm run test-tts
699
702
  ```
700
703
 
701
- La segunda instrucción ejecuta tres mensajes falsos, sin YouTube ni OBS. No la ejecutes a la vez que otro lector TTS: la exclusión de voces es por proceso. Para desactivar el chat, omite la variable o usa `YOUTUBE_CHAT_TTS_ENABLED=false`. No se inicia ninguna conexión YouTube cuando está desactivado. En Windows/Linux el overlay continúa pero esta voz no se inicia.
704
+ La segunda instrucción ejecuta tres mensajes falsos, sin YouTube ni OBS. No la ejecutes a la vez que otro lector TTS: la exclusión de voces es por proceso. Para desactivar el chat, omite la variable o usa `YOUTUBE_CHAT_TTS_ENABLED=false`. No se inicia ninguna conexión YouTube cuando está desactivado. El TTS también se inicia en Windows x64 y Linux x64/ARM64 con audio de escritorio.
702
705
 
703
706
  Si no hay directo con chat, comprueba cada 60 segundos. Al conectar omite el historial anterior a la conexión. Mantiene en memoria el identificador del chat, el cursor de continuación y hasta 1.000 IDs recientes. Ante una desconexión reintenta a 1, 2, 5, 10 y como máximo 30 segundos; una única conexión por instancia. Al terminar el directo o deshabilitarse el chat cancela la voz pendiente y vuelve a buscar. Si hay varios directos simultáneos, elige el primero por ID y lo indica en el log.
704
707
 
705
- Los mensajes normales se transforman en eventos `youtube_chat_message`; los eventos administrativos, Super Chat y membresías no se leen en V1. No se escriben mensajes en disco. La terminal muestra mensajes sanitizados y estados YouTube/TTS, sin tokens. Los fallos de OAuth, chat o voz no detienen el overlay.
708
+ Los mensajes normales se transforman en eventos `youtube_chat_message`; los eventos administrativos, Super Chat y membresías no se leen en V1. No se escriben mensajes de texto en disco. En Windows, el audio se guarda temporalmente como WAV y se elimina al finalizar o cancelar la lectura. La terminal muestra mensajes sanitizados y estados YouTube/TTS, sin tokens. Los fallos de OAuth, chat o voz no detienen el overlay.
706
709
 
707
710
  ### Ajustes sencillos
708
711
 
@@ -719,7 +722,7 @@ Opcionalmente crea `youtube-tts.json` dentro de la carpeta de datos que muestra
719
722
  }
720
723
  ```
721
724
 
722
- Reinicia el backend para aplicar cambios. Los mensajes se truncan sin cortar palabras cuando es posible, los nombres se limitan a 40 caracteres y las URLs se sustituyen por «Enlace». Si la cola está llena, elimina el pendiente más antiguo; nunca interrumpe una voz para superponer otra. Al detener el backend cancela la voz activa y descarta los pendientes. Cada mensaje usa un proceso nuevo de `/usr/bin/say`, sin shell, sin seleccionar dispositivo, y con un tiempo máximo de 120 segundos.
725
+ Reinicia el backend para aplicar cambios. Los mensajes se truncan sin cortar palabras cuando es posible, los nombres se limitan a 40 caracteres y las URLs se sustituyen por «Enlace». Si la cola está llena, elimina el pendiente más antiguo; nunca interrumpe una voz para superponer otra. Al detener el backend cancela la voz activa y descarta los pendientes. Cada mensaje usa un proceso Node aislado para síntesis y reproducción, sin shell y con un tiempo máximo de 120 segundos. La cancelación termina ese proceso y su audio. En macOS, si Kokoro falla, se usa `say` por defecto; en Windows/Linux se registra el error y continúa la cola.
723
726
 
724
727
  La moderación es un filtro local pequeño de patrones ES/EN para insultos discriminatorios graves, amenazas directas, llamados a violencia y algunas expresiones sexuales explícitas. Sustituye los fragmentos por «censurado» y descarta mensajes sin contenido restante. Conserva groserías coloquiales como «qué mierda de tirada» o «ese weón tuvo demasiada suerte». **No entiende contexto ni garantiza detectar todas las variantes o idiomas**; amplía `extraBlockedTerms` según el canal. También filtra los nombres y elimina instrucciones de voz embebidas antes de ejecutar `say`.
725
728
 
@@ -728,3 +731,313 @@ La moderación es un filtro local pequeño de patrones ES/EN para insultos discr
728
731
  `npm test` incluye OAuth con callback local y renovación simulada, gRPC local real, descubrimiento de directos, continuación tras desconexión, duplicados, fin del directo, backoff, sanitización y cola sin superposición. No requiere credenciales ni envía mensajes a YouTube. La prueba real de autorización, recepción de un directo y cambio físico de salida de audio requiere la cuenta del canal y comprobación del operador.
729
732
 
730
733
  Referencias oficiales: [OAuth de escritorio](https://developers.google.com/youtube/v3/guides/auth/installed-apps), [descubrimiento de directos](https://developers.google.com/youtube/v3/live/docs/liveBroadcasts/list), [chat por streaming](https://developers.google.com/youtube/v3/live/streaming-live-chat), [duración de tokens en Testing](https://developers.google.com/identity/protocols/oauth2).
734
+
735
+ ## Listas de ejército para OBS
736
+
737
+ En el administrador abre **Listas de ejército**. Cada jugador tiene su propia lista y un enlace **Abrir lista para OBS**. El nombre, la facción y los destacamentos se editan en **Partida** y aparecen en la cabecera de la lista.
738
+
739
+ 1. Pulsa **Añadir unidad** e introduce su nombre.
740
+ 2. Añade, si corresponde, el nombre de una mejora, varios personajes (uno por línea) y una nota breve.
741
+ 3. Ordena las unidades arrastrando el asa **⠿** y soltándola antes o después de otra unidad. **Subir / Bajar** también está disponible para teclado.
742
+ 4. Marca **En reservas** para mostrar la etiqueta de reserva, o **Unidad destruida** para tacharla y atenuarla. Una unidad destruida deja de mostrar la etiqueta En reservas.
743
+ 5. Pulsa **Guardar lista**. Los cambios llegan a OBS sin refrescar la fuente. Las modificaciones quedan como borrador hasta guardarlas; el navegador avisa si intentas cerrar con cambios pendientes.
744
+
745
+ En OBS crea dos escenas nuevas, cada una con una fuente **Navegador**, y pega el enlace de su jugador. Configura **1920 × 1080** (se escala proporcionalmente en otras resoluciones 16:9). Puedes cambiar de escena libremente. La URL lleva un identificador estable del jugador: sigue mostrando su lista aunque intercambies izquierda/derecha. Las etiquetas de color sí siguen el lado actual. Toda la lista aparece en una sola pantalla, sin páginas ni rotación. El diseño ajusta columnas y tamaño de texto para que quepa; las listas muy extensas tendrán texto más pequeño. Las unidades consecutivas con el mismo nombre se agrupan bajo un encabezado con su cantidad; cada copia conserva sus personajes, mejora, notas y estado. Si otra unidad interrumpe la secuencia, se inicia un grupo nuevo.
746
+
747
+ La vista está en `/army-list.html?player=IDENTIFICADOR`; copia la URL exacta desde el panel. También existen `/army-list.html?side=left` y `?side=right` para fuentes que deban seguir un lado, en vez de a una persona.
748
+
749
+ Puedes asignar una **URL de imagen de fondo** a cada jugador desde el editor. Debe ser un enlace HTTP/HTTPS directo y accesible desde OBS; deja el campo vacío para usar el fondo predeterminado. La imagen se carga en el navegador, no en el servidor, y se oscurece para mantener la legibilidad. El fondo se guarda junto con la lista y se incluye al exportar JSON.
750
+
751
+ ### Importar y exportar
752
+
753
+ **Exportar JSON** descarga la lista actual, incluidos los cambios del borrador. **Importar JSON** permite recuperarla o cargar un archivo con este formato:
754
+
755
+ ```json
756
+ {
757
+ "backgroundUrl": "",
758
+ "units": [
759
+ {
760
+ "name": "Rubric Marines",
761
+ "enhancement": "Nombre de la mejora",
762
+ "characters": ["Ahriman", "Exalted Sorcerer"],
763
+ "notes": "Vigilar el objetivo central",
764
+ "deepStrike": true,
765
+ "destroyed": false
766
+ }
767
+ ]
768
+ }
769
+ ```
770
+
771
+ También se acepta directamente el array de unidades. Los identificadores se generan al importar. La importación queda como borrador: revisa y guarda. No se interpretan automáticamente PDFs ni exportaciones de otras aplicaciones en esta versión.
772
+
773
+ Límites: 80 unidades por jugador; nombre y mejora hasta 100 caracteres; hasta 8 personajes de 80 caracteres cada uno; notas hasta 160 caracteres. No se impone que los nombres de unidades sean distintos: puedes tener varias escuadras iguales.
774
+
775
+ Las listas se guardan en `rosters.json` dentro de la carpeta de datos, fuera del paquete npm. Para respaldarlas, conserva también `overlay-state.json`, que contiene el vínculo de cada jugador. Reiniciar el servicio o restablecer los puntos de la partida no borra las listas. Al preparar otra partida puedes editar o importar las nuevas listas. Si otra pestaña modificó una lista, el servidor rechaza sobrescribirla con un borrador antiguo; exporta tu borrador si quieres conservarlo y recarga para obtener la versión actual.
776
+
777
+ Si el panel indica que el servicio está desactualizado al guardar un fondo, detén `npm start` con Ctrl+C y ejecútalo de nuevo. Recargar el navegador no actualiza el servidor Node. La URL queda en el borrador para guardarla después del reinicio. El overlay de listas ocupa toda la fuente de OBS incluso si no usa una proporción 16:9.
778
+
779
+ Cada lista admite un nombre opcional (máximo 80 caracteres), editable en «Listas de ejército». Se muestra encima del nombre del jugador en OBS y se conserva al importar/exportar JSON mediante el campo `name`. Sin nombre se muestra «ORDEN DE BATALLA». Reinicia el servicio después de actualizar para activar el guardado del campo.
780
+
781
+ Marca **Battleshocked** en una unidad y guarda la lista para mostrar su etiqueta ámbar en OBS. El estado se conserva en el campo `battleshocked` del JSON; las listas antiguas lo toman como falso. Las unidades destruidas ocultan esta etiqueta. Desmárcalo cuando deje de aplicarse.
782
+
783
+ Cuando una unidad tiene personajes asignados, sus nombres son el título principal y debajo aparece «Lidera:» con el nombre de la unidad. En los grupos consecutivos cada unidad conserva sus propios personajes y estados.
784
+
785
+ Los enlaces del administrador para OBS son fijos por color: `/army-list.html?side=blue` y `/army-list.html?side=red`. Usa «Copiar URL azul/roja» para copiarlos al portapapeles. Al intercambiar jugadores muestran la lista del nuevo ocupante de ese color, sin cambiar la URL en OBS. Los enlaces anteriores siguen siendo compatibles.
786
+
787
+ Las listas se guardan automáticamente al dejar de escribir brevemente, reordenar o cambiar estados. El panel muestra el estado del guardado y reintenta si falla la conexión. Completa los nombres de las unidades nuevas para guardar. No cierres el panel mientras indique cambios pendientes o errores.
788
+
789
+ Si se habilita `httpFallback` y `streamList` rechaza las credenciales con código 16, el chat cambia automáticamente a consultas HTTP de respaldo. Respeta el intervalo de YouTube con un mínimo de 15 segundos; puede haber esa demora adicional en la lectura. Este modo consume cuota por consulta (5 unidades, hasta unas 1.200 por hora a intervalos de 15 segundos).
790
+
791
+ ### Autenticación de streamList y respaldo opcional
792
+
793
+ Si gRPC rechaza OAuth aunque la API HTTP funcione, se puede probar la autenticación oficial mediante API key. En Google Cloud crea una clave restringida a YouTube Data API v3 y guárdala en un archivo local fuera del repositorio. En `youtube-tts.json` configura `"apiKeyFile": "/ruta/privada/youtube-api-key.txt"`. Reinicia el servicio. OAuth sigue usándose para detectar el directo; streamList envía solo la API key. La clave no se imprime ni se exporta al overlay.
794
+
795
+ El respaldo por consultas HTTP está **desactivado por defecto** para conservar cuota. Para habilitarlo explícitamente configura `"httpFallback": true`. Cada consulta consume cuota; no equivale a una conexión streamList persistente.
796
+
797
+
798
+ ### Instalación y configuración de Kokoro
799
+
800
+ Requiere Node 22+. `npm install` instala las dependencias TTS, pero no descarga el modelo. Kokoro descarga automáticamente su modelo v1.0 q8 y tres voces españolas (aproximadamente 96 MB) al leer el primer mensaje o ejecutar una prueba de voz con ese proveedor. Si el TTS está desactivado o eliges `say`, no descarga ni carga el modelo. La caché se guarda en `~/.cache/wargaming-overlay/kokoro/<revisión>`. Después de la primera descarga, la síntesis funciona sin internet. No hay pasos de instalación manual obligatorios para TTS. Opcionalmente puedes preparar la caché antes de transmitir con `npm run setup-tts`. Si una descarga falla, las siguientes lecturas reintentan y se aplica el respaldo configurado. La conexión a YouTube sí requiere internet.
801
+
802
+ Sistemas de audio compatibles: macOS Intel/Apple Silicon (CoreAudio), Windows x64 (PowerShell/SoundPlayer y ONNX WebAssembly), Linux x64/ARM64 con glibc y ALSA (`libasound.so.2`) y una salida de audio de escritorio. Windows ARM64, Alpine/musl y sistemas sin audio no se consideran compatibles con reproducción. No se requieren Python, Rust, CMake, ffmpeg ni espeak instalados en el sistema. Linux necesita sus bibliotecas de audio habituales; el paquete no instala componentes del sistema operativo.
803
+
804
+ Añade estas propiedades a `youtube-tts.json`, conservando las demás:
805
+
806
+ ```json
807
+ {
808
+ "ttsProvider": "kokoro",
809
+ "ttsFallback": "say",
810
+ "kokoroVoice": "ef_dora",
811
+ "kokoroSpeed": 1
812
+ }
813
+ ```
814
+
815
+ En Windows/Linux usa `"ttsFallback": "none"`. Voces: `ef_dora`, `em_alex`, `em_santa`. Velocidad: de 0.5 a 2. Para volver a la voz anterior en macOS cambia `"ttsProvider": "say"`. Reinicia el backend para aplicar los cambios. También puedes usar `TTS_PROVIDER`, `TTS_FALLBACK`, `KOKORO_VOICE`, `KOKORO_SPEED` y `KOKORO_CACHE_DIR`; las variables tienen prioridad sobre el JSON. `KOKORO_SKIP_DOWNLOAD=true` omite la descarga del comando opcional `npm run setup-tts`, pero no durante la primera lectura con Kokoro.
816
+
817
+ El modelo se fija a Kokoro v1.0 q8 y a una revisión verificada, para evitar cambios silenciosos. Cambiar el modelo requiere actualizar el manifiesto `tts/kokoro-assets.js` (revisión, archivos y SHA-256) y verificar que conserva las entradas/salida ONNX usadas por `tts/kokoro-engine.js`; no se aceptan IDs de modelos arbitrarios.
818
+
819
+ La cola FIFO, los filtros y el overlay conservan su funcionamiento. En Windows se usa ONNX WebAssembly para evitar instalar el runtime de Visual C++; la reproducción usa PowerShell incluido en Windows y termina con `PlaySync`. La cancelación termina el árbol de procesos. En macOS/Linux se usa ONNX nativo y CoreAudio/ALSA. La síntesis se realiza en un proceso aparte para no bloquear HTTP/SSE. Las pruebas de generación real se realizaron en macOS; la reproducción física en macOS y la ejecución real en Windows/Linux siguen pendientes. Las pruebas automáticas usan audio simulado y no reproducen sonido.
820
+
821
+ En la prueba local, la generación nativa tardó aproximadamente 2 s en caliente y 3.7 s en frío para una frase de 3.9 s. El backend WebAssembly que usa Windows tardó aproximadamente 6.5 s en la misma Mac; no es una medición de rendimiento en Windows.
822
+
823
+
824
+ ### Probar un texto sin iniciar el servicio
825
+
826
+ ```sh
827
+ wargaming-overlay --test-tts "Pedro dice: Magnus falló una carga de ocho pulgadas."
828
+ ```
829
+
830
+ También está disponible el alias `warhammer-overlay --test-tts "Hola mundo"`. Ambos nombres ejecutan el mismo CLI.
831
+
832
+ Desde el repositorio:
833
+
834
+ ```sh
835
+ npm run start -- --test-tts "Hola, esta es una prueba de voz."
836
+ npm run start -- --test-tts "Hola, soy Alex." --voice em_alex --fallback none
837
+ npm run start -- --test-tts "Esta es la voz anterior de macOS." --provider say
838
+ ```
839
+
840
+ El comando lee el texto por la salida de audio predeterminada, espera a que termine la reproducción y sale. No inicia el servidor, el lector del teléfono ni conexiones a YouTube/OBS; tampoco modifica la partida o la configuración guardada. El texto de prueba se envía directamente al TTS, sin añadir nombre de usuario ni aplicar los filtros del chat. Si falta el modelo de Kokoro, lo descarga automáticamente la primera vez.
841
+
842
+ Por defecto utiliza las opciones de voz de `youtube-tts.json` en la carpeta de datos habitual y las variables de entorno TTS. Puedes elegir otra configuración con `--data-dir RUTA`. Las opciones del comando tienen prioridad sobre las variables y el archivo, y solo se aplican a esa ejecución:
843
+
844
+ | Opción | Valores |
845
+ | --- | --- |
846
+ | `--test-tts` | Texto entre comillas; obligatorio y no vacío. |
847
+ | `--voice` | `ef_dora`, `em_alex`, `em_santa`. |
848
+ | `--provider` | `kokoro` o `say` (solo macOS). |
849
+ | `--fallback` | `say` (solo macOS) o `none`. Usa `none` para probar exclusivamente Kokoro. |
850
+ | `--speed` | Número de 0.5 a 2; afecta a Kokoro. |
851
+ | `--data-dir` | Carpeta que contiene `youtube-tts.json`. |
852
+
853
+ `Ctrl+C` cancela síntesis y reproducción. El comando termina con código 0 cuando finaliza sin error, o 1 si falla la configuración o la lectura. Evita ejecutarlo a la vez que otra instancia TTS para no superponer voces.
854
+
855
+
856
+ ### Elegir `say` en macOS
857
+
858
+ Puedes usar la voz nativa como proveedor principal, sin cargar Kokoro. Para una ejecución del servicio:
859
+
860
+ ```sh
861
+ YOUTUBE_CHAT_TTS_ENABLED=true TTS_PROVIDER=say npm run start
862
+ ```
863
+
864
+ Para conservar la elección, configura `"ttsProvider": "say"` en `youtube-tts.json`. Para una lectura puntual:
865
+
866
+ ```sh
867
+ npm run start -- --test-tts "Hola mundo" --provider say
868
+ ```
869
+
870
+ Para volver a Kokoro, configura `"ttsProvider": "kokoro"` y elimina cualquier `TTS_PROVIDER=say` exportado. `"ttsFallback": "say"` conserva la voz nativa como respaldo de Kokoro en macOS; `"ttsFallback": "none"` lo deshabilita. `say` no está disponible en Windows/Linux.
871
+
872
+
873
+ ### Cambios de la versión 3.0.0
874
+
875
+ Requiere Node 22 o posterior. Incorpora Kokoro local opcional, selección directa de `say` en macOS, prueba puntual con `--test-tts "texto"` y alias `warhammer-overlay`. El modelo se descarga automáticamente solo al usar Kokoro. Incluye las correcciones de lectura de objetivos primarios y continuidad/autenticación del chat que estaban preparadas en el proyecto.
876
+
877
+
878
+ ### Cambiar estados desde la lista del overlay
879
+
880
+ Haz clic en una unidad en la pantalla de listas para abrir un menú pequeño con **Battleshocked**, **Destruida** y **En reservas**. Cada opción se activa/desactiva de forma independiente; vuelve a pulsarla para quitar el estado. En los grupos de unidades repetidas puedes pulsar una fila individual o el título del grupo y elegir Unidad 1, Unidad 2, etc. Los cambios se guardan y se sincronizan con el administrador y las demás ventanas sin modificar nombres, personajes, notas ni el orden.
881
+
882
+ No hay botones permanentes, resaltado al pasar el ratón ni avisos de guardado en el overlay; se conservan las indicaciones existentes de los estados. El menú se cierra al elegir una opción, pulsar fuera o Escape. También se abre con Enter/Espacio sobre una unidad. En OBS usa **Interactuar** para hacer clic en una fuente de navegador. El menú temporal sí forma parte de la página: si no quieres que se vea en emisión, opera una copia de la misma URL en otra ventana del navegador; los estados se sincronizan con OBS.
883
+
884
+ ## Listas remotas desde los teléfonos (opcional)
885
+
886
+ El mismo repositorio incluye un segundo servicio Node, independiente del overlay. Publica dos páginas móviles fijas: `https://tu-backend/blue` y `https://tu-backend/red`. Los jugadores no necesitan cuenta ni contraseña. El manager habilita/cierra cada lado desde el panel local; ambos empiezan cerrados y vuelven a cerrarse al reiniciar el backend. Al terminar de analizar y guardar una lista, ese lado se cierra automáticamente.
887
+
888
+ ### Ejecutar el backend
889
+
890
+ En el servidor remoto, con Node 22+:
891
+
892
+ ```sh
893
+ npm ci
894
+ # Configura las variables siguientes en el proveedor de hosting, o en un archivo .env privado.
895
+ npm run start:remote-lists
896
+ ```
897
+
898
+ Para cargar un archivo de variables usando Node:
899
+
900
+ ```sh
901
+ node --env-file=/ruta/backend.env remote-lists/start.js
902
+ ```
903
+
904
+ La plantilla versionada es `remote-lists/.env.example`. Cópiala a `.env.dev` para desarrollo local y configura allí las credenciales; `.env.dev` está excluido de Git y del paquete npm. Para cargarla: `node --env-file=.env.dev remote-lists/start.js`. Variables:
905
+
906
+ | Variable | Valor / uso |
907
+ | --- | --- |
908
+ | `OPENAI_API_KEY` | Obligatoria, solo en el backend remoto. Nunca se envía al jugador ni al Mac. |
909
+ | `OPENAI_MODEL` | `gpt-6-luna` por defecto; modelo compatible con Responses API y JSON Schema estricto. |
910
+ | `REMOTE_LISTS_MANAGER_TOKEN` | Obligatoria, al menos 24 caracteres. Protege los botones y la descarga del manager, no los enlaces de los jugadores. |
911
+ | `REMOTE_LISTS_PORT` | 8080; también acepta `PORT` del hosting. |
912
+ | `REMOTE_LISTS_HOST` | `0.0.0.0`; usa `127.0.0.1` para pruebas solo en el Mac. |
913
+ | `REMOTE_LISTS_DEMO` | `false`; `true` devuelve listas de ejemplo sin OpenAI ni consultas a Wahapedia. No lo actives en producción. |
914
+ | `REMOTE_LISTS_DATA_DIR` | `./remote-lists-data`; **debe ser un volumen persistente**. |
915
+ | `REMOTE_LISTS_TOKEN_BUDGET` | 500000 tokens reservados como máximo durante la vida del archivo de estado. |
916
+ | `REMOTE_LISTS_MAX_IMPORTS` | 2 importaciones por lado y apertura; cada importación real puede hacer hasta dos llamadas al LLM. |
917
+ | `REMOTE_LISTS_MAX_OUTPUT_TOKENS` | 4000 tokens por respuesta. |
918
+ | `REMOTE_LISTS_OPEN_MINUTES` | 30 minutos; cierra las subidas abandonadas. |
919
+ | `REMOTE_LISTS_TRUST_PROXY` | `false`; activa solo si el proxy de confianza **sobrescribe** `X-Forwarded-For` con una única IP cliente y el backend no es accesible por otra vía. |
920
+
921
+ Puedes generar la credencial del manager con `node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"` y guardarla como variable del hosting. Los jugadores siguen usando exclusivamente los dos enlaces fijos.
922
+
923
+ Despliega **una sola instancia/réplica**, con disco persistente y HTTPS mediante tu proveedor o un proxy. El estado se guarda en `lists.json` por reemplazo atómico; el bloqueo y la limitación de frecuencia son por proceso. No uses varias réplicas compartiendo ese archivo. `/health` permite comprobar que el proceso responde. El backend no sirve archivos de datos ni credenciales. El script deja el servidor preparado para desplegar; no contrata hosting ni publica una instancia automáticamente.
924
+
925
+ ### Activar en tu overlay local
926
+
927
+ ```sh
928
+ npm run start -- --remote-lists
929
+ # O con el comando instalado:
930
+ wargaming-overlay start --remote-lists
931
+ ```
932
+
933
+ En **Listas de ejército → Listas desde los teléfonos**, guarda la dirección del backend (por ejemplo `https://listas.ejemplo.com`) y la credencial del manager. La conexión se guarda en `remote-lists-config.json` dentro de la carpeta de datos, con permisos de archivo 0600 en sistemas que los soportan. La API local nunca devuelve la credencial guardada. También puedes configurar `REMOTE_LISTS_URL` y `REMOTE_LISTS_MANAGER_TOKEN` como variables del proceso local; al reiniciar, esas variables tienen prioridad sobre el archivo.
934
+
935
+ 1. Copia el enlace azul o rojo y habilita la subida de ese lado.
936
+ 2. El jugador pega el texto exportado de su app y pulsa **Subir lista**. El servidor analiza y guarda automáticamente el resultado; el jugador solo recibe la confirmación de éxito. No se asignan personajes a unidades por mera cercanía en el texto.
937
+ 3. Pulsa **Sincronizar listas**: descarga y aplica inmediatamente las listas guardadas al lado correspondiente, incluyendo facción y destacamentos. Conserva el fondo local y reinicia los estados solo cuando llega una importación nueva. No hay un segundo botón de carga.
938
+
939
+ No sincroniza periódicamente ni necesita abrir puertos en tu Mac. Una lista cargada queda guardada localmente y funciona sin internet. Si la lista local o los jugadores cambian durante la descarga, rechaza la sincronización para no sobrescribirlos: guarda los cambios y vuelve a sincronizar. El panel también impide cargar mientras haya ediciones manuales pendientes. Cargar la misma importación de nuevo no reemplaza estados modificados durante la partida.
940
+
941
+ **Sin `--remote-lists` no aparecen estos controles, no se cargan credenciales remotas y no se conecta al backend.** El overlay continúa como antes. La prueba `--test-tts` y el backend remoto son funciones independientes.
942
+
943
+ ### Límites de uso y gasto
944
+
945
+ - Subida cerrada: se rechaza antes de llamar al LLM. Solo el manager autenticado puede abrirla.
946
+ - Hasta 6000 caracteres de texto, una interpretación activa por lado, cupo de intentos por apertura .
947
+ - Una subida exitosa cierra el lado y bloquea nuevos envíos hasta que el manager lo habilite otra vez. Consultar y sincronizar no llaman al LLM.
948
+ - El modelo, el máximo de salida y el presupuesto los fija el servidor, no el jugador. No se habilitan herramientas, búsquedas ni ejecución de código en la llamada al modelo.
949
+ - Antes de cada llamada se reserva un límite conservador de tokens (bytes UTF-8 del payload completo, instrucciones, esquema, referencias, margen y máximo de salida). Ambas fases de la interpretación se descuentan por separado antes de enviarse. La reserva **no se devuelve**, incluso ante fallo, para no permitir que errores/reintentos eludan el límite. Es un cupo de tokens, no una medición exacta del gasto en dólares. No incluye uso de esa clave en otras aplicaciones.
950
+ - El presupuesto y los intentos se conservan al reiniciar. Abrir de nuevo reinicia el cupo del lado, pero no el presupuesto global. Para un evento nuevo usa una carpeta de datos nueva; no borres el estado durante una importación.
951
+ - Hasta 60 solicitudes públicas por minuto por IP; una sola instancia. Si estás detrás de un proxy sin la configuración de confianza, el límite puede compartirse entre todos los clientes del proxy.
952
+
953
+ Los enlaces son públicos: cualquiera que los conozca puede ocupar un turno mientras esté abierto. Estos controles acotan el gasto, pero no identifican al jugador. El manager puede corregir las listas desde su editor local después de sincronizar. Las llamadas del LLM se validan de nuevo en el servidor y los textos se muestran como texto, no como HTML.
954
+
955
+ La integración se prueba con un LLM simulado, incluyendo gasto, cierres, errores, persistencia y sincronización local. Se requiere una clave real y un hosting configurado para validar una importación con el modelo en producción.
956
+
957
+
958
+ ## Equipo relevante, referencias de 11.ª y pruebas locales
959
+
960
+ El importador real tiene dos fases: extrae unidades y TODO el equipo declarado, identifica la facción y el nombre inglés de referencia, y luego compara las elecciones con las secciones WARGEAR OPTIONS y UNIT COMPOSITION de Wahapedia **exclusivamente en `/wh40k11ed/`**. Conserva en `notes` reemplazos, añadidos opcionales y elecciones entre alternativas (incluida una opción predeterminada cuando existen alternativas); omite equipo fijo sin alternativas. Mantiene las cantidades declaradas; las notas generadas se limitan a elecciones de equipo verificadas. No añade equipo ausente ni comprueba la legalidad de la lista.
961
+
962
+ Las fichas e índices se guardan en `REMOTE_LISTS_DATA_DIR/references-11`. Cada entrada caduca exactamente **7 días desde su descarga**; al necesitarla de nuevo se elimina la entrada vencida y se renueva. No hay renovación en segundo plano. Si la renovación falla, no se utiliza una referencia vencida ni una de 10.ª: deja vacías las notas de equipo que no puede verificar y muestra un aviso para revisión. Las unidades traducidas se buscan mediante el nombre canónico inglés que propone el modelo; si no hay coincidencia exacta, requieren revisión.
963
+
964
+ Se descargan como máximo 16 fichas distintas por importación, reutilizando fichas para unidades repetidas; el contexto de fuentes se limita a 24 KB. La descarga no usa búsquedas de OpenAI ni concede navegación libre al LLM. La segunda llamada, cuando hay referencias, utiliza solo los fragmentos pertinentes y su consumo se incluye en el cupo persistente de tokens. Si faltan referencias, se muestra un aviso en el manager al sincronizar y no se publica equipo sin verificar basándose en la memoria del modelo. Las notas del manager admiten 160 caracteres; el manager puede corregir los resúmenes en el editor local.
965
+
966
+ ### Prueba completa sin clave ni gasto
967
+
968
+ Desde la raíz del repositorio, instala las dependencias con `npm ci`. Si aún no tienes `.env.dev`, créalo a partir de la plantilla (no sobrescribas uno existente):
969
+
970
+ ```sh
971
+ cp remote-lists/.env.example .env.dev
972
+ ```
973
+
974
+ Para probar solo en tu computadora, configura `REMOTE_LISTS_HOST=127.0.0.1` en ese archivo. Después abre dos terminales:
975
+
976
+ ```sh
977
+ # Terminal 1: backend DEMO. Carga .env.dev, pero no usa su clave OpenAI.
978
+ npm run dev:remote-lists
979
+
980
+ # Terminal 2: tu overlay local habitual, con los nuevos controles.
981
+ npm run start -- --remote-lists
982
+ ```
983
+
984
+ 1. Abre `http://127.0.0.1:8765` y entra en **Listas de ejército**.
985
+ 2. Guarda la conexión `http://127.0.0.1:8080` y el valor `REMOTE_LISTS_MANAGER_TOKEN` de `.env.dev`. Si está vacío, el script demo usa `demo-manager-token-local-only`. Solo es una credencial de prueba: para producción configura una propia de al menos 24 caracteres.
986
+ 3. Pulsa **Habilitar subida azul**. Abre `http://127.0.0.1:8080/blue` en otra pestaña; para rojo usa `/red` y habilita ese lado.
987
+ 4. Pega `remote-lists/examples/demo-list.txt`. Pulsa **Subir lista** y espera el mensaje de éxito.
988
+ 5. Vuelve al panel: pulsa **Sincronizar listas**. Abre la lista para OBS y comprueba nombres y comentarios.
989
+
990
+ El modo demo está identificado en la página y devuelve una respuesta predefinida. Prueba análisis, guardado automático, cierre y sincronización; **no demuestra la calidad de la interpretación ni consulta fichas reales**. `npm run dev:remote-lists` fuerza el modo demo aunque `.env.dev` diga `REMOTE_LISTS_DEMO=false`.
991
+
992
+ ### Prueba real con OpenAI y Wahapedia
993
+
994
+ Detén el backend demo. En `.env.dev`, sustituye `OPENAI_API_KEY` por tu clave, configura la credencial del manager y deja `REMOTE_LISTS_DEMO=false`. Inicia:
995
+
996
+ ```sh
997
+ node --env-file=.env.dev remote-lists/start.js
998
+ ```
999
+
1000
+ Repite el mismo flujo con una lista real de 11.ª. Esta prueba sí usa internet y consume tokens. Usa una clave con permisos de escritura para Responses API y acceso al modelo. Si falta una ficha o Wahapedia no responde, el resultado mostrará qué equipo necesita revisión. El coste y la calidad reales del LLM no se han verificado sin una clave real.
1001
+
1002
+ ### Probar desde un teléfono físico
1003
+
1004
+ Teléfono y Mac deben estar en la misma Wi-Fi. Cambia `REMOTE_LISTS_HOST=0.0.0.0` en `.env.dev`, reinicia el backend y abre en el teléfono `http://IP_LOCAL_DEL_MAC:8080/blue` o `/red`. Puedes consultar la IP en los ajustes de Wi-Fi de macOS. El panel local sigue conectándose a `http://127.0.0.1:8080`; sus enlaces localhost deben sustituirse por la IP del Mac para esta prueba. Si macOS pide permiso para conexiones entrantes de Node, habilítalo para la prueba. No requiere abrir puertos del router. HTTP y credenciales ficticias son solo para desarrollo; en hosting usa HTTPS.
1005
+
1006
+ La página funciona en teléfono y computadora, con entradas de 16 px y botones de al menos 44 px. El jugador pega el texto y pulsa **Subir lista**: aparece un indicador de carga y, al guardar, únicamente el mensaje de éxito. Si falla, conserva el texto para reintentar. No hay vista previa, editor ni paso de confirmación.
1007
+
1008
+ ### Verificación automática
1009
+
1010
+ ```sh
1011
+ npm test
1012
+ node --test test/wargear-references.test.js test/remote-lists.test.js
1013
+ ```
1014
+
1015
+ Las pruebas verifican caducidad exacta a los siete días, rechazo de referencias/redirects de 10.ª, omisión del equipo sin referencia, aceptación de fichas sin opciones, exclusión del equipo fijo y comparación con índices de unidades y cobro de ambas fases antes de enviar peticiones. También comprueban presupuestos, autenticación del manager, subidas cerradas, persistencia, guardado automático y protección de listas locales.
1016
+
1017
+ ### Facción, destacamentos, líderes y notas
1018
+
1019
+ El prompt recibe el catálogo de facciones del manager. La facción y los destacamentos se extraen automáticamente y al sincronizar se asignan al jugador correspondiente. El manager puede corregirlos en su panel. Si no se pudo identificar un dato, no se inventa. Para listas confirmadas antes de esta función, vuelve a subir la lista para incorporar esos campos.
1020
+
1021
+ Los personajes con una asignación explícita se incorporan a `characters` de la unidad liderada y se muestran juntos en una tarjeta, conservando mejoras y notas. El título muestra todos los líderes y apoyos unidos por «+», y debajo aparece el nombre de la unidad liderada. Si la exportación no declara la asignación, los personajes se conservan separados y el manager puede asignarlos en su editor local. No se deduce la asociación por proximidad en el texto.
1022
+
1023
+ Las notas generadas usan cantidades y nombres breves, un elemento por línea, con un máximo total de 160 caracteres. El overlay presenta cada línea como un bullet; también admite notas antiguas separadas por punto y coma. Si las notas combinadas de un líder y su unidad exceden el límite, el resultado se recorta y muestra un aviso para revisión.
1024
+
1025
+ La página del jugador muestra un indicador de carga durante el análisis, lo retira tanto al terminar como ante un error y mantiene el botón deshabilitado para evitar envíos duplicados.
1026
+
1027
+ ### Evitar equipo obligatorio en los comentarios
1028
+
1029
+ La extracción inicial conserva el equipo declarado internamente en `wargear`, pero deja `notes` vacío. Las fichas de 11.ª con UNIT COMPOSITION y sin WARGEAR OPTIONS se aceptan como equipo fijo: no generan notas ni necesitan una segunda llamada al LLM. Esto también se aplica a los líderes y apoyos antes de agruparlos con su unidad. Por ejemplo, las armas obligatorias de Belisarius Cawl y Cybernetica Datasmith no aparecen en el overlay.
1030
+
1031
+ Solo las fichas con opciones pasan a la comparación de elecciones. Si falta una ficha, caduca y no se puede renovar, o falta un resultado de comparación, las notas permanecen vacías y aparece un aviso en el manager al sincronizar. No se usa el listado completo de armas como respaldo. Las listas ya confirmadas no se reescriben: después de desplegar el backend actualizado, habilita la subida y vuelve a subir y sincronizar la lista.
1032
+
1033
+ Las apariciones repetidas de la misma arma se suman antes de comparar las elecciones (dos líneas `1x Heavy reaper autocannon` se convierten en `2× Heavy reaper autocannon`). Las cantidades del resumen se contrastan con ese inventario; los calificadores de miniatura se mantienen separados. La comparación recibe también el texto original para comprobar la extracción y debe incluir las armas predeterminadas reemplazables, no solo las sustituciones.
1034
+
1035
+ La pantalla de listas llena las columnas de arriba abajo, empezando por la izquierda; pasa a la siguiente cuando ya no cabe otra tarjeta completa. Mantiene cada grupo unido y ajusta la tipografía para mostrar la lista completa.
1036
+
1037
+ El orden visual predeterminado es: personajes épicos, personajes, unidades lideradas, unidades restantes y, al final, vehículos y monstruos. Dentro de cada categoría se conserva el orden original. Las unidades con líderes o apoyos asignados se muestran como grupo liderado. El importador genera `category` (`epic_character`, `character`, `unit`, `vehicle`, `monster`) y el manager permite cambiarla en **Tipo de unidad**. Las listas antiguas sin categoría se consideran unidades normales: vuelve a subirlas para clasificarlas automáticamente o asigna el tipo en el editor. El orden guardado del JSON no cambia.
1038
+
1039
+ ### Cambios de la versión 3.1.0
1040
+
1041
+ Listas remotas opcionales con subida en un paso, sincronización directa, facción y destacamentos automáticos, agrupación de líderes y apoyos, referencias de 11.ª con caché de siete días y notas limitadas a elecciones de equipo. Conserva cantidades repetidas, omite equipo obligatorio o sin verificar y controla el consumo del backend. Las listas se distribuyen llenando primero la izquierda y se ordenan por personajes épicos, personajes, unidades lideradas, otras unidades y vehículos/monstruos. Incluye edición de estados desde el overlay.
1042
+
1043
+ En Railway configura `ONNXRUNTIME_NODE_INSTALL=skip` para evitar descargar componentes GPU del TTS que el backend de listas no utiliza.
@@ -5,3 +5,11 @@ Los PNG de `public/assets/` proceden del overlay local del usuario. Este proyect
5
5
  Antes de publicar el paquete, el titular debe revisar los recursos y añadir las atribuciones/licencias pertinentes o reemplazarlos por recursos redistribuibles. La licencia **0BSD** del código no concede derechos sobre imágenes de terceros.
6
6
 
7
7
  Los nombres de facciones se usan como referencias descriptivas. El paquete no declara afiliación oficial.
8
+
9
+
10
+ ## TTS local
11
+
12
+ - Kokoro v1.0 y recursos ONNX: Apache-2.0, https://huggingface.co/onnx-community/Kokoro-82M-v1.0-ONNX (revisión 1939ad2a8e416c0acfeecc08a694d14ef25f2231).
13
+ - ONNX Runtime: MIT, https://github.com/microsoft/onnxruntime.
14
+ - node-cpal: ISC, https://github.com/saeta-eth/node-cpal.
15
+ - eSpeak NG Emscripten: GPL-3.0, https://github.com/echogarden-project/espeak-ng-emscripten; fuente del motor: https://github.com/echogarden-project/espeak-ng. Se distribuye como dependencia npm y se usa para fonetizar español. Su licencia se incluye en `tts/licenses/espeak-ng-COPYING`; la licencia 0BSD del código propio no reemplaza las licencias de las dependencias.
package/battle_reader.py CHANGED
@@ -48,6 +48,9 @@ def parse(xml):
48
48
  if not details and label and i+2 < len(texts) and re.match(r'\d+VP', texts[i+1]) and texts[i+2].isdigit():
49
49
  details = (None, t, texts[i+1])
50
50
  counter_index = i+2
51
+ elif not details and label and i+2 < len(texts) and texts[i+1].isdigit() and re.match(r'\d+VP', texts[i+2]):
52
+ details = (None, t, texts[i+2])
53
+ counter_index = i+1
51
54
  elif not details and label and i>0 and i+1<len(texts) and re.match(r'\d+VP',texts[i-1]) and texts[i+1].isdigit():
52
55
  details = (None, t, texts[i-1])
53
56
  counter_index = i+1
@@ -91,7 +94,7 @@ def parse(xml):
91
94
  players[owner]['secondariesComplete'] = True
92
95
  if players[owner]['secondaries'] is None:
93
96
  players[owner]['secondaries'] = []
94
- return dict(round=rnd, half=None, players=players)
97
+ return dict(round=rnd, half=None, players=players, primarySchemaVersion=1)
95
98
 
96
99
  def merge(previous, fresh):
97
100
  now = time.time()
package/cli.js CHANGED
@@ -8,6 +8,7 @@ const help=`wargaming-overlay ${version} — overlay local para OBS
8
8
 
9
9
  wargaming-overlay start Inicia el overlay y lector
10
10
  wargaming-overlay youtube-auth --client-secret RUTA Autoriza YouTube una vez
11
+ wargaming-overlay --test-tts "Hola" Lee un texto y termina
11
12
  wargaming-overlay doctor Revisa Python, ADB y teléfono
12
13
  wargaming-overlay setup Guía de configuración
13
14
  wargaming-overlay setup --install-adb Instala ADB con el gestor del sistema
@@ -15,6 +16,7 @@ const help=`wargaming-overlay ${version} — overlay local para OBS
15
16
  wargaming-overlay migrate CARPETA Copia el estado del overlay anterior
16
17
 
17
18
  Opciones de start/doctor:
19
+ --remote-lists Habilita enlaces y sincronización de listas remotas
18
20
  --port 8765 Puerto local (1024–65535); no necesita sudo
19
21
  --serial SERIAL Selecciona un dispositivo
20
22
  --interval 3 Pausa entre lecturas (segundos, mínimo 1)
@@ -22,22 +24,38 @@ Opciones de start/doctor:
22
24
  --python RUTA Python 3.8 o posterior
23
25
  --data-dir RUTA Carpeta para guardar configuración y partida
24
26
 
27
+ Opciones de prueba de voz:
28
+ --test-tts "TEXTO" Texto que se leerá (obligatorio)
29
+ --voice NOMBRE ef_dora, em_alex o em_santa
30
+ --provider NOMBRE kokoro o say (say solo macOS)
31
+ --fallback NOMBRE say o none
32
+ --speed NUMERO Velocidad entre 0.5 y 2
33
+ --data-dir RUTA Lee la configuración TTS de esa carpeta
34
+
25
35
  Por defecto: ~/Library/Application Support/wargaming-overlay en macOS;
26
36
  %LOCALAPPDATA%/wargaming-overlay en Windows; ~/.local/share/wargaming-overlay en Linux. Solo escucha en 127.0.0.1.
27
37
  Ctrl+C detiene el servidor y lector. El último estado queda guardado.
28
38
  `;
29
- async function main(){
30
- const args=process.argv.slice(2);
39
+ async function main(argv=process.argv.slice(2),{voiceTest}={}){
40
+ const args=[...argv];
31
41
  if(args.includes('--help')||args.includes('-h')){console.log(help);return}
32
42
  if(args.includes('--version')){console.log(version);return}
33
- const command=args.shift()||'start',options={};let source;
34
- const keys={'--port':'port','--serial':'serial','--interval':'interval','--adb':'adb','--python':'python','--data-dir':'dataDir','--client-secret':'clientSecret'};
35
- while(args.length){const a=args.shift();if(a==='--install-adb')options.install=true;else if(a==='--dry-run')options.dryRun=true;else if(keys[a]){const value=args.shift();if(!value||value.startsWith('--'))throw Error(`Falta el valor de ${a}`);options[keys[a]]=value}else if(command==='migrate'&&!source&&!a.startsWith('-'))source=a;else throw Error(`Argumento desconocido: ${a}. Usa --help.`)}
36
- if(!['start','doctor','setup','migrate','youtube-auth'].includes(command))throw Error('Comando desconocido. Usa --help.');
43
+ let command=args[0]&&!args[0].startsWith('-')?args.shift():'start';
44
+ const options={};let source;
45
+ const keys={'--port':'port','--serial':'serial','--interval':'interval','--adb':'adb','--python':'python','--data-dir':'dataDir','--client-secret':'clientSecret','--test-tts':'testText','--text':'text','--voice':'voice','--provider':'provider','--fallback':'fallback','--speed':'speed'};
46
+ while(args.length){const a=args.shift();if(a==='--remote-lists')options.remoteLists=true;else if(a==='--install-adb')options.install=true;else if(a==='--dry-run')options.dryRun=true;else if(keys[a]){const value=args.shift();if(value===undefined||(!['--text','--test-tts'].includes(a)&&(!value||value.startsWith('--'))))throw Error(`Falta el valor de ${a}`);options[keys[a]]=value}else if(command==='migrate'&&!source&&!a.startsWith('-'))source=a;else throw Error(`Argumento desconocido: ${a}. Usa --help.`)}
47
+ if(options.remoteLists&&command!=='start')throw Error('--remote-lists solo se usa con start.');
48
+ if(options.testText!==undefined){if(command!=='start')throw Error('--test-tts solo se usa sin subcomando o con start.');command='test-tts';options.text=options.testText;delete options.testText;}
49
+ if(!['start','doctor','setup','migrate','youtube-auth','test-tts'].includes(command))throw Error('Comando desconocido. Usa --help.');
50
+ for(const key of ['text','voice','provider','fallback','speed'])if(options[key]!==undefined&&command!=='test-tts')throw Error(`--${key} solo se usa con test-tts.`);
37
51
  if(options.clientSecret && command!=='youtube-auth')throw Error('--client-secret solo se usa con youtube-auth.');
38
52
  if(command==='youtube-auth'){await new (require('./youtube/auth').Auth)().authorize(options.clientSecret);return;}
39
53
  const dataDir=path.resolve(options.dataDir||dataDirectory());
40
54
  if((options.install||options.dryRun)&&command!=='setup')throw Error('--install-adb y --dry-run solo se usan con setup.');
55
+ if(command==='test-tts'){
56
+ for(const key of ['port','serial','interval','adb','python'])if(options[key]!==undefined)throw Error(`--${key} no se usa con test-tts.`);
57
+ await (voiceTest||require('./tts/voice-command').testVoice)(options.text,{...options,dataDir});return;
58
+ }
41
59
  if(command==='migrate'){
42
60
  if(!source)throw Error('Indica la carpeta anterior: wargaming-overlay migrate /ruta/al/overlay-anterior');
43
61
  const copies=['overlay-state.json','game-state.json'].filter(f=>fs.existsSync(path.join(source,f)));
@@ -75,7 +93,9 @@ async function main(){
75
93
  const r=check(options);if(!r.adb||!r.python||['PYTHON_VERSION','PYTHON_MISSING'].includes(r.code))throw Error(r.message);
76
94
  if(!r.ok)console.error(r.message+'\nEl panel permanecerá disponible mientras conectas el teléfono.');
77
95
  fs.mkdirSync(dataDir,{recursive:true});fs.accessSync(dataDir,fs.constants.W_OK);
96
+ process.env.OVERLAY_REMOTE_LISTS_ENABLED=options.remoteLists?'true':'false';
78
97
  process.env.OVERLAY_DATA_DIR=dataDir;process.env.PORT=String(port);process.env.OVERLAY_ADB=r.adb;process.env.OVERLAY_PYTHON=r.python;process.env.OVERLAY_SERIAL=options.serial||'';process.env.OVERLAY_INTERVAL=String(interval);
79
98
  require('./server');
80
99
  }
81
- main().catch(e=>{console.error('Error: '+e.message);process.exitCode=1;});
100
+ if(require.main===module)main().catch(e=>{console.error('Error: '+e.message);process.exitCode=1;});
101
+ module.exports={main};
package/game-store.js CHANGED
@@ -7,7 +7,8 @@ function mergeGame(previous,incoming){
7
7
  const same=samePlayers && previous.round===incoming.round;
8
8
  const advance=samePlayers && incoming.round===previous.round+1 && incoming.round>(previous.turn?.highestRound??previous.round);
9
9
  const players=incoming.players.map(p=>{
10
- const prior=samePlayers?previous.players.find(old=>old.name===p.name):null;
10
+ let prior=samePlayers?previous.players.find(old=>old.name===p.name):null;
11
+ if(prior&&incoming.primarySchemaVersion===1&&previous.primarySchemaVersion!==1)prior={...prior,primaryObjectives:null};
11
12
  const old=same?prior:advance?{
12
13
  // Primary/secondary totals are cumulative; objective progress belongs to the round.
13
14
  cp:prior.cp,primary:prior.primary,secondary:prior.secondary,
package/package.json CHANGED
@@ -1,19 +1,23 @@
1
1
  {
2
2
  "name": "@ing.jorgeu/wargaming-overlay",
3
- "version": "2.5.0",
3
+ "version": "3.1.0",
4
4
  "description": "Overlay local para OBS con lectura Android por ADB",
5
5
  "bin": {
6
- "wargaming-overlay": "cli.js"
6
+ "wargaming-overlay": "cli.js",
7
+ "warhammer-overlay": "cli.js"
7
8
  },
8
9
  "scripts": {
9
10
  "start": "node cli.js start",
10
11
  "test": "node --test",
11
12
  "prepublishOnly": "npm test",
12
13
  "facts:coverage": "node scripts/fact-coverage.js",
13
- "test-tts": "node scripts/tts-demo.js"
14
+ "test-tts": "node scripts/tts-demo.js",
15
+ "setup-tts": "node scripts/setup-kokoro.js",
16
+ "start:remote-lists": "node remote-lists/start.js",
17
+ "dev:remote-lists": "node --env-file=.env.dev remote-lists/dev.js"
14
18
  },
15
19
  "engines": {
16
- "node": ">=20"
20
+ "node": ">=22"
17
21
  },
18
22
  "files": [
19
23
  "cli.js",
@@ -22,6 +26,7 @@
22
26
  "reader.py",
23
27
  "battle_reader.py",
24
28
  "game-store.js",
29
+ "rosters.js",
25
30
  "reader-status.js",
26
31
  "turn-tracker.js",
27
32
  "public",
@@ -38,7 +43,9 @@
38
43
  "device_connection.py",
39
44
  "youtube",
40
45
  "tts",
41
- "scripts/tts-demo.js"
46
+ "scripts/tts-demo.js",
47
+ "scripts/setup-kokoro.js",
48
+ "remote-lists"
42
49
  ],
43
50
  "license": "0BSD",
44
51
  "publishConfig": {
@@ -57,7 +64,13 @@
57
64
  "cli"
58
65
  ],
59
66
  "dependencies": {
67
+ "@echogarden/espeak-ng-emscripten": "0.3.5",
60
68
  "@grpc/grpc-js": "^1.14.5",
61
- "@grpc/proto-loader": "^0.8.1"
69
+ "@grpc/proto-loader": "^0.8.1",
70
+ "onnxruntime-node": "1.30.0",
71
+ "onnxruntime-web": "1.30.0"
72
+ },
73
+ "optionalDependencies": {
74
+ "node-cpal": "1.0.0"
62
75
  }
63
76
  }