echogarden 0.11.12 → 0.11.13
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/data/schemas/options.json +16 -0
- package/dist/api/Alignment.js +2 -2
- package/dist/api/Alignment.js.map +1 -1
- package/dist/api/Recognition.js +2 -2
- package/dist/api/Recognition.js.map +1 -1
- package/dist/api/Synthesis.js +5 -4
- package/dist/api/Synthesis.js.map +1 -1
- package/dist/api/Translation.js +2 -2
- package/dist/api/Translation.js.map +1 -1
- package/dist/audio/AudioUtilities.d.ts +1 -0
- package/dist/audio/AudioUtilities.js +25 -7
- package/dist/audio/AudioUtilities.js.map +1 -1
- package/dist/cli/CLI.js +2 -2
- package/dist/cli/CLI.js.map +1 -1
- package/dist/recognition/WhisperSTT.js +2 -2
- package/dist/recognition/WhisperSTT.js.map +1 -1
- package/dist/subtitles/Subtitles.d.ts +10 -7
- package/dist/subtitles/Subtitles.js +268 -207
- package/dist/subtitles/Subtitles.js.map +1 -1
- package/docs/Options.md +4 -2
- package/package.json +7 -6
- package/src/alignment/DTWMfccSequenceAlignment.ts +43 -0
- package/src/alignment/DTWSequenceAlignment.ts +121 -0
- package/src/alignment/DTWSequenceAlignmentWindowed.ts +210 -0
- package/src/alignment/LevenshteinSequenceAlignment.ts +126 -0
- package/src/alignment/SpeechAlignment.ts +488 -0
- package/src/api/API.ts +12 -0
- package/src/api/APIOptions.ts +15 -0
- package/src/api/Alignment.ts +329 -0
- package/src/api/Common.ts +16 -0
- package/src/api/Denoising.ts +120 -0
- package/src/api/LanguageDetection.ts +286 -0
- package/src/api/Recognition.ts +344 -0
- package/src/api/Synthesis.ts +1735 -0
- package/src/api/Translation.ts +143 -0
- package/src/api/Vad.ts +172 -0
- package/src/audio/AudioBufferConversion.ts +248 -0
- package/src/audio/AudioPlayer.ts +358 -0
- package/src/audio/AudioRecorder.ts +91 -0
- package/src/audio/AudioUtilities.ts +392 -0
- package/src/audio/SoxPath.ts +24 -0
- package/src/cli/CLI.ts +1360 -0
- package/src/cli/CLIConfigFile.ts +91 -0
- package/src/cli/CLILauncher.ts +26 -0
- package/src/cli/CLIOptionsSchema.ts +54 -0
- package/src/cli/CLIParser.ts +41 -0
- package/src/cli/CLIStarter.ts +40 -0
- package/src/codecs/FFMpegTranscoder.ts +214 -0
- package/src/codecs/TIMITCodec.ts +17 -0
- package/src/codecs/WaveCodec.ts +260 -0
- package/src/denoising/RNNoise.ts +95 -0
- package/src/dsp/BiquadFilter.ts +488 -0
- package/src/dsp/FFT.ts +187 -0
- package/src/dsp/MFCC.ts +227 -0
- package/src/dsp/MelSpectogram.ts +145 -0
- package/src/dsp/Rubberband.ts +249 -0
- package/src/dsp/Sonic.ts +59 -0
- package/src/dsp/SpeexResampler.ts +79 -0
- package/src/math/VectorMath.ts +812 -0
- package/src/nlp/ChineseSegmentation.ts +68 -0
- package/src/nlp/CompromiseNLP.ts +113 -0
- package/src/nlp/EspeakPhonemizer.ts +168 -0
- package/src/nlp/IPA.ts +139 -0
- package/src/nlp/JapaneseSegmentation.ts +53 -0
- package/src/nlp/Lexicon.ts +119 -0
- package/src/nlp/PhoneConversion.ts +508 -0
- package/src/nlp/Segmentation.ts +237 -0
- package/src/nlp/TextNormalizer.ts +160 -0
- package/src/recognition/AmazonTranscribeSTT.ts +112 -0
- package/src/recognition/AzureCognitiveServicesSTT.ts +76 -0
- package/src/recognition/GoogleCloudSTT.ts +92 -0
- package/src/recognition/SileroSTT.ts +173 -0
- package/src/recognition/VoskSTT.ts +112 -0
- package/src/recognition/WhisperSTT.ts +1518 -0
- package/src/server/Client.ts +297 -0
- package/src/server/Server.ts +178 -0
- package/src/server/ServerStarter.ts +12 -0
- package/src/server/Worker.ts +400 -0
- package/src/server/WorkerStarter.ts +38 -0
- package/src/speech-language-detection/SileroLanguageDetection.ts +105 -0
- package/src/subtitles/Subtitles.ts +478 -0
- package/src/synthesis/AwsPollyTTS.ts +78 -0
- package/src/synthesis/AzureCognitiveServicesTTS.ts +146 -0
- package/src/synthesis/CoquiServerTTS.ts +29 -0
- package/src/synthesis/ElevenLabsTTS.ts +104 -0
- package/src/synthesis/EspeakTTS.ts +552 -0
- package/src/synthesis/FliteTTS.ts +387 -0
- package/src/synthesis/GoogleCloudTTS.ts +112 -0
- package/src/synthesis/GoogleTranslateTTS.ts +210 -0
- package/src/synthesis/MicrosoftEdgeTTS.ts +298 -0
- package/src/synthesis/SamTTS.ts +30 -0
- package/src/synthesis/SapiTTS.ts +222 -0
- package/src/synthesis/StreamlabsPollyTTS.ts +114 -0
- package/src/synthesis/SvoxPicoTTS.ts +318 -0
- package/src/synthesis/VitsTTS.ts +734 -0
- package/src/tests/Test.ts +24 -0
- package/src/text-language-detection/FastTextLanguageDetection.ts +53 -0
- package/src/text-language-detection/TinyLDLanguageDetection.ts +16 -0
- package/src/typings/Fillers.d.ts +41 -0
- package/src/utilities/BinaryArrayConversion.ts +159 -0
- package/src/utilities/Compression.ts +91 -0
- package/src/utilities/FileDownloader.ts +201 -0
- package/src/utilities/FileSystem.ts +265 -0
- package/src/utilities/Hashing.ts +230 -0
- package/src/utilities/Locale.ts +119 -0
- package/src/utilities/Logger.ts +72 -0
- package/src/utilities/NdArrayUtilities.ts +31 -0
- package/src/utilities/ObjectUtilities.ts +169 -0
- package/src/utilities/OpenPromise.ts +13 -0
- package/src/utilities/PackageManager.ts +97 -0
- package/src/utilities/Queue.ts +17 -0
- package/src/utilities/RandomGenerator.ts +237 -0
- package/src/utilities/SignalChannel.ts +22 -0
- package/src/utilities/TarballMaker.ts +68 -0
- package/src/utilities/Timeline.ts +231 -0
- package/src/utilities/Timer.ts +93 -0
- package/src/utilities/Utilities.ts +574 -0
- package/src/utilities/WasmMemoryManager.ts +516 -0
- package/src/utilities/WebReader.ts +55 -0
- package/src/utilities/WikipediaReader.ts +41 -0
- package/src/voice-activity-detection/SileroVAD.ts +86 -0
- package/src/voice-activity-detection/WebRtcVAD.ts +76 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { SynthesisVoice } from "../api/API.js"
|
|
2
|
+
import { decodeWaveBuffer } from "../audio/AudioUtilities.js"
|
|
3
|
+
import { Logger } from "../utilities/Logger.js"
|
|
4
|
+
import { getRandomHexString, logToStderr, resolveModuleMainPath } from "../utilities/Utilities.js"
|
|
5
|
+
import { getAppTempDir, readFile, open, close, remove, ensureDir } from "../utilities/FileSystem.js"
|
|
6
|
+
import path from "node:path"
|
|
7
|
+
import { escape } from 'html-escaper'
|
|
8
|
+
|
|
9
|
+
const log = logToStderr
|
|
10
|
+
|
|
11
|
+
export type FliteVoiceName = "kal" | "kal16" | "awb" | "rms" | "slt" | string
|
|
12
|
+
|
|
13
|
+
let fliteModuleObject: WebAssembly.Module
|
|
14
|
+
|
|
15
|
+
export async function synthesize(text: string, voice: FliteVoiceName, voiceDir: string | undefined, rate: number, pitchMeanHz?: number, pitchStdDev?: number) {
|
|
16
|
+
const logger = new Logger()
|
|
17
|
+
logger.start("Get Flite WASI instance")
|
|
18
|
+
|
|
19
|
+
const randomId = getRandomHexString(16)
|
|
20
|
+
|
|
21
|
+
const outFileName = `${randomId}.out.wav`
|
|
22
|
+
const stdOutFileName = `${randomId}.stdout`
|
|
23
|
+
|
|
24
|
+
const tempDir = getAppTempDir("flite")
|
|
25
|
+
await ensureDir(tempDir)
|
|
26
|
+
|
|
27
|
+
const tempAudioFilePath = path.join(tempDir, outFileName)
|
|
28
|
+
const tempStdOutFilePath = path.join(tempDir, stdOutFileName)
|
|
29
|
+
|
|
30
|
+
const stdOutFileFd = await open(tempStdOutFilePath, "w+")
|
|
31
|
+
|
|
32
|
+
if (voiceDir) {
|
|
33
|
+
voice = `voices/${voice}.flitevox`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const optionArgs = [
|
|
37
|
+
'-voice', voice,
|
|
38
|
+
'--setf', `duration_stretch=${1.0 / rate}`,
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
if (pitchMeanHz != null) {
|
|
42
|
+
optionArgs.push('--setf', `int_f0_target_mean=${pitchMeanHz}`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (pitchStdDev != null) {
|
|
46
|
+
optionArgs.push('--setf', `int_f0_target_stddev=${pitchStdDev}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const preopens: { [k: string]: string } = {
|
|
50
|
+
'.': tempDir,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (voiceDir != undefined) {
|
|
54
|
+
preopens['./voices'] = voiceDir
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { WASI } = await import('wasi')
|
|
58
|
+
|
|
59
|
+
const wasi = new WASI({
|
|
60
|
+
env: {
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
args: ['--',
|
|
64
|
+
//'-ssml',
|
|
65
|
+
'-psdur',
|
|
66
|
+
...optionArgs,
|
|
67
|
+
//` ${escape(text)} `,
|
|
68
|
+
` ${text} `,
|
|
69
|
+
outFileName
|
|
70
|
+
],
|
|
71
|
+
|
|
72
|
+
preopens,
|
|
73
|
+
|
|
74
|
+
stdout: stdOutFileFd,
|
|
75
|
+
|
|
76
|
+
returnOnExit: true
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// Some WASI binaries require `const importObject = { wasi_unstable: wasi.wasiImport }`
|
|
80
|
+
const importObject = { wasi_snapshot_preview1: wasi.wasiImport }
|
|
81
|
+
|
|
82
|
+
const moduleObject = await getModuleObject()
|
|
83
|
+
const instance = await WebAssembly.instantiate(moduleObject, importObject)
|
|
84
|
+
|
|
85
|
+
logger.start("Synthesize with flite")
|
|
86
|
+
const exitCode: number = wasi.start(instance) as any
|
|
87
|
+
|
|
88
|
+
await close(stdOutFileFd)
|
|
89
|
+
|
|
90
|
+
async function cleanup() {
|
|
91
|
+
await remove(tempAudioFilePath)
|
|
92
|
+
await remove(tempStdOutFilePath)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (exitCode != 0) {
|
|
96
|
+
await cleanup()
|
|
97
|
+
throw new Error(`Flite failed with exit code ${exitCode}`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const waveData = await readFile(tempAudioFilePath)
|
|
101
|
+
const stdOutString = await readFile(tempStdOutFilePath, "utf8")
|
|
102
|
+
|
|
103
|
+
await cleanup()
|
|
104
|
+
|
|
105
|
+
const events = parseEventsFromTrace(stdOutString)
|
|
106
|
+
|
|
107
|
+
const { rawAudio } = decodeWaveBuffer(waveData)
|
|
108
|
+
|
|
109
|
+
logger.end()
|
|
110
|
+
|
|
111
|
+
return { rawAudio, events }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function getModuleObject() {
|
|
115
|
+
if (!fliteModuleObject) {
|
|
116
|
+
const fliteWasiPath = await resolveModuleMainPath('@echogarden/flite-wasi')
|
|
117
|
+
|
|
118
|
+
fliteModuleObject = await WebAssembly.compile(await readFile(fliteWasiPath))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return fliteModuleObject
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseEventsFromTrace(eventTrace: string) {
|
|
125
|
+
//log(eventTrace)
|
|
126
|
+
|
|
127
|
+
const eventStrings = eventTrace.trim().split(" ")
|
|
128
|
+
const eventCount = eventStrings.length
|
|
129
|
+
|
|
130
|
+
const events: FliteEvent[] = []
|
|
131
|
+
let phraseStartOffset = 0
|
|
132
|
+
|
|
133
|
+
for (let i = 0; i < eventCount; i++) {
|
|
134
|
+
const eventString = eventStrings[i]
|
|
135
|
+
const splitPoint = eventString.lastIndexOf(":")
|
|
136
|
+
|
|
137
|
+
const id = eventString.substring(0, splitPoint)
|
|
138
|
+
const startTime = events.length > 0 ? events[events.length - 1].endTime : 0
|
|
139
|
+
|
|
140
|
+
let eventType: FliteEventType
|
|
141
|
+
|
|
142
|
+
if (id == "pau") {
|
|
143
|
+
eventType = "pause"
|
|
144
|
+
} else if (id == "\npau") {
|
|
145
|
+
eventType = "phrasePause"
|
|
146
|
+
|
|
147
|
+
phraseStartOffset = startTime
|
|
148
|
+
} else {
|
|
149
|
+
eventType = "phone"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const endTime = phraseStartOffset + parseFloat(eventString.substring(splitPoint + 1))
|
|
153
|
+
|
|
154
|
+
events.push({
|
|
155
|
+
type: eventType,
|
|
156
|
+
id,
|
|
157
|
+
startTime,
|
|
158
|
+
endTime
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return events
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export type FliteEventType = "phone" | "pause" | "phrasePause"
|
|
166
|
+
|
|
167
|
+
export type FliteEvent = {
|
|
168
|
+
type: FliteEventType,
|
|
169
|
+
id: string,
|
|
170
|
+
startTime: number,
|
|
171
|
+
endTime: number
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const voiceList: SynthesisVoice[] = [
|
|
175
|
+
// Built-in voices
|
|
176
|
+
{
|
|
177
|
+
name: "slt",
|
|
178
|
+
languages: ["en-US", "en"],
|
|
179
|
+
gender: "female"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "kal16",
|
|
183
|
+
languages: ["en-US", "en"],
|
|
184
|
+
gender: "male"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: "rms",
|
|
188
|
+
languages: ["en-US", "en"],
|
|
189
|
+
gender: "male"
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: "awb",
|
|
193
|
+
languages: ["en-GB-SCOTLAND", "en-GB", "en"],
|
|
194
|
+
gender: "male"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "kal",
|
|
198
|
+
languages: ["en-US", "en"],
|
|
199
|
+
gender: "male"
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
// Voices loaded from packages
|
|
204
|
+
// US English voices
|
|
205
|
+
{
|
|
206
|
+
name: "cmu_us_aew",
|
|
207
|
+
languages: ["en-US", "en"],
|
|
208
|
+
gender: "male",
|
|
209
|
+
packageName: "flite-cmu_us_aew"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "cmu_us_ahw",
|
|
213
|
+
languages: ["en-US", "en"],
|
|
214
|
+
gender: "male",
|
|
215
|
+
packageName: "flite-cmu_us_ahw"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "cmu_us_aup",
|
|
219
|
+
languages: ["en-US", "en"],
|
|
220
|
+
gender: "male",
|
|
221
|
+
packageName: "flite-cmu_us_aup"
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: "cmu_us_awb",
|
|
225
|
+
languages: ["en-US", "en"],
|
|
226
|
+
gender: "male",
|
|
227
|
+
packageName: "flite-cmu_us_awb"
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: "cmu_us_axb",
|
|
231
|
+
languages: ["en-US", "en"],
|
|
232
|
+
gender: "female",
|
|
233
|
+
packageName: "flite-cmu_us_axb"
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: "cmu_us_bdl",
|
|
237
|
+
languages: ["en-US", "en"],
|
|
238
|
+
gender: "male",
|
|
239
|
+
packageName: "flite-cmu_us_bdl"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: "cmu_us_clb",
|
|
243
|
+
languages: ["en-US", "en"],
|
|
244
|
+
gender: "female",
|
|
245
|
+
packageName: "flite-cmu_us_clb"
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "cmu_us_eey",
|
|
249
|
+
languages: ["en-US", "en"],
|
|
250
|
+
gender: "female",
|
|
251
|
+
packageName: "flite-cmu_us_eey"
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: "cmu_us_fem",
|
|
255
|
+
languages: ["en-US", "en"],
|
|
256
|
+
gender: "male",
|
|
257
|
+
packageName: "flite-cmu_us_fem"
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: "cmu_us_gka",
|
|
261
|
+
languages: ["en-US", "en"],
|
|
262
|
+
gender: "male",
|
|
263
|
+
packageName: "flite-cmu_us_gka"
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: "cmu_us_jmk",
|
|
267
|
+
languages: ["en-US", "en"],
|
|
268
|
+
gender: "male",
|
|
269
|
+
packageName: "flite-cmu_us_jmk"
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: "cmu_us_ksp",
|
|
273
|
+
languages: ["en-US", "en"],
|
|
274
|
+
gender: "male",
|
|
275
|
+
packageName: "flite-cmu_us_ksp"
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: "cmu_us_ljm",
|
|
279
|
+
languages: ["en-US", "en"],
|
|
280
|
+
gender: "female",
|
|
281
|
+
packageName: "flite-cmu_us_ljm"
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
name: "cmu_us_lnh",
|
|
285
|
+
languages: ["en-US", "en"],
|
|
286
|
+
gender: "female",
|
|
287
|
+
packageName: "flite-cmu_us_lnh"
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: "cmu_us_rms",
|
|
291
|
+
languages: ["en-US", "en"],
|
|
292
|
+
gender: "male",
|
|
293
|
+
packageName: "flite-cmu_us_rms"
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
name: "cmu_us_rxr",
|
|
297
|
+
languages: ["en-US", "en"],
|
|
298
|
+
gender: "male",
|
|
299
|
+
packageName: "flite-cmu_us_rxr"
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: "cmu_us_slp",
|
|
303
|
+
languages: ["en-US", "en"],
|
|
304
|
+
gender: "female",
|
|
305
|
+
packageName: "flite-cmu_us_slp"
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: "cmu_us_slt",
|
|
309
|
+
languages: ["en-US", "en"],
|
|
310
|
+
gender: "female",
|
|
311
|
+
packageName: "flite-cmu_us_slt"
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
// Indic voices
|
|
315
|
+
{
|
|
316
|
+
name: "cmu_indic_ben_rm",
|
|
317
|
+
languages: ["be", "bn"],// Bengali
|
|
318
|
+
gender: "female",
|
|
319
|
+
packageName: "flite-cmu_indic_ben_rm"
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: "cmu_indic_guj_ad",
|
|
323
|
+
languages: ["gu"],// Gujarati
|
|
324
|
+
gender: "male",
|
|
325
|
+
packageName: "flite-cmu_indic_guj_ad"
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
name: "cmu_indic_guj_dp",
|
|
329
|
+
languages: ["gu"],// Gujarati
|
|
330
|
+
gender: "female",
|
|
331
|
+
packageName: "flite-cmu_indic_guj_dp"
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
name: "cmu_indic_guj_kt",
|
|
335
|
+
languages: ["gu"],// Gujarati
|
|
336
|
+
gender: "female",
|
|
337
|
+
packageName: "flite-cmu_indic_guj_kt"
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: "cmu_indic_hin_ab",
|
|
341
|
+
languages: ["hi"],// Hindi
|
|
342
|
+
gender: "female",
|
|
343
|
+
packageName: "flite-cmu_indic_hin_ab"
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "cmu_indic_kan_plv",
|
|
347
|
+
languages: ["ka"],// Kannada
|
|
348
|
+
gender: "female",
|
|
349
|
+
packageName: "flite-cmu_indic_kan_plv"
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: "cmu_indic_mar_aup",
|
|
353
|
+
languages: ["mr"],// Marathi
|
|
354
|
+
gender: "male",
|
|
355
|
+
packageName: "flite-cmu_indic_mar_aup"
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
name: "cmu_indic_mar_slp",
|
|
359
|
+
languages: ["mr"],// Marathi
|
|
360
|
+
gender: "female",
|
|
361
|
+
packageName: "flite-cmu_indic_mar_slp"
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: "cmu_indic_pan_amp",
|
|
365
|
+
languages: ["pa"],// Punjabi
|
|
366
|
+
gender: "female",
|
|
367
|
+
packageName: "flite-cmu_indic_pan_amp"
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
name: "cmu_indic_tel_kpn",
|
|
371
|
+
languages: ["te"],// Telugu
|
|
372
|
+
gender: "female",
|
|
373
|
+
packageName: "flite-cmu_indic_tel_kpn"
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: "cmu_indic_tel_sk",
|
|
377
|
+
languages: ["te"],// Telugu
|
|
378
|
+
gender: "male",
|
|
379
|
+
packageName: "flite-cmu_indic_tel_sk"
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: "cmu_indic_tel_ss",
|
|
383
|
+
languages: ["te"],// Telugu
|
|
384
|
+
gender: "female",
|
|
385
|
+
packageName: "flite-cmu_indic_tel_ss"
|
|
386
|
+
},
|
|
387
|
+
]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { request } from "gaxios"
|
|
2
|
+
import { Logger } from "../utilities/Logger.js"
|
|
3
|
+
import { logToStderr } from "../utilities/Utilities.js"
|
|
4
|
+
|
|
5
|
+
const log = logToStderr
|
|
6
|
+
|
|
7
|
+
export async function synthesize(text: string, apiKey: string, languageCode = "en-US", voice = "en-US-Wavenet-C", speakingRate = 1.0, pitchDeltaSemitones = 0.0, volumeGainDb = 0.0, ssml = false, audioEncoding: AudioEncoding = "MP3_64_KBPS", sampleRate = 24000) {
|
|
8
|
+
const logger = new Logger()
|
|
9
|
+
logger.start("Request synthesis from Google Cloud")
|
|
10
|
+
|
|
11
|
+
const requestBody = {
|
|
12
|
+
input: {
|
|
13
|
+
text: undefined as (string | undefined),
|
|
14
|
+
ssml: undefined as (string | undefined)
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
voice: {
|
|
18
|
+
languageCode,
|
|
19
|
+
name: voice
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
audioConfig: {
|
|
23
|
+
audioEncoding,
|
|
24
|
+
speakingRate,
|
|
25
|
+
pitch: pitchDeltaSemitones,
|
|
26
|
+
volumeGainDb,
|
|
27
|
+
sampleRateHertz: sampleRate
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
enableTimePointing: ["SSML_MARK"]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (ssml) {
|
|
34
|
+
requestBody.input.ssml = text
|
|
35
|
+
} else {
|
|
36
|
+
requestBody.input.text = text
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const response = await request<any>({
|
|
40
|
+
method: "POST",
|
|
41
|
+
|
|
42
|
+
url: `https://texttospeech.googleapis.com/v1beta1/text:synthesize`,
|
|
43
|
+
|
|
44
|
+
params: {
|
|
45
|
+
"key": apiKey
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
headers: {
|
|
49
|
+
"User-Agent": ""
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
data: requestBody,
|
|
53
|
+
|
|
54
|
+
responseType: "json"
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
logger.start("Parse result")
|
|
58
|
+
|
|
59
|
+
const result = parseResponseBody(response.data)
|
|
60
|
+
|
|
61
|
+
logger.end()
|
|
62
|
+
|
|
63
|
+
return result
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseResponseBody(responseBody: any) {
|
|
67
|
+
const audioData = Buffer.from(responseBody.audioContent, "base64")
|
|
68
|
+
const timepoints: timePoint[] = responseBody.timepoints
|
|
69
|
+
|
|
70
|
+
return { audioData, timepoints }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Voices with audio samples: https://cloud.google.com/text-to-speech/docs/voices
|
|
74
|
+
export async function getVoiceList(apiKey: string) {
|
|
75
|
+
const requestURL = `https://texttospeech.googleapis.com/v1beta1/voices`
|
|
76
|
+
|
|
77
|
+
const response = await request<any>({
|
|
78
|
+
method: "GET",
|
|
79
|
+
|
|
80
|
+
url: requestURL,
|
|
81
|
+
|
|
82
|
+
params: {
|
|
83
|
+
"key": apiKey
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
headers: {
|
|
87
|
+
"User-Agent": ""
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
responseType: "json"
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const responseData = response.data
|
|
94
|
+
|
|
95
|
+
const voices: GoogleCloudVoice[] = responseData.voices
|
|
96
|
+
|
|
97
|
+
return voices
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type GoogleCloudVoice = {
|
|
101
|
+
name: string
|
|
102
|
+
languageCodes: string[]
|
|
103
|
+
ssmlGender: "MALE" | "FEMALE"
|
|
104
|
+
naturalSampleRateHertz: number
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type AudioEncoding = "LINEAR16" | "MP3" | "MP3_64_KBPS" | "OGG_OPUS" | "MULAW" | "ALAW"
|
|
108
|
+
|
|
109
|
+
export type timePoint = {
|
|
110
|
+
markName: string,
|
|
111
|
+
timeSeconds: number
|
|
112
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { request } from "gaxios"
|
|
2
|
+
import { Phrase, splitToFragments } from "../nlp/Segmentation.js"
|
|
3
|
+
import { concatFloat32Arrays, logToStderr } from "../utilities/Utilities.js"
|
|
4
|
+
import * as FFMpegTranscoder from "../codecs/FFMpegTranscoder.js"
|
|
5
|
+
import { trimAudioEnd } from "../audio/AudioUtilities.js"
|
|
6
|
+
import { Logger } from "../utilities/Logger.js"
|
|
7
|
+
import { Timeline } from "../utilities/Timeline.js"
|
|
8
|
+
import { getShortLanguageCode } from "../utilities/Locale.js"
|
|
9
|
+
|
|
10
|
+
const log = logToStderr
|
|
11
|
+
|
|
12
|
+
const maxTextLengthPerRequest = 200
|
|
13
|
+
|
|
14
|
+
export async function synthesizeLongText(text: string, languageCode = "en", tld = "us", sentenceEndPause = 0.75, segmentEndPause = 1.0) {
|
|
15
|
+
if (text.length == 0) {
|
|
16
|
+
throw new Error("Text is empty")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const logger = new Logger()
|
|
20
|
+
logger.start("Prepare and split text")
|
|
21
|
+
|
|
22
|
+
const fragments = await splitToFragments(text, maxTextLengthPerRequest, languageCode)
|
|
23
|
+
|
|
24
|
+
const audioFragments: Float32Array[] = []
|
|
25
|
+
let fragmentsSampleRate = 0
|
|
26
|
+
|
|
27
|
+
const timeline: Timeline = []
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < fragments.length; i++) {
|
|
30
|
+
const fragment = fragments[i]
|
|
31
|
+
|
|
32
|
+
logger.start(`Request synthesis for text fragment ${i + 1}/${fragments.length} from Google Translate`)
|
|
33
|
+
const fragmentMp3Stream = await synthesizeShortText(fragment.text, languageCode, tld)
|
|
34
|
+
|
|
35
|
+
if (fragmentMp3Stream.length == 0) {
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const rawAudio = await FFMpegTranscoder.decodeToChannels(fragmentMp3Stream, 24000, 1)
|
|
40
|
+
fragmentsSampleRate = rawAudio.sampleRate
|
|
41
|
+
|
|
42
|
+
let targetEndingSilenceTime: number
|
|
43
|
+
|
|
44
|
+
if (fragment.lastSegment?.isSentenceFinalizer) {
|
|
45
|
+
targetEndingSilenceTime = 0.75
|
|
46
|
+
} else if (fragment.lastSegment instanceof Phrase) {
|
|
47
|
+
targetEndingSilenceTime = 0.1
|
|
48
|
+
} else {
|
|
49
|
+
targetEndingSilenceTime = 0
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const trimmedAudio = trimAudioEnd(rawAudio.audioChannels[0], targetEndingSilenceTime * rawAudio.sampleRate)
|
|
53
|
+
|
|
54
|
+
audioFragments.push(trimmedAudio)
|
|
55
|
+
|
|
56
|
+
const startTime = timeline.length == 0 ? 0 : timeline[timeline.length - 1].endTime
|
|
57
|
+
const endTime = startTime + (trimmedAudio.length / fragmentsSampleRate)
|
|
58
|
+
|
|
59
|
+
timeline.push({
|
|
60
|
+
type: "segment",
|
|
61
|
+
text: fragment.text,
|
|
62
|
+
startTime,
|
|
63
|
+
endTime
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
logger.end()
|
|
68
|
+
|
|
69
|
+
const rawAudio = { audioChannels: [concatFloat32Arrays(audioFragments)], sampleRate: fragmentsSampleRate }
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
rawAudio,
|
|
73
|
+
timeline
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function synthesizeShortText(text: string, languageCode = "en", tld = "us") {
|
|
78
|
+
if (text.length > maxTextLengthPerRequest) {
|
|
79
|
+
throw new Error(`Text is ${text.length} characters, which is longer than the maximum of ${maxTextLengthPerRequest}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (languageCode != "zh-CN" && languageCode != "zh-TW") {
|
|
83
|
+
languageCode = getShortLanguageCode(languageCode)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const bodyFormParameters = [text, languageCode, null, "null"]
|
|
87
|
+
const requestForm = [[["jQ1olc", JSON.stringify(bodyFormParameters), null, "generic"]]]
|
|
88
|
+
const stringifiedForm = JSON.stringify(requestForm)
|
|
89
|
+
const requestBody = `f.req=${encodeURIComponent(stringifiedForm)}&`
|
|
90
|
+
|
|
91
|
+
const response = await request<string>({
|
|
92
|
+
method: "POST",
|
|
93
|
+
|
|
94
|
+
url: `https://translate.google.${tld}/_/TranslateWebserverUi/data/batchexecute`,
|
|
95
|
+
|
|
96
|
+
params: {
|
|
97
|
+
"rpcids": "jQ1olc"
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
headers: {
|
|
101
|
+
"sec-ch-ua": `".Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103"`,
|
|
102
|
+
"x-same-domain": "1",
|
|
103
|
+
"dnt": "1",
|
|
104
|
+
"content-type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
105
|
+
"sec-ch-ua-mobile": "?0",
|
|
106
|
+
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36",
|
|
107
|
+
"sec-ch-ua-arch": "x86",
|
|
108
|
+
"sec-ch-ua-full-version": "103.0.5060.114",
|
|
109
|
+
"sec-ch-ua-platform-version": "10.0.0",
|
|
110
|
+
"sec-ch-ua-full-version-list": `".Not/A)Brand";v="99.0.0.0", "Google Chrome";v="103.0.5060.114", "Chromium";v="103.0.5060.114"`,
|
|
111
|
+
"sec-ch-ua-bitness": "64",
|
|
112
|
+
"sec-ch-ua-model": "",
|
|
113
|
+
"sec-ch-ua-platform": "Windows",
|
|
114
|
+
"accept": "*/*",
|
|
115
|
+
"origin": "https://translate.google.com",
|
|
116
|
+
"sec-fetch-site": "same-origin",
|
|
117
|
+
"sec-fetch-mode": "cors",
|
|
118
|
+
"sec-fetch-dest": "empty",
|
|
119
|
+
"referer": "https://translate.google.com/",
|
|
120
|
+
"accept-encoding": "gzip, deflate, br",
|
|
121
|
+
"accept-language": "en-US,en;q=0.9",
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
body: requestBody,
|
|
125
|
+
|
|
126
|
+
responseType: "text"
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
//log(response.data)
|
|
130
|
+
|
|
131
|
+
const audioChunks: Buffer[] = []
|
|
132
|
+
|
|
133
|
+
for (const line of response.data.split(/\r?\n/)) {
|
|
134
|
+
const match = line.match(/"jQ1olc","\[\\"(.*)\\"]/)
|
|
135
|
+
|
|
136
|
+
if (match != null) {
|
|
137
|
+
const base64AudioChunk = match[1]
|
|
138
|
+
audioChunks.push(Buffer.from(base64AudioChunk, "base64"))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const resultMp3Stream = Buffer.concat(audioChunks)
|
|
143
|
+
return resultMp3Stream
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const supportedLanguageLookup: { [langCode: string]: string } = {
|
|
147
|
+
"af": "Afrikaans",
|
|
148
|
+
"ar": "Arabic",
|
|
149
|
+
"bg": "Bulgarian",
|
|
150
|
+
"bn": "Bengali",
|
|
151
|
+
"bs": "Bosnian",
|
|
152
|
+
"ca": "Catalan",
|
|
153
|
+
"cs": "Czech",
|
|
154
|
+
"cy": "Welsh",
|
|
155
|
+
"da": "Danish",
|
|
156
|
+
"de": "German",
|
|
157
|
+
"el": "Greek",
|
|
158
|
+
"en": "English",
|
|
159
|
+
"eo": "Esperanto",
|
|
160
|
+
"es": "Spanish",
|
|
161
|
+
"et": "Estonian",
|
|
162
|
+
"fi": "Finnish",
|
|
163
|
+
"fr": "French",
|
|
164
|
+
"gu": "Gujarati",
|
|
165
|
+
"hi": "Hindi",
|
|
166
|
+
"hr": "Croatian",
|
|
167
|
+
"hu": "Hungarian",
|
|
168
|
+
"hy": "Armenian",
|
|
169
|
+
"id": "Indonesian",
|
|
170
|
+
"is": "Icelandic",
|
|
171
|
+
"it": "Italian",
|
|
172
|
+
"iw": "Hebrew",
|
|
173
|
+
"ja": "Japanese",
|
|
174
|
+
"jw": "Javanese",
|
|
175
|
+
"km": "Khmer",
|
|
176
|
+
"kn": "Kannada",
|
|
177
|
+
"ko": "Korean",
|
|
178
|
+
"la": "Latin",
|
|
179
|
+
"lv": "Latvian",
|
|
180
|
+
"mk": "Macedonian",
|
|
181
|
+
"ms": "Malay",
|
|
182
|
+
"ml": "Malayalam",
|
|
183
|
+
"mr": "Marathi",
|
|
184
|
+
"my": "Myanmar (Burmese)",
|
|
185
|
+
"ne": "Nepali",
|
|
186
|
+
"nl": "Dutch",
|
|
187
|
+
"no": "Norwegian",
|
|
188
|
+
"pl": "Polish",
|
|
189
|
+
"pt": "Portuguese",
|
|
190
|
+
"ro": "Romanian",
|
|
191
|
+
"ru": "Russian",
|
|
192
|
+
"si": "Sinhala",
|
|
193
|
+
"sk": "Slovak",
|
|
194
|
+
"sq": "Albanian",
|
|
195
|
+
"sr": "Serbian",
|
|
196
|
+
"su": "Sundanese",
|
|
197
|
+
"sv": "Swedish",
|
|
198
|
+
"sw": "Swahili",
|
|
199
|
+
"ta": "Tamil",
|
|
200
|
+
"te": "Telugu",
|
|
201
|
+
"th": "Thai",
|
|
202
|
+
"tl": "Filipino",
|
|
203
|
+
"tr": "Turkish",
|
|
204
|
+
"uk": "Ukrainian",
|
|
205
|
+
"ur": "Urdu",
|
|
206
|
+
"vi": "Vietnamese",
|
|
207
|
+
"zh-CN": "Chinese",
|
|
208
|
+
"zh-TW": "Chinese (Mandarin/Taiwan)",
|
|
209
|
+
"zh": "Chinese (Mandarin)",
|
|
210
|
+
}
|