# 姿势意图格式（Pose Intent）v1

让文本模型（或人）用“身体在做什么”来描述姿势，而不是写关节角度。`src/intent.js` 把意图编译成普通的姿势条目（和 `src/catalog/*.js` 里的一样），所以预览、VMD、VRMA、ControlNet 控制图都能直接用。

```json
{
  "base": "seiza",
  "head": { "tilt": 12 },
  "arms": {
    "left":  { "touch": "cheek" },
    "right": { "place": "face_side", "shape": "peace" }
  }
}
```

正坐，左手掌贴在脸颊上，右手在脸旁比 V，头往左歪 12°。

- 在线试：`tools/intent/`（Playground：编辑 JSON，实时显示错误、提示和 5 视角，下载 VMD / VRMA）
- 命令行：`node tools/intent.js pose.json --vmd --vrma`
- JSON Schema：[`schema/pose-intent.schema.json`](../schema/pose-intent.schema.json)（由 `node tools/intent.js --schema` 生成）
- 示例：[`examples/intents/`](../examples/intents/)

## 为什么要这一层

配方（catalog 里的条目）对文本模型来说已经比 VMD 容易得多，但仍然要写欧拉角、IK 目标坐标、手心朝向向量，这些正是模型最容易弄反的地方。意图格式只保留模型擅长的决定：

| 模型决定 | 求解器负责 |
|---|---|
| 基础姿态：站、跪、正坐、蹲、躺…… | 腿、髋、躯干的具体角度（来自姿势库） |
| 手放在哪：身体部位（脸颊、腰、膝盖）或空中位置（头顶、脸旁、前方） | 手腕位置、手指和手心朝向、肘的方向、双骨 IK |
| 手型：比 V、握拳、指向…… | 每根手指的弯曲 |
| 身体前倾 / 侧弯 / 扭转、头的点头 / 转头 / 歪头（角度） | 分配到脊柱、胸、颈、头 |
| 动作：几个时间点的姿势 | 插值、按骨架重定向 |

所有身体部位都跟着对应的骨骼走，所以同一个“左手摸脸颊”在站、跪、躺的基础姿态上都成立。

## 约定

- **左右**：都是角色**自己的**左右。角色面向镜头，所以它的左手在画面右边。
- **角度**：度。`turn`、`tilt`、`side`、`twist` 为正 = 朝角色自己的左边；`forward`、`nod` 为正 = 向前弯 / 低头。
- **坐标**（只在 `offset` 和向量形式的方向里用到）：米，+Y 向上，+Z 是角色的正前方，+X 是角色的左边。

## 字段

### 顶层

| 字段 | 类型 | 说明 |
|---|---|---|
| `base` | 字符串 | 基础姿态：预设名，或姿势库里任意单人条目的 id。默认 `stand` |
| `lean` | `{ forward, side, twist }` | 躯干，度（±90），叠加在基础姿态上；脊柱 60 %、胸 40 % |
| `head` | `{ nod, turn, tilt }` 或 `{ gaze }` | 头，度（±80）；颈 40 %、头 60 %。写了就替换基础姿态的头 |
| `arms` | `{ left?, right?, both? }` | 手臂，见下。`both` 对两侧都生效（右侧自动镜像），`left` / `right` 覆盖它 |
| `scope` | `full` \| `upper` | `upper` 只控制上半身（腿和髋交给待机动作）。默认跟随基础姿态 |
| `frames` | 数组（≥ 2） | 动作，见下 |
| `loop` | 布尔 | 动作是否循环 |
| `previewT` | 秒 | 缩略图取哪一帧 |
| `id` | `a-z0-9_` | 也是导出文件名，默认 `intent` |
| `names` | `{ zh, en, ja }` | 显示名称 |
| `description`, `tags` | | 说明、标签 |
| `version` | `1` | 可选；写了就必须是 1 |

### 基础姿态 `base`

**预设**（只取腿、髋和躯干，手臂用下表的默认，头回正）：

| 预设 | 来自条目 | 没写手臂时 |
|---|---|---|
| `stand` | —（自然站立） | 自然下垂 |
| `kneel` | `kneel_hands_on_thighs` | 手放大腿 |
| `seiza` | `seiza_tuck_hair` | 手放大腿 |
| `half_kneel` | `half_kneel_hand_on_knee` | 保留原姿势 |
| `squat` | `squat` | 手搭膝盖 |
| `sit` | `sit_hug_knees` | 保留原姿势（抱膝） |
| `side_sit` | `side_sit_hand_on_cheek` | 手放大腿 |
| `w_sit` | `wsit_hands_on_thighs` | 手放大腿 |
| `lie_back` | `supine_full_stretch` | 自然下垂（贴身） |
| `lie_side` | `side_lie_relaxed` | 保留原姿势 |
| `lie_front` | `prone_head_on_arms` | 保留原姿势 |
| `all_fours` | `tabletop_look_ahead` | 保留原姿势（撑地） |

**姿势库条目 id**：用那个条目的整个姿势作底，没写的手臂和头都保留原样。例如 `"base": "side_sit_hand_on_cheek", "head": { "turn": -25 }` = 原姿势只把头转开。动作条目取 `previewT` 附近的那一帧。`node tools/intent.js --vocab --library` 列出所有可用 id。

### 手臂

每只手臂是下面之一：

- `"rest"`：自然下垂
- `"keep"`：保留基础姿态的这只手臂
- `{ "touch": <身体部位>, "of"?: "same" | "other" }`：手掌贴在身体部位上。`of: "other"` 表示另一侧的部位（右手摸左脸颊）；中线上的部位（嘴、胸口、肚子……）不分左右
- `{ "place": <空中位置> }`：手在空中某个位置，跟着胸口走
- `{ "height": <高度>, "reach"?: <伸展>, "dir"?: <方向> }`：用描述里的词放手，见下面“按高度、伸展、方向放手”

三种对象都可以再加：

| 字段 | 说明 |
|---|---|
| `shape` | 手型：`relaxed` 放松、`open` 张开、`flat` 并拢伸直、`fist` 拳、`point` 指、`peace` 比 V、`thumbsUp` 大拇指、`heart` 比心半边 |
| `fingers` | 手指指向：`up` `down` `forward` `back` `out`（朝这只手那一侧）`in`，或 `[x, y, z]` |
| `palm` | 手心朝向，同上 |
| `elbow` | 肘尖朝向，同上 |
| `offset` | 目标点微调 `[x, y, z]`，米 |

方向词按手镜像（`out` 对左手是 +X，对右手是 −X）；向量写在 `both` 里时右手自动镜像，写在 `left` / `right` 里按原样使用。

**身体部位**（`touch`）：

| 头 | 颈・躯干 | 腰・腿 |
|---|---|---|
| `top` 头顶、`forehead` 额头、`eye` 眼、`mouth` 嘴、`chin` 下巴（托腮）、`cheek` 脸颊、`ear` 耳、`hair` 头侧头发（别头发）、`back_of_head` 后脑 | `nape` 后颈、`neck` 颈侧、`collarbone` 锁骨、`shoulder` 肩（默认另一侧）、`chest` 胸口、`belly` 肚子、`waist` 腰侧 | `hip` 胯（叉腰）、`lower_back` 后腰（手背在身后）、`thigh` 大腿、`knee` 膝盖、`shin` 小腿、`ankle` 脚踝 |

**空中位置**（`place`）：

| 位置 | 样子 | 默认手型 |
|---|---|---|
| `overhead` | 举过头顶 | open |
| `raised` | 举到头侧（举手、挥手） | open |
| `face_side` | 脸旁（比 V） | relaxed |
| `front_chest` | 胸前（比心、合十、拿东西） | relaxed |
| `forward` | 向前伸（指、够东西） | relaxed |
| `out` | 向侧面伸 | relaxed |
| `low_front` | 身前下方（双手交叠） | relaxed |
| `offer` | 手心向上递出 | flat |

**按高度、伸展、方向放手**（`height` / `reach` / `dir`）：手腕在胸的坐标系里（跟着胸口走），中性身材上：

| 字段 | 词 |
|---|---|
| `height` 手腕高度 | `above_head` 头顶上方、`head` 头高、`shoulder` 平肩、`chest` 胸前、`waist` 腰高、`hip` 髋高、`below_hips` 髋下 |
| `reach` 手臂伸展（默认 `half`） | `straight` 伸直、`half` 半伸、`bent` 弯曲 |
| `dir` 从肩出发、从上往下看的方向（默认 `forward`） | `forward` 向前、`forward_out` 斜前方、`out` 向两侧、`back_out` 斜后方、`back` 向后、`back_across` 背后横过、`across` 横过身体、`forward_across` 斜向内前方 |

每个词是一个范围，编译器找三个词都满足、又最靠近各自中间的手腕位置；做不到的组合（弯曲着举过头顶）取手臂够得到的最近一点。上下方向不用单独说：高度和伸展已经定了。手型、手心、肘向沿用离得最近的空中位置，也可以照常用 `shape` / `palm` / `elbow` / `offset` 改。这套词和舞蹈动作标注的手部事实（`src/decompile.js` 的 `handFacts`）是同一套，所以“把真实的手读成词再编译回去”中位只差 10.6 cm（命名位置是 18 cm，见 [import.md](import.md)）。文字 → 意图默认不提供这种写法（`textToIntent({ handHeights: true })` 打开），这样 text2pose 的评测不受影响。在 300 个舞蹈动作上，用了这种写法后文字 → 意图的手从 20.7 cm 近到 11.1 cm（[import.md](import.md)）。

三种手臂写法也能叠到别的动作上（MoMask 生成的、导入的舞蹈）：`src/arm-layer.js` 在一段时间里把手放到意图说的位置、前后平滑过渡，见 [import.md](import.md) 的“在导入的动作上叠手臂”。

### 头 `head`

`{ "nod": 10, "turn": -20, "tilt": 8 }`，或 `{ "gaze": "camera" | "down" | "up" | "left" | "right" }`（可以和角度一起写，角度叠加上去）。

### 动作 `frames`

```json
{
  "id": "stand_to_squat",
  "frames": [
    { "t": 0,   "base": "stand", "arms": { "both": "rest" } },
    { "t": 1.0, "base": "squat" },
    { "t": 1.8, "base": "squat", "head": { "nod": 8 }, "arms": { "right": { "touch": "chin" } } }
  ]
}
```

- 每一帧在顶层字段的基础上覆盖（对象逐层合并，其他值替换），`t` 是秒，必须递增
- `base` 可以在帧之间改变，就是姿势之间的过渡
- 帧里的手臂可以只写要改的字段（例如只改 `offset`），会合并到顶层同一只手臂上；换了写法（`touch` / `place` / `height` 之间）时，旧写法的字段（含 `offset`）不再带过来
- 帧之间是线性插值（四元数球面插值），所以过渡比较匀速；想要自然的过渡，多加中间帧

### 镜头 `camera`

按“拍什么”描述镜头，导出时在**目标模型自己的骨骼上**取景，编译成 MMD 镜头动作（单独的镜头 VMD）：

```json
"camera": {
  "shot": "full", "pitch": 8,
  "keys": [
    { "t": 2,   "shot": "bust", "ease": "inOut" },
    { "t": 3.5, "yaw": 25,      "ease": "inOut" }
  ]
}
```

| 字段 | 含义 |
|---|---|
| `shot` | 景别：`full` 全身、`knee` 膝上、`waist` 腰上、`bust` 胸上、`face` 脸部特写 |
| `yaw` | 水平角度（度），正值 = 绕到角色**自己的左边**；0 = 正面 |
| `pitch` | 俯仰（度），正值 = 从上往下拍 |
| `roll` | 镜头倾斜（荷兰角，度） |
| `fov` | 垂直视角（度），MMD 默认 30 |
| `dolly` | 距离是景别取景距离的多少倍（0.8 = 近 20 %），默认 1；推拉时头顶保持在景别给的位置（近了裁掉下面的身体，远了多露出下面） |
| `follow` | `true` 时每 0.13 秒左右按当时的身体重新取景（会走动的动作用） |
| `keys` | 之后的镜头状态：`t` 秒，只写要改的字段，其余沿用上一个；`ease` 是怎样移到这个状态：`linear`、`in`、`out`、`inOut`（默认）、`cut`（硬切）。也可以是一个**运镜**：`{ "t": 1, "move": "push_in", "speed": "normal", "seconds"?: 2.4 }`，从 `t` 开始，按电影里的典型幅度和时长运动 |

- **取景方式**：每种景别有一组必须入画的身体部位（全身 = 头顶、头部、膝、脚踝、脚底、髋、手；胸上 = 头顶、头部、胸、肩……），编译器找能把它们放进画面（留边距）的最近距离，并把它们居中。关节和头部按有厚度的球体算，所以跪坐时着地的膝盖、近距离时的头都不会被切掉。
- **换模型**：取景在每个模型自己的骨骼上算（和手的接触点一样），所以同一个配方给高个子模型也是同样的构图，只是距离不同。镜头 VMD 是目标代码，换模型从意图重新编译。
- **角度是相对世界的**（角色初始的正面），不是相对头或身体；躺着的角色要看到脸，用 `pitch` 从上往下拍。
- **运镜**（`MOVES`，数据来自 E.T.：Courant 等，ECCV 2024，从 29,661 个电影镜头里取出 22,946 段至少 1 秒的单一运镜，按相机相对人物髋部的运动统计；只保留这些统计数字，数据本身不进仓库）：

  | `move` | 做什么 | 典型时长 | 速度 gentle / normal / strong（人物不动） |
  |---|---|---|---|
  | `push_in` / `pull_out` | 推近 / 拉远（`dolly`） | 2.4 / 2.3 秒 | 每秒距离 ×e^∓0.046 / 0.095 / 0.166（normal 推完约近 21 %） |
  | `orbit_left` / `orbit_right` | 绕人物转到镜头自己的左 / 右（E.T. 的 trucking） | 2.3 秒 | 每秒 3.9 / 8.6 / 16.5° |
  | `boom_up` / `boom_down` | 升高往下拍 / 降低往上拍（`pitch`） | 2.0 / 1.9 秒 | 每秒 2.8 / 6.5 / 10.6° |
  | `hold` | 不动 | 3.5 秒 | — |

  `speed` 是统计的 50 / 75 / 90 分位（默认 `normal`）；`follow: true` 时（人物在走动）用人物移动时的统计，推拉和绕转大约快一倍。电影运镜都很克制：典型的推近只近 6–20 %，要推到脸部特写用景别（`shot`）换。E.T. 里向左横移的样本只有 43 段（向右 3,063 段），左右合并统计。环绕方向：镜头面对人物时，镜头往自己右边转 = 绕到人物自己的左边（`yaw` 增大）。
- **取景检查**：逐帧检查头顶是否出画，景别承诺的手 / 脚 / 膝是否出画（从一个景别移到另一个时，只查两者都包含的部位）。提示会给出时间段，并建议加镜头关键帧或打开 `follow`：
  ```
  camera: top of the head out of frame at 0.53-1.80 s (the body moves out of the framing: add a camera key there or set "follow": true)
  ```
  推近（`dolly` < 1）时故意裁掉身体，那段不检查。
- 命令行：`node tools/intent.js pose.json --camera` 输出 `<id>.camera.vmd` 和 `.camera.vmd.txt`（用意图里的 `camera`；也可以 `--camera '{"shot":"bust"}'` 临时指定；没有就用全身）。`--pmx model.pmx` 按那个模型取景。Playground 里有“镜头”画面和“下载镜头 VMD”。
- MMD 镜头的约定（已对照 MMDAgent-EX 的 Render / CameraController、saba 和 three.js MMDLoader；有 roll 时按 MMDAgent-EX 的 Y·X·Z 顺序）：注视点、欧拉角（弧度）、负的距离、整数视角；每个关键帧带 6 条 Bezier 曲线（注视点 X/Y/Z、旋转、距离、视角，字节顺序 x1,x2,y1,y2），曲线描述**移到这个关键帧**的过程；硬切是相邻两帧的两个关键帧。镜头文件的模型名是 `カメラ・照明`，在 MMD 里加载到镜头上，不是加载到模型上。
- 文字 → 姿势（`tools/text2pose.js`）暂时不生成镜头：评测用的提示词和输出格式保持不变。

## 校验和反馈

编译分两层，都返回能直接交还给模型的文字：

1. **错误**（不能编译，`IntentError.problems`）：未知字段、未知部位 / 位置 / 手型（附“你是不是想写 …”和可选值列表）、角度超范围、时间不递增
   ```
   intent.arms.left.touch: unknown value "cheeks" (did you mean "cheek"?); use one of top, forehead, …
   ```
2. **提示**（能编译，但姿势可能不对，`warnings`）：身体部位穿进地面、手够不到目标（例如站着摸脚踝）
   ```
   leftHand misses its target by 42 cm
   ```

## 给文本模型用

推荐的循环：

1. 系统提示里放词汇表：`node tools/intent.js --vocab`（约 20 行），需要姿势库 id 时加 `--library`
2. 用 JSON Schema 做工具定义（function calling / structured output），模型只能输出合法结构
3. 编译；有错误或提示就把文字原样交回模型重写
4. 需要更高把握时，渲染 5 视角图给能看图的模型确认

### 现成的：文字 → VMD（`tools/text2pose.js`）

```bash
npm install                     # 只有这一步需要依赖：@anthropic-ai/sdk
node tools/text2pose.js "跪坐着，一只手托腮，歪头笑"
node tools/text2pose.js "挥手打招呼" --pmx path/to/model.pmx --effort high
```

`src/text-to-intent.js` 实现上面的循环：

- 模型 `claude-opus-5-5`，effort 默认 `medium`（`--effort` 可改）
- 系统提示 = 词汇表 + 姿势库的全年龄条目（id、中文名、英文名），用 prompt caching，第二次起大部分输入从缓存读
- 结构化输出（`output_config.format`）：返回值一定是合法 JSON，`base` 只能是预设或姿势库 id；Schema 是 `intentSchema()` 去掉结构化输出不支持的约束后的版本，那些约束仍由编译器检查
- 编译出错或有提示时，把文字交回模型重写，默认最多 3 轮；模型对提示原样返回同一个意图，视为有意如此
- 开启服务端拒答回退（`fallbacks: "default"`）：被安全分类器拒绝时，在同一次调用里换模型重试
- 输出 `out/text2pose/<id>.intent.json`、`<id>.vmd`（通用骨架或 `--pmx` 指定的模型）、`<id>.vrma`

两种调用方式（`--backend`）：

- `api`：Claude API，需要 `ANTHROPIC_API_KEY` 或 `ant auth login`
- `cli`：Claude Code 的 `claude -p`，用你的 Claude 登录（先在终端里 `claude auth login` 一次），不需要 API key，用量计入 Claude 套餐。调用时关掉工具、插件、hooks、MCP 和记忆，在空目录里运行；`claude -p` 只接受一段提示，所以修正轮次会把上一次的意图和编译器反馈写进同一段提示里（API 方式是真正的多轮对话）

默认：设置了 `ANTHROPIC_API_KEY` 就用 `api`，否则用 `cli`。每一轮是一次模型调用。`src/text-to-intent.js` 本身不依赖 SDK，由调用方传入 client，以后也可以接到网页里。

可靠程度从高到低：从姿势库选 id > 用预设加手臂和头 > 用 `frames` 编排 > 用 `offset` / 向量微调。

## 限制

- 部位是在参考体型上量的，手放在身体表面附近，不看衣服和头发
- 手只能贴在自己身上或空中；扶墙、撑地还要用姿势库条目作底（`"keep"` 手臂）；碰别人用双人意图（[duo-intent.md](duo-intent.md)）
- 腿和髋只能来自基础姿态，不能单独描述（例如“左脚往前一步”）
- 双人姿势用双人意图：两个单人意图 + 相对位置 + 接触点，见 [duo-intent.md](duo-intent.md)
- 动作是关键帧插值，没有物理和惯性

## 版本

现在是 `version: 1`。新增部位、位置、预设不改版本；字段含义变化时版本加一，编译器会拒绝不认识的版本。
