# Balões de fala e modelos locais

> Faça qualquer unidade do jogo exibir um balão de fala sobre a cabeça, com qualquer identidade; conecte um LLM local e cada frase que entra vira uma resposta sobre a unidade.

Fonte: https://war3ai.com/pt/docs/speech/

Os balões são uma camada visual: não afetam o resultado da partida e servem para lives, narração e depuração.

- Qualquer unidade fala, com qualquer identidade; várias unidades podem falar ao mesmo tempo;
- tamanho da fonte, cor, largura, rabicho, transparência e velocidade de digitação podem ser personalizados em cada balão;
- conecta direto a um LLM local (LM Studio), com saída em streaming: o balão é atualizado enquanto o texto é gerado.

## Usando no Bot

O jeito mais simples é o `say` que vem no SDK:

```python
g.say(hero, "Sigam-me, ataquem!", seconds=4)
```

## Inicialização e interface

**O jeito mais fácil: página inicial do Farsight, “Central de controle”** — primeiro clique em “LLM local → Iniciar e carregar modelo” (o servidor local do LM Studio + carregar na VRAM o modelo configurado) e depois em “Balões de fala → Iniciar”. No cartão dá para ver os logs, parar e reiniciar.

A interface fica na página “Balões de fala”, na barra lateral esquerda do Farsight: fazer uma unidade falar (escolher a unidade, escrever o texto, ajustar o estilo, conversar com o modelo), roda de conversa dos camponeses, diálogo na câmera, gatilhos da partida e configurações do modelo; tudo atua na instância escolhida na barra superior.

Também dá para usar a linha de comando:

```bash
python speech/speak_launch.py              # inicia o servidor do modelo local + carrega e aquece o modelo + inicia a API de balões
python speech/speak_launch.py --restart    # reinicia a API depois de alterar o código
python speech/speak_launch.py --stop       # para a API e descarrega o modelo da VRAM
```

Cada etapa segue a regra “se já existe, pula”, então rodar de novo não tem efeito colateral.

## API HTTP

Padrão: `http://127.0.0.1:8872/` (a porta fica em `ports.speech`, no `openwar3.json`); qualquer programa pode chamá-la.

### Fazer uma unidade falar `POST /api/say`

```json
{
  "inst": 16,
  "bubbles": [
    { "unit": "0x14A12614", "name": "Rei da Montanha", "text": "Sigam-me, ataquem!" },
    { "unit": "0x14A12924", "name": "Arquimago", "text": "Deixa que eu lanço a Nevasca.",
      "style": { "font_px": 26, "text_color": "#FFE080", "bg_color": "#C0102040" } },
    { "screen": [960, 110], "key": 1, "name": "Narrador", "text": "A primeira leva de orcs chega em 30 segundos.",
      "style": { "tail": false, "type_ms": 0 } },
    { "world": [-4684, 2644], "key": 2, "text": "Ponto de encontro", "style": { "font_px": 16 } }
  ]
}
```

| Campo | Descrição |
|---|---|
| `unit` / `world` / `screen` | Escolha um: segue a unidade (se ela tiver barra de vida, fica logo acima dela) / coordenada do mapa / pixel da tela (para narração) |
| `name` | Quem fala, exibido na primeira linha; pode ser qualquer texto, não precisa ser esta unidade |
| `text` | O texto, com quebra de linha automática |
| `duration_ms` | Por quanto tempo fica visível; 0 = automático, 3 ~ 5 segundos |
| `key` | Identificador de balões de mundo / tela; uma mensagem nova com a mesma key substitui a antiga |
| `update` | Se o mesmo balão já existir, troca só o texto, sem reiniciar o tempo (para streaming) |
| `style` | `font_px`, `max_width_px`, `text_color`, `bg_color`, `border_color`, `tail`, `side_px`, `opacity`, `type_ms`, `font`… |

No máximo 32 balões ao mesmo tempo; o custo médio é de cerca de 0.1 ~ 0.2 ms por frame.

### Conversar com o modelo local `POST /api/chat`

```json
{
  "inst": 16, "unit": "0x14A12614", "name": "Rei da Montanha",
  "persona": "Você interpreta Muradin, o Rei da Montanha de Warcraft, expansivo e bom de copo. Uma ou duas frases coloquiais, no máximo 40 palavras.",
  "message": "Tem um bando de ogros ali na frente. Vamos atacar?",
  "stream": true
}
```

Retorna `{"reply": "...", "first_token_ms": 283, "total_ms": 342}`, e a resposta já aparece sobre aquela unidade ao mesmo tempo. Cada unidade lembra as últimas 6 rodadas de conversa.

### Outros

| API | Descrição |
|---|---|
| `GET /api/instances` | Jogos em execução |
| `GET /api/units?inst=16&mine=true&heroes=true` | Lista de unidades (com nome em chinês, coordenadas, vida) |
| `POST /api/clear` | Remove um balão ou todos |
| `GET /api/llm`, `POST /api/llm` | Ver / alterar a configuração do modelo (`base_url`, `model`, `max_tokens`, `temperature`) |
| `POST /api/banter` | Roda de conversa dos camponeses: os trabalhadores da base se revezam reclamando conforme a persona de cada um, com abertura narrada (todos os dados da partida são reais) |
| `POST /api/camtalk` | Diálogo na câmera: os heróis e seguidores em cena conversam conforme seus papéis |
| `POST /api/events` | Gatilhos da partida: início de combate, fim de combate, herói morto, subida de tier, construção destruída… só fala quando algo acontece |

## Como escolher o modelo local

Testado numa RTX 5090 (5 falas de jogo):

| Modelo | VRAM | Velocidade | Uma resposta | Conclusão |
|---|---|---|---|---|
| **Qwen3.6-35B-A3B** (MoE, só 3B ativos por vez), Q4, raciocínio desligado | 20.6 GB | cerca de 142 token/s | **cerca de 0.3 s** (primeiro token em cerca de 0.27 s) | Recomendado: rápido, roleplay natural em chinês |
| gpt-oss-20b (MXFP4), raciocínio low | 11.3 GB | cerca de 280 token/s | 0.3 ~ 0.8 s | Para quando a VRAM é curta; chinês um pouco sem graça |
| Qwen3.6-27B (denso), Q4 | 17.2 GB | cerca de 39 token/s | Ainda pensando aos 5.5 s | Não serve para diálogo em tempo real |

- **A velocidade depende dos “parâmetros ativos por vez”, não do total**: o MoE de 35B ativa só 3B e é 3 ~ 4 vezes mais rápido que o denso de 27B.
- **É preciso desligar o “raciocínio” (thinking)**: senão, todos os tokens vão para o raciocínio e nenhuma palavra sai na resposta.
- O balão digita cerca de 22 caracteres por segundo, então a velocidade de geração já não é o gargalo; o que realmente afeta a experiência é a **latência do primeiro token**.

> **Falas que soam reais**
>
> Passe ao modelo sempre dados reais da partida (número de partidas, vitórias e derrotas, tropas, estoque) e diga explicitamente “use apenas estes fatos”. Nos testes, sem essa restrição, o modelo inventava batalhas que nunca aconteceram.
