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.
- package/README.md +1 -1
- package/data/lexicons/heteronyms.en.json +45 -3
- package/data/schemas/options.json +64 -0
- package/dist/api/Synthesis.d.ts +10 -1
- package/dist/api/Synthesis.js +76 -9
- package/dist/api/Synthesis.js.map +1 -1
- package/dist/recognition/WhisperSTT.js +7 -4
- package/dist/recognition/WhisperSTT.js.map +1 -1
- package/dist/synthesis/GnuSpeechTTS.d.ts +12 -0
- package/dist/synthesis/GnuSpeechTTS.js +36 -0
- package/dist/synthesis/GnuSpeechTTS.js.map +1 -0
- package/dist/synthesis/KokoroTTS.d.ts +22 -0
- package/dist/synthesis/KokoroTTS.js +595 -0
- package/dist/synthesis/KokoroTTS.js.map +1 -0
- package/dist/synthesis/OpenAICloudTTS.js +15 -0
- package/dist/synthesis/OpenAICloudTTS.js.map +1 -1
- package/dist/synthesis/VitsTTS.js +6 -1
- package/dist/synthesis/VitsTTS.js.map +1 -1
- package/dist/utilities/FileReader.js +1 -1
- package/dist/utilities/PackageManager.js +4 -0
- package/dist/utilities/PackageManager.js.map +1 -1
- package/dist/utilities/Utilities.d.ts +1 -0
- package/dist/utilities/Utilities.js +8 -0
- package/dist/utilities/Utilities.js.map +1 -1
- package/dist/utilities/WasmMemoryManager.d.ts +1 -1
- package/docs/Development.md +1 -1
- package/docs/Engines.md +4 -1
- package/docs/Licenses.md +2 -1
- package/docs/Options.md +9 -5
- package/docs/Tasklist.md +1 -1
- package/package.json +14 -13
- package/src/api/Synthesis.ts +132 -10
- package/src/recognition/WhisperSTT.ts +8 -4
- package/src/synthesis/GnuSpeechTTS.ts +40 -0
- package/src/synthesis/KokoroTTS.ts +693 -0
- package/src/synthesis/OpenAICloudTTS.ts +15 -0
- package/src/synthesis/VitsTTS.ts +6 -1
- package/src/utilities/FileReader.ts +1 -1
- package/src/utilities/PackageManager.ts +5 -0
- 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],
|