nx_tone.nx source
↩ module page · 155 lines · 6294 B
1// nx_tone.nx -- WRITING arc, rung W-TONE-1: the VOICE / TONE FINGERPRINT.
2// Operator intent: "help me write to people in my tone." This organ turns a
3// reference text (the operator's own writing) into a deterministic, length-
4// invariant STYLE VECTOR, and scores how close any candidate draft sits to it --
5// so a generated draft can be checked/steered toward "my voice".
6//
7// Pure integer, NO floats, NO syscalls, NO own buffers (the nx_note shape, so any
8// lane -- tutor, local-LLM, daemon -- can call it). Reuses nx_writecraft
9// (counts) + nx_writestyle (prose signals) -- DRY, never reimplemented.
10//
11// A FINGERPRINT is TN_DIMS dims, each normalized to PER-1000 so a 40-word note
12// and a 40k-word corpus compare directly (the key to honest matching):
13// tf[0] avg sentence length (words *1000 / sentences) -- cadence
14// tf[1] avg word length letters (letters *1000 / words) -- diction weight
15// tf[2] syllables per word (syll *1000 / words) -- lexical density
16// tf[3] complex-word ratio (complex *1000 / words) -- formality
17// tf[4] adverb ratio (adverbs *1000 / words)
18// tf[5] passive ratio (passive *1000 / words)
19// tf[6] weak/filler ratio (weak *1000 / words)
20// tf[7] dialogue fraction (chars-in-quotes *1000 / chars) -- voice
21// tf[8] hard-sentence ratio (hard *1000 / sentences) -- rhythm
22// tf[9] comma density (commas *1000 / words) -- subordination
23// tf[10] exclamation density (bangs *1000 / sentences) -- emotional pitch
24// tf[11] question density (qmarks *1000 / sentences) -- rhetorical style
25//
26// tn_dist = L1 (sum of abs diffs). tn_closer is a THRESHOLD-FREE comparator
27// (which of two voices a candidate matches) so there is NO magic-number cutoff in
28// the organ; a match THRESHOLD, when one is needed, arrives as operator DATA
29// (rule 11), exactly like the register classifier's threshold.
30//
31// HONEST LIMIT (v1): L1 over raw per-1000 dims is dominated by the high-dynamic-
32// range dims (cadence tf[0] can reach ~30000 while ratio dims sit ~0..400). The
33// gate proves correct voice-matching on contrasting samples; PER-DIMENSION
34// WEIGHTING (operator DATA weights, like register weights) is the next rung.
35//
36// license_tier: ORIGINAL
37// module: nishi-core.write.tone
38// depends: nishi-core.write.craft, nishi-core.write.style
39// capability: WRITE_TONE_FINGERPRINT
40import "nx_writecraft.nx"
41import "nx_writestyle.nx"
42
43const TN_DIMS: i64 = 12
44
45// count commas / exclamations / questions into out3[0..2]
46func tn_punct(t: *u8, n: i64, out3: *i64) -> i64 {
47 var commas: i64 = 0
48 var bangs: i64 = 0
49 var quest: i64 = 0
50 var i: i64 = 0
51 while i < n {
52 let c: i64 = t[i] as i64
53 if c == 44 { commas = commas + 1 } // ,
54 if c == 33 { bangs = bangs + 1 } // !
55 if c == 63 { quest = quest + 1 } // ?
56 i = i + 1
57 }
58 out3[0] = commas
59 out3[1] = bangs
60 out3[2] = quest
61 return 0
62}
63
64// fill tf[0..TN_DIMS-1] (caller allocs tf >= 12 i64); sc = caller scratch >= 8 i64
65// (reused for wc_counts, then ws_signals, then tn_punct). returns the word count.
66func tn_fingerprint(t: *u8, n: i64, tf: *i64, sc: *i64) -> i64 {
67 wc_counts(t, n, sc)
68 var W: i64 = sc[1]
69 var S: i64 = sc[2]
70 let Y: i64 = sc[3]
71 let C: i64 = sc[4]
72 let L: i64 = sc[0]
73 if W < 1 { W = 1 }
74 if S < 1 { S = 1 }
75 tf[0] = (W * 1000) / S
76 tf[1] = (L * 1000) / W
77 tf[2] = (Y * 1000) / W
78 tf[3] = (C * 1000) / W
79 ws_signals(t, n, sc)
80 tf[4] = (sc[0] * 1000) / W // adverbs
81 tf[5] = (sc[1] * 1000) / W // passive
82 tf[6] = (sc[2] * 1000) / W // weak
83 tf[7] = sc[3] // dialogue_milli (already per-1000)
84 tf[8] = (sc[5] * 1000) / S // hard sentences
85 tn_punct(t, n, sc)
86 tf[9] = (sc[0] * 1000) / W // commas
87 tf[10] = (sc[1] * 1000) / S // exclamations
88 tf[11] = (sc[2] * 1000) / S // questions
89 return W
90}
91
92// L1 distance between two fingerprints over k dims (lower = more alike)
93func tn_dist(a: *i64, b: *i64, k: i64) -> i64 {
94 var sum: i64 = 0
95 var i: i64 = 0
96 while i < k {
97 var d: i64 = a[i] - b[i]
98 if d < 0 { d = 0 - d }
99 sum = sum + d
100 i = i + 1
101 }
102 return sum
103}
104
105// 1 if cand is closer to voice a than to voice b (threshold-free), else 0
106func tn_closer(cand: *i64, a: *i64, b: *i64, k: i64) -> i64 {
107 let da: i64 = tn_dist(cand, a, k)
108 let db: i64 = tn_dist(cand, b, k)
109 if da < db { return 1 }
110 return 0
111}
112
113// ---- W-TONE-2: PER-DIMENSION WEIGHTING + DRIFT (closes the v1 L1-dominance limit)
114// Weights w[] are PER-1000 (w[i]=1000 => unweighted; w[i]=0 => ignore that dim),
115// so an operator DATA file can damp the high-range cadence dim and let diction /
116// signal dims carry the match -- no magic numbers baked into the organ (rule 11).
117
118// weighted L1: sum w[i]*|a-b| / 1000. w[i]=1000 reproduces tn_dist exactly.
119func tn_dist_w(a: *i64, b: *i64, w: *i64, k: i64) -> i64 {
120 var sum: i64 = 0
121 var i: i64 = 0
122 while i < k {
123 var d: i64 = a[i] - b[i]
124 if d < 0 { d = 0 - d }
125 sum = sum + (w[i] * d) / 1000
126 i = i + 1
127 }
128 return sum
129}
130
131// 1 if cand is weighted-closer to voice a than to voice b, else 0
132func tn_closer_w(cand: *i64, a: *i64, b: *i64, w: *i64, k: i64) -> i64 {
133 let da: i64 = tn_dist_w(cand, a, w, k)
134 let db: i64 = tn_dist_w(cand, b, w, k)
135 if da < db { return 1 }
136 return 0
137}
138
139// per-dim SIGNED weighted deviation of cand from a voice ref into out[0..k-1]:
140// out[i] > 0 => cand is HIGHER than the ref on dim i (steer it down)
141// out[i] < 0 => cand is LOWER than the ref on dim i (steer it up)
142// returns the total weighted L1 distance (== tn_dist_w for non-negative weights).
143func tn_drift(cand: *i64, ref: *i64, w: *i64, k: i64, out: *i64) -> i64 {
144 var total: i64 = 0
145 var i: i64 = 0
146 while i < k {
147 let raw: i64 = cand[i] - ref[i]
148 out[i] = (w[i] * raw) / 1000
149 var d: i64 = out[i]
150 if d < 0 { d = 0 - d }
151 total = total + d
152 i = i + 1
153 }
154 return total
155}