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.
Files changed (57) hide show
  1. package/data/schemas/options.json +3 -1
  2. package/dist/alignment/DTWSequenceAlignmentWindowed.js +68 -27
  3. package/dist/alignment/DTWSequenceAlignmentWindowed.js.map +1 -1
  4. package/dist/alignment/SpeechAlignment.d.ts +1 -1
  5. package/dist/alignment/SpeechAlignment.js +6 -21
  6. package/dist/alignment/SpeechAlignment.js.map +1 -1
  7. package/dist/api/Alignment.js +47 -28
  8. package/dist/api/Alignment.js.map +1 -1
  9. package/dist/api/LanguageDetection.js +3 -3
  10. package/dist/api/LanguageDetection.js.map +1 -1
  11. package/dist/api/Recognition.js +1 -1
  12. package/dist/api/Recognition.js.map +1 -1
  13. package/dist/api/Synthesis.js +1 -1
  14. package/dist/api/Synthesis.js.map +1 -1
  15. package/dist/api/Translation.js +1 -1
  16. package/dist/api/Translation.js.map +1 -1
  17. package/dist/api/TranslationAlignment.js +1 -1
  18. package/dist/api/TranslationAlignment.js.map +1 -1
  19. package/dist/audio/AudioBufferConversion.d.ts +2 -2
  20. package/dist/audio/AudioBufferConversion.js +46 -36
  21. package/dist/audio/AudioBufferConversion.js.map +1 -1
  22. package/dist/cli/CLI.js +1 -0
  23. package/dist/cli/CLI.js.map +1 -1
  24. package/dist/codecs/FFMpegTranscoder.js +2 -1
  25. package/dist/codecs/FFMpegTranscoder.js.map +1 -1
  26. package/dist/codecs/WaveCodec.js +8 -3
  27. package/dist/codecs/WaveCodec.js.map +1 -1
  28. package/dist/recognition/WhisperSTT.d.ts +1 -0
  29. package/dist/recognition/WhisperSTT.js +13 -6
  30. package/dist/recognition/WhisperSTT.js.map +1 -1
  31. package/dist/synthesis/EspeakTTS.js +5 -0
  32. package/dist/synthesis/EspeakTTS.js.map +1 -1
  33. package/dist/utilities/Compression.d.ts +1 -0
  34. package/dist/utilities/Compression.js +11 -1
  35. package/dist/utilities/Compression.js.map +1 -1
  36. package/dist/utilities/LEB128.d.ts +5 -0
  37. package/dist/utilities/LEB128.js +168 -0
  38. package/dist/utilities/LEB128.js.map +1 -0
  39. package/docs/Options.md +2 -1
  40. package/docs/Tasklist.md +3 -3
  41. package/package.json +7 -7
  42. package/src/alignment/DTWSequenceAlignmentWindowed.ts +69 -29
  43. package/src/alignment/SpeechAlignment.ts +7 -23
  44. package/src/api/Alignment.ts +48 -29
  45. package/src/api/LanguageDetection.ts +3 -3
  46. package/src/api/Recognition.ts +1 -1
  47. package/src/api/Synthesis.ts +1 -1
  48. package/src/api/Translation.ts +1 -1
  49. package/src/api/TranslationAlignment.ts +1 -1
  50. package/src/audio/AudioBufferConversion.ts +46 -36
  51. package/src/cli/CLI.ts +1 -0
  52. package/src/codecs/FFMpegTranscoder.ts +3 -1
  53. package/src/codecs/WaveCodec.ts +11 -3
  54. package/src/recognition/WhisperSTT.ts +14 -7
  55. package/src/synthesis/EspeakTTS.ts +7 -1
  56. package/src/utilities/Compression.ts +16 -1
  57. package/src/utilities/LEB128.ts +237 -0
@@ -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. Detecting language')
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 getDtwWindowDurationsAndGranularities() {
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
- granularities = ['auto']
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 (typeof options.dtw!.windowDuration == 'number') {
119
- if (granularities.length == 1) {
120
+ if (options.dtw!.windowDuration) {
121
+ if (typeof options.dtw!.windowDuration === 'number') {
120
122
  windowDurations = [options.dtw!.windowDuration]
121
- } else if (granularities.length == 2) {
122
- windowDurations = [options.dtw!.windowDuration, 15]
123
+ } else if (Array.isArray(options.dtw!.windowDuration)) {
124
+ windowDurations = options.dtw!.windowDuration
123
125
  } else {
124
- 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]'.`)
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
- throw new Error('No window duration given')
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(`Unequal element counts in options. 'dtw.granularity' has ${granularities.length} items, but 'dtw.windowDuration' has ${windowDurations.length} items. Can't infer what number of DTW passes were intended.`)
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: 'auto',
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(`\nDetecting speech language starting at audio offset`, `${startOffset.toFixed(1)}`, chalk.magentaBright)
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('Detecting text language using tinyld')
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('Detecting text language using FastText')
270
+ logger.start('Detect text language using FastText')
271
271
 
272
272
  detectedLanguageProbabilities = await detectLanguage(input)
273
273
 
@@ -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. Detecting speech language')
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
@@ -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. Detecting language')
57
+ logger.start('No language or voice specified. Detect language')
58
58
 
59
59
  let segmentsPlainText = segments
60
60
 
@@ -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. Detecting speech language')
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. Detecting speech language')
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
- if (targetSampleFormat == SampleFormat.PCM) {
12
- if (targetBitDepth == 8) {
13
- return Buffer.from(float32ToUint8Pcm(interleavedChannels).buffer)
14
- } else if (targetBitDepth == 16) {
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 == 24) {
18
+ } else if (targetBitDepth === 24) {
17
19
  return BinaryArrayConversion.int24ToBufferLE(float32ToInt24Pcm(interleavedChannels))
18
- } else if (targetBitDepth == 32) {
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 == SampleFormat.Float) {
24
- if (targetBitDepth == 32) {
25
+ } else if (targetSampleFormat === SampleFormat.Float) {
26
+ if (targetBitDepth === 32) {
25
27
  return BinaryArrayConversion.float32ToBufferLE(interleavedChannels)
26
- } else if (targetBitDepth == 64) {
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 == SampleFormat.Alaw) {
32
- if (targetBitDepth == 8) {
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 == SampleFormat.Mulaw) {
38
- if (targetBitDepth == 8) {
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 == SampleFormat.PCM) {
52
- if (sourceBitDepth == 8) {
53
- interleavedChannels = uint8PcmToFloat32(audioBuffer)
54
- } else if (sourceBitDepth == 16) {
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 == 24) {
58
+ } else if (sourceBitDepth === 24) {
57
59
  interleavedChannels = int24PcmToFloat32(BinaryArrayConversion.bufferLEToInt24(audioBuffer))
58
- } else if (sourceBitDepth == 32) {
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 == SampleFormat.Float) {
64
- if (sourceBitDepth == 32) {
65
+ } else if (sourceSampleFormat === SampleFormat.Float) {
66
+ if (sourceBitDepth === 32) {
65
67
  interleavedChannels = BinaryArrayConversion.bufferLEToFloat32(audioBuffer)
66
- } else if (sourceBitDepth == 64) {
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 == SampleFormat.Alaw) {
72
- if (sourceBitDepth == 8) {
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 == SampleFormat.Mulaw) {
78
- if (sourceBitDepth == 8) {
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 uint8PcmToFloat32(input: Uint8Array) {
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] - 128
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 float32ToUint8Pcm(input: Float32Array) {
103
- const output = new Uint8Array(input.length)
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] = ((sample < 0 ? sample * 128 : sample * 127) | 0) + 128
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 == 0) {
192
+ if (channelCount === 0) {
189
193
  throw new Error('Empty channel array received')
190
194
  }
191
195
 
192
- if (channelCount == 1) {
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 == 0) {
216
+ if (channelCount === 0) {
213
217
  throw new Error('0 channel count received')
214
218
  }
215
219
 
216
- if (channelCount == 1) {
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
- return Math.max(-1, Math.min(floatSample, 1))
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
- resolve(Buffer.concat(stdoutChunks))
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)
@@ -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
- // If the data chunk is truncated, but truncations are ignored,
95
- // it would be read up to the end of the buffer
96
- dataBuffers.push(waveData.subarray(readOffset, readOffset + subChunkSize))
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
- // Update previous text tokens
461
- previousPartTextTokens = partTokens.filter(token => this.isTextToken(token))
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: compressedStringBytes.length / originalStringBytes.length
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
+ }