nx_response_signal.nx source
↩ module page · 170 lines · 6247 B
1// nx_response_signal.nx -- arousal response signals for eroticism ladder.
2//
3// The eroticism rank 10 (ACTIVE_PENETRATION_WITH_RESPONSE) requires
4// the substrate to detect VISIBLE arousal response signals beyond just
5// pose. Three independent axes:
6//
7// parted_lips — dark region between upper/lower lip in mid-face
8// flushed_skin — R-elevation on cheek region vs torso baseline
9// skin_sheen — moisture/sweat sheen above baseline specular
10//
11// Combined: a count 0..3 of which signals are present. Feeds the
12// response_count input of nx_eroticism_ladder.
13//
14// Output flat-array:
15// result[0] = parted_lips_present (0/1)
16// result[1] = flushed_skin_present (0/1)
17// result[2] = skin_sheen_present (0/1)
18// result[3] = signal_count (sum)
19// result[4] = parted_dark_pixels (raw)
20// result[5] = cheek_R_minus_torso_R (delta, signed)
21// result[6] = sheen_bright_pixel_pct (x100)
22
23// nx_safety_envelope:
24// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
25// sil_target: SIL1
26// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
27// verdict: NOT_YET_EVALUATED
28
29import "nx_syscalls.nx"
30import "nx_image.nx"
31
32const NX_RESP_RES_PARTED: i64 = 0
33const NX_RESP_RES_FLUSHED: i64 = 1
34const NX_RESP_RES_SHEEN: i64 = 2
35const NX_RESP_RES_COUNT: i64 = 3
36const NX_RESP_RES_PARTED_DARK: i64 = 4
37const NX_RESP_RES_CHEEK_DELTA: i64 = 5
38const NX_RESP_RES_SHEEN_PCT: i64 = 6
39const NX_RESP_RES_FIELDS: i64 = 7
40
41// Thresholds.
42const NX_RESP_PARTED_DARK_MIN: i64 = 12 // need ≥12 dark pixels in mouth band
43const NX_RESP_FLUSHED_R_DELTA_MIN: i64 = 15 // cheek R must be ≥ 15 above torso R baseline
44const NX_RESP_SHEEN_PCT_MIN: i64 = 8 // ≥ 8% bright (Y > 220) in skin
45
46// Detect a dark region (mouth opening) in the middle horizontal band of
47// the face bbox. Returns the count of pixels with luminance < 60.
48func nx_resp_count_dark_in_mouth_band(rgb: *Image,
49 face_min_x: i64, face_min_y: i64,
50 face_max_x: i64, face_max_y: i64) -> i64 {
51 let face_h: i64 = face_max_y - face_min_y + 1
52 // Mouth band: ~70% to ~85% down the face.
53 let y0: i64 = face_min_y + (face_h * 70) / 100
54 let y1: i64 = face_min_y + (face_h * 85) / 100
55 var count: i64 = 0
56 var y: i64 = y0
57 while y < y1 {
58 if y >= 0 { if y < rgb.height {
59 var x: i64 = face_min_x
60 while x <= face_max_x {
61 if x >= 0 { if x < rgb.width {
62 let r: i64 = nx_image_get(rgb, x, y, 0)
63 let g: i64 = nx_image_get(rgb, x, y, 1)
64 let bch: i64 = nx_image_get(rgb, x, y, 2)
65 let yv: i64 = (77 * r + 150 * g + 29 * bch) / 256
66 if yv < 60 { count = count + 1 }
67 } }
68 x = x + 1
69 }
70 } }
71 y = y + 1
72 }
73 return count
74}
75
76// Mean R channel in a rectangular region.
77func nx_resp_mean_r(rgb: *Image, x0: i64, y0: i64, x1: i64, y1: i64) -> i64 {
78 var sum: i64 = 0
79 var n: i64 = 0
80 var y: i64 = y0
81 while y < y1 {
82 if y >= 0 { if y < rgb.height {
83 var x: i64 = x0
84 while x < x1 {
85 if x >= 0 { if x < rgb.width {
86 sum = sum + nx_image_get(rgb, x, y, 0)
87 n = n + 1
88 } }
89 x = x + 1
90 }
91 } }
92 y = y + 1
93 }
94 if n == 0 { return 0 }
95 return sum / n
96}
97
98// Percent of skin-mask pixels with luminance > 220 (sheen).
99func nx_resp_sheen_pct(rgb: *Image, mask: *Image) -> i64 {
100 let w: i64 = rgb.width
101 let h: i64 = rgb.height
102 var bright: i64 = 0
103 var total: i64 = 0
104 var y: i64 = 0
105 while y < h {
106 var x: i64 = 0
107 while x < w {
108 if nx_image_get(mask, x, y, 0) > 0 {
109 total = total + 1
110 let r: i64 = nx_image_get(rgb, x, y, 0)
111 let g: i64 = nx_image_get(rgb, x, y, 1)
112 let bch: i64 = nx_image_get(rgb, x, y, 2)
113 let yv: i64 = (77 * r + 150 * g + 29 * bch) / 256
114 if yv > 220 { bright = bright + 1 }
115 }
116 x = x + 1
117 }
118 y = y + 1
119 }
120 if total == 0 { return 0 }
121 return (bright * 100) / total
122}
123
124// Compute response signals.
125// bbox_pack layout (i64 array, 12 entries):
126// [0..3] face x0, y0, x1, y1
127// [4..7] cheek x0, y0, x1, y1
128// [8..11] torso x0, y0, x1, y1
129// Args reduced to 4 to avoid nxc2 codegen bug with high-arity functions.
130func nx_response_signals(rgb: *Image, skin_mask: *Image,
131 bbox_pack: *i64, result: *i64) -> i64 {
132 var i: i64 = 0
133 while i < NX_RESP_RES_FIELDS { result[i] = 0; i = i + 1 }
134
135 let face_x0: i64 = bbox_pack[0]
136 let face_y0: i64 = bbox_pack[1]
137 let face_x1: i64 = bbox_pack[2]
138 let face_y1: i64 = bbox_pack[3]
139 let cheek_x0: i64 = bbox_pack[4]
140 let cheek_y0: i64 = bbox_pack[5]
141 let cheek_x1: i64 = bbox_pack[6]
142 let cheek_y1: i64 = bbox_pack[7]
143 let torso_x0: i64 = bbox_pack[8]
144 let torso_y0: i64 = bbox_pack[9]
145 let torso_x1: i64 = bbox_pack[10]
146 let torso_y1: i64 = bbox_pack[11]
147
148 let dark: i64 = nx_resp_count_dark_in_mouth_band(rgb, face_x0, face_y0, face_x1, face_y1)
149 result[NX_RESP_RES_PARTED_DARK] = dark
150 var parted: i64 = 0
151 if dark >= NX_RESP_PARTED_DARK_MIN { parted = 1 }
152 result[NX_RESP_RES_PARTED] = parted
153
154 let cheek_r: i64 = nx_resp_mean_r(rgb, cheek_x0, cheek_y0, cheek_x1, cheek_y1)
155 let torso_r: i64 = nx_resp_mean_r(rgb, torso_x0, torso_y0, torso_x1, torso_y1)
156 let delta: i64 = cheek_r - torso_r
157 result[NX_RESP_RES_CHEEK_DELTA] = delta
158 var flushed: i64 = 0
159 if delta >= NX_RESP_FLUSHED_R_DELTA_MIN { flushed = 1 }
160 result[NX_RESP_RES_FLUSHED] = flushed
161
162 let pct: i64 = nx_resp_sheen_pct(rgb, skin_mask)
163 result[NX_RESP_RES_SHEEN_PCT] = pct
164 var sheen: i64 = 0
165 if pct >= NX_RESP_SHEEN_PCT_MIN { sheen = 1 }
166 result[NX_RESP_RES_SHEEN] = sheen
167
168 result[NX_RESP_RES_COUNT] = parted + flushed + sheen
169 return 0
170}