# 头顶气泡与本地模型

> 让游戏里任意单位以任意身份头顶弹出对话气泡；接上本地大模型，一句话进、一句回复出在单位头上。

来源: https://war3ai.com/docs/speech/

气泡是观赏层：不影响胜负，适合直播、解说、调试。

- 任意单位、任意身份说话；多个单位可以同时说；
- 每个气泡的字号、颜色、宽度、尾巴、透明度、打字速度都能单独定制；
- 能直接接本地大模型（LM Studio），支持流式输出：边生成边更新气泡。

## 从 Bot 里用

最简单的方式是 SDK 自带的 `say`：

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

## 启动和界面

**最省事：远见首页「控制中心」**——先点「本地大模型 → 启动并载入模型」（LM Studio 本地服务 + 把配好的模型载进显存），再点「头顶聊天气泡 → 启动」。卡片上能看日志、停止、重启。

界面在远见左侧的「头顶气泡」页：让单位说话（选单位、写字、调样式、和模型对话）、农民茶话会、镜头对白、战况触发、模型设置，按顶栏选的实例操作。

命令行也可以：

```bash
python speech/speak_launch.py              # 起本地模型服务 + 载入模型并预热 + 起气泡接口
python speech/speak_launch.py --restart    # 改了代码后重起接口
python speech/speak_launch.py --stop       # 停接口，并把模型从显存卸掉
```

每一步都是「在就跳过」，重复运行没有副作用。

## HTTP 接口

默认 `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 轮对话。

### 其它

| 接口 | 说明 |
|---|---|
| `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 个字，生成速度已经不是瓶颈，真正影响体验的是**首字延迟**。

> **让台词「像真的」**
>
> 给模型的战况一律用真数据（场次、胜负、兵力、库存），并明确「只许用这些事实」。实测不加这条限制时，模型会编出没发生过的战斗。
