echogarden 2.0.13 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/data/lexicons/heteronyms.en.json +45 -3
  3. package/data/schemas/options.json +64 -0
  4. package/dist/api/Synthesis.d.ts +10 -1
  5. package/dist/api/Synthesis.js +76 -9
  6. package/dist/api/Synthesis.js.map +1 -1
  7. package/dist/recognition/WhisperSTT.js +7 -4
  8. package/dist/recognition/WhisperSTT.js.map +1 -1
  9. package/dist/synthesis/GnuSpeechTTS.d.ts +12 -0
  10. package/dist/synthesis/GnuSpeechTTS.js +36 -0
  11. package/dist/synthesis/GnuSpeechTTS.js.map +1 -0
  12. package/dist/synthesis/KokoroTTS.d.ts +22 -0
  13. package/dist/synthesis/KokoroTTS.js +595 -0
  14. package/dist/synthesis/KokoroTTS.js.map +1 -0
  15. package/dist/synthesis/OpenAICloudTTS.js +15 -0
  16. package/dist/synthesis/OpenAICloudTTS.js.map +1 -1
  17. package/dist/synthesis/VitsTTS.js +6 -1
  18. package/dist/synthesis/VitsTTS.js.map +1 -1
  19. package/dist/utilities/FileReader.js +1 -1
  20. package/dist/utilities/PackageManager.js +4 -0
  21. package/dist/utilities/PackageManager.js.map +1 -1
  22. package/dist/utilities/Utilities.d.ts +1 -0
  23. package/dist/utilities/Utilities.js +8 -0
  24. package/dist/utilities/Utilities.js.map +1 -1
  25. package/dist/utilities/WasmMemoryManager.d.ts +1 -1
  26. package/docs/Development.md +1 -1
  27. package/docs/Engines.md +4 -1
  28. package/docs/Licenses.md +2 -1
  29. package/docs/Options.md +9 -5
  30. package/docs/Tasklist.md +1 -1
  31. package/package.json +14 -13
  32. package/src/api/Synthesis.ts +132 -10
  33. package/src/recognition/WhisperSTT.ts +8 -4
  34. package/src/synthesis/GnuSpeechTTS.ts +40 -0
  35. package/src/synthesis/KokoroTTS.ts +693 -0
  36. package/src/synthesis/OpenAICloudTTS.ts +15 -0
  37. package/src/synthesis/VitsTTS.ts +6 -1
  38. package/src/utilities/FileReader.ts +1 -1
  39. package/src/utilities/PackageManager.ts +5 -0
  40. package/src/utilities/Utilities.ts +10 -0
@@ -0,0 +1,693 @@
1
+ import type * as Onnx from 'onnxruntime-node'
2
+ import { Logger } from '../utilities/Logger.js'
3
+ import { readdir, readFileAsBinary } from '../utilities/FileSystem.js'
4
+ import { joinPath } from '../utilities/PathUtilities.js'
5
+ import { getOnnxSessionOptions, OnnxExecutionProvider } from '../utilities/OnnxUtilities.js'
6
+ import { SynthesisVoice } from '../api/Synthesis.js'
7
+ import { concatAudioSegments, getRawAudioDuration, RawAudio } from '../audio/AudioUtilities.js'
8
+ import { defaultEspeakOptions, EspeakOptions } from '../synthesis/EspeakTTS.js'
9
+ import { Lexicon } from '../nlp/Lexicon.js'
10
+ import { indexOfLastMatchingNumberInRange } from '../utilities/Utilities.js'
11
+ import { simplifyPunctuationCharacters } from '../nlp/TextNormalizer.js'
12
+ import { getShortLanguageCode } from '../utilities/Locale.js'
13
+
14
+ const cachedInstanceLookup = new Map<string, KokoroTTS>()
15
+
16
+ export async function synthesizeSentence(
17
+ text: string,
18
+ voice: SynthesisVoice,
19
+ speed: number,
20
+ lexicons: Lexicon[],
21
+ modelPath: string,
22
+ voicesPath: string,
23
+ executionProviders: OnnxExecutionProvider[]
24
+ ) {
25
+ const cacheLookupKey = `${modelPath} ${voicesPath}`
26
+
27
+ let kokoroTTS: KokoroTTS | undefined = cachedInstanceLookup.get(cacheLookupKey)
28
+
29
+ if (!kokoroTTS) {
30
+ kokoroTTS = new KokoroTTS(modelPath, voicesPath, executionProviders)
31
+
32
+ cachedInstanceLookup.clear()
33
+ cachedInstanceLookup.set(cacheLookupKey, kokoroTTS)
34
+ }
35
+
36
+ const result = await kokoroTTS.synthesizeSentence(text, voice, speed, lexicons)
37
+
38
+ return result
39
+ }
40
+
41
+ export class KokoroTTS {
42
+ session?: Onnx.InferenceSession
43
+
44
+ constructor(
45
+ public readonly modelPath: string,
46
+ public readonly voicesPath: string,
47
+ public readonly executionProviders: OnnxExecutionProvider[]
48
+ ) {
49
+ }
50
+
51
+ async synthesizeSentence(sentenceText: string, voice: SynthesisVoice, speed: number, lexicons: Lexicon[]) {
52
+ await this.initializeSessionIfNeeded()
53
+
54
+ const logger = new Logger()
55
+
56
+ const Onnx = await import('onnxruntime-node')
57
+
58
+ const voiceEntry = voiceList.find(entry => entry.name === voice.name && entry.languages[0] === voice.languages[0])
59
+
60
+ if (!voiceEntry) {
61
+ throw new Error(`Kokoro voice '${voice.name}' was not found.`)
62
+ }
63
+
64
+ sentenceText = //simplifyPunctuationCharacters(sentence.trim())
65
+ sentenceText
66
+ .replaceAll('(', ', ')
67
+ .replaceAll(')', ', ')
68
+ .replaceAll('—', ', ')
69
+
70
+ const simplifiedSentenceText = simplifyPunctuationCharacters(sentenceText.trim())
71
+
72
+ const voiceLanguage = voiceEntry.languages[0]
73
+ const voiceGender = voiceEntry.gender
74
+ const voiceId = voiceEntry.name.toLocaleLowerCase()
75
+
76
+ let filenamePrefix = filenameLanguagePrefixLookup[voiceLanguage]
77
+
78
+ if (filenamePrefix === undefined) {
79
+ throw new Error(`Unsupported voice language '${voiceLanguage}'`)
80
+ }
81
+
82
+ if (voiceGender === 'male') {
83
+ filenamePrefix += 'm'
84
+ } else if (voiceGender === 'female') {
85
+ filenamePrefix += 'f'
86
+ } else {
87
+ throw new Error(`Unsupported voice gender '${voiceGender}'`)
88
+ }
89
+
90
+ const voicePath = joinPath(this.voicesPath, `${filenamePrefix}_${voiceId}.bin`)
91
+
92
+ const voiceFile = new Float32Array((await readFileAsBinary(voicePath)).buffer)
93
+
94
+ const Espeak = await import('../synthesis/EspeakTTS.js')
95
+
96
+ const espeakOptions: EspeakOptions = {
97
+ ...defaultEspeakOptions,
98
+ voice: languageToEspeakVoice[voiceLanguage],
99
+ useKlatt: false
100
+ }
101
+
102
+ //await logger.startAsync('Phonemize text')
103
+
104
+ const {
105
+ referenceSynthesizedAudio,
106
+ referenceTimeline,
107
+ fragments,
108
+ phonemizedFragmentsSubstitutions,
109
+ phonemizedSentence
110
+ } = await Espeak.preprocessAndSynthesize(sentenceText, voiceLanguage, espeakOptions, lexicons)
111
+
112
+ logger.end()
113
+
114
+ const clauseBreakTokenId = charToTokenIDLookup[',']
115
+ const wordBreakTokenId = charToTokenIDLookup[' ']
116
+
117
+ let sentenceEndChar: string
118
+
119
+ if (simplifiedSentenceText.endsWith('?') || simplifiedSentenceText.endsWith(`?"`) || simplifiedSentenceText.endsWith(`?)`)) {
120
+ sentenceEndChar = '?'
121
+ } else if (simplifiedSentenceText.endsWith('!') || simplifiedSentenceText.endsWith(`!"`) || simplifiedSentenceText.endsWith(`!)`)) {
122
+ sentenceEndChar = '!'
123
+ } else {
124
+ sentenceEndChar = '.'
125
+ }
126
+
127
+ const sentenceEndTokenId = charToTokenIDLookup[sentenceEndChar]
128
+
129
+ const allTokenIds: number[] = []
130
+
131
+ for (let clauseIndex = 0; clauseIndex < phonemizedSentence.length; clauseIndex++) {
132
+ const clause = phonemizedSentence[clauseIndex]
133
+
134
+ for (let wordIndex = 0; wordIndex < clause.length; wordIndex++) {
135
+ const word = clause[wordIndex]
136
+
137
+ for (const phoneme of word) {
138
+ let processedPhoneme = phoneme
139
+
140
+ if (getShortLanguageCode(voice.languages[0]) === 'en') {
141
+ processedPhoneme = processedPhoneme
142
+ .replace(/ʲ/g, "j")
143
+ .replace(/r/g, "ɹ")
144
+ .replace(/x/g, "k")
145
+ .replace(/ɬ/g, "l")
146
+ }
147
+
148
+ for (const phonemeCharacter of processedPhoneme) {
149
+ const id = charToTokenIDLookup[phonemeCharacter]
150
+
151
+ if (id !== undefined) {
152
+ allTokenIds.push(id)
153
+ }
154
+ }
155
+ }
156
+
157
+ if (wordIndex < clause.length - 1) {
158
+ allTokenIds.push(wordBreakTokenId)
159
+ } else {
160
+ if (clauseIndex < phonemizedSentence.length - 1) {
161
+ allTokenIds.push(clauseBreakTokenId)
162
+ allTokenIds.push(wordBreakTokenId)
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ allTokenIds.push(sentenceEndTokenId)
169
+
170
+ const maxPartLength = 510
171
+
172
+ const parts: number[][] = []
173
+
174
+ {
175
+ let startIndex = 0
176
+ let endIndex = 0
177
+
178
+ while (endIndex < allTokenIds.length) {
179
+ endIndex = startIndex + maxPartLength
180
+
181
+ if (endIndex >= allTokenIds.length) {
182
+ endIndex = allTokenIds.length
183
+ } else {
184
+ const indexOfLastClauseBreak = indexOfLastMatchingNumberInRange(allTokenIds, clauseBreakTokenId, startIndex, endIndex)
185
+
186
+ if (indexOfLastClauseBreak !== -1) {
187
+ endIndex = indexOfLastClauseBreak + 1
188
+ } else {
189
+ const indexOfLastWordBreak = indexOfLastMatchingNumberInRange(allTokenIds, wordBreakTokenId, startIndex, endIndex)
190
+
191
+ if (indexOfLastWordBreak !== -1) {
192
+ endIndex = indexOfLastWordBreak + 1
193
+ }
194
+ }
195
+ }
196
+
197
+ const partTokenIds = allTokenIds.slice(startIndex, endIndex)
198
+
199
+ if (partTokenIds[partTokenIds.length - 1] === clauseBreakTokenId) {
200
+ partTokenIds.pop()
201
+ }
202
+
203
+
204
+ if (partTokenIds.find(
205
+ tokenId => ![wordBreakTokenId, clauseBreakTokenId, sentenceEndTokenId].includes(tokenId))) {
206
+
207
+ parts.push([0, ...partTokenIds, 0])
208
+ }
209
+
210
+ startIndex = endIndex
211
+ }
212
+ }
213
+
214
+ const styleLength = 256
215
+ const sampleRate = 24000
216
+
217
+ const audioParts: Float32Array[][] = []
218
+
219
+ for (let partIndex = 0; partIndex < parts.length; partIndex++) {
220
+ if (parts.length === 1) {
221
+ await logger.startAsync(`Synthesize sentence with ONNX model`)
222
+ } else {
223
+ await logger.startAsync(`Synthesize sentence fragment ${partIndex + 1}/${parts.length} with ONNX model`)
224
+ }
225
+
226
+ const part = parts[partIndex]
227
+
228
+ const styleDataStartOffset = part.length * styleLength
229
+
230
+ const styleData = voiceFile.slice(styleDataStartOffset, styleDataStartOffset + styleLength)
231
+
232
+ const modelInputs = {
233
+ input_ids: new Onnx.Tensor('int64', BigInt64Array.from(part.map(x => BigInt(x))), [1, part.length]),
234
+ style: new Onnx.Tensor('float32', styleData, [1, styleLength]),
235
+ speed: new Onnx.Tensor('float32', [speed], [1]),
236
+ }
237
+
238
+ const result = await this.session!.run(modelInputs)
239
+
240
+ const outSamples = result.waveform.data as Float32Array
241
+
242
+ audioParts.push([outSamples])
243
+ }
244
+
245
+ logger.end()
246
+
247
+ const concatenatedAudioParts = audioParts.length > 0 ? concatAudioSegments(audioParts) : [new Float32Array(0)]
248
+
249
+ const synthesizedAudio: RawAudio = { audioChannels: concatenatedAudioParts, sampleRate }
250
+
251
+ await logger.startAsync('Align with reference synthesized audio')
252
+
253
+ const { alignUsingDtw } = await import('../alignment/SpeechAlignment.js')
254
+
255
+ const referenceWordTimeline = referenceTimeline.flatMap(clause => clause.timeline!)
256
+
257
+ const dtwWindowDuration = Math.max(5, Math.ceil(0.2 * getRawAudioDuration(synthesizedAudio)))
258
+ const mappedTimeline = await alignUsingDtw(synthesizedAudio, referenceSynthesizedAudio, referenceWordTimeline, ['high'], [dtwWindowDuration])
259
+
260
+ logger.end()
261
+
262
+ return { rawAudio: synthesizedAudio, timeline: mappedTimeline }
263
+ }
264
+
265
+ async initializeSessionIfNeeded() {
266
+ if (this.session) {
267
+ return
268
+ }
269
+
270
+ const logger = new Logger()
271
+ await logger.startAsync('Initialize Kokoro ONNX synthesis model')
272
+
273
+ const filesInModelPath = await readdir(this.modelPath)
274
+ const onnxModelFilename = filesInModelPath.find(filename => filename.endsWith('.onnx'))
275
+
276
+ if (!onnxModelFilename) {
277
+ throw new Error(`Couldn't file any ONNX model file in ${this.modelPath}`)
278
+ }
279
+
280
+ const onnxModelFilepath = joinPath(this.modelPath, onnxModelFilename)
281
+ const onnxSessionOptions = getOnnxSessionOptions({ executionProviders: this.executionProviders })
282
+
283
+ const Onnx = await import('onnxruntime-node')
284
+
285
+ this.session = await Onnx.InferenceSession.create(onnxModelFilepath, onnxSessionOptions)
286
+
287
+ logger.end()
288
+ }
289
+ }
290
+
291
+ const charToTokenIDLookup: Record<string, number> = {
292
+ ';': 1,
293
+ ':': 2,
294
+ ',': 3,
295
+ '.': 4,
296
+ '!': 5,
297
+ '?': 6,
298
+ '—': 9,
299
+ '…': 10,
300
+ '\'': 11,
301
+ '(': 12,
302
+ ')': 13,
303
+ '“': 14,
304
+ '”': 15,
305
+ ' ': 16,
306
+ '\u0303': 17,
307
+ 'ʣ': 18,
308
+ 'ʥ': 19,
309
+ 'ʦ': 20,
310
+ 'ʨ': 21,
311
+ 'ᵝ': 22,
312
+ '\uAB67': 23,
313
+ 'A': 24,
314
+ 'I': 25,
315
+ 'O': 31,
316
+ 'Q': 33,
317
+ 'S': 35,
318
+ 'T': 36,
319
+ 'W': 39,
320
+ 'Y': 41,
321
+ 'ᵊ': 42,
322
+ 'a': 43,
323
+ 'b': 44,
324
+ 'c': 45,
325
+ 'd': 46,
326
+ 'e': 47,
327
+ 'f': 48,
328
+ 'h': 50,
329
+ 'i': 51,
330
+ 'j': 52,
331
+ 'k': 53,
332
+ 'l': 54,
333
+ 'm': 55,
334
+ 'n': 56,
335
+ 'o': 57,
336
+ 'p': 58,
337
+ 'q': 59,
338
+ 'r': 60,
339
+ 's': 61,
340
+ 't': 62,
341
+ 'u': 63,
342
+ 'v': 64,
343
+ 'w': 65,
344
+ 'x': 66,
345
+ 'y': 67,
346
+ 'z': 68,
347
+ 'ɑ': 69,
348
+ 'ɐ': 70,
349
+ 'ɒ': 71,
350
+ 'æ': 72,
351
+ 'β': 75,
352
+ 'ɔ': 76,
353
+ 'ɕ': 77,
354
+ 'ç': 78,
355
+ 'ɖ': 80,
356
+ 'ð': 81,
357
+ 'ʤ': 82,
358
+ 'ə': 83,
359
+ 'ɚ': 85,
360
+ 'ɛ': 86,
361
+ 'ɜ': 87,
362
+ 'ɟ': 90,
363
+ 'ɡ': 92,
364
+ 'ɥ': 99,
365
+ 'ɨ': 101,
366
+ 'ɪ': 102,
367
+ 'ʝ': 103,
368
+ 'ɯ': 110,
369
+ 'ɰ': 111,
370
+ 'ŋ': 112,
371
+ 'ɳ': 113,
372
+ 'ɲ': 114,
373
+ 'ɴ': 115,
374
+ 'ø': 116,
375
+ 'ɸ': 118,
376
+ 'θ': 119,
377
+ 'œ': 120,
378
+ 'ɹ': 123,
379
+ 'ɾ': 125,
380
+ 'ɻ': 126,
381
+ 'ʁ': 128,
382
+ 'ɽ': 129,
383
+ 'ʂ': 130,
384
+ 'ʃ': 131,
385
+ 'ʈ': 132,
386
+ 'ʧ': 133,
387
+ 'ʊ': 135,
388
+ 'ʋ': 136,
389
+ 'ʌ': 138,
390
+ 'ɣ': 139,
391
+ 'ɤ': 140,
392
+ 'χ': 142,
393
+ 'ʎ': 143,
394
+ 'ʒ': 147,
395
+ 'ʔ': 148,
396
+ 'ˈ': 156,
397
+ 'ˌ': 157,
398
+ 'ː': 158,
399
+ 'ʰ': 162,
400
+ 'ʲ': 164,
401
+ '↓': 169,
402
+ '→': 171,
403
+ '↗': 172,
404
+ '↘': 173,
405
+ 'ᵻ': 177
406
+ }
407
+
408
+ const filenameLanguagePrefixLookup: Record<string, string> = {
409
+ 'en-US': 'a',
410
+ 'en-GB': 'b',
411
+ 'es-ES': 'e',
412
+ 'fr-FR': 'f',
413
+ 'hi-IN': 'h',
414
+ 'it-IT': 'i',
415
+ 'ja-JP': 'j',
416
+ 'pt-BR': 'p',
417
+ 'zh-CN': 'z',
418
+ }
419
+
420
+ const languageToEspeakVoice: Record<string, string> = {
421
+ 'en-US': 'en-us',
422
+ 'en-GB': 'en-gb-x-rp',
423
+ 'es-ES': 'es-419',
424
+ 'fr-FR': 'fr',
425
+ 'hi-IN': 'hi',
426
+ 'it-IT': 'it',
427
+ 'ja-JP': 'ja',
428
+ 'pt-BR': 'pt-br',
429
+ 'zh-CN': 'cmn',
430
+ }
431
+
432
+ export const voiceList: SynthesisVoice[] = [
433
+ // US English voices
434
+ {
435
+ name: 'Heart',
436
+ languages: ['en-US', 'en'],
437
+ gender: 'female',
438
+ },
439
+ {
440
+ name: 'Bella',
441
+ languages: ['en-US', 'en'],
442
+ gender: 'female',
443
+ },
444
+ {
445
+ name: 'Nicole',
446
+ languages: ['en-US', 'en'],
447
+ gender: 'female',
448
+ },
449
+ {
450
+ name: 'Aoede',
451
+ languages: ['en-US', 'en'],
452
+ gender: 'female',
453
+ },
454
+ {
455
+ name: 'Kore',
456
+ languages: ['en-US', 'en'],
457
+ gender: 'female',
458
+ },
459
+ {
460
+ name: 'Sarah',
461
+ languages: ['en-US', 'en'],
462
+ gender: 'female',
463
+ },
464
+ {
465
+ name: 'Nova',
466
+ languages: ['en-US', 'en'],
467
+ gender: 'female',
468
+ },
469
+ {
470
+ name: 'Sky',
471
+ languages: ['en-US', 'en'],
472
+ gender: 'female',
473
+ },
474
+ {
475
+ name: 'Alloy',
476
+ languages: ['en-US', 'en'],
477
+ gender: 'female',
478
+ },
479
+ {
480
+ name: 'Jessica',
481
+ languages: ['en-US', 'en'],
482
+ gender: 'female',
483
+ },
484
+ {
485
+ name: 'River',
486
+ languages: ['en-US', 'en'],
487
+ gender: 'female',
488
+ },
489
+ {
490
+ name: 'Michael',
491
+ languages: ['en-US', 'en'],
492
+ gender: 'male',
493
+ },
494
+ {
495
+ name: 'Fenrir',
496
+ languages: ['en-US', 'en'],
497
+ gender: 'male',
498
+ },
499
+ {
500
+ name: 'Puck',
501
+ languages: ['en-US', 'en'],
502
+ gender: 'male',
503
+ },
504
+ {
505
+ name: 'Echo',
506
+ languages: ['en-US', 'en'],
507
+ gender: 'male',
508
+ },
509
+ {
510
+ name: 'Eric',
511
+ languages: ['en-US', 'en'],
512
+ gender: 'male',
513
+ },
514
+ {
515
+ name: 'Liam',
516
+ languages: ['en-US', 'en'],
517
+ gender: 'male',
518
+ },
519
+ {
520
+ name: 'Onyx',
521
+ languages: ['en-US', 'en'],
522
+ gender: 'male',
523
+ },
524
+ {
525
+ name: 'Santa',
526
+ languages: ['en-US', 'en'],
527
+ gender: 'male',
528
+ },
529
+ {
530
+ name: 'Adam',
531
+ languages: ['en-US', 'en'],
532
+ gender: 'male',
533
+ },
534
+
535
+ // UK English voices
536
+ {
537
+ name: 'Emma',
538
+ languages: ['en-GB', 'en'],
539
+ gender: 'female',
540
+ },
541
+ {
542
+ name: 'Isabella',
543
+ languages: ['en-GB', 'en'],
544
+ gender: 'female',
545
+ },
546
+ {
547
+ name: 'Alice',
548
+ languages: ['en-GB', 'en'],
549
+ gender: 'female',
550
+ },
551
+ {
552
+ name: 'Lily',
553
+ languages: ['en-GB', 'en'],
554
+ gender: 'female',
555
+ },
556
+ {
557
+ name: 'George',
558
+ languages: ['en-GB', 'en'],
559
+ gender: 'male',
560
+ },
561
+ {
562
+ name: 'Fable',
563
+ languages: ['en-GB', 'en'],
564
+ gender: 'male',
565
+ },
566
+ {
567
+ name: 'Lewis',
568
+ languages: ['en-GB', 'en'],
569
+ gender: 'male',
570
+ },
571
+ {
572
+ name: 'Daniel',
573
+ languages: ['en-GB', 'en'],
574
+ gender: 'male',
575
+ },
576
+
577
+ // Spanish (Spain) voices
578
+ {
579
+ name: 'Dora',
580
+ languages: ['es-ES', 'es'],
581
+ gender: 'female',
582
+ },
583
+ {
584
+ name: 'Alex',
585
+ languages: ['es-ES', 'es'],
586
+ gender: 'male',
587
+ },
588
+ {
589
+ name: 'Santa',
590
+ languages: ['es-ES', 'es'],
591
+ gender: 'male',
592
+ },
593
+
594
+ // French (France) voices
595
+ {
596
+ name: 'Siwis',
597
+ languages: ['fr-FR', 'fr'],
598
+ gender: 'female',
599
+ },
600
+
601
+ // Hindi (India) voices
602
+ {
603
+ name: 'Alpha',
604
+ languages: ['hi-IN', 'hi'],
605
+ gender: 'female',
606
+ },
607
+ {
608
+ name: 'Beta',
609
+ languages: ['hi-IN', 'hi'],
610
+ gender: 'female',
611
+ },
612
+ {
613
+ name: 'Omega',
614
+ languages: ['hi-IN', 'hi'],
615
+ gender: 'male',
616
+ },
617
+ {
618
+ name: 'Psi',
619
+ languages: ['hi-IN', 'hi'],
620
+ gender: 'male',
621
+ },
622
+
623
+ // Italian (Italy) voices
624
+ {
625
+ name: 'Sara',
626
+ languages: ['it-IT', 'it'],
627
+ gender: 'female',
628
+ },
629
+ {
630
+ name: 'Nicola',
631
+ languages: ['it-IT', 'it'],
632
+ gender: 'male',
633
+ },
634
+
635
+ // Portuguese (Brazil) voices
636
+ {
637
+ name: 'Dora',
638
+ languages: ['pt-BR', 'pt'],
639
+ gender: 'female',
640
+ },
641
+ {
642
+ name: 'Alex',
643
+ languages: ['pt-BR', 'pt'],
644
+ gender: 'male',
645
+ },
646
+ {
647
+ name: 'Santa',
648
+ languages: ['pt-BR', 'pt'],
649
+ gender: 'male',
650
+ },
651
+
652
+ // Chinese (China) voices
653
+ {
654
+ name: 'Xiaobei',
655
+ languages: ['zh-CN', 'zh'],
656
+ gender: 'female',
657
+ },
658
+ {
659
+ name: 'Xiaoni',
660
+ languages: ['zh-CN', 'zh'],
661
+ gender: 'female',
662
+ },
663
+ {
664
+ name: 'Xiaoxiao',
665
+ languages: ['zh-CN', 'zh'],
666
+ gender: 'female',
667
+ },
668
+ {
669
+ name: 'Xiaoyi',
670
+ languages: ['zh-CN', 'zh'],
671
+ gender: 'female',
672
+ },
673
+ {
674
+ name: 'Yunjian',
675
+ languages: ['zh-CN', 'zh'],
676
+ gender: 'male',
677
+ },
678
+ {
679
+ name: 'Yunxi',
680
+ languages: ['zh-CN', 'zh'],
681
+ gender: 'male',
682
+ },
683
+ {
684
+ name: 'Yunxia',
685
+ languages: ['zh-CN', 'zh'],
686
+ gender: 'male',
687
+ },
688
+ {
689
+ name: 'Yunyang',
690
+ languages: ['zh-CN', 'zh'],
691
+ gender: 'male',
692
+ },
693
+ ]
@@ -168,6 +168,16 @@ export const voiceList: SynthesisVoice[] = [
168
168
  languages: ['en-US', ...supportedLanguages],
169
169
  gender: 'male',
170
170
  },
171
+ {
172
+ name: 'ash',
173
+ languages: ['en-US', ...supportedLanguages],
174
+ gender: 'male',
175
+ },
176
+ {
177
+ name: 'coral',
178
+ languages: ['en-US', ...supportedLanguages],
179
+ gender: 'female',
180
+ },
171
181
  {
172
182
  name: 'echo',
173
183
  languages: ['en-US', ...supportedLanguages],
@@ -188,6 +198,11 @@ export const voiceList: SynthesisVoice[] = [
188
198
  languages: ['en-US', ...supportedLanguages],
189
199
  gender: 'female',
190
200
  },
201
+ {
202
+ name: 'sage',
203
+ languages: ['en-US', ...supportedLanguages],
204
+ gender: 'female',
205
+ },
191
206
  {
192
207
  name: 'shimmer',
193
208
  languages: ['en-US', ...supportedLanguages],