@rig-c/pose 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rig-c
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,115 @@
1
- # Temporary Holding Version
1
+ # @rig-c/pose
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Puts motion from a pose onto a rig. The motion is joint positions over time: from a video, a dataset, or points placed by hand. The rig is one [rig-c](https://github.com/rig-c/core) built. A body-18 pose sequence becomes bone keys in rig-c's motion format, and the keys are checked before anything is written.
4
+
5
+ **0.x is the prototype.** The format, the command and the output can change in any release until 1.0.
6
+
7
+ Part of the rig-c toolchain: [core](https://github.com/rig-c/core) compiles and gates a rig, [parts](https://github.com/rig-c/parts) builds one from a painting, [play](https://github.com/rig-c/play) plays it, and pose moves it.
8
+
9
+ <!-- demo -->
10
+
11
+ ## What it does
12
+
13
+ ```sh
14
+ rig-pose retarget --rig out/rig --map map.json --pose dance.pose.json --out motion.json --name dance
15
+ ```
16
+
17
+ For every frame of the pose and every bone the map names, the segment that bone carries (an upper arm: shoulder → elbow) points somewhere in the picture. The bone is turned so its segment points the same way: the picture angle, flipped into rig-c's y-up world, minus the bone's angle at the rig's setup pose (read through rig-c's own poser), less the turn of the mapped bone above it — because a `rotate` key in rig-c adds to the setup rotation and a child turns with its parent. The travel of the pelvis midpoint, scaled by a length measured on both the rig and the pose, moves the hip. The result is one animation in rig-c's motion format (`rigc-motion/1`): a `rotate` track per mapped bone, a `translate` track on the hip, one key per pose frame.
18
+
19
+ Nothing is written until every check passes:
20
+
21
+ | Check | Fails with |
22
+ | --- | --- |
23
+ | rig-c's gate is green with the rig as it is, and again with the animation in (`rigc build`) | `RIG_GATE_BASE`, `RIG_GATE`, then rigc's own `FAIL` lines |
24
+ | every mapped bone **lands** on the angle the pose asks for, on every frame — the gated build is posed by rig-c's poser and measured (0.01° / 0.01 units) | `POSE_LANDS`: the bone, the worst error, the frame |
25
+ | elbows and knees bend only the way they can: at every joint inside a chain, the inner angle stays on the side of the rest pose's | `POSE_BEND_ONE_WAY`: the chain, the joint, the frame, the angle |
26
+ | every point the map needs is good on at least one frame | `POSE_POINT_NEVER_GOOD` |
27
+ | the map resolves against the rig: bones exist, chains descend, aims descend | `MAP_*`, naming both sides |
28
+
29
+ and what the run cannot decide for you it lists:
30
+
31
+ - `HOLD` — a joint missing, or scored under `--min-score`, holds its last good value; one line per span, with the frames and the reason.
32
+ - `BEND` — with `--bends clamp`, a hinge bent the wrong way is straightened on that frame instead of refusing.
33
+ - `FOLD` — `rigc render --folds` over the new animation: every mesh that folds, and when. A reading, not a gate.
34
+ - `POSTURE` — when the map has a pelvis chain (joints `r_hip`, `l_hip`) and a `translate`: the hip line's tilt range, and the sway — the x-range of mid_hip − mid_shoulder — in rig units and in shoulder widths, the pose's beside the posed rig's. A reading, not a gate.
35
+
36
+ `--smooth <n>` smooths each joint's positions, after its holds, with a Savitzky–Golay filter: at every frame, the least-squares cubic over the *n* frames around it (clipped at the ends), read at that frame. It is centred, so it moves no motion in time, and a cubic keeps the peaks a moving average flattens. `1` (the default) is none; otherwise *n* is odd and at least 5, since a cubic through 3 points passes through all three.
37
+
38
+ **Use `--smooth 7` at 24 fps.** It was measured on a 13 s dance on a @rig-c/parts rig, read back from the gated build: the motion's timing did not move (speed cross-correlation lag 0 on the wrists, ankles and hip), the frame-to-frame second difference dropped 2.5× on the wrists, 1.7–2.1× on the ankles and 1.5× on the hip, and the hip's 95th-percentile speed fell to 0.85×. The criterion set beforehand, a 2× drop on every point, was not met on the hip. That criterion was flawed: the hip's second difference is mostly real motion, while the filter cuts white noise 4.6×. A measure that isolates noise is a later card, and the default stays off until it passes.
39
+
40
+ Exit codes: `0` written, `1` refused (every reason printed, nothing written), `2` bad invocation. `rig-pose --help` has every flag.
41
+
42
+ ## The pose: `rig-pose-sequence/1`
43
+
44
+ @rig-c/parts' `keypoints.json` (`spine-parts-keypoints/1`) extended to frames — the same vocabulary, one more level:
45
+
46
+ ```json
47
+ { "spec": "rig-pose-sequence/1",
48
+ "space": { "units": "picture-px", "origin": "top-left", "y": "down" },
49
+ "width": 1080, "height": 1920,
50
+ "source": "what produced the file",
51
+ "frames": [
52
+ { "t": 0, "people": [ { "id": "a", "joints": {
53
+ "neck": { "state": "observed", "at": [540, 410], "score": 0.93 },
54
+ "r_shoulder": { "state": "observed", "at": [470, 430], "score": 0.91 },
55
+ "r_wrist": { "state": "missing" } } } ] } ] }
56
+ ```
57
+
58
+ - `t` is seconds and strictly increases; the animation's keys are at `t − t₀`.
59
+ - Joints are body-18 (`nose neck r_shoulder r_elbow r_wrist l_shoulder l_elbow l_wrist r_hip r_knee r_ankle l_hip l_knee l_ankle r_eye l_eye r_ear l_ear`); `r`/`l` are the **subject's** sides. `state` is `observed`, `occluded` or `missing`, as in the keypoints file. An unlisted joint is missing.
60
+ - Two differences from the keypoints file: the unit is `picture-px`, and `at` may lie outside the picture (a hand leaves the frame; the score says how much to believe it).
61
+ - More than one person: `--person <id>`.
62
+ - `score` is whatever the pose model gives. RTMPose's goes above 1, so `--min-score` is a cut on that model's scale, not a probability.
63
+
64
+ **From COCO-17** — the layout most pose models and datasets use — `rig-pose convert --coco in.json --out pose.json`, or `retarget --coco`. The input is `{ "format": "coco17-sequence/1", "width", "height", "source", "frames": [{ "t", "keypoints" }] }`, keypoints in COCO's order as 17 `[x, y, score]` or 51 numbers, or `null` for a frame with nobody. The mapping: COCO's `left_*`/`right_*` → body-18's `l_*`/`r_*` (both are the subject's sides), a score of 0 → `missing`, and body-18's `neck`, which COCO lacks, is the shoulders' midpoint scored the lower of the two (OpenPose's definition).
65
+
66
+ ## The map: `rig-pose-map/1`
67
+
68
+ Which bones carry which joints. You write it for your rig:
69
+
70
+ ```json
71
+ { "spec": "rig-pose-map/1",
72
+ "chains": [
73
+ { "name": "trunk", "bones": ["chest"], "joints": ["mid_hip", "neck"], "aim": ["neck"] },
74
+ { "name": "arm_r", "bones": ["sleeve_r0", "sleeve_r1"], "joints": ["r_shoulder", "r_elbow", "r_wrist"] },
75
+ { "name": "leg_l", "bones": ["leg_l0", "leg_l1"], "joints": ["l_hip", "l_knee", "l_ankle"] } ],
76
+ "translate": { "bone": "hip", "joint": "mid_hip",
77
+ "scale": { "pose": ["mid_hip", "neck"], "rig": ["hip", "neck"] } } }
78
+ ```
79
+
80
+ - A chain's bone *k* carries the segment from joint *k* to joint *k*+1, so it lists one joint more than bones; each bone descends from the one before (a control bone between them is fine).
81
+ - `aim`, one entry per bone: `"x"` (default) — the bone's own x axis is the segment; right for a limb link @rig-c/parts turned along its limb. A point — the direction from this bone's origin to that point; right for a trunk bone, whose own axis parts leaves at 0°. `"a>b"` — the direction from point a to point b; right for a head, turned by its eye line: `{ "bones": ["head"], "joints": ["r_eye", "l_eye"], "aim": ["eye_r>eye_l"] }`, or for a pelvis, turned by its hip line: `{ "name": "pelvis", "bones": ["hip"], "joints": ["r_hip", "l_hip"], "aim": ["leg_r0>leg_l0"] }`.
82
+ - A **point** of the rig is a bone's origin (`name`), the end of its x axis (`name:tip`), or the midpoint of two of those (`"a+b"`: `"sleeve_r0+sleeve_l0"` is the shoulders' midpoint). An aim's points must be the bone itself or descend from it.
83
+ - `reach: true`, on a one-bone chain from the translated joint (a trunk from `mid_hip`): the bone is aimed from where its origin is posed — the hip moved and turned — at where the pose puts the segment's end relative to mid_hip, scaled. Without it, a tilted pelvis carries the chest's origin sideways and the trunk, parallel to the pose's, misses the shoulders. Its aim is one point, from the bone's own origin: `{ "name": "trunk", "bones": ["chest"], "joints": ["mid_hip", "mid_shoulder"], "aim": ["sleeve_r0+sleeve_l0"], "reach": true }`. The chain must descend from the translated bone.
84
+ - `bend`: every joint inside a chain is a hinge. `"rest"` (default) takes the side it may bend from the rig's rest pose and refuses a rest that is straight (under 0.5°); `"ccw"`/`"cw"` state it; `"free"` skips the check.
85
+ ⚠️ **Seen from the front, an elbow's side is not a joint limit.** An elbow flexes toward the camera, and its picture falls on either side of the upper arm depending on how the arm is turned. On a real 13 s front-facing dance (313 frames, RTMPose), the right elbow sat on the "wrong" side of the rig's rest on 165 frames (median 23°), and the left on 200 (median 29°, up to 179° with the hands behind the head). Every one was a real elbow bend. The knees strayed at most 8.8° and 14.7°, which is pose noise around a straight leg. So for front-facing material: arms `"free"`; legs on `"rest"`, with `--bends clamp` straightening the noise.
86
+ - `mid_hip` and `mid_shoulder` are the midpoints of the two hips and the two shoulders.
87
+ - `translate.scale` is one length measured on both: two points on the rig, two points in the pose (its median over the frames where both are good). Pick a long segment that does not foreshorten much.
88
+
89
+ ## Requirements
90
+
91
+ [Bun](https://bun.sh) ≥ 1.2 on `PATH` — rig-pose, like rig-c, runs its TypeScript sources directly. `rig-c` comes as a dependency. With `@esotericsoftware/spine-core` installed beside it, rig-c's gate runs the spine-core round trip; without it, rig-c's own gate, and the gate's report says which.
92
+
93
+ ## What 0.2 fixes
94
+
95
+ A flat painting turned by its bones matches each limb's direction, not its depth. On a real 13 s front-facing dance, retargeted onto a @rig-c/parts rig, five moments were found where it cannot follow. They were found by measuring the pose against the rig and checked against the video:
96
+
97
+ | | t (s) | what the dancer does | why bone rotation cannot follow |
98
+ | --- | --- | --- | --- |
99
+ | S1 | 0.0–1.1 | hands clasped behind the back | depth order: the forearms go behind the body |
100
+ | S2 | 1.0–2.25 | a hand to the ear, then the forearm toward the camera | foreshortening (the upper arm at 0.27 of its length) and depth order |
101
+ | S3 | 3.7–7.5 | hands behind the head | depth order; both arm meshes fold through the whole span |
102
+ | S4 | 7.7–10.9 | hands crossed in front | which forearm is in front |
103
+ | S5 | 11.1–13.0 | an arm thrust at the camera, then a V sign | foreshortening (0.63) and the shape of the hand; the arm mesh folds |
104
+
105
+ The answer is a second drawing of the arm for each of those poses, shown in place of the bent mesh while the pose lasts. 0.2 will have rig-pose choose when to show it, key the switch in the motion, and list every fold no drawing covers ([#6](https://github.com/rig-c/pose/issues/6)).
106
+
107
+ ## What 0.1.0 does not do
108
+
109
+ - **Extract poses from video.** The input is joint positions; how you get them (a pose model, a dataset, a hand) is yours.
110
+ - **Turn the body.** 2D only: a segment pointing at the camera foreshortens, and the rig cannot. The direction is matched; the length is not.
111
+ - **Plant feet** beyond what the pose gives, **hands and fingers** (no bones for them yet, rig-c/parts#171), mobile.
112
+
113
+ ## Licence
114
+
115
+ MIT, see `LICENSE`.
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * rig-pose's `bin` entry has one job: hand off to Bun.
6
+ *
7
+ * npm's `bin` field has to be something any installed Node can run, but
8
+ * rig-pose is a Bun program (it runs its TypeScript sources directly, and so
9
+ * does rig-c underneath it). If `bun` is on PATH, run the real CLI (cli.ts,
10
+ * next to this file's directory) under it — argv, stdio, the exit code and
11
+ * signals pass straight through. If it is not, say so once and stop.
12
+ */
13
+
14
+ const path = require('node:path');
15
+ const { spawnSync } = require('node:child_process');
16
+
17
+ const cli = path.join(__dirname, '..', 'cli.ts');
18
+ const result = spawnSync('bun', [cli, ...process.argv.slice(2)], { stdio: 'inherit' });
19
+
20
+ if (result.error) {
21
+ if (result.error.code === 'ENOENT') {
22
+ process.stderr.write('rig-pose runs on Bun, which was not found on PATH — install it from https://bun.sh\n');
23
+ } else {
24
+ process.stderr.write(`rig-pose: could not launch bun: ${result.error.message}\n`);
25
+ }
26
+ process.exit(1);
27
+ }
28
+
29
+ if (result.signal) {
30
+ process.kill(process.pid, result.signal);
31
+ } else {
32
+ process.exit(typeof result.status === 'number' ? result.status : 1);
33
+ }
package/cli.ts ADDED
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * rig-pose — puts motion from a body-18 pose sequence onto a rig rig-c built.
4
+ *
5
+ * The command line is the interface for an agent that cannot see the rig: every
6
+ * refusal names the rule, the object, the value found and the value required,
7
+ * and the exit code says which kind of outcome it was.
8
+ */
9
+ import { readFileSync, writeFileSync } from 'node:fs';
10
+ import pkg from './package.json' with { type: 'json' };
11
+ import { convertCoco17, writePoseSequence } from './src/pose.ts';
12
+ import { lineOf, Refusal } from './src/problems.ts';
13
+ import { runRetarget, type RetargetArgs } from './src/run.ts';
14
+
15
+ const HELP = `rig-pose ${pkg.version} — motion from a pose sequence onto a rig-c rig
16
+
17
+ Usage:
18
+ rig-pose retarget --rig <rig> --map <map.json> --pose <pose.json> --out <motion.json> [--name dance] [flags]
19
+ rig-pose convert --coco <coco17.json> --out <pose.json>
20
+ rig-pose --version | --help
21
+
22
+ retarget
23
+ --rig <path> the rig spec rigc builds from: a rig.json, or a directory holding one
24
+ (with its motion.json beside it — @rig-c/parts writes the pair to <out>/rig/)
25
+ --map <map.json> rig-pose-map/1: which bones carry which joints
26
+ --pose <pose.json> rig-pose-sequence/1 (body-18 per frame), or COCO-17 with --coco
27
+ --out <motion.json> where the motion spec goes: the rig's own animations plus the new one,
28
+ ready for \`rigc build --rig <rig> --motion <motion.json>\`
29
+ --name <name> the new animation's name (default: dance); refused if the rig has one by that name
30
+ --coco read --pose as coco17-sequence/1 and convert it first
31
+ --person <id> the person to follow when the sequence holds more than one
32
+ --base-motion <path> the rig's motion spec, when it is not motion.json beside rig.json
33
+ --min-score <s> a joint scored below s is not good on that frame and holds its last good
34
+ value (default: 0.3); a joint with no score is good
35
+ --smooth <n> Savitzky–Golay over n frames of joint positions: a centred least-squares cubic,
36
+ read at each frame (default: 1, none); n is odd and ≥ 5 — a cubic through 3
37
+ points passes through all three, so 3 would change nothing
38
+ --bends refuse|clamp a hinge (elbow, knee: any joint inside a chain) bent the wrong way refuses
39
+ the run (default), or is straightened on that frame and listed
40
+ --bend-tolerance <d> degrees past straight a hinge may bend the wrong way before it counts (default: 0)
41
+ --profile spine|spine-html rigc's gate profile (default: spine, rigc's own default)
42
+ --build <dir> keep the gated build (skeleton.json, .atlas, .model.json) here
43
+ --render <dir> keep rigc's render of the animation (frames, contact.png, folds.json) here
44
+ --report <file> write the run's measurements (holds, bends, landings, folds) as JSON
45
+
46
+ It writes nothing until rigc's gate is green with the animation in, every mapped bone lands on
47
+ the angle the pose asks for (posed by rig-c's own poser), and no hinge bends the wrong way.
48
+ \`rigc render --folds\` is a reading: the run lists every mesh that folds, and still writes.
49
+
50
+ convert
51
+ --coco <file> coco17-sequence/1: { format, width, height, source, frames: [{ t, keypoints }] },
52
+ keypoints in COCO's order as 17 [x, y, score] or 51 numbers, or null
53
+ --out <file> the rig-pose-sequence/1 it becomes; neck = the shoulders' midpoint
54
+
55
+ Exit codes: 0 written, 1 refused (every reason printed), 2 bad invocation.
56
+ `;
57
+
58
+ class Usage extends Error {}
59
+
60
+ function parseFlags(argv: string[], flags: readonly string[], switches: readonly string[]): Map<string, string> {
61
+ const out = new Map<string, string>();
62
+ for (let i = 0; i < argv.length; i++) {
63
+ const a = argv[i]!;
64
+ if (!a.startsWith('--')) throw new Usage(`unexpected argument "${a}"`);
65
+ const name = a.slice(2);
66
+ if (switches.includes(name)) {
67
+ out.set(name, 'true');
68
+ continue;
69
+ }
70
+ if (!flags.includes(name)) throw new Usage(`unknown flag ${a}; the flags are ${[...flags, ...switches].map((f) => `--${f}`).join(', ')}`);
71
+ const v = argv[++i];
72
+ if (v === undefined || v.startsWith('--')) throw new Usage(`${a} needs a value`);
73
+ if (out.has(name)) throw new Usage(`${a} is given twice`);
74
+ out.set(name, v);
75
+ }
76
+ return out;
77
+ }
78
+
79
+ function need(f: Map<string, string>, name: string): string {
80
+ const v = f.get(name);
81
+ if (v === undefined) throw new Usage(`--${name} is required`);
82
+ return v;
83
+ }
84
+
85
+ function num(f: Map<string, string>, name: string, dflt: number, ok: (n: number) => boolean, what: string): number {
86
+ const v = f.get(name);
87
+ if (v === undefined) return dflt;
88
+ const n = Number(v);
89
+ if (!Number.isFinite(n) || !ok(n)) throw new Usage(`--${name} is "${v}"; it must be ${what}`);
90
+ return n;
91
+ }
92
+
93
+ function smoothOf(f: Map<string, string>): number {
94
+ const n = num(f, 'smooth', 1, (v) => Number.isInteger(v) && v >= 1 && v % 2 === 1, '1 (none) or an odd whole number ≥ 5');
95
+ if (n === 3) throw new Usage('--smooth is "3"; a cubic fitted through 3 points passes through all three, so a window of 3 changes nothing — it must be 1 (none) or an odd whole number ≥ 5');
96
+ return n;
97
+ }
98
+
99
+ function retargetArgs(argv: string[]): RetargetArgs {
100
+ const f = parseFlags(argv, ['rig', 'map', 'pose', 'out', 'name', 'person', 'base-motion', 'min-score', 'smooth', 'bends', 'bend-tolerance', 'profile', 'build', 'render', 'report'], ['coco']);
101
+ const bends = f.get('bends') ?? 'refuse';
102
+ if (bends !== 'refuse' && bends !== 'clamp') throw new Usage(`--bends is "${bends}"; it must be refuse or clamp`);
103
+ const profile = f.get('profile') ?? 'spine';
104
+ if (profile !== 'spine' && profile !== 'spine-html') throw new Usage(`--profile is "${profile}"; it must be spine or spine-html`);
105
+ const name = f.get('name') ?? 'dance';
106
+ if (!/^[A-Za-z0-9_.-]+$/.test(name)) throw new Usage(`--name is "${name}"; an animation name here is letters, digits, _ . -`);
107
+ return {
108
+ rig: need(f, 'rig'),
109
+ map: need(f, 'map'),
110
+ pose: need(f, 'pose'),
111
+ out: need(f, 'out'),
112
+ name,
113
+ coco: f.has('coco'),
114
+ person: f.get('person'),
115
+ baseMotion: f.get('base-motion'),
116
+ minScore: num(f, 'min-score', 0.3, (n) => n >= 0, 'a number ≥ 0'),
117
+ smooth: smoothOf(f),
118
+ bends,
119
+ bendTolerance: num(f, 'bend-tolerance', 0, (n) => n >= 0 && n < 180, 'degrees in [0, 180)'),
120
+ profile,
121
+ build: f.get('build'),
122
+ render: f.get('render'),
123
+ report: f.get('report'),
124
+ };
125
+ }
126
+
127
+ export function main(argv: string[]): number {
128
+ const [cmd, ...rest] = argv;
129
+ if (cmd === undefined || cmd === '--help' || cmd === '-h' || cmd === 'help') {
130
+ process.stdout.write(HELP);
131
+ return 0;
132
+ }
133
+ if (cmd === '--version' || cmd === '-v') {
134
+ process.stdout.write(`${pkg.version}\n`);
135
+ return 0;
136
+ }
137
+ try {
138
+ if (rest.includes('--help')) {
139
+ process.stdout.write(HELP);
140
+ return 0;
141
+ }
142
+ if (cmd === 'retarget') {
143
+ runRetarget(retargetArgs(rest), (l) => process.stdout.write(`${l}\n`));
144
+ return 0;
145
+ }
146
+ if (cmd === 'convert') {
147
+ const f = parseFlags(rest, ['coco', 'out'], []);
148
+ const input = need(f, 'coco');
149
+ const out = need(f, 'out');
150
+ const seq = convertCoco17(readFileSync(input, 'utf8'), input);
151
+ writeFileSync(out, writePoseSequence(seq));
152
+ process.stdout.write(`rig-pose convert: ${seq.frames.length} frame(s) of COCO-17 → body-18; wrote ${out}\n`);
153
+ return 0;
154
+ }
155
+ process.stderr.write(`rig-pose: unknown command "${cmd}" — the commands are: retarget, convert\n`);
156
+ return 2;
157
+ } catch (e) {
158
+ if (e instanceof Usage) {
159
+ process.stderr.write(`rig-pose ${cmd}: ${e.message} (rig-pose --help)\n`);
160
+ return 2;
161
+ }
162
+ if (e instanceof Refusal) {
163
+ for (const p of e.problems) process.stderr.write(`FAIL ${lineOf(p)}\n`);
164
+ process.stderr.write(`rig-pose ${cmd}: ${e.problems.length} problem(s) — nothing written\n`);
165
+ return 1;
166
+ }
167
+ throw e;
168
+ }
169
+ }
170
+
171
+ if (import.meta.main) process.exit(main(process.argv.slice(2)));
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "@rig-c/pose",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Puts motion from a body-18 pose sequence onto a rig built with rig-c: bone keys in rig-c's motion format, checked through rig-c's gate before anything is written. A CLI for agents that cannot see the rig.",
5
+ "type": "module",
6
+ "bin": {
7
+ "rig-pose": "./bin/rig-pose.cjs"
8
+ },
9
+ "engines": {
10
+ "bun": ">=1.2.0"
11
+ },
12
+ "license": "MIT",
13
+ "author": "firejune",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/rig-c/pose.git"
17
+ },
18
+ "homepage": "https://github.com/rig-c/pose#readme",
19
+ "bugs": "https://github.com/rig-c/pose/issues",
20
+ "keywords": [
21
+ "rigc",
22
+ "rig-c",
23
+ "pose",
24
+ "retarget",
25
+ "motion-retargeting",
26
+ "keypoints",
27
+ "body-18",
28
+ "coco",
29
+ "skeletal-animation",
30
+ "2d",
31
+ "ai-agents",
32
+ "cli"
33
+ ],
34
+ "files": [
35
+ "bin/rig-pose.cjs",
36
+ "cli.ts",
37
+ "src"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "scripts": {
43
+ "test": "bun test ./test",
44
+ "typecheck": "bunx tsc --noEmit",
45
+ "lint": "bunx eslint .",
46
+ "prepublishOnly": "bun run typecheck && bun run lint && bun run test"
47
+ },
48
+ "dependencies": {
49
+ "rig-c": "^2.47.1"
50
+ },
51
+ "devDependencies": {
52
+ "@esotericsoftware/spine-core": "4.3.13",
53
+ "@types/bun": "^1.4.2",
54
+ "eslint": "^10.11.0",
55
+ "typescript": "^5.9.3",
56
+ "typescript-eslint": "^8.70.1"
57
+ }
58
+ }
package/src/joints.ts ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The joint vocabulary: body-18, as @rig-c/parts spells it in
3
+ * `spine-parts-keypoints/1` (its `src/skeleton.ts`), and nothing else.
4
+ *
5
+ * `r_` and `l_` are the SUBJECT's sides: for a figure facing the viewer,
6
+ * `r_shoulder` is on the left of the picture.
7
+ */
8
+ export const BODY18 = [
9
+ 'nose', 'neck',
10
+ 'r_shoulder', 'r_elbow', 'r_wrist',
11
+ 'l_shoulder', 'l_elbow', 'l_wrist',
12
+ 'r_hip', 'r_knee', 'r_ankle',
13
+ 'l_hip', 'l_knee', 'l_ankle',
14
+ 'r_eye', 'l_eye', 'r_ear', 'l_ear',
15
+ ] as const;
16
+ export type Body18 = (typeof BODY18)[number];
17
+
18
+ /**
19
+ * Points a map may name that body-18 does not have, each the midpoint of two
20
+ * body-18 joints. A virtual point is good on a frame only when both of its
21
+ * joints are, and its score is the lower of the two.
22
+ */
23
+ export const VIRTUAL: Readonly<Record<string, readonly [Body18, Body18]>> = {
24
+ mid_hip: ['r_hip', 'l_hip'],
25
+ mid_shoulder: ['r_shoulder', 'l_shoulder'],
26
+ };
27
+
28
+ export const POINT_NAMES: readonly string[] = [...BODY18, ...Object.keys(VIRTUAL)];
29
+
30
+ export function isBody18(name: string): name is Body18 {
31
+ return (BODY18 as readonly string[]).includes(name);
32
+ }
33
+
34
+ /**
35
+ * COCO-17, the keypoint layout of the COCO dataset and of most pose models'
36
+ * output, in its standard order.
37
+ */
38
+ export const COCO17 = [
39
+ 'nose', 'left_eye', 'right_eye', 'left_ear', 'right_ear',
40
+ 'left_shoulder', 'right_shoulder', 'left_elbow', 'right_elbow',
41
+ 'left_wrist', 'right_wrist', 'left_hip', 'right_hip',
42
+ 'left_knee', 'right_knee', 'left_ankle', 'right_ankle',
43
+ ] as const;
44
+
45
+ /**
46
+ * COCO-17 → body-18. Sixteen joints are the same joint under another name
47
+ * (COCO's `left_` is the subject's left, as body-18's `l_` is); body-18's
48
+ * `neck`, which COCO does not have, is the midpoint of the two shoulders, as
49
+ * OpenPose defines it, observed only when both shoulders are and scored the
50
+ * lower of the two.
51
+ */
52
+ export const COCO17_TO_BODY18: Readonly<Record<(typeof COCO17)[number], Body18>> = {
53
+ nose: 'nose',
54
+ left_eye: 'l_eye',
55
+ right_eye: 'r_eye',
56
+ left_ear: 'l_ear',
57
+ right_ear: 'r_ear',
58
+ left_shoulder: 'l_shoulder',
59
+ right_shoulder: 'r_shoulder',
60
+ left_elbow: 'l_elbow',
61
+ right_elbow: 'r_elbow',
62
+ left_wrist: 'l_wrist',
63
+ right_wrist: 'r_wrist',
64
+ left_hip: 'l_hip',
65
+ right_hip: 'r_hip',
66
+ left_knee: 'l_knee',
67
+ right_knee: 'r_knee',
68
+ left_ankle: 'l_ankle',
69
+ right_ankle: 'r_ankle',
70
+ };