code wiki / (root) / nx_imgsearch.nx

nx_imgsearch.nx source

↩ module page · 834 lines · 43119 B

1// nx_imgsearch.nx -- THE REVERSE-IMAGE SERVICE ORGAN: the MCP/API face of the multi-tier engine. 2// 3// Every verb emits STRICT JSON on stdout, because the callers are agents and workflows, not humans. 4// That drives three rules the whole file obeys: 5// * HONEST ABSENT. "not found" is `{"present":false}` with a reason, never an empty list that a 6// caller can mistake for "not searched" and never the nearest thing dressed up as a match. 7// * SELF-DESCRIBING. `status` returns the live tier roster, every threshold, and the MEASURED 8// per-class robustness with its gaps named, so an agent can decide whether this engine can answer 9// its question before asking. A tool that cannot state its own limits cannot be trusted with one. 10// * EVERY RESULT CARRIES ITS PROVENANCE -- which tier matched, at what distance, at which 11// orientation, and whether that tier was CONFIDENT (inside its own threshold) or merely nearest. 12// 13// INDEX FORMAT (versioned, fixed-stride, mmap-first -- the scale decision): 14// header 32B : magic "NXIMGIDX" | u32 version | u32 stride | i64 count | i64 cidblob_off 15// record 96B : i64 dhash | u8 visdesc[80] | u32 cid_off | u32 cid_len 16// then : the cid string blob 17// Fixed stride means O(1) random access and no parsing, so the index is opened with sys_map_file -- 18// the same read-only file-backed map that moved the text shard off the RAM ceiling onto the page 19// cache. The 80-dimension descriptor is QUANTISED TO u8 AT INGEST (its dimensions are already on a 20// 0..256 scale, so this costs at most one unit on a saturated bin): 96 bytes per image on disk 21// against 648 bytes unquantised, which is the difference between a 10M-image index fitting a NAS 22// volume and not. Same integer-quantisation-at-ingest discipline the text engine rides. 23// 24// HONEST CEILING, reported in the JSON rather than hidden: `query` currently EXPANDS the mmap'd 25// records into the engine's i64 descriptor arrays, so serving RAM is ~648 bytes/image even though 26// storage is 96. The named fix is a u8-native similarity tier that computes L1 straight off the 27// mapped bytes, making serving disk-bound like the text side. Until that lands the ceiling is real 28// and this tool prints it. 29// license_tier: ORIGINAL 30import "nx_imgsearch_engine.nx" 31import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc) 32import "nx_imgcorpus.nx" 33import "nx_image_gray.nx" 34import "nx_dir.nx" 35const IS_MAGIC_1024: i64 = 1024 36const IS_MAGIC_4096: i64 = 4096 37const IS_MAGIC_99999: i64 = 99999 38 39const IS_MAGIC0: i64 = 78 // 'N' 40const IS_VERSION: i64 = 2 // v2 adds the LOCAL keypoint pack per record (occlusion-robust tier) 41// record: [0..8) dhash | [8..88) visdesc u8[80] | [88..92) cid_off u32 | [92..96) cid_len u32 42// | [96..1256) local keypoint pack = 145 x i64 (FAST+BRIEF, the crop/letterbox tier) 43const IS_STRIDE: i64 = 1256 44const IS_LOCAL_OFF: i64 = 96 // byte offset of the local pack within a record 45const IS_LOCAL_N: i64 = 145 // i64 slots in the local pack (kp_pack_dim) 46const IS_HDR: i64 = 32 47const IS_VDIM: i64 = 80 48const IS_MAXTOPK: i64 = 32 49const IS_RAM_PER_IMG: i64 = 1816 // i64 descriptor footprint per image once expanded for serving 50// WORKING RESOLUTION CAP. Full-resolution photos (multi-megapixel) make the per-pixel tiers -- the 51// LOCAL FAST keypoint scan especially -- exceed the edge's 15s synchronous budget on the NAS CPU. 52// Every descriptor is computed at a bounded resolution instead: dHash/aHash already downscale to 9x8 53// internally, and running the edge-orientation + keypoint tiers at a canonical scale is standard 54// practice (it also bounds work independent of input size = the real scale lever for ingest). Applied 55// identically at index and query time so descriptors stay comparable. Below the cap = untouched, so 56// the synthetic bench (64x64) is unaffected. 57const IS_WORKDIM: i64 = 512 58 59func s_puts(p: *u8) -> i64 { var n: i64 = 0; while p[n] != (0 as u8) { n = n + 1 } sys_write(1, p, n); return 0 } 60func s_putb(p: *u8, n: i64) -> i64 { if n > 0 { sys_write(1, p, n) } return 0 } 61// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 62// per call and never freed it. At page granularity that is 4096B leaked PER CALL -- the 63// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff. A BENCH is the worst home for 64// it: its purpose is millions of iterations. nxi_* is MSB-first and allocates NOTHING. 65func s_num(v: i64) -> i64 { nxi_out(v); return 0 } 66func s_strlen(p: *u8) -> i64 { var n: i64 = 0; while p[n] != (0 as u8) { n = n + 1 } return n } 67func s_streq(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } if b[i] != (0 as u8) { return 0 } return 1 } 68func s_atoi(p: *u8) -> i64 { var v: i64 = 0; var i: i64 = 0; while p[i] != (0 as u8) { if p[i] >= (48 as u8) { if p[i] <= (57 as u8) { v = v * 10 + ((p[i] - (48 as u8)) as i64) } } i = i + 1 } return v } 69 70// JSON string body: escape backslash, quote, and control bytes. Callers are parsers, so a raw 71// quote inside a crawled URL must never be able to break the document. 72func s_jstr(p: *u8, n: i64) -> i64 { 73 var i: i64 = 0 74 while i < n { 75 let c: i64 = p[i] as i64 76 if c == 34 { s_puts("\\\"" as *u8) } 77 else { if c == 92 { s_puts("\\\\" as *u8) } 78 else { if c < 32 { s_puts(" " as *u8) } 79 else { sys_write(1, ((p as i64) + i) as *u8, 1) } } } 80 i = i + 1 81 } 82 return 0 83} 84func s_jkey(k: *u8) -> i64 { s_puts("\"" as *u8); s_puts(k); s_puts("\":" as *u8); return 0 } 85 86// ---- little-endian binary helpers ---- 87func s_put_i64(b: *u8, off: i64, v: i64) -> i64 { 88 var i: i64 = 0 89 while i < 8 { b[off + i] = ((v >> (i * 8)) & 255) as u8; i = i + 1 } 90 return 0 91} 92func s_get_i64(b: *u8, off: i64) -> i64 { 93 var v: i64 = 0 94 var i: i64 = 7 95 while i >= 0 { v = (v << 8) | (b[off + i] as i64); i = i - 1 } 96 return v 97} 98func s_put_u32(b: *u8, off: i64, v: i64) -> i64 { 99 var i: i64 = 0 100 while i < 4 { b[off + i] = ((v >> (i * 8)) & 255) as u8; i = i + 1 } 101 return 0 102} 103func s_get_u32(b: *u8, off: i64) -> i64 { 104 var v: i64 = 0 105 var i: i64 = 3 106 while i >= 0 { v = (v << 8) | (b[off + i] as i64); i = i - 1 } 107 return v 108} 109 110// the engine configuration this service ships. ONE place, so status/query/index cannot drift apart. 111func s_build_engine(cap: i64) -> *nx_imgengine { 112 let e: *nx_imgengine = nx_imgengine_new(cap) 113 nx_imgengine_add_tier(e, nx_imgtier_new(NX_IT_KIND_COPY, NX_IT_COPY_THRESH, 1000)) 114 nx_imgengine_add_tier(e, nx_imgtier_new(NX_IT_KIND_ORIENT, NX_IT_COPY_THRESH, 1000)) 115 nx_imgengine_add_tier(e, nx_imgtier_new(NX_IT_KIND_LOCAL, NX_IT_LOCAL_THRESH, 900)) 116 nx_imgengine_add_tier(e, nx_imgtier_new(NX_IT_KIND_SIMILAR, NX_IT_SIMILAR_THRESH, 800)) 117 return e 118} 119 120func s_klass_name(k: i64) -> *u8 { 121 if k == NX_IT_CLASS_IDENTITY { return "identity" as *u8 } 122 return "similarity" as *u8 123} 124 125func s_emit_tiers(e: *nx_imgengine) -> i64 { 126 s_jkey("tiers" as *u8); s_puts("[" as *u8) 127 var i: i64 = 0 128 while i < nx_imgengine_ntiers(e) { 129 let t: *nx_imgtier = nx_imgengine_tier(e, i) 130 if i > 0 { s_puts("," as *u8) } 131 s_puts("{" as *u8) 132 s_jkey("name" as *u8); s_puts("\"" as *u8); s_puts(nx_imgtier_name(t)); s_puts("\"," as *u8) 133 s_jkey("class" as *u8); s_puts("\"" as *u8); s_puts(s_klass_name(nx_imgtier_klass(t))); s_puts("\"," as *u8) 134 s_jkey("threshold" as *u8); s_num(nx_imgtier_threshold(t)); s_puts("," as *u8) 135 s_jkey("index_dim" as *u8); s_num(nx_imgtier_idx_dim(t)); s_puts("," as *u8) 136 s_jkey("query_dim" as *u8); s_num(nx_imgtier_qry_dim(t)); s_puts("," as *u8) 137 s_jkey("weight" as *u8); s_num(nx_imgtier_weight(t)) 138 s_puts("}" as *u8) 139 i = i + 1 140 } 141 s_puts("]" as *u8) 142 return 0 143} 144 145// ---- verb: status ------------------------------------------------------------------------------ 146// Self-description INCLUDING the measured weaknesses. An agent must be able to learn that this 147// engine cannot yet answer a heavy-crop question without having to run the query and be misled. 148func s_verb_status() -> i64 { 149 let e: *nx_imgengine = s_build_engine(1) 150 s_puts("{" as *u8) 151 s_jkey("tool" as *u8); s_puts("\"nx_imgsearch\"," as *u8) 152 s_jkey("version" as *u8); s_num(IS_VERSION); s_puts("," as *u8) 153 s_jkey("verb" as *u8); s_puts("\"status\"," as *u8) 154 s_jkey("ok" as *u8); s_puts("true," as *u8) 155 s_jkey("engine" as *u8); s_puts("{" as *u8) 156 s_emit_tiers(e); s_puts("," as *u8) 157 s_jkey("fusion" as *u8); s_puts("\"reciprocal-rank-fusion, abstention-gated\"," as *u8) 158 s_jkey("rrf_k" as *u8); s_num(NX_IE_RRF_K); s_puts("," as *u8) 159 s_jkey("ranking_complexity" as *u8); s_puts("\"hash tiers: BK-tree sublinear candidate generation (exact within-threshold, ~6x fewer comparisons on clustered corpora); similarity tier: O(N) heap scan pending ANN graph (seq717)\"" as *u8) 160 s_puts("}," as *u8) 161 s_jkey("index_format" as *u8); s_puts("{" as *u8) 162 s_jkey("magic" as *u8); s_puts("\"NXIMGIDX\"," as *u8) 163 s_jkey("stride_bytes" as *u8); s_num(IS_STRIDE); s_puts("," as *u8) 164 s_jkey("open_mode" as *u8); s_puts("\"mmap (sys_map_file), page-cache resident\"," as *u8) 165 s_jkey("serving_ram_bytes_per_image" as *u8); s_num(IS_RAM_PER_IMG) 166 s_puts("}," as *u8) 167 // MEASURED robustness -- these are nx_imgbench numbers, not aspirations. 168 s_jkey("measured_robustness_permille" as *u8); s_puts("{" as *u8) 169 s_jkey("note" as *u8); s_puts("\"recall@1 vs 256 distractors; UPPER BOUND on real-web\"," as *u8) 170 s_jkey("identity" as *u8); s_puts("1000," as *u8) 171 s_jkey("rescale" as *u8); s_puts("1000," as *u8) 172 s_jkey("brightness_contrast" as *u8); s_puts("1000," as *u8) 173 s_jkey("noise_recompress" as *u8); s_puts("1000," as *u8) 174 s_jkey("watermark" as *u8); s_puts("1000," as *u8) 175 s_jkey("mirror_rotate" as *u8); s_puts("1000," as *u8) 176 s_jkey("crop_10" as *u8); s_puts("1000," as *u8) 177 s_jkey("crop_20" as *u8); s_puts("1000," as *u8) 178 s_jkey("crop_30" as *u8); s_puts("968," as *u8) 179 s_jkey("letterbox" as *u8); s_puts("1000," as *u8) 180 s_jkey("crop_rescale" as *u8); s_puts("906," as *u8) 181 s_jkey("overall" as *u8); s_puts("992" as *u8) 182 s_puts("}," as *u8) 183 s_jkey("classes_survived" as *u8); s_puts("17," as *u8) 184 s_jkey("classes_total" as *u8); s_puts("17," as *u8) 185 s_jkey("known_gaps" as *u8); s_puts("[" as *u8) 186 s_puts("{\"class\":\"crop+rescale\",\"permille\":906,\"needs\":\"CLOSED 2026-08-05 -- scale invariance ships on the QUERY side (index unchanged, no reindex): the query is described across a 5-step half-octave sweep and the tier takes the best, so a same-scale class cannot regress by construction. A SEARCHED scale must clear a higher inlier bar than the image own scale (multiple-comparisons correction) -- without it an out-of-corpus image claimed a false identity match.\"}," as *u8) 187 s_puts("{\"class\":\"semantic-lookalike\",\"permille\":-1,\"status\":\"UNMEASURED\",\"note\":\"-1 means NEVER MEASURED, not zero. No semantic class exists in the 17-transform ruler, so the previous literal 0 was a CLAIM, not a result. Order: (1) build a semantic ruler from labelled same-concept pairs, (2) measure the EXISTING descriptors as the honest baseline, (3) only then a quantised learned image-embedding tier (NX_IT_KIND_SEMANTIC slot reserved). WARNING nx_embedding.nx is the LLM token-ID lookup, NOT an image encoder.\"}," as *u8) 188 s_puts("{\"class\":\"avif-decode\",\"permille\":0,\"needs\":\"AVIF decoder (C5). WebP CLOSED 2026-08-05: VP8L lossless AND lossy VP8 keyframes both decode via the chokepoint (nx_vp8_kf, gate 8/8 oracle-anchored); julia corpus 120/120 indexed\"}" as *u8) 189 s_puts("]" as *u8) 190 s_puts("}\n" as *u8) 191 return 0 192} 193 194// ---- shared ingest: decode one image FILE and write its 4-tier record ---- 195// One record-writer, used by BOTH `index` (manifest-driven) and `walkdir` (directory-driven), so the 196// two ingest paths can never drift in what they store. Holds the tier objects + scratch buffers so a 197// caller allocates them once and reuses across an entire corpus. Returns the NEW cid-blob offset on 198// success, or -1 if the image was undecodable (caller counts it as skipped -- never fatal). 199struct SwIngest { 200 tcopy: i64, 201 tsim: i64, 202 tlocal: i64, 203 dh: i64, 204 vd: i64, 205 lp: i64, 206 wh: i64, 207} 208const SWINGEST_BYTES: i64 = 56 209 210// Nearest-neighbour downscale a gray image so its larger side is at most `maxdim`, preserving aspect. 211// Returns the same buffer if already within bounds (no copy); otherwise a fresh bounded buffer. 212// outwh receives the working dimensions. This is the single choke point that bounds ALL tier work. 213func s_downscale_max(gray: *u8, w: i64, h: i64, maxdim: i64, outwh: *i64) -> *u8 { 214 if w <= maxdim { if h <= maxdim { outwh[0] = w; outwh[1] = h; return gray } } 215 var nw: i64 = w 216 var nh: i64 = h 217 if w >= h { nw = maxdim; nh = h * maxdim / w } else { nh = maxdim; nw = w * maxdim / h } 218 if nw < 1 { nw = 1 } 219 if nh < 1 { nh = 1 } 220 let out: *u8 = sys_mmap(nw * nh) 221 var oy: i64 = 0 222 while oy < nh { 223 let sy: i64 = (oy * h) / nh 224 var ox: i64 = 0 225 while ox < nw { 226 let sx: i64 = (ox * w) / nw 227 out[oy * nw + ox] = gray[sy * w + sx] 228 ox = ox + 1 229 } 230 oy = oy + 1 231 } 232 outwh[0] = nw 233 outwh[1] = nh 234 return out 235} 236 237func s_ingest_new() -> *SwIngest { 238 let g: *SwIngest = sys_mmap(SWINGEST_BYTES) as *SwIngest 239 g.tcopy = nx_imgtier_new(NX_IT_KIND_COPY, NX_IT_COPY_THRESH, 1000) as i64 240 g.tsim = nx_imgtier_new(NX_IT_KIND_SIMILAR, NX_IT_SIMILAR_THRESH, 800) as i64 241 g.tlocal = nx_imgtier_new(NX_IT_KIND_LOCAL, NX_IT_LOCAL_THRESH, 900) as i64 242 g.dh = sys_mmap(8) as i64 243 g.vd = sys_mmap(8 * IS_VDIM) as i64 244 g.lp = sys_mmap(8 * IS_LOCAL_N) as i64 245 g.wh = sys_mmap(16) as i64 246 return g 247} 248 249// write image at `path` into record slot nrec; cid string appended to cidb at cpos. Returns new cpos, 250// or -1 if undecodable. gray/rgb decoding + all four descriptors are computed exactly as at query time. 251func s_ingest_file(g: *SwIngest, recs: *u8, nrec: i64, cidb: *u8, cpos: i64, cid: *u8, path: *u8) -> i64 { 252 let wh: *i64 = g.wh as *i64 253 let graw: *u8 = nx_img_to_gray(path, wh) 254 if graw == (0 as *u8) { return 0 - 1 } 255 let gray: *u8 = s_downscale_max(graw, wh[0], wh[1], IS_WORKDIM, wh) 256 let ro: i64 = nrec * IS_STRIDE 257 let tcopy: *nx_imgtier = g.tcopy as *nx_imgtier 258 let tsim: *nx_imgtier = g.tsim as *nx_imgtier 259 let tlocal: *nx_imgtier = g.tlocal as *nx_imgtier 260 let dh: *i64 = g.dh as *i64 261 let vd: *i64 = g.vd as *i64 262 let lp: *i64 = g.lp as *i64 263 nx_imgtier_describe_index(tcopy, gray, 0 as *u8, wh[0], wh[1], dh) 264 s_put_i64(recs, ro, dh[0]) 265 nx_imgtier_describe_index(tsim, gray, 0 as *u8, wh[0], wh[1], vd) 266 var d: i64 = 0 267 while d < IS_VDIM { 268 var q: i64 = vd[d] 269 if q < 0 { q = 0 } 270 if q > 255 { q = 255 } 271 recs[ro + 8 + d] = q as u8 272 d = d + 1 273 } 274 nx_imgtier_describe_index(tlocal, gray, 0 as *u8, wh[0], wh[1], lp) 275 var lq: i64 = 0 276 while lq < IS_LOCAL_N { s_put_i64(recs, ro + IS_LOCAL_OFF + lq*8, lp[lq]); lq = lq + 1 } 277 let cl: i64 = s_strlen(cid) 278 s_put_u32(recs, ro + 88, cpos) 279 s_put_u32(recs, ro + 92, cl) 280 var c: i64 = 0 281 while c < cl { cidb[cpos + c] = cid[c]; c = c + 1 } 282 return cpos + cl 283} 284 285// write the NXIMGIDX header + records + cid blob to outpath. Returns total bytes, or -1 on open fail. 286func s_write_index(outpath: *u8, recs: *u8, nrec: i64, cidb: *u8, cpos: i64) -> i64 { 287 let hdr: *u8 = sys_mmap(IS_HDR) 288 hdr[0] = 78 as u8; hdr[1] = 88 as u8; hdr[2] = 73 as u8; hdr[3] = 77 as u8 289 hdr[4] = 71 as u8; hdr[5] = 73 as u8; hdr[6] = 68 as u8; hdr[7] = 88 as u8 290 s_put_u32(hdr, 8, IS_VERSION) 291 s_put_u32(hdr, 12, IS_STRIDE) 292 s_put_i64(hdr, 16, nrec) 293 s_put_i64(hdr, 24, IS_HDR + nrec * IS_STRIDE) 294 let fd: i64 = sys_openat_wr(outpath, 0x1a4) 295 if fd < 0 { return 0 - 1 } 296 sys_write(fd, hdr, IS_HDR) 297 sys_write(fd, recs, nrec * IS_STRIDE) 298 sys_write(fd, cidb, cpos) 299 sys_close(fd) 300 return IS_HDR + nrec * IS_STRIDE + cpos 301} 302 303// ---- verb: index ------------------------------------------------------------------------------- 304// manifest rows: cid<TAB>image_path. Undecodable images are SKIPPED and COUNTED, never fatal -- one 305// bad file must not abort an ingest of millions. 306func s_verb_index(manifest: *u8, outpath: *u8) -> i64 { 307 let mbox: *i64 = sys_mmap(16) as *i64 308 let mbuf: *u8 = sys_read_file(manifest, mbox) 309 if mbuf == (0 as *u8) { 310 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"index\",\"ok\":false,\"error\":\"manifest unreadable\"}\n" as *u8) 311 sys_exit(1); return 1 312 } 313 let mn: i64 = mbox[0] 314 var nrows: i64 = 0 315 var i: i64 = 0 316 while i < mn { if mbuf[i] == (10 as u8) { nrows = nrows + 1 } i = i + 1 } 317 nrows = nrows + 1 318 319 let recs: *u8 = sys_mmap(IS_STRIDE * (nrows + 2)) 320 let cidb: *u8 = sys_mmap(IS_MAGIC_1024 * (nrows + 2)) 321 var nrec: i64 = 0 322 var cpos: i64 = 0 323 var skipped: i64 = 0 324 let ing: *SwIngest = s_ingest_new() 325 326 var ls: i64 = 0 327 i = 0 328 while i <= mn { 329 var eol: i64 = 0 330 if i == mn { eol = 1 } else { if mbuf[i] == (10 as u8) { eol = 1 } } 331 if eol == 1 { 332 if i > ls { 333 var tab: i64 = 0 - 1 334 var k: i64 = ls 335 while k < i { if mbuf[k] == (9 as u8) { if tab < 0 { tab = k } } k = k + 1 } 336 if tab > ls { 337 mbuf[tab] = 0 as u8 338 if i < mn { mbuf[i] = 0 as u8 } 339 let cid: *u8 = ((mbuf as i64) + ls) as *u8 340 let path: *u8 = ((mbuf as i64) + tab + 1) as *u8 341 let ncp: i64 = s_ingest_file(ing, recs, nrec, cidb, cpos, cid, path) 342 if ncp < 0 { skipped = skipped + 1 } else { cpos = ncp; nrec = nrec + 1 } 343 } 344 } 345 ls = i + 1 346 } 347 i = i + 1 348 } 349 350 if s_write_index(outpath, recs, nrec, cidb, cpos) < 0 { 351 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"index\",\"ok\":false,\"error\":\"cannot open output\"}\n" as *u8) 352 sys_exit(1); return 1 353 } 354 355 s_puts("{" as *u8) 356 s_jkey("tool" as *u8); s_puts("\"nx_imgsearch\"," as *u8) 357 s_jkey("verb" as *u8); s_puts("\"index\"," as *u8) 358 s_jkey("ok" as *u8); s_puts("true," as *u8) 359 s_jkey("indexed" as *u8); s_num(nrec); s_puts("," as *u8) 360 s_jkey("skipped_undecodable" as *u8); s_num(skipped); s_puts("," as *u8) 361 s_jkey("bytes" as *u8); s_num(IS_HDR + nrec * IS_STRIDE + cpos); s_puts("," as *u8) 362 s_jkey("bytes_per_image" as *u8); s_num(IS_STRIDE); s_puts("," as *u8) 363 s_jkey("path" as *u8); s_puts("\"" as *u8); s_jstr(outpath, s_strlen(outpath)); s_puts("\"" as *u8) 364 s_puts("}\n" as *u8) 365 return 0 366} 367 368// ---- verb: walkdir (ASYNC) + walkdirsync + walkstatus ------------------------------------------ 369// Point the engine at a REAL directory of images and build the index -- the bridge from a synthetic 370// bench to a production corpus. Walks one directory (nx_dir bounded getdents), indexes every .png / 371// .jpg / .jpeg, filename = cid. Undecodable images (incl. WebP/AVIF, a filed gap) are SKIPPED and 372// COUNTED, never fatal. BOUNDED by `max`, reported when capped. 373// 374// WHY ASYNC: a full-resolution PNG decode is expensive, and the sovereign edge drops any synchronous 375// backend response that takes over ~15s -- so a foreground walkdir over more than a couple of real 376// photos times out and the daemon reaps it. `walkdir` therefore SELF-DETACHES: it forks a child that 377// setsid()s into its own session (surviving the parent's return + the closed request pipe, the same 378// primitive nx_daemon/nx_hostctl use for NAS daemons), redirects its fds to a log, indexes in the 379// background with no time limit, and writes the result to `<index>.status` when done. The parent 380// returns STARTED immediately, well inside the edge budget. `walkstatus <index>` polls that marker. 381// `walkdirsync` runs it foreground (for small dirs / local tests). HONEST BOUND: the detached child 382// still inherits the executor's address-space RLIMIT, so a single run is bounded by memory, not time; 383// an unbounded 100k+ corpus is the hostctl-sub rung (filed) -- but async lifts the reachable count 384// from ~1 (foreground, time-bound) to as many as fit RLIMIT. 385const SW_WALK_DEFAULT: i64 = 400 386const SW_WALK_HARDCAP: i64 = 50000 387 388func s_path_join(root: *u8, name: *u8, out: *u8) -> i64 { 389 var o: i64 = 0 390 var i: i64 = 0 391 while root[i] != (0 as u8) { out[o] = root[i]; o = o + 1; i = i + 1 } 392 if o > 0 { if out[o-1] != (47 as u8) { out[o] = 47 as u8; o = o + 1 } } 393 i = 0 394 while name[i] != (0 as u8) { out[o] = name[i]; o = o + 1; i = i + 1 } 395 out[o] = 0 as u8 396 return o 397} 398 399func s_is_image_name(name: *u8, nl: i64) -> i64 { 400 if nx_dir_name_ends_with(name, nl, ".png" as *u8, 4) == 1 { return 1 } 401 if nx_dir_name_ends_with(name, nl, ".jpg" as *u8, 4) == 1 { return 1 } 402 if nx_dir_name_ends_with(name, nl, ".jpeg" as *u8, 5) == 1 { return 1 } 403 if nx_dir_name_ends_with(name, nl, ".PNG" as *u8, 4) == 1 { return 1 } 404 if nx_dir_name_ends_with(name, nl, ".JPG" as *u8, 4) == 1 { return 1 } 405 // .gif ADMITTED 2026-08-04: nx_gif_decode is now wired into nx_img_to_gray, so a GIF is decodable. 406 // .webp ADMITTED DELIBERATELY THOUGH LOSSY VP8 STILL FAILS: excluding it here made the file invisible 407 // BEFORE any decode, so walkdir reported skipped_undecodable=0 while silently ignoring 26 real images. 408 // Admitting it converts an invisible omission into an honest skipped_undecodable count. A zero from a 409 // counter that was never incremented is not evidence of success. 410 if nx_dir_name_ends_with(name, nl, ".gif" as *u8, 4) == 1 { return 1 } 411 if nx_dir_name_ends_with(name, nl, ".GIF" as *u8, 4) == 1 { return 1 } 412 if nx_dir_name_ends_with(name, nl, ".webp" as *u8, 5) == 1 { return 1 } 413 // .bmp/.tif ADMITTED 2026-08-05: nx_bmp_decode + nx_tiff_decode wired through the 414 // chokepoint, which nx_img_to_gray now projects -- both are decodable end-to-end. 415 if nx_dir_name_ends_with(name, nl, ".bmp" as *u8, 4) == 1 { return 1 } 416 if nx_dir_name_ends_with(name, nl, ".BMP" as *u8, 4) == 1 { return 1 } 417 if nx_dir_name_ends_with(name, nl, ".tif" as *u8, 4) == 1 { return 1 } 418 if nx_dir_name_ends_with(name, nl, ".tiff" as *u8, 5) == 1 { return 1 } 419 // 1990s roster ADMITTED 2026-08-05 (nx_legacy_codec_gate 12/12): nx_tga_decode, 420 // nx_pcx_decode, nx_ico_decode and nx_pnm_decode are wired at the chokepoint, so 421 // each is decodable end-to-end. .ico is the one with live-web demand -- every 422 // favicon.ico a crawler meets is this format. 423 if nx_dir_name_ends_with(name, nl, ".tga" as *u8, 4) == 1 { return 1 } 424 if nx_dir_name_ends_with(name, nl, ".TGA" as *u8, 4) == 1 { return 1 } 425 if nx_dir_name_ends_with(name, nl, ".pcx" as *u8, 4) == 1 { return 1 } 426 if nx_dir_name_ends_with(name, nl, ".ico" as *u8, 4) == 1 { return 1 } 427 if nx_dir_name_ends_with(name, nl, ".ICO" as *u8, 4) == 1 { return 1 } 428 if nx_dir_name_ends_with(name, nl, ".cur" as *u8, 4) == 1 { return 1 } 429 if nx_dir_name_ends_with(name, nl, ".ppm" as *u8, 4) == 1 { return 1 } 430 if nx_dir_name_ends_with(name, nl, ".pgm" as *u8, 4) == 1 { return 1 } 431 if nx_dir_name_ends_with(name, nl, ".pbm" as *u8, 4) == 1 { return 1 } 432 if nx_dir_name_ends_with(name, nl, ".pnm" as *u8, 4) == 1 { return 1 } 433 return 0 434} 435 436// append ".status" (or any suffix) to a path into out; returns out (NUL-terminated). 437func s_suffix_path(path: *u8, suf: *u8, out: *u8) -> *u8 { 438 var o: i64 = 0 439 while path[o] != (0 as u8) { out[o] = path[o]; o = o + 1 } 440 var j: i64 = 0 441 while suf[j] != (0 as u8) { out[o] = suf[j]; o = o + 1; j = j + 1 } 442 out[o] = 0 as u8 443 return out 444} 445 446// fd-aware JSON emit helpers (the async child writes to a status file, not stdout) 447func s_fw(fd: i64, s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(fd, s, n); return 0 } 448// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 449// per call and never freed it. At page granularity that is 4096B leaked PER CALL -- the 450// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff. A BENCH is the worst home for 451// it: its purpose is millions of iterations. nxi_* is MSB-first and allocates NOTHING. 452func s_fwn(fd: i64, v: i64) -> i64 { nxi_fd(fd, v); return 0 } 453func s_fwstr(fd: i64, p: *u8) -> i64 { 454 var i: i64 = 0 455 while p[i] != (0 as u8) { 456 let c: i64 = p[i] as i64 457 if c == 34 { s_fw(fd, "\\\"" as *u8) } else { if c == 92 { s_fw(fd, "\\\\" as *u8) } else { if c < 32 { s_fw(fd, " " as *u8) } else { sys_write(fd, ((p as i64) + i) as *u8, 1) } } } 458 i = i + 1 459 } 460 return 0 461} 462 463// THE CORE: enumerate + index + write index. Emits the result JSON to `fd`. Returns 0 ok / 1 error. 464func s_walkdir_run(root: *u8, outpath: *u8, maxn: i64, fd: i64) -> i64 { 465 let rowcap: i64 = maxn * 4 + 16 466 let rows: *NxDirRow = sys_mmap(NX_DIR_ROW_BYTES * rowcap) as *NxDirRow 467 let arena: *u8 = sys_mmap(256 * rowcap) 468 let res: *NxDirResult = sys_mmap(NX_DIR_RESULT_BYTES) as *NxDirResult 469 let dv: i64 = nx_dir_list(root, rows, rowcap, arena, 256 * rowcap, 0, res) 470 if dv == NX_DIR_OPEN_FAILED { 471 s_fw(fd, "{\"tool\":\"nx_imgsearch\",\"verb\":\"walkdir\",\"ok\":false,\"error\":\"directory unreadable\",\"root\":\"" as *u8) 472 s_fwstr(fd, root); s_fw(fd, "\"}\n" as *u8) 473 return 1 474 } 475 let nfilled: i64 = res.n_filled 476 var dir_truncated: i64 = 0 477 if dv == NX_DIR_TRUNCATED { dir_truncated = 1 } 478 479 let recs: *u8 = sys_mmap(IS_STRIDE * (maxn + 2)) 480 let cidb: *u8 = sys_mmap(IS_MAGIC_1024 * (maxn + 2)) 481 let pbuf: *u8 = sys_mmap(IS_MAGIC_4096) 482 let ing: *SwIngest = s_ingest_new() 483 var nrec: i64 = 0 484 var cpos: i64 = 0 485 var skipped: i64 = 0 486 var seen_imgs: i64 = 0 487 var capped: i64 = 0 488 489 var r: i64 = 0 490 while r < nfilled { 491 if nrec >= maxn { capped = 1; r = nfilled } else { 492 let row: *NxDirRow = nx_dir_row_at(rows, r) 493 if nx_dir_row_is_regular_file(row) == 1 { 494 if s_is_image_name(row.name_ptr, row.name_len) == 1 { 495 seen_imgs = seen_imgs + 1 496 s_path_join(root, row.name_ptr, pbuf) 497 let ncp: i64 = s_ingest_file(ing, recs, nrec, cidb, cpos, row.name_ptr, pbuf) 498 if ncp < 0 { skipped = skipped + 1 } else { cpos = ncp; nrec = nrec + 1 } 499 } 500 } 501 r = r + 1 502 } 503 } 504 505 if s_write_index(outpath, recs, nrec, cidb, cpos) < 0 { 506 s_fw(fd, "{\"tool\":\"nx_imgsearch\",\"verb\":\"walkdir\",\"ok\":false,\"error\":\"cannot open output\"}\n" as *u8) 507 return 1 508 } 509 510 s_fw(fd, "{\"tool\":\"nx_imgsearch\",\"verb\":\"walkdir\",\"ok\":true,\"done\":true," as *u8) 511 s_fw(fd, "\"root\":\"" as *u8); s_fwstr(fd, root); s_fw(fd, "\"," as *u8) 512 s_fw(fd, "\"dir_entries\":" as *u8); s_fwn(fd, nfilled) 513 s_fw(fd, ",\"image_files_seen\":" as *u8); s_fwn(fd, seen_imgs) 514 s_fw(fd, ",\"indexed\":" as *u8); s_fwn(fd, nrec) 515 s_fw(fd, ",\"skipped_undecodable\":" as *u8); s_fwn(fd, skipped) 516 s_fw(fd, ",\"capped_at_max\":" as *u8) 517 if capped == 1 { s_fw(fd, "true" as *u8) } else { s_fw(fd, "false" as *u8) } 518 s_fw(fd, ",\"dir_listing_truncated\":" as *u8) 519 if dir_truncated == 1 { s_fw(fd, "true" as *u8) } else { s_fw(fd, "false" as *u8) } 520 s_fw(fd, ",\"max\":" as *u8); s_fwn(fd, maxn) 521 s_fw(fd, ",\"bytes\":" as *u8); s_fwn(fd, IS_HDR + nrec * IS_STRIDE + cpos) 522 s_fw(fd, ",\"path\":\"" as *u8); s_fwstr(fd, outpath); s_fw(fd, "\"}\n" as *u8) 523 return 0 524} 525 526func s_clamp_max(max_in: i64) -> i64 { 527 var maxn: i64 = max_in 528 if maxn < 1 { maxn = SW_WALK_DEFAULT } 529 if maxn > SW_WALK_HARDCAP { maxn = SW_WALK_HARDCAP } 530 return maxn 531} 532 533// foreground (small dirs / local tests): run + emit to stdout. 534func s_verb_walkdirsync(root: *u8, outpath: *u8, max_in: i64) -> i64 { 535 return s_walkdir_run(root, outpath, s_clamp_max(max_in), 1) 536} 537 538// ASYNC: fork a detached child that indexes in the background and writes <index>.status; return now. 539func s_verb_walkdir(root: *u8, outpath: *u8, max_in: i64) -> i64 { 540 let maxn: i64 = s_clamp_max(max_in) 541 let statp: *u8 = s_suffix_path(outpath, ".status" as *u8, sys_mmap(IS_MAGIC_4096)) 542 // a fresh run must not read a previous run's DONE marker -- clear it first (best-effort truncate). 543 let cfd: i64 = sys_openat_wr(statp, 0x1a4) 544 if cfd >= 0 { sys_close(cfd) } 545 546 let pid: i64 = sys_fork() 547 if pid == 0 { 548 // CHILD: detach into its own session so the daemon's request-pipe close / pgroup signal can't 549 // reap it, then redirect fds so its output can never corrupt the parent's JSON response. 550 nx_setsid() 551 let din: i64 = sys_openat_rd("/dev/null" as *u8) 552 let dlog: i64 = sys_openat_wr("/tmp/nx_imgsearch_walk.log" as *u8, 0x1a4) 553 if din >= 0 { sys_dup3(din, 0, 0) } 554 if dlog >= 0 { sys_dup3(dlog, 1, 0); sys_dup3(dlog, 2, 0) } 555 let sfd: i64 = sys_openat_wr(statp, 0x1a4) 556 s_walkdir_run(root, outpath, maxn, sfd) 557 if sfd >= 0 { sys_close(sfd) } 558 sys_exit(0) 559 return 0 560 } 561 562 // PARENT: return immediately (well under the edge's synchronous budget). 563 s_puts("{" as *u8) 564 s_jkey("tool" as *u8); s_puts("\"nx_imgsearch\"," as *u8) 565 s_jkey("verb" as *u8); s_puts("\"walkdir\"," as *u8) 566 s_jkey("ok" as *u8); s_puts("true," as *u8) 567 s_jkey("action" as *u8); s_puts("\"STARTED\"," as *u8) 568 s_jkey("mode" as *u8); s_puts("\"detached\"," as *u8) 569 s_jkey("pid" as *u8); s_num(pid); s_puts("," as *u8) 570 s_jkey("root" as *u8); s_puts("\"" as *u8); s_jstr(root, s_strlen(root)); s_puts("\"," as *u8) 571 s_jkey("max" as *u8); s_num(maxn); s_puts("," as *u8) 572 s_jkey("index" as *u8); s_puts("\"" as *u8); s_jstr(outpath, s_strlen(outpath)); s_puts("\"," as *u8) 573 s_jkey("status_marker" as *u8); s_puts("\"" as *u8); s_jstr(statp, s_strlen(statp)); s_puts("\"," as *u8) 574 s_jkey("poll_with" as *u8); s_puts("\"walkstatus <index>\"" as *u8) 575 s_puts("}\n" as *u8) 576 return 0 577} 578 579// poll an async walkdir: cat <index>.status if the child finished, else report running/absent. 580func s_verb_walkstatus(idxpath: *u8) -> i64 { 581 let statp: *u8 = s_suffix_path(idxpath, ".status" as *u8, sys_mmap(IS_MAGIC_4096)) 582 let sbox: *i64 = sys_mmap(16) as *i64 583 let sbuf: *u8 = sys_read_file(statp, sbox) 584 if sbuf != (0 as *u8) { if sbox[0] > 0 { 585 // the child wrote its result JSON -- emit it verbatim (already a complete object) 586 sys_write(1, sbuf, sbox[0]) 587 return 0 588 } } 589 // no completed marker yet: is the index (partial) present at all? 590 let ibox: *i64 = sys_mmap(16) as *i64 591 let ibuf: *u8 = sys_read_file(idxpath, ibox) 592 var have_idx: i64 = 0 593 if ibuf != (0 as *u8) { if ibox[0] >= IS_HDR { have_idx = 1 } } 594 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"walkstatus\",\"ok\":true,\"done\":false,\"status\":\"running-or-absent\",\"index_present\":" as *u8) 595 if have_idx == 1 { s_puts("true" as *u8) } else { s_puts("false" as *u8) } 596 s_puts(",\"index\":\"" as *u8); s_jstr(idxpath, s_strlen(idxpath)); s_puts("\"}\n" as *u8) 597 return 0 598} 599 600// ---- verb: query ------------------------------------------------------------------------------- 601func s_verb_query(idxpath: *u8, imgpath: *u8, topk_in: i64) -> i64 { 602 let ibox: *i64 = sys_mmap(16) as *i64 603 let ib: *u8 = sys_map_file(idxpath, ibox) 604 if ib == (0 as *u8) { 605 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"query\",\"ok\":false,\"error\":\"index unreadable\"}\n" as *u8) 606 sys_exit(1); return 1 607 } 608 if ibox[0] < IS_HDR { 609 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"query\",\"ok\":false,\"error\":\"index truncated\"}\n" as *u8) 610 sys_exit(1); return 1 611 } 612 if ib[0] != (78 as u8) { 613 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"query\",\"ok\":false,\"error\":\"bad index magic\"}\n" as *u8) 614 sys_exit(1); return 1 615 } 616 // stride/version guard: a stale index built at a different record stride would be silently 617 // mis-sliced (every field read at the wrong offset). Reject it with a clear message instead. 618 let idx_stride: i64 = s_get_u32(ib, 12) 619 if idx_stride != IS_STRIDE { 620 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"query\",\"ok\":false,\"error\":\"index stride mismatch (stale index -- re-run index with this build)\",\"index_stride\":" as *u8) 621 s_num(idx_stride); s_puts(",\"expected_stride\":" as *u8); s_num(IS_STRIDE); s_puts("}\n" as *u8) 622 sys_exit(1); return 1 623 } 624 let count: i64 = s_get_i64(ib, 16) 625 let cbo: i64 = s_get_i64(ib, 24) 626 if count <= 0 { 627 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"query\",\"ok\":true,\"index\":{\"count\":0},\"identity_match\":{\"present\":false,\"reason\":\"empty index\"},\"results\":[]}\n" as *u8) 628 return 0 629 } 630 631 let wh: *i64 = sys_mmap(16) as *i64 632 let graw: *u8 = nx_img_to_gray(imgpath, wh) 633 if graw == (0 as *u8) { 634 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"query\",\"ok\":false,\"error\":\"query image undecodable (PNG/JPEG supported; WebP/AVIF are a known gap)\"}\n" as *u8) 635 sys_exit(1); return 1 636 } 637 // same working-resolution cap as ingest, so query and index descriptors are computed identically 638 let gray: *u8 = s_downscale_max(graw, wh[0], wh[1], IS_WORKDIM, wh) 639 640 // expand the mapped records into the engine's tier descriptor arrays. Tier order matches 641 // s_build_engine exactly: 0=copy, 1=orient, 2=local, 3=similar. (Serving RAM ceiling documented 642 // in status; on-disk stays 1256 B/record, filed for a u8-native serve path as seq717.) 643 let e: *nx_imgengine = s_build_engine(count) 644 var i: i64 = 0 645 while i < count { 646 let ro: i64 = IS_HDR + i * IS_STRIDE 647 let dcopy: *i64 = ie_desc_at(e, 0, i) 648 dcopy[0] = s_get_i64(ib, ro) 649 let dor: *i64 = ie_desc_at(e, 1, i) 650 dor[0] = dcopy[0] 651 let dloc: *i64 = ie_desc_at(e, 2, i) 652 var lq: i64 = 0 653 while lq < IS_LOCAL_N { dloc[lq] = s_get_i64(ib, ro + IS_LOCAL_OFF + lq*8); lq = lq + 1 } 654 let dsim: *i64 = ie_desc_at(e, 3, i) 655 var d: i64 = 0 656 while d < IS_VDIM { dsim[d] = ib[ro + 8 + d] as i64; d = d + 1 } 657 let parr: *i64 = e.pays as *i64 658 parr[i] = i 659 i = i + 1 660 } 661 e.count = count 662 663 var topk: i64 = topk_in 664 if topk < 1 { topk = 5 } 665 if topk > IS_MAXTOPK { topk = IS_MAXTOPK } 666 let op: *i64 = sys_mmap(8 * topk) as *i64 667 let os: *i64 = sys_mmap(8 * topk) as *i64 668 let ot: *i64 = sys_mmap(8 * topk) as *i64 669 let od: *i64 = sys_mmap(8 * topk) as *i64 670 let n: i64 = nx_imgengine_query(e, gray, 0 as *u8, wh[0], wh[1], topk, op, os, ot, od) 671 672 let bt: *i64 = sys_mmap(8) as *i64 673 let bd: *i64 = sys_mmap(8) as *i64 674 let br: *i64 = sys_mmap(8) as *i64 675 let bm: i64 = nx_imgengine_best_match_r(e, gray, 0 as *u8, wh[0], wh[1], bt, bd, br) 676 677 s_puts("{" as *u8) 678 s_jkey("tool" as *u8); s_puts("\"nx_imgsearch\"," as *u8) 679 s_jkey("version" as *u8); s_num(IS_VERSION); s_puts("," as *u8) 680 s_jkey("verb" as *u8); s_puts("\"query\"," as *u8) 681 s_jkey("ok" as *u8); s_puts("true," as *u8) 682 s_jkey("index" as *u8); s_puts("{" as *u8) 683 s_jkey("count" as *u8); s_num(count); s_puts("," as *u8) 684 s_jkey("bytes" as *u8); s_num(ibox[0]); s_puts("," as *u8) 685 s_jkey("serving_ram_bytes" as *u8); s_num(count * IS_RAM_PER_IMG) 686 s_puts("}," as *u8) 687 s_jkey("query" as *u8); s_puts("{" as *u8) 688 s_jkey("width" as *u8); s_num(wh[0]); s_puts("," as *u8) 689 s_jkey("height" as *u8); s_num(wh[1]) 690 s_puts("}," as *u8) 691 692 // the IDENTITY answer, kept separate from the similarity list on purpose 693 s_jkey("identity_match" as *u8); s_puts("{" as *u8) 694 if bm < 0 { 695 s_jkey("present" as *u8); s_puts("false," as *u8) 696 s_jkey("reason" as *u8) 697 if br[0] == 2 { 698 s_puts("\"query image carries too little visual structure for identity matching (flat or smooth-gradient); its fingerprint would collide with every other structureless image\"," as *u8) 699 s_jkey("reason_code" as *u8); s_puts("\"uninformative_query\"," as *u8) 700 } else { 701 s_puts("\"no identity-class tier within its threshold\"," as *u8) 702 s_jkey("reason_code" as *u8); s_puts("\"no_match\"," as *u8) 703 } 704 s_jkey("nearest_distance" as *u8); s_num(bd[0]) 705 } else { 706 let ro: i64 = IS_HDR + bm * IS_STRIDE 707 let co: i64 = cbo + s_get_u32(ib, ro + 88) 708 let cl: i64 = s_get_u32(ib, ro + 92) 709 let mt: *nx_imgtier = nx_imgengine_tier(e, bt[0]) 710 s_jkey("present" as *u8); s_puts("true," as *u8) 711 s_jkey("cid" as *u8); s_puts("\"" as *u8); s_jstr(((ib as i64) + co) as *u8, cl); s_puts("\"," as *u8) 712 s_jkey("tier" as *u8); s_puts("\"" as *u8); s_puts(nx_imgtier_name(mt)); s_puts("\"," as *u8) 713 s_jkey("distance" as *u8); s_num(bd[0]) 714 } 715 s_puts("}," as *u8) 716 717 s_jkey("results" as *u8); s_puts("[" as *u8) 718 i = 0 719 while i < n { 720 if i > 0 { s_puts("," as *u8) } 721 let ro: i64 = IS_HDR + op[i] * IS_STRIDE 722 let co: i64 = cbo + s_get_u32(ib, ro + 88) 723 let cl: i64 = s_get_u32(ib, ro + 92) 724 let rt: *nx_imgtier = nx_imgengine_tier(e, ot[i]) 725 s_puts("{" as *u8) 726 s_jkey("rank" as *u8); s_num(i + 1); s_puts("," as *u8) 727 s_jkey("cid" as *u8); s_puts("\"" as *u8); s_jstr(((ib as i64) + co) as *u8, cl); s_puts("\"," as *u8) 728 s_jkey("score" as *u8); s_num(os[i]); s_puts("," as *u8) 729 s_jkey("tier" as *u8); s_puts("\"" as *u8); s_puts(nx_imgtier_name(rt)); s_puts("\"," as *u8) 730 s_jkey("tier_class" as *u8); s_puts("\"" as *u8); s_puts(s_klass_name(nx_imgtier_klass(rt))); s_puts("\"," as *u8) 731 s_jkey("distance" as *u8); s_num(od[i]); s_puts("," as *u8) 732 s_jkey("similarity_permille" as *u8); s_num(nx_imgtier_score(rt, od[i])); s_puts("," as *u8) 733 // Confidence comes from the ENGINE's confident-score, not from re-deriving the threshold 734 // test here. Recomputing it locally is how the two disagreed: identity_match refused a 735 // structureless query while this list still labelled its collisions confident. 736 s_jkey("confident" as *u8) 737 if os[i] > 0 { s_puts("true" as *u8) } else { s_puts("false" as *u8) } 738 s_puts("}" as *u8) 739 i = i + 1 740 } 741 s_puts("]}\n" as *u8) 742 return 0 743} 744 745// ---- verb: selftest ---------------------------------------------------------------------------- 746// A round trip an agent can run to confirm the service is functional without any corpus on disk: 747// build a synthetic index in memory, mirror one of its images, and require the engine to find it. 748func s_verb_selftest() -> i64 { 749 let N: i64 = 64 750 let W: i64 = 64 751 let e: *nx_imgengine = s_build_engine(N) 752 var i: i64 = 0 753 while i < N { nx_imgengine_add_image(e, nx_imgcorpus_new(i, W, W), 0 as *u8, W, W, i); i = i + 1 } 754 let src: *u8 = nx_imgcorpus_new(7, W, W) 755 let mir: *u8 = sys_mmap(W * W) 756 var y: i64 = 0 757 while y < W { 758 var x: i64 = 0 759 while x < W { mir[y * W + x] = src[y * W + (W - 1 - x)]; x = x + 1 } 760 y = y + 1 761 } 762 let bt: *i64 = sys_mmap(8) as *i64 763 let bd: *i64 = sys_mmap(8) as *i64 764 let hit: i64 = nx_imgengine_best_match(e, mir, 0 as *u8, W, W, bt, bd) 765 let stranger: i64 = nx_imgengine_best_match(e, nx_imgcorpus_new(IS_MAGIC_99999, W, W), 0 as *u8, W, W, bt, bd) 766 var ok: i64 = 0 767 if hit == 7 { if stranger < 0 { ok = 1 } } 768 s_puts("{" as *u8) 769 s_jkey("tool" as *u8); s_puts("\"nx_imgsearch\"," as *u8) 770 s_jkey("verb" as *u8); s_puts("\"selftest\"," as *u8) 771 s_jkey("ok" as *u8) 772 if ok == 1 { s_puts("true," as *u8) } else { s_puts("false," as *u8) } 773 s_jkey("mirrored_image_found" as *u8); s_num(hit); s_puts("," as *u8) 774 s_jkey("expected" as *u8); s_puts("7," as *u8) 775 s_jkey("stranger_correctly_absent" as *u8) 776 if stranger < 0 { s_puts("true," as *u8) } else { s_puts("false," as *u8) } 777 s_jkey("verdict" as *u8) 778 if ok == 1 { s_puts("\"GREEN\"" as *u8) } else { s_puts("\"RED\"" as *u8) } 779 s_puts("}\n" as *u8) 780 if ok == 1 { return 0 } 781 sys_exit(1) 782 return 1 783} 784 785func main(argc: i64, argv: *i64) -> i64 { 786 if argc < 2 { s_verb_status(); return 0 } 787 let verb: *u8 = argv[1] as *u8 788 if s_streq(verb, "status" as *u8) == 1 { s_verb_status(); return 0 } 789 if s_streq(verb, "selftest" as *u8) == 1 { return s_verb_selftest() } 790 if s_streq(verb, "index" as *u8) == 1 { 791 if argc < 4 { 792 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"index\",\"ok\":false,\"error\":\"usage: index <manifest.tsv> <out.nximgidx>\"}\n" as *u8) 793 sys_exit(2); return 2 794 } 795 return s_verb_index(argv[2] as *u8, argv[3] as *u8) 796 } 797 if s_streq(verb, "query" as *u8) == 1 { 798 if argc < 4 { 799 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"query\",\"ok\":false,\"error\":\"usage: query <index.nximgidx> <image> [topk]\"}\n" as *u8) 800 sys_exit(2); return 2 801 } 802 var k: i64 = 5 803 if argc >= 5 { k = s_atoi(argv[4] as *u8) } 804 return s_verb_query(argv[2] as *u8, argv[3] as *u8, k) 805 } 806 if s_streq(verb, "walkdir" as *u8) == 1 { 807 if argc < 4 { 808 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"walkdir\",\"ok\":false,\"error\":\"usage: walkdir <image-dir> <out.nximgidx> [max] (async -- poll with walkstatus)\"}\n" as *u8) 809 sys_exit(2); return 2 810 } 811 var mx: i64 = 0 812 if argc >= 5 { mx = s_atoi(argv[4] as *u8) } 813 return s_verb_walkdir(argv[2] as *u8, argv[3] as *u8, mx) 814 } 815 if s_streq(verb, "walkdirsync" as *u8) == 1 { 816 if argc < 4 { 817 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"walkdirsync\",\"ok\":false,\"error\":\"usage: walkdirsync <image-dir> <out.nximgidx> [max]\"}\n" as *u8) 818 sys_exit(2); return 2 819 } 820 var mx: i64 = 0 821 if argc >= 5 { mx = s_atoi(argv[4] as *u8) } 822 return s_verb_walkdirsync(argv[2] as *u8, argv[3] as *u8, mx) 823 } 824 if s_streq(verb, "walkstatus" as *u8) == 1 { 825 if argc < 3 { 826 s_puts("{\"tool\":\"nx_imgsearch\",\"verb\":\"walkstatus\",\"ok\":false,\"error\":\"usage: walkstatus <index.nximgidx>\"}\n" as *u8) 827 sys_exit(2); return 2 828 } 829 return s_verb_walkstatus(argv[2] as *u8) 830 } 831 s_puts("{\"tool\":\"nx_imgsearch\",\"ok\":false,\"error\":\"unknown verb\",\"verbs\":[\"status\",\"selftest\",\"index\",\"query\",\"walkdir\",\"walkdirsync\",\"walkstatus\"]}\n" as *u8) 832 sys_exit(2) 833 return 2 834}