spine-parts 0.8.2 → 0.9.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 CHANGED
@@ -77,7 +77,9 @@ built from its own proposal with no hand edit at all —
77
77
  - **A character config** (`config.json`) — the part plan, the head box, the bones,
78
78
  which bones may pull which layer, and the idle's sines. `src/config.ts` is its
79
79
  schema and documents every field; the loader refuses an unknown or a missing field
80
- by name. [docs/AUTHORING.md](docs/AUTHORING.md) says where each value comes from.
80
+ by name, and reads nothing under `note`, a `<name>_note` (a string) or a key
81
+ beginning `x-` (any JSON — a project's own records, such as a past build's gate
82
+ results). [docs/AUTHORING.md](docs/AUTHORING.md) says where each value comes from.
81
83
 
82
84
  ## What it gives out
83
85
 
package/docs/AUTHORING.md CHANGED
@@ -47,9 +47,18 @@ examples use, and the one this guide assumes:
47
47
 
48
48
  `src/config.ts` is the schema and the loader. Every key is known or refused
49
49
  (`CONFIG_KEY_KNOWN`); a required key that is missing is `CONFIG_FIELD_PRESENT`.
50
- The one open door is annotation: `note`, and any key ending in `_note`, may hold a
51
- string anywhere an object is, and nothing reads it — write a hand correction's
52
- reason there, beside the value it corrected (the examples do).
50
+ There are two open doors, and nothing reads what stands behind either. An
51
+ annotation: `note`, and any key ending in `_note`, may hold a string anywhere an
52
+ object is — write a hand correction's reason there, beside the value it corrected
53
+ (the examples do). A record: any key beginning `x-` with a name after it may hold
54
+ any JSON value in the same places — a project's own provenance, such as the gate
55
+ results of a past build (`"x-status": {…}`) or the seeds a search rejected, with
56
+ reasons (`generation.x-seeds_tried`). The record test comes first, so
57
+ `x-seed_note` is a record. `xstatus`, `x_status`, `X-status` and a bare `x-` are
58
+ not record names and are refused like any unknown key. The doors are not open in
59
+ `meshes`, `regions` or `motion.blink.still`: their keys are part names, and an
60
+ `x-` key there is a part like any other. Every refusal of an unknown key, of a
61
+ retired one and of an annotation that is not a string says where a record goes.
53
62
 
54
63
  **Coordinates** in `bones`, `meshes` and `motion` are **rig pixels, y down, origin
55
64
  top-left** — the parts' own space, which is the painting times `assemble.rig_scale`.
@@ -570,7 +579,7 @@ See-through with another seed.
570
579
  | rule | means | change |
571
580
  | --- | --- | --- |
572
581
  | `CONFIG_FILE_PRESENT`, `CONFIG_IS_JSON` | no such file, or not JSON | `--config` |
573
- | `CONFIG_KEY_KNOWN`, `CONFIG_KEY_RETIRED`, `CONFIG_FIELD_PRESENT`, `CONFIG_FIELD_TYPE` | an unknown key (a retired one says what replaces it), a missing one, a wrong type | the named field |
582
+ | `CONFIG_KEY_KNOWN`, `CONFIG_KEY_RETIRED`, `CONFIG_FIELD_PRESENT`, `CONFIG_FIELD_TYPE` | an unknown key (a retired one says what replaces it), a missing one, a wrong type; a project's own record under a plain key, or an annotation (`note`, `<name>_note`) holding something other than a string | the named field; a record goes under a key beginning `x-` (any JSON, read by nothing), a remark under `note` or `<name>_note` (a string) — §3 |
574
583
  | `CONFIG_HEAD_BOX_SQUARE` | `seethrough.head_box` is not a non-empty square | `head_box` — take `propose --head-box`'s |
575
584
  | `CONFIG_TAG_KNOWN`, `CONFIG_PART_NAME`, `CONFIG_PART_UNIQUE` | a plan entry names no v3 tag, a part name that is not a file name, or a part or layer twice — a patch named like a plan part or another patch included | `assemble.plan`, `assemble.patches` |
576
585
  | `CONFIG_NAME_RESOLVES` | a parent, segment, region, track, blink member, blink still part or bone, extend part or patch `draw.before` names nothing declared above it | the named reference, or the declaration it needs |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spine-parts",
3
- "version": "0.8.2",
3
+ "version": "0.9.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": {
@@ -19,14 +19,16 @@
19
19
  *
20
20
  * The meta file holds the positive and negative prompts verbatim, what was
21
21
  * dropped as `DUP_WORDS`, where the pose words came from, the checkpoint, the
22
- * LoRAs and control as the graph used them (defaults resolved), the sampler,
23
- * the elapsed seconds and the prompt id — never the host.
22
+ * LoRAs, control and sampler as the graph used them (defaults resolved, each
23
+ * by its named fields, so an annotation or an `x-` record its author wrote in
24
+ * the config never reaches the meta), the elapsed seconds and the prompt id —
25
+ * never the host.
24
26
  */
25
27
  import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
26
28
  import { join } from 'node:path';
27
29
  import type { PaintConfig } from '../config.ts';
28
30
  import { type Problem, refuseIfAny } from '../errors.ts';
29
- import { buildPrompts, checkGraph, paintingGraph, resolvedControl, resolvedLoras } from '../graphs.ts';
31
+ import { buildPrompts, checkGraph, paintingGraph, resolvedControl, resolvedLoras, resolvedSampler } from '../graphs.ts';
30
32
  import { decodePngBytes, encodePngBytes } from '../raster/png.ts';
31
33
  import { renderSkeleton } from '../skeleton.ts';
32
34
  import { ComfyClient, historyImages, refuse } from './client.ts';
@@ -103,7 +105,7 @@ export async function runPainting(client: ComfyClient, run: PaintRun, say: (line
103
105
  key: run.config.key,
104
106
  checkpoint: g.checkpoint,
105
107
  loras: resolvedLoras(g.loras),
106
- sampler: g.sampler,
108
+ sampler: resolvedSampler(g.sampler),
107
109
  control,
108
110
  pose_from: prompts.poseFrom,
109
111
  dropped: prompts.dropped,
package/src/config.ts CHANGED
@@ -12,10 +12,20 @@
12
12
  *
13
13
  * 🔒 **Every key is known or refused.** An unknown key is refused by name and
14
14
  * so is a missing one — a config that half-loads is a rig nobody can reason
15
- * about. The one open door is annotation: `note`, and any key ending in
16
- * `_note`, may hold a string anywhere an object is expected, and nothing reads
17
- * it. Annotations are how a hand correction records its reason beside the value
18
- * it corrected, and refusing them would push that reason out of the file.
15
+ * about. There are two open doors, and both lead nowhere — nothing reads what
16
+ * stands behind them. Annotation: `note`, and any key ending in `_note`, may
17
+ * hold a string anywhere an object is expected. Annotations are how a hand
18
+ * correction records its reason beside the value it corrected, and refusing
19
+ * them would push that reason out of the file. Record: any key beginning with
20
+ * `x-` and at least one character after it may hold any JSON value in the same
21
+ * places (issue #70) — a project's own provenance, such as the gate results of
22
+ * a past build or the seeds a search rejected, kept beside the conditions that
23
+ * made the rig. The record test comes first, so `x-seed_note` is a record. The
24
+ * part-name maps (`meshes`, `regions`, `motion.blink.still`) are not objects
25
+ * in this sense: their keys are part names, and an `x-` key there is a part
26
+ * name like any other. No stage writes a vouched object whole into an output;
27
+ * each writer names its fields (`resolvedSampler` and its kin in
28
+ * `src/graphs.ts`), which is what keeps both doors read by nothing.
19
29
  *
20
30
  * ⛔ **The generation block is inline.** Configs written for the private
21
31
  * reference pointed at a character file and an art-job id elsewhere
@@ -224,11 +234,23 @@ const RETIRED: Record<string, Record<string, string>> = {
224
234
  art_job: 'named a job record outside this tree; put the costume words themselves in generation.costume and where they came from in generation.costume_note',
225
235
  identity_override: 'overrode the character file\'s identity; with the values inline, write the identity you want in generation.identity',
226
236
  lora_strength: 'overrode every LoRA\'s strength; with the values inline, set generation.loras[i].strength on each',
227
- seed0: 'recorded where a seed search started; the chosen seed is generation.seed, and the search belongs in generation.seed_note',
228
- seeds_tried: 'recorded the seeds a search tried; the chosen seed is generation.seed, and the search belongs in generation.seed_note',
237
+ seed0: 'recorded where a seed search started; the chosen seed is generation.seed, and the search belongs in generation.seed_note, or as a structured record in generation.x-seeds_tried',
238
+ seeds_tried: 'recorded the seeds a search tried; the chosen seed is generation.seed, and the search belongs in generation.seed_note, or as a structured record in generation.x-seeds_tried',
229
239
  },
230
240
  };
231
241
 
242
+ /**
243
+ * A project's own record: a key beginning `x-` with at least one character
244
+ * after it, holding any JSON value, read by nothing. `X-`, `x_` and a bare
245
+ * `x-` are not record names, so a typo of one is still refused by name.
246
+ */
247
+ function isRecordKey(key: string): boolean {
248
+ return key.length > 2 && key.startsWith('x-');
249
+ }
250
+
251
+ /** What every refusal of a key the schema does not know adds after its own sentence: the two doors. */
252
+ const DOORS = 'a project\'s own record goes under a key beginning "x-" (any JSON, read by nothing), a remark under note or <name>_note (a string)';
253
+
232
254
  // ---------------------------------------------------------------------------
233
255
  // the checker
234
256
  // ---------------------------------------------------------------------------
@@ -242,7 +264,7 @@ class Check {
242
264
  this.problems.push({ code, object, detail });
243
265
  }
244
266
 
245
- /** An object with exactly these keys (plus annotations); returns it, or null after refusing. */
267
+ /** An object with exactly these keys (plus records and annotations); returns it, or null after refusing. */
246
268
  object(path: string, v: Json, required: readonly string[], optional: readonly string[]): Record<string, Json> | null {
247
269
  if (typeof v !== 'object' || v === null || Array.isArray(v)) {
248
270
  this.fail('CONFIG_FIELD_TYPE', path, `is ${show(v)}; an object is required`);
@@ -252,13 +274,16 @@ class Check {
252
274
  const section = path.replace(/^config\.?/, '');
253
275
  for (const key of Object.keys(o)) {
254
276
  if (required.includes(key) || optional.includes(key)) continue;
277
+ if (isRecordKey(key)) continue;
255
278
  if (key === 'note' || key.endsWith('_note')) {
256
- if (typeof o[key] !== 'string') this.fail('CONFIG_FIELD_TYPE', `${path}.${key}`, `is ${show(o[key])}; an annotation is a string`);
279
+ if (typeof o[key] !== 'string') {
280
+ this.fail('CONFIG_FIELD_TYPE', `${path}.${key}`, `is ${show(o[key])}; an annotation is a string — a structured record goes under a key beginning "x-" (any JSON, read by nothing)`);
281
+ }
257
282
  continue;
258
283
  }
259
284
  const retired = RETIRED[section]?.[key];
260
285
  if (retired !== undefined) this.fail('CONFIG_KEY_RETIRED', `${path}.${key}`, retired);
261
- else this.fail('CONFIG_KEY_KNOWN', `${path}.${key}`, `is not a field here; known: ${[...required, ...optional].join(', ')}`);
286
+ else this.fail('CONFIG_KEY_KNOWN', `${path}.${key}`, `is not a field here; known: ${[...required, ...optional].join(', ')} — ${DOORS}`);
262
287
  }
263
288
  for (const key of required) if (!(key in o)) this.fail('CONFIG_FIELD_PRESENT', `${path}.${key}`, 'is absent and required');
264
289
  return o;
@@ -421,9 +446,10 @@ const TOP_KEYS = ['key', 'generation', 'seethrough', 'assemble', 'bones', 'meshe
421
446
  * vouched for (for `paint` that is everything but `key` and `generation`; for
422
447
  * `layers`, `assemble.plan`, `extend_below_crop`, `patches`, `bones`, `meshes`,
423
448
  * `regions` and `motion`; for `assemble`, the last four); a caller that needs
424
- * them uses {@link parseConfig}. Every other key is still refused by name, and
425
- * a retired one (`generation.character_file` and its kin) still with what
426
- * replaces it: a config is not allowed to be half-known at any stage.
449
+ * them uses {@link parseConfig}. Every other key that is not a record or an
450
+ * annotation is still refused by name, and a retired one
451
+ * (`generation.character_file` and its kin) still with what replaces it: a
452
+ * config is not allowed to be half-known at any stage.
427
453
  */
428
454
  export function parseEarlyConfig(raw: Json, door: 'paint'): PaintConfig;
429
455
  export function parseEarlyConfig(raw: Json, door: 'layers'): EarlyConfig;
package/src/graphs.ts CHANGED
@@ -28,7 +28,7 @@
28
28
  * The two heads are the reference's verbatim, including the safety terms of
29
29
  * the negative head (no nudity, no minors): they are a guard, not a cast.
30
30
  */
31
- import type { Control, Generation, Lora } from './config.ts';
31
+ import type { Control, Generation, Lora, Sampler } from './config.ts';
32
32
  import type { Problem } from './errors.ts';
33
33
  import { SKELETONS } from './skeleton.ts';
34
34
 
@@ -106,6 +106,16 @@ export function resolvedControl(c: Control): Required<Control> {
106
106
  return { skeleton: c.skeleton, strength: c.strength, end_percent: c.end_percent, model: c.model ?? CONTROL_MODEL };
107
107
  }
108
108
 
109
+ /**
110
+ * The sampler as the graph uses it: its four fields by name, in this order.
111
+ * The config's own object also carries whatever annotations and `x-` records
112
+ * its author wrote, and the loader vouches for the four fields only, so a
113
+ * writer that copied the object whole would carry those into an output.
114
+ */
115
+ export function resolvedSampler(s: Sampler): Sampler {
116
+ return { steps: s.steps, cfg: s.cfg, sampler: s.sampler, scheduler: s.scheduler };
117
+ }
118
+
109
119
  /**
110
120
  * The painting graph: checkpoint -> LoRAs in order -> the two text encoders ->
111
121
  * (ControlNetApplyAdvanced over the uploaded skeleton) -> KSampler -> VAE