Mai Linh Vietnamese TTS: CoreML (.mlpackage) + ONNX + Swift + scripts
Browse files- Mai Linh/mailinh250626.onnx +3 -0
- Mai Linh/mailinh250626.onnx.json +492 -0
- Mai Linh/mailinh_7BKaqqkh.mp3 +0 -0
- MaiLinh.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- MaiLinh.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- MaiLinh.mlpackage/Manifest.json +18 -0
- MaiLinhTTS.swift +87 -0
- README.md +47 -0
- bench_coreml_vs_onnx.py +63 -0
- convert_coreml.py +309 -0
- coreml_tts.py +49 -0
- say.sh +15 -0
- tts.py +49 -0
Mai Linh/mailinh250626.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ab05b27fe6b93149ebd844d2bd2d38f5804b58722d00aedd19cbab67b5963b6c
|
| 3 |
+
size 63516050
|
Mai Linh/mailinh250626.onnx.json
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"audio": {
|
| 3 |
+
"sample_rate": 22050,
|
| 4 |
+
"quality": "medium"
|
| 5 |
+
},
|
| 6 |
+
"espeak": {
|
| 7 |
+
"voice": "vi"
|
| 8 |
+
},
|
| 9 |
+
"inference": {
|
| 10 |
+
"noise_scale": 0.667,
|
| 11 |
+
"length_scale": 1,
|
| 12 |
+
"noise_w": 0.8
|
| 13 |
+
},
|
| 14 |
+
"phoneme_map": {},
|
| 15 |
+
"phoneme_id_map": {
|
| 16 |
+
"_": [
|
| 17 |
+
0
|
| 18 |
+
],
|
| 19 |
+
"^": [
|
| 20 |
+
1
|
| 21 |
+
],
|
| 22 |
+
"$": [
|
| 23 |
+
2
|
| 24 |
+
],
|
| 25 |
+
" ": [
|
| 26 |
+
3
|
| 27 |
+
],
|
| 28 |
+
"!": [
|
| 29 |
+
4
|
| 30 |
+
],
|
| 31 |
+
"'": [
|
| 32 |
+
5
|
| 33 |
+
],
|
| 34 |
+
"(": [
|
| 35 |
+
6
|
| 36 |
+
],
|
| 37 |
+
")": [
|
| 38 |
+
7
|
| 39 |
+
],
|
| 40 |
+
",": [
|
| 41 |
+
8
|
| 42 |
+
],
|
| 43 |
+
"-": [
|
| 44 |
+
9
|
| 45 |
+
],
|
| 46 |
+
".": [
|
| 47 |
+
10
|
| 48 |
+
],
|
| 49 |
+
":": [
|
| 50 |
+
11
|
| 51 |
+
],
|
| 52 |
+
";": [
|
| 53 |
+
12
|
| 54 |
+
],
|
| 55 |
+
"?": [
|
| 56 |
+
13
|
| 57 |
+
],
|
| 58 |
+
"a": [
|
| 59 |
+
14
|
| 60 |
+
],
|
| 61 |
+
"b": [
|
| 62 |
+
15
|
| 63 |
+
],
|
| 64 |
+
"c": [
|
| 65 |
+
16
|
| 66 |
+
],
|
| 67 |
+
"d": [
|
| 68 |
+
17
|
| 69 |
+
],
|
| 70 |
+
"e": [
|
| 71 |
+
18
|
| 72 |
+
],
|
| 73 |
+
"f": [
|
| 74 |
+
19
|
| 75 |
+
],
|
| 76 |
+
"h": [
|
| 77 |
+
20
|
| 78 |
+
],
|
| 79 |
+
"i": [
|
| 80 |
+
21
|
| 81 |
+
],
|
| 82 |
+
"j": [
|
| 83 |
+
22
|
| 84 |
+
],
|
| 85 |
+
"k": [
|
| 86 |
+
23
|
| 87 |
+
],
|
| 88 |
+
"l": [
|
| 89 |
+
24
|
| 90 |
+
],
|
| 91 |
+
"m": [
|
| 92 |
+
25
|
| 93 |
+
],
|
| 94 |
+
"n": [
|
| 95 |
+
26
|
| 96 |
+
],
|
| 97 |
+
"o": [
|
| 98 |
+
27
|
| 99 |
+
],
|
| 100 |
+
"p": [
|
| 101 |
+
28
|
| 102 |
+
],
|
| 103 |
+
"q": [
|
| 104 |
+
29
|
| 105 |
+
],
|
| 106 |
+
"r": [
|
| 107 |
+
30
|
| 108 |
+
],
|
| 109 |
+
"s": [
|
| 110 |
+
31
|
| 111 |
+
],
|
| 112 |
+
"t": [
|
| 113 |
+
32
|
| 114 |
+
],
|
| 115 |
+
"u": [
|
| 116 |
+
33
|
| 117 |
+
],
|
| 118 |
+
"v": [
|
| 119 |
+
34
|
| 120 |
+
],
|
| 121 |
+
"w": [
|
| 122 |
+
35
|
| 123 |
+
],
|
| 124 |
+
"x": [
|
| 125 |
+
36
|
| 126 |
+
],
|
| 127 |
+
"y": [
|
| 128 |
+
37
|
| 129 |
+
],
|
| 130 |
+
"z": [
|
| 131 |
+
38
|
| 132 |
+
],
|
| 133 |
+
"æ": [
|
| 134 |
+
39
|
| 135 |
+
],
|
| 136 |
+
"ç": [
|
| 137 |
+
40
|
| 138 |
+
],
|
| 139 |
+
"ð": [
|
| 140 |
+
41
|
| 141 |
+
],
|
| 142 |
+
"ø": [
|
| 143 |
+
42
|
| 144 |
+
],
|
| 145 |
+
"ħ": [
|
| 146 |
+
43
|
| 147 |
+
],
|
| 148 |
+
"ŋ": [
|
| 149 |
+
44
|
| 150 |
+
],
|
| 151 |
+
"œ": [
|
| 152 |
+
45
|
| 153 |
+
],
|
| 154 |
+
"ǀ": [
|
| 155 |
+
46
|
| 156 |
+
],
|
| 157 |
+
"ǁ": [
|
| 158 |
+
47
|
| 159 |
+
],
|
| 160 |
+
"ǂ": [
|
| 161 |
+
48
|
| 162 |
+
],
|
| 163 |
+
"ǃ": [
|
| 164 |
+
49
|
| 165 |
+
],
|
| 166 |
+
"ɐ": [
|
| 167 |
+
50
|
| 168 |
+
],
|
| 169 |
+
"ɑ": [
|
| 170 |
+
51
|
| 171 |
+
],
|
| 172 |
+
"ɒ": [
|
| 173 |
+
52
|
| 174 |
+
],
|
| 175 |
+
"ɓ": [
|
| 176 |
+
53
|
| 177 |
+
],
|
| 178 |
+
"ɔ": [
|
| 179 |
+
54
|
| 180 |
+
],
|
| 181 |
+
"ɕ": [
|
| 182 |
+
55
|
| 183 |
+
],
|
| 184 |
+
"ɖ": [
|
| 185 |
+
56
|
| 186 |
+
],
|
| 187 |
+
"ɗ": [
|
| 188 |
+
57
|
| 189 |
+
],
|
| 190 |
+
"ɘ": [
|
| 191 |
+
58
|
| 192 |
+
],
|
| 193 |
+
"ə": [
|
| 194 |
+
59
|
| 195 |
+
],
|
| 196 |
+
"ɚ": [
|
| 197 |
+
60
|
| 198 |
+
],
|
| 199 |
+
"ɛ": [
|
| 200 |
+
61
|
| 201 |
+
],
|
| 202 |
+
"ɜ": [
|
| 203 |
+
62
|
| 204 |
+
],
|
| 205 |
+
"ɞ": [
|
| 206 |
+
63
|
| 207 |
+
],
|
| 208 |
+
"ɟ": [
|
| 209 |
+
64
|
| 210 |
+
],
|
| 211 |
+
"ɠ": [
|
| 212 |
+
65
|
| 213 |
+
],
|
| 214 |
+
"ɡ": [
|
| 215 |
+
66
|
| 216 |
+
],
|
| 217 |
+
"ɢ": [
|
| 218 |
+
67
|
| 219 |
+
],
|
| 220 |
+
"ɣ": [
|
| 221 |
+
68
|
| 222 |
+
],
|
| 223 |
+
"ɤ": [
|
| 224 |
+
69
|
| 225 |
+
],
|
| 226 |
+
"ɥ": [
|
| 227 |
+
70
|
| 228 |
+
],
|
| 229 |
+
"ɦ": [
|
| 230 |
+
71
|
| 231 |
+
],
|
| 232 |
+
"ɧ": [
|
| 233 |
+
72
|
| 234 |
+
],
|
| 235 |
+
"ɨ": [
|
| 236 |
+
73
|
| 237 |
+
],
|
| 238 |
+
"ɪ": [
|
| 239 |
+
74
|
| 240 |
+
],
|
| 241 |
+
"ɫ": [
|
| 242 |
+
75
|
| 243 |
+
],
|
| 244 |
+
"ɬ": [
|
| 245 |
+
76
|
| 246 |
+
],
|
| 247 |
+
"ɭ": [
|
| 248 |
+
77
|
| 249 |
+
],
|
| 250 |
+
"ɮ": [
|
| 251 |
+
78
|
| 252 |
+
],
|
| 253 |
+
"ɯ": [
|
| 254 |
+
79
|
| 255 |
+
],
|
| 256 |
+
"ɰ": [
|
| 257 |
+
80
|
| 258 |
+
],
|
| 259 |
+
"ɱ": [
|
| 260 |
+
81
|
| 261 |
+
],
|
| 262 |
+
"ɲ": [
|
| 263 |
+
82
|
| 264 |
+
],
|
| 265 |
+
"ɳ": [
|
| 266 |
+
83
|
| 267 |
+
],
|
| 268 |
+
"ɴ": [
|
| 269 |
+
84
|
| 270 |
+
],
|
| 271 |
+
"ɵ": [
|
| 272 |
+
85
|
| 273 |
+
],
|
| 274 |
+
"ɶ": [
|
| 275 |
+
86
|
| 276 |
+
],
|
| 277 |
+
"ɸ": [
|
| 278 |
+
87
|
| 279 |
+
],
|
| 280 |
+
"ɹ": [
|
| 281 |
+
88
|
| 282 |
+
],
|
| 283 |
+
"ɺ": [
|
| 284 |
+
89
|
| 285 |
+
],
|
| 286 |
+
"ɻ": [
|
| 287 |
+
90
|
| 288 |
+
],
|
| 289 |
+
"ɽ": [
|
| 290 |
+
91
|
| 291 |
+
],
|
| 292 |
+
"ɾ": [
|
| 293 |
+
92
|
| 294 |
+
],
|
| 295 |
+
"ʀ": [
|
| 296 |
+
93
|
| 297 |
+
],
|
| 298 |
+
"ʁ": [
|
| 299 |
+
94
|
| 300 |
+
],
|
| 301 |
+
"ʂ": [
|
| 302 |
+
95
|
| 303 |
+
],
|
| 304 |
+
"ʃ": [
|
| 305 |
+
96
|
| 306 |
+
],
|
| 307 |
+
"ʄ": [
|
| 308 |
+
97
|
| 309 |
+
],
|
| 310 |
+
"ʈ": [
|
| 311 |
+
98
|
| 312 |
+
],
|
| 313 |
+
"ʉ": [
|
| 314 |
+
99
|
| 315 |
+
],
|
| 316 |
+
"ʊ": [
|
| 317 |
+
100
|
| 318 |
+
],
|
| 319 |
+
"ʋ": [
|
| 320 |
+
101
|
| 321 |
+
],
|
| 322 |
+
"ʌ": [
|
| 323 |
+
102
|
| 324 |
+
],
|
| 325 |
+
"ʍ": [
|
| 326 |
+
103
|
| 327 |
+
],
|
| 328 |
+
"ʎ": [
|
| 329 |
+
104
|
| 330 |
+
],
|
| 331 |
+
"ʏ": [
|
| 332 |
+
105
|
| 333 |
+
],
|
| 334 |
+
"ʐ": [
|
| 335 |
+
106
|
| 336 |
+
],
|
| 337 |
+
"ʑ": [
|
| 338 |
+
107
|
| 339 |
+
],
|
| 340 |
+
"ʒ": [
|
| 341 |
+
108
|
| 342 |
+
],
|
| 343 |
+
"ʔ": [
|
| 344 |
+
109
|
| 345 |
+
],
|
| 346 |
+
"ʕ": [
|
| 347 |
+
110
|
| 348 |
+
],
|
| 349 |
+
"ʘ": [
|
| 350 |
+
111
|
| 351 |
+
],
|
| 352 |
+
"ʙ": [
|
| 353 |
+
112
|
| 354 |
+
],
|
| 355 |
+
"ʛ": [
|
| 356 |
+
113
|
| 357 |
+
],
|
| 358 |
+
"ʜ": [
|
| 359 |
+
114
|
| 360 |
+
],
|
| 361 |
+
"ʝ": [
|
| 362 |
+
115
|
| 363 |
+
],
|
| 364 |
+
"ʟ": [
|
| 365 |
+
116
|
| 366 |
+
],
|
| 367 |
+
"ʡ": [
|
| 368 |
+
117
|
| 369 |
+
],
|
| 370 |
+
"ʢ": [
|
| 371 |
+
118
|
| 372 |
+
],
|
| 373 |
+
"ʲ": [
|
| 374 |
+
119
|
| 375 |
+
],
|
| 376 |
+
"ˈ": [
|
| 377 |
+
120
|
| 378 |
+
],
|
| 379 |
+
"ˌ": [
|
| 380 |
+
121
|
| 381 |
+
],
|
| 382 |
+
"ː": [
|
| 383 |
+
122
|
| 384 |
+
],
|
| 385 |
+
"ˑ": [
|
| 386 |
+
123
|
| 387 |
+
],
|
| 388 |
+
"˞": [
|
| 389 |
+
124
|
| 390 |
+
],
|
| 391 |
+
"β": [
|
| 392 |
+
125
|
| 393 |
+
],
|
| 394 |
+
"θ": [
|
| 395 |
+
126
|
| 396 |
+
],
|
| 397 |
+
"χ": [
|
| 398 |
+
127
|
| 399 |
+
],
|
| 400 |
+
"ᵻ": [
|
| 401 |
+
128
|
| 402 |
+
],
|
| 403 |
+
"ⱱ": [
|
| 404 |
+
129
|
| 405 |
+
],
|
| 406 |
+
"0": [
|
| 407 |
+
130
|
| 408 |
+
],
|
| 409 |
+
"1": [
|
| 410 |
+
131
|
| 411 |
+
],
|
| 412 |
+
"2": [
|
| 413 |
+
132
|
| 414 |
+
],
|
| 415 |
+
"3": [
|
| 416 |
+
133
|
| 417 |
+
],
|
| 418 |
+
"4": [
|
| 419 |
+
134
|
| 420 |
+
],
|
| 421 |
+
"5": [
|
| 422 |
+
135
|
| 423 |
+
],
|
| 424 |
+
"6": [
|
| 425 |
+
136
|
| 426 |
+
],
|
| 427 |
+
"7": [
|
| 428 |
+
137
|
| 429 |
+
],
|
| 430 |
+
"8": [
|
| 431 |
+
138
|
| 432 |
+
],
|
| 433 |
+
"9": [
|
| 434 |
+
139
|
| 435 |
+
],
|
| 436 |
+
"̧": [
|
| 437 |
+
140
|
| 438 |
+
],
|
| 439 |
+
"̃": [
|
| 440 |
+
141
|
| 441 |
+
],
|
| 442 |
+
"̪": [
|
| 443 |
+
142
|
| 444 |
+
],
|
| 445 |
+
"̯": [
|
| 446 |
+
143
|
| 447 |
+
],
|
| 448 |
+
"̩": [
|
| 449 |
+
144
|
| 450 |
+
],
|
| 451 |
+
"ʰ": [
|
| 452 |
+
145
|
| 453 |
+
],
|
| 454 |
+
"ˤ": [
|
| 455 |
+
146
|
| 456 |
+
],
|
| 457 |
+
"ε": [
|
| 458 |
+
147
|
| 459 |
+
],
|
| 460 |
+
"↓": [
|
| 461 |
+
148
|
| 462 |
+
],
|
| 463 |
+
"#": [
|
| 464 |
+
149
|
| 465 |
+
],
|
| 466 |
+
"\"": [
|
| 467 |
+
150
|
| 468 |
+
],
|
| 469 |
+
"↑": [
|
| 470 |
+
151
|
| 471 |
+
],
|
| 472 |
+
"̺": [
|
| 473 |
+
152
|
| 474 |
+
],
|
| 475 |
+
"̻": [
|
| 476 |
+
153
|
| 477 |
+
]
|
| 478 |
+
},
|
| 479 |
+
"num_symbols": 256,
|
| 480 |
+
"num_speakers": 1,
|
| 481 |
+
"speaker_id_map": {},
|
| 482 |
+
"piper_version": "1.0.0",
|
| 483 |
+
"language": {
|
| 484 |
+
"code": "vi_VN",
|
| 485 |
+
"family": "vi",
|
| 486 |
+
"region": "VN",
|
| 487 |
+
"name_native": "Tiếng Việt",
|
| 488 |
+
"name_english": "Vietnamese",
|
| 489 |
+
"country_english": "Vietnam"
|
| 490 |
+
},
|
| 491 |
+
"dataset": "mailinh"
|
| 492 |
+
}
|
Mai Linh/mailinh_7BKaqqkh.mp3
ADDED
|
Binary file (59 kB). View file
|
|
|
MaiLinh.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b23be913049ebfe10e73606ffb3a51bf53257804ac644cb0d2d2465bd4e900ba
|
| 3 |
+
size 447215
|
MaiLinh.mlpackage/Data/com.apple.CoreML/weights/weight.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d863447b3ee555db545137360b8f09e5d5bc2e270af5d25c47e076b4bd439dba
|
| 3 |
+
size 64940288
|
MaiLinh.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"212E4D8C-9780-421A-A76E-222EB6990D60": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Specification",
|
| 7 |
+
"name": "model.mlmodel",
|
| 8 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 9 |
+
},
|
| 10 |
+
"44CF003F-6AE9-4964-9EAF-FDE358FC4167": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Weights",
|
| 13 |
+
"name": "weights",
|
| 14 |
+
"path": "com.apple.CoreML/weights"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "212E4D8C-9780-421A-A76E-222EB6990D60"
|
| 18 |
+
}
|
MaiLinhTTS.swift
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Foundation
|
| 2 |
+
import CoreML
|
| 3 |
+
import AVFoundation
|
| 4 |
+
|
| 5 |
+
/// Mai Linh - Vietnamese TTS (Piper/VITS) chay tren CoreML.
|
| 6 |
+
///
|
| 7 |
+
/// Model I/O:
|
| 8 |
+
/// input : MLMultiArray [1, 256] Int32 - phoneme ids, pad 0 cho du 256
|
| 9 |
+
/// input_lengths : MLMultiArray [1] Int32 - so phoneme id thuc (<= 256)
|
| 10 |
+
/// scales : MLMultiArray [3] Float32 - [noise, length, noise_w] = [0.667, 1.0, 0.8]
|
| 11 |
+
/// Output:
|
| 12 |
+
/// audio : MLMultiArray Float32 - 22050Hz mono, do dai co dinh (max_frames*256)
|
| 13 |
+
/// n_frames : MLMultiArray Int32 - so frame that; CAT audio tai n_frames*256
|
| 14 |
+
///
|
| 15 |
+
/// CON THIEU (phai tu lam): phonemize tieng Viet -> phoneme ids.
|
| 16 |
+
/// Dung espeak-ng (voice "vi") de ra chuoi IPA, roi map qua `phoneme_id_map`
|
| 17 |
+
/// trong mailinh250626.onnx.json, chen pad id 0 giua cac phoneme (logic phonemes_to_ids cua piper).
|
| 18 |
+
final class MaiLinhTTS {
|
| 19 |
+
static let sampleRate = 22050
|
| 20 |
+
static let hop = 256
|
| 21 |
+
static let maxPhonemes = 256
|
| 22 |
+
|
| 23 |
+
private let model: MLModel
|
| 24 |
+
|
| 25 |
+
init() throws {
|
| 26 |
+
// Xcode tu bien dich MaiLinh.mlpackage -> MaiLinh.mlmodelc khi build.
|
| 27 |
+
let url = Bundle.main.url(forResource: "MaiLinh", withExtension: "mlmodelc")!
|
| 28 |
+
model = try MLModel(contentsOf: url)
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
/// Tra ve waveform mono Float (22050Hz), da cat dung do dai + fade-out.
|
| 32 |
+
func synthesize(phonemeIds: [Int32],
|
| 33 |
+
noiseScale: Float = 0.667,
|
| 34 |
+
lengthScale: Float = 1.0,
|
| 35 |
+
noiseW: Float = 0.8) throws -> [Float] {
|
| 36 |
+
let L = Self.maxPhonemes
|
| 37 |
+
let n = min(phonemeIds.count, L)
|
| 38 |
+
|
| 39 |
+
let input = try MLMultiArray(shape: [1, NSNumber(value: L)], dataType: .int32)
|
| 40 |
+
for i in 0..<L { input[i] = 0 }
|
| 41 |
+
for i in 0..<n { input[i] = NSNumber(value: phonemeIds[i]) }
|
| 42 |
+
|
| 43 |
+
let lengths = try MLMultiArray(shape: [1], dataType: .int32)
|
| 44 |
+
lengths[0] = NSNumber(value: n)
|
| 45 |
+
|
| 46 |
+
let scales = try MLMultiArray(shape: [3], dataType: .float32)
|
| 47 |
+
scales[0] = NSNumber(value: noiseScale)
|
| 48 |
+
scales[1] = NSNumber(value: lengthScale)
|
| 49 |
+
scales[2] = NSNumber(value: noiseW)
|
| 50 |
+
|
| 51 |
+
let provider = try MLDictionaryFeatureProvider(dictionary: [
|
| 52 |
+
"input": input, "input_lengths": lengths, "scales": scales,
|
| 53 |
+
])
|
| 54 |
+
let out = try model.prediction(from: provider)
|
| 55 |
+
|
| 56 |
+
let audio = out.featureValue(for: "audio")!.multiArrayValue!
|
| 57 |
+
let nFrames = out.featureValue(for: "n_frames")!.multiArrayValue![0].intValue
|
| 58 |
+
let cut = min(nFrames * Self.hop, audio.count)
|
| 59 |
+
|
| 60 |
+
var samples = [Float](repeating: 0, count: cut)
|
| 61 |
+
let p = audio.dataPointer.assumingMemoryBound(to: Float32.self)
|
| 62 |
+
for i in 0..<cut { samples[i] = p[i] }
|
| 63 |
+
|
| 64 |
+
// fade-out ~8ms de bo transient o ranh gioi mask (tieng "up")
|
| 65 |
+
let f = min(Int(0.008 * Double(Self.sampleRate)), cut)
|
| 66 |
+
for i in 0..<f {
|
| 67 |
+
let t = Double(i) / Double(max(f - 1, 1))
|
| 68 |
+
samples[cut - f + i] *= Float(0.5 * (1.0 + cos(.pi * t)))
|
| 69 |
+
}
|
| 70 |
+
return samples
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/// Phat waveform Float qua AVAudioEngine.
|
| 74 |
+
func play(_ samples: [Float], engine: AVAudioEngine, player: AVAudioPlayerNode) throws {
|
| 75 |
+
let fmt = AVAudioFormat(standardFormatWithSampleRate: Double(Self.sampleRate), channels: 1)!
|
| 76 |
+
let buf = AVAudioPCMBuffer(pcmFormat: fmt, frameCapacity: AVAudioFrameCount(samples.count))!
|
| 77 |
+
buf.frameLength = AVAudioFrameCount(samples.count)
|
| 78 |
+
memcpy(buf.floatChannelData![0], samples, samples.count * MemoryLayout<Float>.size)
|
| 79 |
+
if !engine.isRunning {
|
| 80 |
+
engine.attach(player)
|
| 81 |
+
engine.connect(player, to: engine.mainMixerNode, format: fmt)
|
| 82 |
+
try engine.start()
|
| 83 |
+
}
|
| 84 |
+
player.scheduleBuffer(buf, completionHandler: nil)
|
| 85 |
+
player.play()
|
| 86 |
+
}
|
| 87 |
+
}
|
README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Mai Linh - Vietnamese TTS (Piper/VITS) — CoreML + ONNX
|
| 2 |
+
|
| 3 |
+
Giọng **Mai Linh** tiếng Việt, chạy on-device. Hai dạng:
|
| 4 |
+
- `MaiLinh.mlpackage` — **CoreML** (cho app iOS/macOS native), fp32, 22050Hz.
|
| 5 |
+
- `Mai Linh/mailinh250626.onnx` + `.onnx.json` — **ONNX** (chạy bằng onnxruntime trên PC/Mac).
|
| 6 |
+
|
| 7 |
+
Credit: voice từ [`quangdung/Piper_checkpoint`](https://huggingface.co/quangdung/Piper_checkpoint) (fine-tune từ `vi_VN/vais1000/medium`), base [OHF-Voice/piper1-gpl](https://github.com/OHF-Voice/piper1-gpl). License MIT.
|
| 8 |
+
|
| 9 |
+
## File
|
| 10 |
+
|
| 11 |
+
| File | Mô tả |
|
| 12 |
+
|---|---|
|
| 13 |
+
| `MaiLinh.mlpackage` | Model CoreML (fp32, L=256 phoneme, M=500 ≈ 5.8s/câu) |
|
| 14 |
+
| `MaiLinhTTS.swift` | Code Swift mẫu: load + chạy + cắt + fade + phát |
|
| 15 |
+
| `Mai Linh/mailinh250626.onnx(.json)` | Model ONNX + config (chứa `phoneme_id_map`) |
|
| 16 |
+
| `coreml_tts.py` / `tts.py` | Inference tham chiếu (Python) cho CoreML / ONNX |
|
| 17 |
+
| `convert_coreml.py` | Script convert ckpt → CoreML (chỉnh L/M/precision rồi chạy lại) |
|
| 18 |
+
|
| 19 |
+
## CoreML — I/O contract
|
| 20 |
+
|
| 21 |
+
**Input**
|
| 22 |
+
- `input` : MLMultiArray `[1, 256]` Int32 — phoneme ids, pad `0` cho đủ 256.
|
| 23 |
+
- `input_lengths` : `[1]` Int32 — số phoneme id thật (≤256).
|
| 24 |
+
- `scales` : `[3]` Float32 — `[noise=0.667, length=1.0, noise_w=0.8]`. `length>1` đọc chậm hơn.
|
| 25 |
+
|
| 26 |
+
**Output**
|
| 27 |
+
- `audio` : Float32 mono 22050Hz, độ dài cố định (M·256 mẫu).
|
| 28 |
+
- `n_frames` : Int32 — số frame thật. **Phải cắt `audio` tại `n_frames*256`** rồi fade-out ngắn, nếu không sẽ có tiếng "ụp" ở cuối.
|
| 29 |
+
|
| 30 |
+
## Tích hợp iOS
|
| 31 |
+
|
| 32 |
+
1. Kéo `MaiLinh.mlpackage` vào Xcode (tự biên dịch ra `.mlmodelc` khi build).
|
| 33 |
+
2. Dùng `MaiLinhTTS.swift` — đã làm sẵn phần load model, dựng input, chạy, cắt theo `n_frames`, fade-out, và phát qua `AVAudioEngine`.
|
| 34 |
+
3. **Phần còn phải tự làm: phonemize tiếng Việt.** Model nhận *phoneme ids*, không nhận text. Cần:
|
| 35 |
+
- `espeak-ng` (voice `vi`) build cho iOS (arm64) để đổi text → chuỗi IPA.
|
| 36 |
+
- Map IPA → ids bằng `phoneme_id_map` trong `mailinh250626.onnx.json`, chèn pad id `0` giữa các phoneme (logic `phonemes_to_ids` của piper).
|
| 37 |
+
4. Câu dài > 5.8s: tách câu rồi tổng hợp từng câu (đúng cách piper làm). Muốn câu dài hơn: tăng `M` trong `convert_coreml.py` rồi convert lại.
|
| 38 |
+
|
| 39 |
+
> Lưu ý: ANE compile fail nên model chạy trên GPU/CPU (vẫn CoreML native). Đủ nhanh (~26ms/câu trên Mac).
|
| 40 |
+
|
| 41 |
+
## Mac/PC (ONNX) — nhanh, linh hoạt
|
| 42 |
+
|
| 43 |
+
```bash
|
| 44 |
+
pip install piper-tts # hoặc dùng .venv kèm theo
|
| 45 |
+
python tts.py "Xin chào các bạn" out.wav # hoặc ./say.sh "..."
|
| 46 |
+
```
|
| 47 |
+
ONNX dynamic, ~52x realtime với 4 thread; không giới hạn độ dài câu.
|
bench_coreml_vs_onnx.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""So sanh toc do model: CoreML (.mlpackage, shape tinh) vs onnx (dynamic)."""
|
| 3 |
+
import time
|
| 4 |
+
import numpy as np
|
| 5 |
+
import onnxruntime as ort
|
| 6 |
+
import coremltools as ct
|
| 7 |
+
from piper import PiperVoice
|
| 8 |
+
|
| 9 |
+
L = 256
|
| 10 |
+
SR, HOP = 22050, 256
|
| 11 |
+
ONNX = "Mai Linh/mailinh250626.onnx"
|
| 12 |
+
CONF = "Mai Linh/mailinh250626.onnx.json"
|
| 13 |
+
scales = np.array([0.667, 1.0, 0.8], dtype=np.float32)
|
| 14 |
+
|
| 15 |
+
v = PiperVoice.load(ONNX, CONF)
|
| 16 |
+
|
| 17 |
+
# onnx session, 4 threads (cau hinh nhanh nhat da do)
|
| 18 |
+
so = ort.SessionOptions(); so.intra_op_num_threads = 4; so.log_severity_level = 3
|
| 19 |
+
onnx_sess = ort.InferenceSession(ONNX, sess_options=so, providers=["CPUExecutionProvider"])
|
| 20 |
+
|
| 21 |
+
ml = ct.models.MLModel("MaiLinh.mlpackage")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def ids_for(text):
|
| 25 |
+
s = v.phonemize(text)
|
| 26 |
+
return np.asarray(v.phonemes_to_ids(s[0]), dtype=np.int64)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def bench_onnx(ids, k=7):
|
| 30 |
+
inp = ids[None, :]
|
| 31 |
+
il = np.array([ids.shape[0]], dtype=np.int64)
|
| 32 |
+
feed = {"input": inp, "input_lengths": il, "scales": scales}
|
| 33 |
+
onnx_sess.run(None, feed) # warmup
|
| 34 |
+
best, n = 1e9, 0
|
| 35 |
+
for _ in range(k):
|
| 36 |
+
t = time.perf_counter(); out = onnx_sess.run(None, feed); dt = time.perf_counter() - t
|
| 37 |
+
best = min(best, dt); n = np.asarray(out[0]).reshape(-1).shape[0]
|
| 38 |
+
return best, n / SR
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def bench_coreml(ids, k=7):
|
| 42 |
+
n = min(ids.shape[0], L)
|
| 43 |
+
arr = np.zeros((1, L), dtype=np.int32); arr[0, :n] = ids[:n]
|
| 44 |
+
feed = {"input": arr, "input_lengths": np.array([n], dtype=np.int32), "scales": scales}
|
| 45 |
+
out = ml.predict(feed) # warmup (+ ANE compile attempt)
|
| 46 |
+
a = np.asarray(out["audio"]).reshape(-1)
|
| 47 |
+
thr = 0.02 * np.max(np.abs(a)); nz = np.where(np.abs(a) > thr)[0]
|
| 48 |
+
spoken = (nz[-1] + 1) / SR if len(nz) else len(a) / SR
|
| 49 |
+
best = 1e9
|
| 50 |
+
for _ in range(k):
|
| 51 |
+
t = time.perf_counter(); ml.predict(feed); dt = time.perf_counter() - t
|
| 52 |
+
best = min(best, dt)
|
| 53 |
+
return best, spoken, len(a) / SR
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
for label, text in [("SHORT", "Xin chào các bạn."),
|
| 57 |
+
("LONG", "Xin chào, tôi là Mai Linh, rất vui được gặp tất cả các bạn ở đây ngày hôm nay nhé.")]:
|
| 58 |
+
ids = ids_for(text)
|
| 59 |
+
o_t, o_dur = bench_onnx(ids)
|
| 60 |
+
c_t, c_spoken, c_full = bench_coreml(ids)
|
| 61 |
+
print(f"\n[{label}] phonemes={ids.shape[0]} (spoken ~{c_spoken:.2f}s)")
|
| 62 |
+
print(f" onnx (4-thread, dynamic): {o_t*1000:7.1f} ms -> audio {o_dur:.2f}s RTF={o_t/o_dur:.4f} {o_dur/o_t:.0f}x")
|
| 63 |
+
print(f" coreml(.mlpackage, fixed): {c_t*1000:7.1f} ms (tinh du {c_full:.2f}s) RTF_spoken={c_t/c_spoken:.4f} {c_spoken/c_t:.0f}x")
|
convert_coreml.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Convert Mai Linh Piper VITS checkpoint -> CoreML .mlpackage (static shapes).
|
| 3 |
+
|
| 4 |
+
VITS co shape phu thuoc du lieu (do dai audio = tong duration). CoreML khong tao
|
| 5 |
+
duoc tensor co shape phu thuoc gia tri -> ep ve shape TINH hoan toan:
|
| 6 |
+
- L: do dai chuoi phoneme co dinh (pad input toi L).
|
| 7 |
+
- M: so frame audio toi da (output luon M*hop mau, ngan hon -> zero qua y_mask).
|
| 8 |
+
Patch them relative-position attention de pad thanh hang so.
|
| 9 |
+
"""
|
| 10 |
+
import sys
|
| 11 |
+
import traceback
|
| 12 |
+
|
| 13 |
+
SRC = "/private/tmp/claude-501/-Users-tmduc3-projects/2be6ca63-e3fb-464a-aa29-ca1f99db870f/scratchpad/piper1-gpl/src"
|
| 14 |
+
sys.path.insert(0, SRC)
|
| 15 |
+
|
| 16 |
+
import numpy as np
|
| 17 |
+
import torch
|
| 18 |
+
import torch.nn.functional as F
|
| 19 |
+
import coremltools as ct
|
| 20 |
+
from piper.train.vits.lightning import VitsModel
|
| 21 |
+
import piper.train.vits.attentions as A
|
| 22 |
+
|
| 23 |
+
CKPT = "Mai Linh/mailinh-epoch=0019.ckpt"
|
| 24 |
+
L = 256 # phoneme length co dinh (pad input toi L; ~1 cau dai)
|
| 25 |
+
M = 500 # max audio frames (~ M*256/22050 = 5.8s)
|
| 26 |
+
HOP = 256
|
| 27 |
+
|
| 28 |
+
# --- patch 1: coremltools _cast (numpy 2.x int(np.array([v])) bug on size-1) ---
|
| 29 |
+
import coremltools.converters.mil.frontend.torch.ops as _ctops
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _cast_fixed(context, node, dtype, dtype_name):
|
| 33 |
+
inputs = _ctops._get_inputs(context, node, expected=1)
|
| 34 |
+
x = inputs[0]
|
| 35 |
+
if not (len(x.shape) == 0 or np.all([d == 1 for d in x.shape])):
|
| 36 |
+
raise ValueError("input to cast must be either a scalar or a length 1 tensor")
|
| 37 |
+
if x.can_be_folded_to_const():
|
| 38 |
+
val = x.val
|
| 39 |
+
if isinstance(val, np.ndarray):
|
| 40 |
+
val = val.reshape(()).item()
|
| 41 |
+
res = x if isinstance(val, dtype) else _ctops.mb.const(val=dtype(val), name=node.name)
|
| 42 |
+
elif len(x.shape) > 0:
|
| 43 |
+
xs = _ctops.mb.squeeze(x=x, name=node.name + "_item")
|
| 44 |
+
res = _ctops.mb.cast(x=xs, dtype=dtype_name, name=node.name)
|
| 45 |
+
else:
|
| 46 |
+
res = _ctops.mb.cast(x=x, dtype=dtype_name, name=node.name)
|
| 47 |
+
context.add(res, node.name)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
_ctops._cast = _cast_fixed
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# aten::IntImplicit (n_channels[0] tensor->int in WN fused op) not implemented
|
| 54 |
+
@_ctops.register_torch_op
|
| 55 |
+
def intimplicit(context, node):
|
| 56 |
+
_cast_fixed(context, node, int, "int32")
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# --- patch 2: relative-position attention pads -> constant length L ---
|
| 60 |
+
MHA = A.MultiHeadAttention
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _get_rel_emb(self, relative_embeddings, length):
|
| 64 |
+
length = L
|
| 65 |
+
pad_length = max(length - (self.window_size + 1), 0)
|
| 66 |
+
s = max((self.window_size + 1) - length, 0)
|
| 67 |
+
e = s + 2 * length - 1
|
| 68 |
+
if pad_length > 0:
|
| 69 |
+
relative_embeddings = F.pad(relative_embeddings, (0, 0, pad_length, pad_length, 0, 0))
|
| 70 |
+
return relative_embeddings[:, s:e]
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _rel2abs(self, x):
|
| 74 |
+
b, h, length = 1, self.n_heads, L
|
| 75 |
+
x = F.pad(x, (0, 1, 0, 0, 0, 0, 0, 0))
|
| 76 |
+
x_flat = x.reshape(b, h, length * 2 * length)
|
| 77 |
+
x_flat = F.pad(x_flat, (0, length - 1, 0, 0, 0, 0))
|
| 78 |
+
return x_flat.reshape(b, h, length + 1, 2 * length - 1)[:, :, :length, length - 1:]
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def _abs2rel(self, x):
|
| 82 |
+
b, h, length = 1, self.n_heads, L
|
| 83 |
+
x = F.pad(x, (0, length - 1, 0, 0, 0, 0, 0, 0))
|
| 84 |
+
x_flat = x.reshape(b, h, length * length + length * (length - 1))
|
| 85 |
+
x_flat = F.pad(x_flat, (length, 0, 0, 0, 0, 0))
|
| 86 |
+
return x_flat.reshape(b, h, length, 2 * length)[:, :, :, 1:]
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
MHA._get_relative_embeddings = _get_rel_emb
|
| 90 |
+
MHA._relative_position_to_absolute_position = _rel2abs
|
| 91 |
+
MHA._absolute_position_to_relative_position = _abs2rel
|
| 92 |
+
|
| 93 |
+
# --- patch 3: rational-quadratic spline (sdp) -> CoreML-friendly form ---
|
| 94 |
+
# Bo boolean-mask indexing (shape dong) bang torch.where; bo searchsorted/gather
|
| 95 |
+
# bang arange-compare + one-hot-sum; bo moi in-place assignment.
|
| 96 |
+
import piper.train.vits.modules as MOD
|
| 97 |
+
import piper.train.vits.transforms as TR
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def _set_first_last(c, first, last):
|
| 101 |
+
fc = torch.full_like(c[..., :1], float(first))
|
| 102 |
+
lc = torch.full_like(c[..., :1], float(last))
|
| 103 |
+
return torch.cat([fc, c[..., 1:-1], lc], dim=-1)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _pwrqt_cf(inputs, uw, uh, ud, inverse=False, tails="linear", tail_bound=1.0,
|
| 107 |
+
min_bin_width=1e-3, min_bin_height=1e-3, min_derivative=1e-3):
|
| 108 |
+
assert tails == "linear"
|
| 109 |
+
num_bins = uw.shape[-1]
|
| 110 |
+
dt = inputs.dtype
|
| 111 |
+
tb = tail_bound
|
| 112 |
+
inside = (inputs >= -tb) & (inputs <= tb)
|
| 113 |
+
|
| 114 |
+
ud = F.pad(ud, (1, 1))
|
| 115 |
+
constant = float(np.log(np.exp(1 - min_derivative) - 1))
|
| 116 |
+
c0 = torch.full_like(ud[..., :1], constant)
|
| 117 |
+
ud = torch.cat([c0, ud[..., 1:-1], c0], dim=-1)
|
| 118 |
+
|
| 119 |
+
widths = F.softmax(uw, -1)
|
| 120 |
+
widths = min_bin_width + (1 - min_bin_width * num_bins) * widths
|
| 121 |
+
cumwidths = F.pad(torch.cumsum(widths, -1), (1, 0), value=0.0)
|
| 122 |
+
cumwidths = 2 * tb * cumwidths - tb
|
| 123 |
+
cumwidths = _set_first_last(cumwidths, -tb, tb)
|
| 124 |
+
widths = cumwidths[..., 1:] - cumwidths[..., :-1]
|
| 125 |
+
|
| 126 |
+
derivatives = min_derivative + F.softplus(ud)
|
| 127 |
+
|
| 128 |
+
heights = F.softmax(uh, -1)
|
| 129 |
+
heights = min_bin_height + (1 - min_bin_height * num_bins) * heights
|
| 130 |
+
cumheights = F.pad(torch.cumsum(heights, -1), (1, 0), value=0.0)
|
| 131 |
+
cumheights = 2 * tb * cumheights - tb
|
| 132 |
+
cumheights = _set_first_last(cumheights, -tb, tb)
|
| 133 |
+
heights = cumheights[..., 1:] - cumheights[..., :-1]
|
| 134 |
+
|
| 135 |
+
x = torch.clamp(inputs, -tb, tb)
|
| 136 |
+
locations = cumheights if inverse else cumwidths
|
| 137 |
+
loc = torch.cat([locations[..., :-1], locations[..., -1:] + 1e-6], dim=-1)
|
| 138 |
+
bin_idx = ((x.unsqueeze(-1) >= loc).to(dt).sum(-1) - 1).clamp(0, num_bins - 1)
|
| 139 |
+
|
| 140 |
+
ar = torch.arange(num_bins, device=inputs.device).to(dt)
|
| 141 |
+
ar1 = torch.arange(num_bins + 1, device=inputs.device).to(dt)
|
| 142 |
+
oh = (bin_idx.unsqueeze(-1) == ar).to(dt)
|
| 143 |
+
oh1 = (bin_idx.unsqueeze(-1) == ar1).to(dt)
|
| 144 |
+
gb = lambda v: (oh * v).sum(-1)
|
| 145 |
+
gc = lambda v: (oh1 * v).sum(-1)
|
| 146 |
+
|
| 147 |
+
in_cumw = gc(cumwidths)
|
| 148 |
+
in_binw = gb(widths)
|
| 149 |
+
in_cumh = gc(cumheights)
|
| 150 |
+
delta = heights / widths
|
| 151 |
+
in_delta = gb(delta)
|
| 152 |
+
in_der = gb(derivatives[..., :num_bins])
|
| 153 |
+
in_der1 = gb(derivatives[..., 1:])
|
| 154 |
+
in_h = gb(heights)
|
| 155 |
+
|
| 156 |
+
if inverse:
|
| 157 |
+
a = (x - in_cumh) * (in_der + in_der1 - 2 * in_delta) + in_h * (in_delta - in_der)
|
| 158 |
+
b = in_h * in_der - (x - in_cumh) * (in_der + in_der1 - 2 * in_delta)
|
| 159 |
+
c = -in_delta * (x - in_cumh)
|
| 160 |
+
disc = torch.clamp(b * b - 4 * a * c, min=0.0)
|
| 161 |
+
root = (2 * c) / (-b - torch.sqrt(disc))
|
| 162 |
+
out_inner = root * in_binw + in_cumw
|
| 163 |
+
else:
|
| 164 |
+
theta = (x - in_cumw) / in_binw
|
| 165 |
+
tomt = theta * (1 - theta)
|
| 166 |
+
num = in_h * (in_delta * theta.pow(2) + in_der * tomt)
|
| 167 |
+
den = in_delta + (in_der + in_der1 - 2 * in_delta) * tomt
|
| 168 |
+
out_inner = in_cumh + num / den
|
| 169 |
+
|
| 170 |
+
outputs = torch.where(inside, out_inner, inputs)
|
| 171 |
+
return outputs, torch.zeros_like(inputs)
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
# unit-check: reimpl vs original (output fidelity)
|
| 175 |
+
def _spline_unit_check():
|
| 176 |
+
torch.manual_seed(0)
|
| 177 |
+
nb = 10
|
| 178 |
+
N = 2000
|
| 179 |
+
xi = (torch.rand(N) * 12 - 6) # span inside+outside tail_bound=5
|
| 180 |
+
uw = torch.randn(N, nb); uh = torch.randn(N, nb); udv = torch.randn(N, nb - 1)
|
| 181 |
+
for inv in (True, False):
|
| 182 |
+
o0, _ = TR.piecewise_rational_quadratic_transform(
|
| 183 |
+
xi.clone(), uw.clone(), uh.clone(), udv.clone(),
|
| 184 |
+
inverse=inv, tails="linear", tail_bound=5.0)
|
| 185 |
+
o1, _ = _pwrqt_cf(xi.clone(), uw.clone(), uh.clone(), udv.clone(),
|
| 186 |
+
inverse=inv, tails="linear", tail_bound=5.0)
|
| 187 |
+
d = (o0 - o1).abs().max().item()
|
| 188 |
+
print(f" spline unit-check inverse={inv}: max|diff|={d:.2e}", flush=True)
|
| 189 |
+
assert d < 1e-3, "spline reimpl mismatch"
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
print("=== spline reimpl unit-check ===", flush=True)
|
| 193 |
+
_spline_unit_check()
|
| 194 |
+
MOD.piecewise_rational_quadratic_transform = _pwrqt_cf
|
| 195 |
+
|
| 196 |
+
# --- load model ---
|
| 197 |
+
torch.manual_seed(1234)
|
| 198 |
+
g = VitsModel.load_from_checkpoint(CKPT, map_location="cpu").model_g
|
| 199 |
+
g.eval()
|
| 200 |
+
with torch.no_grad():
|
| 201 |
+
g.dec.remove_weight_norm()
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def seq_mask_fixed(length, maxlen):
|
| 205 |
+
rng = torch.arange(maxlen, dtype=length.dtype, device=length.device)
|
| 206 |
+
return rng.unsqueeze(0) < length.unsqueeze(1)
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def gen_path_fixed(duration, mask):
|
| 210 |
+
# duration [b,1,L], mask [b,1,M,L] -> path [b,1,M,L]
|
| 211 |
+
b = 1
|
| 212 |
+
cum = torch.cumsum(duration, -1).view(b * L)
|
| 213 |
+
path = seq_mask_fixed(cum, M).type_as(mask).view(b, L, M)
|
| 214 |
+
path = path - F.pad(path, (0, 0, 1, 0, 0, 0))[:, :-1]
|
| 215 |
+
return (path.unsqueeze(1).transpose(2, 3)) * mask
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
class StaticInfer(torch.nn.Module):
|
| 219 |
+
def __init__(self, g):
|
| 220 |
+
super().__init__()
|
| 221 |
+
self.g = g
|
| 222 |
+
|
| 223 |
+
def forward(self, text, text_lengths, scales):
|
| 224 |
+
g = self.g
|
| 225 |
+
noise_scale, length_scale, noise_scale_w = scales[0], scales[1], scales[2]
|
| 226 |
+
x, m_p, logs_p, x_mask = g.enc_p(text, text_lengths)
|
| 227 |
+
logw = g.dp(x, x_mask, g=None, reverse=True, noise_scale=noise_scale_w)
|
| 228 |
+
w = torch.exp(logw) * x_mask * length_scale
|
| 229 |
+
w_ceil = torch.ceil(w)
|
| 230 |
+
y_lengths = torch.clamp_min(torch.sum(w_ceil, [1, 2]), 1.0).long()
|
| 231 |
+
y_mask = seq_mask_fixed(y_lengths, M).unsqueeze(1).type_as(x_mask) # [b,1,M]
|
| 232 |
+
attn_mask = x_mask.unsqueeze(2) * y_mask.unsqueeze(-1) # [b,1,M,L]
|
| 233 |
+
attn = gen_path_fixed(w_ceil, attn_mask) # [b,1,M,L]
|
| 234 |
+
m_p = torch.matmul(attn.squeeze(1), m_p.transpose(1, 2)).transpose(1, 2)
|
| 235 |
+
logs_p = torch.matmul(attn.squeeze(1), logs_p.transpose(1, 2)).transpose(1, 2)
|
| 236 |
+
z_p = m_p + torch.randn_like(m_p) * torch.exp(logs_p) * noise_scale
|
| 237 |
+
z = g.flow(z_p, y_mask, g=None, reverse=True)
|
| 238 |
+
o = g.dec(z * y_mask, g=None) # [b,1,M*HOP]
|
| 239 |
+
return o, y_lengths # y_lengths = so frame audio that su -> cat dung cho het noise duoi
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
net = StaticInfer(g).eval()
|
| 243 |
+
|
| 244 |
+
seq = torch.randint(1, g.n_vocab, (1, L), dtype=torch.long)
|
| 245 |
+
ln = torch.LongTensor([L])
|
| 246 |
+
scales_det = torch.FloatTensor([0.0, 1.0, 0.0]) # deterministic for validation
|
| 247 |
+
|
| 248 |
+
print(f"=== L={L} M={M} -> audio {M*HOP} samples ({M*HOP/22050:.2f}s) ===", flush=True)
|
| 249 |
+
print("=== stage 1: trace ===", flush=True)
|
| 250 |
+
with torch.no_grad():
|
| 251 |
+
ref, ref_nf = net(seq, ln, scales_det)
|
| 252 |
+
traced = torch.jit.trace(net, (seq, ln, scales_det), check_trace=False)
|
| 253 |
+
print("trace OK, ref audio:", tuple(ref.shape), flush=True)
|
| 254 |
+
|
| 255 |
+
print("=== stage 2: coremltools.convert (static) ===", flush=True)
|
| 256 |
+
try:
|
| 257 |
+
mlmodel = ct.convert(
|
| 258 |
+
traced,
|
| 259 |
+
inputs=[
|
| 260 |
+
ct.TensorType(name="input", shape=(1, L), dtype=np.int32),
|
| 261 |
+
ct.TensorType(name="input_lengths", shape=(1,), dtype=np.int32),
|
| 262 |
+
ct.TensorType(name="scales", shape=(3,), dtype=np.float32),
|
| 263 |
+
],
|
| 264 |
+
outputs=[ct.TensorType(name="audio"), ct.TensorType(name="n_frames")],
|
| 265 |
+
minimum_deployment_target=ct.target.iOS16,
|
| 266 |
+
convert_to="mlprogram",
|
| 267 |
+
compute_precision=ct.precision.FLOAT32, # giu fp32 nhu ban goc, tranh nhieu fp16
|
| 268 |
+
compute_units=ct.ComputeUnit.ALL,
|
| 269 |
+
)
|
| 270 |
+
# metadata (set trong-bo-nho truoc khi save; KHONG load+save lai vi save ve
|
| 271 |
+
# cung path se xoa target roi copy that bai -> mat file)
|
| 272 |
+
mlmodel.short_description = "Mai Linh - Vietnamese TTS (Piper/VITS) CoreML. Input phoneme ids -> 22050Hz mono audio."
|
| 273 |
+
mlmodel.author = "Hert4 (beyoru). Voice: quangdung/Piper_checkpoint. Base: OHF-Voice/piper1-gpl."
|
| 274 |
+
mlmodel.license = "MIT"
|
| 275 |
+
mlmodel.version = "1.0"
|
| 276 |
+
mlmodel.input_description["input"] = "Phoneme ids (int32), pad 0 toi do dai 256."
|
| 277 |
+
mlmodel.input_description["input_lengths"] = "So phoneme id thuc (int32, <=256)."
|
| 278 |
+
mlmodel.input_description["scales"] = "[noise, length, noise_w], mac dinh [0.667,1.0,0.8]; length>1 doc cham."
|
| 279 |
+
mlmodel.output_description["audio"] = "Waveform fp32 mono 22050Hz, do dai co dinh; cat tai n_frames*256."
|
| 280 |
+
mlmodel.output_description["n_frames"] = "So frame that; cat audio tai n_frames*256 roi fade-out."
|
| 281 |
+
_u = mlmodel.user_defined_metadata
|
| 282 |
+
_u.update({"sample_rate": "22050", "hop_length": "256", "max_phonemes": str(L),
|
| 283 |
+
"max_frames": str(M), "default_scales": "0.667,1.0,0.8", "language": "vi",
|
| 284 |
+
"voice": "Mai Linh", "phonemizer": "espeak-ng vi + phoneme_id_map (mailinh250626.onnx.json)"})
|
| 285 |
+
|
| 286 |
+
_OUT = "/Users/tmduc3/projects/piper-mailinh/MaiLinh.mlpackage"
|
| 287 |
+
import os as _os, shutil as _sh
|
| 288 |
+
if _os.path.exists(_OUT):
|
| 289 |
+
_sh.rmtree(_OUT)
|
| 290 |
+
mlmodel.save(_OUT)
|
| 291 |
+
print(f"CONVERT OK -> {_OUT} exists={_os.path.exists(_OUT)}", flush=True)
|
| 292 |
+
except Exception:
|
| 293 |
+
print("CONVERT FAILED:", flush=True)
|
| 294 |
+
traceback.print_exc()
|
| 295 |
+
sys.exit(2)
|
| 296 |
+
|
| 297 |
+
print("=== stage 3: validate CoreML vs PyTorch (deterministic) ===", flush=True)
|
| 298 |
+
out = mlmodel.predict({
|
| 299 |
+
"input": seq.numpy().astype(np.int32),
|
| 300 |
+
"input_lengths": ln.numpy().astype(np.int32),
|
| 301 |
+
"scales": scales_det.numpy().astype(np.float32),
|
| 302 |
+
})
|
| 303 |
+
ca = np.asarray(out["audio"]).reshape(-1)
|
| 304 |
+
ra = ref.numpy().reshape(-1)
|
| 305 |
+
n = min(len(ca), len(ra))
|
| 306 |
+
diff = np.abs(ca[:n] - ra[:n])
|
| 307 |
+
print(f"coreml audio shape {ca.shape}, pytorch {ra.shape}", flush=True)
|
| 308 |
+
print(f"max|diff|={diff.max():.6f} mean|diff|={diff.mean():.6e}", flush=True)
|
| 309 |
+
print(f"n_frames coreml={int(np.asarray(out['n_frames']).reshape(-1)[0])} pytorch={int(ref_nf.reshape(-1)[0])}", flush=True)
|
coreml_tts.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Inference giong Mai Linh bang CoreML MaiLinh.mlpackage (khong can onnxruntime
|
| 3 |
+
cho phan model; chi dung piper de phonemize text -> ids)."""
|
| 4 |
+
import sys
|
| 5 |
+
import wave
|
| 6 |
+
import numpy as np
|
| 7 |
+
import coremltools as ct
|
| 8 |
+
from piper import PiperVoice
|
| 9 |
+
|
| 10 |
+
L = 256
|
| 11 |
+
SR = 22050
|
| 12 |
+
HOP = 256
|
| 13 |
+
|
| 14 |
+
text = sys.argv[1] if len(sys.argv) > 1 else "Xin chào, tôi là Mai Linh."
|
| 15 |
+
out = sys.argv[2] if len(sys.argv) > 2 else "coreml_demo.wav"
|
| 16 |
+
|
| 17 |
+
# phonemize (dung config da sinh)
|
| 18 |
+
v = PiperVoice.load("Mai Linh/mailinh250626.onnx", "Mai Linh/mailinh250626.onnx.json")
|
| 19 |
+
sentences = v.phonemize(text)
|
| 20 |
+
ids = v.phonemes_to_ids(sentences[0])
|
| 21 |
+
n = min(len(ids), L)
|
| 22 |
+
arr = np.zeros((1, L), dtype=np.int32)
|
| 23 |
+
arr[0, :n] = np.asarray(ids[:n], dtype=np.int32)
|
| 24 |
+
|
| 25 |
+
m = ct.models.MLModel("/Users/tmduc3/projects/piper-mailinh/MaiLinh.mlpackage")
|
| 26 |
+
res = m.predict({
|
| 27 |
+
"input": arr,
|
| 28 |
+
"input_lengths": np.array([n], dtype=np.int32),
|
| 29 |
+
"scales": np.array([0.667, 1.0, 0.8], dtype=np.float32),
|
| 30 |
+
})
|
| 31 |
+
audio = np.asarray(res["audio"]).reshape(-1)
|
| 32 |
+
|
| 33 |
+
# cat dung tai do dai that (n_frames*HOP) -> bo het vung mask=0 va transient "up"
|
| 34 |
+
nf = int(np.asarray(res["n_frames"]).reshape(-1)[0])
|
| 35 |
+
cut = min(nf * HOP, len(audio))
|
| 36 |
+
audio = audio[:cut].copy()
|
| 37 |
+
|
| 38 |
+
# fade-out ~8ms de xoa transient con sot o ranh gioi (do receptive field cua decoder)
|
| 39 |
+
f = min(int(0.008 * SR), len(audio))
|
| 40 |
+
if f > 1:
|
| 41 |
+
audio[-f:] *= 0.5 * (1 + np.cos(np.linspace(0.0, np.pi, f)))
|
| 42 |
+
|
| 43 |
+
pcm = (np.clip(audio, -1, 1) * 32767).astype(np.int16)
|
| 44 |
+
with wave.open(out, "wb") as w:
|
| 45 |
+
w.setnchannels(1)
|
| 46 |
+
w.setsampwidth(2)
|
| 47 |
+
w.setframerate(SR)
|
| 48 |
+
w.writeframes(pcm.tobytes())
|
| 49 |
+
print(f"phonemes={n} audio={len(audio)/SR:.2f}s saved: {out}")
|
say.sh
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Mai Linh - Vietnamese TTS (Piper) - CLI đơn giản, tối ưu 4 thread.
|
| 3 |
+
# Usage:
|
| 4 |
+
# ./say.sh "câu tiếng việt" -> out.wav (tự phát)
|
| 5 |
+
# ./say.sh "câu tiếng việt" giong.wav -> giong.wav
|
| 6 |
+
# ./say.sh "câu tiếng việt" giong.wav 1.2 -> đọc chậm hơn
|
| 7 |
+
set -euo pipefail
|
| 8 |
+
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 9 |
+
|
| 10 |
+
TEXT="${1:?Usage: ./say.sh \"text\" [out.wav] [length_scale]}"
|
| 11 |
+
OUT="${2:-out.wav}"
|
| 12 |
+
LEN="${3:-1.0}"
|
| 13 |
+
|
| 14 |
+
"$DIR/.venv/bin/python" "$DIR/tts.py" "$TEXT" "$OUT" "$LEN"
|
| 15 |
+
command -v afplay >/dev/null 2>&1 && afplay "$OUT"
|
tts.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Mai Linh - Vietnamese TTS (Piper) - CLI tối ưu 4 thread cho Apple Silicon.
|
| 3 |
+
|
| 4 |
+
python tts.py "câu tiếng việt" -> out.wav
|
| 5 |
+
python tts.py "câu tiếng việt" giong.wav -> giong.wav
|
| 6 |
+
python tts.py "câu tiếng việt" giong.wav 1.2 -> đọc chậm hơn (length_scale)
|
| 7 |
+
echo "câu" | python tts.py -> đọc từ stdin
|
| 8 |
+
"""
|
| 9 |
+
import sys
|
| 10 |
+
import wave
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
import onnxruntime as ort
|
| 14 |
+
from piper import PiperVoice, SynthesisConfig
|
| 15 |
+
|
| 16 |
+
DIR = Path(__file__).resolve().parent
|
| 17 |
+
MODEL = DIR / "Mai Linh" / "mailinh250626.onnx"
|
| 18 |
+
CONF = DIR / "Mai Linh" / "mailinh250626.onnx.json"
|
| 19 |
+
THREADS = 4 # sweet spot benchmark trên chip M (52x realtime); >4 chậm lại
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def main() -> None:
|
| 23 |
+
text = sys.argv[1] if len(sys.argv) > 1 else sys.stdin.read()
|
| 24 |
+
out = sys.argv[2] if len(sys.argv) > 2 else "out.wav"
|
| 25 |
+
length = float(sys.argv[3]) if len(sys.argv) > 3 else 1.0
|
| 26 |
+
|
| 27 |
+
so = ort.SessionOptions()
|
| 28 |
+
so.intra_op_num_threads = THREADS
|
| 29 |
+
so.log_severity_level = 3
|
| 30 |
+
|
| 31 |
+
voice = PiperVoice.load(str(MODEL), str(CONF))
|
| 32 |
+
voice.session = ort.InferenceSession(
|
| 33 |
+
str(MODEL), sess_options=so, providers=["CPUExecutionProvider"]
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
syn = SynthesisConfig(length_scale=length)
|
| 37 |
+
sr = voice.config.sample_rate
|
| 38 |
+
with wave.open(out, "wb") as w:
|
| 39 |
+
w.setnchannels(1)
|
| 40 |
+
w.setsampwidth(2)
|
| 41 |
+
w.setframerate(sr)
|
| 42 |
+
for chunk in voice.synthesize(text.strip(), syn_config=syn):
|
| 43 |
+
w.writeframes(chunk.audio_int16_bytes)
|
| 44 |
+
|
| 45 |
+
print(f"saved: {out}")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
if __name__ == "__main__":
|
| 49 |
+
main()
|