@rig-c/review 0.0.0-stage → 0.2.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 +21 -0
- package/README.md +376 -2
- package/docs/DESIGN.md +72 -0
- package/docs/REVIEW_PACKAGE.md +627 -0
- package/examples/demo/SOURCE.md +5 -0
- package/examples/demo/chime.wav +0 -0
- package/examples/demo/lantern.svg +10 -0
- package/examples/demo/pose.json +244 -0
- package/examples/demo/review.json +67 -0
- package/package.json +49 -4
- package/src/animation-selection.ts +29 -0
- package/src/build.ts +8 -0
- package/src/cli.ts +116 -0
- package/src/compare-playhead.ts +55 -0
- package/src/compare-presentation.ts +43 -0
- package/src/compare-server.ts +63 -0
- package/src/compare-session.ts +26 -0
- package/src/compare.ts +57 -0
- package/src/comparison-eligibility.ts +54 -0
- package/src/control-server.ts +83 -0
- package/src/control.ts +132 -0
- package/src/feedback-export.ts +197 -0
- package/src/feedback-validation.ts +38 -0
- package/src/frame-clock.ts +36 -0
- package/src/front-worker.ts +2 -0
- package/src/front.ts +127 -0
- package/src/manifest.ts +140 -0
- package/src/open-protocol.ts +31 -0
- package/src/open-selection.ts +11 -0
- package/src/open-shell.ts +2 -0
- package/src/open-worker.ts +2 -0
- package/src/open.ts +142 -0
- package/src/pose.ts +27 -0
- package/src/probe-reference.ts +16 -0
- package/src/reference-frame-index.ts +16 -0
- package/src/reference-frames.ts +62 -0
- package/src/reference.ts +63 -0
- package/src/report.ts +88 -0
- package/src/server.ts +62 -0
- package/src/session-history.ts +99 -0
- package/src/session.ts +58 -0
- package/src/strip-export.ts +69 -0
- package/src/strip-render.ts +171 -0
- package/src/strip.ts +51 -0
- package/src/versions.ts +20 -0
- package/web/camera-view.ts +44 -0
- package/web/compare.css +140 -0
- package/web/compare.html +7 -0
- package/web/compare.ts +546 -0
- package/web/control.ts +67 -0
- package/web/index.html +13 -0
- package/web/inspection-persistence.ts +219 -0
- package/web/instance-access.ts +23 -0
- package/web/main.ts +972 -0
- package/web/part-selection.ts +136 -0
- package/web/renderer-status.ts +31 -0
- package/web/report-view.ts +51 -0
- package/web/rig-overlays.ts +150 -0
- package/web/styles.css +139 -0
- package/web/view-controls.ts +69 -0
- package/web/workspace-layout.ts +84 -0
- package/web/workspace-preferences.ts +95 -0
- package/web/workspace-ui.ts +91 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Abstract lantern source study
|
|
2
|
+
|
|
3
|
+
The SVG, synthetic canonical pose sequence coordinates, and short synthetic sine-wave chime were authored for this repository. They contain no imported or private art.
|
|
4
|
+
|
|
5
|
+
This package deliberately has no compiled rig. It demonstrates source comparison, checkpoint notes, audio audition, explicit review status, and structured frame/target/category feedback. Playback becomes available when an agent supplies `model` pointing to a gated rigc build's `skeleton.model.json`, with its local page images. The pose sequence demonstrates the canonical `rig-pose-sequence/1` transport shape; its coordinates were authored here and were not extracted or measured from footage. This source sketch is not proof of rig quality.
|
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" viewBox="0 0 800 800">
|
|
2
|
+
<rect width="800" height="800" fill="#182029"/>
|
|
3
|
+
<circle cx="400" cy="390" r="270" fill="#202c38"/>
|
|
4
|
+
<path d="M400 92V220" stroke="#89aaa9" stroke-width="16"/>
|
|
5
|
+
<circle cx="400" cy="220" r="22" fill="#a8c8c3"/>
|
|
6
|
+
<path d="M310 275L400 238L490 275L520 487L400 560L280 487Z" fill="#e9bb69" stroke="#f8dc9c" stroke-width="9"/>
|
|
7
|
+
<path d="M310 275L400 320L490 275M400 320V560M280 487L400 435L520 487" fill="none" stroke="#b87e4b" stroke-width="10"/>
|
|
8
|
+
<path d="M400 568V650M374 650H426" stroke="#89aaa9" stroke-width="13" stroke-linecap="round"/>
|
|
9
|
+
<text x="400" y="733" text-anchor="middle" fill="#a8b9c3" font-family="sans-serif" font-size="20">ORIGINAL SOURCE STUDY · NO COMPILED MODEL</text>
|
|
10
|
+
</svg>
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
{
|
|
2
|
+
"spec": "rig-pose-sequence/1",
|
|
3
|
+
"space": {
|
|
4
|
+
"units": "picture-px",
|
|
5
|
+
"origin": "top-left",
|
|
6
|
+
"y": "down"
|
|
7
|
+
},
|
|
8
|
+
"width": 800,
|
|
9
|
+
"height": 800,
|
|
10
|
+
"source": "synthetic-schema-example-not-a-Nova-extraction",
|
|
11
|
+
"frames": [
|
|
12
|
+
{
|
|
13
|
+
"t": 0,
|
|
14
|
+
"people": [
|
|
15
|
+
{
|
|
16
|
+
"id": "sample",
|
|
17
|
+
"joints": {
|
|
18
|
+
"neck": {
|
|
19
|
+
"state": "observed",
|
|
20
|
+
"at": [
|
|
21
|
+
400,
|
|
22
|
+
220
|
|
23
|
+
],
|
|
24
|
+
"score": 1
|
|
25
|
+
},
|
|
26
|
+
"r_shoulder": {
|
|
27
|
+
"state": "observed",
|
|
28
|
+
"at": [
|
|
29
|
+
310,
|
|
30
|
+
275
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"r_elbow": {
|
|
34
|
+
"state": "observed",
|
|
35
|
+
"at": [
|
|
36
|
+
280,
|
|
37
|
+
387
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"r_wrist": {
|
|
41
|
+
"state": "observed",
|
|
42
|
+
"at": [
|
|
43
|
+
280,
|
|
44
|
+
487
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"l_shoulder": {
|
|
48
|
+
"state": "observed",
|
|
49
|
+
"at": [
|
|
50
|
+
490,
|
|
51
|
+
275
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"l_elbow": {
|
|
55
|
+
"state": "observed",
|
|
56
|
+
"at": [
|
|
57
|
+
520,
|
|
58
|
+
387
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"l_wrist": {
|
|
62
|
+
"state": "observed",
|
|
63
|
+
"at": [
|
|
64
|
+
520,
|
|
65
|
+
487
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
"r_hip": {
|
|
69
|
+
"state": "observed",
|
|
70
|
+
"at": [
|
|
71
|
+
374,
|
|
72
|
+
560
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
"l_hip": {
|
|
76
|
+
"state": "observed",
|
|
77
|
+
"at": [
|
|
78
|
+
426,
|
|
79
|
+
560
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
"nose": {
|
|
83
|
+
"state": "missing"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"t": 0.5,
|
|
91
|
+
"people": [
|
|
92
|
+
{
|
|
93
|
+
"id": "sample",
|
|
94
|
+
"joints": {
|
|
95
|
+
"neck": {
|
|
96
|
+
"state": "observed",
|
|
97
|
+
"at": [
|
|
98
|
+
420,
|
|
99
|
+
220
|
|
100
|
+
],
|
|
101
|
+
"score": 1
|
|
102
|
+
},
|
|
103
|
+
"r_shoulder": {
|
|
104
|
+
"state": "observed",
|
|
105
|
+
"at": [
|
|
106
|
+
330,
|
|
107
|
+
275
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
"r_elbow": {
|
|
111
|
+
"state": "observed",
|
|
112
|
+
"at": [
|
|
113
|
+
300,
|
|
114
|
+
387
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
"r_wrist": {
|
|
118
|
+
"state": "observed",
|
|
119
|
+
"at": [
|
|
120
|
+
300,
|
|
121
|
+
487
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
"l_shoulder": {
|
|
125
|
+
"state": "observed",
|
|
126
|
+
"at": [
|
|
127
|
+
510,
|
|
128
|
+
275
|
|
129
|
+
]
|
|
130
|
+
},
|
|
131
|
+
"l_elbow": {
|
|
132
|
+
"state": "observed",
|
|
133
|
+
"at": [
|
|
134
|
+
540,
|
|
135
|
+
387
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
"l_wrist": {
|
|
139
|
+
"state": "observed",
|
|
140
|
+
"at": [
|
|
141
|
+
540,
|
|
142
|
+
487
|
|
143
|
+
]
|
|
144
|
+
},
|
|
145
|
+
"r_hip": {
|
|
146
|
+
"state": "observed",
|
|
147
|
+
"at": [
|
|
148
|
+
394,
|
|
149
|
+
560
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
"l_hip": {
|
|
153
|
+
"state": "observed",
|
|
154
|
+
"at": [
|
|
155
|
+
446,
|
|
156
|
+
560
|
|
157
|
+
]
|
|
158
|
+
},
|
|
159
|
+
"nose": {
|
|
160
|
+
"state": "missing"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"t": 1,
|
|
168
|
+
"people": [
|
|
169
|
+
{
|
|
170
|
+
"id": "sample",
|
|
171
|
+
"joints": {
|
|
172
|
+
"neck": {
|
|
173
|
+
"state": "observed",
|
|
174
|
+
"at": [
|
|
175
|
+
400,
|
|
176
|
+
220
|
|
177
|
+
],
|
|
178
|
+
"score": 1
|
|
179
|
+
},
|
|
180
|
+
"r_shoulder": {
|
|
181
|
+
"state": "observed",
|
|
182
|
+
"at": [
|
|
183
|
+
310,
|
|
184
|
+
275
|
|
185
|
+
]
|
|
186
|
+
},
|
|
187
|
+
"r_elbow": {
|
|
188
|
+
"state": "observed",
|
|
189
|
+
"at": [
|
|
190
|
+
280,
|
|
191
|
+
387
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
"r_wrist": {
|
|
195
|
+
"state": "observed",
|
|
196
|
+
"at": [
|
|
197
|
+
280,
|
|
198
|
+
487
|
|
199
|
+
]
|
|
200
|
+
},
|
|
201
|
+
"l_shoulder": {
|
|
202
|
+
"state": "observed",
|
|
203
|
+
"at": [
|
|
204
|
+
490,
|
|
205
|
+
275
|
|
206
|
+
]
|
|
207
|
+
},
|
|
208
|
+
"l_elbow": {
|
|
209
|
+
"state": "observed",
|
|
210
|
+
"at": [
|
|
211
|
+
520,
|
|
212
|
+
387
|
|
213
|
+
]
|
|
214
|
+
},
|
|
215
|
+
"l_wrist": {
|
|
216
|
+
"state": "observed",
|
|
217
|
+
"at": [
|
|
218
|
+
520,
|
|
219
|
+
487
|
|
220
|
+
]
|
|
221
|
+
},
|
|
222
|
+
"r_hip": {
|
|
223
|
+
"state": "observed",
|
|
224
|
+
"at": [
|
|
225
|
+
374,
|
|
226
|
+
560
|
|
227
|
+
]
|
|
228
|
+
},
|
|
229
|
+
"l_hip": {
|
|
230
|
+
"state": "observed",
|
|
231
|
+
"at": [
|
|
232
|
+
426,
|
|
233
|
+
560
|
|
234
|
+
]
|
|
235
|
+
},
|
|
236
|
+
"nose": {
|
|
237
|
+
"state": "missing"
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
]
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "rig-review/1",
|
|
3
|
+
"id": "abstract-lantern",
|
|
4
|
+
"title": "Lantern \u00b7 first inspection",
|
|
5
|
+
"revision": "source-study-1",
|
|
6
|
+
"references": [
|
|
7
|
+
{
|
|
8
|
+
"id": "source",
|
|
9
|
+
"label": "Original geometric study",
|
|
10
|
+
"type": "image",
|
|
11
|
+
"path": "lantern.svg",
|
|
12
|
+
"width": 800,
|
|
13
|
+
"height": 800
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "pose",
|
|
17
|
+
"label": "Canonical pose sequence \u00b7 synthetic example",
|
|
18
|
+
"type": "pose",
|
|
19
|
+
"path": "pose.json"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"animations": [],
|
|
23
|
+
"checkpoints": [
|
|
24
|
+
{
|
|
25
|
+
"id": "silhouette",
|
|
26
|
+
"title": "Check silhouette and hanging joint",
|
|
27
|
+
"workspace": "modeling",
|
|
28
|
+
"reference": "source",
|
|
29
|
+
"note": "This original abstract SVG is source material. No compiled model is included; record structured feedback rather than assuming playback."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "joints",
|
|
33
|
+
"title": "Check the intended joint chain",
|
|
34
|
+
"workspace": "modeling",
|
|
35
|
+
"reference": "pose",
|
|
36
|
+
"note": "An authored synthetic rig-pose-sequence/1 schema example, not extracted from footage. Picture pixels, top-left origin, Y down; no rig binding is inferred."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "sound",
|
|
40
|
+
"title": "Audition the proposed soft chime",
|
|
41
|
+
"workspace": "motion",
|
|
42
|
+
"note": "Play the local original tone and leave an audio note. No animation timing is claimed."
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"authoring": [
|
|
46
|
+
{
|
|
47
|
+
"label": "Source provenance and next build",
|
|
48
|
+
"path": "SOURCE.md"
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"audio": {
|
|
52
|
+
"clips": [
|
|
53
|
+
{
|
|
54
|
+
"id": "chime",
|
|
55
|
+
"label": "Original synthetic chime",
|
|
56
|
+
"path": "chime.wav"
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"cues": [
|
|
60
|
+
{
|
|
61
|
+
"id": "sample",
|
|
62
|
+
"clip": "chime",
|
|
63
|
+
"time": 0
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/review",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Local human rig inspection and structured feedback export",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"review": "bun src/cli.ts",
|
|
9
|
+
"check": "bun src/cli.ts check",
|
|
10
|
+
"demo": "bun src/cli.ts examples/demo/review.json",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"test": "bun test",
|
|
13
|
+
"build": "bun src/build.ts",
|
|
14
|
+
"feedback": "bun src/cli.ts feedback",
|
|
15
|
+
"open": "bun src/cli.ts open",
|
|
16
|
+
"smoke": "bun scripts/install-smoke.ts"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@napi-rs/canvas": "1.0.11",
|
|
20
|
+
"@rig-c/play": "1.12.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/bun": "^1.3.0",
|
|
24
|
+
"typescript": "^5.9.3"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"bun": ">=1.3.0"
|
|
28
|
+
},
|
|
29
|
+
"bin": {
|
|
30
|
+
"rig-review": "src/cli.ts"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"src",
|
|
34
|
+
"web",
|
|
35
|
+
"examples/demo",
|
|
36
|
+
"docs/REVIEW_PACKAGE.md",
|
|
37
|
+
"docs/DESIGN.md",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"exports": {
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
},
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/rig-c/review.git"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Actual catalog membership selects a clip; report timing never invents one. */
|
|
2
|
+
import type {FeedbackCatalog} from './feedback-validation.ts';
|
|
3
|
+
import type {ReportSeeds} from './report.ts';
|
|
4
|
+
export interface AnimationSelection {name:string;source:'cli'|'report'|'unique';reportPaths?:string[]}
|
|
5
|
+
interface Source {label:string;catalog:Pick<FeedbackCatalog,'animations'>;report:Pick<ReportSeeds,'animation'|'path'>|null}
|
|
6
|
+
export function selectAnimation(sources:Source[],explicit?:string,allowStatic=false):AnimationSelection|null {
|
|
7
|
+
if(!sources.length||sources.length>2)throw new Error('open.animation: expected one or two actual catalogs');
|
|
8
|
+
if(explicit!==undefined&&(!explicit.trim()))throw new Error('open.animation: expected nonempty animation name');
|
|
9
|
+
const requireName=(name:string)=>{for(const source of sources){if(source.catalog.animations===null)throw new Error(`open.${source.label}.animation: validation unavailable without compiled model`);if(!source.catalog.animations.some(clip=>clip.name===name))throw new Error(`open.${source.label}.animation: model has no animation ${JSON.stringify(name)}`);}};
|
|
10
|
+
if(explicit!==undefined){requireName(explicit);return {name:explicit,source:'cli'};}
|
|
11
|
+
if(allowStatic&&sources.length===1&&sources[0]!.catalog.animations===null)return null;
|
|
12
|
+
const reports=sources.filter(source=>source.report?.animation!==undefined);
|
|
13
|
+
const names=[...new Set(reports.map(source=>source.report!.animation!))];
|
|
14
|
+
if(names.length>1)throw new Error(`open.animation: conflicting report animations: ${reports.map(source=>`${source.label}=${JSON.stringify(source.report!.animation)}`).join(', ')}; select --animation explicitly`);
|
|
15
|
+
if(names.length===1){requireName(names[0]!);return {name:names[0]!,source:'report',reportPaths:[...new Set(reports.map(source=>source.report!.path))]};}
|
|
16
|
+
if(sources.some(source=>source.catalog.animations===null))throw new Error('open.animation: validation unavailable without compiled models');
|
|
17
|
+
const common=sources[0]!.catalog.animations!.filter(clip=>sources.every(source=>source.catalog.animations!.some(other=>other.name===clip.name))).map(clip=>clip.name);
|
|
18
|
+
if(allowStatic&&sources.length===1&&sources[0]!.catalog.animations!.length===0)return null;
|
|
19
|
+
if(common.length!==1)throw new Error(`open.animation: expected --animation; ${sources.length>1?'common animations':'model animations'}: ${common.join(', ')||'(none)'}`);
|
|
20
|
+
return {name:common[0]!,source:'unique'};
|
|
21
|
+
}
|
|
22
|
+
export function animationDiagnostic(selection:AnimationSelection):string {
|
|
23
|
+
return `animation: ${selection.name} (${selection.source==='cli'?'from --animation':selection.source==='report'?'from '+selection.reportPaths!.join(', '):'unique actual clip'})`;
|
|
24
|
+
}
|
|
25
|
+
/** Runtime init validates again against loaded public assets; never fall back on stale hints. */
|
|
26
|
+
export function initialPlayerAnimation(names:readonly string[],selected:string|undefined,checkpoint?:{animation?:string}):string|null {
|
|
27
|
+
const name=checkpoint?.animation??selected;if(name!==undefined){if(!names.includes(name))throw new Error(`initial.animation: model has no animation ${JSON.stringify(name)}`);return name;}
|
|
28
|
+
return names[0]??null;
|
|
29
|
+
}
|
package/src/build.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
export const appRoot=fileURLToPath(new URL('..',import.meta.url));
|
|
3
|
+
export async function buildWeb(mode:'single'|'compare'='single'):Promise<Blob> {
|
|
4
|
+
const result=await Bun.build({entrypoints:[appRoot+(mode==='compare'?'web/compare.ts':'web/main.ts')],target:'browser',format:'esm',minify:false});
|
|
5
|
+
if(!result.success)throw new Error(result.logs.map(v=>v.message).join('\n'));
|
|
6
|
+
const output=result.outputs.find(v=>v.path.endsWith('.js'));if(!output)throw new Error('frontend bundle: JavaScript output missing');return output;
|
|
7
|
+
}
|
|
8
|
+
if(import.meta.main){await buildWeb();await buildWeb('compare');console.log('Frontend bundles built successfully (single and compare).');}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import {randomBytes} from 'node:crypto';
|
|
3
|
+
import {parseControlCommand} from './control.ts';
|
|
4
|
+
import { loadReview } from './manifest.ts';
|
|
5
|
+
import { formatFeedbackIssues } from './feedback-export.ts';
|
|
6
|
+
import {loadReferencedReview,selectedReference,assertReviewUnchanged} from './reference.ts';
|
|
7
|
+
import {animationDiagnostic,selectAnimation} from './animation-selection.ts';
|
|
8
|
+
import {shellWord} from './open-shell.ts';
|
|
9
|
+
import {openReview,controlReview,loadOpenPackage,defaultStateDir,type OpenInputs} from './open.ts';
|
|
10
|
+
import { readFeedbackSession, readFeedbackCompareSession } from './session-history.ts';
|
|
11
|
+
import { startServer } from './server.ts';
|
|
12
|
+
import { loadCompare } from './compare.ts';
|
|
13
|
+
import { startCompareServer } from './compare-server.ts';
|
|
14
|
+
import { readSession, type Feedback } from './session.ts';
|
|
15
|
+
import { readCompareSession } from './compare-session.ts';
|
|
16
|
+
import { exportReviewStrip, exportCompareStrip, stripJson, writeStripOutput, validateStripOutput } from './strip-export.ts';
|
|
17
|
+
import type { StripInterval, StripRequest } from './strip.ts';
|
|
18
|
+
const usage='Usage: rig-review open <dir> [--reference <video>] [--compare <dir>] [--animation <name>] [--control] [--direct]\n open is managed by default: one loopback front at 127.0.0.1:4318 (RIG_REVIEW_FRONT_PORT overrides) serves /instances/<key>/; --direct serves the worker on its own port.\n rig-review control <dir> [--reference <video>] [--compare <dir>] [--animation <name>] [--page <id> --expected-revision <n> --command <JSON> --wait-ms <n>]\n rig-review feedback <dir> [--reference <video>] [--compare <dir>] [--animation <name>] [--format json|issue]\n Usage: bun run review -- <review.json|output-directory> [--port 4317] [--checkpoint ID]\n bun run check -- <review.json|output-directory>\n bun run feedback -- <review.json|output-directory>\n bun run review -- compare <a> <b> --reference <video> --animation <name> [--port 4317]\n bun run check -- compare <a> <b> --reference <video> --animation <name>\n bun run feedback -- compare <a> <b> --reference <video> --animation <name>';
|
|
19
|
+
async function stripCommand(args:string[],options:{stateDir?:string}){
|
|
20
|
+
const compare=args[0]==='compare';if(compare)args.shift();const a=args.shift(),b=compare?args.shift():undefined;if(!a||compare&&!b)throw new Error('strip: expected package input(s)');
|
|
21
|
+
let reference:string|undefined,referenceVideo:string|undefined,animation:string|undefined,basis:'a'|'b'|undefined,range:string|undefined,output:string|undefined,jsonOnly=false,all=false;
|
|
22
|
+
while(args.length){const flag=args.shift();if(flag==='--json'){jsonOnly=true;continue;}if(flag==='--all-feedback'){all=true;continue;}const value=args.shift();if(!value)throw new Error(`Missing value for ${flag}`);if(flag==='--reference')reference=value;else if(flag==='--reference-video')referenceVideo=value;else if(flag==='--animation')animation=value;else if(flag==='--basis'){if(value!=='a'&&value!=='b')throw new Error('strip --basis: expected a | b');basis=value;}else if(flag==='--frames')range=value;else if(flag==='--out')output=value;else throw new Error(`Unknown strip option ${flag}`);}
|
|
23
|
+
if(!animation||(!reference&&!referenceVideo))throw new Error('strip: --animation and explicit --reference are required (single uses declared reference id, compare uses local video)');
|
|
24
|
+
if(all===!!range)throw new Error('strip: choose exactly one --frames FIRST:LAST or --all-feedback');
|
|
25
|
+
if(all&&basis)throw new Error('strip --basis: use with --frames; feedback batches retain each recorded side');
|
|
26
|
+
if(!jsonOnly&&!output)throw new Error('strip: PNG requires --out outside source packages');
|
|
27
|
+
let intervals:StripInterval[]=[];
|
|
28
|
+
if(range){const match=/^(\d+):(\d+)$/.exec(range);if(!match)throw new Error('strip --frames: expected inclusive FIRST:LAST');if(compare&&!basis)throw new Error('strip --basis: explicitly select a | b');intervals=[{id:'selection',basis:compare?basis!:'single',frameStart:Number(match[1]),frameEnd:Number(match[2])}];}
|
|
29
|
+
const request=(hash:string):StripRequest=>({format:jsonOnly?'json':'png',sourceHash:hash,animation:animation!,...(compare?{}:{referenceId:reference}),intervals});
|
|
30
|
+
const fromFeedback=(feedback:(Feedback&{side?:'a'|'b'})[])=>feedback.map(f=>{const context=f.context;if(!context||typeof context!=='object'||Array.isArray(context)||context.animation!==animation)throw new Error('strip --all-feedback: every interval must identify the selected animation');return {id:f.id,feedbackId:f.id,basis:compare?f.side!:'single' as const,frameStart:f.frameStart,frameEnd:f.frameEnd};});
|
|
31
|
+
let result,roots:string[];
|
|
32
|
+
if(compare){if(referenceVideo)throw new Error('strip compare: use --reference local-video');const p=await loadCompare({a,b:b!,reference:reference!,animation});roots=[p.a.root,p.b.root];if(all)intervals=fromFeedback((await readCompareSession(p,options.stateDir)).feedback);if(output){await validateStripOutput(output,roots);if(!jsonOnly)await validateStripOutput(output+'.json',roots);}result=await exportCompareStrip(p,request(p.response.sourceHash));}
|
|
33
|
+
else {if(basis)throw new Error('strip --basis: single package uses single basis');const p=await loadReferencedReview(a,referenceVideo);if(!reference)reference=selectedReference(p)?.binding.reference.id;roots=[p.root];if(all)intervals=fromFeedback((await readSession(p,options.stateDir)).feedback);if(output){await validateStripOutput(output,roots);if(!jsonOnly)await validateStripOutput(output+'.json',roots);}result=await exportReviewStrip(p,request(p.sourceHash));}
|
|
34
|
+
if(output){await writeStripOutput(result,output,roots);console.log(`Strip exported: ${output}${result.png?' (PNG + .json provenance)':''}`);}else console.log(stripJson(result).trimEnd());
|
|
35
|
+
}
|
|
36
|
+
export function parseOpenInputs(args:string[]):OpenInputs {
|
|
37
|
+
const input=args.shift();if(!input||input.startsWith('--'))throw new Error(usage);const result:OpenInputs={input},seen=new Set<string>();
|
|
38
|
+
while(args.length){const flag=args.shift()!;if(seen.has(flag))throw new Error(`Duplicate open option ${flag}`);seen.add(flag);if(flag==='--control'){result.control=true;continue;}if(flag==='--direct'){result.direct=true;continue;}const value=args.shift();if(value===undefined||value==='')throw new Error(`Missing value for ${flag}`);if(flag==='--reference')result.reference=value;else if(flag==='--compare')result.compare=value;else if(flag==='--animation')result.animation=value;else throw new Error(`Unknown open option ${flag}`);}return result;
|
|
39
|
+
}
|
|
40
|
+
async function controlCommand(args:string[],stateDir?:string){
|
|
41
|
+
const input=args.shift();if(!input||input==='--help'||input==='-h'){console.log(usage);return;}
|
|
42
|
+
const inputs:OpenInputs={input},options:Parameters<typeof controlReview>[1]={stateDir},seen=new Set<string>();
|
|
43
|
+
while(args.length){const flag=args.shift()!;if(seen.has(flag))throw new Error(`Duplicate control option ${flag}`);seen.add(flag);const value=args.shift();if(value===undefined||value==='')throw new Error(`Missing value for ${flag}`);
|
|
44
|
+
if(flag==='--reference')inputs.reference=value;else if(flag==='--compare')inputs.compare=value;else if(flag==='--animation')inputs.animation=value;
|
|
45
|
+
else if(flag==='--page')options.pageId=value;else if(flag==='--request-id')options.requestId=value;
|
|
46
|
+
else if(flag==='--expected-revision'){const n=Number(value);if(!/^\d+$/.test(value)||!Number.isSafeInteger(n))throw new Error('--expected-revision: expected safe integer >= 0');options.expectedRevision=n;}
|
|
47
|
+
else if(flag==='--wait-ms'){const n=Number(value);if(!/^\d+$/.test(value)||!Number.isSafeInteger(n)||n>30000)throw new Error('--wait-ms: expected integer 0..30000');options.waitMs=n;}
|
|
48
|
+
else if(flag==='--command'){if(value.length>8192)throw new Error('--command: JSON exceeds 8 KiB');options.command=parseControlCommand(JSON.parse(value));}
|
|
49
|
+
else throw new Error(`Unknown control option ${flag}`);
|
|
50
|
+
}
|
|
51
|
+
const result=await controlReview(inputs,options);console.log(JSON.stringify(result,null,2));if(options.command&&(result as {status?:string}).status!=='applied')process.exitCode=2;
|
|
52
|
+
}
|
|
53
|
+
/** Formatting is CLI-only; session lookup/save contracts remain unchanged. */
|
|
54
|
+
export function feedbackFormat(args:string[]):{args:string[];format:'json'|'issue'} {
|
|
55
|
+
const remaining:string[]=[];let format:'json'|'issue'='json',seen=false,inputs=-1;
|
|
56
|
+
for(let i=0;i<args.length;i++){
|
|
57
|
+
const token=args[i]!;
|
|
58
|
+
// Compare owns its next two positional inputs, even when they look like flags.
|
|
59
|
+
if(inputs>0){remaining.push(token);inputs--;continue;}
|
|
60
|
+
if(token==='--format'){
|
|
61
|
+
if(seen)throw new Error('feedback --format: duplicate option');seen=true;
|
|
62
|
+
const value=args[++i];if(value!=='json'&&value!=='issue')throw new Error('feedback --format: expected json | issue');format=value;continue;
|
|
63
|
+
}
|
|
64
|
+
remaining.push(token);
|
|
65
|
+
if(inputs<0){inputs=token==='compare'?2:0;continue;}
|
|
66
|
+
// The existing CLI consumes option/value pairs. Leave their validation to it;
|
|
67
|
+
// in particular, --animation may legitimately own the value "--format".
|
|
68
|
+
const value=args[++i];if(value!==undefined)remaining.push(value);
|
|
69
|
+
}
|
|
70
|
+
return {args:remaining,format};
|
|
71
|
+
}
|
|
72
|
+
/** Help flags are options; positional inputs and known option values remain literal. */
|
|
73
|
+
function commandHelpRequested(args:readonly string[],command:'open'|'review'|'check'|'feedback'):boolean {
|
|
74
|
+
if(args[0]==='--help'||args[0]==='-h')return true;
|
|
75
|
+
const compare=command!=='open'&&args[0]==='compare';
|
|
76
|
+
const flags=command==='open'?['--reference','--compare','--animation']:compare?['--reference','--animation','--port']:['--port','--checkpoint','--reference','--compare','--animation'];
|
|
77
|
+
for(let i=compare?3:1;i<args.length;){
|
|
78
|
+
const flag=args[i];if(flag==='--help'||flag==='-h')return true;
|
|
79
|
+
if(command==='open'&&(flag==='--control'||flag==='--direct')){i++;continue;}if(!flags.includes(flag!))return false;i+=2;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
export async function main(args:string[],options:{stateDir?:string}={}){
|
|
84
|
+
options={stateDir:options.stateDir??defaultStateDir()};if(args[0]==='--')args.shift();
|
|
85
|
+
if(args[0]==='control')return controlCommand(args.slice(1),options.stateDir);
|
|
86
|
+
if(args[0]==='open'){const openArgs=args.slice(1);if(commandHelpRequested(openArgs,'open')){console.log(usage);return;}const inputs=parseOpenInputs(openArgs);const result=await openReview(inputs,options);console.log(result.url);if(inputs.control)console.error('Inspection control enabled; query the current connected page with rig-review control and the same inputs.');if(result.selection)console.error(animationDiagnostic(result.selection));console.error(`Review ${result.reused?'reused':'started'}; feedback: rig-review feedback ${[result.inputs.input,...(result.inputs.compare?['--compare',result.inputs.compare]:[]),...(result.inputs.reference?['--reference',result.inputs.reference]:[]),...(result.inputs.animation?['--animation',result.inputs.animation]:[])].map(shellWord).join(' ')}`);return;}
|
|
87
|
+
if(args[0]==='strip'){args.shift();if(args.includes('--help')){console.log(usage+'\nStrip: strip <package> --reference <declared-video-id> --animation <name> (--frames FIRST:LAST | --all-feedback) [--json] [--out path]\n strip compare <a> <b> --reference <video> --animation <name> --basis a|b (--frames FIRST:LAST | --all-feedback) [--json] [--out path]');return;}return stripCommand(args,options);}let command:'review'|'check'|'feedback'='review';if(args[0]==='check'||args[0]==='feedback'){command=args.shift() as 'check'|'feedback';}
|
|
88
|
+
if(args[0]==='--help'||args[0]==='-h'){console.log(usage);return;}
|
|
89
|
+
let format:'json'|'issue'='json';if(command==='feedback'){const parsed=feedbackFormat(args);args=parsed.args;format=parsed.format;}
|
|
90
|
+
// Feedback formatting assigns --format/value ownership before the help pass.
|
|
91
|
+
if(commandHelpRequested(args,command)){console.log(usage);return;}
|
|
92
|
+
const printFeedback=(session:Parameters<typeof formatFeedbackIssues>[0])=>console.log(format==='issue'?formatFeedbackIssues(session).trimEnd():JSON.stringify(session,null,2));
|
|
93
|
+
if(args[0]==='compare'){
|
|
94
|
+
args.shift();const a=args.shift(),b=args.shift();if(!a||!b)throw new Error(usage);let reference:string|undefined,animation:string|undefined,port=4317;
|
|
95
|
+
while(args.length){const flag=args.shift(),value=args.shift();if(!value)throw new Error(`Missing value for ${flag}`);if(flag==='--reference')reference=value;else if(flag==='--animation')animation=value;else if(flag==='--port'){port=Number(value);if(!Number.isInteger(port)||port<0||port>65535)throw new Error('--port: expected integer 0…65535');}else throw new Error(`Unknown compare option ${flag}`);}
|
|
96
|
+
if(!reference)throw new Error('compare: --reference and --animation are required');
|
|
97
|
+
// A report naming the clip stands in for --animation; without one the flag stays required.
|
|
98
|
+
if(animation===undefined){const [ra,rb]=await Promise.all([loadReview(a),loadReview(b)]);if(ra.report?.animation===undefined&&rb.report?.animation===undefined)throw new Error('compare: --reference and --animation are required');const selection=selectAnimation([{label:'a',catalog:ra.feedbackCatalog,report:ra.report},{label:'b',catalog:rb.feedbackCatalog,report:rb.report}])!;animation=selection.name;console.error(animationDiagnostic(selection));}
|
|
99
|
+
const p=await loadCompare({a,b,reference,animation});
|
|
100
|
+
if(command==='feedback'){printFeedback(await readFeedbackCompareSession(p,options.stateDir));return;}
|
|
101
|
+
if(command==='check'){console.log(`Comparison valid: ${p.response.title}\nSource SHA-256: ${p.response.sourceHash}\nAnimation: ${animation}\nReference SHA-256: ${p.response.reference.sha256}\nPlayback: two compiled models present`);return;}
|
|
102
|
+
const capability=randomBytes(32).toString('hex'),server=await startCompareServer(p,{port,stateDir:options.stateDir,capability});console.log(`Compare ready: http://127.0.0.1:${server.port}/#cap=${capability}\nComparison: ${p.response.title}\nSource SHA-256: ${p.response.sourceHash}\nPress Ctrl+C to stop.`);return;
|
|
103
|
+
}
|
|
104
|
+
const input=args.shift();if(!input)throw new Error(usage);let port=4317;let checkpoint:string|undefined,reference:string|undefined,compare:string|undefined,animation:string|undefined;
|
|
105
|
+
while(args.length){const flag=args.shift();const value=args.shift();if(!value)throw new Error(`Missing value for ${flag}`);if(flag==='--port'){port=Number(value);if(!Number.isInteger(port)||port<0||port>65535)throw new Error('--port: expected integer 0…65535');}else if(flag==='--checkpoint')checkpoint=value;else if(flag==='--reference')reference=value;else if(flag==='--compare')compare=value;else if(flag==='--animation')animation=value;else throw new Error(`Unknown option ${flag}\n${usage}`);}
|
|
106
|
+
if(compare){if(command!=='feedback')throw new Error('--compare: use open or feedback');const loaded=await loadOpenPackage({input,compare,reference,animation});if(loaded.mode!=='compare')throw new Error('Expected comparison');printFeedback(await readFeedbackCompareSession(loaded.p,options.stateDir));return;}
|
|
107
|
+
const p=await loadReferencedReview(input,reference);// Serving starts on the report's clip when it names one; check and feedback validate only an explicit flag.
|
|
108
|
+
const fromReport=command==='review'&&animation===undefined&&p.report?.animation!==undefined;
|
|
109
|
+
const selection=animation!==undefined||fromReport?selectAnimation([{label:'single',catalog:p.feedbackCatalog,report:p.report}],animation,true):null;const initialAnimation=selection?.name;if(command==='feedback'){const saved=await readFeedbackSession(p,options.stateDir);await assertReviewUnchanged(p);printFeedback(saved);return;}if(command==='check'){console.log(`Package valid: ${p.manifest.title}\nRevision: ${p.manifest.revision}\nSource SHA-256: ${p.sourceHash}\nDeclared files: ${p.files.size}\nPlayback: ${p.manifest.model?'compiled model present':'unavailable (no model)'}`);return;}
|
|
110
|
+
if(checkpoint&&!p.manifest.checkpoints.some(v=>v.id===checkpoint))throw new Error(`Unknown checkpoint ${checkpoint}`);
|
|
111
|
+
// Foreground serving uses the same `#cap=` fragment handoff as open: the printed link alone can submit.
|
|
112
|
+
const capability=randomBytes(32).toString('hex'),server=await startServer(p,{port,stateDir:options.stateDir,initialAnimation,capability});const url=new URL(`http://127.0.0.1:${server.port}/`);if(checkpoint)url.searchParams.set('checkpoint',checkpoint);url.hash='cap='+capability;
|
|
113
|
+
if(fromReport&&selection)console.error(animationDiagnostic(selection));
|
|
114
|
+
console.log(`Review ready: ${url}\nPackage: ${p.manifest.title} (${p.manifest.revision})\nSource SHA-256: ${p.sourceHash}\nPress Ctrl+C to stop.`);
|
|
115
|
+
}
|
|
116
|
+
if(import.meta.main)main(Bun.argv.slice(2)).catch(e=>{console.error(e instanceof Error?e.message:e);process.exitCode=1;});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Both inspection players reopen on exactly the same fixed-step history. */
|
|
2
|
+
import { frameAtTime, type FrameClock } from './frame-clock.ts';
|
|
3
|
+
|
|
4
|
+
export const compareSeekStep = 1 / 60;
|
|
5
|
+
export interface CompareDomain { start: 0; end: number }
|
|
6
|
+
export interface ReferenceSample { time: number; source: 'video-frame' | 'current-time' }
|
|
7
|
+
export interface InspectionPlayer { readonly time: number; pause(): void; seek(time: number): void; draw(): void }
|
|
8
|
+
export interface CompareTrace { reference: number; a: number; b: number; source: ReferenceSample['source'] }
|
|
9
|
+
|
|
10
|
+
/** Feedback can describe only a successfully synchronized inspection state. */
|
|
11
|
+
export function feedbackTrace(ready: boolean, seeking: boolean, trace: CompareTrace | null): CompareTrace | null {
|
|
12
|
+
return ready && !seeking ? trace : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Runtime accumulation may differ by an admitted ULP; the validated master
|
|
16
|
+
* mediaTime is the timestamp each side's explicit producer clock interprets. */
|
|
17
|
+
export function inspectionFrame(clock: FrameClock | null | undefined, trace: CompareTrace | null): number | null {
|
|
18
|
+
return trace ? frameAtTime(clock, trace.reference) : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function commonDomain(referenceDuration: number, aDuration: number, bDuration: number): CompareDomain {
|
|
22
|
+
for (const [name, value] of [['reference', referenceDuration], ['a', aDuration], ['b', bDuration]] as const) {
|
|
23
|
+
if (!Number.isFinite(value) || value < 0) throw new Error(`compare ${name} duration: expected finite seconds >= 0`);
|
|
24
|
+
}
|
|
25
|
+
return { start: 0, end: Math.min(referenceDuration, aDuration, bDuration) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function requireCommonTime(time: number, domain: CompareDomain): void {
|
|
29
|
+
if (!Number.isFinite(time) || time < domain.start || time > domain.end) throw new Error(`compare time ${time}: outside shared domain ${domain.start}–${domain.end} s`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** No frame(dt), resume, per-side clamp or loop exists on this inspection path. */
|
|
33
|
+
export function prepareSynchronizedPair(sample: ReferenceSample, a: InspectionPlayer, b: InspectionPlayer, domain: CompareDomain): CompareTrace {
|
|
34
|
+
requireCommonTime(sample.time, domain);
|
|
35
|
+
a.pause(); b.pause();
|
|
36
|
+
// Prepare both poses before either draw. A refusal is a failed pair, never a
|
|
37
|
+
// success trace made by copying the requested time into the returned values.
|
|
38
|
+
a.seek(sample.time); b.seek(sample.time);
|
|
39
|
+
const trace = { reference: sample.time, a: a.time, b: b.time, source: sample.source };
|
|
40
|
+
if (![trace.a, trace.b].every(Number.isFinite) || Math.abs(trace.a - sample.time) > 1e-9 || Math.abs(trace.b - sample.time) > 1e-9 || trace.a !== trace.b) throw new Error(`compare playhead mismatch: reference=${trace.reference}, a=${trace.a}, b=${trace.b}`);
|
|
41
|
+
return trace;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function drawSynchronizedPair(a:InspectionPlayer,b:InspectionPlayer):void {a.draw();b.draw();}
|
|
45
|
+
|
|
46
|
+
export function seekSynchronizedPair(sample: ReferenceSample,a:InspectionPlayer,b:InspectionPlayer,domain:CompareDomain):CompareTrace {
|
|
47
|
+
const trace=prepareSynchronizedPair(sample,a,b,domain);drawSynchronizedPair(a,b);return trace;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The browser adapter resolves only after the reference seek has completed. */
|
|
51
|
+
export async function seekAfterReference(requested: number, seekReference: (time: number) => Promise<ReferenceSample>, a: InspectionPlayer, b: InspectionPlayer, domain: CompareDomain): Promise<CompareTrace> {
|
|
52
|
+
requireCommonTime(requested, domain);
|
|
53
|
+
const presented = await seekReference(requested);
|
|
54
|
+
return seekSynchronizedPair(presented, a, b, domain);
|
|
55
|
+
}
|