echogarden 1.3.0 → 1.3.2

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 (66) hide show
  1. package/dist/alignment/TextAlignment.d.ts +16 -0
  2. package/dist/alignment/TextAlignment.js +63 -0
  3. package/dist/alignment/TextAlignment.js.map +1 -0
  4. package/dist/api/Alignment.js +6 -1
  5. package/dist/api/Alignment.js.map +1 -1
  6. package/dist/api/Denoising.js +0 -1
  7. package/dist/api/Denoising.js.map +1 -1
  8. package/dist/api/LanguageDetection.js +2 -2
  9. package/dist/api/LanguageDetection.js.map +1 -1
  10. package/dist/api/Recognition.js +3 -1
  11. package/dist/api/Recognition.js.map +1 -1
  12. package/dist/api/Synthesis.js +1 -1
  13. package/dist/api/Synthesis.js.map +1 -1
  14. package/dist/api/Translation.js +3 -1
  15. package/dist/api/Translation.js.map +1 -1
  16. package/dist/api/TranslationAlignment.js +4 -1
  17. package/dist/api/TranslationAlignment.js.map +1 -1
  18. package/dist/api/VoiceActivityDetection.js +5 -2
  19. package/dist/api/VoiceActivityDetection.js.map +1 -1
  20. package/dist/audio/AudioUtilities.d.ts +1 -1
  21. package/dist/audio/AudioUtilities.js +2 -2
  22. package/dist/audio/AudioUtilities.js.map +1 -1
  23. package/dist/cli/CLILauncher.js +1 -2
  24. package/dist/cli/CLILauncher.js.map +1 -1
  25. package/dist/codecs/FFMpegTranscoder.js +4 -1
  26. package/dist/codecs/FFMpegTranscoder.js.map +1 -1
  27. package/dist/codecs/WaveCodec.d.ts +1 -1
  28. package/dist/codecs/WaveCodec.js +13 -5
  29. package/dist/codecs/WaveCodec.js.map +1 -1
  30. package/dist/dsp/SpeexResampler.js +49 -21
  31. package/dist/dsp/SpeexResampler.js.map +1 -1
  32. package/dist/synthesis/CoquiServerTTS.js +1 -1
  33. package/dist/synthesis/CoquiServerTTS.js.map +1 -1
  34. package/dist/synthesis/VitsTTS.js +1 -1
  35. package/dist/synthesis/VitsTTS.js.map +1 -1
  36. package/dist/text-translation/NLLBTextTranslation.d.ts +1 -0
  37. package/dist/text-translation/NLLBTextTranslation.js +237 -0
  38. package/dist/text-translation/NLLBTextTranslation.js.map +1 -0
  39. package/dist/utilities/BinaryArrayConversion.d.ts +18 -8
  40. package/dist/utilities/BinaryArrayConversion.js +63 -36
  41. package/dist/utilities/BinaryArrayConversion.js.map +1 -1
  42. package/dist/utilities/PackageManager.js +2 -0
  43. package/dist/utilities/PackageManager.js.map +1 -1
  44. package/docs/Development.md +1 -1
  45. package/docs/Server.md +1 -1
  46. package/docs/Tasklist.md +1 -1
  47. package/package.json +11 -10
  48. package/src/alignment/TextAlignment.ts +96 -0
  49. package/src/api/Alignment.ts +10 -1
  50. package/src/api/Denoising.ts +0 -2
  51. package/src/api/LanguageDetection.ts +2 -3
  52. package/src/api/Recognition.ts +3 -1
  53. package/src/api/Synthesis.ts +1 -1
  54. package/src/api/Translation.ts +3 -1
  55. package/src/api/TranslationAlignment.ts +5 -1
  56. package/src/api/VoiceActivityDetection.ts +7 -4
  57. package/src/audio/AudioUtilities.ts +2 -2
  58. package/src/cli/CLILauncher.ts +1 -2
  59. package/src/codecs/FFMpegTranscoder.ts +5 -1
  60. package/src/codecs/WaveCodec.ts +15 -5
  61. package/src/dsp/SpeexResampler.ts +62 -23
  62. package/src/synthesis/CoquiServerTTS.ts +1 -1
  63. package/src/synthesis/VitsTTS.ts +2 -2
  64. package/src/text-translation/NLLBTextTranslation.ts +252 -0
  65. package/src/utilities/BinaryArrayConversion.ts +73 -41
  66. package/src/utilities/PackageManager.ts +3 -0
@@ -0,0 +1,96 @@
1
+ import { type PreTrainedModel, type PreTrainedTokenizer } from '@echogarden/transformers-nodejs-lite'
2
+ import { Logger } from '../utilities/Logger.js'
3
+ import { loadPackage } from '../utilities/PackageManager.js'
4
+
5
+ export async function alignText(text1: string, text2: string) {
6
+ const logger = new Logger()
7
+
8
+ text1 = text1.replaceAll(/(\r?\n)+/g, ' ')
9
+ text2 = text2.replaceAll(/(\r?\n)+/g, ' ')
10
+
11
+ const modelPath = await loadPackage(`xenova-multilingual-e5-small-quantized`)
12
+
13
+ const embeddingModel = new E5TextEmbedding(modelPath)
14
+
15
+ logger.start(`Initialize E5 embedding model`)
16
+ await embeddingModel.initializeIfNeeded()
17
+
18
+ logger.start(`Tokenize text 1`)
19
+ const inputs1 = await embeddingModel.tokenizeToModelInputs(text1)
20
+
21
+ logger.start(`Infer embeddings for text 1`)
22
+ const embeddings1 = await embeddingModel.inferTokenEmbeddings(inputs1)
23
+
24
+ logger.start(`Tokenize text 2`)
25
+ const inputs2 = await embeddingModel.tokenizeToModelInputs(text2)
26
+
27
+ logger.start(`Infer embeddings for text 2`)
28
+ const embeddings2 = await embeddingModel.inferTokenEmbeddings(inputs1)
29
+
30
+ logger.end()
31
+
32
+ logger.log(embeddings1)
33
+ }
34
+
35
+ export class E5TextEmbedding {
36
+ tokenizer?: PreTrainedTokenizer
37
+ model?: PreTrainedModel
38
+
39
+ constructor(public readonly modelPath: string) {
40
+ }
41
+
42
+ async tokenizeToModelInputs(text: string) {
43
+ await this.initializeIfNeeded()
44
+
45
+ const inputs = await this.tokenizer!(text)
46
+
47
+ return inputs
48
+ }
49
+
50
+ async inferTokenEmbeddings(inputs: any) {
51
+ await this.initializeIfNeeded()
52
+
53
+ const tokensText = this.tokenizer!.model.convert_ids_to_tokens(Array.from(inputs.input_ids.data))
54
+
55
+ const result = await this.model!(inputs)
56
+
57
+ const lastHiddenState = result.last_hidden_state
58
+
59
+ const tokenCount = lastHiddenState.dims[1]
60
+ const embeddingSize = lastHiddenState.dims[2]
61
+
62
+ const tokenEmbeddings: TokenEmbeddingData[] = []
63
+
64
+ for (let i = 0; i < tokenCount; i++) {
65
+ const tokenEmbeddingVector = lastHiddenState.data.slice(i * embeddingSize, (i + 1) * embeddingSize)
66
+
67
+ const tokenId = Number(inputs.input_ids.data[i])
68
+ const tokenText = tokensText[i]
69
+
70
+ tokenEmbeddings.push({
71
+ id: tokenId,
72
+ text: tokenText,
73
+ embeddingVector: tokenEmbeddingVector
74
+ })
75
+ }
76
+
77
+ return tokenEmbeddings
78
+ }
79
+
80
+ async initializeIfNeeded() {
81
+ if (this.tokenizer && this.model) {
82
+ return
83
+ }
84
+
85
+ const { AutoTokenizer, AutoModel } = await import('@echogarden/transformers-nodejs-lite')
86
+
87
+ this.tokenizer = await AutoTokenizer.from_pretrained(this.modelPath)
88
+ this.model = await AutoModel.from_pretrained(this.modelPath)
89
+ }
90
+ }
91
+
92
+ export interface TokenEmbeddingData {
93
+ id: number
94
+ text: string
95
+ embeddingVector: Float32Array
96
+ }
@@ -13,10 +13,15 @@ import { DtwGranularity, createAlignmentReferenceUsingEspeak } from '../alignmen
13
13
  import { type SubtitlesConfig } from '../subtitles/Subtitles.js'
14
14
  import { type EspeakOptions, defaultEspeakOptions } from '../synthesis/EspeakTTS.js'
15
15
  import { isWord } from '../nlp/Segmentation.js'
16
+ import { alignText } from '../alignment/TextAlignment.js'
17
+ import { translateText } from '../text-translation/NLLBTextTranslation.js'
16
18
 
17
19
  const log = logToStderr
18
20
 
19
21
  export async function align(input: AudioSourceParam, transcript: string, options: AlignmentOptions): Promise<AlignmentResult> {
22
+ //await alignText(transcript, transcript)
23
+ //await translateText(transcript, 'en', 'de')
24
+
20
25
  const logger = new Logger()
21
26
 
22
27
  const startTimestamp = logger.getTimestamp()
@@ -38,8 +43,10 @@ export async function align(input: AudioSourceParam, transcript: string, options
38
43
  logger.end()
39
44
  logger.log(``)
40
45
 
46
+ logger.start(`Resample audio to 16kHz mono`)
41
47
  sourceRawAudio = await ensureRawAudio(isolatedRawAudio, 16000, 1)
42
48
  } else {
49
+ logger.start(`Resample audio to 16kHz mono`)
43
50
  sourceRawAudio = await ensureRawAudio(inputRawAudio, 16000, 1)
44
51
  }
45
52
 
@@ -52,7 +59,7 @@ export async function align(input: AudioSourceParam, transcript: string, options
52
59
  logger.end()
53
60
  }
54
61
 
55
- logger.start('Prepare for alignment')
62
+ logger.start('Normalize and trim audio')
56
63
 
57
64
  sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
58
65
  sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
@@ -69,6 +76,8 @@ export async function align(input: AudioSourceParam, transcript: string, options
69
76
  }
70
77
  }
71
78
 
79
+ logger.end()
80
+
72
81
  let language: string
73
82
 
74
83
  if (options.language) {
@@ -14,8 +14,6 @@ export async function denoise(input: AudioSourceParam, options: DenoisingOptions
14
14
  const logger = new Logger()
15
15
  const startTime = logger.getTimestamp()
16
16
 
17
- logger.start('Prepare for denoising')
18
-
19
17
  options = extendDeep(defaultDenoisingOptions, options)
20
18
 
21
19
  const inputRawAudio = await ensureRawAudio(input)
@@ -28,6 +28,7 @@ export async function detectSpeechLanguage(input: AudioSourceParam, options: Spe
28
28
 
29
29
  const inputRawAudio = await ensureRawAudio(input)
30
30
 
31
+ logger.start(`Resample audio to 16kHz mono`)
31
32
  let sourceRawAudio = await ensureRawAudio(inputRawAudio, 16000, 1)
32
33
  sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
33
34
  sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
@@ -39,13 +40,11 @@ export async function detectSpeechLanguage(input: AudioSourceParam, options: Spe
39
40
  logger.end()
40
41
  }
41
42
 
42
- logger.start('Prepare for speech language detection')
43
+ logger.start(`Initialize ${options.engine} module`)
43
44
 
44
45
  const defaultLanguage = options.defaultLanguage!
45
46
  const fallbackThresholdProbability = options.fallbackThresholdProbability!
46
47
 
47
- logger.start(`Initialize ${options.engine} module`)
48
-
49
48
  let detectedLanguageProbabilities: LanguageDetectionResults
50
49
 
51
50
  switch (options.engine) {
@@ -41,8 +41,10 @@ export async function recognize(input: AudioSourceParam, options: RecognitionOpt
41
41
  logger.end()
42
42
  logger.log(``)
43
43
 
44
+ logger.start(`Resample audio to 16kHz mono`)
44
45
  sourceRawAudio = await ensureRawAudio(isolatedRawAudio, 16000, 1)
45
46
  } else {
47
+ logger.start(`Resample audio to 16kHz mono`)
46
48
  sourceRawAudio = await ensureRawAudio(inputRawAudio, 16000, 1)
47
49
  }
48
50
 
@@ -55,7 +57,7 @@ export async function recognize(input: AudioSourceParam, options: RecognitionOpt
55
57
  logger.end()
56
58
  }
57
59
 
58
- logger.start('Prepare for recognition')
60
+ logger.start('Normalize and trim audio')
59
61
 
60
62
  sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
61
63
  sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
@@ -301,7 +301,7 @@ async function synthesizeSegment(text: string, options: SynthesisOptions) {
301
301
 
302
302
  const startTimestamp = logger.getTimestamp()
303
303
 
304
- logger.start('Prepare for synthesis')
304
+ logger.start('Prepare text for synthesis')
305
305
 
306
306
  const simplifiedText = simplifyPunctuationCharacters(text)
307
307
 
@@ -43,8 +43,10 @@ export async function translateSpeech(input: AudioSourceParam, options: SpeechTr
43
43
  logger.end()
44
44
  logger.log(``)
45
45
 
46
+ logger.start(`Resample audio to 16kHz mono`)
46
47
  sourceRawAudio = await ensureRawAudio(isolatedRawAudio, 16000, 1)
47
48
  } else {
49
+ logger.start(`Resample audio to 16kHz mono`)
48
50
  sourceRawAudio = await ensureRawAudio(inputRawAudio, 16000, 1)
49
51
  }
50
52
 
@@ -57,7 +59,7 @@ export async function translateSpeech(input: AudioSourceParam, options: SpeechTr
57
59
  logger.end()
58
60
  }
59
61
 
60
- logger.start('Prepare for speech translation')
62
+ logger.start('Normalize and trim audio')
61
63
 
62
64
  sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
63
65
  sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
@@ -35,8 +35,10 @@ export async function alignTranslation(input: AudioSourceParam, transcript: stri
35
35
  logger.end()
36
36
  logger.log(``)
37
37
 
38
+ logger.start(`Resample audio to 16kHz mono`)
38
39
  sourceRawAudio = await ensureRawAudio(isolatedRawAudio, 16000, 1)
39
40
  } else {
41
+ logger.start(`Resample audio to 16kHz mono`)
40
42
  sourceRawAudio = await ensureRawAudio(inputRawAudio, 16000, 1)
41
43
  }
42
44
 
@@ -49,11 +51,13 @@ export async function alignTranslation(input: AudioSourceParam, transcript: stri
49
51
  logger.end()
50
52
  }
51
53
 
52
- logger.start('Prepare for alignment')
54
+ logger.start('Normalize and trim audio')
53
55
 
54
56
  sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
55
57
  sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
56
58
 
59
+ logger.end()
60
+
57
61
  let sourceLanguage: string
58
62
 
59
63
  if (options.sourceLanguage) {
@@ -20,16 +20,15 @@ export async function detectVoiceActivity(input: AudioSourceParam, options: VADO
20
20
 
21
21
  const startTimestamp = logger.getTimestamp()
22
22
 
23
- logger.start('Prepare for voice activity detection')
24
-
25
23
  const inputRawAudio = await ensureRawAudio(input)
26
24
 
25
+ logger.start(`Resample audio to 16kHz mono`)
27
26
  let sourceRawAudio = await ensureRawAudio(inputRawAudio, 16000, 1)
28
27
 
29
- options = extendDeep(defaultVADOptions, options)
30
-
31
28
  logger.start(`Detect voice activity with ${options.engine}`)
32
29
 
30
+ options = extendDeep(defaultVADOptions, options)
31
+
33
32
  const activityThreshold = options.activityThreshold!
34
33
 
35
34
  let verboseTimeline: Timeline
@@ -224,6 +223,10 @@ export function convertCroppedToUncroppedTimeline(timeline: Timeline, uncropTime
224
223
  function mapUsingUncropTimeline(startTimeInCroppedAudio: number, endTimeInCroppedAudio: number, uncropTimeline: Timeline) {
225
224
  let offsetInCroppedAudio = 0
226
225
 
226
+ if (endTimeInCroppedAudio < startTimeInCroppedAudio) {
227
+ endTimeInCroppedAudio = startTimeInCroppedAudio
228
+ }
229
+
227
230
  let bestOverlapDuration = -1
228
231
  let mappedStartTime = -1
229
232
  let mappedEndTime = -1
@@ -11,8 +11,8 @@ export function encodeRawAudioToWave(rawAudio: RawAudio, bitDepth: BitDepth = 16
11
11
  return encodeWave(rawAudio, bitDepth, sampleFormat, speakerPositionMask)
12
12
  }
13
13
 
14
- export function decodeWaveToRawAudio(waveFileBuffer: Buffer, ignoreTruncatedChunks = false) {
15
- return decodeWave(waveFileBuffer, ignoreTruncatedChunks)
14
+ export function decodeWaveToRawAudio(waveFileBuffer: Buffer, ignoreTruncatedChunks = true, ignoreOverflowingDataChunks = true) {
15
+ return decodeWave(waveFileBuffer, ignoreTruncatedChunks, ignoreOverflowingDataChunks)
16
16
  }
17
17
 
18
18
  ////////////////////////////////////////////////////////////////////////////////////////////////
@@ -12,9 +12,8 @@ const scriptArgs = process.argv.slice(2)
12
12
  const cliScriptPath = resolveToModuleRootDir('dist/cli/CLIStarter.js')
13
13
 
14
14
  const args = [
15
- '--no-warnings',
16
- '--no-experimental-fetch',
17
15
  '--experimental-wasi-unstable-preview1',
16
+ '--no-warnings',
18
17
  cliScriptPath,
19
18
  ...scriptArgs
20
19
  ]
@@ -37,7 +37,11 @@ export async function decodeToChannels(input: string | Buffer, outSampleRate?: n
37
37
 
38
38
  const waveAudio = await transcode(input, outputOptions)
39
39
 
40
- const { rawAudio } = decodeWaveToRawAudio(waveAudio, true)
40
+ const logger = new Logger()
41
+
42
+ logger.start(`Convert wave buffer to raw audio`)
43
+ const { rawAudio } = decodeWaveToRawAudio(waveAudio)
44
+ logger.end()
41
45
 
42
46
  return rawAudio
43
47
  }
@@ -18,18 +18,20 @@ export function encodeWave(rawAudio: RawAudio, bitDepth: BitDepth = 16, sampleFo
18
18
 
19
19
  const dataSubChunkBuffer = Buffer.alloc(4 + 4 + audioDataLength)
20
20
  dataSubChunkBuffer.write('data', 0, 'ascii')
21
- dataSubChunkBuffer.writeUint32LE(audioDataLength, 4)
21
+ const dataChunkLength = Math.min(audioDataLength, 4294967295) // Ensure large data chunk length is clipped to max
22
+ dataSubChunkBuffer.writeUint32LE(dataChunkLength, 4)
22
23
  dataSubChunkBuffer.set(audioBuffer, 8)
23
24
 
24
25
  const riffChunkHeaderBuffer = Buffer.alloc(12)
25
26
  riffChunkHeaderBuffer.write('RIFF', 0, 'ascii')
26
- riffChunkHeaderBuffer.writeUint32LE(4 + formatSubChunkBuffer.length + dataSubChunkBuffer.length, 4)
27
+ const riffChunkLength = Math.min(4 + formatSubChunkBuffer.length + dataSubChunkBuffer.length, 4294967295) // Ensure large RIFF chunk length is clipped to max
28
+ riffChunkHeaderBuffer.writeUint32LE(riffChunkLength, 4)
27
29
  riffChunkHeaderBuffer.write('WAVE', 8, 'ascii')
28
30
 
29
31
  return Buffer.concat([riffChunkHeaderBuffer, formatSubChunkBuffer, dataSubChunkBuffer])
30
32
  }
31
33
 
32
- export function decodeWave(waveData: Buffer, ignoreTruncatedChunks = false) {
34
+ export function decodeWave(waveData: Buffer, ignoreTruncatedChunks = true, ignoreOverflowingDataChunks = true) {
33
35
  let readOffset = 0
34
36
 
35
37
  const riffId = waveData.subarray(readOffset, readOffset + 4).toString('ascii')
@@ -40,7 +42,7 @@ export function decodeWave(waveData: Buffer, ignoreTruncatedChunks = false) {
40
42
 
41
43
  readOffset += 4
42
44
 
43
- const riffChunkSize = waveData.readUInt32LE(readOffset)
45
+ let riffChunkSize = waveData.readUInt32LE(readOffset)
44
46
 
45
47
  readOffset += 4
46
48
 
@@ -50,6 +52,10 @@ export function decodeWave(waveData: Buffer, ignoreTruncatedChunks = false) {
50
52
  throw new Error('Not a valid wave file. No WAVE id found at offset 8.')
51
53
  }
52
54
 
55
+ if (ignoreOverflowingDataChunks && riffChunkSize === 4294967295) {
56
+ riffChunkSize = waveData.length - 8
57
+ }
58
+
53
59
  if (riffChunkSize < waveData.length - 8) {
54
60
  throw new Error(`RIFF chunk length ${riffChunkSize} is smaller than the remaining size of the buffer (${waveData.length - 8})`)
55
61
  }
@@ -67,7 +73,7 @@ export function decodeWave(waveData: Buffer, ignoreTruncatedChunks = false) {
67
73
  const subChunkIdentifier = waveData.subarray(readOffset, readOffset + 4).toString('ascii')
68
74
  readOffset += 4
69
75
 
70
- const subChunkSize = waveData.readUInt32LE(readOffset)
76
+ let subChunkSize = waveData.readUInt32LE(readOffset)
71
77
  readOffset += 4
72
78
 
73
79
  if (!ignoreTruncatedChunks && subChunkSize > waveData.length - readOffset) {
@@ -81,6 +87,10 @@ export function decodeWave(waveData: Buffer, ignoreTruncatedChunks = false) {
81
87
  throw new Error('A data subchunk was encountered before a format subchunk')
82
88
  }
83
89
 
90
+ if (ignoreOverflowingDataChunks && subChunkSize === 4294967295) {
91
+ subChunkSize = waveData.length - readOffset
92
+ }
93
+
84
94
  // If the data chunk is truncated, but truncations are ignored,
85
95
  // it would be read up to the end of the buffer
86
96
  dataBuffers.push(waveData.subarray(readOffset, readOffset + subChunkSize))
@@ -1,16 +1,27 @@
1
1
  import { RawAudio, cloneRawAudio } from '../audio/AudioUtilities.js'
2
+ import { concatFloat32Arrays } from '../utilities/Utilities.js'
2
3
  import { WasmMemoryManager } from '../utilities/WasmMemoryManager.js'
3
4
 
4
5
  let speexResamplerInstance: any
5
6
 
6
- export async function resampleAudioSpeex(rawAudio: RawAudio, outSampleRate: number, quality = 0) {
7
+ export async function resampleAudioSpeex(rawAudio: RawAudio, outSampleRate: number, quality = 0): Promise<RawAudio> {
7
8
  const channelCount = rawAudio.audioChannels.length
8
9
  const inSampleRate = rawAudio.sampleRate
9
10
 
10
- if (inSampleRate == outSampleRate) {
11
+ const totalSampleCount = rawAudio.audioChannels[0].length
12
+ const sampleRateRatio = outSampleRate / inSampleRate
13
+
14
+ if (inSampleRate === outSampleRate) {
11
15
  return cloneRawAudio(rawAudio)
12
16
  }
13
17
 
18
+ if (totalSampleCount === 0) {
19
+ return {
20
+ ...cloneRawAudio(rawAudio),
21
+ sampleRate: outSampleRate
22
+ } as RawAudio
23
+ }
24
+
14
25
  const m = await getSpeexResamplerInstance()
15
26
  const wasmMemory = new WasmMemoryManager(m)
16
27
 
@@ -23,48 +34,76 @@ export async function resampleAudioSpeex(rawAudio: RawAudio, outSampleRate: numb
23
34
  }
24
35
 
25
36
  const initErrRef = wasmMemory.allocInt32()
26
- const resamplerState = m._speex_resampler_init(channelCount, inSampleRate, outSampleRate, quality, initErrRef.address)
37
+ const resamplerStateAddress = m._speex_resampler_init(channelCount, inSampleRate, outSampleRate, quality, initErrRef.address)
27
38
  let resultCode = initErrRef.value
28
39
 
29
40
  if (resultCode != 0) {
30
41
  throw new Error(`Speex resampler failed while initializing with code ${resultCode}: ${speexResultCodeToString(resultCode)}`)
31
42
  }
32
43
 
33
- const resampledAudio: RawAudio = { audioChannels: [], sampleRate: outSampleRate }
44
+ const inputLatency = m._speex_resampler_get_input_latency(resamplerStateAddress)
45
+ const outputLatency = m._speex_resampler_get_output_latency(resamplerStateAddress)
46
+
47
+ const maxChunkSize = 2 ** 20
48
+
49
+ const inputChunkSampleCountRef = wasmMemory.allocInt32()
50
+ const outputChunkSampleCountRef = wasmMemory.allocInt32()
51
+
52
+ const inputChunkSamplesRef = wasmMemory.allocFloat32Array(maxChunkSize * 2)
53
+ const outputChunkSamplesRef = wasmMemory.allocFloat32Array(Math.floor(maxChunkSize * sampleRateRatio) * 2)
54
+
55
+ const resampledAudioChunksForChannels: Float32Array[][] = []
56
+
57
+ for (let channelIndex = 0; channelIndex < channelCount; channelIndex++) {
58
+ resampledAudioChunksForChannels.push([])
59
+ }
34
60
 
35
61
  for (let channelIndex = 0; channelIndex < channelCount; channelIndex++) {
36
- const channelSamples = rawAudio.audioChannels[channelIndex]
62
+ for (let readOffset = 0; readOffset < totalSampleCount;) {
63
+ const isLastChunk = readOffset + maxChunkSize >= totalSampleCount
37
64
 
38
- const inputLatency = m._speex_resampler_get_input_latency(resamplerState)
39
- const outputLatency = m._speex_resampler_get_output_latency(resamplerState)
65
+ const inputPaddingSize = isLastChunk ? inputLatency : 0
66
+ const maxSamplesToRead = Math.min(maxChunkSize, totalSampleCount - readOffset) + inputPaddingSize
40
67
 
41
- const inSampleCount = channelSamples.length
42
- const inSampleCountRef = wasmMemory.allocInt32()
43
- inSampleCountRef.value = inSampleCount + inputLatency
68
+ const maxSamplesToWrite = outputChunkSamplesRef.length
44
69
 
45
- const inSamplesRef = wasmMemory.allocFloat32Array(inSampleCountRef.value)
46
- inSamplesRef.view.set(channelSamples)
70
+ const inputChunkSamplesForChannel = rawAudio.audioChannels[channelIndex].slice(readOffset, readOffset + maxSamplesToRead)
47
71
 
48
- const outSampleCount = Math.floor((inSampleCount / inSampleRate) * outSampleRate)
49
- const outSampleCountRef = wasmMemory.allocInt32()
50
- outSampleCountRef.value = outSampleCount + outputLatency
72
+ inputChunkSampleCountRef.value = maxSamplesToRead
73
+ outputChunkSampleCountRef.value = maxSamplesToWrite
51
74
 
52
- const outSamplesRef = wasmMemory.allocFloat32Array(outSampleCountRef.value)
75
+ inputChunkSamplesRef.view.set(inputChunkSamplesForChannel)
76
+ resultCode = m._speex_resampler_process_float(resamplerStateAddress, channelIndex, inputChunkSamplesRef.address, inputChunkSampleCountRef.address, outputChunkSamplesRef.address, outputChunkSampleCountRef.address)
53
77
 
54
- resultCode = m._speex_resampler_process_float(resamplerState, channelIndex, inSamplesRef.address, inSampleCountRef.address, outSamplesRef.address, outSampleCountRef.address)
78
+ if (resultCode != 0) {
79
+ throw new Error(`Speex resampler failed while resampling with code ${resultCode}: ${speexResultCodeToString(resultCode)}`)
80
+ }
55
81
 
56
- if (resultCode != 0) {
57
- throw new Error(`Speex resampler failed while resampling with code ${resultCode}: ${speexResultCodeToString(resultCode)}`)
58
- }
82
+ const samplesReadCount = inputChunkSampleCountRef.value
83
+ const samplesWrittenCount = outputChunkSampleCountRef.value
84
+
85
+ const resampledChannelAudio = outputChunkSamplesRef.view.slice(0, samplesWrittenCount)
59
86
 
60
- const resampledChannelAudio = outSamplesRef.view.slice(outputLatency)
87
+ resampledAudioChunksForChannels[channelIndex].push(resampledChannelAudio)
61
88
 
62
- resampledAudio.audioChannels.push(resampledChannelAudio)
89
+ readOffset += samplesReadCount
90
+ }
63
91
  }
64
92
 
65
- m._speex_resampler_destroy(resamplerState)
93
+ m._speex_resampler_destroy(resamplerStateAddress)
66
94
  wasmMemory.freeAll()
67
95
 
96
+ const resampledAudio: RawAudio = {
97
+ audioChannels: [],
98
+ sampleRate: outSampleRate
99
+ }
100
+
101
+ for (let i = 0; i < channelCount; i++) {
102
+ resampledAudioChunksForChannels[i][0] = resampledAudioChunksForChannels[i][0].slice(outputLatency)
103
+
104
+ resampledAudio.audioChannels.push(concatFloat32Arrays(resampledAudioChunksForChannels[i]))
105
+ }
106
+
68
107
  return resampledAudio
69
108
  }
70
109
 
@@ -21,7 +21,7 @@ export async function synthesize(text: string, speakerId: string | null, serverU
21
21
 
22
22
  const waveData = Buffer.from(response.data)
23
23
 
24
- const rawAudio = decodeWaveToRawAudio(waveData).rawAudio
24
+ const { rawAudio } = decodeWaveToRawAudio(waveData)
25
25
 
26
26
  logger.end()
27
27
 
@@ -52,7 +52,7 @@ export class VitsTTS {
52
52
 
53
53
  await this.initializeIfNeeded()
54
54
 
55
- await logger.startAsync('Prepare for synthesis')
55
+ await logger.startAsync('Prepare for VITS synthesis')
56
56
 
57
57
  const metadata = this.metadata
58
58
  const phonemeMap = this.phonemeMap!
@@ -78,7 +78,7 @@ export class VitsTTS {
78
78
  voice: espeakVoice,
79
79
  useKlatt: false
80
80
  }
81
-
81
+
82
82
  const { referenceSynthesizedAudio, referenceTimeline, fragments, phonemizedFragmentsSubstitutions, phonemizedSentence } = await Espeak.preprocessAndSynthesize(sentence, languageCode, espeakOptions, lexicons)
83
83
 
84
84
  if (phonemizedSentence.length == 0) {