echogarden 1.2.0 → 1.3.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.
@@ -6,7 +6,7 @@ import { Logger } from '../utilities/Logger.js'
6
6
 
7
7
  import * as API from './API.js'
8
8
  import { Timeline, addTimeOffsetToTimeline, addWordTextOffsetsToTimeline, wordTimelineToSegmentSentenceTimeline } from '../utilities/Timeline.js'
9
- import { formatLanguageCodeWithName, getDefaultDialectForLanguageCodeIfPossible, getShortLanguageCode, normalizeLanguageCode } from '../utilities/Locale.js'
9
+ import { formatLanguageCodeWithName, getDefaultDialectForLanguageCodeIfPossible, getShortLanguageCode, parseLangIdentifier } from '../utilities/Locale.js'
10
10
  import { type WhisperAlignmentOptions } from '../recognition/WhisperSTT.js'
11
11
  import chalk from 'chalk'
12
12
  import { DtwGranularity, createAlignmentReferenceUsingEspeak } from '../alignment/SpeechAlignment.js'
@@ -72,14 +72,20 @@ export async function align(input: AudioSourceParam, transcript: string, options
72
72
  let language: string
73
73
 
74
74
  if (options.language) {
75
- language = normalizeLanguageCode(options.language!)
75
+ const languageData = await parseLangIdentifier(options.language)
76
+
77
+ language = languageData.Name
78
+
79
+ logger.logTitledMessage('Language specified', formatLanguageCodeWithName(language))
76
80
  } else {
77
81
  logger.start('No language specified. Detecting language')
78
82
  const { detectedLanguage } = await API.detectTextLanguage(transcript, options.languageDetection || {})
83
+
79
84
  language = detectedLanguage
80
85
 
81
86
  logger.end()
82
- logger.logTitledMessage('Language detected', formatLanguageCodeWithName(detectedLanguage))
87
+
88
+ logger.logTitledMessage('Language detected', formatLanguageCodeWithName(language))
83
89
  }
84
90
 
85
91
  language = getDefaultDialectForLanguageCodeIfPossible(language)
@@ -6,7 +6,7 @@ import { Logger } from '../utilities/Logger.js'
6
6
 
7
7
  import * as API from './API.js'
8
8
  import { Timeline, addWordTextOffsetsToTimeline, wordTimelineToSegmentSentenceTimeline } from '../utilities/Timeline.js'
9
- import { formatLanguageCodeWithName, getShortLanguageCode, normalizeLanguageCode } from '../utilities/Locale.js'
9
+ import { formatLanguageCodeWithName, parseLangIdentifier } from '../utilities/Locale.js'
10
10
  import { loadPackage } from '../utilities/PackageManager.js'
11
11
  import chalk from 'chalk'
12
12
 
@@ -62,24 +62,27 @@ export async function recognize(input: AudioSourceParam, options: RecognitionOpt
62
62
 
63
63
  const engine = options.engine!
64
64
 
65
- if (!options.language) { // && options.engine != 'whisper') {
66
- logger.start('No language specified. Detecting speech language')
67
- const { detectedLanguage } = await API.detectSpeechLanguage(sourceRawAudio, options.languageDetection!)
65
+ if (options.language) {
66
+ const languageData = await parseLangIdentifier(options.language)
68
67
 
69
- logger.end()
70
- logger.logTitledMessage('Language detected', formatLanguageCodeWithName(detectedLanguage))
68
+ options.language = languageData.Name
71
69
 
72
- options.language = detectedLanguage
73
- } else {
74
70
  logger.end()
71
+ logger.logTitledMessage('Language specified', formatLanguageCodeWithName(options.language))
72
+ } else {
73
+ logger.start('No language specified. Detecting speech language')
74
+ const { detectedLanguage } = await API.detectSpeechLanguage(sourceRawAudio, options.languageDetection!)
75
75
 
76
- const specifiedLanguageFormatted = formatLanguageCodeWithName(getShortLanguageCode(normalizeLanguageCode(options.language)))
76
+ options.language = detectedLanguage
77
77
 
78
- logger.logTitledMessage('Language specified', specifiedLanguageFormatted)
78
+ logger.end()
79
+ logger.logTitledMessage('Language detected', formatLanguageCodeWithName(detectedLanguage))
79
80
  }
80
81
 
81
- const languageCode = normalizeLanguageCode(options.language)
82
- const shortLanguageCode = getShortLanguageCode(languageCode)
82
+ const languageData = await parseLangIdentifier(options.language)
83
+
84
+ const languageCode = languageData.Name
85
+ const shortLanguageCode = languageData.TwoLetterISOLanguageName
83
86
 
84
87
  let transcript: string
85
88
  let timeline: Timeline | undefined
@@ -15,7 +15,7 @@ import { loadLexiconsForLanguage } from '../nlp/Lexicon.js'
15
15
  import * as API from './API.js'
16
16
  import { Timeline, TimelineEntry, addTimeOffsetToTimeline, multiplyTimelineByFactor } from '../utilities/Timeline.js'
17
17
  import { getAppDataDir, ensureDir, existsSync, isFileIsUpToDate, readAndParseJsonFile, writeFileSafe } from '../utilities/FileSystem.js'
18
- import { formatLanguageCodeWithName, getShortLanguageCode, normalizeLanguageCode, defaultDialectForLanguageCode } from '../utilities/Locale.js'
18
+ import { formatLanguageCodeWithName, getShortLanguageCode, normalizeLanguageCode, defaultDialectForLanguageCode, parseLangIdentifier, normalizeIdentifierToLangaugeCode } from '../utilities/Locale.js'
19
19
  import { loadPackage } from '../utilities/PackageManager.js'
20
20
  import { EngineMetadata, appName } from './Common.js'
21
21
  import { shouldCancelCurrentTask } from '../server/Worker.js'
@@ -328,7 +328,15 @@ async function synthesizeSegment(text: string, options: SynthesisOptions) {
328
328
  logger.start(`Initialize ${engine} module`)
329
329
 
330
330
  const voice = selectedVoice.name
331
- const language = options.language ? normalizeLanguageCode(options.language) : selectedVoice.languages[0]
331
+
332
+ let language: string
333
+
334
+ if (options.language) {
335
+ language = await normalizeIdentifierToLangaugeCode(options.language)
336
+ } else {
337
+ language = selectedVoice.languages[0]
338
+ }
339
+
332
340
  const voiceGender = selectedVoice.gender
333
341
 
334
342
  const speed = clip(options.speed!, 0.1, 10.0)
@@ -1548,7 +1556,7 @@ export async function requestVoiceList(options: VoiceListRequestOptions): Promis
1548
1556
  voiceList = await loadVoiceList()
1549
1557
  }
1550
1558
 
1551
- const languageCode = normalizeLanguageCode(options.language || '')
1559
+ const languageCode = await normalizeIdentifierToLangaugeCode(options.language || '')
1552
1560
 
1553
1561
  if (languageCode) {
1554
1562
  let filteredVoiceList = voiceList.filter(voice => voice.languages.includes(languageCode))
@@ -1611,7 +1619,7 @@ export interface RequestVoiceListResult {
1611
1619
  }
1612
1620
 
1613
1621
  export async function selectBestOfflineEngineForLanguage(language: string): Promise<SynthesisEngine> {
1614
- language = normalizeLanguageCode(language)
1622
+ language = await normalizeIdentifierToLangaugeCode(language)
1615
1623
 
1616
1624
  const VitsTTS = await import('../synthesis/VitsTTS.js')
1617
1625
 
@@ -1621,22 +1629,6 @@ export async function selectBestOfflineEngineForLanguage(language: string): Prom
1621
1629
  return 'vits'
1622
1630
  }
1623
1631
 
1624
- const FliteTTS = await import('../synthesis/FliteTTS.js')
1625
-
1626
- const fliteLanguages = getAllLangCodesFromVoiceList(FliteTTS.voiceList)
1627
-
1628
- if (fliteLanguages.includes(language)) {
1629
- return 'flite'
1630
- }
1631
-
1632
- const SvoxPicoTTS = await import('../synthesis/SvoxPicoTTS.js')
1633
-
1634
- const picoLanguages = getAllLangCodesFromVoiceList(SvoxPicoTTS.voiceList)
1635
-
1636
- if (picoLanguages.includes(language)) {
1637
- return 'pico'
1638
- }
1639
-
1640
1632
  return 'espeak'
1641
1633
  }
1642
1634
 
@@ -6,7 +6,7 @@ import { Logger } from '../utilities/Logger.js'
6
6
 
7
7
  import { Timeline, addWordTextOffsetsToTimeline, wordTimelineToSegmentSentenceTimeline } from '../utilities/Timeline.js'
8
8
  import { type WhisperOptions } from '../recognition/WhisperSTT.js'
9
- import { formatLanguageCodeWithName, getShortLanguageCode, normalizeLanguageCode } from '../utilities/Locale.js'
9
+ import { formatLanguageCodeWithName, getShortLanguageCode, normalizeIdentifierToLangaugeCode, parseLangIdentifier } from '../utilities/Locale.js'
10
10
  import { EngineMetadata } from './Common.js'
11
11
  import { type SpeechLanguageDetectionOptions, detectSpeechLanguage } from './API.js'
12
12
  import chalk from 'chalk'
@@ -62,28 +62,31 @@ export async function translateSpeech(input: AudioSourceParam, options: SpeechTr
62
62
  sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
63
63
  sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
64
64
 
65
- if (!options.sourceLanguage) {
66
- logger.start('No source language specified. Detecting speech language')
67
- const { detectedLanguage } = await detectSpeechLanguage(sourceRawAudio, options.languageDetection || {})
65
+ if (options.sourceLanguage) {
66
+ const languageData = await parseLangIdentifier(options.sourceLanguage)
68
67
 
69
- logger.end()
70
- logger.logTitledMessage('Source language detected', formatLanguageCodeWithName(detectedLanguage))
68
+ options.sourceLanguage = languageData.Name
71
69
 
72
- options.sourceLanguage = detectedLanguage
73
- } else {
74
70
  logger.end()
71
+ logger.logTitledMessage('Source language specified', formatLanguageCodeWithName(options.sourceLanguage))
72
+ } else {
73
+ logger.start('No source language specified. Detecting speech language')
74
+ const { detectedLanguage } = await detectSpeechLanguage(sourceRawAudio, options.languageDetection || {})
75
75
 
76
- const specifiedLanguageFormatted = formatLanguageCodeWithName(getShortLanguageCode(normalizeLanguageCode(options.sourceLanguage)))
76
+ options.sourceLanguage = detectedLanguage
77
77
 
78
- logger.logTitledMessage('Source language', specifiedLanguageFormatted)
78
+ logger.end()
79
+ logger.logTitledMessage('Source language detected', formatLanguageCodeWithName(detectedLanguage))
79
80
  }
80
81
 
81
- logger.logTitledMessage('Target language', formatLanguageCodeWithName(getShortLanguageCode(normalizeLanguageCode(options.targetLanguage!))))
82
+ options.targetLanguage = await normalizeIdentifierToLangaugeCode(options.targetLanguage!)
83
+
84
+ logger.logTitledMessage('Target language', formatLanguageCodeWithName(options.targetLanguage))
82
85
 
83
86
  logger.start('Preprocess audio for translation')
84
87
 
85
88
  const engine = options.engine!
86
- const sourceLanguage = normalizeLanguageCode(options.sourceLanguage!)
89
+ const sourceLanguage = options.sourceLanguage!
87
90
  const targetLanguage = options.targetLanguage!
88
91
 
89
92
  let transcript: string
@@ -119,10 +122,6 @@ export async function translateSpeech(input: AudioSourceParam, options: SpeechTr
119
122
 
120
123
  ({ transcript, timeline: wordTimeline } = await WhisperSTT.recognize(sourceRawAudio, modelName, modelDir, 'translate', sourceLanguage, whisperOptions))
121
124
 
122
- addWordTextOffsetsToTimeline(wordTimeline, transcript);
123
-
124
- ({ segmentTimeline } = await wordTimelineToSegmentSentenceTimeline(wordTimeline, transcript, targetLanguage, 'single', 'preserve'))
125
-
126
125
  break
127
126
  }
128
127
 
@@ -155,9 +154,7 @@ export async function translateSpeech(input: AudioSourceParam, options: SpeechTr
155
154
  modelName,
156
155
  modelPath,
157
156
  whisperCppOptions,
158
- ));
159
-
160
- ({ segmentTimeline } = await wordTimelineToSegmentSentenceTimeline(wordTimeline, transcript, targetLanguage, 'single', 'preserve'))
157
+ ))
161
158
 
162
159
  break
163
160
  }
@@ -194,13 +191,21 @@ export async function translateSpeech(input: AudioSourceParam, options: SpeechTr
194
191
 
195
192
  // If the audio was cropped before recognition, map the timestamps back to the original audio
196
193
  if (sourceUncropTimeline && sourceUncropTimeline.length > 0) {
197
- API.convertCroppedToUncroppedTimeline(segmentTimeline, sourceUncropTimeline)
198
-
199
194
  if (wordTimeline) {
200
195
  API.convertCroppedToUncroppedTimeline(wordTimeline, sourceUncropTimeline)
196
+ } else if (segmentTimeline) {
197
+ API.convertCroppedToUncroppedTimeline(segmentTimeline, sourceUncropTimeline)
201
198
  }
202
199
  }
203
200
 
201
+ if (wordTimeline) {
202
+ addWordTextOffsetsToTimeline(wordTimeline, transcript)
203
+ }
204
+
205
+ if (!segmentTimeline) {
206
+ ({ segmentTimeline } = await wordTimelineToSegmentSentenceTimeline(wordTimeline!, transcript, targetLanguage, 'single', 'preserve'))
207
+ }
208
+
204
209
  logger.log('')
205
210
  logger.logDuration(`Total speech translation time`, startTimestamp, chalk.magentaBright)
206
211
 
@@ -6,7 +6,7 @@ import { Logger } from '../utilities/Logger.js'
6
6
 
7
7
  import * as API from './API.js'
8
8
  import { Timeline, addWordTextOffsetsToTimeline, wordTimelineToSegmentSentenceTimeline } from '../utilities/Timeline.js'
9
- import { formatLanguageCodeWithName, getShortLanguageCode, normalizeLanguageCode } from '../utilities/Locale.js'
9
+ import { formatLanguageCodeWithName, getShortLanguageCode, normalizeIdentifierToLangaugeCode, parseLangIdentifier } from '../utilities/Locale.js'
10
10
  import { type WhisperAlignmentOptions } from '../recognition/WhisperSTT.js'
11
11
  import chalk from 'chalk'
12
12
  import { type SubtitlesConfig } from '../subtitles/Subtitles.js'
@@ -57,18 +57,25 @@ export async function alignTranslation(input: AudioSourceParam, transcript: stri
57
57
  let sourceLanguage: string
58
58
 
59
59
  if (options.sourceLanguage) {
60
- sourceLanguage = normalizeLanguageCode(options.sourceLanguage!)
60
+ const languageData = await parseLangIdentifier(options.sourceLanguage)
61
+
62
+ sourceLanguage = languageData.Name
63
+
64
+ logger.end()
65
+ logger.logTitledMessage('Source language specified', formatLanguageCodeWithName(sourceLanguage))
61
66
  } else {
62
67
  logger.start('No source language specified. Detecting speech language')
63
68
  const { detectedLanguage } = await API.detectSpeechLanguage(sourceRawAudio, options.languageDetection || {})
64
69
 
70
+ sourceLanguage = detectedLanguage
71
+
65
72
  logger.end()
66
73
  logger.logTitledMessage('Source language detected', formatLanguageCodeWithName(detectedLanguage))
67
-
68
- sourceLanguage = detectedLanguage
69
74
  }
70
75
 
71
- const targetLanguage = normalizeLanguageCode(options.targetLanguage!)
76
+ const targetLanguage = await normalizeIdentifierToLangaugeCode(options.targetLanguage!)
77
+
78
+ logger.logTitledMessage('Target language', formatLanguageCodeWithName(targetLanguage))
72
79
 
73
80
  let mappedTimeline: Timeline
74
81
 
@@ -1,6 +1,6 @@
1
1
  import { extendDeep } from '../utilities/ObjectUtilities.js'
2
2
 
3
- import { logToStderr } from '../utilities/Utilities.js'
3
+ import { logToStderr, roundToDigits } from '../utilities/Utilities.js'
4
4
  import { AudioSourceParam, RawAudio, cropToTimeline, ensureRawAudio, } from '../audio/AudioUtilities.js'
5
5
  import { Logger } from '../utilities/Logger.js'
6
6
 
@@ -191,35 +191,69 @@ function frameProbabilitiesToTimeline(frameProbabilities: number[], frameDuratio
191
191
  }
192
192
 
193
193
  export function convertCroppedToUncroppedTimeline(timeline: Timeline, uncropTimeline: Timeline) {
194
+ if (timeline.length === 0) {
195
+ return
196
+ }
197
+
194
198
  for (const entry of timeline) {
195
- entry.startTime = mapTimestampUsingUncropTimeline(entry.startTime, uncropTimeline)
196
- entry.endTime = mapTimestampUsingUncropTimeline(entry.endTime, uncropTimeline)
199
+ const {
200
+ mappedStartTime,
201
+ mappedEndTime
202
+ } = mapUsingUncropTimeline(entry.startTime, entry.endTime, uncropTimeline)
203
+
204
+ const mapSubTimeline = (subTimeline: Timeline | undefined) => {
205
+ if (!subTimeline) {
206
+ return
207
+ }
208
+
209
+ for (const subEntry of subTimeline) {
210
+ subEntry.startTime = Math.min(mappedStartTime + (subEntry.startTime - entry.startTime), mappedEndTime)
211
+ subEntry.endTime = Math.min(mappedStartTime + (subEntry.endTime - entry.startTime), mappedEndTime)
197
212
 
198
- if (entry.timeline) {
199
- convertCroppedToUncroppedTimeline(entry.timeline, uncropTimeline)
213
+ mapSubTimeline(subEntry.timeline)
214
+ }
200
215
  }
216
+
217
+ mapSubTimeline(entry.timeline)
218
+
219
+ entry.startTime = mappedStartTime
220
+ entry.endTime = mappedEndTime
201
221
  }
202
222
  }
203
223
 
204
- export function mapTimestampUsingUncropTimeline(timeInCroppedAudio: number, uncropTimeline: Timeline) {
224
+ function mapUsingUncropTimeline(startTimeInCroppedAudio: number, endTimeInCroppedAudio: number, uncropTimeline: Timeline) {
205
225
  let offsetInCroppedAudio = 0
206
226
 
207
- for (let i = 0; i < uncropTimeline.length; i++) {
208
- const entry = uncropTimeline[i]
227
+ let bestOverlapDuration = -1
228
+ let mappedStartTime = -1
229
+ let mappedEndTime = -1
209
230
 
210
- const entryDuration = entry.endTime - entry.startTime
231
+ for (const uncropEntry of uncropTimeline) {
232
+ const uncropEntryDuration = uncropEntry.endTime - uncropEntry.startTime
211
233
 
212
- const endOffset = offsetInCroppedAudio + entryDuration
234
+ const overlapStartTime = Math.max(startTimeInCroppedAudio, offsetInCroppedAudio)
235
+ const overlapEndTime = Math.min(endTimeInCroppedAudio, offsetInCroppedAudio + uncropEntryDuration)
213
236
 
214
- if ((i === uncropTimeline.length - 1) ||
215
- (timeInCroppedAudio >= offsetInCroppedAudio && timeInCroppedAudio < endOffset)) {
216
- return entry.startTime + (timeInCroppedAudio - offsetInCroppedAudio)
237
+ const overlapDuration = overlapEndTime - overlapStartTime
238
+
239
+ if (overlapDuration >= 0 && overlapDuration > bestOverlapDuration) {
240
+ bestOverlapDuration = overlapDuration
241
+
242
+ mappedStartTime = uncropEntry.startTime + (overlapStartTime - offsetInCroppedAudio)
243
+ mappedEndTime = uncropEntry.startTime + (overlapEndTime - offsetInCroppedAudio)
217
244
  }
218
245
 
219
- offsetInCroppedAudio += entryDuration
246
+ offsetInCroppedAudio += uncropEntryDuration
247
+ }
248
+
249
+ if (bestOverlapDuration === -1) {
250
+ throw new Error(`No match found in uncrop timeline (should not occur)`)
220
251
  }
221
252
 
222
- throw new Error(`Should not be reached`)
253
+ return {
254
+ mappedStartTime,
255
+ mappedEndTime
256
+ }
223
257
  }
224
258
 
225
259
  export interface VADResult {
package/src/cli/CLI.ts CHANGED
@@ -102,7 +102,11 @@ export async function start(processArgs: string[]) {
102
102
  process.exit(1)
103
103
  }
104
104
 
105
- const { operationArgs, parsedArgumentsLookup } = parseCLIArguments(processArgs.slice(1))
105
+ const { operationArgs, parsedArgumentsLookup } = parseCLIArguments(processArgs.slice(1))
106
+
107
+ const globalOptionsLookup = new Map<string, string>()
108
+ const cliOptionsLookup = new Map<string, string>()
109
+ const operationsOptionsLookup = new Map<string, string>()
106
110
 
107
111
  if (!parsedArgumentsLookup.has('config')) {
108
112
  const defaultConfigFile = `./${appName}.config`
@@ -135,10 +139,6 @@ export async function start(processArgs: string[]) {
135
139
  sectionName = 'speak'
136
140
  }
137
141
 
138
- const globalOptionsLookup = new Map<string, string>()
139
- const cliOptionsLookup = new Map<string, string>()
140
- const operationsOptionsLookup = new Map<string, string>()
141
-
142
142
  if (parsedConfigFile.has('global')) {
143
143
  for (const [key, value] of parsedConfigFile.get('global')!) {
144
144
  globalOptionsLookup.set(key, value)
@@ -156,27 +156,27 @@ export async function start(processArgs: string[]) {
156
156
  operationsOptionsLookup.set(key, value)
157
157
  }
158
158
  }
159
+ }
159
160
 
160
- const globalOptionsKeys = API.listGlobalOptions()
161
- const cliOptionsKeys = CLIOptionsKeys
161
+ const globalOptionsKeys = API.listGlobalOptions()
162
+ const cliOptionsKeys = CLIOptionsKeys
162
163
 
163
- for (const [key, value] of parsedArgumentsLookup) {
164
- if (globalOptionsKeys.includes(key)) {
165
- globalOptionsLookup.set(key, value)
166
- } else if (cliOptionsKeys.includes(key as any)) {
167
- cliOptionsLookup.set(key, value)
168
- } else {
169
- operationsOptionsLookup.set(key, value)
170
- }
164
+ for (const [key, value] of parsedArgumentsLookup) {
165
+ if (globalOptionsKeys.includes(key)) {
166
+ globalOptionsLookup.set(key, value)
167
+ } else if (cliOptionsKeys.includes(key as any)) {
168
+ cliOptionsLookup.set(key, value)
169
+ } else {
170
+ operationsOptionsLookup.set(key, value)
171
171
  }
172
+ }
172
173
 
173
- operationData.operation = operation
174
- operationData.operationArgs = operationArgs
174
+ operationData.operation = operation
175
+ operationData.operationArgs = operationArgs
175
176
 
176
- operationData.globalOptions = await optionsLookupToTypedObject(globalOptionsLookup, 'GlobalOptions')
177
- operationData.cliOptions = await optionsLookupToTypedObject(cliOptionsLookup, 'CLIOptions')
178
- operationData.operationOptionsLookup = operationsOptionsLookup
179
- }
177
+ operationData.globalOptions = await optionsLookupToTypedObject(globalOptionsLookup, 'GlobalOptions')
178
+ operationData.cliOptions = await optionsLookupToTypedObject(cliOptionsLookup, 'CLIOptions')
179
+ operationData.operationOptionsLookup = operationsOptionsLookup
180
180
  } catch (e: any) {
181
181
  resetActiveLogger()
182
182
 
@@ -997,7 +997,7 @@ async function detectVoiceActivity(operationData: CLIOperationData) {
997
997
  const normalizedAudio = normalizeAudioLevel(inputRawAudio)
998
998
 
999
999
  const timelineToPlay = verboseTimeline.map(entry => {
1000
- return {...entry, type: 'word' } as TimelineEntry
1000
+ return { ...entry, type: 'word' } as TimelineEntry
1001
1001
  })
1002
1002
 
1003
1003
  await playAudioWithWordTimeline(normalizedAudio, timelineToPlay)
@@ -13,7 +13,7 @@ import { getRawAudioDuration, RawAudio, sliceRawAudio } from '../audio/AudioUtil
13
13
  import { readFile } from '../utilities/FileSystem.js'
14
14
  import path from 'path'
15
15
  import type { LanguageDetectionResults } from '../api/API.js'
16
- import { getShortLanguageCode, languageCodeToName } from '../utilities/Locale.js'
16
+ import { formatLanguageCodeWithName, getShortLanguageCode, languageCodeToName } from '../utilities/Locale.js'
17
17
  import { loadPackage } from '../utilities/PackageManager.js'
18
18
  import chalk from 'chalk'
19
19
  import { XorShift32RNG } from '../utilities/RandomGenerator.js'
@@ -35,13 +35,13 @@ export async function recognize(
35
35
  options = extendDeep(defaultWhisperOptions, options)
36
36
 
37
37
  if (sourceRawAudio.sampleRate != 16000) {
38
- throw new Error('Source audio must have a sampling rate of 16000')
38
+ throw new Error('Source audio must have a sampling rate of 16000 Hz')
39
39
  }
40
40
 
41
41
  sourceLanguage = getShortLanguageCode(sourceLanguage)
42
42
 
43
43
  if (!(sourceLanguage in languageIdLookup)) {
44
- throw new Error(`The language ${languageCodeToName(sourceLanguage)} is not supported by the Whisper engine.`)
44
+ throw new Error(`The language ${formatLanguageCodeWithName(sourceLanguage)} is not supported by the Whisper engine.`)
45
45
  }
46
46
 
47
47
  if (isEnglishOnlyModel(modelName) && sourceLanguage != 'en') {
@@ -93,7 +93,7 @@ export async function align(
93
93
  sourceLanguage = getShortLanguageCode(sourceLanguage)
94
94
 
95
95
  if (!(sourceLanguage in languageIdLookup)) {
96
- throw new Error(`The language ${languageCodeToName(sourceLanguage)} is not supported by the Whisper engine.`)
96
+ throw new Error(`The language ${formatLanguageCodeWithName(sourceLanguage)} is not supported by the Whisper engine.`)
97
97
  }
98
98
 
99
99
  if (isEnglishOnlyModel(modelName) && sourceLanguage != 'en') {
@@ -134,7 +134,7 @@ export async function alignEnglishTranslation(
134
134
  sourceLanguage = getShortLanguageCode(sourceLanguage)
135
135
 
136
136
  if (!(sourceLanguage in languageIdLookup)) {
137
- throw new Error(`The source language ${languageCodeToName(sourceLanguage)} is not supported by the Whisper engine.`)
137
+ throw new Error(`The source language ${formatLanguageCodeWithName(sourceLanguage)} is not supported by the Whisper engine.`)
138
138
  }
139
139
 
140
140
  if (isEnglishOnlyModel(modelName)) {
@@ -784,6 +784,8 @@ export class Whisper {
784
784
  return false
785
785
  }
786
786
 
787
+ // If this is the first token in the part, unconditionally decode a timestamp token
788
+ // for time 0.0
787
789
  if (isInitialState) {
788
790
  addToken(timestampTokensStart, timestampTokenLogits, 1.0, crossAttentionQKsForToken)
789
791
 
@@ -22,6 +22,39 @@ export function formatLanguageCodeWithName(languageCode: string, styleId: 1 | 2
22
22
  }
23
23
  }
24
24
 
25
+ export async function normalizeIdentifierToLangaugeCode(langIdentifier: string) {
26
+ const result = await parseLangIdentifier(langIdentifier)
27
+
28
+ return result.Name
29
+ }
30
+
31
+ export async function normalizeIdentifierToShortLanguageCode(langIdentifier: string) {
32
+ const result = await parseLangIdentifier(langIdentifier)
33
+
34
+ return result.TwoLetterISOLanguageName
35
+ }
36
+
37
+ export async function parseLangIdentifier(langIdentifier: string) {
38
+ if (!langIdentifier) {
39
+ return emptyLangInfoEntry
40
+ }
41
+
42
+ await loadLangInfoEntriesIfNeeded()
43
+
44
+ langIdentifier = langIdentifier.trim().toLowerCase()
45
+
46
+ for (const entry of langInfoEntries) {
47
+ if (langIdentifier === entry.NameLowerCase ||
48
+ langIdentifier === entry.ThreeLetterISOLanguageName ||
49
+ langIdentifier === entry.EnglishNameLowerCase) {
50
+
51
+ return entry
52
+ }
53
+ }
54
+
55
+ throw new Error(`Couldn't parse language identifier '${langIdentifier}'.`)
56
+ }
57
+
25
58
  export function getShortLanguageCode(langCode: string) {
26
59
  const dashIndex = langCode.indexOf('-')
27
60
 
@@ -48,7 +81,7 @@ export function normalizeLanguageCode(langCode: string) {
48
81
 
49
82
  const isoToLcidLookup = new Map<string, number>()
50
83
  const lcidToIsoLookup = new Map<number, string[]>()
51
- const lcidEntries: LCIDEntry[] = []
84
+ let langInfoEntries: LangInfoEntry[] = []
52
85
 
53
86
  export async function isoToLcidLanguageCode(iso: string) {
54
87
  await loadLcidLookupIfNeeded()
@@ -63,19 +96,13 @@ export async function lcidToIsoLanguageCode(lcid: number) {
63
96
  }
64
97
 
65
98
  async function loadLcidLookupIfNeeded() {
66
- if (lcidEntries.length > 0) {
67
- return lcidEntries
68
- }
69
-
70
- const lcidLookup: LCIDLookup = await readAndParseJsonFile(resolveToModuleRootDir('data/tables/lcid-table.json'))
71
-
72
- for (const isoName in lcidLookup) {
73
- const lcidEntry = lcidLookup[isoName]
74
- lcidEntries.push(lcidEntry)
99
+ await loadLangInfoEntriesIfNeeded()
75
100
 
101
+ for (const lcidEntry of langInfoEntries) {
102
+ const name = lcidEntry.Name
76
103
  const lcidValue = lcidEntry.LCID
77
104
 
78
- isoToLcidLookup.set(isoName, lcidValue)
105
+ isoToLcidLookup.set(name, lcidValue)
79
106
 
80
107
  let entry = lcidToIsoLookup.get(lcidValue)
81
108
 
@@ -84,10 +111,25 @@ async function loadLcidLookupIfNeeded() {
84
111
  lcidToIsoLookup.set(lcidValue, entry)
85
112
  }
86
113
 
87
- entry.push(isoName)
114
+ entry.push(name)
88
115
  }
89
116
 
90
- return lcidEntries
117
+ return langInfoEntries
118
+ }
119
+
120
+ async function loadLangInfoEntriesIfNeeded() {
121
+ if (langInfoEntries.length > 0) {
122
+ return
123
+ }
124
+
125
+ const entries = await readAndParseJsonFile(resolveToModuleRootDir('data/tables/lcid-table.json')) as LangInfoEntry[]
126
+
127
+ for (const entry of entries) {
128
+ entry.NameLowerCase = entry.Name.toLowerCase()
129
+ entry.EnglishNameLowerCase = entry.EnglishName.toLowerCase()
130
+
131
+ langInfoEntries.push(entry)
132
+ }
91
133
  }
92
134
 
93
135
  export function getDefaultDialectForLanguageCodeIfPossible(langCode: string) {
@@ -107,14 +149,34 @@ export const defaultDialectForLanguageCode: { [lang: string]: string } = {
107
149
  'nl': 'nl-NL'
108
150
  }
109
151
 
110
- type LCIDLookup = { [isoLangCode: string]: LCIDEntry }
152
+ export interface LangInfoEntry {
153
+ LCID: number
154
+
155
+ Name: string
156
+ NameLowerCase: string
157
+
158
+ TwoLetterISOLanguageName: string
159
+ ThreeLetterISOLanguageName: string
160
+ ThreeLetterWindowsLanguageName: string
161
+
162
+ EnglishName: string
163
+ EnglishNameLowerCase: string
164
+
165
+ ANSICodePage: string
166
+ }
167
+
168
+ export const emptyLangInfoEntry: LangInfoEntry = {
169
+ LCID: -1,
170
+
171
+ Name: '',
172
+ NameLowerCase: '',
173
+
174
+ TwoLetterISOLanguageName: '',
175
+ ThreeLetterISOLanguageName: '',
176
+ ThreeLetterWindowsLanguageName: '',
177
+
178
+ EnglishName: 'Empty',
179
+ EnglishNameLowerCase: 'empty',
111
180
 
112
- export interface LCIDEntry {
113
- 'LCID': number
114
- 'Name': string
115
- 'TwoLetterISOLanguageName': string,
116
- 'ThreeLetterISOLanguageName': string,
117
- 'ThreeLetterWindowsLanguageName': string,
118
- 'EnglishName': string
119
- 'ANSICodePage': string
181
+ ANSICodePage: ''
120
182
  }