code wiki / (root) / nx_caption_emit.nx

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}