# Burbujas y modelos locales

> Haz que cualquier unidad del juego muestre burbujas de diálogo con cualquier identidad; conecta un LLM local y cada frase que entra recibe una respuesta sobre la cabeza de la unidad.

Fuente: https://war3ai.com/es/docs/speech/

Las burbujas son una capa para el espectador: no afectan al resultado y sirven para directos, comentarios y depuración.

- Cualquier unidad puede hablar con cualquier identidad; varias unidades pueden hablar a la vez;
- El tamaño de letra, el color, el ancho, la cola, la opacidad y la velocidad de escritura de cada burbuja se pueden personalizar por separado;
- Se conecta directamente a un LLM local (LM Studio) con salida en streaming: la burbuja se actualiza mientras se genera el texto.

## Desde un Bot

Lo más sencillo es el `say` que incluye el SDK:

```python
g.say(hero, "¡Conmigo, a la carga!", seconds=4)
```

## Arranque e interfaz

**Lo más cómodo: el "Centro de control" de la página de inicio de Farsight**. Primero pulsa "LLM local → Iniciar y cargar el modelo" (el servicio local de LM Studio + cargar en la VRAM el modelo configurado) y después "Burbujas de diálogo → Iniciar". En las tarjetas puedes ver el log, detener y reiniciar.

La interfaz está en la página "Burbujas de diálogo" de la barra lateral izquierda de Farsight: hacer hablar a unidades (elegir la unidad, escribir el texto, ajustar el estilo, conversar con el modelo), tertulia de campesinos, diálogos de cámara, reacciones a la partida y ajustes del modelo; todo actúa sobre la instancia elegida en la barra superior.

También desde la línea de comandos:

```bash
python speech/speak_launch.py              # arranca el servicio del modelo local + carga y precalienta el modelo + arranca la API de burbujas
python speech/speak_launch.py --restart    # reinicia la API tras cambiar el código
python speech/speak_launch.py --stop       # detiene la API y descarga el modelo de la VRAM
```

Cada paso es "si ya está, se omite", así que ejecutarlo varias veces no tiene efectos secundarios.

## API HTTP

Por defecto, `http://127.0.0.1:8872/` (el puerto está en `ports.speech` de `openwar3.json`); cualquier programa puede llamarla.

### Hacer hablar a una unidad `POST /api/say`

```json
{
  "inst": 16,
  "bubbles": [
    { "unit": "0x14A12614", "name": "Rey de la Montaña", "text": "¡Conmigo, a la carga!" },
    { "unit": "0x14A12924", "name": "Archimago", "text": "Yo lanzo la Ventisca.",
      "style": { "font_px": 26, "text_color": "#FFE080", "bg_color": "#C0102040" } },
    { "screen": [960, 110], "key": 1, "name": "Narrador", "text": "La primera oleada de orcos llega en 30 segundos.",
      "style": { "tail": false, "type_ms": 0 } },
    { "world": [-4684, 2644], "key": 2, "text": "Punto de reunión", "style": { "font_px": 16 } }
  ]
}
```

| Campo | Descripción |
|---|---|
| `unit` / `world` / `screen` | Uno de los tres: sigue a una unidad (si tiene barra de vida, justo encima de ella) / coordenadas del mapa / píxeles de pantalla (para el narrador) |
| `name` | Quién habla, en la primera línea; texto libre, no tiene por qué ser esa unidad |
| `text` | Texto, con salto de línea automático |
| `duration_ms` | Cuánto tiempo se muestra; 0 = automático, 3 ~ 5 segundos |
| `key` | Número de una burbuja de mundo / pantalla; un mensaje nuevo con la misma key sustituye al anterior |
| `update` | Si ya existe esa burbuja, solo cambia el texto sin reiniciar el temporizador (para streaming) |
| `style` | `font_px`, `max_width_px`, `text_color`, `bg_color`, `border_color`, `tail`, `side_px`, `opacity`, `type_ms`, `font`… |

Como máximo 32 burbujas a la vez; el coste medio por fotograma es de unos 0.1 ~ 0.2 ms.

### Conversar con un modelo local `POST /api/chat`

```json
{
  "inst": 16, "unit": "0x14A12614", "name": "Rey de la Montaña",
  "persona": "Eres Muradin Barbabronce, el Rey de la Montaña de Warcraft: campechano y amante de la cerveza. Responde con una o dos frases coloquiales, de no más de 40 caracteres.",
  "message": "Hay un grupo de ogros delante, ¿cargamos o no?",
  "stream": true
}
```

Devuelve `{"reply": "...", "first_token_ms": 283, "total_ms": 342}`, y para entonces la respuesta ya está sobre la cabeza de esa unidad. Cada unidad recuerda las últimas 6 rondas de conversación.

### Otros

| Endpoint | Descripción |
|---|---|
| `GET /api/instances` | Partidas en curso |
| `GET /api/units?inst=16&mine=true&heroes=true` | Lista de unidades (con nombre en chino, coordenadas y vida) |
| `POST /api/clear` | Borra una burbuja o todas |
| `GET /api/llm`, `POST /api/llm` | Ver / cambiar la configuración del modelo (`base_url`, `model`, `max_tokens`, `temperature`) |
| `POST /api/banter` | Tertulia de campesinos: los trabajadores de la base se quejan por turnos según su personaje, con una presentación al inicio (todos los datos de la partida son reales) |
| `POST /api/camtalk` | Diálogos de cámara: los héroes y sus acompañantes que salen en cámara conversan según su identidad |
| `POST /api/events` | Reacciones a la partida: empieza un combate, termina, cae un héroe, se sube de tier, destruyen un edificio… solo hablan cuando pasa algo |

## Cómo elegir un modelo local

Medido en una RTX 5090 (5 frases de diálogo del juego):

| Modelo | VRAM | Velocidad | Una respuesta | Conclusión |
|---|---|---|---|---|
| **Qwen3.6-35B-A3B** (MoE, solo 3B activos por paso), Q4, sin razonamiento | 20.6 GB | Aprox. 142 token/s | **Aprox. 0.3 s** (primer token en aprox. 0.27 s) | Recomendado: rápido y con un rol natural en chino |
| gpt-oss-20b (MXFP4), razonamiento low | 11.3 GB | Aprox. 280 token/s | 0.3 ~ 0.8 s | Si vas justo de VRAM; en chino, algo plano |
| Qwen3.6-27B (denso), Q4 | 17.2 GB | Aprox. 39 token/s | A los 5.5 s seguía pensando | No sirve para diálogo en tiempo real |

- **La velocidad depende de los "parámetros activos por paso", no del total**: el MoE de 35B solo activa 3B y es 3 ~ 4 veces más rápido que el denso de 27B.
- **Hay que desactivar el "razonamiento"**: si no, todos los tokens se van en pensar y no responde nada.
- Las burbujas escriben unos 22 caracteres por segundo, así que la velocidad de generación ya no es el cuello de botella; lo que de verdad marca la experiencia es la **latencia del primer token**.

> **Que los diálogos parezcan reales**
>
> Pasa siempre al modelo datos reales de la partida (partidas jugadas, victorias y derrotas, tropas, recursos) y dile claramente "usa solo estos hechos". En las pruebas, sin esa restricción, el modelo se inventaba combates que nunca ocurrieron.
