code wiki / (root) / nx_storybeat.nx

nx_storybeat.nx source

↩ module page · 237 lines · 9196 B

1// nx_storybeat.nx -- media-agnostic narrative beat. 2// 3// A StoryBeat is the atomic unit of a parsed narrative across ANY 4// source medium. The downstream director pipeline (nx_arc / 5// nx_moment_resolve / nx_directors_note) consumes a stream of 6// StoryBeats and does not care whether they came from text prose, 7// a video transcript, a comic-panel image, or a GIF sequence. 8// 9// Each per-medium ingester (nx_text_ingest, nx_image_ingest, 10// nx_gif_ingest, nx_video_ingest, nx_audio_ingest) emits beats 11// into a StoryDb. A beat always points back into the MediaPool 12// for full-resolution playback / re-extraction. 13// 14// Schema is FROZEN below this line per cardinal feedback-bug-class- 15// prevention-additive-not-restrictive: append-only fields, never 16// renumber or reorder existing ones. Downstream consumers index 17// by field offset. 18// 19// See: docs/NISHI_MEDIA_INGESTION_ROADMAP.md 20// 21// nx_safety_envelope: 22// intended_use: "Schema and helpers for the atomic narrative 23// beat unit consumed by the director pipeline." 24// sil_target: SIL2 25// asil_target: QM 26// dal_target: DAL C 27// iec_62304_class: NONE 28// evidence: [no_floating_point, 29// fixed_struct_size_128_bytes, 30// sealed_enum_beat_kind, 31// sealed_enum_domain_code, 32// sealed_enum_consent_tier, 33// media_id_references_pool_no_aliasing] 34// hazard_register: [bug-tape-beat-kind-collision, 35// bug-tape-affect-pack-overflow] 36// residual_risk: "Affect axes packed i16 each; values outside 37// [-32768,32767] silently saturate. Caller 38// uses Q10 fixed-point so saturation is safe." 39// verdict: NOT_YET_EVALUATED 40 41import "nx_syscalls.nx" 42 43// ===== sealed beat kinds =========================================== 44// 45// What this beat REPRESENTS. Stays stable forever. Append new 46// values at the end; never renumber. 47 48const NX_BEAT_UNKNOWN: i64 = 0 49const NX_BEAT_NARRATION: i64 = 1 // 3rd-person prose narration 50const NX_BEAT_DIALOGUE: i64 = 2 // quoted speech 51const NX_BEAT_ACTION: i64 = 3 // verb-led action sentence 52const NX_BEAT_DESCRIPTION: i64 = 4 // setting / appearance description 53const NX_BEAT_TRANSITION: i64 = 5 // time / scene break ("Hours later") 54const NX_BEAT_HEADING: i64 = 6 // chapter / scene header line 55const NX_BEAT_MEDIA_FRAME: i64 = 7 // single frame from video / gif / image 56const NX_BEAT_MEDIA_CAPTION: i64 = 8 // ASR / OCR caption from audio / image 57const NX_BEAT_INTERIORITY: i64 = 9 // 1st-person inner thought (italics) 58const NX_BEAT_GESTURE: i64 = 10 // non-verbal body / face expression 59 60// ===== sealed domain codes ========================================= 61// 62// Coarse content domain. Used by nx_consent_boundary and the 63// director-initiative gating logic. 64 65const NX_DOMAIN_UNKNOWN: i64 = 0 66const NX_DOMAIN_MUNDANE: i64 = 1 // breakfast, work, errands 67const NX_DOMAIN_INTIMATE: i64 = 2 // kiss / hug / cuddle, no explicit 68const NX_DOMAIN_EROTIC: i64 = 3 // sexual content, consenting adults 69const NX_DOMAIN_FANTASY: i64 = 4 // fantasy / sci-fi setting tag 70const NX_DOMAIN_VIOLENCE: i64 = 5 // physical violence (combat / injury) 71 72// ===== sealed consent tiers ======================================== 73// 74// What did the source consent to being used for? The user's own 75// inputs are at the most-permissive tier; third-party ingestions 76// are constrained. 77 78const NX_CONSENT_UNKNOWN: i64 = 0 79const NX_CONSENT_PUBLIC: i64 = 1 // public-domain / CC0 80const NX_CONSENT_PRIVATE_OK: i64 = 2 // user-owned, private use only 81const NX_CONSENT_EROTIC_OK: i64 = 3 // user explicitly tagged for erotic 82const NX_CONSENT_EXPLICIT: i64 = 4 // explicit user authorization granted 83 84// ===== sealed arc stage hints ====================================== 85// 86// Mirrors nx_arc.nx's 16-stage data model. This is a HINT only 87// (the arc fitter can override). Matched by ordinal to keep the 88// two enums in lock-step; if you add a stage to nx_arc, mirror here. 89 90const NX_ARCSTAGE_UNKNOWN: i64 = 0 91const NX_ARCSTAGE_SETUP: i64 = 1 92const NX_ARCSTAGE_HOOK: i64 = 2 93const NX_ARCSTAGE_TRIGGER: i64 = 3 94const NX_ARCSTAGE_ENGAGE: i64 = 4 95const NX_ARCSTAGE_RISING: i64 = 5 96const NX_ARCSTAGE_OBSTACLE: i64 = 6 97const NX_ARCSTAGE_TURN: i64 = 7 98const NX_ARCSTAGE_DEEPEN: i64 = 8 99const NX_ARCSTAGE_BUILD: i64 = 9 100const NX_ARCSTAGE_PEAK: i64 = 10 101const NX_ARCSTAGE_CLIMAX: i64 = 11 102const NX_ARCSTAGE_BREATH: i64 = 12 103const NX_ARCSTAGE_AFTER: i64 = 13 104const NX_ARCSTAGE_RESOLVE: i64 = 14 105const NX_ARCSTAGE_COOL: i64 = 15 106 107// ===== bit flags =================================================== 108 109const NX_BEAT_FLAG_IS_QUOTED: i64 = 1 // text was inside "..." 110const NX_BEAT_FLAG_IS_ITALIC: i64 = 2 // interiority styling 111const NX_BEAT_FLAG_HAS_SUMMARY: i64 = 4 // summary_off/len valid 112const NX_BEAT_FLAG_USER_PINNED: i64 = 8 // user starred this beat 113const NX_BEAT_FLAG_DIRECTOR_CUE: i64 = 16 // marked for director init 114 115// ===== StoryBeat struct ============================================ 116// 117// Fixed 128 bytes (16 * i64). Schema FROZEN; append-only. 118 119struct StoryBeat { 120 beat_kind: i64, // NX_BEAT_* 121 media_type: i64, // NX_MEDIA_TYPE_* (mirror nx_media_pool) 122 media_id: i64, // index into MediaPool 123 124 // Span within source media. Semantics depend on media_type: 125 // TEXT/HTML : byte offset into UTF-8 bytes 126 // IMAGE : pixel (y * width + x), span_end - span_start = pixel count 127 // GIF : frame index 128 // VIDEO : video PTS in 90 kHz ticks (MPEG standard) 129 // AUDIO : sample index at source sample rate 130 span_start: i64, 131 span_end: i64, 132 133 location_id: i64, // 0 = unknown / not extracted 134 outfit_id: i64, 135 character_id: i64, 136 intent_id: i64, 137 138 domain_code: i64, // NX_DOMAIN_* 139 consent_tier: i64, // NX_CONSENT_* 140 arc_stage_hint: i64, // NX_ARCSTAGE_* 141 142 affect_packed: i64, // 4x i16 packed: arousal, tenderness, 143 // urgency, intimacy (Q10) 144 flags: i64, // NX_BEAT_FLAG_* 145 146 summary_off: i64, // offset into StoryDb summary arena 147 summary_len: i64, // 0 if no synopsis attached 148} 149 150const NX_STORYBEAT_BYTES: i64 = 128 151 152// ===== affect packing ============================================== 153// 154// Pack 4 i16 lanes (Q10 fixed-point) into one i64. Layout: 155// bits 0..15 : arousal 156// bits 16..31 : tenderness 157// bits 32..47 : urgency 158// bits 48..63 : intimacy 159// 160// Values outside i16 range silently saturate. Caller uses Q10 161// so the safe input range is [-32, 32] which fits well. 162 163func nx_beat_pack_affect(arousal: i64, tenderness: i64, 164 urgency: i64, intimacy: i64) -> i64 { 165 let a: i64 = arousal & 0xFFFF 166 let b: i64 = tenderness & 0xFFFF 167 let c: i64 = urgency & 0xFFFF 168 let d: i64 = intimacy & 0xFFFF 169 return a | (b << 16) | (c << 32) | (d << 48) 170} 171 172func nx_beat_affect_arousal(packed: i64) -> i64 { 173 let v: i64 = packed & 0xFFFF 174 if v >= 0x8000 { return v - 0x10000 } 175 return v 176} 177 178func nx_beat_affect_tenderness(packed: i64) -> i64 { 179 let v: i64 = (packed >> 16) & 0xFFFF 180 if v >= 0x8000 { return v - 0x10000 } 181 return v 182} 183 184func nx_beat_affect_urgency(packed: i64) -> i64 { 185 let v: i64 = (packed >> 32) & 0xFFFF 186 if v >= 0x8000 { return v - 0x10000 } 187 return v 188} 189 190func nx_beat_affect_intimacy(packed: i64) -> i64 { 191 let v: i64 = (packed >> 48) & 0xFFFF 192 if v >= 0x8000 { return v - 0x10000 } 193 return v 194} 195 196// ===== beat constructor / mutator helpers ========================== 197 198func nx_beat_init(b: *StoryBeat, beat_kind: i64, media_type: i64, 199 media_id: i64, span_start: i64, span_end: i64) { 200 b.beat_kind = beat_kind 201 b.media_type = media_type 202 b.media_id = media_id 203 b.span_start = span_start 204 b.span_end = span_end 205 b.location_id = 0 206 b.outfit_id = 0 207 b.character_id = 0 208 b.intent_id = 0 209 b.domain_code = NX_DOMAIN_UNKNOWN 210 b.consent_tier = NX_CONSENT_UNKNOWN 211 b.arc_stage_hint = NX_ARCSTAGE_UNKNOWN 212 b.affect_packed = 0 213 b.flags = 0 214 b.summary_off = 0 215 b.summary_len = 0 216} 217 218func nx_beat_set_affect(b: *StoryBeat, arousal: i64, tenderness: i64, 219 urgency: i64, intimacy: i64) { 220 b.affect_packed = nx_beat_pack_affect(arousal, tenderness, 221 urgency, intimacy) 222} 223 224func nx_beat_set_summary(b: *StoryBeat, off: i64, len: i64) { 225 b.summary_off = off 226 b.summary_len = len 227 b.flags = b.flags | NX_BEAT_FLAG_HAS_SUMMARY 228} 229 230func nx_beat_mark_director_cue(b: *StoryBeat) { 231 b.flags = b.flags | NX_BEAT_FLAG_DIRECTOR_CUE 232} 233 234func nx_beat_has_director_cue(b: *StoryBeat) -> i64 { 235 if (b.flags & NX_BEAT_FLAG_DIRECTOR_CUE) != 0 { return 1 } 236 return 0 237}