nx_concept_embedding.nx source
↩ module page · 377 lines · 12294 B
1// nx_concept_embedding.nx -- typed concept manifest + storage contract.
2//
3// Closes the STORAGE half of the image-ingestion loop:
4//
5// nx_image_feature_extract N images -> N feature vectors
6// nx_image_feature_centroid N vectors -> centroid (concept signature)
7// nx_concept_embedding centroid + provenance -> manifest
8// <nishi-library writer> manifest -> content-addressed storage
9// nx_cosine_similarity new image vs stored concept -> match
10// nx_render_target_match rendered vs target -> verdict
11//
12// USE CASES (all three of user 2026-05-15's verticals):
13//
14// RETAIL VIRTUAL TRY-ON:
15// name = "lingerie_velvet_red_xs"
16// centroid = mean(features across 5 photos of the product)
17// source_hashes = sha256 of each product photo
18// n_examples = 5
19// Later prompt: "model wearing [concept:lingerie_velvet_red_xs]"
20// -> blend stored centroid into prompt embedding (IP-adapter path)
21// -> render with the actual product visualized
22//
23// VIDEO-GAME ASSET CATALOG:
24// name = "weapon_runic_greatsword"
25// centroid = mean(concept art frames)
26// -> in-engine asset gen conditioned on the concept
27//
28// STORY-CHARACTER REFERENCE:
29// name = "char_elara"
30// centroid = mean(prior renders the user approved)
31// -> future story renders condition on this concept for identity
32// stability across the arc
33//
34// MANIFEST STRUCTURE (canonical-bytes-serializable, content-addressable):
35// schema_version v1 forever; new version = new struct kind
36// name_buf + name_len concept's human-readable handle
37// centroid NX_IMAGE_FEATURE_LEN nx_int (Q10) cells
38// n_examples how many images contributed to the centroid
39// fidelity_q10 mean fidelity across the N source extracts
40// source_hash_buf 32 * n_examples bytes (sha256 per source img)
41// created_iso_buf optional ISO-8601 timestamp (caller-set)
42//
43// CONTENT-ADDRESSING: caller computes sha256 of canonical_bytes(manifest)
44// via the existing nx_sha256.nx primitive. The hash IS the concept's
45// global identifier in nishi-library's CAS store. Two callers
46// producing the same centroid from the same source images get the
47// same manifest hash -- substrate-level idempotency without
48// coordination.
49//
50// genealogy_id: gal_2022_textual_inversion + ye_2023_ip_adapter +
51// salton_1971_smart + ipfs_benet_2014 +
52// nix_dolstra_2004
53// lineage_id: concept_embedding_manifest_q10
54
55// nx_safety_envelope:
56// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
57// sil_target: SIL1
58// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
59// verdict: NOT_YET_EVALUATED
60
61import "nx_syscalls.nx"
62import "nx_tier.nx"
63import "nx_image_feature_extract.nx"
64import "nx_cosine_similarity.nx"
65
66const NX_CE_SCHEMA_VERSION: nx_int = 1
67const NX_CE_HASH_BYTES: nx_int = 32 // sha256 = 32 bytes
68const NX_CE_Q: nx_int = 1024
69
70// Maximum name length. Beyond this, the manifest is truncated (caller
71// must check name_len == requested len).
72const NX_CE_NAME_MAX: nx_int = 128
73
74// Match-quality bands when comparing two concept embeddings via
75// centroid cosine. Reuses the NX_STRSIM_*-style semantics but
76// concept-specific.
77const NX_CE_BAND_DISTINCT: nx_int = 0
78const NX_CE_BAND_LOOSE: nx_int = 1
79const NX_CE_BAND_RELATED: nx_int = 2
80const NX_CE_BAND_NEAR_DUPLICATE: nx_int = 3
81const NX_CE_BAND_IDENTICAL: nx_int = 4
82const NX_CE_N_BANDS: nx_int = 5
83
84struct ConceptEmbedding {
85 schema_version: nx_int,
86 name_buf: *u8,
87 name_len: nx_int,
88 centroid: *nx_int, // NX_IMAGE_FEATURE_LEN cells
89 n_examples: nx_int,
90 fidelity_q10: nx_int,
91 source_hash_buf: *u8, // 32 * n_examples bytes
92 created_iso_buf: *u8, // optional; may be empty
93 created_iso_len: nx_int,
94}
95
96// ===== Builder ========================================================
97//
98// Caller allocates centroid + source_hash_buf (and optionally
99// created_iso_buf) and passes them in. The substrate doesn't own the
100// storage of the input buffers; it owns the struct cells.
101
102func nx_concept_embedding_build(
103 name_buf: *u8, name_len: nx_int,
104 centroid: *nx_int,
105 n_examples: nx_int,
106 fidelity_q10: nx_int,
107 source_hash_buf: *u8,
108 created_iso_buf: *u8, created_iso_len: nx_int,
109 out: *ConceptEmbedding
110) -> nx_int {
111 if name_len < 0 { return 1 }
112 if name_len > NX_CE_NAME_MAX { return 2 }
113 if n_examples < 0 { return 3 }
114 out.schema_version = NX_CE_SCHEMA_VERSION
115 out.name_buf = name_buf
116 out.name_len = name_len
117 out.centroid = centroid
118 out.n_examples = n_examples
119 out.fidelity_q10 = fidelity_q10
120 out.source_hash_buf = source_hash_buf
121 out.created_iso_buf = created_iso_buf
122 out.created_iso_len = created_iso_len
123 return 0
124}
125
126// ===== Canonical bytes ===============================================
127//
128// Emits a byte sequence the caller will sha256 to get the manifest's
129// content-address. Format:
130//
131// "ce.v1\n"
132// "name=" name_buf "\n"
133// "n_examples=" n_examples "\n"
134// "fidelity_q10=" fidelity_q10 "\n"
135// "centroid=" comma-separated nx_int values "\n"
136// "source_hashes=" hex-of-each-32-byte-hash separated by comma "\n"
137// "created=" created_iso_buf "\n"
138//
139// Caller supplies a buffer + max length; substrate fills it and
140// returns the byte count written, or -1 if the buffer is too small.
141
142func _ce_write_byte(buf: *u8, max: nx_int, pos: nx_int, b: u8) -> nx_int {
143 if pos >= max { return -1 }
144 buf[pos] = b
145 return pos + 1
146}
147
148func _ce_write_bytes(buf: *u8, max: nx_int, pos: nx_int, src: *u8, n: nx_int) -> nx_int {
149 var p: nx_int = pos
150 var i: nx_int = 0
151 while i < n {
152 if p >= max { return -1 }
153 buf[p] = src[i]
154 p = p + 1
155 i = i + 1
156 }
157 return p
158}
159
160// Write a decimal integer. Returns new pos or -1 on overflow.
161func _ce_write_int(buf: *u8, max: nx_int, pos: nx_int, v: nx_int) -> nx_int {
162 if v == 0 {
163 return _ce_write_byte(buf, max, pos, 48)
164 }
165 var n: nx_int = v
166 var neg: nx_int = 0
167 if n < 0 {
168 neg = 1
169 n = -n
170 }
171 // Count digits.
172 var digits: nx_int = 0
173 var tmp: nx_int = n
174 while tmp > 0 {
175 digits = digits + 1
176 tmp = tmp / 10
177 }
178 let total: nx_int = digits + neg
179 if pos + total > max { return -1 }
180 var p: nx_int = pos
181 if neg == 1 {
182 buf[p] = 45 // '-'
183 p = p + 1
184 }
185 // Write digits right-to-left into the slot.
186 var k: nx_int = p + digits - 1
187 while n > 0 {
188 let d: nx_int = n - (n / 10) * 10
189 buf[k] = 48 + d // ascii '0' + digit
190 n = n / 10
191 k = k - 1
192 }
193 return p + digits
194}
195
196// Hex-encode a byte to two ASCII chars.
197func _ce_hex_nibble(b: u8) -> u8 {
198 if b < 10 { return 48 + b }
199 return 87 + b // 'a' + (b - 10)
200}
201
202func _ce_write_hex(buf: *u8, max: nx_int, pos: nx_int, src: *u8, n: nx_int) -> nx_int {
203 var p: nx_int = pos
204 var i: nx_int = 0
205 while i < n {
206 if p + 2 > max { return -1 }
207 let hi: u8 = src[i] / 16
208 let lo: u8 = src[i] - hi * 16
209 buf[p] = _ce_hex_nibble(hi)
210 buf[p + 1] = _ce_hex_nibble(lo)
211 p = p + 2
212 i = i + 1
213 }
214 return p
215}
216
217func nx_concept_embedding_canonical_bytes(
218 m: *ConceptEmbedding,
219 out: *u8, max: nx_int
220) -> nx_int {
221 var p: nx_int = 0
222
223 // Header
224 let lit_hdr: *u8 = sys_mmap(8)
225 lit_hdr[0] = 99 // c
226 lit_hdr[1] = 101 // e
227 lit_hdr[2] = 46 // .
228 lit_hdr[3] = 118 // v
229 lit_hdr[4] = 49 // 1
230 lit_hdr[5] = 10 // \n
231 p = _ce_write_bytes(out, max, p, lit_hdr, 6)
232 if p < 0 { return -1 }
233
234 // name=<name>\n
235 let lit_name: *u8 = sys_mmap(8)
236 lit_name[0] = 110 // n
237 lit_name[1] = 97 // a
238 lit_name[2] = 109 // m
239 lit_name[3] = 101 // e
240 lit_name[4] = 61 // =
241 p = _ce_write_bytes(out, max, p, lit_name, 5)
242 if p < 0 { return -1 }
243 p = _ce_write_bytes(out, max, p, m.name_buf, m.name_len)
244 if p < 0 { return -1 }
245 p = _ce_write_byte(out, max, p, 10)
246 if p < 0 { return -1 }
247
248 // n_examples=<n>\n
249 let lit_n: *u8 = sys_mmap(16)
250 lit_n[0] = 110 // n
251 lit_n[1] = 95 // _
252 lit_n[2] = 101 // e
253 lit_n[3] = 120 // x
254 lit_n[4] = 97 // a
255 lit_n[5] = 109 // m
256 lit_n[6] = 112 // p
257 lit_n[7] = 108 // l
258 lit_n[8] = 101 // e
259 lit_n[9] = 115 // s
260 lit_n[10] = 61 // =
261 p = _ce_write_bytes(out, max, p, lit_n, 11)
262 if p < 0 { return -1 }
263 p = _ce_write_int(out, max, p, m.n_examples)
264 if p < 0 { return -1 }
265 p = _ce_write_byte(out, max, p, 10)
266 if p < 0 { return -1 }
267
268 // centroid=<v0>,<v1>,...,<v15>\n
269 let lit_c: *u8 = sys_mmap(16)
270 lit_c[0] = 99 // c
271 lit_c[1] = 101 // e
272 lit_c[2] = 110 // n
273 lit_c[3] = 116 // t
274 lit_c[4] = 114 // r
275 lit_c[5] = 111 // o
276 lit_c[6] = 105 // i
277 lit_c[7] = 100 // d
278 lit_c[8] = 61 // =
279 p = _ce_write_bytes(out, max, p, lit_c, 9)
280 if p < 0 { return -1 }
281 var i: nx_int = 0
282 while i < NX_IMAGE_FEATURE_LEN {
283 if i > 0 {
284 p = _ce_write_byte(out, max, p, 44) // ,
285 if p < 0 { return -1 }
286 }
287 p = _ce_write_int(out, max, p, m.centroid[i])
288 if p < 0 { return -1 }
289 i = i + 1
290 }
291 p = _ce_write_byte(out, max, p, 10)
292 if p < 0 { return -1 }
293
294 // source_hashes=<hex>,<hex>...\n
295 let lit_sh: *u8 = sys_mmap(16)
296 lit_sh[0] = 115 // s
297 lit_sh[1] = 111 // o
298 lit_sh[2] = 117 // u
299 lit_sh[3] = 114 // r
300 lit_sh[4] = 99 // c
301 lit_sh[5] = 101 // e
302 lit_sh[6] = 115 // s
303 lit_sh[7] = 61 // =
304 p = _ce_write_bytes(out, max, p, lit_sh, 8)
305 if p < 0 { return -1 }
306 var j: nx_int = 0
307 while j < m.n_examples {
308 if j > 0 {
309 p = _ce_write_byte(out, max, p, 44)
310 if p < 0 { return -1 }
311 }
312 let off: nx_int = j * NX_CE_HASH_BYTES
313 // m.source_hash_buf is *u8; we need pointer to start + offset.
314 // Use a small loop that advances byte-by-byte (substrate doesn't
315 // support pointer arithmetic on *u8 in const-expr; we write hex
316 // by reading via index).
317 var bb: nx_int = 0
318 while bb < NX_CE_HASH_BYTES {
319 if p + 2 > max { return -1 }
320 let b: u8 = m.source_hash_buf[off + bb]
321 let hi: u8 = b / 16
322 let lo: u8 = b - hi * 16
323 out[p] = _ce_hex_nibble(hi)
324 out[p + 1] = _ce_hex_nibble(lo)
325 p = p + 2
326 bb = bb + 1
327 }
328 j = j + 1
329 }
330 p = _ce_write_byte(out, max, p, 10)
331 if p < 0 { return -1 }
332
333 // fidelity=<n>\n
334 let lit_f: *u8 = sys_mmap(16)
335 lit_f[0] = 102 // f
336 lit_f[1] = 105 // i
337 lit_f[2] = 100 // d
338 lit_f[3] = 101 // e
339 lit_f[4] = 108 // l
340 lit_f[5] = 105 // i
341 lit_f[6] = 116 // t
342 lit_f[7] = 121 // y
343 lit_f[8] = 61 // =
344 p = _ce_write_bytes(out, max, p, lit_f, 9)
345 if p < 0 { return -1 }
346 p = _ce_write_int(out, max, p, m.fidelity_q10)
347 if p < 0 { return -1 }
348 p = _ce_write_byte(out, max, p, 10)
349 if p < 0 { return -1 }
350
351 return p
352}
353
354// ===== Compare two concepts via centroid cosine ======================
355//
356// Returns signed Q10 cosine + sealed-enum band classifier.
357
358func nx_concept_embedding_compare(a: *ConceptEmbedding, b: *ConceptEmbedding) -> nx_int {
359 return nx_cosine_similarity(
360 a.centroid, NX_IMAGE_FEATURE_LEN,
361 b.centroid, NX_IMAGE_FEATURE_LEN
362 )
363}
364
365func nx_concept_embedding_classify(similarity_q10: nx_int) -> nx_int {
366 if similarity_q10 < 256 { return NX_CE_BAND_DISTINCT }
367 if similarity_q10 < 512 { return NX_CE_BAND_LOOSE }
368 if similarity_q10 < 768 { return NX_CE_BAND_RELATED }
369 if similarity_q10 < 921 { return NX_CE_BAND_NEAR_DUPLICATE }
370 return NX_CE_BAND_IDENTICAL
371}
372
373func nx_concept_embedding_band_is_valid(band: nx_int) -> nx_int {
374 if band < 0 { return 0 }
375 if band >= NX_CE_N_BANDS { return 0 }
376 return 1
377}