code wiki / _hdl_build / nx_companion_wardrobe.nx
nx_companion_wardrobe.nx source
↩ module page · 272 lines · 16349 B
1// nx_companion_wardrobe.nx -- the CompanionWardrobe object: the occlusion-aware photo-prompt compiler (the Z-Image
2// "painter not compositor" fix). Resolves a scene into per-zone clothing-OR-bare with a POSITIVE occlusion assertion
3// (fabric obscures when clothed; bare only when the scene asks) so clothed shots stay clothed + nude only on request
4// = the controllable wardrobe layer. Composes the anatomy-neutral identity from CompanionIdentity. Extracted from
5// nx_gen_orchestrator (monolith -> objects); behaviour-preserving (body byte-identical except private cw_* helpers).
6// ⚠this is where ER-8 negatives + ControlNet pose/structure control will compose. Object model: companion_arch.md.
7// license_tier: ORIGINAL
8import "nx_syscalls.nx"
9import "nx_companion_identity.nx"
10import "nx_gen_conf_lib.nx" // G13 (2026-08-30): the lane's one two-path conf reader
11
12func cw_cat(dst: *u8, off: i64, s: *u8) -> i64 { var o: i64=off; var i: i64=0; while s[i]!=(0 as u8){dst[o]=s[i]; o=o+1; i=i+1} return o }
13func cw_catb(dst: *u8, off: i64, src: *u8, n: i64) -> i64 { var o: i64=off; var i: i64=0; while i<n {dst[o]=src[i]; o=o+1; i=i+1} return o }
14func cw_contains_ci(hay: *u8, hlen: i64, needle: *u8) -> i64 {
15 var nl: i64 = 0; while needle[nl]!=(0 as u8) { nl=nl+1 }
16 if nl==0 { return 0 }
17 var i: i64 = 0
18 while i + nl <= hlen {
19 var j: i64 = 0; var ok: i64 = 1
20 while j < nl {
21 var a: i64 = hay[i+j]&0xff; var b2: i64 = needle[j]&0xff
22 if a>=65 { if a<=90 { a=a+32 } }
23 if b2>=65 { if b2<=90 { b2=b2+32 } }
24 if a!=b2 { ok=0; j=nl } else { j=j+1 }
25 }
26 if ok==1 { return 1 }
27 i=i+1
28 }
29 return 0
30}
31
32// ---- G13 (2026-08-30) PROMPT WEIGHTING AS DATA --------------------------------------------------------------------
33// The served engine ALREADY honours A1111 attention syntax on the user turn of the Z-Image chat template, read from
34// elder-sdcpp 2026-08-30: conditioner.hpp routes conditioner_params.text through parse_prompt_attention (util.cpp)
35// via LLMEmbedder::tokenize, then multiplies every token's hidden state by its weight with a mean-preserving rescale
36// (the block after llm->compute). So "(text:1.20)" is a LIVE control here and 1.00 is the identity -- not the no-op
37// that looks like a control which the sdapi extra-args path in the same server once was.
38// knowledge/gen_prompt_weight.conf is the SSOT and its contract PREDATES this code (read before writing, 2026-08-30):
39// integers in PERMIL, in order of appearance -- [0] MIN and [1] MAX of the PERMITTED ENVELOPE (100 2000 = 0.1..2.0)
40// and, ADDITIVELY (rule 19; absent from the file today), [2] trigger-slot, [3] camera-slot and [4] identity-anchor
41// weights, each 1000 = unchanged. A weight outside the envelope is REFUSED by named code, never clamped -- the conf's
42// own reasoning: a clamp turns a typo like (skin:13) into a silently different render that reads as a model defect.
43// Two callers, two consequences: a USER group in a studio prompt refuses the request with the group's weight named
44// (cw_weight_admit); a DATA slot weight out of envelope falls back to the BARE slot -- the pre-G13 baseline render,
45// never a mutated one (cw_weight_slot). Absent, unreadable or short conf -> the documented envelope and 1000 for
46// every slot, so the composed prompt is BYTE-IDENTICAL to the pre-G13 prompt until an operator edits data: feature
47// OFF by default, and that off/on byte difference is what nx_gen_promptweight_gate proves. Wildcard expansion (the
48// other half of the G13 done-rule) is NOT in this verb.
49const CW_W_IDENT: i64 = 1000
50const CW_W_MIN_DEFAULT: i64 = 100
51const CW_W_MAX_DEFAULT: i64 = 2000
52const CW_W_SLOTS: i64 = 5
53const CW_W_BYTES: i64 = 40
54const CW_W_MIN: i64 = 0
55const CW_W_MAX: i64 = 1
56const CW_W_TRIGGER: i64 = 2
57const CW_W_CAMERA: i64 = 3
58const CW_W_IDENTITY: i64 = 4
59const CW_W_REFUSED: i64 = 0 - 1
60const CW_W_PERMIL: i64 = 1000
61const CW_CH_LPAREN: i64 = 40
62const CW_CH_RPAREN: i64 = 41
63const CW_CH_COLON: i64 = 58
64const CW_CH_DOT: i64 = 46
65const CW_CH_D0: i64 = 48
66const CW_CH_D9: i64 = 57
67const CW_CH_BACKSLASH: i64 = 92
68const CW_B10: i64 = 10
69const CW_B100: i64 = 100
70const CW_W_CONF_REL: *u8 = "knowledge/gen_prompt_weight.conf"
71const CW_W_CONF_ABS: *u8 = "/volume1/homes/elderwesto/nishihost/knowledge/gen_prompt_weight.conf"
72func cw_putnum(dst: *u8, off: i64, v: i64) -> i64 {
73 let t: *u8 = sys_mmap(28)
74 var m: i64 = v
75 var k: i64 = 0
76 if m == 0 { t[0] = CW_CH_D0 as u8; k = 1 }
77 while m > 0 { t[k] = (CW_CH_D0 + (m % CW_B10)) as u8; m = m / CW_B10; k = k + 1 }
78 var o: i64 = off
79 var q: i64 = k - 1
80 while q >= 0 { dst[o] = t[q]; o = o + 1; q = q - 1 }
81 return o
82}
83// parse conf BYTES into out[0..CW_W_SLOTS): the documented envelope and identity slots first; returns how many ints the
84// data set. Fewer than two ints, or an inverted envelope (min > max), is unreadable data and keeps the documented envelope.
85func cw_weights_parse(b: *u8, n: i64, out: *i64) -> i64 {
86 out[CW_W_MIN] = CW_W_MIN_DEFAULT
87 out[CW_W_MAX] = CW_W_MAX_DEFAULT
88 var k: i64 = CW_W_TRIGGER
89 while k < CW_W_SLOTS { out[k] = CW_W_IDENT; k = k + 1 }
90 if n <= 0 { return 0 }
91 let got: i64 = gc_ints(b, n, out, CW_W_SLOTS)
92 if got < 2 { out[CW_W_MIN] = CW_W_MIN_DEFAULT; out[CW_W_MAX] = CW_W_MAX_DEFAULT }
93 if out[CW_W_MIN] > out[CW_W_MAX] { out[CW_W_MIN] = CW_W_MIN_DEFAULT; out[CW_W_MAX] = CW_W_MAX_DEFAULT }
94 return got
95}
96// the two-path read (daemon cwd, then the nishihost tree) through the lane's ONE conf reader
97func cw_weights_load(out: *i64) -> i64 {
98 let szp: *i64 = sys_mmap(16) as *i64
99 let b: i64 = gc_read2(CW_W_CONF_REL, CW_W_CONF_ABS, szp)
100 if b == 0 { return cw_weights_parse(0 as *u8, 0, out) }
101 return cw_weights_parse(b as *u8, szp[0], out)
102}
103// 1 when permil w sits inside the envelope env[CW_W_MIN]..env[CW_W_MAX], else 0
104func cw_weight_admitted(w: i64, env: *i64) -> i64 { if w < env[CW_W_MIN] { return 0 } if w > env[CW_W_MAX] { return 0 } return 1 }
105// emit text at dst+off carrying weight w (PERMIL): empty text emits nothing; 1000 emits the bare text (byte-identical
106// to no weighting); outside the envelope REFUSES (CW_W_REFUSED, nothing written); else "(text:d.ddd)" -- the
107// engine's own syntax, three decimals because the unit is permil
108func cw_prompt_weight(dst: *u8, off: i64, text: *u8, w: i64, env: *i64) -> i64 {
109 if text[0] == (0 as u8) { return off }
110 if cw_weight_admitted(w, env) == 0 { return CW_W_REFUSED }
111 if w == CW_W_IDENT { return cw_cat(dst, off, text) }
112 var o: i64 = off
113 dst[o] = CW_CH_LPAREN as u8; o = o + 1
114 o = cw_cat(dst, o, text)
115 dst[o] = CW_CH_COLON as u8; o = o + 1
116 o = cw_putnum(dst, o, w / CW_W_PERMIL)
117 dst[o] = CW_CH_DOT as u8; o = o + 1
118 let frac: i64 = w % CW_W_PERMIL
119 dst[o] = (CW_CH_D0 + (frac / CW_B100)) as u8; o = o + 1
120 dst[o] = (CW_CH_D0 + ((frac / CW_B10) % CW_B10)) as u8; o = o + 1
121 dst[o] = (CW_CH_D0 + (frac % CW_B10)) as u8; o = o + 1
122 dst[o] = CW_CH_RPAREN as u8; o = o + 1
123 return o
124}
125// a DATA slot: weighted when admitted; the bare slot (the pre-G13 baseline) when the conf row is out of envelope
126func cw_weight_slot(dst: *u8, off: i64, text: *u8, wts: *i64, slot: i64) -> i64 {
127 let r: i64 = cw_prompt_weight(dst, off, text, wts[slot], wts)
128 if r < 0 { return cw_cat(dst, off, text) }
129 return r
130}
131// parse a decimal weight at s[i0..n) into PERMIL ("1.3" -> 1300, "0.95" -> 950, "13" -> 13000, ".5" -> 500);
132// endp[0] = the index after the number; CW_W_REFUSED when no digit is present. Fractional digits past the third are
133// consumed, not counted.
134func cw_weight_permil_at(s: *u8, n: i64, i0: i64, endp: *i64) -> i64 {
135 var i: i64 = i0
136 var v: i64 = 0
137 var any: i64 = 0
138 var scan: i64 = 1
139 while scan == 1 { if i >= n { scan = 0 } else { let c: i64 = s[i] & 0xff; if c >= CW_CH_D0 { if c <= CW_CH_D9 { v = v * CW_B10 + (c - CW_CH_D0); any = 1; i = i + 1 } else { scan = 0 } } else { scan = 0 } } }
140 v = v * CW_W_PERMIL
141 if i < n { if (s[i] & 0xff) == CW_CH_DOT {
142 i = i + 1
143 var scale: i64 = CW_B100
144 var fscan: i64 = 1
145 while fscan == 1 { if i >= n { fscan = 0 } else { let c: i64 = s[i] & 0xff; if c >= CW_CH_D0 { if c <= CW_CH_D9 { v = v + (c - CW_CH_D0) * scale; scale = scale / CW_B10; any = 1; i = i + 1 } else { fscan = 0 } } else { fscan = 0 } } }
146 } }
147 endp[0] = i
148 if any == 0 { return CW_W_REFUSED }
149 return v
150}
151// ADMISSION of the weights a USER wrote into free text: every "(...:w)" group's w must sit inside the envelope.
152// Returns the number of weighted groups seen; CW_W_REFUSED with badat[0] = index of the offending ':' and badw[0] =
153// its permil on the first violation. A backslash-escaped paren is literal, as in the engine's own parser; a ':'
154// outside any open group, or one not followed by a number, is prose and is ignored.
155func cw_weight_admit(s: *u8, n: i64, env: *i64, badat: *i64, badw: *i64) -> i64 {
156 var depth: i64 = 0
157 var groups: i64 = 0
158 var i: i64 = 0
159 let endp: *i64 = sys_mmap(16) as *i64
160 while i < n {
161 let c: i64 = s[i] & 0xff
162 var step: i64 = 1
163 if c == CW_CH_BACKSLASH { step = 2 }
164 if c == CW_CH_LPAREN { depth = depth + 1 }
165 if c == CW_CH_RPAREN { if depth > 0 { depth = depth - 1 } }
166 if c == CW_CH_COLON { if depth > 0 {
167 let w: i64 = cw_weight_permil_at(s, n, i + 1, endp)
168 if w >= 0 {
169 groups = groups + 1
170 if cw_weight_admitted(w, env) == 0 { badat[0] = i; badw[0] = w; return CW_W_REFUSED }
171 step = endp[0] - i
172 }
173 } }
174 i = i + step
175 }
176 return groups
177}
178
179func go_build_photo_prompt(scene: *u8, slen: i64, expr: *u8, out: *u8) -> i64 {
180 let ident: *u8 = sys_mmap(2048); go_load_appearance(ident, 2048)
181 let wts: *i64 = sys_mmap(CW_W_BYTES) as *i64
182 cw_weights_load(wts)
183 var o: i64 = cw_weight_slot(out, 0, ident, wts, CW_W_IDENTITY)
184 // --- exposure state from the scene (nudity intent) ---
185 var tex: i64 = 0; var hex: i64 = 0
186 if cw_contains_ci(scene, slen, "naked" as *u8)==1 { tex=1; hex=1 }
187 if cw_contains_ci(scene, slen, "nude" as *u8)==1 { tex=1; hex=1 }
188 if cw_contains_ci(scene, slen, "shower" as *u8)==1 { tex=1; hex=1 }
189 if cw_contains_ci(scene, slen, "in the bath" as *u8)==1 { tex=1; hex=1 }
190 if cw_contains_ci(scene, slen, "topless" as *u8)==1 { tex=1 }
191 if cw_contains_ci(scene, slen, "tits" as *u8)==1 { tex=1 }
192 if cw_contains_ci(scene, slen, "boobs out" as *u8)==1 { tex=1 }
193 if cw_contains_ci(scene, slen, "bare breast" as *u8)==1 { tex=1 }
194 if cw_contains_ci(scene, slen, "flash" as *u8)==1 { tex=1 }
195 if cw_contains_ci(scene, slen, "bottomless" as *u8)==1 { hex=1 }
196 if cw_contains_ci(scene, slen, "pussy" as *u8)==1 { hex=1 }
197 if cw_contains_ci(scene, slen, "no panties" as *u8)==1 { hex=1 }
198 // --- did the user name a garment? ---
199 var cloth: i64 = 0
200 if cw_contains_ci(scene, slen, "bikini" as *u8)==1 { cloth=1 }
201 if cw_contains_ci(scene, slen, "swimsuit" as *u8)==1 { cloth=1 }
202 if cw_contains_ci(scene, slen, "dress" as *u8)==1 { cloth=1 }
203 if cw_contains_ci(scene, slen, "lingerie" as *u8)==1 { cloth=1 }
204 if cw_contains_ci(scene, slen, "shirt" as *u8)==1 { cloth=1 }
205 if cw_contains_ci(scene, slen, "blouse" as *u8)==1 { cloth=1 }
206 if cw_contains_ci(scene, slen, "top" as *u8)==1 { cloth=1 }
207 if cw_contains_ci(scene, slen, "bra" as *u8)==1 { cloth=1 }
208 if cw_contains_ci(scene, slen, "sweater" as *u8)==1 { cloth=1 }
209 if cw_contains_ci(scene, slen, "hoodie" as *u8)==1 { cloth=1 }
210 if cw_contains_ci(scene, slen, "tank" as *u8)==1 { cloth=1 }
211 if cw_contains_ci(scene, slen, "shorts" as *u8)==1 { cloth=1 }
212 if cw_contains_ci(scene, slen, "skirt" as *u8)==1 { cloth=1 }
213 if cw_contains_ci(scene, slen, "jeans" as *u8)==1 { cloth=1 }
214 if cw_contains_ci(scene, slen, "pants" as *u8)==1 { cloth=1 }
215 if cw_contains_ci(scene, slen, "leggings" as *u8)==1 { cloth=1 }
216 if cw_contains_ci(scene, slen, "nightgown" as *u8)==1 { cloth=1 }
217 if cw_contains_ci(scene, slen, "robe" as *u8)==1 { cloth=1 }
218 if cw_contains_ci(scene, slen, "uniform" as *u8)==1 { cloth=1 }
219 if cw_contains_ci(scene, slen, "outfit" as *u8)==1 { cloth=1 }
220 if cw_contains_ci(scene, slen, "wearing" as *u8)==1 { cloth=1 }
221 if cw_contains_ci(scene, slen, "sundress" as *u8)==1 { cloth=1 }
222 // --- emit the scene (setting + whatever garment the user named) ---
223 o = cw_cat(out, o, ", " as *u8)
224 o = cw_catb(out, o, scene, slen)
225 // --- default outfit when nothing named + not exposed (setting-aware; never leave a zone undefined) ---
226 if tex==0 { if cloth==0 {
227 if cw_contains_ci(scene, slen, "beach" as *u8)==1 { o = cw_cat(out, o, ", wearing a cute string bikini" as *u8) }
228 else { if cw_contains_ci(scene, slen, "pool" as *u8)==1 { o = cw_cat(out, o, ", wearing a cute bikini" as *u8) }
229 else { if cw_contains_ci(scene, slen, "gym" as *u8)==1 { o = cw_cat(out, o, ", wearing a fitted sports bra and leggings" as *u8) }
230 else { if cw_contains_ci(scene, slen, "bed" as *u8)==1 { o = cw_cat(out, o, ", wearing a soft silky camisole and shorts" as *u8) }
231 else { o = cw_cat(out, o, ", wearing a cute fitted summer sundress" as *u8) } } } }
232 } }
233 // --- TORSO occlusion resolution (positive assertion; the load-bearing fix) ---
234 if tex==1 { o = cw_cat(out, o, ". Her breasts are completely bare and fully exposed, pale ghost areolas with small pert pale pink nipples, her nipples are clearly visible" as *u8) }
235 else { o = cw_cat(out, o, ". The opaque fabric completely covers and obscures her breasts, a solid smooth surface with zero nipple visibility through the material" as *u8) }
236 // --- HIPS (only when exposed; otherwise the outfit/default covers it) ---
237 if hex==1 { o = cw_cat(out, o, ". She is nude below the waist, bare and exposed" as *u8) }
238 // --- expression (from her mood) + quality ---
239 o = cw_cat(out, o, expr)
240 // QUALITY TAIL -- data-driven via go_load_quality (nx_companion_identity.nx), tunable in
241 // elara_quality.txt with NO rebuild. The old inline booru tail
242 // (", photorealistic, natural lighting, sharp focus, high detail, 8k") is RETIRED: in the post-repair
243 // window of the 568,944-row beauty corpus "photorealistic", "8k" and "high detail" occur ZERO times --
244 // the mature system had abandoned booru-style quality tags in favour of concrete photographic language.
245 let qual: *u8 = sys_mmap(1024); go_load_quality(qual, 1024)
246 o = cw_cat(out, o, ", " as *u8); o = cw_cat(out, o, qual)
247 out[o]=0 as u8
248 return o
249}
250// THE ONE PHOTO-PROMPT COMPOSER (2026-08-27). The chat photo path AND nx_elara_prompt_probe both call THIS, so
251// the composition order exists in exactly one place -- the probe used to re-implement the order itself, a
252// duplicate ruler that could stay GREEN while the daemon drifted. June slot order, RESTORED: realism TRIGGER
253// (the LoRA's activation words, elara_trigger.txt) -> CAMERA line (elara_shots.txt, rotated by seed) ->
254// occlusion-aware identity+scene BODY (go_build_photo_prompt, which ends with the quality tail). Z-Image is a
255// DiT with RoPE theta=256 -- the earliest tokens dominate the conditioning, which is why the trigger and the
256// camera lead and the identity anchor still lands inside the first 77 tokens. Returns the composed length;
257// out is NUL-terminated. A blank elara_trigger.txt yields camera-first (the pre-2026-08-27 order), by contract.
258func go_compose_photo_prompt(scene: *u8, slen: i64, expr: *u8, seed: i64, out: *u8) -> i64 {
259 let body: *u8 = sys_mmap(16384)
260 var bo: i64 = go_build_photo_prompt(scene, slen, expr, body)
261 if bo < 0 { bo = 0 }
262 let trig: *u8 = sys_mmap(512); go_load_trigger(trig, 512)
263 let shot: *u8 = sys_mmap(256); go_pick_shot(shot, 256, seed)
264 let wts: *i64 = sys_mmap(CW_W_BYTES) as *i64
265 cw_weights_load(wts)
266 var o: i64 = 0
267 if trig[0]!=(0 as u8) { o = cw_weight_slot(out, 0, trig, wts, CW_W_TRIGGER); o = cw_cat(out, o, ", " as *u8) }
268 if shot[0]!=(0 as u8) { o = cw_weight_slot(out, o, shot, wts, CW_W_CAMERA); o = cw_cat(out, o, ". " as *u8) }
269 o = cw_cat(out, o, body)
270 out[o]=0 as u8
271 return o
272}