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.
- package/dist/alignment/TextAlignment.d.ts +16 -0
- package/dist/alignment/TextAlignment.js +63 -0
- package/dist/alignment/TextAlignment.js.map +1 -0
- package/dist/api/Alignment.js +6 -1
- package/dist/api/Alignment.js.map +1 -1
- package/dist/api/Denoising.js +0 -1
- package/dist/api/Denoising.js.map +1 -1
- package/dist/api/LanguageDetection.js +2 -2
- package/dist/api/LanguageDetection.js.map +1 -1
- package/dist/api/Recognition.js +3 -1
- package/dist/api/Recognition.js.map +1 -1
- package/dist/api/Synthesis.js +1 -1
- package/dist/api/Synthesis.js.map +1 -1
- package/dist/api/Translation.js +3 -1
- package/dist/api/Translation.js.map +1 -1
- package/dist/api/TranslationAlignment.js +4 -1
- package/dist/api/TranslationAlignment.js.map +1 -1
- package/dist/api/VoiceActivityDetection.js +5 -2
- package/dist/api/VoiceActivityDetection.js.map +1 -1
- package/dist/audio/AudioUtilities.d.ts +1 -1
- package/dist/audio/AudioUtilities.js +2 -2
- package/dist/audio/AudioUtilities.js.map +1 -1
- package/dist/cli/CLILauncher.js +1 -2
- package/dist/cli/CLILauncher.js.map +1 -1
- package/dist/codecs/FFMpegTranscoder.js +4 -1
- package/dist/codecs/FFMpegTranscoder.js.map +1 -1
- package/dist/codecs/WaveCodec.d.ts +1 -1
- package/dist/codecs/WaveCodec.js +13 -5
- package/dist/codecs/WaveCodec.js.map +1 -1
- package/dist/dsp/SpeexResampler.js +49 -21
- package/dist/dsp/SpeexResampler.js.map +1 -1
- package/dist/synthesis/CoquiServerTTS.js +1 -1
- package/dist/synthesis/CoquiServerTTS.js.map +1 -1
- package/dist/synthesis/VitsTTS.js +1 -1
- package/dist/synthesis/VitsTTS.js.map +1 -1
- package/dist/text-translation/NLLBTextTranslation.d.ts +1 -0
- package/dist/text-translation/NLLBTextTranslation.js +237 -0
- package/dist/text-translation/NLLBTextTranslation.js.map +1 -0
- package/dist/utilities/BinaryArrayConversion.d.ts +18 -8
- package/dist/utilities/BinaryArrayConversion.js +63 -36
- package/dist/utilities/BinaryArrayConversion.js.map +1 -1
- package/dist/utilities/PackageManager.js +2 -0
- package/dist/utilities/PackageManager.js.map +1 -1
- package/docs/Development.md +1 -1
- package/docs/Server.md +1 -1
- package/docs/Tasklist.md +1 -1
- package/package.json +11 -10
- package/src/alignment/TextAlignment.ts +96 -0
- package/src/api/Alignment.ts +10 -1
- package/src/api/Denoising.ts +0 -2
- package/src/api/LanguageDetection.ts +2 -3
- package/src/api/Recognition.ts +3 -1
- package/src/api/Synthesis.ts +1 -1
- package/src/api/Translation.ts +3 -1
- package/src/api/TranslationAlignment.ts +5 -1
- package/src/api/VoiceActivityDetection.ts +7 -4
- package/src/audio/AudioUtilities.ts +2 -2
- package/src/cli/CLILauncher.ts +1 -2
- package/src/codecs/FFMpegTranscoder.ts +5 -1
- package/src/codecs/WaveCodec.ts +15 -5
- package/src/dsp/SpeexResampler.ts +62 -23
- package/src/synthesis/CoquiServerTTS.ts +1 -1
- package/src/synthesis/VitsTTS.ts +2 -2
- package/src/text-translation/NLLBTextTranslation.ts +252 -0
- package/src/utilities/BinaryArrayConversion.ts +73 -41
- 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
|
+
}
|
package/src/api/Alignment.ts
CHANGED
|
@@ -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('
|
|
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) {
|
package/src/api/Denoising.ts
CHANGED
|
@@ -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(
|
|
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) {
|
package/src/api/Recognition.ts
CHANGED
|
@@ -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('
|
|
60
|
+
logger.start('Normalize and trim audio')
|
|
59
61
|
|
|
60
62
|
sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
|
|
61
63
|
sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
|
package/src/api/Synthesis.ts
CHANGED
|
@@ -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
|
|
package/src/api/Translation.ts
CHANGED
|
@@ -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('
|
|
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('
|
|
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 =
|
|
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
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
package/src/cli/CLILauncher.ts
CHANGED
|
@@ -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
|
|
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
|
}
|
package/src/codecs/WaveCodec.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
62
|
+
for (let readOffset = 0; readOffset < totalSampleCount;) {
|
|
63
|
+
const isLastChunk = readOffset + maxChunkSize >= totalSampleCount
|
|
37
64
|
|
|
38
|
-
|
|
39
|
-
|
|
65
|
+
const inputPaddingSize = isLastChunk ? inputLatency : 0
|
|
66
|
+
const maxSamplesToRead = Math.min(maxChunkSize, totalSampleCount - readOffset) + inputPaddingSize
|
|
40
67
|
|
|
41
|
-
|
|
42
|
-
const inSampleCountRef = wasmMemory.allocInt32()
|
|
43
|
-
inSampleCountRef.value = inSampleCount + inputLatency
|
|
68
|
+
const maxSamplesToWrite = outputChunkSamplesRef.length
|
|
44
69
|
|
|
45
|
-
|
|
46
|
-
inSamplesRef.view.set(channelSamples)
|
|
70
|
+
const inputChunkSamplesForChannel = rawAudio.audioChannels[channelIndex].slice(readOffset, readOffset + maxSamplesToRead)
|
|
47
71
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
outSampleCountRef.value = outSampleCount + outputLatency
|
|
72
|
+
inputChunkSampleCountRef.value = maxSamplesToRead
|
|
73
|
+
outputChunkSampleCountRef.value = maxSamplesToWrite
|
|
51
74
|
|
|
52
|
-
|
|
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
|
-
|
|
78
|
+
if (resultCode != 0) {
|
|
79
|
+
throw new Error(`Speex resampler failed while resampling with code ${resultCode}: ${speexResultCodeToString(resultCode)}`)
|
|
80
|
+
}
|
|
55
81
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
82
|
+
const samplesReadCount = inputChunkSampleCountRef.value
|
|
83
|
+
const samplesWrittenCount = outputChunkSampleCountRef.value
|
|
84
|
+
|
|
85
|
+
const resampledChannelAudio = outputChunkSamplesRef.view.slice(0, samplesWrittenCount)
|
|
59
86
|
|
|
60
|
-
|
|
87
|
+
resampledAudioChunksForChannels[channelIndex].push(resampledChannelAudio)
|
|
61
88
|
|
|
62
|
-
|
|
89
|
+
readOffset += samplesReadCount
|
|
90
|
+
}
|
|
63
91
|
}
|
|
64
92
|
|
|
65
|
-
m._speex_resampler_destroy(
|
|
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)
|
|
24
|
+
const { rawAudio } = decodeWaveToRawAudio(waveData)
|
|
25
25
|
|
|
26
26
|
logger.end()
|
|
27
27
|
|
package/src/synthesis/VitsTTS.ts
CHANGED
|
@@ -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) {
|