nx_ingest_cli.nx source
↩ module page · 298 lines · 11359 B
1// nx_ingest_cli.nx -- CLI driver for the paste-HTML -> POV-caption pipeline.
2//
3// Reads raw HTML from stdin (up to 256 KiB), runs the full Tier 0-5
4// ingestion stack, and writes the POV-shifted Literotica caption to
5// stdout. Exit code 0 on success.
6//
7// Usage (under qemu-riscv64-static):
8//
9// cat chapter.html | qemu-riscv64-static _offc/nx_ingest_cli.elf
10//
11// Or via the bench wrapper:
12//
13// bench/nx_ingest_cli.sh path/to/chapter.html
14//
15// Workflow today (TLS-blocked):
16// 1. Browser -> open the source page (literotica chapter, nhentai
17// gallery, fanfiction.net chapter, anywhere)
18// 2. Ctrl+U to view source, save as .html OR copy-paste into a
19// file
20// 3. Pipe that file into this CLI
21// 4. POV-shifted first-person caption arrives on stdout
22//
23// Workflow once TLS unblocks (SHA-256 fix):
24// 1. The same CLI accepts a URL arg
25// 2. Internally calls nx_media_fetch URL transport
26// 3. Same pipeline
27//
28// Per cardinal feedback-user-owns-every-bit: this is a one-shot
29// process, no background daemon, no auto-cataloging without
30// caller opt-in. Output goes to stdout; the user redirects.
31//
32// nx_safety_envelope:
33// intended_use: "CLI adapter that runs paste-HTML through
34// the full ingestion + caption pipeline,
35// emitting POV-shifted output to stdout."
36// sil_target: SIL2
37// asil_target: QM
38// dal_target: DAL C
39// iec_62304_class: NONE
40// evidence: [no_floating_point,
41// bounded_read_loop,
42// single_pipeline_invocation_per_call,
43// deterministic_pov_gender_default]
44// hazard_register: [bug-tape-stdin-truncated-at-cap,
45// bug-tape-empty-input-returns-empty-output]
46// residual_risk: "Hard 256 KiB stdin cap; pasting larger
47// documents truncates silently. Future
48// revision streams in chunks."
49// verdict: NOT_YET_EVALUATED
50
51import "nx_syscalls.nx"
52import "nx_media_pool.nx"
53import "nx_storybeat.nx"
54import "nx_storydb.nx"
55import "nx_html_extract.nx"
56import "nx_text_ingest.nx"
57import "nx_term_registry.nx"
58import "nx_term_extract.nx"
59import "nx_term_cooccur.nx"
60import "nx_arc_anchor.nx"
61import "nx_scene_index.nx"
62import "nx_pov_shift.nx"
63import "nx_caption_emit.nx"
64import "nx_content_guard.nx"
65const NX_MAGIC_4096: i64 = 4096
66
67const NX_INGEST_INPUT_CAP: i64 = 262144 // 256 KiB
68const NX_INGEST_OUTPUT_CAP: i64 = 524288 // 512 KiB
69
70// Default protagonist gender for V1. V2: parse from CLI arg.
71const NX_INGEST_DEFAULT_GENDER: i64 = 1 // NX_POV_GENDER_HE
72
73// Default output format. V2: parse from CLI arg.
74const NX_INGEST_DEFAULT_FORMAT: i64 = 2 // NX_CAP_FORMAT_LITEROTICA
75
76func nx_ingest_read_stdin(buf: *u8, cap: i64) -> i64 {
77 var total: i64 = 0
78 let BUDGET: i64 = (cap / NX_MAGIC_4096) + 16
79 var iter: i64 = 0
80 var done: i64 = 0
81 while done == 0 {
82 if iter >= BUDGET { done = 1 }
83 if done == 0 {
84 if total >= cap { done = 1 }
85 if done == 0 {
86 let dst: *u8 = ((buf as i64) + total) as *u8
87 let want: i64 = cap - total
88 let got: i64 = sys_read(0, dst, want)
89 if got < 0 { done = 1 }
90 if got == 0 { done = 1 }
91 if got > 0 { total = total + got }
92 }
93 }
94 iter = iter + 1
95 }
96 return total
97}
98
99func nx_ingest_write_stdout(buf: *u8, n: i64) -> i64 {
100 if n <= 0 { return 0 }
101 var off: i64 = 0
102 let BUDGET: i64 = (n / NX_MAGIC_4096) + 16
103 var iter: i64 = 0
104 while off < n {
105 if iter >= BUDGET { off = n }
106 if off < n {
107 let src: *u8 = ((buf as i64) + off) as *u8
108 let want: i64 = n - off
109 let wrote: i64 = sys_write(1, src, want)
110 if wrote <= 0 { off = n }
111 if wrote > 0 { off = off + wrote }
112 }
113 iter = iter + 1
114 }
115 return off
116}
117
118// Write a literal byte string to stderr (informational).
119func nx_ingest_log(s: *u8, n: i64) -> i64 {
120 return sys_write(2, s, n)
121}
122
123func main() -> i64 {
124 // ---- 1. Read HTML from stdin ----
125 let in_buf: *u8 = sys_mmap(NX_INGEST_INPUT_CAP)
126 let in_n: i64 = nx_ingest_read_stdin(in_buf, NX_INGEST_INPUT_CAP)
127 if in_n <= 0 {
128 nx_ingest_log("nx_ingest_cli: empty input\n" as *u8, 25)
129 return 1
130 }
131
132 // ---- 2. Extract title + body text from HTML ----
133 let extract: *HtmlExtract = nx_html_extract(in_buf, in_n)
134 if extract.body_len <= 0 {
135 nx_ingest_log("nx_ingest_cli: html_extract produced no body\n" as *u8, 45)
136 return 2
137 }
138
139 // ---- 2b. Top-level content guard: refuse NTR-coded sources ----
140 //
141 // Runs BEFORE the pipeline so even single-paragraph NTR sources
142 // (too short to produce arc anchors) still get refused. Per
143 // user spec: "i am the dominant actor acting on the scene not
144 // watching" -- multi-male-active scenes are NTR-coded unless
145 // the user IS the active POV.
146 let top_flags: i64 = nx_content_guard_scan_bytes(extract.body_buf,
147 extract.body_len)
148 let top_v: i64 = nx_content_guard_verdict(top_flags)
149 if top_v == NX_CG_VERDICT_CONFIRMED {
150 let exp_top: *u8 = sys_mmap(256)
151 let lt: i64 = nx_content_guard_emit_explain(top_flags, exp_top, 256)
152 if lt > 0 { sys_write(1, exp_top, lt) }
153 let tail: *u8 = "# nx_ingest_cli: source refused at top-level guard\n" as *u8
154 sys_write(1, tail, 52)
155 return 0
156 }
157 if top_v == NX_CG_VERDICT_SUSPECT {
158 let exp_top2: *u8 = sys_mmap(256)
159 let lt2: i64 = nx_content_guard_emit_explain(top_flags, exp_top2, 256)
160 if lt2 > 0 { sys_write(1, exp_top2, lt2) }
161 }
162
163 // ---- 3. Ingest body into StoryDb ----
164 let db: *StoryDb = nx_storydb_alloc()
165 let mid: i64 = nx_media_pool_add(db.media_pool, NX_MEDIA_TYPE_TEXT,
166 NX_MEDIA_SRC_USER_PASTE,
167 extract.body_buf, extract.body_len,
168 0 as *u8, 0,
169 0 as *u8, 0,
170 0)
171 if mid < 0 { return 3 }
172 let irc: i64 = nx_text_ingest(db, mid)
173 if irc != NX_TEXT_INGEST_OK {
174 nx_ingest_log("nx_ingest_cli: text_ingest failed\n" as *u8, 35)
175 return 4
176 }
177
178 // ---- 4. Learn vocabulary + co-occurrence ----
179 let reg: *TermRegistry = nx_term_registry_alloc()
180 if nx_term_extract_all(db, reg) != NX_EXTR_OK {
181 nx_ingest_log("nx_ingest_cli: term_extract failed\n" as *u8, 36)
182 return 5
183 }
184 let co: *Cooccur = nx_cooccur_alloc()
185 let _np: i64 = nx_cooccur_observe_db(co, db)
186
187 // ---- 5. Discover scene anchors ----
188 let scratch_buf: *u8 = sys_mmap(32 * NX_COOCCUR_PAIR_BYTES)
189 let scratch: *CooccurPair = scratch_buf as *CooccurPair
190 let anchor_buf: *u8 = sys_mmap(16 * NX_ARC_ANCHOR_BYTES)
191 let anchors: *ArcAnchor = anchor_buf as *ArcAnchor
192 let n_anch: i64 = nx_arc_anchor_discover(co, reg, scratch, 32,
193 anchors, 16)
194
195 // ---- 6. Build scene index ----
196 let idx: *SceneIndex = nx_scene_index_alloc()
197 let _nb: i64 = nx_scene_index_build(idx, db, anchors, n_anch)
198
199 // ---- 6b. Content guard: scan for NTR / multi-male-active / etc. ----
200 //
201 // Per user spec: "i am the dominant actor acting on the scene
202 // not watching". NTR_CONFIRMED candidates get refused inline
203 // and excluded from the POV-shift output. SUSPECT candidates
204 // get a warning header but proceed.
205 let _ng: i64 = nx_content_guard_scan_index(idx, db)
206 var any_confirmed: i64 = 0
207 var any_suspect: i64 = 0
208 var ci_iter: i64 = 0
209 while ci_iter < idx.n_candidates {
210 let c: *SceneCandidate = nx_scene_candidate_at(idx, ci_iter)
211 let v: i64 = nx_content_guard_verdict(c.flags)
212 if v == NX_CG_VERDICT_CONFIRMED { any_confirmed = 1 }
213 if v == NX_CG_VERDICT_SUSPECT { any_suspect = 1 }
214 ci_iter = ci_iter + 1
215 }
216
217 if any_confirmed != 0 {
218 let exp_buf: *u8 = sys_mmap(512)
219 // Emit ALL refusal lines (one per CONFIRMED candidate).
220 var ej: i64 = 0
221 while ej < idx.n_candidates {
222 let c2: *SceneCandidate = nx_scene_candidate_at(idx, ej)
223 let v2: i64 = nx_content_guard_verdict(c2.flags)
224 if v2 == NX_CG_VERDICT_CONFIRMED {
225 let ln: i64 = nx_content_guard_emit_explain(c2.flags,
226 exp_buf, 512)
227 if ln > 0 { sys_write(1, exp_buf, ln) }
228 }
229 ej = ej + 1
230 }
231 let tail: *u8 = "# nx_ingest_cli: at least one CONFIRMED candidate; POV body suppressed\n" as *u8
232 sys_write(1, tail, 72)
233 return 0
234 }
235
236 if any_suspect != 0 {
237 let exp_buf2: *u8 = sys_mmap(512)
238 var ek: i64 = 0
239 while ek < idx.n_candidates {
240 let c3: *SceneCandidate = nx_scene_candidate_at(idx, ek)
241 let v3: i64 = nx_content_guard_verdict(c3.flags)
242 if v3 == NX_CG_VERDICT_SUSPECT {
243 let ln2: i64 = nx_content_guard_emit_explain(c3.flags,
244 exp_buf2, 512)
245 if ln2 > 0 { sys_write(1, exp_buf2, ln2) }
246 }
247 ek = ek + 1
248 }
249 }
250
251 // ---- 7. POV-shift the FULL body text ----
252 //
253 // V1: shift the whole body in one pass. The companion
254 // surfacer later picks per-scene candidates and emits them in
255 // the chosen format. Stdout is the shifted full text so
256 // the user sees the literotica-style rewrite immediately.
257 let out_buf: *u8 = sys_mmap(NX_INGEST_OUTPUT_CAP)
258 let out_n: i64 = nx_pov_shift(NX_INGEST_DEFAULT_GENDER,
259 extract.body_buf, extract.body_len,
260 out_buf, NX_INGEST_OUTPUT_CAP)
261 if out_n <= 0 {
262 nx_ingest_log("nx_ingest_cli: pov_shift produced empty output\n" as *u8, 48)
263 return 6
264 }
265
266 // ---- 8. Emit ----
267 //
268 // Header line (#) so downstream tools can split logical sections
269 // when we extend output with anchor metadata or RAW_JSON.
270 let hdr: *u8 = "# nx_ingest_cli: POV-shifted body (gender=he->I)\n" as *u8
271 nx_ingest_write_stdout(hdr, 50)
272 nx_ingest_write_stdout(out_buf, out_n)
273 nx_ingest_write_stdout("\n" as *u8, 1)
274
275 // Anchor summary to stderr so it doesn't pollute the captured
276 // POV-shifted text but the user still sees what got discovered.
277 nx_ingest_log("# anchors discovered: " as *u8, 22)
278 // Tiny int-to-decimal for n_anch (max 16 anchors).
279 let nb_buf: *u8 = sys_mmap(8)
280 if n_anch == 0 { nb_buf[0] = 0x30; sys_write(2, nb_buf, 1) }
281 if n_anch > 0 {
282 var x: i64 = n_anch
283 var dn: i64 = 0
284 while x > 0 {
285 nb_buf[dn] = 0x30 + (x - ((x / 10) * 10))
286 dn = dn + 1
287 x = x / 10
288 }
289 var di: i64 = dn - 1
290 while di >= 0 {
291 sys_write(2, ((nb_buf as i64) + di) as *u8, 1)
292 di = di - 1
293 }
294 }
295 nx_ingest_log("\n" as *u8, 1)
296
297 return 0
298}