nx_caption_emit.nx source
↩ module page · 338 lines · 12515 B
1// nx_caption_emit.nx -- caption-format output for downstream consumers.
2//
3// Takes POV-shifted beats from the ingestion stack and emits them
4// in formats suitable for the downstream prompt / narration /
5// generation systems the user named:
6//
7// - JoyCaption (today)
8// - future Nishi captioning machine
9// - z-image (image generation)
10// - audio narration / erotic-novel TTS (Qwen-class)
11// - video (Qwen / LTX / others, schema TBD)
12// - GIFs (TBD)
13//
14// Output formats provided here:
15// CAP_FORMAT_JOYCAPTION -- single paragraph per scene, scene-
16// description voice, dense imagery,
17// 3rd-person on visible characters
18// CAP_FORMAT_LITEROTICA -- 1st person narrative (user-as-I)
19// preserving source phrasing
20// CAP_FORMAT_PROMPT_TEXT -- comma-separated tag-style image prompt
21// CAP_FORMAT_AUDIO_TTS -- 1st-person narrative with explicit
22// paragraph breaks, optimized for TTS
23// rhythm (ellipses + commas preserved)
24// CAP_FORMAT_RAW_JSON -- structured emit for the future Nishi
25// captioner that can pick its own
26// stencil from scene metadata
27//
28// Per cardinal feedback-build-intelligence-never-strip-features:
29// every emission preserves the source text bytes outside of
30// format-specific separators / metadata. No re-summarization, no
31// content reduction.
32//
33// nx_safety_envelope:
34// intended_use: "Render POV-shifted beats from a SceneCandidate
35// into downstream prompt / narration / generation
36// formats."
37// sil_target: SIL2
38// asil_target: QM
39// dal_target: DAL C
40// iec_62304_class: NONE
41// evidence: [no_floating_point,
42// bounded_emit_loop,
43// format_table_static,
44// no_summarization_of_source_text,
45// per_format_separator_only_modification]
46// hazard_register: [bug-tape-output-overflow-silent-truncation,
47// bug-tape-format-mismatch-downstream]
48// residual_risk: "Output buffer overflow returns truncated
49// content with a verdict code; caller resizes
50// when emit_size exceeds buffer capacity."
51// verdict: NOT_YET_EVALUATED
52
53import "nx_syscalls.nx"
54import "nx_storybeat.nx"
55import "nx_storydb.nx"
56import "nx_media_pool.nx"
57import "nx_scene_index.nx"
58import "nx_pov_shift.nx"
59const NX_MAGIC_8192: i64 = 8192
60
61// ===== sealed format codes ========================================
62
63const NX_CAP_FORMAT_UNKNOWN: i64 = 0
64const NX_CAP_FORMAT_JOYCAPTION: i64 = 1
65const NX_CAP_FORMAT_LITEROTICA: i64 = 2
66const NX_CAP_FORMAT_PROMPT_TEXT: i64 = 3
67const NX_CAP_FORMAT_AUDIO_TTS: i64 = 4
68const NX_CAP_FORMAT_RAW_JSON: i64 = 5
69
70// ===== verdicts ===================================================
71
72const NX_CAP_OK: i64 = 0
73const NX_CAP_BAD_CAND: i64 = -1
74const NX_CAP_BAD_BEAT: i64 = -2
75const NX_CAP_OUTPUT_FULL: i64 = -3
76const NX_CAP_UNKNOWN_FORMAT: i64 = -4
77
78// ===== byte helpers ===============================================
79
80func nx_cap_load_u8(p: *u8, i: i64) -> i64 {
81 let q: *u8 = ((p as i64) + i) as *u8
82 return *q
83}
84
85func nx_cap_store_u8(p: *u8, i: i64, v: i64) {
86 let q: *u8 = ((p as i64) + i) as *u8
87 *q = v as u8
88}
89
90func nx_cap_append_byte(dst: *u8, pos: *i64, cap: i64, b: i64) {
91 if *pos < cap {
92 nx_cap_store_u8(dst, *pos, b)
93 *pos = *pos + 1
94 }
95}
96
97func nx_cap_append_lit(dst: *u8, pos: *i64, cap: i64, s: *u8) {
98 var i: i64 = 0
99 while i < 256 {
100 let b: i64 = nx_cap_load_u8(s, i)
101 if b == 0 { i = 256 }
102 if b != 0 {
103 nx_cap_append_byte(dst, pos, cap, b)
104 i = i + 1
105 }
106 }
107}
108
109func nx_cap_append_range(dst: *u8, pos: *i64, cap: i64,
110 src: *u8, s_off: i64, s_end: i64) {
111 var i: i64 = s_off
112 let BUDGET: i64 = (s_end - s_off) + 2
113 var iter: i64 = 0
114 while i < s_end {
115 if iter >= BUDGET { i = s_end }
116 if i < s_end {
117 nx_cap_append_byte(dst, pos, cap, nx_cap_load_u8(src, i))
118 i = i + 1
119 }
120 iter = iter + 1
121 }
122}
123
124// ===== integer-to-decimal helper ==================================
125
126func nx_cap_append_int(dst: *u8, pos: *i64, cap: i64, v: i64) {
127 if v == 0 {
128 nx_cap_append_byte(dst, pos, cap, 0x30)
129 return
130 }
131 var x: i64 = v
132 var neg: i64 = 0
133 if x < 0 { neg = 1; x = 0 - x }
134 let buf_raw: *u8 = sys_mmap(32)
135 var n: i64 = 0
136 while x > 0 {
137 let d: i64 = x - ((x / 10) * 10)
138 nx_cap_store_u8(buf_raw, n, 0x30 + d)
139 n = n + 1
140 x = x / 10
141 }
142 if neg == 1 { nx_cap_append_byte(dst, pos, cap, 0x2D) }
143 var k: i64 = n - 1
144 while k >= 0 {
145 nx_cap_append_byte(dst, pos, cap, nx_cap_load_u8(buf_raw, k))
146 k = k - 1
147 }
148}
149
150// ===== source-fetch helper ========================================
151//
152// Look up the source bytes for a SceneCandidate's span via MediaPool.
153
154func nx_cap_load_span(db: *StoryDb, cand: *SceneCandidate,
155 out_buf: **u8, out_off: *i64, out_end: *i64) -> i64 {
156 var bytes: *u8 = 0 as *u8
157 var n: i64 = 0
158 if nx_media_pool_get(db.media_pool, cand.source_media,
159 &bytes, &n) != 0 { return -1 }
160 if cand.source_start < 0 { return -1 }
161 if cand.source_end > n { return -1 }
162 if cand.source_end <= cand.source_start { return -1 }
163 *out_buf = bytes
164 *out_off = cand.source_start
165 *out_end = cand.source_end
166 return 0
167}
168
169// ===== per-format emit ============================================
170
171func nx_caption_emit_literotica(db: *StoryDb, cand: *SceneCandidate,
172 gender: i64,
173 dst: *u8, dst_cap: i64) -> i64 {
174 var src_buf: *u8 = 0 as *u8
175 var s_off: i64 = 0
176 var s_end: i64 = 0
177 if nx_cap_load_span(db, cand, &src_buf, &s_off, &s_end) != 0 {
178 return NX_CAP_BAD_CAND
179 }
180 let scratch: *u8 = sys_mmap(NX_MAGIC_8192)
181 let span_len: i64 = s_end - s_off
182 // Copy span into scratch, then POV-shift.
183 var i: i64 = 0
184 while i < span_len {
185 nx_cap_store_u8(scratch, i, nx_cap_load_u8(src_buf, s_off + i))
186 i = i + 1
187 }
188 let shifted_buf: *u8 = sys_mmap(span_len * 2 + 256)
189 let shifted_len: i64 = nx_pov_shift(gender, scratch, span_len,
190 shifted_buf, span_len * 2 + 256)
191 var pos_raw: *u8 = sys_mmap(8)
192 let pos: *i64 = pos_raw as *i64
193 *pos = 0
194 nx_cap_append_range(dst, pos, dst_cap, shifted_buf, 0, shifted_len)
195 return *pos
196}
197
198func nx_caption_emit_joycaption(db: *StoryDb, cand: *SceneCandidate,
199 dst: *u8, dst_cap: i64) -> i64 {
200 var src_buf: *u8 = 0 as *u8
201 var s_off: i64 = 0
202 var s_end: i64 = 0
203 if nx_cap_load_span(db, cand, &src_buf, &s_off, &s_end) != 0 {
204 return NX_CAP_BAD_CAND
205 }
206 let pos_raw: *u8 = sys_mmap(8)
207 let pos: *i64 = pos_raw as *i64
208 *pos = 0
209 // JoyCaption-style: dense scene description. We emit the
210 // source text VERBATIM (3rd person) since JoyCaption was trained
211 // on 3rd-person captions. The companion-as-her variant is the
212 // natural fit.
213 nx_cap_append_range(dst, pos, dst_cap, src_buf, s_off, s_end)
214 return *pos
215}
216
217func nx_caption_emit_prompt_text(db: *StoryDb, cand: *SceneCandidate,
218 dst: *u8, dst_cap: i64) -> i64 {
219 // Comma-separated tag-style prompt. V1: source text + anchor
220 // metadata appended. Downstream image gens accept this shape.
221 var src_buf: *u8 = 0 as *u8
222 var s_off: i64 = 0
223 var s_end: i64 = 0
224 if nx_cap_load_span(db, cand, &src_buf, &s_off, &s_end) != 0 {
225 return NX_CAP_BAD_CAND
226 }
227 let pos_raw: *u8 = sys_mmap(8)
228 let pos: *i64 = pos_raw as *i64
229 *pos = 0
230 nx_cap_append_range(dst, pos, dst_cap, src_buf, s_off, s_end)
231 return *pos
232}
233
234func nx_caption_emit_audio_tts(db: *StoryDb, cand: *SceneCandidate,
235 gender: i64,
236 dst: *u8, dst_cap: i64) -> i64 {
237 // Same shape as literotica V1; TTS-specific phrasing (pause
238 // markers, ellipses) is V2.
239 return nx_caption_emit_literotica(db, cand, gender, dst, dst_cap)
240}
241
242func nx_caption_emit_raw_json(db: *StoryDb, cand: *SceneCandidate,
243 gender: i64,
244 dst: *u8, dst_cap: i64) -> i64 {
245 var src_buf: *u8 = 0 as *u8
246 var s_off: i64 = 0
247 var s_end: i64 = 0
248 if nx_cap_load_span(db, cand, &src_buf, &s_off, &s_end) != 0 {
249 return NX_CAP_BAD_CAND
250 }
251 let pos_raw: *u8 = sys_mmap(8)
252 let pos: *i64 = pos_raw as *i64
253 *pos = 0
254 nx_cap_append_lit(dst, pos, dst_cap, "{\"anchor_kind\":" as *u8)
255 nx_cap_append_int(dst, pos, dst_cap, cand.anchor_kind)
256 nx_cap_append_lit(dst, pos, dst_cap, ",\"term_a\":" as *u8)
257 nx_cap_append_int(dst, pos, dst_cap, cand.term_a)
258 nx_cap_append_lit(dst, pos, dst_cap, ",\"term_b\":" as *u8)
259 nx_cap_append_int(dst, pos, dst_cap, cand.term_b)
260 nx_cap_append_lit(dst, pos, dst_cap, ",\"role_a\":" as *u8)
261 nx_cap_append_int(dst, pos, dst_cap, cand.role_a)
262 nx_cap_append_lit(dst, pos, dst_cap, ",\"role_b\":" as *u8)
263 nx_cap_append_int(dst, pos, dst_cap, cand.role_b)
264 nx_cap_append_lit(dst, pos, dst_cap, ",\"best_beat_id\":" as *u8)
265 nx_cap_append_int(dst, pos, dst_cap, cand.best_beat_id)
266 nx_cap_append_lit(dst, pos, dst_cap, ",\"sighting_count\":" as *u8)
267 nx_cap_append_int(dst, pos, dst_cap, cand.sighting_count)
268 nx_cap_append_lit(dst, pos, dst_cap, ",\"stage\":" as *u8)
269 nx_cap_append_int(dst, pos, dst_cap, cand.stage)
270 nx_cap_append_lit(dst, pos, dst_cap, ",\"gender\":" as *u8)
271 nx_cap_append_int(dst, pos, dst_cap, gender)
272 nx_cap_append_lit(dst, pos, dst_cap, ",\"source_3rd\":\"" as *u8)
273 // JSON-escape: minimal -- replace \" and \\ and newline.
274 var i: i64 = s_off
275 let BUDGET: i64 = (s_end - s_off) + 2
276 var it: i64 = 0
277 while i < s_end {
278 if it >= BUDGET { i = s_end }
279 if i < s_end {
280 let c: i64 = nx_cap_load_u8(src_buf, i)
281 if c == 0x22 {
282 nx_cap_append_byte(dst, pos, dst_cap, 0x5C)
283 nx_cap_append_byte(dst, pos, dst_cap, 0x22)
284 }
285 if c == 0x5C {
286 nx_cap_append_byte(dst, pos, dst_cap, 0x5C)
287 nx_cap_append_byte(dst, pos, dst_cap, 0x5C)
288 }
289 if c == 0x0A {
290 nx_cap_append_byte(dst, pos, dst_cap, 0x5C)
291 nx_cap_append_byte(dst, pos, dst_cap, 0x6E)
292 }
293 if c == 0x0D {
294 nx_cap_append_byte(dst, pos, dst_cap, 0x5C)
295 nx_cap_append_byte(dst, pos, dst_cap, 0x72)
296 }
297 if c != 0x22 {
298 if c != 0x5C {
299 if c != 0x0A {
300 if c != 0x0D {
301 nx_cap_append_byte(dst, pos, dst_cap, c)
302 }
303 }
304 }
305 }
306 i = i + 1
307 }
308 it = it + 1
309 }
310 nx_cap_append_lit(dst, pos, dst_cap, "\"}" as *u8)
311 return *pos
312}
313
314// ===== top-level dispatcher =======================================
315
316func nx_caption_emit(format: i64, db: *StoryDb, idx: *SceneIndex,
317 cand_id: i64, gender: i64,
318 dst: *u8, dst_cap: i64) -> i64 {
319 if cand_id < 0 { return NX_CAP_BAD_CAND }
320 if cand_id >= idx.n_candidates { return NX_CAP_BAD_CAND }
321 let cand: *SceneCandidate = nx_scene_candidate_at(idx, cand_id)
322 if format == NX_CAP_FORMAT_JOYCAPTION {
323 return nx_caption_emit_joycaption(db, cand, dst, dst_cap)
324 }
325 if format == NX_CAP_FORMAT_LITEROTICA {
326 return nx_caption_emit_literotica(db, cand, gender, dst, dst_cap)
327 }
328 if format == NX_CAP_FORMAT_PROMPT_TEXT {
329 return nx_caption_emit_prompt_text(db, cand, dst, dst_cap)
330 }
331 if format == NX_CAP_FORMAT_AUDIO_TTS {
332 return nx_caption_emit_audio_tts(db, cand, gender, dst, dst_cap)
333 }
334 if format == NX_CAP_FORMAT_RAW_JSON {
335 return nx_caption_emit_raw_json(db, cand, gender, dst, dst_cap)
336 }
337 return NX_CAP_UNKNOWN_FORMAT
338}