beyoru commited on
Commit
1d16aee
·
verified ·
1 Parent(s): 275ed72

Mai Linh Vietnamese TTS: CoreML (.mlpackage) + ONNX + Swift + scripts

Browse files
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()