# 頭頂氣泡與本機模型

> 讓遊戲裡任意單位以任意身分在頭頂彈出對話氣泡；接上本機 LLM，一句話輸入，一句回覆就出現在單位頭上。

來源: https://war3ai.com/zh-tw/docs/speech/

氣泡屬於觀賞層：不影響勝負，適合直播、賽事解說和偵錯。

- 任意單位、任意身分都能說話；多個單位可以同時說；
- 每個氣泡的字級、顏色、寬度、尾巴、透明度、打字速度都能個別自訂；
- 能直接接上本機 LLM（LM Studio），支援串流輸出：一邊生成一邊更新氣泡。

## 從 Bot 裡使用

最簡單的方式是 SDK 內建的 `say`：

```python
g.say(hero, "跟我衝！", seconds=4)
```

## 啟動與介面

**最省事：遠見首頁「控制中心」**——先點「本地大模型 → 啟動並載入模型」（LM Studio 本機服務 + 把設定好的模型載入顯示記憶體），再點「頭頂聊天氣泡 → 啟動」。卡片上能看日誌、停止、重新啟動。

介面在遠見左側的「頭頂氣泡」頁：讓單位說話（選單位、輸入文字、調整樣式、和模型對話）、農民茶話會、鏡頭對白、戰況觸發、模型設定，都針對頂端列選取的實例操作。

也可以用命令列：

```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/`（連接埠設定在 `openwar3.json` 的 `ports.speech`），任何程式都能呼叫。

### 讓單位說話 `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": "你扮演魔獸爭霸裡的山丘之王穆拉丁，豪爽、愛喝酒。一兩句口語，不超過40個字。",
  "message": "前面有一群食人魔，我們衝不衝？",
  "stream": true
}
```

回傳 `{"reply": "...", "first_token_ms": 283, "total_ms": 342}`，同時這句回覆已經出現在那個單位頭上。同一個單位會記住最近 6 輪對話。

### 其他

| API | 說明 |
|---|---|
| `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，關閉思考 | 20.6 GB | 約 142 token/s | **約 0.3 秒**（首字約 0.27 秒） | 推薦：速度快、中文角色扮演自然 |
| gpt-oss-20b（MXFP4），推理 low | 11.3 GB | 約 280 token/s | 0.3 ~ 0.8 秒 | 顯示記憶體吃緊時使用，中文略顯平淡 |
| Qwen3.6-27B（稠密），Q4 | 17.2 GB | 約 39 token/s | 5.5 秒後仍在思考 | 不適合即時對話 |

- **速度看「每次啟用的參數量」，不看總參數量**：35B 的 MoE 只啟用 3B，比 27B 稠密模型快 3 ~ 4 倍。
- **一定要關閉「思考」**：不關的話 token 全花在思考上，一個字都沒回。
- 氣泡逐字打出的速度約每秒 22 個字，生成速度已經不是瓶頸，真正影響體驗的是**首字延遲**。

> **讓台詞「像真的」**
>
> 給模型的戰況一律使用真實資料（場次、勝負、兵力、庫存），並明確要求「只能使用這些事實」。實測發現，不加這條限制時，模型會編造出沒發生過的戰鬥。
