spine-parts 0.4.0 → 0.5.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/NOTICE.md +3 -1
- package/README.md +23 -2
- package/cli.ts +21 -8
- package/docs/AUTHORING.md +93 -27
- package/package.json +2 -2
- package/skills/spine-parts/SKILL.md +7 -5
- package/src/check.ts +452 -30
- package/src/config.ts +14 -0
- package/src/instruments.ts +358 -3
- package/src/propose.ts +32 -7
package/NOTICE.md
CHANGED
|
@@ -39,12 +39,14 @@ wrapper code.
|
|
|
39
39
|
| Weights | Licence as stated on the model card | Observed |
|
|
40
40
|
| --- | --- | --- |
|
|
41
41
|
| [`layerdifforg/seethroughv0.0.2_layerdiff3d`](https://huggingface.co/layerdifforg/seethroughv0.0.2_layerdiff3d) | `license:apache-2.0` tag | 2026-09-27, Hugging Face model API |
|
|
42
|
+
| (same card, after its 2026-09-28 commit *"Add licence terms, NOTICE and base model metadata"*) | `license:openrail++` tag; the card's *Licence* section says the maintainers' own contributions are Apache-2.0 and the weights also inherit the licences of the models they are derived from — Animagine XL 4.0 / Stable Diffusion XL 1.0 (CreativeML Open RAIL++-M) and LayerDiffuse (CreativeML Open RAIL-M) | 2026-09-28, Hugging Face model API and card |
|
|
42
43
|
| [`layerdifforg/seethroughv0.0.1_marigold`](https://huggingface.co/layerdifforg/seethroughv0.0.1_marigold) | no licence tag on the card | 2026-09-27, Hugging Face model API |
|
|
44
|
+
| (same card, after its 2026-09-28 revision) | `license:openrail++` tag | 2026-09-28, Hugging Face model API |
|
|
43
45
|
|
|
44
46
|
Maintainer statement, on the Hugging Face discussion
|
|
45
47
|
[`layerdifforg/seethroughv0.0.2_layerdiff3d/discussions/1`](https://huggingface.co/layerdifforg/seethroughv0.0.2_layerdiff3d/discussions/1)
|
|
46
48
|
(user `24yearsold`, 2026-04-07): *"Yes all our models are aligned with our main
|
|
47
|
-
repo to use Apache 2.0."*
|
|
49
|
+
repo to use Apache 2.0."* The 2026-09-28 card revision above narrows that: Apache-2.0 for the maintainers' contributions, with the base models' Open RAIL terms inherited. spine-parts does not distribute these weights; a user who fetches them accepts the terms the card states on the day they fetch.
|
|
48
50
|
|
|
49
51
|
### Training data
|
|
50
52
|
|
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ page** (issue #2):
|
|
|
90
90
|
| `check/build/skeleton.json`, `skeleton.atlas`, `skeleton.png` | **the artifact** — Spine 4.3 skeleton data and one packed page, written by `rigc build --pack` and gated under both profiles |
|
|
91
91
|
| `parts/*.png`, `parts.json`, `recomposite_rig.png`, `recomposite_error_rig.png` | the loose parts, each cropped to its alpha box; the record of where every part came from, how many of its pixels were re-taken from the painting, and the recomposite's uncovered holes with their boxes; the flat stack of parts; its error map — red where no part covers a pixel the painting has, blue where a part covers it in the wrong colour |
|
|
92
92
|
| `rig/` | `rig.json` and `motion.json` in spine-rigc's spec, `mesh_report.json`, the padded `images/` |
|
|
93
|
-
| `check/` | both gate files verbatim, the idle's frames, `contact.png`, `motion_heat.png`, `check.json` |
|
|
93
|
+
| `check/` | both gate files verbatim, the idle's frames and their `geometry.json` (skinned vertices per frame), `contact.png`, `motion_heat.png`, `check.json` |
|
|
94
94
|
| `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 |
|
|
95
95
|
|
|
96
96
|
The last lines of a green build are the pack line, printed beside the Spine example
|
|
@@ -132,6 +132,27 @@ spine-parts at what it wrote:
|
|
|
132
132
|
| Upstream CLI | `python inference/scripts/inference_psd.py --srcp <image> --save_to_psd` in a checkout of [shitagaki-lab/see-through](https://github.com/shitagaki-lab/see-through) | the `.psd` in `workspace/layerdiff_output/` |
|
|
133
133
|
| ComfyUI wrapper | [jtydhr88/ComfyUI-See-through](https://github.com/jtydhr88/ComfyUI-See-through) on your own ComfyUI box; the optional `comfy` adapter (`spine-parts comfy seethrough`) drives it | the `layers.json` + PNGs it writes |
|
|
134
134
|
|
|
135
|
+
**The head run is needed on every route that was read.** See-through's model
|
|
136
|
+
already separates the head in a second stage of its own: it crops the head the
|
|
137
|
+
first stage found, runs the face tags on that crop at the same `resolution`, and
|
|
138
|
+
pastes the result back **resized to the full image's scale**. So the face comes
|
|
139
|
+
back at the full run's pixel density whichever route ran it, and the separate
|
|
140
|
+
head run on `st_input_head.png` is what gives the eyes more pixels
|
|
141
|
+
(read on 2026-09-28 at see-through `a25a549` and wrapper 0.5.0 `98d754b`):
|
|
142
|
+
|
|
143
|
+
- *Hugging Face Space* — the same two-stage code, with `resolution` capped at
|
|
144
|
+
1280 (768 by default): the head run is still needed.
|
|
145
|
+
- *ModelScope demo* — its code was not read, so what the head run adds on that
|
|
146
|
+
route has not been measured.
|
|
147
|
+
- *Upstream CLI* — the `.psd` frame is the `resolution`-sided square the first
|
|
148
|
+
stage ran on (1280 by default), and the head stage is pasted into it at that
|
|
149
|
+
scale: the head run is still needed.
|
|
150
|
+
- *ComfyUI wrapper* — `SeeThrough_GenerateLayers` runs both stages with no input
|
|
151
|
+
to change the second, at most `resolution` 2048, which on the examples' 2432 px
|
|
152
|
+
square is 1.19 source px per layer px against 0.77 and 0.47 for their head
|
|
153
|
+
crops; at the 1024 the examples ran, the full run's right eye white is 22×13 px
|
|
154
|
+
and the head run's 60×38: the head run is still needed.
|
|
155
|
+
|
|
135
156
|
**See-through is required; ComfyUI is not.** The wrapper is one route among
|
|
136
157
|
four, and `spine-parts comfy` is only a convenience for that route: every
|
|
137
158
|
stage after See-through reads files, whichever route wrote them. On the two
|
|
@@ -222,7 +243,7 @@ agent skill.
|
|
|
222
243
|
| `propose --parts --source --out [--compare <config>]` | propose bones, meshes, regions and an idle; draw the overlay |
|
|
223
244
|
| `propose … --from-config <config>` | draw and LINT the config's current bones |
|
|
224
245
|
| `rig --config --parts --out [--idle-keys ctl\|direct]` | author `rig.json` + `motion.json`, written only after spine-rigc's round trip is green; `--idle-keys` says whether the idle's keys on mesh-driving bones go through `<bone>_ctl` parents (default) or stay on the bones with `invariants.idleDrivesMeshes` declared |
|
|
225
|
-
| `check --rig --out [--parts]` | build packed, gate under both profiles, render the idle, measure seam, loop and the
|
|
246
|
+
| `check --rig --out [--parts]` | build packed, gate under both profiles, render the idle, measure seam, loop and the six judgement lines (mesh texture stretch among them, from the idle's `render --geometry`), and report the recomposite's holes from `parts.json` |
|
|
226
247
|
| `loop --frames <dir> --out <file.gif \| file.png> [--palette]` | encode a rendered idle as a looping GIF, lossless APNG, or indexed APNG (`--palette`) |
|
|
227
248
|
| `build --config --source --full --head --out [--seam] [--project] [--loop]` | assemble, rig and check in one process, stopping at the first refusal |
|
|
228
249
|
|
package/cli.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { basename, dirname, extname, join } from 'node:path';
|
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
19
|
import { DEFAULT_PROJECT_RULE, DEFAULT_SEAM_RULE, HOLES_LISTED, PROJECT_RULES, type ProjectRule, proposeFields, proposePlan, SEAM_RULES, type SeamRule } from './src/assemble.ts';
|
|
20
20
|
import { assembleStage, build, checkStage, ERROR_MAP_FILE, loopStage, readRuns, readSource, rigStage } from './src/build.ts';
|
|
21
|
-
import { findRigc, PARTS_HOME_SENTENCE, type RigcRunner, SEAM_MEAN_BAR, SEAM_PX_BAR, SEAM_PX_LEVEL, SPINEBOY_YARDSTICK } from './src/check.ts';
|
|
21
|
+
import { findRigc, PARTS_HOME_SENTENCE, RIGC_GEOMETRY_VERSION, type RigcRunner, SEAM_MEAN_BAR, SEAM_PX_BAR, SEAM_PX_LEVEL, SPINEBOY_YARDSTICK, TEXTURE_STRETCH_CEILING } from './src/check.ts';
|
|
22
22
|
import { ComfyClient, resolveHost, runPainting, runSeeThrough } from './src/comfy/index.ts';
|
|
23
23
|
import { type CharacterConfig, loadConfig, loadEarlyConfig } from './src/config.ts';
|
|
24
24
|
import { PartsError, problemLine } from './src/errors.ts';
|
|
@@ -77,7 +77,8 @@ usage:
|
|
|
77
77
|
regions, motion with its blink — and a blink.still cut for a lash that
|
|
78
78
|
reaches far above its eyewhite, when a clear row allows one; no blink
|
|
79
79
|
when no part is an eyewhite, and no brows in it when no part is an
|
|
80
|
-
eyebrow, each said in a note
|
|
80
|
+
eyebrow, each said in a note; an iris or lash on a side with no
|
|
81
|
+
eyewhite rides head, said in a note — and notes)
|
|
81
82
|
and the overlay to correct against, <out>/render/landmarks.png and
|
|
82
83
|
landmarks_head.png. Prints every
|
|
83
84
|
note and a LINT line for each chain link that lies off its mesh's art, for a
|
|
@@ -105,8 +106,9 @@ usage:
|
|
|
105
106
|
and one idle of sines and a blink whose closed hold is at least one
|
|
106
107
|
12 fps frame, so the idle frames and the loop show the eyes shut
|
|
107
108
|
(RIG_BLINK_HOLD_SPANS_A_FRAME otherwise); no blink when the config
|
|
108
|
-
states no motion.blink, and a blink group that names no bone
|
|
109
|
-
by the loader, CONFIG_BLINK_GROUP_MEMBERS
|
|
109
|
+
states no motion.blink, and a blink group that names no bone, or one
|
|
110
|
+
bone twice, is refused by the loader, CONFIG_BLINK_GROUP_MEMBERS or
|
|
111
|
+
CONFIG_BLINK_GROUP_UNIQUE, before rigc starts. --parts is the
|
|
110
112
|
directory holding parts.json and parts/<name>.png. The result is built
|
|
111
113
|
through spine-rigc (profile spine-html, packed, then validated under
|
|
112
114
|
profile spine) in a scratch directory first, and --out receives
|
|
@@ -127,19 +129,30 @@ usage:
|
|
|
127
129
|
Both are only read. Into --out:
|
|
128
130
|
build/ (rigc build --profile spine-html --pack: the packed atlas is the
|
|
129
131
|
artifact), gate_spine-html.txt and gate_spine.txt (the gate lines
|
|
130
|
-
verbatim), idle_frames/ (rigc render --animation idle --fps 12 --max 640
|
|
132
|
+
verbatim), idle_frames/ (rigc render --animation idle --fps 12 --max 640
|
|
133
|
+
--geometry: the frames and geometry.json, the skinned vertices and
|
|
134
|
+
every bone's world transform per frame),
|
|
131
135
|
contact.png, motion_heat.png and check.json. PASS needs both gates
|
|
132
136
|
"0 failed", the seam (setup pose vs the flat composite of parts/) at mean
|
|
133
137
|
|d| <= ${SEAM_MEAN_BAR.toFixed(1)} with <= ${SEAM_PX_BAR} px over ${SEAM_PX_LEVEL}, and the loop (idle frame 0 vs the
|
|
134
|
-
frame at t = duration) at max |d| 0. Then
|
|
138
|
+
frame at t = duration) at max |d| 0. Then six judgement lines, each in
|
|
135
139
|
check.json and on the console as NAME: PASS|FAIL|SKIP with its figures and
|
|
136
140
|
bars (AUTHORING §7): BREATH_VISIBLE (the topwear moves, the footwear does
|
|
137
141
|
not, each rendered alone), BLINK_NO_HOLE (the setup pose with the blink
|
|
138
142
|
held shut shows no background inside the eyewhite box), CHAIN_LAG (every
|
|
139
143
|
rotate track lags its keyed ancestor and amplitude grows down each
|
|
140
144
|
chain, read off motion.json), TIP_OVER_ROOT (each handwear/bottomwear
|
|
141
|
-
part's lower half travels further than its upper half)
|
|
142
|
-
STILL_REGIONS_DARK (the
|
|
145
|
+
part's lower half travels further than its upper half),
|
|
146
|
+
STILL_REGIONS_DARK (the face outline in the frame of the bone the face's
|
|
147
|
+
slot rides, at most twice the resampler's own error on this rig — read
|
|
148
|
+
off the whole rig moved rigidly as the idle moves that bone — with the
|
|
149
|
+
screen-space figure reported beside it; the feet in screen space) and
|
|
150
|
+
TEXTURE_STRETCH (every mesh triangle's edges over the idle against their
|
|
151
|
+
rest length, max(ratio, 1/ratio) <= ${TEXTURE_STRETCH_CEILING}; the worst
|
|
152
|
+
triangle is named by slot, triangle, vertices, edge and frame).
|
|
153
|
+
Needs spine-rigc ${RIGC_GEOMETRY_VERSION} or later, whose render writes
|
|
154
|
+
geometry.json; an older rigc is refused, CHECK_RIGC_VERSION, before
|
|
155
|
+
anything is built.
|
|
143
156
|
Regions come from parts.json's See-through tags; a line with nothing to
|
|
144
157
|
read says SKIP and why — neither a pass nor a failure — and PASS needs
|
|
145
158
|
every line that measured to be PASS. Then RECOMPOSITE_HOLES: REPORTED,
|
package/docs/AUTHORING.md
CHANGED
|
@@ -76,7 +76,7 @@ read by no CPU stage).
|
|
|
76
76
|
| `regions.<part>` | rig | **proposed**: the bone a rigid part rides. Every plan part is exactly one of a mesh or a region (`CONFIG_PART_ATTACHED`) |
|
|
77
77
|
| `motion.duration` | rig; check (the loop is measured at this time) | proposed as 4 s; a whole number of 1/12 s ticks, because `check` renders at 12 fps |
|
|
78
78
|
| `motion.tracks` | rig | **proposed**, then tuned. Single `{bone, prop, amp, period, phase, base?}` or chain `{chain, amps, period, phase, lag}` — one amplitude per link, link `i` at phase `phase + lag·i`. Every `period` must divide `duration` (`CONFIG_PERIOD_DIVIDES_DURATION`) |
|
|
79
|
-
| `motion.blink` | rig | **proposed**: `{t, eyes, brows, squash, brow_drop}`; the whole blink, `t` to `t + 0.364` s (`t + 0.314` s without brows: the brows' keys are the ones that end last), must fit inside the idle (`RIG_BLINK_INSIDE_IDLE`). The `eyes` group's `scaley` squashes every part on the eye bones about the bone's origin, the eyewhite's centre. Optional: a figure with no `eyewhite-r`/`-l` part has no eye bone, so `propose` writes no `blink` and notes `no blink: no eyewhite part (looked for: eyewhite-r, eyewhite-l), …`; with eyes and no `eyebrow-r`/`-l` part it writes the blink without `brows` and `brow_drop` and notes `blink without brows: …`. `brows` and `brow_drop` are stated together or not at all (`CONFIG_BLINK_BROWS_PAIRED`), and a group that names no bone is refused (`CONFIG_BLINK_GROUP_MEMBERS`) — rigc would refuse it one stage later, `group "eyes" declares no members
|
|
79
|
+
| `motion.blink` | rig | **proposed**: `{t, eyes, brows, squash, brow_drop}`; the whole blink, `t` to `t + 0.364` s (`t + 0.314` s without brows: the brows' keys are the ones that end last), must fit inside the idle (`RIG_BLINK_INSIDE_IDLE`). The `eyes` group's `scaley` squashes every part on the eye bones about the bone's origin, the eyewhite's centre. Optional: a figure with no `eyewhite-r`/`-l` part has no eye bone, so `propose` writes no `blink` and notes `no blink: no eyewhite part (looked for: eyewhite-r, eyewhite-l), …`; with eyes and no `eyebrow-r`/`-l` part it writes the blink without `brows` and `brow_drop` and notes `blink without brows: …`. `brows` and `brow_drop` are stated together or not at all (`CONFIG_BLINK_BROWS_PAIRED`), and a group that names no bone is refused (`CONFIG_BLINK_GROUP_MEMBERS`) — rigc would refuse it one stage later, `group "eyes" declares no members` — as is a group that names one bone twice (`CONFIG_BLINK_GROUP_UNIQUE`; rigc: `group "eyes" names member "eye" twice`). An `irides-*` or `eyelash-*` part on a side with no eyewhite has no eye bone to ride: `propose` places it on `head` as a region, which the blink does not move, and notes `no eyewhite part for eye_r (looked for: eyewhite-r): iris (head:irides-r) is placed on head as a region, …`. The timing is fixed, not a field (table below) |
|
|
80
80
|
| `motion.blink.still` | rig | **proposed** only for a lash `propose` notes (below), then checked: `{<part>: {row, bone}}` — the rows of that region part above `row` (rig px, y down) are drawn by a second slot `<part>_still` on `bone` and do not blink; rows from `row` down keep the part's slot and blink. The part must be a region on a bone `eyes` names, `bone` one it does not (`CONFIG_STILL_OFF_THE_BLINK`), and `row` a row of the part with no art across its whole width (`RIG_STILL_ROW_CLEAR`) |
|
|
81
81
|
|
|
82
82
|
The blink's timing is the tree's (`BLINK` in `src/motion.ts`), in seconds after
|
|
@@ -101,7 +101,8 @@ leave it 0.01 s after the lids open. The rig stage refuses a hold under one fram
|
|
|
101
101
|
name (`RIG_BLINK_HOLD_SPANS_A_FRAME`, §6), counted over the same 250,000 phases.
|
|
102
102
|
|
|
103
103
|
The proposer's reach (from `src/propose.ts`): roles come from each part's tag —
|
|
104
|
-
`face` makes `hip`/`chest`/`neck`/`head`, the eyewhites make the eye bones
|
|
104
|
+
`face` makes `hip`/`chest`/`neck`/`head`, the eyewhites make the eye bones (irides and
|
|
105
|
+
lashes ride them, or `head` on a side with no eyewhite), brows are
|
|
105
106
|
told apart by position, `bottomwear` makes the hip and three skirt chains,
|
|
106
107
|
`handwear` makes sleeve chains (one blob or two), `front hair` makes fringe chains
|
|
107
108
|
and a lock chain for each strand that hangs below the chin, `back hair` makes a bun
|
|
@@ -226,7 +227,11 @@ full loader before its assemble stage writes anything.
|
|
|
226
227
|
4. `spine-parts propose --head-box --full inputs/layers/full --canvas <W>x<H>` →
|
|
227
228
|
`seethrough.head_box`. Run `spine-parts inputs` again: it now also writes
|
|
228
229
|
`st_input_head.png`, the painting cropped to that box at its exact size.
|
|
229
|
-
5. **See-through, head run** (external), on that crop.
|
|
230
|
+
5. **See-through, head run** (external), on that crop. See-through's own second,
|
|
231
|
+
head stage does not replace it: that stage's layers are pasted back at the full
|
|
232
|
+
run's scale on every route that was read (README *Getting See-through layers*),
|
|
233
|
+
so the full run's eyes stay at its density — 22×13 px for the demo's right eye
|
|
234
|
+
white at `resolution` 1024, against 60×38 px in the head run.
|
|
230
235
|
6. `spine-parts assemble --propose-plan --source … --full … --head … --config config.json`
|
|
231
236
|
→ paste `plan` and `extend_below_crop` into `config.assemble`. Config: the
|
|
232
237
|
`layers` row, with `head_box`.
|
|
@@ -260,9 +265,9 @@ proposal in as the steps above say (`RL01`); every step must exit 0.
|
|
|
260
265
|
| `layers` | the table, then the `WARN` lines | every tag the plan will need has opaque pixels; one `face` in the head run; `0 WARN line(s)` | a run whose layer PNG is not its box's size (`LAYERS_PNG_MATCHES_BBOX`); a `WARN PLAN_LAYER_…` line (§6, *Plausibility*) |
|
|
261
266
|
| `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 |
|
|
262
267
|
| `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`, then `uncovered holes (8-connected): N` and the largest five as `uncovered hole K: <px> px at x,y wxh (between "<part>" <px> px, …)`; and look at `render/recomposite_error_rig.png` | on the examples: `sample` 0.84 / 98.0 % / 4,512 / 1,185, 259 holes, the largest 94 px; `demo` 2.39 / 95.8 % / 11,050 / 1,564, 453 holes, the largest 70 px (default rule) — many slivers along part edges, no hole a region could hide | one large hole: part of the figure is in no layer — a plan entry is missing, hair left the head crop sideways (the demo's `hair_back` is taken from the full run for that reason), or See-through split one garment into two and left the space between them in neither (a skirt as two trouser legs); the `between` parts say where. When neither run holds it at all, an `assemble.patches` entry cuts it from the painting (below) |
|
|
263
|
-
| `propose` | `note:` lines, `LINT` lines, `landmarks.png` | no LINT line: every chain link lies on its mesh's art, and the hip is below the chest and the figure's top quarter | a link off the art (a bone on the background) — move it onto the layer; `LINT hip at [x, y] is not below chest at [x, y]: …` or `LINT hip at [x, y] is above 0.25 of the figure height (figure y T..B, so hip y must be at least L): a hip at the shoulders` — move `hip` down to the waist (and `chest` between it and the neck); a `hanging strand … -- no chain proposed` note — that strand hangs stiff until you add a chain down the x and rows it names (§3); a `no blink: …` or `blink without brows: …` note — the idle will not blink (or its brows will not drop), because no part came from an `eyewhite` (or `eyebrow`) layer; the note names the tags looked for |
|
|
268
|
+
| `propose` | `note:` lines, `LINT` lines, `landmarks.png` | no LINT line: every chain link lies on its mesh's art, and the hip is below the chest and the figure's top quarter | a link off the art (a bone on the background) — move it onto the layer; `LINT hip at [x, y] is not below chest at [x, y]: …` or `LINT hip at [x, y] is above 0.25 of the figure height (figure y T..B, so hip y must be at least L): a hip at the shoulders` — move `hip` down to the waist (and `chest` between it and the neck); a `hanging strand … -- no chain proposed` note — that strand hangs stiff until you add a chain down the x and rows it names (§3); a `no blink: …` or `blink without brows: …` note — the idle will not blink (or its brows will not drop), because no part came from an `eyewhite` (or `eyebrow`) layer; the note names the tags looked for; a `no eyewhite part for eye_<s> …` note — the iris and lash parts it names ride `head` and do not blink, because no `eyewhite-<s>` part made that side's eye bone: take the eyewhite from the other run, or keep them on `head` |
|
|
264
269
|
| `rig` (inside `build`) | one line per mesh: vertices, triangles, bones, influences, `cover`; the `bones` line; the `idle keys` line; then rigc's gate lines (with A15's declared SKIP under `--idle-keys direct`) | `cover 1.00000` on every mesh, both gates `0 failed` | `RIG_LATTICE_ONE_LOOP`: change that mesh's `grid` |
|
|
265
|
-
| `check` (inside `build`) | the gate lines verbatim, the pack line, `loop:`, `seam:`, the
|
|
270
|
+
| `check` (inside `build`) | the gate lines verbatim, the pack line, `loop:`, `seam:`, the six judgement lines and `RECOMPOSITE_HOLES` (§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) |
|
|
266
271
|
| `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) |
|
|
267
272
|
|
|
268
273
|
`loop` writes three files from one frame set, and they are not interchangeable.
|
|
@@ -482,6 +487,7 @@ See-through with another seed.
|
|
|
482
487
|
| `CONFIG_AMPS_MATCH_CHAIN` | a chain track's `amps` is not one per link | that track's `amps` |
|
|
483
488
|
| `CONFIG_PERIOD_DIVIDES_DURATION` | a period that is not a whole fraction of the idle — the loop could not close | that track's `period`, or `motion.duration` |
|
|
484
489
|
| `CONFIG_BLINK_GROUP_MEMBERS` | `motion.blink.eyes` or `motion.blink.brows` is `[]`: a group with no members, which rigc refuses at the gate | a figure with no eyewhite part has nothing to blink: leave `motion.blink` out; one with no eyebrow part: leave `brows` and `brow_drop` out |
|
|
490
|
+
| `CONFIG_BLINK_GROUP_UNIQUE` | `motion.blink.eyes` or `motion.blink.brows` names one bone more than once; the detail gives the bone and every index it sits at. rigc refuses it at the gate (`group "eyes" names member "eye" twice`) | name each bone once: the group keys every member it names |
|
|
485
491
|
| `CONFIG_BLINK_BROWS_PAIRED` | `motion.blink.brows` without `brow_drop`, or `brow_drop` without `brows` | state both or neither |
|
|
486
492
|
| `CONFIG_STILL_OFF_THE_BLINK` | a `motion.blink.still` entry names a region on a bone the blink's `eyes` does not name (nothing to hold still), or its `bone` is one the blink's `eyes` names (the still piece would blink) | that entry's part, or its `bone` — the eye bone's parent, `head` as proposed |
|
|
487
493
|
|
|
@@ -550,6 +556,7 @@ See-through with another seed.
|
|
|
550
556
|
| `CHECK_RIGC_PRESENT` | no `rigc` binary found (every place looked is listed) | `bun install` |
|
|
551
557
|
| `CHECK_INPUT_PRESENT` on `…/parts.json` | `--parts` named a directory without `parts.json` — most often `parts/` itself. The detail says it: "--parts names the directory holding parts.json and parts/, not parts/ itself (after build, that is build's --out, whose rig is <out>/rig); without --parts it is --rig", and names the parent when the parent holds `parts.json` | `--parts` — the directory above `parts/` |
|
|
552
558
|
| `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) |
|
|
559
|
+
| `CHECK_RIGC_VERSION` | `rigc --version` is below 1.4.0, or prints no version: its `render` has no `--geometry`, which `TEXTURE_STRETCH` reads. Refused before anything is built | `bun install` (this package depends on spine-rigc ^1.5.1), or put a newer `rigc` first on `PATH` |
|
|
553
560
|
| `CHECK_RIGC_GREEN` | a rigc step failed; its line is quoted | as `RIG_RIGC_GREEN` |
|
|
554
561
|
| `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 |
|
|
555
562
|
| `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 |
|
|
@@ -558,8 +565,9 @@ See-through with another seed.
|
|
|
558
565
|
| `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 |
|
|
559
566
|
| `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) |
|
|
560
567
|
| `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) |
|
|
561
|
-
| `CHECK_STILL_REGIONS_DARK` | the heat map is brighter than the ceiling
|
|
562
|
-
| `
|
|
568
|
+
| `CHECK_STILL_REGIONS_DARK` | over the face outline, carried into the frame of the bone the face's slot rides: something moves there that the head does not carry, above twice the resampler's own error on this rig (both figures quoted, with the screen-space one beside them); over the feet, the heat map is brighter than the ceiling | on the face: a mesh over it weighted to a bone the head does not carry (`segments`), or a swinging part (a bang, a chain) whose art crosses the face outline; on the feet: a mesh weighted to a swinging bone, or a region on the wrong bone |
|
|
569
|
+
| `CHECK_TEXTURE_STRETCH` | a mesh triangle's edge, in some idle frame, is more than 1.926544 times its rest length or less than 1/1.926544 of it (the mesh, triangle, its three vertices, the edge and the frame are named); or a rest edge has length 0 | the chain tracks' `amps` on the bones that mesh is weighted to (the stretch grows with them), or the mesh's `segments` — a vertex blending two bones that swing against each other |
|
|
570
|
+
| `CHECK_SEAM_FRAME_SIZE`, `FRAMES_SIDECAR`, `CHECK_GEOMETRY_FILE` | rigc's render is not what its `frames.json` says, or its `geometry.json` is not a whole `rigc-geometry/1` export of the frames beside it (a frame count, a mesh whose vertex count differs from its rest entry; for the face half, another viewport than `frames.json`'s, other frame indices, the head bone or a feature's bone missing from a frame, a non-finite transform or time) | a rigc problem; report it |
|
|
563
571
|
| `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 |
|
|
564
572
|
| `BUILD_ARTIFACT_PRESENT` | the packed build lacks its `.json`, `.atlas` or page | a rigc problem; report it |
|
|
565
573
|
|
|
@@ -578,8 +586,8 @@ implementation's bars):
|
|
|
578
586
|
On the examples: `sample` 23/23 and 14/14, seam 0.207 with 0 pixels over 40, loop 0;
|
|
579
587
|
`demo` (default rule) 23/23 and 14/14, seam 0.326 with 2 pixels over 40, loop 0.
|
|
580
588
|
|
|
581
|
-
A green gate cannot see a wrong animation, so `check` also writes
|
|
582
|
-
lines** (issue #11), each a key of `check.json` and a console line
|
|
589
|
+
A green gate cannot see a wrong animation, so `check` also writes six **judgement
|
|
590
|
+
lines** (issue #11; `TEXTURE_STRETCH`, issue #31), each a key of `check.json` and a console line
|
|
583
591
|
`NAME: PASS|FAIL|SKIP — <figures and bars>`, read the same way as the lines above: a
|
|
584
592
|
FAIL makes `PASS` false and prints its own `FAIL CHECK_<NAME>` line (§6); a SKIP
|
|
585
593
|
says why the rig gave the line nothing to read, and is neither a pass nor a failure —
|
|
@@ -590,7 +598,9 @@ change from idle frame 0, in levels of 255, on the idle's 640-pixel grid.
|
|
|
590
598
|
Each bar follows one rule: a floor is half the weaker example's figure and a ceiling
|
|
591
599
|
twice the worse one's, so the weaker example clears it by a factor of two; a bar the
|
|
592
600
|
model itself fixes (a part on an unkeyed bone does not move, a lag is above 0, a hole
|
|
593
|
-
is 0 pixels) is that value, not a margin
|
|
601
|
+
is 0 pixels) is that value, not a margin — and where the model's value is an error
|
|
602
|
+
the rig itself measures (a face the head carries is still in the head's frame up to
|
|
603
|
+
the resampler's error), the bar is that measurement times the same two. The figures are this port's, measured on
|
|
594
604
|
the two examples [observed]:
|
|
595
605
|
|
|
596
606
|
| line | measures | bar | `demo` | `sample` | SKIP when |
|
|
@@ -600,7 +610,8 @@ the two examples [observed]:
|
|
|
600
610
|
| ″ (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 |
|
|
601
611
|
| `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 |
|
|
602
612
|
| `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 |
|
|
603
|
-
| `STILL_REGIONS_DARK` | the
|
|
613
|
+
| `STILL_REGIONS_DARK` | the face outline **in the head's own frame**: each idle frame sampled (bilinear) where the head bone has carried each setup-pose pixel of the region, the heat taken over those samples. The head bone is the bone the `face` parts' slot rides in `rig.json`, and its per-frame world transform is `rigc render --geometry`'s. The region is the frame pixels whose whole footprint — 1 + 1/scale rig px each way, what the rasteriser's and this sampler's bilinear taps can reach — is `face` on top of the flat stack, clear by that reach of where `eyewhite`, `irides`, `eyelash`, `eyebrow` and `mouth` go in the head's frame over the idle. `resampler_heat_mean` is the same figure off the whole rig moved rigidly as the idle moves the head (the root keyed to the head's motion, every other key dropped), where every pixel is still in the head's frame by construction. `screen_heat_mean`/`max` over `screen_px` are the screen-space figures this line held until issue #33 (where `face` is on top, less the features' setup boxes), reported. The feet (where `footwear` is on top): the idle's heat in screen space — they ride the root, which the idle does not move, so the screen is their own frame | face `head_frame_heat_mean` ≤ 2 × `resampler_heat_mean` (`mean_ceiling`); feet mean ≤ 3.244 | face 1.321 ≤ 2.292 (resampler 1.146; screen 16.988); feet 1.622 | face 0.884 ≤ 1.765 (resampler 0.883; screen 11.475); feet 0 | no `face` and no `footwear` part (one of the two absent leaves that half unmeasured; so do face parts on two slot bones, or idle frames with no `geometry.json`) |
|
|
614
|
+
| `TEXTURE_STRETCH` | every mesh triangle's three edges in every idle frame, read from the `geometry.json` the idle render writes (`rigc render --geometry`, spine-rigc 1.4.0 or later): skinned length over rest length, the rest being the setup pose's bones with no deform. The figure is the rig's worst max(ratio, 1/ratio); each mesh's largest and smallest ratio, with triangle, vertices, edge and frame, is the detail | max(ratio, 1/ratio) ≤ 1.926544 | 1.388: `hair_back` triangle 449, edge 42-43 at 0.720 in frame 26 (largest stretch 1.291, frame 2) | 1.227: `sleeves` triangle 169, edge 126-51 at 0.815 in frame 28 (largest stretch 1.196, frame 5) | the rig draws no mesh, or the render wrote no `geometry.json` |
|
|
604
615
|
|
|
605
616
|
After them, one **reported line** with no bar:
|
|
606
617
|
|
|
@@ -651,22 +662,70 @@ What each figure is, and is not:
|
|
|
651
662
|
mean heat is 1.1 times the upper half's while its centroid travels 3.7 times as far.
|
|
652
663
|
The halves split the box across its rows, which assumes the part hangs — true of a
|
|
653
664
|
front-facing standing figure, the only input this tool takes.
|
|
654
|
-
- **
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
665
|
+
- **Stretch is read on edges, both ways, and the ceiling doubles |ln r|.** Compression
|
|
666
|
+
distorts a texture as much as stretch — an edge at half its rest length squeezes
|
|
667
|
+
the texels on it by the factor an edge at twice its length spreads them — so the
|
|
668
|
+
figure is max(ratio, 1/ratio), and on both examples the worst triangle is a
|
|
669
|
+
compression (demo 0.720, sample 0.815). The ceiling rule is applied to the quantity
|
|
670
|
+
whose zero means "nothing happened", as it is for heat: for a ratio that is |ln r|,
|
|
671
|
+
so twice demo's |ln 1.388| is ln(1.388²) = ln 1.926544 and sample clears it by 3.2
|
|
672
|
+
times. Doubling the ratio itself (2.776) was rejected: its zero is not at 1, and it
|
|
673
|
+
passes an edge drawn at 2.7 times its rest length. The rest is the art's own
|
|
674
|
+
proportions — on both examples every rest edge equals its `uvs` edge times the
|
|
675
|
+
attachment's size to within 2.4e-5, the uvs' six-decimal rounding.
|
|
676
|
+
- **The face is measured in the head's frame, and its bar is the model's (issue #33).**
|
|
677
|
+
Until #33 the face half read the screen-space heat map, where the head's intended
|
|
678
|
+
roll lights the outline on both examples (16.988 and 11.475, the figures now
|
|
679
|
+
reported as `screen_heat_mean`), so its ceiling was twice the worse example,
|
|
680
|
+
33.976 — a ceiling a smooth fixture face sliding two rig pixels stayed under (24.8;
|
|
681
|
+
eight pixels measured 85.5). The judgement means *nothing moves on the face that
|
|
682
|
+
the head does not move*, so the heat is now taken after each frame's face region is
|
|
683
|
+
carried back through the head bone's transform. A region the head carries is then
|
|
684
|
+
still up to the resampler's error, and nothing more. That error depends on the art
|
|
685
|
+
and the scale — 1.146 on the demo, 0.883 on the sample, 0.47 on the selftest's
|
|
686
|
+
smooth fixture face — so no one constant can be the bar: it is measured on the same
|
|
687
|
+
rig, off a throwaway animation that moves the whole rig rigidly as the idle moves
|
|
688
|
+
the head, and the ceiling is twice that (the tree's one margin). The calibration is
|
|
689
|
+
its own `rigc build`: sharing the setup still's build widened the still's viewport
|
|
690
|
+
(rigc fits it to every animation a skeleton holds) and moved the seam.
|
|
691
|
+
- **The region is the pixels that read the face and nothing else.** A pixel at the
|
|
692
|
+
outline's rim, or next to a feature, reads a neighbour's art through the two
|
|
693
|
+
bilinear taps, and that neighbour moving lights it with nothing on the face moving:
|
|
694
|
+
measured over the rim, a fixture face that does not move at all over a breathing
|
|
695
|
+
torso read 1.533 against a bar of 0 (its head never moves, so its resampler error is
|
|
696
|
+
exactly 0), and the proposer's own strand fixture went red on the brows the blink
|
|
697
|
+
drops below their setup boxes. So the region keeps only pixels whose whole
|
|
698
|
+
footprint is the face (228 of the demo's 515 screen-space pixels, 271 of the
|
|
699
|
+
sample's 453), clear of where each feature goes in the head's frame over the idle
|
|
700
|
+
(its setup box carried by its own bone and back through the head's). Measured over
|
|
701
|
+
the rim instead, both examples sat at 1.31 and 1.34 times their calibration, and
|
|
702
|
+
that excess was the bangs' art sliding over the rim.
|
|
703
|
+
- **What the examples show in the head's frame.** The sample is still to its
|
|
704
|
+
calibration (0.884 against 0.883). The demo reads 1.15 times it (1.321 against
|
|
705
|
+
1.146), and most of that is the calibration's own noise, not motion: the face
|
|
706
|
+
rendered alone (`--slot face`), rigid on the head by construction, reads 1.296 on
|
|
707
|
+
the idle's grid against 1.146 on the calibration's — rigc fits each render's
|
|
708
|
+
viewport to its own animation, so the two grids sit at a different sub-pixel phase
|
|
709
|
+
on the art (13 %). The rest, 0.025, is `hair_front`: rendered without it the figure
|
|
710
|
+
is the face-alone one. Both clear the bar, the demo by a factor of 1.74 and the sample by 2.00.
|
|
711
|
+
- **What the line now catches, and where it stops.** The selftest's fixture face,
|
|
712
|
+
a mesh weighted wholly to a bone that slides two rig pixels under its head, reads
|
|
713
|
+
27.886 against a ceiling of 0 on a still head (screen space 26.901, which the
|
|
714
|
+
retired 33.976 passed) and 27.936 against 0.94 on a rolling one. The same plant on
|
|
715
|
+
the real examples, at their render scale of about 0.5 (one rig pixel is half a frame
|
|
716
|
+
pixel): two pixels read 3.216 on the sample (ceiling 1.765, red) and 2.181 on the
|
|
717
|
+
demo (2.292, green); one pixel reads 1.614 and 1.526 (both green). A smooth skin
|
|
718
|
+
interior sliding half a frame pixel changes little, and the rim that shows it most is
|
|
719
|
+
the rim the region leaves out. The line also does not judge a face that moves with
|
|
720
|
+
the bone its own slot rides: that is the head moving, whatever the bone, so a face
|
|
721
|
+
region on the wrong bone is the feet half's and the eye's to catch, not this line's.
|
|
722
|
+
- **The feet stay in screen space.** They ride the root, which the idle does not key,
|
|
723
|
+
so the screen is their own frame. On the demo the long skirt swings over the shoes,
|
|
724
|
+
which lights 1.622.
|
|
725
|
+
|
|
726
|
+
Nothing is left that only an eye answers. "No visible texture stretch" left this list
|
|
727
|
+
with issue #31 (`TEXTURE_STRETCH`), and "the face outline held still in the head's own
|
|
728
|
+
frame" with issue #33 (the face half of `STILL_REGIONS_DARK`, above).
|
|
670
729
|
|
|
671
730
|
## 8. What one character costs
|
|
672
731
|
|
|
@@ -680,3 +739,10 @@ Still only an eye answers, each a missing instrument rather than a question to a
|
|
|
680
739
|
About 6.5 minutes of GPU and under a minute of CPU per character, before any hand
|
|
681
740
|
correction. The GPU figures are one run each on one machine; the painting's figure
|
|
682
741
|
excludes choosing among seeds (the demo was chosen from eighteen candidates).
|
|
742
|
+
Since issue #33 `check` runs one more `rigc build` and one more idle render (the face
|
|
743
|
+
calibration, §7); the idle's `geometry.json` is the one issue #31 already writes. The `build` row
|
|
744
|
+
was not re-measured: A/B against the tree before that change, alternating on one
|
|
745
|
+
machine under a load average of 5 to 11, the difference in user + sys was +5.6 and
|
|
746
|
+
+0.8 s on the demo and +6.5 and −1.6 s on the sample over two rounds — inside what
|
|
747
|
+
the load moves the same build by (the pre-change demo build alone took 43.6 to
|
|
748
|
+
45.8 s there).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-parts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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": {
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"ag-psd": "^31.0.2",
|
|
60
|
-
"spine-rigc": "^1.
|
|
60
|
+
"spine-rigc": "^1.5.1"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/bun": "^1.4.2",
|
|
@@ -26,9 +26,9 @@ stage writes are the whole interface.
|
|
|
26
26
|
the value found and the value required; AUTHORING §6 maps every rule to the field
|
|
27
27
|
or input that has to change.
|
|
28
28
|
- **Green is not "looks right".** `check` measures the gates, the seam, the loop and
|
|
29
|
-
|
|
30
|
-
`TIP_OVER_ROOT`, `STILL_REGIONS_DARK`
|
|
31
|
-
|
|
29
|
+
six judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`,
|
|
30
|
+
`TIP_OVER_ROOT`, `STILL_REGIONS_DARK` — its face half in the head's own frame —
|
|
31
|
+
and `TEXTURE_STRETCH`) (AUTHORING §7). A judgement line that says
|
|
32
32
|
SKIP was not verified: report it as such, with its reason, never as passed.
|
|
33
33
|
- **A hole in the layers is invisible to every bar.** Painting that no part holds is
|
|
34
34
|
missing from the setup pose and from the flat stack alike, so the seam passes over
|
|
@@ -68,8 +68,10 @@ stage writes are the whole interface.
|
|
|
68
68
|
7. `spine-parts build --config … --source … --full … --head … --out out --loop`.
|
|
69
69
|
It stops at the first stage that refuses, printing that stage's lines under
|
|
70
70
|
`[assemble]`, `[rig]` or `[check]`. Fix what the FAIL line names and build again.
|
|
71
|
-
8. Read `out/check/check.json` — the
|
|
72
|
-
and the loop: a FAIL names the field to change (AUTHORING §6)
|
|
71
|
+
8. Read `out/check/check.json` — the six judgement lines the same way as the seam
|
|
72
|
+
and the loop: a FAIL names the field to change (AUTHORING §6);
|
|
73
|
+
`TEXTURE_STRETCH` names the worst mesh triangle (slot, triangle, vertices, edge,
|
|
74
|
+
frame), so quote it even on a PASS. Quote
|
|
73
75
|
`RECOMPOSITE_HOLES` (uncovered px, hole count, the largest hole's box) in the
|
|
74
76
|
report whatever `PASS` says. The artifact is the three paths the build prints
|
|
75
77
|
last: `check/build/skeleton.json`, `skeleton.atlas` and the packed page. Report
|