@rig-c/parts 0.0.0-stage → 1.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE.md +134 -0
- package/README.md +433 -2
- package/bin/rig-parts.cjs +43 -0
- package/cli.ts +1240 -0
- package/docs/AUTHORING.md +2387 -0
- package/docs/MIGRATION.md +150 -0
- package/docs/STABILITY.md +262 -0
- package/package.json +70 -4
- package/skills/rig-parts/SKILL.md +167 -0
- package/src/apng.ts +381 -0
- package/src/assemble.ts +2259 -0
- package/src/autoamplitude.ts +194 -0
- package/src/autoenvelope.ts +404 -0
- package/src/automesh.ts +585 -0
- package/src/automotion.ts +447 -0
- package/src/autoreplay.ts +516 -0
- package/src/build.ts +1240 -0
- package/src/check.ts +2942 -0
- package/src/comfy/client.ts +245 -0
- package/src/comfy/index.ts +19 -0
- package/src/comfy/painting.ts +136 -0
- package/src/comfy/seethrough.ts +171 -0
- package/src/config.ts +2283 -0
- package/src/contour.ts +1901 -0
- package/src/coords.ts +26 -0
- package/src/diagnostics.ts +198 -0
- package/src/errors.ts +39 -0
- package/src/gif.ts +213 -0
- package/src/graphs.ts +302 -0
- package/src/headbox.ts +173 -0
- package/src/heat.ts +313 -0
- package/src/inputs.ts +100 -0
- package/src/instruments.ts +1145 -0
- package/src/keypoints.ts +296 -0
- package/src/layers.ts +598 -0
- package/src/localweights.ts +217 -0
- package/src/mesh.ts +362 -0
- package/src/motion.ts +311 -0
- package/src/palette.ts +208 -0
- package/src/parts.ts +410 -0
- package/src/propose.ts +2540 -0
- package/src/pyfmt.ts +135 -0
- package/src/raster/blur.ts +80 -0
- package/src/raster/components.ts +165 -0
- package/src/raster/composite.ts +81 -0
- package/src/raster/index.ts +9 -0
- package/src/raster/morph.ts +116 -0
- package/src/raster/png.ts +82 -0
- package/src/raster/poly.ts +166 -0
- package/src/raster/resize.ts +301 -0
- package/src/raster/types.ts +89 -0
- package/src/raster/warp.ts +121 -0
- package/src/requirements.ts +1240 -0
- package/src/rig.ts +1440 -0
- package/src/round.ts +62 -0
- package/src/scene.ts +1414 -0
- package/src/sheet.ts +128 -0
- package/src/skeleton.ts +400 -0
- package/src/structure.ts +858 -0
- package/src/tags.ts +121 -0
- package/src/weights.ts +124 -0
- package/workflows/seethrough.json +68 -0
package/src/tags.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* See-through's layer vocabulary, as the tags a layer arrives named by.
|
|
3
|
+
*
|
|
4
|
+
* ⭐ These are the **v3** tags — the vocabulary `pipeline.unet.get_tag_version()`
|
|
5
|
+
* reports on the `seethroughv0.0.2_layerdiff3d` weights, read out of the
|
|
6
|
+
* upstream code: 13 body tags and 11 head tags, 24 in all. The older v2
|
|
7
|
+
* vocabulary has 19 tags and a single `hair` tag; it is not what the published
|
|
8
|
+
* v0.0.2 weights emit, and a layer named by it is refused as unknown rather
|
|
9
|
+
* than mapped, because a mapping would be a guess about which v3 tag it meant.
|
|
10
|
+
*
|
|
11
|
+
* Six tags are split left/right by upstream (`_tag_lr_split`, which needs the
|
|
12
|
+
* tag's mask to be two separate blobs): `handwear`, `eyewhite`, `irides`,
|
|
13
|
+
* `eyelash`, `eyebrow`, `ears`. A split layer is named `<tag>-r` / `<tag>-l`.
|
|
14
|
+
* **[observed]** on wrapper output: `-r` is the half at the smaller image x —
|
|
15
|
+
* the character's right as they face the viewer — and the wrapper emits both
|
|
16
|
+
* halves even when one is a few pixels (a 6x6 `handwear-l` beside a 275x352
|
|
17
|
+
* `handwear-r`), so "both halves present" says nothing about the split having
|
|
18
|
+
* found two real blobs.
|
|
19
|
+
*
|
|
20
|
+
* `head` is in the v3 list but is not emitted as a layer: upstream's README
|
|
21
|
+
* says the pipeline "stratifies the character into up to 23 semantic layers"
|
|
22
|
+
* (24 tags less `head`, whose pass arrives as its parts), and **[observed]**
|
|
23
|
+
* no wrapper output read so far carries one. It stays in the vocabulary
|
|
24
|
+
* because it is upstream's word.
|
|
25
|
+
*
|
|
26
|
+
* Nothing downstream classifies a part by its NAME. The role of a part — what
|
|
27
|
+
* a proposer may do with it — is read from the tag it came from, which is why
|
|
28
|
+
* `parts.json` records `from` for every part.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
export const BODY_TAGS = [
|
|
32
|
+
'front hair',
|
|
33
|
+
'back hair',
|
|
34
|
+
'head',
|
|
35
|
+
'neck',
|
|
36
|
+
'neckwear',
|
|
37
|
+
'topwear',
|
|
38
|
+
'handwear',
|
|
39
|
+
'bottomwear',
|
|
40
|
+
'legwear',
|
|
41
|
+
'footwear',
|
|
42
|
+
'tail',
|
|
43
|
+
'wings',
|
|
44
|
+
'objects',
|
|
45
|
+
] as const;
|
|
46
|
+
|
|
47
|
+
export const HEAD_TAGS = [
|
|
48
|
+
'headwear',
|
|
49
|
+
'face',
|
|
50
|
+
'irides',
|
|
51
|
+
'eyebrow',
|
|
52
|
+
'eyewhite',
|
|
53
|
+
'eyelash',
|
|
54
|
+
'eyewear',
|
|
55
|
+
'ears',
|
|
56
|
+
'earwear',
|
|
57
|
+
'nose',
|
|
58
|
+
'mouth',
|
|
59
|
+
] as const;
|
|
60
|
+
|
|
61
|
+
export type BaseTag = (typeof BODY_TAGS)[number] | (typeof HEAD_TAGS)[number];
|
|
62
|
+
|
|
63
|
+
/** The tags upstream splits into `-r` / `-l` halves. */
|
|
64
|
+
export const SPLIT_TAGS = ['handwear', 'eyewhite', 'irides', 'eyelash', 'eyebrow', 'ears'] as const;
|
|
65
|
+
|
|
66
|
+
export type SplitTag = (typeof SPLIT_TAGS)[number];
|
|
67
|
+
export type Side = 'r' | 'l';
|
|
68
|
+
|
|
69
|
+
/** Every v3 tag, in the order upstream lists them. */
|
|
70
|
+
export const V3_TAGS: readonly BaseTag[] = [...BODY_TAGS, ...HEAD_TAGS];
|
|
71
|
+
|
|
72
|
+
export interface TagReading {
|
|
73
|
+
/** The name as the layer carries it: `eyebrow-l`, `front hair`. */
|
|
74
|
+
name: string;
|
|
75
|
+
base: BaseTag;
|
|
76
|
+
/** `null` for an unsplit tag or an unsplit form of a split tag. */
|
|
77
|
+
side: Side | null;
|
|
78
|
+
group: 'body' | 'head';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Read a layer name as a v3 tag, or `null` when it is not one.
|
|
83
|
+
*
|
|
84
|
+
* Accepted: any of the 24 base tags, and `<tag>-r` / `<tag>-l` for the six
|
|
85
|
+
* split tags. A suffix on a tag upstream never splits (`topwear-l`) is not a
|
|
86
|
+
* tag — upstream cannot have produced it, so accepting it would be accepting a
|
|
87
|
+
* layer somebody renamed.
|
|
88
|
+
*/
|
|
89
|
+
export function readTag(name: string): TagReading | null {
|
|
90
|
+
const split = /^(.+)-([rl])$/.exec(name);
|
|
91
|
+
if (split !== null && (SPLIT_TAGS as readonly string[]).includes(split[1])) {
|
|
92
|
+
const base = split[1] as SplitTag;
|
|
93
|
+
return { name, base, side: split[2] as Side, group: groupOf(base) };
|
|
94
|
+
}
|
|
95
|
+
if ((V3_TAGS as readonly string[]).includes(name)) {
|
|
96
|
+
const base = name as BaseTag;
|
|
97
|
+
return { name, base, side: null, group: groupOf(base) };
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function groupOf(tag: BaseTag): 'body' | 'head' {
|
|
103
|
+
return (HEAD_TAGS as readonly string[]).includes(tag) ? 'head' : 'body';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Every name `readTag` accepts, sorted — what a refusal quotes as "required". */
|
|
107
|
+
export function acceptedTagNames(): string[] {
|
|
108
|
+
const names = new Set<string>(V3_TAGS);
|
|
109
|
+
for (const t of SPLIT_TAGS) {
|
|
110
|
+
names.add(`${t}-r`);
|
|
111
|
+
names.add(`${t}-l`);
|
|
112
|
+
}
|
|
113
|
+
return [...names].sort();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The mirror of a split layer name (`eyebrow-l` <-> `eyebrow-r`), or `null` for an unsided one. */
|
|
117
|
+
export function pairOf(name: string): string | null {
|
|
118
|
+
const t = readTag(name);
|
|
119
|
+
if (t === null || t.side === null) return null;
|
|
120
|
+
return `${t.base}-${t.side === 'r' ? 'l' : 'r'}`;
|
|
121
|
+
}
|
package/src/weights.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vertex weights by distance to bone SEGMENTS.
|
|
3
|
+
*
|
|
4
|
+
* Each mesh names its candidate segments in the config — a chain (every link,
|
|
5
|
+
* origin to the next link, the last to the chain's tip), a single bone (origin
|
|
6
|
+
* to its tip), or an explicit `[bone, [x0, y0], [x1, y1]]`. That list is the
|
|
7
|
+
* one authored decision about a layer: which bones may pull it. The weights
|
|
8
|
+
* then follow from the geometry and nothing else:
|
|
9
|
+
*
|
|
10
|
+
* w = 1 / (d + r)^e d = distance from the vertex to the segment
|
|
11
|
+
*
|
|
12
|
+
* per candidate, with `e` the mesh's declared `exponent` (issue #161) and 2 —
|
|
13
|
+
* {@link DISTANCE_EXPONENT}, the rule as it always was — where none is
|
|
14
|
+
* declared; a bone named by two segments keeps the larger; the top
|
|
15
|
+
* {@link MAX_INFLUENCES} are kept, normalised, any below {@link MIN_WEIGHT}
|
|
16
|
+
* dropped, the rest normalised again, rounded to 5 decimals, and the last
|
|
17
|
+
* weight written as `1 - sum(others)` so every vertex sums to exactly 1 after
|
|
18
|
+
* rounding. Ties in the sort keep the order the bones first appeared among the
|
|
19
|
+
* segments (Python's `sorted` is stable, and so is `Array.prototype.sort`).
|
|
20
|
+
*
|
|
21
|
+
* Distances are the reference's arithmetic, operation for operation — the
|
|
22
|
+
* projection `t = clip(((p - a) . ab) / |ab|^2, 0, 1)` and `sqrt(dx^2 + dy^2)`,
|
|
23
|
+
* not `Math.hypot`, whose last bit differs — because a weight rounded at the
|
|
24
|
+
* fifth decimal is where a one-ulp difference becomes a visible one.
|
|
25
|
+
*
|
|
26
|
+
* ⛔ **No invented segment.** The reference gave a single bone with no tip and
|
|
27
|
+
* no chain child the segment `(x, y) -> (x, y + 1)`: one pixel, straight down,
|
|
28
|
+
* a direction nothing in the config says. Here that is `RIG_SEGMENT_DEFINED`,
|
|
29
|
+
* naming the bone and the two fields that would define it. A chain link whose
|
|
30
|
+
* next point is its own origin is refused the same way (`RIG_CHAIN_POINTS`).
|
|
31
|
+
*/
|
|
32
|
+
import type { Point } from './config.ts';
|
|
33
|
+
|
|
34
|
+
/** At most this many bones pull one vertex — Spine's usual budget and the reference's `[:4]`. */
|
|
35
|
+
export const MAX_INFLUENCES = 4;
|
|
36
|
+
|
|
37
|
+
/** A normalised weight under this is dropped before the final normalisation — the reference's `>= 0.03`. */
|
|
38
|
+
export const MIN_WEIGHT = 0.03;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The exponent of the distance rule where a mesh declares none: `1/(d + r)²`,
|
|
42
|
+
* the reference's rule. At this value the weight is computed as
|
|
43
|
+
* `(d + r) * (d + r)`, the reference's operations, and never through
|
|
44
|
+
* `Math.pow`, which the language does not promise to round as one multiply
|
|
45
|
+
* does — so a mesh that declares nothing, or declares 2, writes the bytes it
|
|
46
|
+
* always wrote.
|
|
47
|
+
*/
|
|
48
|
+
export const DISTANCE_EXPONENT = 2;
|
|
49
|
+
|
|
50
|
+
export interface Segment {
|
|
51
|
+
bone: string;
|
|
52
|
+
a: Point;
|
|
53
|
+
b: Point;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Distance from `p` to the segment `a -> b`; a zero-length segment is its point. */
|
|
57
|
+
export function segmentDistance(p: Point, a: Point, b: Point): number {
|
|
58
|
+
const abx = b[0] - a[0];
|
|
59
|
+
const aby = b[1] - a[1];
|
|
60
|
+
const len2 = abx * abx + aby * aby;
|
|
61
|
+
let t = 0;
|
|
62
|
+
if (len2 > 0) {
|
|
63
|
+
t = ((p[0] - a[0]) * abx + (p[1] - a[1]) * aby) / len2;
|
|
64
|
+
t = t < 0 ? 0 : t > 1 ? 1 : t;
|
|
65
|
+
}
|
|
66
|
+
const dx = p[0] - (a[0] + t * abx);
|
|
67
|
+
const dy = p[1] - (a[1] + t * aby);
|
|
68
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface Influence {
|
|
72
|
+
bone: string;
|
|
73
|
+
/** Unrounded, normalised over the kept influences. */
|
|
74
|
+
weight: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The cap and the floor `influences()` applies. The lattice and the contour
|
|
79
|
+
* mode always run at {@link DEFAULT_LIMITS} (the reference's `[:4]` and
|
|
80
|
+
* `>= 0.03`); the automatic mode (`src/automesh.ts`, issue #126) passes the
|
|
81
|
+
* author's numbers instead, because rig-c's reduction is handed explicit
|
|
82
|
+
* influence limits on every weighted call and the 0.03 floor is not one the
|
|
83
|
+
* author wrote (P19). The arithmetic is the same either way, so with the
|
|
84
|
+
* defaults every byte the two older modes write is unchanged.
|
|
85
|
+
*/
|
|
86
|
+
export interface InfluenceLimits {
|
|
87
|
+
maxInfluences: number;
|
|
88
|
+
minWeight: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const DEFAULT_LIMITS: InfluenceLimits = { maxInfluences: MAX_INFLUENCES, minWeight: MIN_WEIGHT };
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The influences on one vertex at `p` (rig pixels), before rounding, under
|
|
95
|
+
* `w = 1/(d + r)^exponent`. The rig stage passes every mesh's exponent
|
|
96
|
+
* explicitly; the default is for readers of today's rule (the tools, the
|
|
97
|
+
* selftest's older controls).
|
|
98
|
+
*/
|
|
99
|
+
export function influences(p: Point, segments: readonly Segment[], r: number, limits: InfluenceLimits = DEFAULT_LIMITS, exponent: number = DISTANCE_EXPONENT): Influence[] {
|
|
100
|
+
const agg = new Map<string, number>();
|
|
101
|
+
for (const s of segments) {
|
|
102
|
+
const d = segmentDistance(p, s.a, s.b);
|
|
103
|
+
// 2 is the reference's multiply, bit for bit (DISTANCE_EXPONENT); any other declared exponent is Math.pow.
|
|
104
|
+
const w = 1.0 / (exponent === DISTANCE_EXPONENT ? (d + r) * (d + r) : Math.pow(d + r, exponent));
|
|
105
|
+
const was = agg.get(s.bone);
|
|
106
|
+
agg.set(s.bone, was === undefined ? w : Math.max(was, w));
|
|
107
|
+
}
|
|
108
|
+
return capAndFloor([...agg.entries()], limits);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The cap and the floor, on raw per-bone values in first-appearance order: sorted (stable), the top
|
|
113
|
+
* `maxInfluences` normalised, those under `minWeight` dropped, the rest normalised again. The distance rule
|
|
114
|
+
* above and bone heat (`src/heat.ts`, issue #161) both end here, so the two rules share one cap and one floor.
|
|
115
|
+
*/
|
|
116
|
+
export function capAndFloor(raw: ReadonlyArray<readonly [string, number]>, limits: InfluenceLimits): Influence[] {
|
|
117
|
+
const top = [...raw].sort((x, y) => y[1] - x[1]).slice(0, limits.maxInfluences);
|
|
118
|
+
let s = 0;
|
|
119
|
+
for (const [, v] of top) s += v;
|
|
120
|
+
const kept = top.map(([bone, v]) => ({ bone, weight: v / s })).filter((e) => e.weight >= limits.minWeight);
|
|
121
|
+
let s2 = 0;
|
|
122
|
+
for (const e of kept) s2 += e.weight;
|
|
123
|
+
return kept.map((e) => ({ bone: e.bone, weight: e.weight / s2 }));
|
|
124
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "API-format graph for jtydhr88/ComfyUI-See-through (its node classes and inputs as a box running the wrapper lists them on /object_info; the adapter checks every node and input against the box's own /object_info before it submits). Every %NAME% string is filled by src/graphs.ts fillSeeThrough, and a placeholder nothing fills is refused before submission. The LoadImage 'upload' widget of the UI format is left out: it is not an input /object_info lists. Top-level keys starting with _ are annotations and are dropped before /prompt. SeeThrough_SavePSD writes one PNG per layer and a <prefix>_<timestamp>_layers.json manifest into the box's output directory, and records that manifest's file name in output/seethrough_psd_info.log; the two SaveImage nodes are previews and are what /history lists.",
|
|
3
|
+
"1": {
|
|
4
|
+
"class_type": "LoadImage",
|
|
5
|
+
"inputs": { "image": "%IMAGE%" }
|
|
6
|
+
},
|
|
7
|
+
"2": {
|
|
8
|
+
"class_type": "SeeThrough_LoadLayerDiffModel",
|
|
9
|
+
"inputs": {
|
|
10
|
+
"model": "layerdifforg/seethroughv0.0.2_layerdiff3d",
|
|
11
|
+
"vae_ckpt": "",
|
|
12
|
+
"unet_ckpt": "",
|
|
13
|
+
"quant_mode": "%QUANT%",
|
|
14
|
+
"cache_tag_embeds": true,
|
|
15
|
+
"group_offload": "%OFFLOAD%",
|
|
16
|
+
"auto_download": true
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"3": {
|
|
20
|
+
"class_type": "SeeThrough_GenerateLayers",
|
|
21
|
+
"inputs": {
|
|
22
|
+
"image": ["1", 0],
|
|
23
|
+
"layerdiff_model": ["2", 0],
|
|
24
|
+
"seed": "%SEED%",
|
|
25
|
+
"resolution": "%RESOLUTION%",
|
|
26
|
+
"num_inference_steps": "%STEPS%"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"4": {
|
|
30
|
+
"class_type": "SeeThrough_LoadDepthModel",
|
|
31
|
+
"inputs": {
|
|
32
|
+
"model": "layerdifforg/seethroughv0.0.1_marigold",
|
|
33
|
+
"quant_mode": "%QUANT%",
|
|
34
|
+
"cache_tag_embeds": true,
|
|
35
|
+
"group_offload": "%OFFLOAD%",
|
|
36
|
+
"auto_download": true
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"5": {
|
|
40
|
+
"class_type": "SeeThrough_GenerateDepth",
|
|
41
|
+
"inputs": {
|
|
42
|
+
"layers": ["3", 0],
|
|
43
|
+
"depth_model": ["4", 0],
|
|
44
|
+
"seed": "%SEED%",
|
|
45
|
+
"resolution_depth": -1
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"6": {
|
|
49
|
+
"class_type": "SeeThrough_PostProcess",
|
|
50
|
+
"inputs": {
|
|
51
|
+
"layers_depth": ["5", 0],
|
|
52
|
+
"tblr_split": true,
|
|
53
|
+
"use_lama": "%LAMA%"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"7": {
|
|
57
|
+
"class_type": "SeeThrough_SavePSD",
|
|
58
|
+
"inputs": { "parts": ["6", 0], "filename_prefix": "%PREFIX%" }
|
|
59
|
+
},
|
|
60
|
+
"8": {
|
|
61
|
+
"class_type": "SaveImage",
|
|
62
|
+
"inputs": { "images": ["3", 1], "filename_prefix": "%PREFIX%_preview_layers" }
|
|
63
|
+
},
|
|
64
|
+
"9": {
|
|
65
|
+
"class_type": "SaveImage",
|
|
66
|
+
"inputs": { "images": ["6", 1], "filename_prefix": "%PREFIX%_preview_parts" }
|
|
67
|
+
}
|
|
68
|
+
}
|