echogarden 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/schemas/options.json +3 -1
- package/dist/alignment/DTWSequenceAlignmentWindowed.js +68 -27
- package/dist/alignment/DTWSequenceAlignmentWindowed.js.map +1 -1
- package/dist/alignment/SpeechAlignment.d.ts +1 -1
- package/dist/alignment/SpeechAlignment.js +6 -21
- package/dist/alignment/SpeechAlignment.js.map +1 -1
- package/dist/api/Alignment.js +47 -28
- package/dist/api/Alignment.js.map +1 -1
- package/dist/api/LanguageDetection.js +3 -3
- package/dist/api/LanguageDetection.js.map +1 -1
- package/dist/api/Recognition.js +1 -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 +1 -1
- package/dist/api/Translation.js.map +1 -1
- package/dist/api/TranslationAlignment.js +1 -1
- package/dist/api/TranslationAlignment.js.map +1 -1
- package/dist/audio/AudioBufferConversion.d.ts +2 -2
- package/dist/audio/AudioBufferConversion.js +46 -36
- package/dist/audio/AudioBufferConversion.js.map +1 -1
- package/dist/cli/CLI.js +1 -0
- package/dist/cli/CLI.js.map +1 -1
- package/dist/codecs/FFMpegTranscoder.js +2 -1
- package/dist/codecs/FFMpegTranscoder.js.map +1 -1
- package/dist/codecs/WaveCodec.js +8 -3
- package/dist/codecs/WaveCodec.js.map +1 -1
- package/dist/recognition/WhisperSTT.d.ts +1 -0
- package/dist/recognition/WhisperSTT.js +13 -6
- package/dist/recognition/WhisperSTT.js.map +1 -1
- package/dist/synthesis/EspeakTTS.js +5 -0
- package/dist/synthesis/EspeakTTS.js.map +1 -1
- package/dist/utilities/Compression.d.ts +1 -0
- package/dist/utilities/Compression.js +11 -1
- package/dist/utilities/Compression.js.map +1 -1
- package/dist/utilities/LEB128.d.ts +5 -0
- package/dist/utilities/LEB128.js +168 -0
- package/dist/utilities/LEB128.js.map +1 -0
- package/docs/Options.md +2 -1
- package/docs/Tasklist.md +3 -3
- package/package.json +7 -7
- package/src/alignment/DTWSequenceAlignmentWindowed.ts +69 -29
- package/src/alignment/SpeechAlignment.ts +7 -23
- package/src/api/Alignment.ts +48 -29
- package/src/api/LanguageDetection.ts +3 -3
- package/src/api/Recognition.ts +1 -1
- package/src/api/Synthesis.ts +1 -1
- package/src/api/Translation.ts +1 -1
- package/src/api/TranslationAlignment.ts +1 -1
- package/src/audio/AudioBufferConversion.ts +46 -36
- package/src/cli/CLI.ts +1 -0
- package/src/codecs/FFMpegTranscoder.ts +3 -1
- package/src/codecs/WaveCodec.ts +11 -3
- package/src/recognition/WhisperSTT.ts +14 -7
- package/src/synthesis/EspeakTTS.ts +7 -1
- package/src/utilities/Compression.ts +16 -1
- package/src/utilities/LEB128.ts +237 -0
package/src/api/Alignment.ts
CHANGED
|
@@ -64,18 +64,6 @@ export async function align(input: AudioSourceParam, transcript: string, options
|
|
|
64
64
|
sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
|
|
65
65
|
sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
|
|
66
66
|
|
|
67
|
-
if (options.dtw!.windowDuration == null) {
|
|
68
|
-
const sourceAudioDuration = getRawAudioDuration(sourceRawAudio)
|
|
69
|
-
|
|
70
|
-
if (sourceAudioDuration < 5 * 60) { // If up to 5 minutes, set window to one minute
|
|
71
|
-
options.dtw!.windowDuration = 60
|
|
72
|
-
} else if (sourceAudioDuration < 60 * 60) { // If up to 1 hour, set window to 20% of total duration
|
|
73
|
-
options.dtw!.windowDuration = Math.ceil(sourceAudioDuration * 0.2)
|
|
74
|
-
} else { // If 1 hour or more, set window to 12 minutes
|
|
75
|
-
options.dtw!.windowDuration = 12 * 60
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
67
|
logger.end()
|
|
80
68
|
|
|
81
69
|
let language: string
|
|
@@ -87,7 +75,7 @@ export async function align(input: AudioSourceParam, transcript: string, options
|
|
|
87
75
|
|
|
88
76
|
logger.logTitledMessage('Language specified', formatLanguageCodeWithName(language))
|
|
89
77
|
} else {
|
|
90
|
-
logger.start('No language specified.
|
|
78
|
+
logger.start('No language specified. Detect language')
|
|
91
79
|
const { detectedLanguage } = await API.detectTextLanguage(transcript, options.languageDetection || {})
|
|
92
80
|
|
|
93
81
|
language = detectedLanguage
|
|
@@ -103,7 +91,9 @@ export async function align(input: AudioSourceParam, transcript: string, options
|
|
|
103
91
|
|
|
104
92
|
const { alignUsingDtwWithRecognition, alignUsingDtw } = await import('../alignment/SpeechAlignment.js')
|
|
105
93
|
|
|
106
|
-
function
|
|
94
|
+
function getDtwWindowGranularitiesAndDurations() {
|
|
95
|
+
const sourceAudioDuration = getRawAudioDuration(sourceRawAudio)
|
|
96
|
+
|
|
107
97
|
let granularities: DtwGranularity[]
|
|
108
98
|
let windowDurations: number[]
|
|
109
99
|
|
|
@@ -112,25 +102,52 @@ export async function align(input: AudioSourceParam, transcript: string, options
|
|
|
112
102
|
} else if (Array.isArray(options.dtw!.granularity)) {
|
|
113
103
|
granularities = options.dtw!.granularity
|
|
114
104
|
} else {
|
|
115
|
-
|
|
105
|
+
if (sourceAudioDuration < 1 * 60) {
|
|
106
|
+
// If up to 1 minute, set granularity to high, single pass
|
|
107
|
+
granularities = ['high']
|
|
108
|
+
} else if (sourceAudioDuration < 5 * 60) {
|
|
109
|
+
// If up to 5 minutes, set granularity to medium, single pass
|
|
110
|
+
granularities = ['medium']
|
|
111
|
+
} else if (sourceAudioDuration < 30 * 60) {
|
|
112
|
+
// If up to 30 minutes, set granularity to low, single pass
|
|
113
|
+
granularities = ['low']
|
|
114
|
+
} else {
|
|
115
|
+
// Otherwisek, use multipass processing, first with xx-low granularity, then low
|
|
116
|
+
granularities = ['xx-low', 'low']
|
|
117
|
+
}
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
if (
|
|
119
|
-
if (
|
|
120
|
+
if (options.dtw!.windowDuration) {
|
|
121
|
+
if (typeof options.dtw!.windowDuration === 'number') {
|
|
120
122
|
windowDurations = [options.dtw!.windowDuration]
|
|
121
|
-
} else if (
|
|
122
|
-
windowDurations =
|
|
123
|
+
} else if (Array.isArray(options.dtw!.windowDuration)) {
|
|
124
|
+
windowDurations = options.dtw!.windowDuration
|
|
123
125
|
} else {
|
|
124
|
-
throw new Error(`
|
|
126
|
+
throw new Error(`'dtw.windowDuration' must be a number or an array of numbers.`)
|
|
125
127
|
}
|
|
126
|
-
} else if (Array.isArray(options.dtw!.windowDuration)) {
|
|
127
|
-
windowDurations = options.dtw!.windowDuration
|
|
128
128
|
} else {
|
|
129
|
-
|
|
129
|
+
if (granularities.length > 2) {
|
|
130
|
+
throw new Error(`More than two passes requested, this requires window durations to be explicitly specified for each pass. For example 'dtw.windowDuration=[600,60,10]'.`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (sourceAudioDuration < 5 * 60) {
|
|
134
|
+
// If up to 5 minutes, set window duration to one minute
|
|
135
|
+
windowDurations = [60]
|
|
136
|
+
} else if (sourceAudioDuration < 2.5 * 60 * 60) {
|
|
137
|
+
// If less than 2.5 hours, set window duration to 20% of total duration
|
|
138
|
+
windowDurations = [Math.ceil(sourceAudioDuration * 0.2)]
|
|
139
|
+
} else {
|
|
140
|
+
// Otherwise, set window duration to 30 minutes
|
|
141
|
+
windowDurations = [30 * 60]
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (granularities.length === 2 && windowDurations.length === 1) {
|
|
146
|
+
windowDurations = [windowDurations[0], 15]
|
|
130
147
|
}
|
|
131
148
|
|
|
132
149
|
if (granularities.length != windowDurations.length) {
|
|
133
|
-
throw new Error(`
|
|
150
|
+
throw new Error(`The option 'dtw.granularity' has ${granularities.length} values, but 'dtw.windowDuration' has ${windowDurations.length} values. The lengths should be equal.`)
|
|
134
151
|
}
|
|
135
152
|
|
|
136
153
|
return { windowDurations, granularities }
|
|
@@ -140,6 +157,8 @@ export async function align(input: AudioSourceParam, transcript: string, options
|
|
|
140
157
|
|
|
141
158
|
switch (options.engine) {
|
|
142
159
|
case 'dtw': {
|
|
160
|
+
const { windowDurations, granularities } = getDtwWindowGranularitiesAndDurations()
|
|
161
|
+
|
|
143
162
|
logger.end()
|
|
144
163
|
|
|
145
164
|
const {
|
|
@@ -149,14 +168,14 @@ export async function align(input: AudioSourceParam, transcript: string, options
|
|
|
149
168
|
|
|
150
169
|
logger.end()
|
|
151
170
|
|
|
152
|
-
const { windowDurations, granularities } = getDtwWindowDurationsAndGranularities()
|
|
153
|
-
|
|
154
171
|
mappedTimeline = await alignUsingDtw(sourceRawAudio, referenceRawAudio, referenceTimeline, granularities, windowDurations)
|
|
155
172
|
|
|
156
173
|
break
|
|
157
174
|
}
|
|
158
175
|
|
|
159
176
|
case 'dtw-ra': {
|
|
177
|
+
const { windowDurations, granularities } = getDtwWindowGranularitiesAndDurations()
|
|
178
|
+
|
|
160
179
|
logger.end()
|
|
161
180
|
|
|
162
181
|
const recognitionOptions: API.RecognitionOptions =
|
|
@@ -181,8 +200,6 @@ export async function align(input: AudioSourceParam, transcript: string, options
|
|
|
181
200
|
|
|
182
201
|
logger.end()
|
|
183
202
|
|
|
184
|
-
const { windowDurations, granularities } = getDtwWindowDurationsAndGranularities()
|
|
185
|
-
|
|
186
203
|
const phoneAlignmentMethod = options.dtw!.phoneAlignmentMethod!
|
|
187
204
|
|
|
188
205
|
const espeakOptions: EspeakOptions = {
|
|
@@ -229,6 +246,8 @@ export async function align(input: AudioSourceParam, transcript: string, options
|
|
|
229
246
|
}
|
|
230
247
|
}
|
|
231
248
|
|
|
249
|
+
logger.start(`Postprocess timeline`)
|
|
250
|
+
|
|
232
251
|
// If the audio was cropped before recognition, map the timestamps back to the original audio
|
|
233
252
|
if (sourceUncropTimeline && sourceUncropTimeline.length > 0) {
|
|
234
253
|
API.convertCroppedToUncroppedTimeline(mappedTimeline, sourceUncropTimeline)
|
|
@@ -354,7 +373,7 @@ export const defaultAlignmentOptions: AlignmentOptions = {
|
|
|
354
373
|
},
|
|
355
374
|
|
|
356
375
|
dtw: {
|
|
357
|
-
granularity:
|
|
376
|
+
granularity: undefined,
|
|
358
377
|
windowDuration: undefined,
|
|
359
378
|
phoneAlignmentMethod: 'dtw'
|
|
360
379
|
},
|
|
@@ -158,7 +158,7 @@ export async function detectSpeechLanguageByParts(sourceRawAudio: RawAudio, getR
|
|
|
158
158
|
const endOffset = Math.min(audioTimeOffset + audioPartDuration, audioDuration)
|
|
159
159
|
const audioPartLength = endOffset - startOffset
|
|
160
160
|
|
|
161
|
-
logger.logTitledMessage(`\
|
|
161
|
+
logger.logTitledMessage(`\nDetect speech language starting at audio offset`, `${startOffset.toFixed(1)}`, chalk.magentaBright)
|
|
162
162
|
const audioPart = sliceRawAudioByTime(sourceRawAudio, startOffset, endOffset)
|
|
163
163
|
|
|
164
164
|
const resultsForPart = await getResultsForAudioPart(audioPart)
|
|
@@ -257,7 +257,7 @@ export async function detectTextLanguage(input: string, options: TextLanguageDet
|
|
|
257
257
|
case 'tinyld': {
|
|
258
258
|
const { detectLanguage } = await import('../text-language-detection/TinyLDLanguageDetection.js')
|
|
259
259
|
|
|
260
|
-
logger.start('
|
|
260
|
+
logger.start('Detect text language using tinyld')
|
|
261
261
|
|
|
262
262
|
detectedLanguageProbabilities = await detectLanguage(input)
|
|
263
263
|
|
|
@@ -267,7 +267,7 @@ export async function detectTextLanguage(input: string, options: TextLanguageDet
|
|
|
267
267
|
case 'fasttext': {
|
|
268
268
|
const { detectLanguage } = await import('../text-language-detection/FastTextLanguageDetection.js')
|
|
269
269
|
|
|
270
|
-
logger.start('
|
|
270
|
+
logger.start('Detect text language using FastText')
|
|
271
271
|
|
|
272
272
|
detectedLanguageProbabilities = await detectLanguage(input)
|
|
273
273
|
|
package/src/api/Recognition.ts
CHANGED
|
@@ -72,7 +72,7 @@ export async function recognize(input: AudioSourceParam, options: RecognitionOpt
|
|
|
72
72
|
logger.end()
|
|
73
73
|
logger.logTitledMessage('Language specified', formatLanguageCodeWithName(options.language))
|
|
74
74
|
} else {
|
|
75
|
-
logger.start('No language specified.
|
|
75
|
+
logger.start('No language specified. Detect speech language')
|
|
76
76
|
const { detectedLanguage } = await API.detectSpeechLanguage(sourceRawAudio, options.languageDetection!)
|
|
77
77
|
|
|
78
78
|
options.language = detectedLanguage
|
package/src/api/Synthesis.ts
CHANGED
|
@@ -54,7 +54,7 @@ async function synthesizeSegments(segments: string[], options: SynthesisOptions,
|
|
|
54
54
|
options = extendDeep(defaultSynthesisOptions, options)
|
|
55
55
|
|
|
56
56
|
if (!options.language && !options.voice) {
|
|
57
|
-
logger.start('No language or voice specified.
|
|
57
|
+
logger.start('No language or voice specified. Detect language')
|
|
58
58
|
|
|
59
59
|
let segmentsPlainText = segments
|
|
60
60
|
|
package/src/api/Translation.ts
CHANGED
|
@@ -72,7 +72,7 @@ export async function translateSpeech(input: AudioSourceParam, options: SpeechTr
|
|
|
72
72
|
logger.end()
|
|
73
73
|
logger.logTitledMessage('Source language specified', formatLanguageCodeWithName(options.sourceLanguage))
|
|
74
74
|
} else {
|
|
75
|
-
logger.start('No source language specified.
|
|
75
|
+
logger.start('No source language specified. Detect speech language')
|
|
76
76
|
const { detectedLanguage } = await detectSpeechLanguage(sourceRawAudio, options.languageDetection || {})
|
|
77
77
|
|
|
78
78
|
options.sourceLanguage = detectedLanguage
|
|
@@ -68,7 +68,7 @@ export async function alignTranslation(input: AudioSourceParam, transcript: stri
|
|
|
68
68
|
logger.end()
|
|
69
69
|
logger.logTitledMessage('Source language specified', formatLanguageCodeWithName(sourceLanguage))
|
|
70
70
|
} else {
|
|
71
|
-
logger.start('No source language specified.
|
|
71
|
+
logger.start('No source language specified. Detect speech language')
|
|
72
72
|
const { detectedLanguage } = await API.detectSpeechLanguage(sourceRawAudio, options.languageDetection || {})
|
|
73
73
|
|
|
74
74
|
sourceLanguage = detectedLanguage
|
|
@@ -8,34 +8,36 @@ import { BitDepth, SampleFormat } from '../codecs/WaveCodec.js'
|
|
|
8
8
|
export function encodeToAudioBuffer(audioChannels: Float32Array[], targetBitDepth: BitDepth = 16, targetSampleFormat: SampleFormat = SampleFormat.PCM) {
|
|
9
9
|
const interleavedChannels = interleaveChannels(audioChannels)
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
audioChannels = [] // Zero the array references to allow the GC to free up memory, if possible
|
|
12
|
+
|
|
13
|
+
if (targetSampleFormat === SampleFormat.PCM) {
|
|
14
|
+
if (targetBitDepth === 8) {
|
|
15
|
+
return BinaryArrayConversion.int8ToBuffer(float32ToInt8Pcm(interleavedChannels))
|
|
16
|
+
} else if (targetBitDepth === 16) {
|
|
15
17
|
return BinaryArrayConversion.int16ToBufferLE(float32ToInt16Pcm(interleavedChannels))
|
|
16
|
-
} else if (targetBitDepth
|
|
18
|
+
} else if (targetBitDepth === 24) {
|
|
17
19
|
return BinaryArrayConversion.int24ToBufferLE(float32ToInt24Pcm(interleavedChannels))
|
|
18
|
-
} else if (targetBitDepth
|
|
20
|
+
} else if (targetBitDepth === 32) {
|
|
19
21
|
return BinaryArrayConversion.int32ToBufferLE(float32ToInt32Pcm(interleavedChannels))
|
|
20
22
|
} else {
|
|
21
23
|
throw new Error(`Unsupported PCM bit depth: ${targetBitDepth}`)
|
|
22
24
|
}
|
|
23
|
-
} else if (targetSampleFormat
|
|
24
|
-
if (targetBitDepth
|
|
25
|
+
} else if (targetSampleFormat === SampleFormat.Float) {
|
|
26
|
+
if (targetBitDepth === 32) {
|
|
25
27
|
return BinaryArrayConversion.float32ToBufferLE(interleavedChannels)
|
|
26
|
-
} else if (targetBitDepth
|
|
28
|
+
} else if (targetBitDepth === 64) {
|
|
27
29
|
return BinaryArrayConversion.float64ToBufferLE(BinaryArrayConversion.float32Tofloat64(interleavedChannels))
|
|
28
30
|
} else {
|
|
29
31
|
throw new Error(`Unsupported float bit depth: ${targetBitDepth}`)
|
|
30
32
|
}
|
|
31
|
-
} else if (targetSampleFormat
|
|
32
|
-
if (targetBitDepth
|
|
33
|
+
} else if (targetSampleFormat === SampleFormat.Alaw) {
|
|
34
|
+
if (targetBitDepth === 8) {
|
|
33
35
|
return Buffer.from(AlawMulaw.alaw.encode(float32ToInt16Pcm(interleavedChannels)))
|
|
34
36
|
} else {
|
|
35
37
|
throw new Error(`Unsupported alaw bit depth: ${targetBitDepth}`)
|
|
36
38
|
}
|
|
37
|
-
} else if (targetSampleFormat
|
|
38
|
-
if (targetBitDepth
|
|
39
|
+
} else if (targetSampleFormat === SampleFormat.Mulaw) {
|
|
40
|
+
if (targetBitDepth === 8) {
|
|
39
41
|
return Buffer.from(AlawMulaw.mulaw.encode(float32ToInt16Pcm(interleavedChannels)))
|
|
40
42
|
} else {
|
|
41
43
|
throw new Error(`Unsupported mulaw bit depth: ${targetBitDepth}`)
|
|
@@ -48,34 +50,34 @@ export function encodeToAudioBuffer(audioChannels: Float32Array[], targetBitDept
|
|
|
48
50
|
export function decodeToChannels(audioBuffer: Buffer, channelCount: number, sourceBitDepth: number, sourceSampleFormat: SampleFormat) {
|
|
49
51
|
let interleavedChannels: Float32Array
|
|
50
52
|
|
|
51
|
-
if (sourceSampleFormat
|
|
52
|
-
if (sourceBitDepth
|
|
53
|
-
interleavedChannels =
|
|
54
|
-
} else if (sourceBitDepth
|
|
53
|
+
if (sourceSampleFormat === SampleFormat.PCM) {
|
|
54
|
+
if (sourceBitDepth === 8) {
|
|
55
|
+
interleavedChannels = int8PcmToFloat32(BinaryArrayConversion.bufferToInt8(audioBuffer))
|
|
56
|
+
} else if (sourceBitDepth === 16) {
|
|
55
57
|
interleavedChannels = int16PcmToFloat32(BinaryArrayConversion.bufferLEToInt16(audioBuffer))
|
|
56
|
-
} else if (sourceBitDepth
|
|
58
|
+
} else if (sourceBitDepth === 24) {
|
|
57
59
|
interleavedChannels = int24PcmToFloat32(BinaryArrayConversion.bufferLEToInt24(audioBuffer))
|
|
58
|
-
} else if (sourceBitDepth
|
|
60
|
+
} else if (sourceBitDepth === 32) {
|
|
59
61
|
interleavedChannels = int32PcmToFloat32(BinaryArrayConversion.bufferLEToInt32(audioBuffer))
|
|
60
62
|
} else {
|
|
61
63
|
throw new Error(`Unsupported PCM bit depth: ${sourceBitDepth}`)
|
|
62
64
|
}
|
|
63
|
-
} else if (sourceSampleFormat
|
|
64
|
-
if (sourceBitDepth
|
|
65
|
+
} else if (sourceSampleFormat === SampleFormat.Float) {
|
|
66
|
+
if (sourceBitDepth === 32) {
|
|
65
67
|
interleavedChannels = BinaryArrayConversion.bufferLEToFloat32(audioBuffer)
|
|
66
|
-
} else if (sourceBitDepth
|
|
68
|
+
} else if (sourceBitDepth === 64) {
|
|
67
69
|
interleavedChannels = BinaryArrayConversion.float64Tofloat32(BinaryArrayConversion.bufferLEToFloat64(audioBuffer))
|
|
68
70
|
} else {
|
|
69
71
|
throw new Error(`Unsupported float bit depth: ${sourceBitDepth}`)
|
|
70
72
|
}
|
|
71
|
-
} else if (sourceSampleFormat
|
|
72
|
-
if (sourceBitDepth
|
|
73
|
+
} else if (sourceSampleFormat === SampleFormat.Alaw) {
|
|
74
|
+
if (sourceBitDepth === 8) {
|
|
73
75
|
interleavedChannels = int16PcmToFloat32(AlawMulaw.alaw.decode(audioBuffer))
|
|
74
76
|
} else {
|
|
75
77
|
throw new Error(`Unsupported alaw bit depth: ${sourceBitDepth}`)
|
|
76
78
|
}
|
|
77
|
-
} else if (sourceSampleFormat
|
|
78
|
-
if (sourceBitDepth
|
|
79
|
+
} else if (sourceSampleFormat === SampleFormat.Mulaw) {
|
|
80
|
+
if (sourceBitDepth === 8) {
|
|
79
81
|
interleavedChannels = int16PcmToFloat32(AlawMulaw.mulaw.decode(audioBuffer))
|
|
80
82
|
} else {
|
|
81
83
|
throw new Error(`Unsupported mulaw bit depth: ${sourceBitDepth}`)
|
|
@@ -84,27 +86,29 @@ export function decodeToChannels(audioBuffer: Buffer, channelCount: number, sour
|
|
|
84
86
|
throw new Error(`Unsupported audio format: ${sourceSampleFormat}`)
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
audioBuffer = Buffer.from([]) // Zero the buffer reference to allow the GC to free up memory, if possible
|
|
90
|
+
|
|
87
91
|
return deInterleaveChannels(interleavedChannels, channelCount)
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
// Int8 PCM <-> Float32 conversion
|
|
91
|
-
export function
|
|
95
|
+
export function int8PcmToFloat32(input: Int8Array) {
|
|
92
96
|
const output = new Float32Array(input.length)
|
|
93
97
|
|
|
94
98
|
for (let i = 0; i < input.length; i++) {
|
|
95
|
-
const sample = input[i]
|
|
99
|
+
const sample = input[i]
|
|
96
100
|
output[i] = sample < 0 ? sample / 128 : sample / 127
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
return output
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
export function
|
|
103
|
-
const output = new
|
|
106
|
+
export function float32ToInt8Pcm(input: Float32Array) {
|
|
107
|
+
const output = new Int8Array(input.length)
|
|
104
108
|
|
|
105
109
|
for (let i = 0; i < input.length; i++) {
|
|
106
110
|
const sample = clampFloatSample(input[i])
|
|
107
|
-
output[i] = (
|
|
111
|
+
output[i] = (sample < 0 ? sample * 128 : sample * 127) | 0
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
return output
|
|
@@ -185,11 +189,11 @@ export function float32ToInt32Pcm(input: Float32Array) {
|
|
|
185
189
|
export function interleaveChannels(channels: Float32Array[]) {
|
|
186
190
|
const channelCount = channels.length
|
|
187
191
|
|
|
188
|
-
if (channelCount
|
|
192
|
+
if (channelCount === 0) {
|
|
189
193
|
throw new Error('Empty channel array received')
|
|
190
194
|
}
|
|
191
195
|
|
|
192
|
-
if (channelCount
|
|
196
|
+
if (channelCount === 1) {
|
|
193
197
|
return channels[0]
|
|
194
198
|
}
|
|
195
199
|
|
|
@@ -209,11 +213,11 @@ export function interleaveChannels(channels: Float32Array[]) {
|
|
|
209
213
|
}
|
|
210
214
|
|
|
211
215
|
export function deInterleaveChannels(interleavedChannels: Float32Array, channelCount: number) {
|
|
212
|
-
if (channelCount
|
|
216
|
+
if (channelCount === 0) {
|
|
213
217
|
throw new Error('0 channel count received')
|
|
214
218
|
}
|
|
215
219
|
|
|
216
|
-
if (channelCount
|
|
220
|
+
if (channelCount === 1) {
|
|
217
221
|
return [interleavedChannels]
|
|
218
222
|
}
|
|
219
223
|
|
|
@@ -244,5 +248,11 @@ export function deInterleaveChannels(interleavedChannels: Float32Array, channelC
|
|
|
244
248
|
// Utilities
|
|
245
249
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
246
250
|
export function clampFloatSample(floatSample: number) {
|
|
247
|
-
|
|
251
|
+
if (floatSample < -1.0) {
|
|
252
|
+
return -1.0
|
|
253
|
+
} else if (floatSample > 1.0) {
|
|
254
|
+
return 1.0
|
|
255
|
+
} else {
|
|
256
|
+
return floatSample
|
|
257
|
+
}
|
|
248
258
|
}
|
package/src/cli/CLI.ts
CHANGED
|
@@ -425,6 +425,7 @@ async function speak(operationData: CLIOperationData) {
|
|
|
425
425
|
} else if (sourceFileExtension == 'srt' || sourceFileExtension == 'vtt') {
|
|
426
426
|
const fileContent = await readFile(sourceFile, { encoding: 'utf-8' })
|
|
427
427
|
textSegments = subtitlesToTimeline(fileContent).map(entry => entry.text)
|
|
428
|
+
//textSegments = [subtitlesToText(fileContent)]
|
|
428
429
|
} else if (sourceFileExtension == 'xml' || sourceFileExtension == 'ssml') {
|
|
429
430
|
options.ssml = true
|
|
430
431
|
textSegments = [fileContent]
|
|
@@ -88,7 +88,9 @@ async function transcode_CLI(ffmpegCommand: string, input: string | Buffer, outp
|
|
|
88
88
|
|
|
89
89
|
process.on('close', (exitCode) => {
|
|
90
90
|
if (exitCode == 0) {
|
|
91
|
-
|
|
91
|
+
const concatenatedChunks = Buffer.concat(stdoutChunks)
|
|
92
|
+
|
|
93
|
+
resolve(concatenatedChunks)
|
|
92
94
|
} else {
|
|
93
95
|
reject(`ffmpeg exited with code ${exitCode}`)
|
|
94
96
|
log(stderrOutput)
|
package/src/codecs/WaveCodec.ts
CHANGED
|
@@ -87,13 +87,15 @@ export function decodeWave(waveData: Buffer, ignoreTruncatedChunks = true, ignor
|
|
|
87
87
|
throw new Error('A data subchunk was encountered before a format subchunk')
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// If the data chunk is truncated or extended beyond 4 GiB,
|
|
91
|
+
// the data would be read up to the end of the buffer
|
|
90
92
|
if (ignoreOverflowingDataChunks && subChunkSize === 4294967295) {
|
|
91
93
|
subChunkSize = waveData.length - readOffset
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
dataBuffers.push(
|
|
96
|
+
const subChunkData = waveData.subarray(readOffset, readOffset + subChunkSize)
|
|
97
|
+
|
|
98
|
+
dataBuffers.push(subChunkData)
|
|
97
99
|
}
|
|
98
100
|
// All sub chunks other than 'data' (e.g. 'LIST', 'fact', 'plst', 'junk' etc.) are ignored
|
|
99
101
|
|
|
@@ -111,6 +113,10 @@ export function decodeWave(waveData: Buffer, ignoreTruncatedChunks = true, ignor
|
|
|
111
113
|
throw new Error('No format subchunk was found in the wave file')
|
|
112
114
|
}
|
|
113
115
|
|
|
116
|
+
if (dataBuffers.length === 0) {
|
|
117
|
+
throw new Error('No data subchunks were found in the wave file')
|
|
118
|
+
}
|
|
119
|
+
|
|
114
120
|
const waveFormat = WaveFormat.deserializeFrom(formatSubChunkBodyBuffer)
|
|
115
121
|
|
|
116
122
|
const sampleFormat = waveFormat.sampleFormat
|
|
@@ -120,6 +126,8 @@ export function decodeWave(waveData: Buffer, ignoreTruncatedChunks = true, ignor
|
|
|
120
126
|
const speakerPositionMask = waveFormat.speakerPositionMask
|
|
121
127
|
|
|
122
128
|
const concatenatedDataBuffers = Buffer.concat(dataBuffers)
|
|
129
|
+
dataBuffers.length = 0 // Allow the garbage collector to free up memory held by the data buffers
|
|
130
|
+
|
|
123
131
|
const audioChannels = AudioBufferConversion.decodeToChannels(concatenatedDataBuffers, channelCount, bitDepth, sampleFormat)
|
|
124
132
|
|
|
125
133
|
return {
|
|
@@ -442,11 +442,6 @@ export class Whisper {
|
|
|
442
442
|
partTokensConfidence = partTokensConfidence.slice(initialTokens.length)
|
|
443
443
|
partCrossAttentionQKs = partCrossAttentionQKs.slice(initialTokens.length)
|
|
444
444
|
|
|
445
|
-
// Compute compression ratio for part (disabled for now)
|
|
446
|
-
if (false) {
|
|
447
|
-
const compressionRatioForPart = (await getDeflateCompressionMetricsForString(this.tokensToText(partTokens))).ratio
|
|
448
|
-
}
|
|
449
|
-
|
|
450
445
|
// Find alignment path
|
|
451
446
|
const alignmentPath = await this.findAlignmentPathFromQKs(partCrossAttentionQKs, partTokens, 0, segmentFrameCount) //, alignmentHeadsIndexes[this.modelName])
|
|
452
447
|
|
|
@@ -457,8 +452,17 @@ export class Whisper {
|
|
|
457
452
|
allDecodedTokens.push(...partTokens)
|
|
458
453
|
timeline.push(...partTimeline)
|
|
459
454
|
|
|
460
|
-
//
|
|
461
|
-
|
|
455
|
+
// Determine compression ratio for recognized text (normalized to lowercase) of this part
|
|
456
|
+
const compressionRatioForPart = (await getDeflateCompressionMetricsForString(this.tokensToText(partTokens).toLocaleLowerCase())).ratio
|
|
457
|
+
|
|
458
|
+
// If the recognized text isn't too repetitive
|
|
459
|
+
if (compressionRatioForPart < options.repetitionThreshold!) {
|
|
460
|
+
// Set current part tokens as the previous part text tokens
|
|
461
|
+
previousPartTextTokens = partTokens.filter(token => this.isTextToken(token))
|
|
462
|
+
} else {
|
|
463
|
+
// Otherwise, set previous part tokens to an empty array
|
|
464
|
+
previousPartTextTokens = []
|
|
465
|
+
}
|
|
462
466
|
|
|
463
467
|
audioOffset = audioEndOffset
|
|
464
468
|
|
|
@@ -544,6 +548,7 @@ export class Whisper {
|
|
|
544
548
|
autoPromptParts: false,
|
|
545
549
|
maxTokensPerPart: Infinity,
|
|
546
550
|
suppressRepetition: false,
|
|
551
|
+
repetitionThreshold: Infinity,
|
|
547
552
|
decodeTimestampTokens: true,
|
|
548
553
|
endTokenThreshold: whisperAlignmentOptions!.endTokenThreshold!,
|
|
549
554
|
includeEndTokenInCandidates: false,
|
|
@@ -2111,6 +2116,7 @@ export interface WhisperOptions {
|
|
|
2111
2116
|
autoPromptParts?: boolean
|
|
2112
2117
|
maxTokensPerPart?: number
|
|
2113
2118
|
suppressRepetition?: boolean
|
|
2119
|
+
repetitionThreshold?: number
|
|
2114
2120
|
decodeTimestampTokens?: boolean
|
|
2115
2121
|
endTokenThreshold?: number
|
|
2116
2122
|
includeEndTokenInCandidates?: boolean
|
|
@@ -2128,6 +2134,7 @@ export const defaultWhisperOptions: WhisperOptions = {
|
|
|
2128
2134
|
autoPromptParts: true,
|
|
2129
2135
|
maxTokensPerPart: 250,
|
|
2130
2136
|
suppressRepetition: true,
|
|
2137
|
+
repetitionThreshold: 2.4,
|
|
2131
2138
|
decodeTimestampTokens: true,
|
|
2132
2139
|
endTokenThreshold: 0.9,
|
|
2133
2140
|
includeEndTokenInCandidates: true,
|
|
@@ -44,7 +44,7 @@ export async function preprocessAndSynthesize(text: string, language: string, es
|
|
|
44
44
|
|
|
45
45
|
for (let i = 0; i < words.length; i++) {
|
|
46
46
|
const currentWord = words[i]
|
|
47
|
-
const previousWord = words[i-1]
|
|
47
|
+
const previousWord = words[i - 1]
|
|
48
48
|
|
|
49
49
|
if (i > 0 && currentWord == previousWord && !wordCharacterPattern.test(currentWord)) {
|
|
50
50
|
wordsWithMerges[wordsWithMerges.length - 1] += currentWord
|
|
@@ -62,6 +62,12 @@ export async function preprocessAndSynthesize(text: string, language: string, es
|
|
|
62
62
|
|
|
63
63
|
const simplifiedFragments = normalizedFragments.map(word => simplifyPunctuationCharacters(word).toLocaleLowerCase())
|
|
64
64
|
|
|
65
|
+
for (let i = 0; i < normalizedFragments.length; i++) {
|
|
66
|
+
if ([`'`, `"`].includes(simplifiedFragments[i])) {
|
|
67
|
+
normalizedFragments[i] = '()'
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
for (let fragmentIndex = 0; fragmentIndex < normalizedFragments.length; fragmentIndex++) {
|
|
66
72
|
const fragment = normalizedFragments[fragmentIndex]
|
|
67
73
|
|
|
@@ -107,6 +107,21 @@ export async function getDeflateCompressionMetricsForString(str: string) {
|
|
|
107
107
|
return {
|
|
108
108
|
originalSize: originalStringBytes.length,
|
|
109
109
|
compressedSize: compressedStringBytes.length,
|
|
110
|
-
ratio:
|
|
110
|
+
ratio: originalStringBytes.length / compressedStringBytes.length
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
+
|
|
114
|
+
export function computeDeltas(data: Float32Array) {
|
|
115
|
+
const deltas = new Float32Array(data.length)
|
|
116
|
+
|
|
117
|
+
let val = 0
|
|
118
|
+
|
|
119
|
+
for (let i = 0; i < data.length; i++) {
|
|
120
|
+
const delta = Math.floor(data[i] - val)
|
|
121
|
+
deltas[i] = delta
|
|
122
|
+
|
|
123
|
+
val += delta
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return deltas
|
|
127
|
+
}
|