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.
- package/data/tables/lcid-table.json +7427 -7427
- package/dist/api/Alignment.js +5 -3
- package/dist/api/Alignment.js.map +1 -1
- package/dist/api/Recognition.js +12 -10
- package/dist/api/Recognition.js.map +1 -1
- package/dist/api/Synthesis.js +10 -14
- package/dist/api/Synthesis.js.map +1 -1
- package/dist/api/Translation.js +21 -14
- package/dist/api/Translation.js.map +1 -1
- package/dist/api/TranslationAlignment.js +8 -4
- package/dist/api/TranslationAlignment.js.map +1 -1
- package/dist/api/VoiceActivityDetection.d.ts +0 -1
- package/dist/api/VoiceActivityDetection.js +38 -15
- package/dist/api/VoiceActivityDetection.js.map +1 -1
- package/dist/cli/CLI.js +20 -20
- package/dist/cli/CLI.js.map +1 -1
- package/dist/recognition/WhisperSTT.js +7 -5
- package/dist/recognition/WhisperSTT.js.map +1 -1
- package/dist/utilities/Locale.d.ts +14 -8
- package/dist/utilities/Locale.js +52 -11
- package/dist/utilities/Locale.js.map +1 -1
- package/docs/CLI.md +5 -5
- package/docs/Options.md +1 -1
- package/docs/Tasklist.md +11 -6
- package/package.json +8 -8
- package/src/api/Alignment.ts +9 -3
- package/src/api/Recognition.ts +15 -12
- package/src/api/Synthesis.ts +12 -20
- package/src/api/Translation.ts +26 -21
- package/src/api/TranslationAlignment.ts +12 -5
- package/src/api/VoiceActivityDetection.ts +49 -15
- package/src/cli/CLI.ts +22 -22
- package/src/recognition/WhisperSTT.ts +7 -5
- package/src/utilities/Locale.ts +84 -22
package/src/api/Alignment.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
87
|
+
|
|
88
|
+
logger.logTitledMessage('Language detected', formatLanguageCodeWithName(language))
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
language = getDefaultDialectForLanguageCodeIfPossible(language)
|
package/src/api/Recognition.ts
CHANGED
|
@@ -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,
|
|
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 (
|
|
66
|
-
|
|
67
|
-
const { detectedLanguage } = await API.detectSpeechLanguage(sourceRawAudio, options.languageDetection!)
|
|
65
|
+
if (options.language) {
|
|
66
|
+
const languageData = await parseLangIdentifier(options.language)
|
|
68
67
|
|
|
69
|
-
|
|
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
|
-
|
|
76
|
+
options.language = detectedLanguage
|
|
77
77
|
|
|
78
|
-
logger.
|
|
78
|
+
logger.end()
|
|
79
|
+
logger.logTitledMessage('Language detected', formatLanguageCodeWithName(detectedLanguage))
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
const
|
|
82
|
-
|
|
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
|
package/src/api/Synthesis.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
package/src/api/Translation.ts
CHANGED
|
@@ -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,
|
|
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 (
|
|
66
|
-
|
|
67
|
-
const { detectedLanguage } = await detectSpeechLanguage(sourceRawAudio, options.languageDetection || {})
|
|
65
|
+
if (options.sourceLanguage) {
|
|
66
|
+
const languageData = await parseLangIdentifier(options.sourceLanguage)
|
|
68
67
|
|
|
69
|
-
|
|
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
|
-
|
|
76
|
+
options.sourceLanguage = detectedLanguage
|
|
77
77
|
|
|
78
|
-
logger.
|
|
78
|
+
logger.end()
|
|
79
|
+
logger.logTitledMessage('Source language detected', formatLanguageCodeWithName(detectedLanguage))
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
224
|
+
function mapUsingUncropTimeline(startTimeInCroppedAudio: number, endTimeInCroppedAudio: number, uncropTimeline: Timeline) {
|
|
205
225
|
let offsetInCroppedAudio = 0
|
|
206
226
|
|
|
207
|
-
|
|
208
|
-
|
|
227
|
+
let bestOverlapDuration = -1
|
|
228
|
+
let mappedStartTime = -1
|
|
229
|
+
let mappedEndTime = -1
|
|
209
230
|
|
|
210
|
-
|
|
231
|
+
for (const uncropEntry of uncropTimeline) {
|
|
232
|
+
const uncropEntryDuration = uncropEntry.endTime - uncropEntry.startTime
|
|
211
233
|
|
|
212
|
-
const
|
|
234
|
+
const overlapStartTime = Math.max(startTimeInCroppedAudio, offsetInCroppedAudio)
|
|
235
|
+
const overlapEndTime = Math.min(endTimeInCroppedAudio, offsetInCroppedAudio + uncropEntryDuration)
|
|
213
236
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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 +=
|
|
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
|
-
|
|
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 }
|
|
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
|
-
|
|
161
|
-
|
|
161
|
+
const globalOptionsKeys = API.listGlobalOptions()
|
|
162
|
+
const cliOptionsKeys = CLIOptionsKeys
|
|
162
163
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
174
|
-
|
|
174
|
+
operationData.operation = operation
|
|
175
|
+
operationData.operationArgs = operationArgs
|
|
175
176
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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
|
|
package/src/utilities/Locale.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
114
|
+
entry.push(name)
|
|
88
115
|
}
|
|
89
116
|
|
|
90
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|