echogarden 1.0.0 → 1.0.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/docs/Options.md CHANGED
@@ -148,14 +148,15 @@ Applies to CLI operation: `transcribe`, API method: `recognize`
148
148
  * `whisper.autoPromptParts`: use previous part's recognized text as prompt for the next part. Disabling this may help to prevent repetition carrying over between parts, in some cases. Defaults to `true`
149
149
  * `whisper.maxTokensPerPart`: maximum number of tokens to decode for each 30 second audio part. Defaults to `250`
150
150
  * `whisper.suppressRepetition`: attempt to suppress decoding repeating token patterns. Defaults to `true`
151
- * `whisper.seed`: use a custom seed for token selection, when temperature is greater than 0. Uses a constant seed by default
151
+ * `whisper.decodeTimestampTokens`: enable/disable decoding of timestamp tokens, since more accurate timing is already extracted via cross-attention weight alignment. For unclear reasons, setting to `false` can significantly reduce the occurrence of hallucinations and token repetition loops, and increases word timestamp accuracy. However, there are cases where this can cause the model to end a part prematurely, especially in singing and less speech-like voice segments, or when there are multiple speakers. Defaults to `true`
152
+ * `whisper.seed`: provide a custom random seed for token selection when temperature is greater than 0. Uses a constant seed by default to ensure reproducibility
152
153
 
153
154
  **Whisper.cpp**:
154
155
  * `whisperCpp.model`: selects which `whisper.cpp` model to use. Can be `tiny`, `tiny.en`, `base`, `base.en`, `small`, `small.en`, `medium`, `medium.en`, `large` (same as `large-v2`), `large-v1`, `large-v2`, `large-v3`. These quantized models are also supported: `tiny-q5_1`, `tiny.en-q5_1`, `tiny.en-q8_0`,`base-q5_1`, `base.en-q5_1`, `small-q5_1`, `small.en-q5_1`, `medium-q5_0`, `medium.en-q5_0`, `large-v2-q5_0`, `large-v3-q5_0`. Defaults to `base` or `base.en`
155
156
  * `whisperCpp.executablePath`: custom `whisper.cpp` executable path (currently required for macOS)
156
157
  * `whisperCpp.build`: type of `whisper.cpp` build to use. Can be set `cpu`, `cublas-11.8.0`, `cublas-12.4.0`. By default, builds are auto-selected and downloaded for Windows x64 (`cpu`, `cublas-11.8.0`, `cublas-12.4.0`) and Linux x64 (`cpu`). Using other builds requires providing a custom `executablePath`
157
158
  * `whisperCpp.threadCount`: number of threads to use, defaults to `4`
158
- * `whisperCpp.splitCount`: number of splits of the audio data to process in parallel (called `--processors` in the CLI). A value greater than `1` can increase memory use significantly, reduce timing accuracy, and slow down execution in some cases. Defaults to `1` (highly recommended)
159
+ * `whisperCpp.splitCount`: number of splits of the audio data to process in parallel (called `--processors` in the `whisper.cpp` CLI). A value greater than `1` can increase memory use significantly, reduce timing accuracy, and slow down execution in some cases. Defaults to `1` (highly recommended)
159
160
  * `whisperCpp.enableGPU`: enable GPU processing. Defaults to `true` on CUDA-enabled builds, otherwise `false`
160
161
  * `whisperCpp.topCandidateCount`: the number of top candidate tokens to consider. Defaults to `5`
161
162
  * `whisperCpp.beamCount`: the number of decoding paths to use during beam search. Defaults to `5`
@@ -205,7 +206,7 @@ Applies to CLI operation: `align`, API method: `align`
205
206
  * `engine`: alignment algorithm to use, can be `dtw`, `dtw-ra` or `whisper`. Defaults to `dtw`
206
207
  * `language`: language code for the audio and transcript ([ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)), like `en`, `fr`, `en-US`, `pt-BR`. Auto-detected from transcript if not set
207
208
  * `crop`: crop to active parts using voice activity detection before starting. Defaults to `true`
208
- * `isolate`: apply source separation to isolate voice before starting recognition. Defaults to `false`
209
+ * `isolate`: apply source separation to isolate voice before starting alignment. Defaults to `false`
209
210
  * `customLexiconPaths`: an array of custom lexicon file paths. Optional
210
211
  * `subtitles`: prefix to provide options for subtitles. Options detailed in section for subtitles
211
212
  * `vad`: prefix to provide options for voice activity detection when `crop` is set to `true`. Options detailed in section for voice activity detection
@@ -221,7 +222,7 @@ Applies to CLI operation: `align`, API method: `align`
221
222
 
222
223
  **DTW-RA only**:
223
224
  * `recognition`: prefix to provide recognition options when using `dtw-ra` method, for example: setting `recognition.engine = whisper` and `recognition.whisper.model = base.en`
224
- * `dtw.phoneAlignmentMethod`: algorithm to use when aligning phones: can either be set to `dtw` or `interpolate`. Defaults to `dtw`
225
+ * `dtw.phoneAlignmentMethod`: algorithm to use when aligning phones: can either be set to `dtw` or `interpolation`. Defaults to `dtw`
225
226
 
226
227
  **Whisper alignment only**:
227
228
  * `whisper`: prefix to provide Whisper options when the `whisper` alignment engine is used (does not apply to `dtw-ra` when `whisper` engine is used, for that use `recognition.whisper` prefix instead).
@@ -333,10 +334,3 @@ These are shared between text-to-speech, speech-to-text and alignment operations
333
334
  * `maxAddedDuration`: maximum extra time (in seconds) that may be added after a cue's end time. This gives the reader additional time to read the cue, and also ensures that very short duration cues aren't shown in a flash. Defaults to `3.0`
334
335
 
335
336
  **Note**: options `maxLineCount`, `maxLineWidth`, `minWordsInLine`, `separatePhrases`, are only effective when using the `segment` and `sentence` modes, and are ignored in all other modes. `maxAddedDuration` doesn't apply to modes `word`, `phone` and `word+phone` (they always use the exact start and end timestamps).
336
-
337
- # General CLI options
338
-
339
- * `--play`, `--no-play`: enable/disable audio playback. Defaults to play if there is no output file specified
340
- * `--overwrite`, `--no-overwrite`: overwrite/keep existing files. Doesn't overwrite by default
341
- * `--debug`, `--no-debug`: show/hide the full details of JavaScript errors, if they occur. Disabled by default
342
- * `--config=...`: path to configuration file to use. See the [CLI guide](CLI.md) for more information about configuration files
package/docs/Releases.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Release notes
2
2
 
3
- ## `1.0.0` (April 4, 2024)
3
+ ## `1.0.0` (April 12, 2024)
4
4
 
5
5
  **New features**:
6
6
  * Add support for [`whisper.cpp`](https://github.com/ggerganov/whisper.cpp), a C++ port of OpenAI's Whisper speech recognition architecture. It is faster than the integrated `whisper` engine, supports large models, and GPU processing. It can now be used for recognition, speech translation and alignment (via `dtw-ra`), though its word timestamps are less accurate than the integrated `whisper` engine.
@@ -13,6 +13,11 @@
13
13
  * Expose several new configuration options for the Whisper engine: a settings for maximum tokens per part, and a setting to enable/disable repetition suppression, set custom random seed, disable/enable decoding of timestamp tokens
14
14
  * Expose more options for the Elevenlabs engine
15
15
 
16
+ **Enhancements**:
17
+ * `detect-voice-activity` now also outputs the cropped voice when an audio file is given as output, with `.cropped` suffix added
18
+ * Improved word segmentation of whisper engine. Words spanning multiple recognized time segments are now split. Word timestamp ranges don't overlap non-speech sections.
19
+ * Whisper and Whisper.cpp outputs now include timestamps for individual recognized tokens
20
+
16
21
  **Behavioral and breaking changes**:
17
22
  * Minimal required node version changed to `18.0.0`
18
23
  * All recognition, alignment, translation and language recognition operations first apply the new adaptive gate VAD (can be changed to any other VAD engine via the `vad.` option prefix) and remove any sections that are not identified as containing voice, before starting processing. This should improve results in most cases, and reduce processing time
@@ -30,11 +35,6 @@
30
35
  * `maxIncreaseDb` post-processing and denoising option renamed to `maxGainIncrease`
31
36
  * `dryMixGainDb` denoising option renamed to `dryMixGain`
32
37
 
33
- **Enhancements**:
34
- * `detect-voice-activity` now also outputs the cropped voice when an audio file is given as output, with `.cropped` suffix added
35
- * Improved word segmentation of whisper engine. Words spanning multiple recognized time segments are now split. Word timestamp ranges don't overlap non-speech sections.
36
- * Whisper and Whisper.cpp outputs now include timestamps for individual recognized tokens
37
-
38
38
  **Fixes**:
39
39
  * Integrated `whisper` engine now uses `tiktoken` to tokenize text, which produces near identical tokenization compared to the official Python implementation. This fixes issues with decoding Chinese characters, correctly encoding prompts, and several others languages with characters that span multiple tokens
40
40
  * Fix voice language lists for multilingual voices in Elevenlabs TTS engine
@@ -43,14 +43,15 @@
43
43
  * Fix issue with identifying legacy IEEE Float wave format
44
44
  * Fix issue with language detection failing with empty inputs
45
45
  * Fix issue with Whisper model failing when no tokens are detected in a part
46
- * Fix issue with phone alignment not getting the right DTW window size, producing warnings like `all cost directions are equal to infinity
46
+ * Fix issue with phone alignment not getting the right DTW window size, producing warnings like `all cost directions are equal to infinity`
47
47
  * Fix phone timelines in DTW-RA
48
48
  * Add more workarounds for eSpeak tokenization bugs
49
49
  * Various other fixes
50
50
 
51
51
  **Documentation**:
52
- * Add new 'Releases' page. Releases before `1.0.x` were retroactively documented based on commit history (may not exactly detail all historical changes)
53
- * Options reference page restructured and updated with some missing information
52
+ * Add new [release notes](./Releases.md) page. Releases before `1.0.x` were retroactively documented based on commit history (may not exactly detail all historical changes)
53
+ * Add new [development page](./Development.md) with a quick guide for setting up a development environment to make changes to the Echogarden codebase
54
+ * Options reference page reorganized and updated with some missing information
54
55
 
55
56
  ## `0.12.x` (March 16, 2024)
56
57
 
@@ -67,6 +68,7 @@
67
68
  **Other**:
68
69
  * Include `package-lock.json` in repository
69
70
 
71
+
70
72
  ## `0.11.x` (August 17, 2023)
71
73
 
72
74
  Many features, enhancements, and fixes were incrementally added over the span of 7 months, up to March 2024.
@@ -87,6 +89,7 @@ Many features, enhancements, and fixes were incrementally added over the span of
87
89
  * Fix audio playback in macOS
88
90
  * Many other fixes
89
91
 
92
+
90
93
  ## `0.10.x` (August 2, 2023)
91
94
 
92
95
  **New features**:
@@ -100,6 +103,7 @@ Many features, enhancements, and fixes were incrementally added over the span of
100
103
  **Fixes**:
101
104
  * Many fixes
102
105
 
106
+
103
107
  ## `0.9.x` (July 29, 2023)
104
108
 
105
109
  **New features**:
@@ -114,6 +118,7 @@ Many features, enhancements, and fixes were incrementally added over the span of
114
118
  * Work around several eSpeak bugs
115
119
  * Many fixes
116
120
 
121
+
117
122
  ## `0.8.x` (July 25, 2023)
118
123
 
119
124
  **New features**:
@@ -122,26 +127,25 @@ Many features, enhancements, and fixes were incrementally added over the span of
122
127
  * Add punctuation thresholds to Whisper decoder
123
128
  * Expose subtitle configuration options to CLI
124
129
 
125
- **Behavioral changes**:
126
- * Rename `subtitles.minWords` to `subtitles.minWordsInLine`
127
-
128
130
  **Enhancements**:
129
131
  * Improve speech language detection to work with arbitrary length audio. Split audio to overlapping parts and detect each part individually. Then average the results.
130
132
  * Add more heteronyms
131
133
 
134
+ **Behavioral changes**:
135
+ * Rename `subtitles.minWords` to `subtitles.minWordsInLine`
136
+
132
137
  **Fixes**:
133
138
  * Improve numerical stability of softmax function
134
139
  * Work around eSpeak bug with markers in long inputs
135
140
  * Various fixes
136
141
 
142
+
137
143
  ## `0.7.x` (July 23, 2023)
138
144
 
139
145
  **New features**:
140
146
  * Alignment: Add option to accept custom lexicons
141
147
  * Implement and add decoder temperature option to Whisper model
142
148
 
143
- **Behavioral changes**:
144
-
145
149
  **Enhancements**:
146
150
  * Alignment: use preprocessing and lexicons
147
151
  * Add language detection to speech translation
@@ -151,10 +155,13 @@ Many features, enhancements, and fixes were incrementally added over the span of
151
155
  * Add colors to log messages
152
156
  * Warn when maximum DTW window duration is smaller than source audio duration.
153
157
 
158
+ **Behavioral changes**:
159
+
154
160
  **Fixes**:
155
161
  * Fix support for SSML input in eSpeak engine
156
162
  * Various fixes
157
163
 
164
+
158
165
  ## `0.6.x` (July 20, 2023)
159
166
 
160
167
  **New features**:
@@ -165,16 +172,16 @@ Many features, enhancements, and fixes were incrementally added over the span of
165
172
  * Add arguments to customize paragraph parsing
166
173
  * Change whitespace option to include option for collapsing all whitespace
167
174
 
168
- **Behavioral changes**:
169
- * Set Elevenlabs defaults to mid-values
170
- * Split plain text to paragraphs using double line breaks by default.
171
- * Rename `awsPolly` options to `amazonPolly` to be more consistent with documentation
172
-
173
175
  **Enhancements**:
174
176
  * Change whitespace option to include option for collapsing all whitespace
175
177
  * Add awareness of guillemets
176
178
  * Log full language of selected voice
177
179
 
180
+ **Behavioral changes**:
181
+ * Set Elevenlabs defaults to mid-values
182
+ * Split plain text to paragraphs using double line breaks by default.
183
+ * Rename `awsPolly` options to `amazonPolly` to be more consistent with documentation
184
+
178
185
  **Fixes**:
179
186
  * Fix and update Elevenlabs engine
180
187
  * Fix incorrect gender properties for some VITS voices
@@ -184,26 +191,25 @@ Many features, enhancements, and fixes were incrementally added over the span of
184
191
  **Other**:
185
192
  * Remove `package-lock.json` from the repository
186
193
 
194
+
187
195
  ## `0.5.x` (July, 19 2023)
188
196
 
189
197
  **New features**:
190
198
 
191
- **Behavioral changes**:
192
- * Move to new package system supporting version tags, and hosted in a Hugging Face repository
193
-
194
199
  **Enhancements**:
195
200
  * Add large numbers of VITS voices
196
201
 
202
+ **Behavioral changes**:
203
+ * Move to new package system supporting version tags, and hosted in a Hugging Face repository
204
+
197
205
  **Fixes**:
198
206
  * Various fixes
199
207
 
208
+
200
209
  ## `0.4.x` (July 9, 2023)
201
210
 
202
211
  **New features**:
203
212
 
204
- **Behavioral changes**:
205
- * Remove support for `afplay` and `aplay` for playback. Only SoX is used now.
206
-
207
213
  **Enhancements**:
208
214
  * Improve text normalization and add support for currencies
209
215
  * Improve heteronym lexicons
@@ -212,32 +218,34 @@ Many features, enhancements, and fixes were incrementally added over the span of
212
218
  * Add some British English (RP) pronunciations to heteronym lexicon
213
219
  * Add large numbers of VITS voices
214
220
 
221
+ **Behavioral changes**:
222
+ * Remove support for `afplay` and `aplay` for playback. Only SoX is used now.
223
+
215
224
  **Fixes**:
216
225
  * Various fixes
217
226
 
227
+
218
228
  ## `0.3.x` (July 1, 2023)
219
229
 
220
230
  **New features**:
221
231
 
222
- **Behavioral changes**:
223
-
224
232
  **Enhancements**:
225
233
  * Upgrade heteronym disambiguation to an improved, rule-based approach, which doesn't use POS tagging
226
234
  * Extend decade normalization
227
235
  * Improve text normalization and add support to currencies
228
236
  * Update the heteronym lexicon
229
237
 
238
+ **Behavioral changes**:
239
+
230
240
  **Fixes**:
231
241
  * Remove sentences containing only whitespace when synthesizing
232
242
  * Many fixes
233
243
 
244
+
234
245
  ## `0.2.x` (May 10, 2023)
235
246
 
236
247
  **New features**:
237
248
 
238
- **Behavioral changes**:
239
- * Remove dependency on `xregexp` package. Use Unicode RegExp instead.
240
-
241
249
  **Enhancements**:
242
250
  * Show current sentence and segment in synthesis log.
243
251
  * Add check for cancellation flag.
@@ -245,6 +253,9 @@ Many features, enhancements, and fixes were incrementally added over the span of
245
253
  * Improve auto TTS engine selection
246
254
  * Many Enhancements
247
255
 
256
+ **Behavioral changes**:
257
+ * Remove dependency on `xregexp` package. Use Unicode RegExp instead.
258
+
248
259
  **Fixes**:
249
260
  * Many fixes
250
261
 
package/docs/Tasklist.md CHANGED
@@ -2,8 +2,12 @@
2
2
 
3
3
  ## Bugs
4
4
 
5
- ### Synthesis
5
+ ### Alignment
6
6
 
7
+ * In DTW-RA, recognition transcript including something like "Question 2.What does Juan", where "2.What" has a point in the middle, is breaking playback of the timeline.
8
+ * DTW-RA will not work correctly with Polish language texts, due to issues with the eSpeak engine pronouncing `|` characters, which are intended to be used as separators and ignored by all other eSpeak languages.
9
+
10
+ ### Synthesis
7
11
 
8
12
  ### Phoneme processing
9
13
  * IPA -> Kirshenbaum translation is still not completely similar to what is output by eSpeak. Also, in rare situations, it outputs characters that are not accepted by eSpeak and eSpeak errors. Investigate when that happens and how to improve on this.
@@ -107,8 +111,8 @@
107
111
  * See if it's possible to reliably use eSpeak as a segmentation engine
108
112
 
109
113
  ### Subtitles
114
+ * Split long words if needed. This is especially important for Chinese
110
115
  * If a subtitle is too short and at the end of the audio, try to extend it back if possible (for example, if the previous subtitle is already extended, take back from it)
111
- * Split long words if needed
112
116
  * Decide how many punctuation characters to allow before breaking to a new line (currently it's infinite)
113
117
  * Add more clause separators, for even more special cases
114
118
  * Add option to output usable word or phoneme-level caption files (investigate how it's done on YouTube auto-captions)
@@ -154,10 +158,10 @@
154
158
  * Currently, when input is set to be SSML, it is wrapped in a `<speak>` tag. Handle the case where the user made their own SSML document wrapped with a `<speak>` tag as well. Currently, it may send invalid input to Azure
155
159
 
156
160
  ### Recognition
161
+ * Recognized word entries that span VAD segment boundaries can be split
157
162
  * Show alternatives when playing in the CLI. Clear current line and rewrite already printed text for alternatives during the speech recognition process
158
163
  * Option to split recognized audio to segments or sentences, as is done with synthesized audio
159
164
  * Try to exclude the timing for trailing punctuation tokens in words that contain them. This can help narrow down the end timestamp to cover the word more tightly
160
- * Recognized word entries that span VAD boundaries may be split
161
165
 
162
166
  ### Recognition / Whisper
163
167
  * May get stuck in a token repeat loop when silence or non-speech segment encountered in audio. Decide what to do
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echogarden",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "An integrated speech toolset, providing a variety of synthesis, recognition, alignment, and other processing tools, designed to be directly accessible to end-users.",
5
5
  "author": "Rotem Dan",
6
6
  "license": "GPL-3.0",
@@ -90,11 +90,11 @@
90
90
  "ndarray": "^1.0.19",
91
91
  "ndarray-ops": "^1.2.2",
92
92
  "onnxruntime-node": "^1.17.0",
93
- "openai": "^4.33.0",
93
+ "openai": "^4.33.1",
94
94
  "sam-js": "^0.2.1",
95
95
  "strip-ansi": "^7.1.0",
96
96
  "tar": "^7.0.0",
97
- "tiktoken": "^1.0.13",
97
+ "tiktoken": "^1.0.14",
98
98
  "tinyld": "^1.3.4",
99
99
  "ws": "^8.16.0",
100
100
  "wtf_wikipedia": "^10.3.0"
@@ -127,7 +127,7 @@
127
127
  "@types/recursive-readdir": "^2.2.4",
128
128
  "@types/tar": "^6.1.12",
129
129
  "@types/ws": "^8.5.10",
130
- "ts-json-schema-generator": "^1.5.1",
130
+ "ts-json-schema-generator": "^2.0.0",
131
131
  "typescript": "^5.4.5"
132
132
  }
133
133
  }
@@ -12,6 +12,7 @@ import { synthesize } from '../api/API.js'
12
12
  import { resampleAudioSpeex } from '../dsp/SpeexResampler.js'
13
13
  import { deepClone } from '../utilities/ObjectUtilities.js'
14
14
  import { zeroIfNaN } from '../math/VectorMath.js'
15
+ import { EspeakOptions } from '../synthesis/EspeakTTS.js'
15
16
 
16
17
  export async function alignUsingDtw(
17
18
  sourceRawAudio: RawAudio,
@@ -155,10 +156,9 @@ export async function alignUsingDtwWithRecognition(
155
156
  referenceRawAudio: RawAudio,
156
157
  referenceTimeline: Timeline,
157
158
  recognitionTimeline: Timeline,
158
- synthesizedRecognizedTranscriptRawAudio: RawAudio,
159
- synthesizedRecognitionTimeline: Timeline,
160
159
  granularities: DtwGranularity[],
161
160
  windowDurations: number[],
161
+ espeakOptions: EspeakOptions,
162
162
  phoneAlignmentMethod: API.PhoneAlignmentMethod = 'interpolation') {
163
163
 
164
164
  const logger = new Logger()
@@ -182,13 +182,25 @@ export async function alignUsingDtwWithRecognition(
182
182
  return interpolatedTimeline
183
183
  }
184
184
 
185
+ // Synthesize the recognized transcript and get its timeline
186
+ logger.start("Synthesize recognized transcript with eSpeak")
187
+ const recognizedWords = recognitionTimeline.map(entry => entry.text)
188
+
189
+ const {
190
+ rawAudio: synthesizedRecognizedTranscriptRawAudio,
191
+ timeline: synthesizedRecognitionTimeline
192
+ } = await createAlignmentReferenceUsingEspeakForFragments(recognizedWords, espeakOptions)
193
+
185
194
  let recognitionTimelineWithPhones: Timeline
186
195
 
187
196
  if (phoneAlignmentMethod == 'interpolation') {
188
197
  // Add phone timelines by interpolating from reference words
189
198
  logger.start('Interpolate phone timing')
190
199
 
191
- recognitionTimelineWithPhones = await interpolatePhoneTimelines(recognitionTimeline, synthesizedRecognitionTimeline)
200
+ recognitionTimelineWithPhones = await interpolatePhoneTimelines(
201
+ recognitionTimeline,
202
+ synthesizedRecognitionTimeline
203
+ )
192
204
  } else if (phoneAlignmentMethod == 'dtw') {
193
205
  logger.start('Align phone timing')
194
206
 
@@ -205,7 +217,7 @@ export async function alignUsingDtwWithRecognition(
205
217
  }
206
218
 
207
219
  // Create a mapping from the synthesized recognized timeline to the recognized timeline
208
- logger.start('Map from the synthesized recognized timeline to the recognized timeline')
220
+ logger.start("Map from the synthesized recognized timeline to the recognized timeline")
209
221
 
210
222
  type SynthesizedToRecognizedTimeMappingEntry = {
211
223
  synthesized: number
@@ -217,16 +229,18 @@ export async function alignUsingDtwWithRecognition(
217
229
  const synthesizedToRecognizedTimeMapping: SynthesizedToRecognizedTimeMapping = []
218
230
 
219
231
  for (let wordEntryIndex = 0; wordEntryIndex < synthesizedRecognitionTimeline.length; wordEntryIndex++) {
220
- const synthesizedWordTimelineEntry = synthesizedRecognitionTimeline[wordEntryIndex]
221
- const recognitionWordTimelineEntry = recognitionTimelineWithPhones[wordEntryIndex]
232
+ const synthesizedTimelineEntry = synthesizedRecognitionTimeline[wordEntryIndex]
233
+ const recognitionTimelineEntry = recognitionTimelineWithPhones[wordEntryIndex]
222
234
 
223
- for (let tokenEntryIndex = 0; tokenEntryIndex < synthesizedWordTimelineEntry.timeline!.length; tokenEntryIndex++) {
224
- const synthesizedTokenTimelineEntry = synthesizedWordTimelineEntry.timeline![tokenEntryIndex]
225
- const recognitionTokenTimelineEntry = recognitionWordTimelineEntry.timeline![tokenEntryIndex]
235
+ synthesizedToRecognizedTimeMapping.push({
236
+ synthesized: synthesizedTimelineEntry.startTime,
237
+ recognized: recognitionTimelineEntry.startTime
238
+ })
226
239
 
227
- for (let phoneEntryIndex = 0; phoneEntryIndex < synthesizedTokenTimelineEntry.timeline!.length; phoneEntryIndex++) {
228
- const synthesizedPhoneTimelineEntry = synthesizedTokenTimelineEntry.timeline![phoneEntryIndex]
229
- const recognitionPhoneTimelineEntry = recognitionTokenTimelineEntry.timeline![phoneEntryIndex]
240
+ if (synthesizedTimelineEntry.timeline) {
241
+ for (let tokenEntryIndex = 0; tokenEntryIndex < synthesizedTimelineEntry.timeline.length; tokenEntryIndex++) {
242
+ const synthesizedPhoneTimelineEntry = synthesizedTimelineEntry.timeline[tokenEntryIndex]
243
+ const recognitionPhoneTimelineEntry = recognitionTimelineEntry.timeline![tokenEntryIndex]
230
244
 
231
245
  synthesizedToRecognizedTimeMapping.push({
232
246
  synthesized: synthesizedPhoneTimelineEntry.startTime,
@@ -239,10 +253,16 @@ export async function alignUsingDtwWithRecognition(
239
253
  })
240
254
  }
241
255
  }
256
+
257
+ synthesizedToRecognizedTimeMapping.push({
258
+ synthesized: synthesizedTimelineEntry.endTime,
259
+ recognized: recognitionTimelineEntry.endTime
260
+ })
242
261
  }
243
262
 
244
- logger.start('Align the synthesized recognized transcript with the synthesized ground-truth transcript')
245
263
  // Align the synthesized recognized transcript to the synthesized reference transcript
264
+ logger.start("Align the synthesized recognized transcript with the synthesized ground-truth transcript")
265
+
246
266
  const alignedSynthesizedRecognitionTimeline = await alignUsingDtw(
247
267
  synthesizedRecognizedTranscriptRawAudio,
248
268
  referenceRawAudio,
@@ -250,41 +270,45 @@ export async function alignUsingDtwWithRecognition(
250
270
  granularities,
251
271
  windowDurations)
252
272
 
253
- function mapTimeline(timeline: Timeline) {
254
- function mapSynthesizedToRecognizedTime(synthesizedTime: number) {
255
- for (let mappingIndex = 0; ; mappingIndex += 1) {
256
- const left = synthesizedToRecognizedTimeMapping[mappingIndex].synthesized
273
+ let currentSynthesizedToRecognizedMappingIndex = 0
274
+
275
+ // Map from synthesized reference timestamps to the recognition timestamps
276
+ function mapSynthesizedToRecognizedTimeAndAdvance(synthesizedTime: number) {
277
+ for (; ; currentSynthesizedToRecognizedMappingIndex += 1) {
278
+ const left = synthesizedToRecognizedTimeMapping[currentSynthesizedToRecognizedMappingIndex].synthesized
257
279
 
258
- let right: number
280
+ let right: number
259
281
 
260
- if (mappingIndex < synthesizedToRecognizedTimeMapping.length - 1) {
261
- right = synthesizedToRecognizedTimeMapping[mappingIndex + 1].synthesized
262
- } else {
263
- right = Infinity
264
- }
282
+ if (currentSynthesizedToRecognizedMappingIndex < synthesizedToRecognizedTimeMapping.length - 1) {
283
+ right = synthesizedToRecognizedTimeMapping[currentSynthesizedToRecognizedMappingIndex + 1].synthesized
284
+ } else {
285
+ right = Infinity
286
+ }
265
287
 
266
- if (left > right) {
267
- throw new Error('Left is greater than right!')
268
- }
288
+ if (left > right) {
289
+ throw new Error("Left is larger than right!")
290
+ }
269
291
 
270
- if (Math.abs(synthesizedTime - left) < Math.abs(synthesizedTime - right)) {
271
- return synthesizedToRecognizedTimeMapping[mappingIndex].recognized
272
- }
292
+ if (Math.abs(synthesizedTime - left) < Math.abs(synthesizedTime - right)) {
293
+ return synthesizedToRecognizedTimeMapping[currentSynthesizedToRecognizedMappingIndex].recognized
273
294
  }
274
295
  }
296
+ }
275
297
 
298
+ function mapTimeline(timeline: Timeline) {
276
299
  const mappedTimeline: Timeline = []
277
300
 
278
301
  for (const entry of timeline) {
279
- const mappedEntry = deepClone(entry)
302
+ const mappedEntry = { ...entry }
280
303
 
281
- mappedEntry.startTime = mapSynthesizedToRecognizedTime(entry.startTime)
282
- mappedEntry.endTime = mapSynthesizedToRecognizedTime(entry.endTime)
304
+ mappedEntry.startTime = mapSynthesizedToRecognizedTimeAndAdvance(entry.startTime)
283
305
 
284
306
  if (entry.timeline) {
285
307
  mappedEntry.timeline = mapTimeline(entry.timeline)
286
308
  }
287
309
 
310
+ mappedEntry.endTime = mapSynthesizedToRecognizedTimeAndAdvance(entry.endTime)
311
+
288
312
  mappedTimeline.push(mappedEntry)
289
313
  }
290
314
 
@@ -301,10 +325,10 @@ export async function alignUsingDtwWithRecognition(
301
325
  export async function interpolatePhoneTimelines(sourceTimeline: Timeline, referenceTimeline: Timeline) {
302
326
  const interpolatedTimeline: Timeline = []
303
327
 
304
- for (let i = 0; i < sourceTimeline.length; i++) {
305
- const referenceEntry = referenceTimeline[i]
328
+ for (let wordEntryIndex = 0; wordEntryIndex < sourceTimeline.length; wordEntryIndex++) {
329
+ const referenceEntry = referenceTimeline[wordEntryIndex]
306
330
 
307
- const interpolatedEntry = deepClone(sourceTimeline[i])
331
+ const interpolatedEntry = deepClone(sourceTimeline[wordEntryIndex])
308
332
  interpolatedTimeline.push(interpolatedEntry)
309
333
 
310
334
  if (interpolatedEntry.type != 'word') {
@@ -335,23 +359,13 @@ export async function interpolatePhoneTimelines(sourceTimeline: Timeline, refere
335
359
  }
336
360
  }
337
361
 
338
- const interpolatedTokenEntries: Timeline = []
339
-
340
- for (const tokenEntry of (referenceEntry.timeline || [])!) {
341
- const interpolatedTokenEntry = mapEntry(tokenEntry)
342
-
343
- const interpolatedPhoneEntries: Timeline = []
344
-
345
- for (const phoneEntry of (tokenEntry.timeline || [])) {
346
- interpolatedPhoneEntries.push(mapEntry(phoneEntry))
347
- }
348
-
349
- interpolatedTokenEntry.timeline = interpolatedPhoneEntries
362
+ const interpolatedPhoneEntries: Timeline = []
350
363
 
351
- interpolatedTokenEntries.push(interpolatedTokenEntry)
364
+ for (const phoneEntry of (referenceEntry.timeline || [])) {
365
+ interpolatedPhoneEntries.push(mapEntry(phoneEntry))
352
366
  }
353
367
 
354
- interpolatedEntry.timeline = interpolatedTokenEntries
368
+ interpolatedEntry.timeline = interpolatedPhoneEntries
355
369
  }
356
370
 
357
371
  return interpolatedTimeline
@@ -424,23 +438,13 @@ export async function alignPhoneTimelines(
424
438
  }
425
439
 
426
440
  // Add phone timeline using the mapped time information
427
- const alignedTokenTimeline: Timeline = []
441
+ const alignedPhoneTimeline: Timeline = []
428
442
 
429
- for (const referenceTokenEntry of (referenceWordEntry.timeline || [])) {
430
- const alignedPhoneTimeline: Timeline = []
431
-
432
- for (const referencePhoneEntry of (referenceTokenEntry.timeline || [])) {
433
- alignedPhoneTimeline.push(mapEntry(referencePhoneEntry))
434
- }
435
-
436
- alignedTokenTimeline.push({
437
- ...mapEntry(referenceTokenEntry),
438
-
439
- timeline: alignedPhoneTimeline
440
- })
443
+ for (const referencePhoneEntry of (referenceWordEntry.timeline || [])) {
444
+ alignedPhoneTimeline.push(mapEntry(referencePhoneEntry))
441
445
  }
442
446
 
443
- alignedWordEntry.timeline = alignedTokenTimeline
447
+ alignedWordEntry.timeline = alignedPhoneTimeline
444
448
 
445
449
  alignedWordTimeline.push(alignedWordEntry)
446
450
  }
@@ -448,6 +452,27 @@ export async function alignPhoneTimelines(
448
452
  return alignedWordTimeline
449
453
  }
450
454
 
455
+ export async function createAlignmentReferenceUsingEspeakForFragments(fragments: string[], espeakOptions: EspeakOptions, insertSeparators = true) {
456
+ const progressLogger = new Logger()
457
+
458
+ progressLogger.start("Load espeak module")
459
+ const Espeak = await import("../synthesis/EspeakTTS.js")
460
+
461
+ progressLogger.start("Create alignment reference with eSpeak")
462
+
463
+ const result = await Espeak.synthesizeFragments(fragments, espeakOptions)
464
+
465
+ result.timeline = result.timeline.flatMap(clause => clause.timeline!)
466
+
467
+ for (const wordEntry of result.timeline) {
468
+ wordEntry.timeline = wordEntry.timeline!.flatMap(tokenEntry => tokenEntry.timeline!)
469
+ }
470
+
471
+ progressLogger.end()
472
+
473
+ return result
474
+ }
475
+
451
476
  export async function createAlignmentReferenceUsingEspeak(transcript: string, language: string, plaintextOptions?: API.PlainTextOptions, customLexiconPaths?: string[], insertSeparators?: boolean) {
452
477
  const logger = new Logger()
453
478