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}