# Реплики и локальные модели

> Любой юнит в игре может заговорить от любого имени — в облачке над головой. Подключите локальную LLM: фраза на входе, ответ появляется над юнитом.

Источник: https://war3ai.com/ru/docs/speech/

Облачки — зрелищный слой: на исход игры они не влияют и подходят для стримов, комментирования и отладки.

- говорить может любой юнит от любого имени; несколько юнитов могут говорить одновременно;
- размер шрифта, цвет, ширина, хвостик, прозрачность и скорость печати настраиваются для каждого облачка отдельно;
- можно напрямую подключить локальную LLM (LM Studio) с потоковым выводом: облачко обновляется по мере генерации.

## Из бота

Проще всего — встроенный в SDK метод `say`:

```python
g.say(hero, "За мной, в атаку!", seconds=4)
```

## Запуск и интерфейс

**Проще всего — главная страница Farsight «Центр управления»**: сначала нажмите «Локальная LLM → Запустить и загрузить модель» (локальный сервер LM Studio + загрузка настроенной модели в видеопамять), затем «Облачка реплик → Запустить». На карточках можно смотреть логи, останавливать и перезапускать.

Интерфейс — на странице «Облачка реплик» в левой панели Farsight: заставить юнита говорить (выбрать юнита, написать текст, настроить стиль, поговорить с моделью), посиделки крестьян, диалоги в кадре, реакция на события, настройки модели; всё применяется к экземпляру, выбранному в верхней панели.

Можно и из командной строки:

```bash
python speech/speak_launch.py              # запустить сервис локальной модели + загрузить и прогреть модель + поднять API облачков
python speech/speak_launch.py --restart    # перезапустить API после изменения кода
python speech/speak_launch.py --stop       # остановить API и выгрузить модель из видеопамяти
```

Каждый шаг работает по принципу «уже запущено — пропустить», поэтому повторный запуск ничего не ломает.

## HTTP API

По умолчанию `http://127.0.0.1:8872/` (порт задаётся в `ports.speech` файла `openwar3.json`); вызывать его может любая программа.

### Заставить юнита говорить `POST /api/say`

```json
{
  "inst": 16,
  "bubbles": [
    { "unit": "0x14A12614", "name": "Горный король", "text": "За мной, в атаку!" },
    { "unit": "0x14A12924", "name": "Архимаг", "text": "Сейчас будет снежная буря.",
      "style": { "font_px": 26, "text_color": "#FFE080", "bg_color": "#C0102040" } },
    { "screen": [960, 110], "key": 1, "name": "Рассказчик", "text": "Первая волна орков прибудет через 30 секунд.",
      "style": { "tail": false, "type_ms": 0 } },
    { "world": [-4684, 2644], "key": 2, "text": "Точка сбора", "style": { "font_px": 16 } }
  ]
}
```

| Поле | Описание |
|---|---|
| `unit` / `world` / `screen` | Одно из трёх: следовать за юнитом (если у него есть полоска здоровья — прямо над ней) / координаты карты / пиксели экрана (для закадрового текста) |
| `name` | Имя говорящего в первой строке — любое, не обязательно имя этого юнита |
| `text` | Текст реплики, переносится автоматически |
| `duration_ms` | Сколько показывать; 0 = автоматически 3 ~ 5 секунд |
| `key` | Номер облачка в мире / на экране: новое сообщение с тем же key заменяет старое |
| `update` | Если такое облачко уже есть — заменить только текст, не сбрасывая таймер (для потокового вывода) |
| `style` | `font_px`, `max_width_px`, `text_color`, `bg_color`, `border_color`, `tail`, `side_px`, `opacity`, `type_ms`, `font`… |

Одновременно — не больше 32 облачков; накладные расходы — в среднем около 0.1 ~ 0.2 ms за кадр.

### Разговор с локальной моделью `POST /api/chat`

```json
{
  "inst": 16, "unit": "0x14A12614", "name": "Горный король",
  "persona": "Ты играешь Мурадина, Горного короля из Warcraft: прямолинейный, любит выпить. Одна-две разговорные фразы, не длиннее 40 символов.",
  "message": "Впереди толпа огров. Атакуем или нет?",
  "stream": true
}
```

Возвращается `{"reply": "...", "first_token_ms": 283, "total_ms": 342}`, и к этому моменту ответ уже висит над этим юнитом. Каждый юнит помнит последние 6 обменов репликами.

### Прочее

| Метод | Описание |
|---|---|
| `GET /api/instances` | Запущенные игры |
| `GET /api/units?inst=16&mine=true&heroes=true` | Список юнитов (с китайскими названиями, координатами, здоровьем) |
| `POST /api/clear` | Убрать одно или все облачка |
| `GET /api/llm`, `POST /api/llm` | Посмотреть / изменить настройки модели (`base_url`, `model`, `max_tokens`, `temperature`) |
| `POST /api/banter` | Посиделки крестьян: рабочие на базе по очереди ворчат в образе своих персонажей, плюс объявление в начале матча (все данные о ходе игры — реальные) |
| `POST /api/camtalk` | Диалоги в кадре: герои и их свита, попавшие в кадр, разговаривают в соответствии со своими ролями |
| `POST /api/events` | Реакция на события: начало боя, конец боя, гибель героя, переход на следующий тир, потеря здания… говорят, только когда что-то случилось |

## Как выбрать локальную модель

Замеры на одной RTX 5090 (5 игровых реплик):

| Модель | Видеопамять | Скорость | Одна реплика | Вывод |
|---|---|---|---|---|
| **Qwen3.6-35B-A3B** (MoE, за раз активно только 3B), Q4, thinking выключен | 20.6 GB | ≈142 token/s | **≈0.3 s** (первый токен ≈0.27 s) | Рекомендуем: быстро, естественно отыгрывает роли на китайском |
| gpt-oss-20b (MXFP4), reasoning low | 11.3 GB | ≈280 token/s | 0.3 ~ 0.8 s | Если мало видеопамяти; китайский чуть более пресный |
| Qwen3.6-27B (плотная), Q4 | 17.2 GB | ≈39 token/s | через 5.5 s всё ещё думает | Не подходит для диалога в реальном времени |

- **Скорость определяется числом параметров, активных за раз, а не общим числом**: MoE на 35B активирует только 3B и работает в 3 ~ 4 раза быстрее плотной 27B.
- **«Размышления» обязательно выключить**: иначе все токены уходят на размышления, и до ответа дело не доходит.
- Облачко печатает около 22 символов в секунду, так что скорость генерации — уже не узкое место; по-настоящему на ощущения влияет **задержка первого токена**.

> **Чтобы реплики звучали «по-настоящему»**
>
> Передавайте модели только реальные данные о ходе игры (число матчей, победы и поражения, численность армии, запасы) и явно пишите «используй только эти факты». Проверено: без этого ограничения модель выдумывает бои, которых не было.
