nx_image_grade_cli.nx source
↩ module page · 241 lines · 8176 B
1// nx_image_grade_cli.nx -- substrate's image-grading CLI binary.
2//
3// CAPABILITY_COMPLETENESS: PARTIAL
4// MISSING_CAPABILITIES:
5// - argv-driven file-path input: substrate's `main()` has no
6// argv access in the current bootstrap convention; v1 reads
7// image bytes from STDIN. Calling pattern:
8// cat image.jpg | nishi-image-grade (or)
9// nishi-image-grade < image.jpg
10// - structured output: v1 emits one human-readable line per
11// axis; v2 (queued) emits canonical JSONL via
12// nx_jsonl_writer so callers can compose against the
13// ledger.
14// - exit-code mapping per failure tier: v1 returns 0 on
15// decode success, 1 on decoder error.
16//
17// COVERED:
18// - stdin -> bytes (up to 16 MiB cap)
19// - format auto-detect via nx_image_grade_v2
20// - PNG / JPEG decode + skin mask + segmenter + landmark
21// labeler + 8 axes invoked on real pixel data
22// - per-axis line emitted to stdout: "axis_id status verdict
23// score phrase"
24// - stderr message on decode error
25//
26// Usage:
27// nxc2 --target x86_64 nx_image_grade_cli.nx > cli.s
28// nx_assembler cli.s -> cli.elf
29// cat image.jpg | ./cli.elf
30//
31// genealogy_id: substrate_image_grade_cli_2026_05_16
32// lineage_id: nx_image_grade_cli_v1
33
34import "nx_syscalls.nx"
35import "nx_runtime.nx"
36import "nx_tier.nx"
37import "nx_image_grade_v2.nx"
38const NX_MAGIC_65536: i64 = 65536
39
40const NX_CLI_MAX_IMAGE_BYTES: nx_int = 16777216 // 16 MiB
41const NX_CLI_READ_CHUNK: nx_int = 65536 // 64 KiB read chunks
42const NX_CLI_LINE_BUF: nx_int = 256
43
44// ===== stdin slurp ===============================================
45//
46// Reads from fd 0 until EOF or cap. Returns bytes-read.
47
48func _cli_slurp_stdin(buf: *u8, max: nx_int) -> nx_int {
49 var total: nx_int = 0
50 var keep: nx_int = 1
51 var safety: nx_int = 0
52 let MAX_ITER: nx_int = NX_MAGIC_65536
53 while keep == 1 {
54 if safety >= MAX_ITER { keep = 0 }
55 else {
56 safety = safety + 1
57 if total >= max { keep = 0 }
58 else {
59 var chunk: nx_int = NX_CLI_READ_CHUNK
60 if (max - total) < chunk { chunk = max - total }
61 let dst: *u8 = (buf as nx_int + total) as *u8
62 let n: nx_int = sys_read(0, dst, chunk)
63 if n <= 0 { keep = 0 }
64 else { total = total + n }
65 }
66 }
67 }
68 return total
69}
70
71// ===== text helpers ==============================================
72//
73// Format integer as ASCII into buf; return new offset.
74
75func _cli_write_int(buf: *u8, off: nx_int, v: nx_int) -> nx_int {
76 if v == 0 {
77 buf[off] = 48 as u8
78 return off + 1
79 }
80 var val: nx_int = v
81 var negative: nx_int = 0
82 if val < 0 {
83 negative = 1
84 val = 0 - val
85 }
86 let scratch: *u8 = (sys_mmap(32)) as *u8
87 var n_digits: nx_int = 0
88 while val > 0 {
89 let d: nx_int = val % 10
90 scratch[n_digits] = (48 + d) as u8
91 val = val / 10
92 n_digits = n_digits + 1
93 }
94 var pos: nx_int = off
95 if negative == 1 {
96 buf[pos] = 45 as u8 // '-'
97 pos = pos + 1
98 }
99 var k: nx_int = n_digits - 1
100 while k >= 0 {
101 buf[pos] = scratch[k]
102 pos = pos + 1
103 k = k - 1
104 }
105 return pos
106}
107
108// Copy NUL-terminated string into buf at off; return new offset.
109func _cli_write_cstr(buf: *u8, off: nx_int, src: *u8, cap: nx_int) -> nx_int {
110 var pos: nx_int = off
111 var i: nx_int = 0
112 var safety: nx_int = 0
113 let MAX: nx_int = NX_CLI_LINE_BUF
114 while safety < MAX {
115 let ch: nx_int = (src[i] as nx_int) & 255
116 if ch == 0 { return pos }
117 if pos >= cap { return pos }
118 buf[pos] = ch as u8
119 pos = pos + 1
120 i = i + 1
121 safety = safety + 1
122 }
123 return pos
124}
125
126// Append a literal NUL-terminated byte sequence.
127func _cli_write_lit(buf: *u8, off: nx_int, lit: *u8) -> nx_int {
128 return _cli_write_cstr(buf, off, lit, NX_CLI_LINE_BUF)
129}
130
131// ===== status -> text label =====================================
132
133func _cli_status_label(status: nx_int) -> *u8 {
134 if status == NX_IGV2_AXIS_OK { return "OK" as *u8 }
135 if status == NX_IGV2_AXIS_PENDING_KEYPOINT { return "PENDING_KEYPOINT" as *u8 }
136 if status == NX_IGV2_AXIS_PENDING_POSE_ESTIMATE { return "PENDING_POSE" as *u8 }
137 if status == NX_IGV2_AXIS_PENDING_VLM { return "PENDING_VLM" as *u8 }
138 if status == NX_IGV2_AXIS_NOT_APPLICABLE { return "N/A" as *u8 }
139 if status == NX_IGV2_AXIS_NO_SKIN_DETECTED { return "NO_SKIN" as *u8 }
140 if status == NX_IGV2_AXIS_INSUFFICIENT_LANDMARKS { return "PARTIAL_LANDMARKS" as *u8 }
141 return "UNKNOWN" as *u8
142}
143
144// ===== emit one axis line ========================================
145//
146// Format:
147// axis=<id> status=<label> verdict=<int> score=<int> "<phrase>"
148//
149// Each line ends with '\n'.
150
151func _cli_emit_axis(ax: *NxIgv2AxisVerdict) -> nx_int {
152 let line: *u8 = (sys_mmap(NX_CLI_LINE_BUF as nx_size)) as *u8
153 var pos: nx_int = 0
154 pos = _cli_write_lit(line, pos, "axis=" as *u8)
155 pos = _cli_write_int(line, pos, ax.axis_id)
156 pos = _cli_write_lit(line, pos, " status=" as *u8)
157 pos = _cli_write_cstr(line, pos, _cli_status_label(ax.status), NX_CLI_LINE_BUF)
158 pos = _cli_write_lit(line, pos, " verdict=" as *u8)
159 pos = _cli_write_int(line, pos, ax.verdict_value)
160 pos = _cli_write_lit(line, pos, " score=" as *u8)
161 pos = _cli_write_int(line, pos, ax.score_q10)
162 pos = _cli_write_lit(line, pos, " phrase=" as *u8)
163 line[pos] = 34 as u8 // '"'
164 pos = pos + 1
165 pos = _cli_write_cstr(line, pos, ax.phrase_buf, NX_CLI_LINE_BUF - 3)
166 line[pos] = 34 as u8 // '"'
167 pos = pos + 1
168 line[pos] = 10 as u8 // '\n'
169 pos = pos + 1
170 sys_write(1, line, pos)
171 return 0
172}
173
174// ===== emit header line ==========================================
175
176func _cli_emit_header(r: *NxIgv2Report) -> nx_int {
177 let line: *u8 = (sys_mmap(NX_CLI_LINE_BUF as nx_size)) as *u8
178 var pos: nx_int = 0
179 pos = _cli_write_lit(line, pos, "image_grade_v2 format=" as *u8)
180 pos = _cli_write_int(line, pos, r.source_format)
181 pos = _cli_write_lit(line, pos, " w=" as *u8)
182 pos = _cli_write_int(line, pos, r.width)
183 pos = _cli_write_lit(line, pos, " h=" as *u8)
184 pos = _cli_write_int(line, pos, r.height)
185 pos = _cli_write_lit(line, pos, " err=" as *u8)
186 pos = _cli_write_int(line, pos, r.error_code)
187 pos = _cli_write_lit(line, pos, " n_axes=" as *u8)
188 pos = _cli_write_int(line, pos, r.n_axes)
189 pos = _cli_write_lit(line, pos, " skin_pixels=" as *u8)
190 pos = _cli_write_int(line, pos, r.skin_pixel_count)
191 pos = _cli_write_lit(line, pos, " skin_blobs=" as *u8)
192 pos = _cli_write_int(line, pos, r.n_skin_blobs)
193 line[pos] = 10 as u8
194 pos = pos + 1
195 sys_write(1, line, pos)
196 return 0
197}
198
199// ===== main ======================================================
200
201func main() -> nx_int {
202 // Slurp stdin into a buffer.
203 let buf: *u8 = (sys_mmap(NX_CLI_MAX_IMAGE_BYTES as nx_size)) as *u8
204 let n: nx_int = _cli_slurp_stdin(buf, NX_CLI_MAX_IMAGE_BYTES)
205 if n <= 0 {
206 let msg: *u8 = "nx_image_grade_cli: no input on stdin\n" as *u8
207 sys_write(2, msg, 38)
208 return 1
209 }
210
211 // Trigger the v2 grader.
212 let r: *NxIgv2Report = nx_image_grade_v2(buf, n)
213 if r == (0 as *NxIgv2Report) {
214 let msg2: *u8 = "nx_image_grade_cli: grader returned null\n" as *u8
215 sys_write(2, msg2, 41)
216 return 2
217 }
218
219 // Emit header.
220 _cli_emit_header(r)
221
222 // When the decoder failed (e.g. NX_IGV2_ERR_DECODER on a PNG whose
223 // DEFLATE stream we can't yet inflate), grade_v2 leaves n_axes
224 // populated but the axes buffer holds junk. Walking it segfaults.
225 if r.error_code != NX_IGV2_OK {
226 return 1
227 }
228
229 // Emit per-axis lines.
230 if r.axes != (0 as *NxIgv2AxisVerdict) {
231 var i: nx_int = 0
232 while i < r.n_axes {
233 let ax: *NxIgv2AxisVerdict =
234 (r.axes as *u8 + (i as nx_size) * NX_IGV2_AXIS_VERDICT_BYTES) as *NxIgv2AxisVerdict
235 _cli_emit_axis(ax)
236 i = i + 1
237 }
238 }
239
240 return 0
241}