spine-parts 0.1.0 → 0.2.0
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/README.md +40 -19
- package/cli.ts +77 -36
- package/docs/AUTHORING.md +142 -18
- package/package.json +1 -1
- package/skills/spine-parts/SKILL.md +12 -5
- package/src/apng.ts +160 -7
- package/src/assemble.ts +177 -14
- package/src/build.ts +76 -20
- package/src/check.ts +354 -23
- package/src/comfy/painting.ts +3 -3
- package/src/config.ts +68 -25
- package/src/gif.ts +15 -127
- package/src/instruments.ts +503 -0
- package/src/palette.ts +208 -0
- package/src/parts.ts +37 -2
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ input is silent.
|
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
17
|
<img src="https://raw.githubusercontent.com/firejune/spine-parts/main/assets/demo-source.png" alt="The demo painting: a generated full-body character in a white and pink frilled dress with long pink twin tails, standing with her hands clasped" height="420" />
|
|
18
|
-
<img src="https://raw.githubusercontent.com/firejune/spine-parts/main/assets/demo-idle.
|
|
18
|
+
<img src="https://raw.githubusercontent.com/firejune/spine-parts/main/assets/demo-idle.png" alt="The same character as a Spine rig, breathing, blinking once and swaying her hair, sleeves and skirt in a four-second loop" height="420" />
|
|
19
19
|
</p>
|
|
20
20
|
|
|
21
21
|
<p align="center">
|
|
@@ -42,8 +42,10 @@ is taken from the full run, because the twin tails leave the head crop sideways
|
|
|
42
42
|
<code>--seam silhouette</code> repaired the navy blobs the default rule leaves on the white
|
|
43
43
|
blouse (recomposite error pixels 11,050 → 9,540); the default stays
|
|
44
44
|
<code>near-white</code>, the reference implementation's rule, so the examples stay
|
|
45
|
-
comparable with it. The loop is <code>spine-parts loop</code>'s
|
|
46
|
-
12 fps, 1,
|
|
45
|
+
comparable with it. The loop is <code>spine-parts loop --palette</code>'s indexed APNG, 48 frames at
|
|
46
|
+
12 fps, 1,706,468 bytes, one 256-entry palette at a measured error of max 57, mean 1.601
|
|
47
|
+
per channel over every frame (the lossless APNG beside it, the exactness record, is
|
|
48
|
+
13,645,611 bytes; the GIF, at the same error, 1,812,041); the painting is shown at half size, resampled and written
|
|
47
49
|
by this package's PNG codec (nothing here encodes JPEG). <code>spine-parts sheet</code>
|
|
48
50
|
made the contact sheet.
|
|
49
51
|
</em></p>
|
|
@@ -86,7 +88,7 @@ page** (issue #2):
|
|
|
86
88
|
| `parts/*.png`, `parts.json`, `recomposite_rig.png` | the loose parts, each cropped to its alpha box; the record of where every part came from and how many of its pixels were re-taken from the painting; the flat stack of parts |
|
|
87
89
|
| `rig/` | `rig.json` and `motion.json` in spine-rigc's spec, `mesh_report.json`, the padded `images/` |
|
|
88
90
|
| `check/` | both gate files verbatim, the idle's frames, `contact.png`, `motion_heat.png`, `check.json` |
|
|
89
|
-
| `idle.
|
|
91
|
+
| `idle.png`, `idle-indexed.png`, `idle.gif` | with `--loop`: the idle as a lossless APNG (the exactness record), an indexed APNG with one shared palette (the small one) and a GIF; the last two print their palette error |
|
|
90
92
|
|
|
91
93
|
The last lines of a green build are the pack line, printed beside the Spine example
|
|
92
94
|
export's `spineboy.png` as a yardstick (a reference, not a bar), and the three
|
|
@@ -203,14 +205,14 @@ agent skill.
|
|
|
203
205
|
| `layers <dir \| layers.json \| file.psd>` | print every layer of a decomposition: draw order, name, tag group, box, size, opaque pixels, depth |
|
|
204
206
|
| `sheet --source <png> --layers <path>… --out <png>` | a labelled contact sheet of the painting and every layer or part |
|
|
205
207
|
| `assemble --propose-plan …` | propose `assemble.plan` and `extend_below_crop` from the two runs |
|
|
206
|
-
| `assemble --source --full --head --config --out [--seam]` | merge the two runs into rig-space parts, `parts.json` and the recomposite |
|
|
208
|
+
| `assemble --source --full --head --config --out [--seam] [--project]` | merge the two runs into rig-space parts, `parts.json` and the recomposite |
|
|
207
209
|
| `propose --head-box --full <run> --canvas WxH` | propose `seethrough.head_box` from the full run, held inside the painting |
|
|
208
210
|
| `propose --parts --source --out [--compare <config>]` | propose bones, meshes, regions and an idle; draw the overlay |
|
|
209
211
|
| `propose … --from-config <config>` | draw and LINT the config's current bones |
|
|
210
212
|
| `rig --config --parts --out` | author `rig.json` + `motion.json`, written only after spine-rigc's round trip is green |
|
|
211
|
-
| `check --rig --out [--parts]` | build packed, gate under both profiles, render the idle, measure seam and
|
|
212
|
-
| `loop --frames <dir> --out <file.gif \| file.png
|
|
213
|
-
| `build --config --source --full --head --out [--seam] [--loop]` | assemble, rig and check in one process, stopping at the first refusal |
|
|
213
|
+
| `check --rig --out [--parts]` | build packed, gate under both profiles, render the idle, measure seam, loop and the five judgement lines |
|
|
214
|
+
| `loop --frames <dir> --out <file.gif \| file.png> [--palette]` | encode a rendered idle as a looping GIF, lossless APNG, or indexed APNG (`--palette`) |
|
|
215
|
+
| `build --config --source --full --head --out [--seam] [--project] [--loop]` | assemble, rig and check in one process, stopping at the first refusal |
|
|
214
216
|
|
|
215
217
|
`spine-parts --help` has every flag. Exit codes: 0 done, 1 input refused (every
|
|
216
218
|
reason is a FAIL line), 2 a usage error or a command this version does not implement.
|
|
@@ -229,9 +231,11 @@ These are limits of the approach, stated so nobody reads more into a green run:
|
|
|
229
231
|
that is, is below.
|
|
230
232
|
- **No success rate is claimed.** Ten characters have been measured stage by stage
|
|
231
233
|
against the reference implementation this package ports — the two public examples
|
|
232
|
-
here and eight private ones, one See-through seed each — and all ten check green
|
|
233
|
-
|
|
234
|
-
|
|
234
|
+
here and eight private ones, one See-through seed each — and all ten check green
|
|
235
|
+
on the gates, the seam and the loop. The judgement lines `check` added since
|
|
236
|
+
(issue #11) have been measured on the two public examples only. That is an
|
|
237
|
+
existence proof, not a rate.
|
|
238
|
+
- **`loop` writes GIF and APNG (lossless and indexed), not WebP**: an animated WebP needs a VP8/VP8L
|
|
235
239
|
encoder, which this package does not carry.
|
|
236
240
|
- **The proposer reads tags, not pictures.** A swinging element painted inside another
|
|
237
241
|
layer (a sash tail in the skirt), hair that is none of the shapes it knows, and
|
|
@@ -239,14 +243,31 @@ These are limits of the approach, stated so nobody reads more into a green run:
|
|
|
239
243
|
|
|
240
244
|
### How much of a rig the model painted
|
|
241
245
|
|
|
242
|
-
`parts.json`
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
246
|
+
`parts.json` splits each part's opaque pixels (`opaque_px`) into `visible_px` — no
|
|
247
|
+
later layer of its See-through run is opaque in front of them — and `occluded_px`,
|
|
248
|
+
and counts the visible ones whose colour was not taken from the painting
|
|
249
|
+
(`visible_not_projected_px`). On the two public examples, from the totals of their
|
|
250
|
+
`expected/parts.json` (the default `--project core`):
|
|
251
|
+
|
|
252
|
+
| | opaque px | occluded | visible, not projected | taken from the painting |
|
|
253
|
+
| --- | --- | --- | --- | --- |
|
|
254
|
+
| `sample` | 298,632 | 79,143 (26.5 %) | 36,227 (12.1 %) | 183,262 (61.4 %) |
|
|
255
|
+
| `demo` | 767,102 | 202,346 (26.4 %) | 76,801 (10.0 %) | 487,955 (63.6 %) |
|
|
256
|
+
|
|
257
|
+
The occluded share is art the painting does not show — a back-hair layer, a neck, the
|
|
258
|
+
parts of an ear under the hair — and is See-through's synthesis by necessity; it is
|
|
259
|
+
what the decomposition is for. The visible-but-not-projected share is synthesis where
|
|
260
|
+
the painting was there to be taken: a part too thin for the reference rule's 5x5
|
|
261
|
+
eroded core, an anti-aliased fringe below alpha 250, a rim beside a layer in front, a
|
|
262
|
+
pixel refused for drift. `--project visible` keeps the erosion only along a rim with a
|
|
263
|
+
layer in front, which takes that share to 22,476 (7.5 %) on `sample` and 49,672 (6.5 %)
|
|
264
|
+
on `demo` (from the `parts.json` of a `--project visible` build). On the eleven thin
|
|
265
|
+
head parts — brows, lashes, irises, eye whites, ears, mouth — it takes 296 of
|
|
266
|
+
`sample`'s 1,289 visible pixels where `core` takes 110, and 824 of `demo`'s 2,162
|
|
267
|
+
where `core` takes 343. What neither rule takes is the fringe: a per-pixel
|
|
268
|
+
classification of those parts under `visible`, recorded with the change that added
|
|
269
|
+
the flag, puts 813 of `sample`'s 993 still-unprojected pixels and 879 of `demo`'s
|
|
270
|
+
1,338 below alpha 250.
|
|
250
271
|
|
|
251
272
|
## Requirements
|
|
252
273
|
|
package/cli.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, wri
|
|
|
16
16
|
import { tmpdir } from 'node:os';
|
|
17
17
|
import { basename, dirname, extname, join } from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
|
-
import { DEFAULT_SEAM_RULE, proposeFields, proposePlan, SEAM_RULES, type SeamRule } from './src/assemble.ts';
|
|
19
|
+
import { DEFAULT_PROJECT_RULE, DEFAULT_SEAM_RULE, PROJECT_RULES, type ProjectRule, proposeFields, proposePlan, SEAM_RULES, type SeamRule } from './src/assemble.ts';
|
|
20
20
|
import { assembleStage, build, checkStage, loopStage, readRuns, readSource, rigStage } from './src/build.ts';
|
|
21
21
|
import { findRigc, type RigcRunner, SEAM_MEAN_BAR, SEAM_PX_BAR, SEAM_PX_LEVEL, SPINEBOY_YARDSTICK } from './src/check.ts';
|
|
22
22
|
import { ComfyClient, resolveHost, runPainting, runSeeThrough } from './src/comfy/index.ts';
|
|
@@ -103,31 +103,53 @@ usage:
|
|
|
103
103
|
contact.png, motion_heat.png and check.json. PASS needs both gates
|
|
104
104
|
"0 failed", the seam (setup pose vs the flat composite of parts/) at mean
|
|
105
105
|
|d| <= ${SEAM_MEAN_BAR.toFixed(1)} with <= ${SEAM_PX_BAR} px over ${SEAM_PX_LEVEL}, and the loop (idle frame 0 vs the
|
|
106
|
-
frame at t = duration) at max |d| 0.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
106
|
+
frame at t = duration) at max |d| 0. Then five judgement lines, each in
|
|
107
|
+
check.json and on the console as NAME: PASS|FAIL|SKIP with its figures and
|
|
108
|
+
bars (AUTHORING §7): BREATH_VISIBLE (the topwear moves, the footwear does
|
|
109
|
+
not, each rendered alone), BLINK_NO_HOLE (the setup pose with the blink
|
|
110
|
+
held shut shows no background inside the eyewhite box), CHAIN_LAG (every
|
|
111
|
+
rotate track lags its keyed ancestor and amplitude grows down each
|
|
112
|
+
chain, read off motion.json), TIP_OVER_ROOT (each handwear/bottomwear
|
|
113
|
+
part's lower half travels further than its upper half) and
|
|
114
|
+
STILL_REGIONS_DARK (the heat map over the face outline and the feet).
|
|
115
|
+
Regions come from parts.json's See-through tags; a line with nothing to
|
|
116
|
+
read says SKIP and why — neither a pass nor a failure — and PASS needs
|
|
117
|
+
every line that measured to be PASS. Prints the pack line
|
|
118
|
+
beside the spineboy yardstick (${SPINEBOY_YARDSTICK}), a reference and not
|
|
119
|
+
a bar. Exit 0 on PASS, 1 on FAIL — every FAIL line names the bar, the value
|
|
120
|
+
and the value required.
|
|
121
|
+
|
|
122
|
+
spine-parts loop --frames <dir> --out <file.png | file.gif> [--palette]
|
|
112
123
|
Encode a frame set rigc render wrote (its --out directory, or the set
|
|
113
124
|
directory inside it) as a looping animation: .png writes an APNG
|
|
114
|
-
(acTL/fcTL/fdAT, lossless), .
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
(acTL/fcTL/fdAT, lossless — the exactness record), .png with --palette an
|
|
126
|
+
indexed APNG (colour type 3, one palette for every frame: 255 median-cut
|
|
127
|
+
colours plus one transparent entry, alpha graded per entry, no dithering,
|
|
128
|
+
filter None — the README-sized file), .gif a GIF89a (the same 255-colour
|
|
129
|
+
median cut, no dithering, LZW, delays rounded so the loop's length is
|
|
130
|
+
exact). The indexed APNG and the GIF print their palette error, per
|
|
131
|
+
channel over every frame. The fps is read from frames.json. When the last
|
|
132
|
+
frame equals frame 0 it is dropped, and the output says so: the loop wraps
|
|
133
|
+
onto frame 0 itself. Animated WebP is not written — it needs a VP8/VP8L
|
|
134
|
+
encoder, out of scope.
|
|
119
135
|
|
|
120
136
|
spine-parts assemble --source <painting.png> --full <dir|psd> --head <dir|psd>
|
|
121
137
|
--config <config.json> --out <dir> [--seam near-white|silhouette]
|
|
138
|
+
[--project core|visible]
|
|
122
139
|
Merge the full-body and head-crop See-through runs into rig-space parts:
|
|
123
140
|
<out>/rig/parts/<name>.png (each cropped to its alpha box), <out>/rig/parts.json
|
|
124
141
|
and <out>/render/recomposite_rig.png. Reads config.seethrough.head_box and
|
|
125
142
|
.resolution and config.assemble.rig_scale, .plan and .extend_below_crop.
|
|
126
|
-
Prints one line per part, the seam override counts,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
143
|
+
Prints one line per part, the seam override counts, the \`pixels:\` totals
|
|
144
|
+
(opaque = visible + occluded; taken from the painting; visible but not
|
|
145
|
+
projected), and \`recomposite vs source\` (mean |d| and % within 8 over the
|
|
146
|
+
mean channel; error px: max channel > 40; uncovered: of those, where no
|
|
147
|
+
part has alpha above 128). Writes nothing unless every check passed.
|
|
148
|
+
--seam defaults to ${DEFAULT_SEAM_RULE}. --project says where a layer takes
|
|
149
|
+
the painting's pixel: core (the reference's) erodes every layer's top-most
|
|
150
|
+
opaque area by 5x5 first, so a part a few pixels wide takes none; visible
|
|
151
|
+
erodes only along a rim where a later layer is in front. It defaults to
|
|
152
|
+
${DEFAULT_PROJECT_RULE}.
|
|
131
153
|
|
|
132
154
|
spine-parts assemble --propose-plan --source <painting.png> --full <dir|psd>
|
|
133
155
|
--head <dir|psd> --config <config.json>
|
|
@@ -162,9 +184,11 @@ usage:
|
|
|
162
184
|
verbatim, checkpoint, LoRAs, sampler, control, elapsed) for --seeds
|
|
163
185
|
seeds from --seed0, and control_<skeleton>.png when generation.control
|
|
164
186
|
is set. The pose words are generation.pose, or the control skeleton's own.
|
|
187
|
+
The config needs only key and generation here; the rest comes later.
|
|
165
188
|
|
|
166
189
|
spine-parts build --config <config.json> --source <painting.png> --full <dir|psd>
|
|
167
|
-
--head <dir|psd> --out <dir> [--seam near-white|silhouette]
|
|
190
|
+
--head <dir|psd> --out <dir> [--seam near-white|silhouette]
|
|
191
|
+
[--project core|visible] [--loop]
|
|
168
192
|
assemble, then rig, then check, in one process, each stage's own lines
|
|
169
193
|
printed under [assemble], [rig] and [check]; the first stage that refuses
|
|
170
194
|
stops the build with its own FAIL lines. The config must already carry
|
|
@@ -172,11 +196,14 @@ usage:
|
|
|
172
196
|
correct the proposal against its overlay, and write the result into the
|
|
173
197
|
config. Into --out: parts/ + parts.json + recomposite_rig.png (assemble),
|
|
174
198
|
rig/ (rig), check/ (check, with the packed build in check/build/), and with
|
|
175
|
-
--loop idle.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
199
|
+
--loop idle.png (lossless APNG), idle-indexed.png (indexed APNG) and
|
|
200
|
+
idle.gif from check/idle_frames/, then one loop: line with the three sizes
|
|
201
|
+
and the two palette errors. The paths it writes are cleared first. A
|
|
202
|
+
green build ends with the pack line beside the spineboy yardstick and the
|
|
203
|
+
three artifact paths — skeleton .json, .atlas and the packed page: the
|
|
204
|
+
packed atlas is the result, the loose parts are the intermediate it was
|
|
205
|
+
made from. --seam defaults to ${DEFAULT_SEAM_RULE}, --project to
|
|
206
|
+
${DEFAULT_PROJECT_RULE} (both as for assemble).
|
|
180
207
|
|
|
181
208
|
spine-parts --version
|
|
182
209
|
spine-parts --help
|
|
@@ -456,14 +483,21 @@ function cmdPropose(args: string[]): number {
|
|
|
456
483
|
}
|
|
457
484
|
|
|
458
485
|
function cmdLoop(args: string[]): number {
|
|
459
|
-
const
|
|
486
|
+
const palettes = args.filter((a) => a === '--palette').length;
|
|
487
|
+
if (palettes > 1) return usage('--palette is given twice');
|
|
488
|
+
const f = flags(
|
|
489
|
+
args.filter((a) => a !== '--palette'),
|
|
490
|
+
['--frames', '--out'],
|
|
491
|
+
'loop',
|
|
492
|
+
);
|
|
460
493
|
if (typeof f === 'string') return usage(f);
|
|
461
494
|
const out = f.get('--out') as string;
|
|
462
495
|
const ext = extname(out).toLowerCase();
|
|
463
496
|
if (ext === '.webp') return usage(`--out ${out}: animated WebP needs a VP8/VP8L encoder, which is out of scope; write .png (APNG) or .gif`);
|
|
464
497
|
if (ext !== '.png' && ext !== '.gif') return usage(`--out ${out} is neither .png (APNG) nor .gif`);
|
|
498
|
+
if (palettes === 1 && ext !== '.png') return usage(`--palette selects the indexed APNG, so --out must be a .png; ${out} is a GIF, which is always one palette`);
|
|
465
499
|
try {
|
|
466
|
-
loopStage(f.get('--frames') as string, out, ext === '.
|
|
500
|
+
loopStage(f.get('--frames') as string, out, ext === '.gif' ? 'gif' : palettes === 1 ? 'indexed' : 'apng', console.log);
|
|
467
501
|
return EXIT_OK;
|
|
468
502
|
} catch (err) {
|
|
469
503
|
return printRefusal(err);
|
|
@@ -473,7 +507,7 @@ function cmdLoop(args: string[]): number {
|
|
|
473
507
|
function cmdAssemble(args: string[]): number {
|
|
474
508
|
const flags = new Map<string, string>();
|
|
475
509
|
let propose = false;
|
|
476
|
-
const valued = ['--source', '--full', '--head', '--config', '--out', '--seam'];
|
|
510
|
+
const valued = ['--source', '--full', '--head', '--config', '--out', '--seam', '--project'];
|
|
477
511
|
for (let i = 0; i < args.length; i++) {
|
|
478
512
|
const flag = args[i];
|
|
479
513
|
if (flag === '--propose-plan') {
|
|
@@ -488,23 +522,25 @@ function cmdAssemble(args: string[]): number {
|
|
|
488
522
|
i++;
|
|
489
523
|
}
|
|
490
524
|
for (const f of ['--source', '--full', '--head', '--config']) if (!flags.has(f)) return usage(`assemble needs ${f}`);
|
|
491
|
-
if (propose && (flags.has('--out') || flags.has('--seam'))) return usage('--propose-plan prints to the console; it takes
|
|
525
|
+
if (propose && (flags.has('--out') || flags.has('--seam') || flags.has('--project'))) return usage('--propose-plan prints to the console; it takes none of --out, --seam, --project');
|
|
492
526
|
if (!propose && !flags.has('--out')) return usage('assemble needs --out <dir>');
|
|
493
527
|
const seam = flags.get('--seam') ?? DEFAULT_SEAM_RULE;
|
|
494
528
|
if (!(SEAM_RULES as readonly string[]).includes(seam)) return usage(`--seam ${seam}; one of ${SEAM_RULES.join(', ')} is required`);
|
|
529
|
+
const project = flags.get('--project') ?? DEFAULT_PROJECT_RULE;
|
|
530
|
+
if (!(PROJECT_RULES as readonly string[]).includes(project)) return usage(`--project ${project}; one of ${PROJECT_RULES.join(', ')} is required`);
|
|
495
531
|
const [source, full, head, config] = ['--source', '--full', '--head', '--config'].map((f) => flags.get(f) as string);
|
|
496
532
|
try {
|
|
497
533
|
if (propose) {
|
|
498
534
|
const src = readSource(source);
|
|
499
535
|
const runs = readRuns(full, head);
|
|
500
|
-
const g = proposeFields(loadEarlyConfig(config));
|
|
536
|
+
const g = proposeFields(loadEarlyConfig(config, 'layers'));
|
|
501
537
|
const proposal = proposePlan(runs.full, runs.head, { sourceW: src.width, sourceH: src.height, ...g });
|
|
502
538
|
console.log(JSON.stringify(proposal, null, 2));
|
|
503
539
|
return EXIT_OK;
|
|
504
540
|
}
|
|
505
541
|
const out = flags.get('--out') as string;
|
|
506
542
|
assembleStage(
|
|
507
|
-
{ source, full, head, config, seam: seam as SeamRule },
|
|
543
|
+
{ source, full, head, config, seam: seam as SeamRule, project: project as ProjectRule },
|
|
508
544
|
{ partsJson: join(out, 'rig', 'parts.json'), partsDir: join(out, 'rig', 'parts'), recomposite: join(out, 'render', 'recomposite_rig.png') },
|
|
509
545
|
console.log,
|
|
510
546
|
);
|
|
@@ -567,7 +603,12 @@ async function cmdComfy(args: string[]): Promise<number> {
|
|
|
567
603
|
for (const x of [wait, timeout, poll]) if (typeof x === 'string') return usage(x);
|
|
568
604
|
const out = v.get('--out');
|
|
569
605
|
if (out === undefined) return usage(`comfy ${sub} needs --out <dir>`);
|
|
606
|
+
const configPath = v.get('--config');
|
|
607
|
+
if (sub === 'paint' && configPath === undefined) return usage('comfy paint needs --config <config.json>');
|
|
570
608
|
try {
|
|
609
|
+
// The config is a local file, so it is answered before any host is: a
|
|
610
|
+
// config comfy paint cannot paint from is refused with no box named.
|
|
611
|
+
const cfg = sub === 'paint' ? loadEarlyConfig(configPath as string, 'paint') : null;
|
|
571
612
|
const host = resolveHost(v.get('--host'), process.env.COMFY_HOST);
|
|
572
613
|
const client = new ComfyClient(host, { poll: poll as number, request: 30 });
|
|
573
614
|
if (sub === 'seethrough') {
|
|
@@ -599,12 +640,10 @@ async function cmdComfy(args: string[]): Promise<number> {
|
|
|
599
640
|
console.log(` wrote ${join(out, 'layers.json')}, meta.json, parts/ (${r.layers.length} PNG) and previews/ (${r.previews.length} PNG); GPU job ended`);
|
|
600
641
|
return EXIT_OK;
|
|
601
642
|
}
|
|
602
|
-
|
|
603
|
-
if (configPath === undefined) return usage('comfy paint needs --config <config.json>');
|
|
604
|
-
const cfg = loadConfig(configPath);
|
|
643
|
+
if (cfg === null) return usage('comfy paint needs --config <config.json>');
|
|
605
644
|
const seeds = intFlag(v, '--seeds', 1, 1);
|
|
606
645
|
if (typeof seeds === 'string') return usage(seeds);
|
|
607
|
-
const seed0 = intFlag(v, '--seed0', cfg.generation
|
|
646
|
+
const seed0 = intFlag(v, '--seed0', cfg.generation.seed, 0);
|
|
608
647
|
if (typeof seed0 === 'string') return usage(seed0);
|
|
609
648
|
console.log(`spine-parts comfy paint: ${cfg.key} -> ${out}`);
|
|
610
649
|
console.log(` ${seeds} seed(s) from ${seed0}${v.has('--seed0') ? '' : ' (generation.seed)'}; wait <= ${wait} s per seed, timeout ${timeout} s`);
|
|
@@ -623,7 +662,7 @@ function cmdInputs(args: string[]): number {
|
|
|
623
662
|
const out = f.get('--out') as string;
|
|
624
663
|
try {
|
|
625
664
|
if (!existsSync(source)) throw new PartsError([{ code: 'INPUTS_SOURCE_PRESENT', object: source, detail: 'no such file; the painting is required' }]);
|
|
626
|
-
const cfg = loadEarlyConfig(f.get('--config') as string);
|
|
665
|
+
const cfg = loadEarlyConfig(f.get('--config') as string, 'layers');
|
|
627
666
|
const painting = readPng(source);
|
|
628
667
|
const r = makeInputs(painting, cfg, source);
|
|
629
668
|
mkdirSync(out, { recursive: true });
|
|
@@ -645,7 +684,7 @@ function cmdInputs(args: string[]): number {
|
|
|
645
684
|
function cmdBuild(args: string[]): number {
|
|
646
685
|
const flags = new Map<string, string>();
|
|
647
686
|
let loop = false;
|
|
648
|
-
const valued = ['--config', '--source', '--full', '--head', '--out', '--seam'];
|
|
687
|
+
const valued = ['--config', '--source', '--full', '--head', '--out', '--seam', '--project'];
|
|
649
688
|
for (let i = 0; i < args.length; i++) {
|
|
650
689
|
const flag = args[i];
|
|
651
690
|
if (flag === '--loop') {
|
|
@@ -663,6 +702,8 @@ function cmdBuild(args: string[]): number {
|
|
|
663
702
|
for (const f of ['--config', '--source', '--full', '--head', '--out']) if (!flags.has(f)) return usage(`build needs ${f}`);
|
|
664
703
|
const seam = flags.get('--seam') ?? DEFAULT_SEAM_RULE;
|
|
665
704
|
if (!(SEAM_RULES as readonly string[]).includes(seam)) return usage(`--seam ${seam}; one of ${SEAM_RULES.join(', ')} is required`);
|
|
705
|
+
const project = flags.get('--project') ?? DEFAULT_PROJECT_RULE;
|
|
706
|
+
if (!(PROJECT_RULES as readonly string[]).includes(project)) return usage(`--project ${project}; one of ${PROJECT_RULES.join(', ')} is required`);
|
|
666
707
|
const [config, source, full, head, out] = ['--config', '--source', '--full', '--head', '--out'].map((f) => flags.get(f) as string);
|
|
667
708
|
let bin: string;
|
|
668
709
|
try {
|
|
@@ -673,7 +714,7 @@ function cmdBuild(args: string[]): number {
|
|
|
673
714
|
const scratch = mkdtempSync(join(tmpdir(), 'spine-parts-build-'));
|
|
674
715
|
try {
|
|
675
716
|
const r = build(
|
|
676
|
-
{ config, source, full, head, out, seam: seam as SeamRule, loop },
|
|
717
|
+
{ config, source, full, head, out, seam: seam as SeamRule, project: project as ProjectRule, loop },
|
|
677
718
|
{ rig: rigGateRunner(), check: rigcRunner(bin), checkBin: bin, scratch: join(scratch, 'rig-gate') },
|
|
678
719
|
console.log,
|
|
679
720
|
);
|
package/docs/AUTHORING.md
CHANGED
|
@@ -9,7 +9,8 @@ has to change.
|
|
|
9
9
|
The worked example throughout is [`examples/sample`](../examples/sample): its
|
|
10
10
|
`config.json` is a complete, loading config, `proposal.json` is what the proposer
|
|
11
11
|
wrote for it, and `expected/` is what the reference implementation produced from
|
|
12
|
-
the same inputs
|
|
12
|
+
the same inputs — except `check.json`, which this port's `build` regenerates since
|
|
13
|
+
the reference has no judgement lines (§7). `bun run fetch-examples` puts its painting and See-through layers
|
|
13
14
|
into `examples/sample/inputs/`.
|
|
14
15
|
|
|
15
16
|
## 1. Prerequisites
|
|
@@ -90,11 +91,22 @@ swing, hair of another shape, whether an accessory swings — is yours to add, a
|
|
|
90
91
|
|
|
91
92
|
Two steps are external: the See-through runs (by any route; the optional `comfy seethrough` adapter is only a client for a ComfyUI box). Everything else is this tool.
|
|
92
93
|
|
|
94
|
+
0. **The painting (optional).** `spine-parts comfy paint --config config.json --out inputs --host <url>`
|
|
95
|
+
generates `painting_<seed>.png` on a ComfyUI box; any other route to a painting
|
|
96
|
+
skips this step and leaves `generation` out. At this point the config needs only
|
|
97
|
+
`key` and `generation`: `comfy paint` reads it through the partial loader's `paint`
|
|
98
|
+
door (`parseEarlyConfig(raw, 'paint')`), which requires those two, reads nothing
|
|
99
|
+
else, and refuses unknown and retired keys like the full loader. A config without
|
|
100
|
+
`generation` is refused as `CONFIG_FIELD_PRESENT: config.generation`, naming the
|
|
101
|
+
fields the block holds. Everything else arrives later: `seethrough` and
|
|
102
|
+
`assemble.rig_scale` before step 1, `head_box` from step 4, the plan from step 6,
|
|
103
|
+
and `bones`, `meshes`, `regions` and `motion` from `propose` in step 7.
|
|
93
104
|
1. **The full image.** `spine-parts inputs --source inputs/painting.png --config config.json --out inputs`
|
|
94
105
|
writes `st_input_full.png`, the painting centred on a white square as tall as it is.
|
|
95
106
|
At this point the config needs only `key`, `seethrough` (without `head_box`) and
|
|
96
107
|
`assemble.rig_scale`: `inputs` and `assemble --propose-plan` read it through the
|
|
97
|
-
partial loader (`parseEarlyConfig`), which
|
|
108
|
+
partial loader's `layers` door (`parseEarlyConfig(raw, 'layers')`), which checks
|
|
109
|
+
`generation` too when it is present, and which refuses unknown and retired keys like the
|
|
98
110
|
full one and leaves the later sections for later. A landscape or translucent
|
|
99
111
|
painting is refused (`INPUTS_PAINTING_PORTRAIT`, `INPUTS_PAINTING_OPAQUE`).
|
|
100
112
|
2. **See-through, full run** (external).
|
|
@@ -123,10 +135,24 @@ Two steps are external: the See-through runs (by any route; the optional `comfy
|
|
|
123
135
|
| --- | --- | --- | --- |
|
|
124
136
|
| `layers` | the table | every tag the plan will need has opaque pixels; one `face` in the head run | a run whose layer PNG is not its box's size (`LAYERS_PNG_MATCHES_BBOX`) |
|
|
125
137
|
| `sheet` of both runs | the tile list (and the sheet, if you can see) | eyes, irises, lashes and brows as left/right pairs in the head run | a head box that cut off an ornament: move `head_box`, re-run the head crop |
|
|
126
|
-
| `assemble` | one line per part, then `recomposite vs source: mean \|d\|, within 8, error px > 40, uncovered error px` | on the examples: `sample` 0.84 / 98.0 % / 4,512 / 1,185; `demo` 2.39 / 95.8 % / 11,050 / 1,564 (default rule) | `uncovered error px` high: part of the figure is in no layer — a plan entry is missing, or hair left the head crop sideways (the demo's `hair_back` is taken from the full run for that reason) |
|
|
138
|
+
| `assemble` | one line per part, the `pixels:` totals (opaque = visible + occluded; taken; visible but not projected), then `recomposite vs source: mean \|d\|, within 8, error px > 40, uncovered error px` | on the examples: `sample` 0.84 / 98.0 % / 4,512 / 1,185; `demo` 2.39 / 95.8 % / 11,050 / 1,564 (default rule) | `uncovered error px` high: part of the figure is in no layer — a plan entry is missing, or hair left the head crop sideways (the demo's `hair_back` is taken from the full run for that reason) |
|
|
127
139
|
| `propose` | `note:` lines, `LINT` lines, `landmarks.png` | no LINT line: every chain link lies on its mesh's art | a link off the art (a bone on the background) — move it onto the layer |
|
|
128
140
|
| `rig` (inside `build`) | one line per mesh: vertices, triangles, bones, influences, `cover`; then rigc's gate lines | `cover 1.00000` on every mesh, both gates `0 failed` | `RIG_LATTICE_ONE_LOOP`: change that mesh's `grid` |
|
|
129
|
-
| `check` (inside `build`) | the gate lines verbatim, the pack line, `loop:`, `seam:`, `check.json` | `check: PASS
|
|
141
|
+
| `check` (inside `build`) | the gate lines verbatim, the pack line, `loop:`, `seam:`, the five judgement lines (§7), `check.json` | `check: PASS`, and a judgement line SKIP only where the character lacks what it reads | `CHECK_SEAM_WITHIN_BAR` or `CHECK_LOOP_CLOSES` (§6) |
|
|
142
|
+
| `loop` (inside `build --loop`, or `loop --frames … --out …`) | the dropped-duplicate line, each file's line, then `loop: idle.png N B (lossless); idle-indexed.png N B (max …, mean …); idle.gif N B (max …, mean …)` | `f0048.png equals f0000.png byte for byte, so it is dropped` | `LOOP_ENCODE` (§6) |
|
|
143
|
+
|
|
144
|
+
`loop` writes three files from one frame set, and they are not interchangeable.
|
|
145
|
+
`idle.png` (`loop --out x.png`) is the lossless APNG: every frame decodes to the
|
|
146
|
+
rendered frame byte for byte, so it is the exactness record. `idle-indexed.png`
|
|
147
|
+
(`loop --out x.png --palette`) is an indexed APNG — colour type 3, one palette for
|
|
148
|
+
every frame (255 median-cut colours and one transparent entry, alpha graded per
|
|
149
|
+
entry, no dithering, filter None) — and is the small file to show. `idle.gif`
|
|
150
|
+
(`loop --out x.gif`) is the same median cut in a GIF. The indexed APNG and the GIF
|
|
151
|
+
print their palette error, per channel over R, G and B of every frame (and alpha's
|
|
152
|
+
max for the APNG); a figure is a measurement of the file, not a bar. On the demo:
|
|
153
|
+
13,645,611 B lossless, 1,706,468 B indexed and 1,812,041 B GIF, both palette files at
|
|
154
|
+
max 57, mean 1.601 — the two share one quantiser, so their error is the same by
|
|
155
|
+
construction and the size is the difference.
|
|
130
156
|
|
|
131
157
|
`--seam near-white` (the default) is the reference implementation's rule: where the
|
|
132
158
|
flat stack of parts differs from the painting by more than 60, the top part takes the
|
|
@@ -136,6 +162,38 @@ outside the figure. On the demo it lowered recomposite error pixels from 11,050
|
|
|
136
162
|
9,540 and changed no check bar; on the sample it changed nothing. The default stays
|
|
137
163
|
the reference's so the examples stay comparable with it.
|
|
138
164
|
|
|
165
|
+
`--project core` (the default) is the reference's projection rule: a layer takes the
|
|
166
|
+
painting's pixel only inside its top-most `alpha >= 250` area eroded by a 5x5 square,
|
|
167
|
+
so a part a few pixels wide (a lash, a brow, an iris) takes none. `--project visible`
|
|
168
|
+
keeps the erosion only along a rim where a later layer of the run is in front, and
|
|
169
|
+
takes every other top-most `alpha >= 250` pixel; neither rule takes a fringe pixel
|
|
170
|
+
(alpha below 250). On the examples it lowered the pixels that are visible but not
|
|
171
|
+
projected from 36,227 to 22,476 (`sample`) and 76,801 to 49,672 (`demo`), recomposite
|
|
172
|
+
error pixels from 4,512 to 4,116 and 11,050 to 9,820, and the check seam from 0.207 to
|
|
173
|
+
0.206 and 0.326 to 0.325, with no other check figure changed. The default stays the
|
|
174
|
+
reference's so the examples stay comparable with it. `build` takes both flags.
|
|
175
|
+
|
|
176
|
+
`parts.json` holds one record per part, in plan order. Its counts:
|
|
177
|
+
|
|
178
|
+
| field | counts |
|
|
179
|
+
| --- | --- |
|
|
180
|
+
| `opaque_px` | the part PNG's pixels with alpha above 8 |
|
|
181
|
+
| `visible_px` | of those, the ones no later layer of their See-through run is opaque (alpha >= 250) in front of — a pixel copied in below the head crop is judged in its extend layer's run |
|
|
182
|
+
| `occluded_px` | the rest of `opaque_px`: art the painting does not show, See-through's synthesis by necessity |
|
|
183
|
+
| `projected_core_px` | the projection rule's candidates: top-most `alpha >= 250`, eroded (`core`) or kept off a front rim (`visible`) |
|
|
184
|
+
| `source_px_taken` | of those, the ones that took the painting's pixel (the reference's count, before any merge) |
|
|
185
|
+
| `visible_not_projected_px` | visible pixels whose colour did not come from projection — too thin for the core, a fringe, a rim, refused for drift, or a merge ring |
|
|
186
|
+
| `refused_drift_px` | candidates refused because See-through's pixel and the painting's differ by more than 90 |
|
|
187
|
+
| `merged_px` | pixels brought in below the head crop, and the ring that closes their seam |
|
|
188
|
+
| `seam_override_px` | pixels the seam pass recoloured to the painting |
|
|
189
|
+
|
|
190
|
+
`visible_px + occluded_px = opaque_px`, and every projected pixel is visible; the stage
|
|
191
|
+
refuses (`ASSEMBLE_COUNTS_ADD_UP`) rather than write counts that break either, and the
|
|
192
|
+
reader refuses a record that breaks them (`PARTS_COUNTS_ADD_UP`). The three visibility
|
|
193
|
+
counts are this port's: a `parts.json` the reference wrote has none of them and still
|
|
194
|
+
reads, but a record with only some of them is refused. The `pixels:` line after the
|
|
195
|
+
per-part lines prints their totals.
|
|
196
|
+
|
|
139
197
|
## 6. Refusals: the rule, and what has to change
|
|
140
198
|
|
|
141
199
|
Every refusal is `FAIL RULE: object — detail`. Inside `build` it is printed under the
|
|
@@ -184,7 +242,7 @@ stage's prefix (`[assemble] FAIL …`), and the build stops there.
|
|
|
184
242
|
| `COMFY_QUEUE_EMPTY`, `COMFY_PROMPT_ACCEPTED`, `COMFY_HISTORY_WITHIN`, `COMFY_RUN_OK` | someone else's job kept the queue busy past `--wait`, the box rejected the graph, the job did not finish within `--timeout`, or it ended in error (quoted) | `--wait`/`--timeout`, or what the quoted error names |
|
|
185
243
|
| `COMFY_HISTORY_OUTPUTS`, `COMFY_VIEW_PRESENT`, `COMFY_MANIFEST_NAMED`, `COMFY_MANIFEST_IS_THIS_RUN`, `COMFY_LAYER_NAME` | the job's outputs are missing, unreadable, belong to another run, or name a layer that is not a plain file name | the wrapper's version on the box; report it |
|
|
186
244
|
| `COMFY_IMAGE_PRESENT`, `COMFY_OUT_EMPTY`, `COMFY_OUT_FREE` | no `--image`, an `--out` that already holds files, or a seed whose painting is already on disk | `--image`, `--out`, `--seed0` |
|
|
187
|
-
| `
|
|
245
|
+
| `CONFIG_FIELD_PRESENT` on `config.generation`, `COMFY_PAINTING_SIZE` | `comfy paint` was given a config with no `generation` block (the line names the fields it holds), or the painting that came back is not twice the latent | `config.generation` |
|
|
188
246
|
|
|
189
247
|
### assemble
|
|
190
248
|
|
|
@@ -197,6 +255,7 @@ stage's prefix (`[assemble] FAIL …`), and the build stops there.
|
|
|
197
255
|
| `ASSEMBLE_RUN_CANVAS` | a run's canvas is not `resolution` square | `seethrough.resolution`, or the run |
|
|
198
256
|
| `ASSEMBLE_PLAN_TAG_IN_RUN`, `ASSEMBLE_EXTEND_TAG_IN_RUN` | an entry takes a tag its run does not hold (the detail lists what it does hold) | that entry's run or tag |
|
|
199
257
|
| `ASSEMBLE_PART_OPAQUE` | a part ended with no opaque pixel | drop the entry, or take the tag from the other run |
|
|
258
|
+
| `ASSEMBLE_COUNTS_ADD_UP` | a part's visible and occluded counts do not add up to its opaque pixels, or a projected pixel is not visible — an assembler bug, not an input problem | report it with the part named; nothing was written |
|
|
200
259
|
|
|
201
260
|
### propose
|
|
202
261
|
|
|
@@ -206,7 +265,7 @@ stage's prefix (`[assemble] FAIL …`), and the build stops there.
|
|
|
206
265
|
| `PROPOSE_SOURCE_PRESENT`, `PROPOSE_PNG_PRESENT`, `PROPOSE_PNG_MATCHES_BOX` | the painting or a part PNG is missing, or a PNG is not its box | `--source`, `--parts` (re-run assemble) |
|
|
207
266
|
| `PROPOSE_FACE_PRESENT` | no part comes from a `face` layer; every other rule scales by it | `assemble.plan` |
|
|
208
267
|
| `PROPOSE_ACCESSORY_BODY` | an accessory has nothing above its pendant rows to hang its bone on | that part's plan entry, or author its bones by hand |
|
|
209
|
-
| `PARTS_*` (`PARTS_FILE_PRESENT`, `PARTS_IS_JSON`, `PARTS_KEY_KNOWN`, `PARTS_FIELD_PRESENT`, `PARTS_FIELD_TYPE`, `PARTS_NAME_UNIQUE`, `PARTS_FROM_KNOWN`, `PARTS_BOX_INSIDE_RIG`) | the `parts.json` read is not assemble's contract | re-run assemble; do not edit `parts.json` |
|
|
268
|
+
| `PARTS_*` (`PARTS_FILE_PRESENT`, `PARTS_IS_JSON`, `PARTS_KEY_KNOWN`, `PARTS_FIELD_PRESENT`, `PARTS_FIELD_TYPE`, `PARTS_NAME_UNIQUE`, `PARTS_FROM_KNOWN`, `PARTS_BOX_INSIDE_RIG`, `PARTS_COUNTS_ADD_UP`) | the `parts.json` read is not assemble's contract (`PARTS_COUNTS_ADD_UP`: `visible_px + occluded_px` is not `opaque_px`, or `visible_not_projected_px` is above `visible_px`) | re-run assemble; do not edit `parts.json` |
|
|
210
269
|
|
|
211
270
|
### rig
|
|
212
271
|
|
|
@@ -225,13 +284,18 @@ stage's prefix (`[assemble] FAIL …`), and the build stops there.
|
|
|
225
284
|
| rule | means | change |
|
|
226
285
|
| --- | --- | --- |
|
|
227
286
|
| `CHECK_RIGC_PRESENT` | no `rigc` binary found (every place looked is listed) | `bun install` |
|
|
228
|
-
| `CHECK_INPUT_PRESENT`, `CHECK_INPUT_IS_JSON`, `CHECK_PART_PNG_PRESENT`, `CHECK_PART_PNG_MATCHES_BOX`, `CHECK_RIG_STAGE_PRESENT`, `CHECK_RIG_STAGE_IS_THE_CANVAS`, `CHECK_RIG_ROOT_BONE`, `CHECK_IDLE_PRESENT` | the rig directory is incomplete or disagrees with `parts.json` | re-run rig (`build` does both) |
|
|
287
|
+
| `CHECK_INPUT_PRESENT`, `CHECK_INPUT_IS_JSON`, `CHECK_PART_PNG_PRESENT`, `CHECK_PART_PNG_MATCHES_BOX`, `CHECK_PART_SLOT_PRESENT`, `CHECK_RIG_STAGE_PRESENT`, `CHECK_RIG_STAGE_IS_THE_CANVAS`, `CHECK_RIG_ROOT_BONE`, `CHECK_IDLE_PRESENT` | the rig directory is incomplete or disagrees with `parts.json` (`CHECK_PART_SLOT_PRESENT`: a part with no slot of its own name, which the judgement lines render it by) | re-run rig (`build` does both) |
|
|
229
288
|
| `CHECK_RIGC_GREEN` | a rigc step failed; its line is quoted | as `RIG_RIGC_GREEN` |
|
|
230
289
|
| `CHECK_LOOP_LAST_FRAME_AT_DURATION` | the idle's last frame does not sit at `duration` | `motion.duration` — a whole number of 1/12 s |
|
|
231
290
|
| `CHECK_LOOP_CLOSES` | frame 0 and the frame at `duration` differ (max and first pixel quoted) | a track whose last key is not its first |
|
|
232
291
|
| `CHECK_SEAM_WITHIN_BAR` | the setup pose does not reproduce the flat stack of parts | usually a region or mesh placed off its part; compare with `recomposite_rig.png` |
|
|
292
|
+
| `CHECK_BREATH_VISIBLE` | the torso (`topwear`), rendered alone, barely moves over the idle — or the feet (`footwear`), rendered alone, move at all | the chest's breath tracks (`motion.tracks` on `chest`), or the torso mesh's `segments`; for the feet, the bone their region rides (`regions.<part>`, `root` in both examples) |
|
|
293
|
+
| `CHECK_BLINK_NO_HOLE` | with the blink held shut, the eyewhite box shows the page where the open eye had art | the layer under the eye: the `face` part has no art there. Take the face from the other run, or add a part under the eye; `motion.blink.squash` only hides the hole less |
|
|
294
|
+
| `CHECK_CHAIN_LAG` | a rotate track leads (or does not lag) the keyed bone above it, or a chain link swings less than the link above | that chain track's `phase`/`lag` (a positive `lag`, a child `phase` above its parent's) or its `amps` (non-decreasing toward the tip) |
|
|
295
|
+
| `CHECK_TIP_OVER_ROOT` | a `handwear`/`bottomwear` part's lower half travels less than 1.4725 times as far as its upper half | the chain track's `amps` (grow toward the tip), or the mesh's `segments` (the chain must be among them) |
|
|
296
|
+
| `CHECK_STILL_REGIONS_DARK` | the heat map is brighter than the ceiling over the face outline or over the feet | the part that moves there: a mesh weighted to a swinging bone (`segments`), or a region on the wrong bone |
|
|
233
297
|
| `CHECK_SEAM_FRAME_SIZE`, `FRAMES_SIDECAR` | rigc's render is not what its `frames.json` says | a rigc problem; report it |
|
|
234
|
-
| `LOOP_ENCODE` | the loop encoder refused a frame (translucent pixel in a GIF, a size change) | the frames |
|
|
298
|
+
| `LOOP_ENCODE` | the loop encoder refused a frame (translucent pixel in a GIF, a size change) | the frames; for a translucent frame write the lossless or the indexed APNG, which keep alpha |
|
|
235
299
|
| `BUILD_ARTIFACT_PRESENT` | the packed build lacks its `.json`, `.atlas` or page | a rigc problem; report it |
|
|
236
300
|
|
|
237
301
|
## 7. The bars `check` enforces, and what only an eye answers today
|
|
@@ -249,16 +313,76 @@ implementation's bars):
|
|
|
249
313
|
On the examples: `sample` 23/23 and 14/14, seam 0.207 with 0 pixels over 40, loop 0;
|
|
250
314
|
`demo` (default rule) 23/23 and 14/14, seam 0.326 with 2 pixels over 40, loop 0.
|
|
251
315
|
|
|
252
|
-
A green
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
316
|
+
A green gate cannot see a wrong animation, so `check` also writes five **judgement
|
|
317
|
+
lines** (issue #11), each a key of `check.json` and a console line
|
|
318
|
+
`NAME: PASS|FAIL|SKIP — <figures and bars>`, read the same way as the lines above: a
|
|
319
|
+
FAIL makes `PASS` false and prints its own `FAIL CHECK_<NAME>` line (§6); a SKIP
|
|
320
|
+
says why the rig gave the line nothing to read, and is neither a pass nor a failure —
|
|
321
|
+
report it as not verified. Every region is chosen by the See-through tag in
|
|
322
|
+
`parts.json`'s `from`, never by a part's name. Heat is a pixel's largest per-channel
|
|
323
|
+
change from idle frame 0, in levels of 255, on the idle's 640-pixel grid.
|
|
324
|
+
|
|
325
|
+
Each bar follows one rule: a floor is half the weaker example's figure and a ceiling
|
|
326
|
+
twice the worse one's, so the weaker example clears it by a factor of two; a bar the
|
|
327
|
+
model itself fixes (a part on an unkeyed bone does not move, a lag is above 0, a hole
|
|
328
|
+
is 0 pixels) is that value, not a margin. The figures are this port's, measured on
|
|
329
|
+
the two examples [observed]:
|
|
330
|
+
|
|
331
|
+
| line | measures | bar | `demo` | `sample` | SKIP when |
|
|
332
|
+
| --- | --- | --- | --- | --- | --- |
|
|
333
|
+
| `BREATH_VISIBLE` | `topwear` parts rendered alone (`rigc render --slot`): heat mean over their box; `footwear` parts alone: heat max over theirs | torso mean ≥ 3.809; feet max ≤ 0 | 15.252; 0 | 7.618; 0 | no `topwear` or no `footwear` part |
|
|
334
|
+
| `BLINK_NO_HOLE` | the setup pose with every `scaley` track on the eyewhite slots' bones held at its closed value, against the setup pose, at full size: pixels in the eyewhite box that show the page where the open eye had art | 0 px | 0 | 0 | no `eyewhite` part, or no `scaley` track on its bones goes below its first key |
|
|
335
|
+
| ″ (reported) | the same box: each closed-eye pixel's max-channel distance to the nearest colour the open eye's box holds — max, and pixels over 40 | none | 15; 0 | 11; 0 | as above |
|
|
336
|
+
| `CHAIN_LAG` | `motion.json`'s rotate tracks read as sines (DFT of the keys: period, amplitude, phase) and arranged by the bone tree — a keyed bone's parent is its nearest keyed ancestor | every lag ≥ 0.001 cycle; amplitude non-decreasing down each unbranched chain | lags 0.040 (neck to head) to 0.120; 12 chains | lags 0.040 to 0.100; 9 chains | no rotate track under another of the same period |
|
|
337
|
+
| `TIP_OVER_ROOT` | each `handwear`/`bottomwear` part alone: how far the centroid of its art travels in the lower half of its box against the upper half | ratio ≥ 1.4725 | `bottomwear` 2.945, `sleeves` 3.716 | `bottomwear` 4.396, `sleeves` 12.475 | no such part |
|
|
338
|
+
| `STILL_REGIONS_DARK` | the idle's heat over the face outline (where `face` is the top part of the flat stack, less the boxes of `eyewhite`, `irides`, `eyelash`, `eyebrow` and `mouth`) and over the feet (where `footwear` is on top); max reported | face mean ≤ 33.976; feet mean ≤ 3.244 | 16.988; 1.622 | 11.475; 0 | no `face` and no `footwear` part (one of the two absent leaves that half unmeasured) |
|
|
339
|
+
|
|
340
|
+
What each figure is, and is not:
|
|
341
|
+
|
|
342
|
+
- **The seam is the answer to "at rest, no gap, white rim or doubled line between
|
|
343
|
+
layers".** A gap shows the page, a rim a colour no part has there, a doubled line a
|
|
344
|
+
part drawn off its place; each changes the setup-pose render against the flat stack,
|
|
345
|
+
which is what the seam bar measures. No separate line is written for it.
|
|
346
|
+
- **The blink is measured at the setup pose, not in an idle frame.** On both examples
|
|
347
|
+
the eyes are fully shut from 2.37 s to 2.41 s, and no 12 fps idle frame falls inside
|
|
348
|
+
that window (`idle_frames_closed` is empty: frames 28 and 29 are 2.333 s and
|
|
349
|
+
2.417 s), so the idle render and the loop encoded from it never show the closed eye.
|
|
350
|
+
rigc's `render` takes no time, so the closed pose is a throwaway animation holding
|
|
351
|
+
the blink tracks' closed value, built and rendered beside the seam's still on the
|
|
352
|
+
same grid — the comparison is then of what the blink alone changed.
|
|
353
|
+
- **The colour-patch figure is not reliable enough for a bar.** "Nearest colour in the
|
|
354
|
+
open eye's box" counts a legitimate colour the open eye never showed (skin under the
|
|
355
|
+
lid) as a patch, and a patch the open eye happened to contain as none. It is
|
|
356
|
+
reported, and 15 and 11 on the examples are what a clean lid looks like.
|
|
357
|
+
- **A lag is read modulo half a cycle.** A sine's sign is half a cycle of phase, and
|
|
358
|
+
the keys cannot tell `amp −0.6, phase 0.2` from `amp 0.6, phase 0.7` — the mirrored
|
|
359
|
+
chains of both examples are written the first way — so the reading folds a step into
|
|
360
|
+
(−¼, ¼] of a cycle and reports amplitudes unsigned. A lag of a quarter cycle or more
|
|
361
|
+
cannot be told from a lead. Tracks whose keys are not a sampled sine are listed as
|
|
362
|
+
`unread`, and a keyed bone under a keyed ancestor of another period (the demo's
|
|
363
|
+
tassel under the head) is listed in `other_period` and not compared.
|
|
364
|
+
- **Tip over root is a centroid, not a displacement.** Art entering or leaving a half
|
|
365
|
+
moves its centroid too. The halves' mean heat was the brief's first choice and was
|
|
366
|
+
rejected: heat is texture times motion, and on the demo's sleeves the lower half's
|
|
367
|
+
mean heat is 1.1 times the upper half's while its centroid travels 3.7 times as far.
|
|
368
|
+
The halves split the box across its rows, which assumes the part hangs — true of a
|
|
369
|
+
front-facing standing figure, the only input this tool takes.
|
|
370
|
+
- **The face ceiling is weak, and the reason is in the examples.** The head rolls, so
|
|
371
|
+
the face outline is lit in both (16.988 and 11.475); twice the worse is a ceiling a
|
|
372
|
+
smooth face sliding two rig pixels stays under (the selftest's fixture measures 24.8
|
|
373
|
+
at that slide and 85.5 at eight). A face-outline instrument that removes the head's
|
|
374
|
+
own motion needs the head bone's world transform per frame, which rigc's `render`
|
|
375
|
+
does not export. The feet half has the same shape on the demo: its long skirt swings
|
|
376
|
+
over the shoes, which lights 1.622.
|
|
377
|
+
|
|
378
|
+
Still only an eye answers, each a missing instrument rather than a question to ask:
|
|
379
|
+
|
|
380
|
+
- **no visible texture stretch** (it appears when amplitudes grow): the measure is a
|
|
381
|
+
mesh triangle's deformed edge length over its rest length, frame by frame, and
|
|
382
|
+
nothing this package runs gives deformed vertices — spine-parts does not link
|
|
383
|
+
`spine-core`, and rigc's `render` exports pixels, not vertices;
|
|
384
|
+
- **the face outline held still in the head's own frame** (above): the heat map
|
|
385
|
+
without the head's roll.
|
|
262
386
|
|
|
263
387
|
## 8. What one character costs
|
|
264
388
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-parts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "AI-authored Spine 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through spine-rigc before anything is written. A CLI for agents that cannot see the image.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|