code wiki / (root) / nx_g2p.nx

nx_g2p.nx source

↩ module page · 178 lines · 8150 B

1// nx_g2p.nx -- Converts lowercase ASCII text to a sequence of phoneme IDs for speech synthesis. 2const PH_MAGIC_1700: i64 = 1700 3const PH_MAGIC_2400: i64 = 2400 4const PH_MAGIC_1840: i64 = 1840 5const PH_MAGIC_2480: i64 = 2480 6const PH_MAGIC_1990: i64 = 1990 7const PH_MAGIC_2550: i64 = 2550 8const PH_MAGIC_1090: i64 = 1090 9const PH_MAGIC_2440: i64 = 2440 10const PH_MAGIC_1190: i64 = 1190 11const PH_MAGIC_2390: i64 = 2390 12const PH_MAGIC_2290: i64 = 2290 13const PH_MAGIC_3010: i64 = 3010 14const PH_MAGIC_2240: i64 = 2240 15const PH_MAGIC_1100: i64 = 1100 16const PH_MAGIC_2200: i64 = 2200 17const PH_MAGIC_1500: i64 = 1500 18const PH_MAGIC_2500: i64 = 2500 19const PH_MAGIC_1300: i64 = 1300 20const PH_MAGIC_2600: i64 = 2600 21const PH_MAGIC_1350: i64 = 1350 22const PH_MAGIC_2000: i64 = 2000 23const PH_MAGIC_1900: i64 = 1900 24const PH_MAGIC_1800: i64 = 1800 25const PH_MAGIC_3400: i64 = 3400 26const PH_MAGIC_3600: i64 = 3600 27const PH_MAGIC_3000: i64 = 3000 28const PH_MAGIC_1400: i64 = 1400 29const PH_MAGIC_1200: i64 = 1200 30const PH_MAGIC_3500: i64 = 3500 31// nx_g2p.nx -- AUDIO arc: TEXT -> PHONEME front-end (grapheme-to-phoneme). 32// The bridge that lets you TYPE words and get a phoneme stream the synthesizer 33// can read aloud (nx_phoneme_synth / nx_voice_read). Sovereign rule-based 34// letter-to-sound: longest-match digraphs (sh/th/ee/oo/ck) then per-letter 35// rules, over a small DATA phoneme inventory (formant-Hz per phoneme). 36// 37// HONEST SCOPE: rule-based English LTS with a compact phoneme set -- deterministic 38// and synthesizable, NOT a full pronunciation dictionary (true English needs an 39// exception lexicon for "though"/"colonel"/etc). A later rung adds a lexicon. 40// 41// Pure integer, NO floats, NO imports (caller owns buffers). 42// nx_g2p(text, len, out_ids, max) -> count (lowercase ASCII; spaces skipped) 43// nx_g2p_phoneme(id, rec) rec=[F1,F2,F3,voiced,dur] 44// 45// license_tier: ORIGINAL 46// module: nishi-core.audio.g2p 47// depends: nishi-core.audio.phoneme_synth (record format) 48// capability: AUDIO_TEXT_TO_PHONEME 49 50// ---- phoneme ids ---- 51const PH_AE: i64 = 0 52const PH_EH: i64 = 1 53const PH_IH: i64 = 2 54const PH_AA: i64 = 3 55const PH_AH: i64 = 4 56const PH_IY: i64 = 5 57const PH_UW: i64 = 6 58const PH_M: i64 = 7 59const PH_N: i64 = 8 60const PH_L: i64 = 9 61const PH_R: i64 = 10 62const PH_W: i64 = 11 63const PH_V: i64 = 12 64const PH_Z: i64 = 13 65const PH_B: i64 = 14 66const PH_D: i64 = 15 67const PH_G: i64 = 16 68const PH_S: i64 = 17 69const PH_SH: i64 = 18 70const PH_TH: i64 = 19 71const PH_F: i64 = 20 72const PH_HH: i64 = 21 73const PH_K: i64 = 22 74const PH_T: i64 = 23 75const PH_P: i64 = 24 76const PH_CH: i64 = 25 77const PH_JH: i64 = 26 78 79const G2P_VDUR: i64 = 1200 // vowel duration (samples) 80const G2P_CDUR: i64 = 500 // consonant duration 81 82// single lowercase letter -> phoneme id, or -1 to skip 83func _g2p_single(c: i64) -> i64 { 84 if c == 97 { return PH_AE } // a 85 if c == 98 { return PH_B } // b 86 if c == 99 { return PH_K } // c 87 if c == 100 { return PH_D } // d 88 if c == 101 { return PH_EH } // e 89 if c == 102 { return PH_F } // f 90 if c == 103 { return PH_G } // g 91 if c == 104 { return PH_HH } // h 92 if c == 105 { return PH_IH } // i 93 if c == 106 { return PH_JH } // j 94 if c == 107 { return PH_K } // k 95 if c == 108 { return PH_L } // l 96 if c == 109 { return PH_M } // m 97 if c == 110 { return PH_N } // n 98 if c == 111 { return PH_AA } // o 99 if c == 112 { return PH_P } // p 100 if c == 113 { return PH_K } // q 101 if c == 114 { return PH_R } // r 102 if c == 115 { return PH_S } // s 103 if c == 116 { return PH_T } // t 104 if c == 117 { return PH_AH } // u 105 if c == 118 { return PH_V } // v 106 if c == 119 { return PH_W } // w 107 if c == 120 { return PH_K } // x 108 if c == 121 { return PH_IY } // y 109 if c == 122 { return PH_Z } // z 110 return 0 - 1 111} 112 113func nx_g2p(text: *u8, len: i64, out_ids: *i64, max: i64) -> i64 { 114 var i: i64 = 0 115 var cnt: i64 = 0 116 while i < len { 117 if cnt >= max { i = len } 118 if i < len { 119 let c0: i64 = text[i] 120 var c1: i64 = 0 121 if i + 1 < len { c1 = text[i + 1] } 122 var id: i64 = 0 - 1 123 var adv: i64 = 1 124 var matched: i64 = 0 125 if c0 == 115 { if c1 == 104 { id = PH_SH; adv = 2; matched = 1 } } // sh 126 if matched == 0 { if c0 == 116 { if c1 == 104 { id = PH_TH; adv = 2; matched = 1 } } } // th 127 if matched == 0 { if c0 == 99 { if c1 == 104 { id = PH_CH; adv = 2; matched = 1 } } } // ch 128 if matched == 0 { if c0 == 101 { if c1 == 101 { id = PH_IY; adv = 2; matched = 1 } } } // ee 129 if matched == 0 { if c0 == 111 { if c1 == 111 { id = PH_UW; adv = 2; matched = 1 } } } // oo 130 if matched == 0 { if c0 == 99 { if c1 == 107 { id = PH_K; adv = 2; matched = 1 } } } // ck 131 if matched == 0 { id = _g2p_single(c0) } 132 if id >= 0 { if cnt < max { out_ids[cnt] = id; cnt = cnt + 1 } } 133 i = i + adv 134 } 135 } 136 return cnt 137} 138 139// phoneme -> formant record [F1,F2,F3,voiced,dur] (approximate English). 140func nx_g2p_phoneme(id: i64, rec: *i64) -> i64 { 141 rec[3] = 1 // default voiced 142 rec[4] = G2P_VDUR 143 // vowels 144 if id == PH_AE { rec[0] = 660; rec[1] = PH_MAGIC_1700; rec[2] = PH_MAGIC_2400; return 0 } 145 if id == PH_EH { rec[0] = 530; rec[1] = PH_MAGIC_1840; rec[2] = PH_MAGIC_2480; return 0 } 146 if id == PH_IH { rec[0] = 390; rec[1] = PH_MAGIC_1990; rec[2] = PH_MAGIC_2550; return 0 } 147 if id == PH_AA { rec[0] = 730; rec[1] = PH_MAGIC_1090; rec[2] = PH_MAGIC_2440; return 0 } 148 if id == PH_AH { rec[0] = 640; rec[1] = PH_MAGIC_1190; rec[2] = PH_MAGIC_2390; return 0 } 149 if id == PH_IY { rec[0] = 270; rec[1] = PH_MAGIC_2290; rec[2] = PH_MAGIC_3010; return 0 } 150 if id == PH_UW { rec[0] = 300; rec[1] = 870; rec[2] = PH_MAGIC_2240; return 0 } 151 // voiced consonants (shorter) 152 rec[4] = G2P_CDUR 153 if id == PH_M { rec[0] = 250; rec[1] = PH_MAGIC_1100; rec[2] = PH_MAGIC_2200; return 0 } 154 if id == PH_N { rec[0] = 250; rec[1] = PH_MAGIC_1500; rec[2] = PH_MAGIC_2500; return 0 } 155 if id == PH_L { rec[0] = 360; rec[1] = PH_MAGIC_1300; rec[2] = PH_MAGIC_2600; return 0 } 156 if id == PH_R { rec[0] = 490; rec[1] = PH_MAGIC_1350; rec[2] = PH_MAGIC_1700; return 0 } 157 if id == PH_W { rec[0] = 300; rec[1] = 610; rec[2] = PH_MAGIC_2200; return 0 } 158 if id == PH_V { rec[0] = 400; rec[1] = PH_MAGIC_1100; rec[2] = PH_MAGIC_2400; return 0 } 159 if id == PH_Z { rec[0] = 300; rec[1] = PH_MAGIC_1500; rec[2] = PH_MAGIC_2500; return 0 } 160 if id == PH_B { rec[0] = 250; rec[1] = 800; rec[2] = PH_MAGIC_2000; return 0 } 161 if id == PH_D { rec[0] = 300; rec[1] = PH_MAGIC_1700; rec[2] = PH_MAGIC_2600; return 0 } 162 if id == PH_G { rec[0] = 250; rec[1] = PH_MAGIC_1900; rec[2] = PH_MAGIC_2500; return 0 } 163 if id == PH_JH { rec[0] = 250; rec[1] = PH_MAGIC_1800; rec[2] = PH_MAGIC_2600; return 0 } 164 // unvoiced consonants (noise source) 165 rec[3] = 0 166 if id == PH_S { rec[0] = PH_MAGIC_2200; rec[1] = PH_MAGIC_3400; rec[2] = PH_MAGIC_3600; return 0 } 167 if id == PH_SH { rec[0] = PH_MAGIC_1800; rec[1] = PH_MAGIC_2400; rec[2] = PH_MAGIC_3000; return 0 } 168 if id == PH_TH { rec[0] = PH_MAGIC_1400; rec[1] = PH_MAGIC_2600; rec[2] = PH_MAGIC_3400; return 0 } 169 if id == PH_F { rec[0] = PH_MAGIC_1200; rec[1] = PH_MAGIC_2200; rec[2] = PH_MAGIC_3000; return 0 } 170 if id == PH_HH { rec[0] = 1000; rec[1] = PH_MAGIC_1500; rec[2] = PH_MAGIC_2500; return 0 } 171 if id == PH_K { rec[0] = PH_MAGIC_1800; rec[1] = PH_MAGIC_2000; rec[2] = PH_MAGIC_2400; return 0 } 172 if id == PH_T { rec[0] = PH_MAGIC_2000; rec[1] = PH_MAGIC_3000; rec[2] = PH_MAGIC_3500; return 0 } 173 if id == PH_P { rec[0] = 800; rec[1] = PH_MAGIC_1500; rec[2] = PH_MAGIC_2200; return 0 } 174 if id == PH_CH { rec[0] = PH_MAGIC_1700; rec[1] = PH_MAGIC_2400; rec[2] = PH_MAGIC_3000; return 0 } 175 // unknown -> neutral schwa-ish, voiced 176 rec[0] = 500; rec[1] = PH_MAGIC_1500; rec[2] = PH_MAGIC_2500; rec[3] = 1; rec[4] = G2P_CDUR 177 return 0 178}