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.
Files changed (122) hide show
  1. package/data/schemas/options.json +16 -0
  2. package/dist/api/Alignment.js +2 -2
  3. package/dist/api/Alignment.js.map +1 -1
  4. package/dist/api/Recognition.js +2 -2
  5. package/dist/api/Recognition.js.map +1 -1
  6. package/dist/api/Synthesis.js +5 -4
  7. package/dist/api/Synthesis.js.map +1 -1
  8. package/dist/api/Translation.js +2 -2
  9. package/dist/api/Translation.js.map +1 -1
  10. package/dist/audio/AudioUtilities.d.ts +1 -0
  11. package/dist/audio/AudioUtilities.js +25 -7
  12. package/dist/audio/AudioUtilities.js.map +1 -1
  13. package/dist/cli/CLI.js +2 -2
  14. package/dist/cli/CLI.js.map +1 -1
  15. package/dist/recognition/WhisperSTT.js +2 -2
  16. package/dist/recognition/WhisperSTT.js.map +1 -1
  17. package/dist/subtitles/Subtitles.d.ts +10 -7
  18. package/dist/subtitles/Subtitles.js +268 -207
  19. package/dist/subtitles/Subtitles.js.map +1 -1
  20. package/docs/Options.md +4 -2
  21. package/package.json +7 -6
  22. package/src/alignment/DTWMfccSequenceAlignment.ts +43 -0
  23. package/src/alignment/DTWSequenceAlignment.ts +121 -0
  24. package/src/alignment/DTWSequenceAlignmentWindowed.ts +210 -0
  25. package/src/alignment/LevenshteinSequenceAlignment.ts +126 -0
  26. package/src/alignment/SpeechAlignment.ts +488 -0
  27. package/src/api/API.ts +12 -0
  28. package/src/api/APIOptions.ts +15 -0
  29. package/src/api/Alignment.ts +329 -0
  30. package/src/api/Common.ts +16 -0
  31. package/src/api/Denoising.ts +120 -0
  32. package/src/api/LanguageDetection.ts +286 -0
  33. package/src/api/Recognition.ts +344 -0
  34. package/src/api/Synthesis.ts +1735 -0
  35. package/src/api/Translation.ts +143 -0
  36. package/src/api/Vad.ts +172 -0
  37. package/src/audio/AudioBufferConversion.ts +248 -0
  38. package/src/audio/AudioPlayer.ts +358 -0
  39. package/src/audio/AudioRecorder.ts +91 -0
  40. package/src/audio/AudioUtilities.ts +392 -0
  41. package/src/audio/SoxPath.ts +24 -0
  42. package/src/cli/CLI.ts +1360 -0
  43. package/src/cli/CLIConfigFile.ts +91 -0
  44. package/src/cli/CLILauncher.ts +26 -0
  45. package/src/cli/CLIOptionsSchema.ts +54 -0
  46. package/src/cli/CLIParser.ts +41 -0
  47. package/src/cli/CLIStarter.ts +40 -0
  48. package/src/codecs/FFMpegTranscoder.ts +214 -0
  49. package/src/codecs/TIMITCodec.ts +17 -0
  50. package/src/codecs/WaveCodec.ts +260 -0
  51. package/src/denoising/RNNoise.ts +95 -0
  52. package/src/dsp/BiquadFilter.ts +488 -0
  53. package/src/dsp/FFT.ts +187 -0
  54. package/src/dsp/MFCC.ts +227 -0
  55. package/src/dsp/MelSpectogram.ts +145 -0
  56. package/src/dsp/Rubberband.ts +249 -0
  57. package/src/dsp/Sonic.ts +59 -0
  58. package/src/dsp/SpeexResampler.ts +79 -0
  59. package/src/math/VectorMath.ts +812 -0
  60. package/src/nlp/ChineseSegmentation.ts +68 -0
  61. package/src/nlp/CompromiseNLP.ts +113 -0
  62. package/src/nlp/EspeakPhonemizer.ts +168 -0
  63. package/src/nlp/IPA.ts +139 -0
  64. package/src/nlp/JapaneseSegmentation.ts +53 -0
  65. package/src/nlp/Lexicon.ts +119 -0
  66. package/src/nlp/PhoneConversion.ts +508 -0
  67. package/src/nlp/Segmentation.ts +237 -0
  68. package/src/nlp/TextNormalizer.ts +160 -0
  69. package/src/recognition/AmazonTranscribeSTT.ts +112 -0
  70. package/src/recognition/AzureCognitiveServicesSTT.ts +76 -0
  71. package/src/recognition/GoogleCloudSTT.ts +92 -0
  72. package/src/recognition/SileroSTT.ts +173 -0
  73. package/src/recognition/VoskSTT.ts +112 -0
  74. package/src/recognition/WhisperSTT.ts +1518 -0
  75. package/src/server/Client.ts +297 -0
  76. package/src/server/Server.ts +178 -0
  77. package/src/server/ServerStarter.ts +12 -0
  78. package/src/server/Worker.ts +400 -0
  79. package/src/server/WorkerStarter.ts +38 -0
  80. package/src/speech-language-detection/SileroLanguageDetection.ts +105 -0
  81. package/src/subtitles/Subtitles.ts +478 -0
  82. package/src/synthesis/AwsPollyTTS.ts +78 -0
  83. package/src/synthesis/AzureCognitiveServicesTTS.ts +146 -0
  84. package/src/synthesis/CoquiServerTTS.ts +29 -0
  85. package/src/synthesis/ElevenLabsTTS.ts +104 -0
  86. package/src/synthesis/EspeakTTS.ts +552 -0
  87. package/src/synthesis/FliteTTS.ts +387 -0
  88. package/src/synthesis/GoogleCloudTTS.ts +112 -0
  89. package/src/synthesis/GoogleTranslateTTS.ts +210 -0
  90. package/src/synthesis/MicrosoftEdgeTTS.ts +298 -0
  91. package/src/synthesis/SamTTS.ts +30 -0
  92. package/src/synthesis/SapiTTS.ts +222 -0
  93. package/src/synthesis/StreamlabsPollyTTS.ts +114 -0
  94. package/src/synthesis/SvoxPicoTTS.ts +318 -0
  95. package/src/synthesis/VitsTTS.ts +734 -0
  96. package/src/tests/Test.ts +24 -0
  97. package/src/text-language-detection/FastTextLanguageDetection.ts +53 -0
  98. package/src/text-language-detection/TinyLDLanguageDetection.ts +16 -0
  99. package/src/typings/Fillers.d.ts +41 -0
  100. package/src/utilities/BinaryArrayConversion.ts +159 -0
  101. package/src/utilities/Compression.ts +91 -0
  102. package/src/utilities/FileDownloader.ts +201 -0
  103. package/src/utilities/FileSystem.ts +265 -0
  104. package/src/utilities/Hashing.ts +230 -0
  105. package/src/utilities/Locale.ts +119 -0
  106. package/src/utilities/Logger.ts +72 -0
  107. package/src/utilities/NdArrayUtilities.ts +31 -0
  108. package/src/utilities/ObjectUtilities.ts +169 -0
  109. package/src/utilities/OpenPromise.ts +13 -0
  110. package/src/utilities/PackageManager.ts +97 -0
  111. package/src/utilities/Queue.ts +17 -0
  112. package/src/utilities/RandomGenerator.ts +237 -0
  113. package/src/utilities/SignalChannel.ts +22 -0
  114. package/src/utilities/TarballMaker.ts +68 -0
  115. package/src/utilities/Timeline.ts +231 -0
  116. package/src/utilities/Timer.ts +93 -0
  117. package/src/utilities/Utilities.ts +574 -0
  118. package/src/utilities/WasmMemoryManager.ts +516 -0
  119. package/src/utilities/WebReader.ts +55 -0
  120. package/src/utilities/WikipediaReader.ts +41 -0
  121. package/src/voice-activity-detection/SileroVAD.ts +86 -0
  122. 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
+ }