echogarden 1.2.1 → 1.3.1
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/alignment/TextAlignment.d.ts +16 -0
- package/dist/alignment/TextAlignment.js +63 -0
- package/dist/alignment/TextAlignment.js.map +1 -0
- package/dist/api/Alignment.js +11 -4
- package/dist/api/Alignment.js.map +1 -1
- package/dist/api/Denoising.js +0 -1
- package/dist/api/Denoising.js.map +1 -1
- package/dist/api/LanguageDetection.js +2 -2
- package/dist/api/LanguageDetection.js.map +1 -1
- package/dist/api/Recognition.js +15 -11
- package/dist/api/Recognition.js.map +1 -1
- package/dist/api/Synthesis.js +11 -15
- package/dist/api/Synthesis.js.map +1 -1
- package/dist/api/Translation.js +24 -15
- package/dist/api/Translation.js.map +1 -1
- package/dist/api/TranslationAlignment.js +12 -5
- package/dist/api/TranslationAlignment.js.map +1 -1
- package/dist/api/VoiceActivityDetection.d.ts +0 -4
- package/dist/api/VoiceActivityDetection.js +43 -20
- package/dist/api/VoiceActivityDetection.js.map +1 -1
- package/dist/audio/AudioUtilities.d.ts +1 -1
- package/dist/audio/AudioUtilities.js +2 -2
- package/dist/audio/AudioUtilities.js.map +1 -1
- package/dist/cli/CLILauncher.js +1 -2
- package/dist/cli/CLILauncher.js.map +1 -1
- package/dist/codecs/FFMpegTranscoder.js +4 -1
- package/dist/codecs/FFMpegTranscoder.js.map +1 -1
- package/dist/codecs/WaveCodec.d.ts +1 -1
- package/dist/codecs/WaveCodec.js +13 -5
- package/dist/codecs/WaveCodec.js.map +1 -1
- package/dist/dsp/SpeexResampler.js +49 -21
- package/dist/dsp/SpeexResampler.js.map +1 -1
- package/dist/recognition/WhisperSTT.js +4 -4
- package/dist/recognition/WhisperSTT.js.map +1 -1
- package/dist/synthesis/CoquiServerTTS.js +1 -1
- package/dist/synthesis/CoquiServerTTS.js.map +1 -1
- package/dist/synthesis/VitsTTS.js +1 -1
- package/dist/synthesis/VitsTTS.js.map +1 -1
- package/dist/text-translation/NLLBTextTranslation.d.ts +1 -0
- package/dist/text-translation/NLLBTextTranslation.js +237 -0
- package/dist/text-translation/NLLBTextTranslation.js.map +1 -0
- package/dist/utilities/Locale.d.ts +14 -8
- package/dist/utilities/Locale.js +52 -11
- package/dist/utilities/Locale.js.map +1 -1
- package/dist/utilities/PackageManager.js +2 -0
- package/dist/utilities/PackageManager.js.map +1 -1
- package/docs/Development.md +1 -1
- package/docs/Server.md +1 -1
- package/docs/Tasklist.md +11 -6
- package/package.json +12 -11
- package/src/alignment/TextAlignment.ts +96 -0
- package/src/api/Alignment.ts +19 -4
- package/src/api/Denoising.ts +0 -2
- package/src/api/LanguageDetection.ts +2 -3
- package/src/api/Recognition.ts +18 -13
- package/src/api/Synthesis.ts +13 -21
- package/src/api/Translation.ts +29 -22
- package/src/api/TranslationAlignment.ts +17 -6
- package/src/api/VoiceActivityDetection.ts +56 -22
- package/src/audio/AudioUtilities.ts +2 -2
- package/src/cli/CLILauncher.ts +1 -2
- package/src/codecs/FFMpegTranscoder.ts +5 -1
- package/src/codecs/WaveCodec.ts +15 -5
- package/src/dsp/SpeexResampler.ts +62 -23
- package/src/recognition/WhisperSTT.ts +4 -4
- package/src/synthesis/CoquiServerTTS.ts +1 -1
- package/src/synthesis/VitsTTS.ts +2 -2
- package/src/text-translation/NLLBTextTranslation.ts +252 -0
- package/src/utilities/Locale.ts +84 -22
- package/src/utilities/PackageManager.ts +3 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type PreTrainedModel, type PreTrainedTokenizer } from '@echogarden/transformers-nodejs-lite'
|
|
2
|
+
import { Logger } from '../utilities/Logger.js'
|
|
3
|
+
import { loadPackage } from '../utilities/PackageManager.js'
|
|
4
|
+
|
|
5
|
+
export async function alignText(text1: string, text2: string) {
|
|
6
|
+
const logger = new Logger()
|
|
7
|
+
|
|
8
|
+
text1 = text1.replaceAll(/(\r?\n)+/g, ' ')
|
|
9
|
+
text2 = text2.replaceAll(/(\r?\n)+/g, ' ')
|
|
10
|
+
|
|
11
|
+
const modelPath = await loadPackage(`xenova-multilingual-e5-small-quantized`)
|
|
12
|
+
|
|
13
|
+
const embeddingModel = new E5TextEmbedding(modelPath)
|
|
14
|
+
|
|
15
|
+
logger.start(`Initialize E5 embedding model`)
|
|
16
|
+
await embeddingModel.initializeIfNeeded()
|
|
17
|
+
|
|
18
|
+
logger.start(`Tokenize text 1`)
|
|
19
|
+
const inputs1 = await embeddingModel.tokenizeToModelInputs(text1)
|
|
20
|
+
|
|
21
|
+
logger.start(`Infer embeddings for text 1`)
|
|
22
|
+
const embeddings1 = await embeddingModel.inferTokenEmbeddings(inputs1)
|
|
23
|
+
|
|
24
|
+
logger.start(`Tokenize text 2`)
|
|
25
|
+
const inputs2 = await embeddingModel.tokenizeToModelInputs(text2)
|
|
26
|
+
|
|
27
|
+
logger.start(`Infer embeddings for text 2`)
|
|
28
|
+
const embeddings2 = await embeddingModel.inferTokenEmbeddings(inputs1)
|
|
29
|
+
|
|
30
|
+
logger.end()
|
|
31
|
+
|
|
32
|
+
logger.log(embeddings1)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class E5TextEmbedding {
|
|
36
|
+
tokenizer?: PreTrainedTokenizer
|
|
37
|
+
model?: PreTrainedModel
|
|
38
|
+
|
|
39
|
+
constructor(public readonly modelPath: string) {
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async tokenizeToModelInputs(text: string) {
|
|
43
|
+
await this.initializeIfNeeded()
|
|
44
|
+
|
|
45
|
+
const inputs = await this.tokenizer!(text)
|
|
46
|
+
|
|
47
|
+
return inputs
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async inferTokenEmbeddings(inputs: any) {
|
|
51
|
+
await this.initializeIfNeeded()
|
|
52
|
+
|
|
53
|
+
const tokensText = this.tokenizer!.model.convert_ids_to_tokens(Array.from(inputs.input_ids.data))
|
|
54
|
+
|
|
55
|
+
const result = await this.model!(inputs)
|
|
56
|
+
|
|
57
|
+
const lastHiddenState = result.last_hidden_state
|
|
58
|
+
|
|
59
|
+
const tokenCount = lastHiddenState.dims[1]
|
|
60
|
+
const embeddingSize = lastHiddenState.dims[2]
|
|
61
|
+
|
|
62
|
+
const tokenEmbeddings: TokenEmbeddingData[] = []
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < tokenCount; i++) {
|
|
65
|
+
const tokenEmbeddingVector = lastHiddenState.data.slice(i * embeddingSize, (i + 1) * embeddingSize)
|
|
66
|
+
|
|
67
|
+
const tokenId = Number(inputs.input_ids.data[i])
|
|
68
|
+
const tokenText = tokensText[i]
|
|
69
|
+
|
|
70
|
+
tokenEmbeddings.push({
|
|
71
|
+
id: tokenId,
|
|
72
|
+
text: tokenText,
|
|
73
|
+
embeddingVector: tokenEmbeddingVector
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return tokenEmbeddings
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async initializeIfNeeded() {
|
|
81
|
+
if (this.tokenizer && this.model) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { AutoTokenizer, AutoModel } = await import('@echogarden/transformers-nodejs-lite')
|
|
86
|
+
|
|
87
|
+
this.tokenizer = await AutoTokenizer.from_pretrained(this.modelPath)
|
|
88
|
+
this.model = await AutoModel.from_pretrained(this.modelPath)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface TokenEmbeddingData {
|
|
93
|
+
id: number
|
|
94
|
+
text: string
|
|
95
|
+
embeddingVector: Float32Array
|
|
96
|
+
}
|
package/src/api/Alignment.ts
CHANGED
|
@@ -6,17 +6,22 @@ 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'
|
|
13
13
|
import { type SubtitlesConfig } from '../subtitles/Subtitles.js'
|
|
14
14
|
import { type EspeakOptions, defaultEspeakOptions } from '../synthesis/EspeakTTS.js'
|
|
15
15
|
import { isWord } from '../nlp/Segmentation.js'
|
|
16
|
+
import { alignText } from '../alignment/TextAlignment.js'
|
|
17
|
+
import { translateText } from '../text-translation/NLLBTextTranslation.js'
|
|
16
18
|
|
|
17
19
|
const log = logToStderr
|
|
18
20
|
|
|
19
21
|
export async function align(input: AudioSourceParam, transcript: string, options: AlignmentOptions): Promise<AlignmentResult> {
|
|
22
|
+
//await alignText(transcript, transcript)
|
|
23
|
+
//await translateText(transcript, 'en', 'de')
|
|
24
|
+
|
|
20
25
|
const logger = new Logger()
|
|
21
26
|
|
|
22
27
|
const startTimestamp = logger.getTimestamp()
|
|
@@ -38,8 +43,10 @@ export async function align(input: AudioSourceParam, transcript: string, options
|
|
|
38
43
|
logger.end()
|
|
39
44
|
logger.log(``)
|
|
40
45
|
|
|
46
|
+
logger.start(`Resample audio to 16kHz mono`)
|
|
41
47
|
sourceRawAudio = await ensureRawAudio(isolatedRawAudio, 16000, 1)
|
|
42
48
|
} else {
|
|
49
|
+
logger.start(`Resample audio to 16kHz mono`)
|
|
43
50
|
sourceRawAudio = await ensureRawAudio(inputRawAudio, 16000, 1)
|
|
44
51
|
}
|
|
45
52
|
|
|
@@ -52,7 +59,7 @@ export async function align(input: AudioSourceParam, transcript: string, options
|
|
|
52
59
|
logger.end()
|
|
53
60
|
}
|
|
54
61
|
|
|
55
|
-
logger.start('
|
|
62
|
+
logger.start('Normalize and trim audio')
|
|
56
63
|
|
|
57
64
|
sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
|
|
58
65
|
sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
|
|
@@ -69,17 +76,25 @@ export async function align(input: AudioSourceParam, transcript: string, options
|
|
|
69
76
|
}
|
|
70
77
|
}
|
|
71
78
|
|
|
79
|
+
logger.end()
|
|
80
|
+
|
|
72
81
|
let language: string
|
|
73
82
|
|
|
74
83
|
if (options.language) {
|
|
75
|
-
|
|
84
|
+
const languageData = await parseLangIdentifier(options.language)
|
|
85
|
+
|
|
86
|
+
language = languageData.Name
|
|
87
|
+
|
|
88
|
+
logger.logTitledMessage('Language specified', formatLanguageCodeWithName(language))
|
|
76
89
|
} else {
|
|
77
90
|
logger.start('No language specified. Detecting language')
|
|
78
91
|
const { detectedLanguage } = await API.detectTextLanguage(transcript, options.languageDetection || {})
|
|
92
|
+
|
|
79
93
|
language = detectedLanguage
|
|
80
94
|
|
|
81
95
|
logger.end()
|
|
82
|
-
|
|
96
|
+
|
|
97
|
+
logger.logTitledMessage('Language detected', formatLanguageCodeWithName(language))
|
|
83
98
|
}
|
|
84
99
|
|
|
85
100
|
language = getDefaultDialectForLanguageCodeIfPossible(language)
|
package/src/api/Denoising.ts
CHANGED
|
@@ -14,8 +14,6 @@ export async function denoise(input: AudioSourceParam, options: DenoisingOptions
|
|
|
14
14
|
const logger = new Logger()
|
|
15
15
|
const startTime = logger.getTimestamp()
|
|
16
16
|
|
|
17
|
-
logger.start('Prepare for denoising')
|
|
18
|
-
|
|
19
17
|
options = extendDeep(defaultDenoisingOptions, options)
|
|
20
18
|
|
|
21
19
|
const inputRawAudio = await ensureRawAudio(input)
|
|
@@ -28,6 +28,7 @@ export async function detectSpeechLanguage(input: AudioSourceParam, options: Spe
|
|
|
28
28
|
|
|
29
29
|
const inputRawAudio = await ensureRawAudio(input)
|
|
30
30
|
|
|
31
|
+
logger.start(`Resample audio to 16kHz mono`)
|
|
31
32
|
let sourceRawAudio = await ensureRawAudio(inputRawAudio, 16000, 1)
|
|
32
33
|
sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
|
|
33
34
|
sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
|
|
@@ -39,13 +40,11 @@ export async function detectSpeechLanguage(input: AudioSourceParam, options: Spe
|
|
|
39
40
|
logger.end()
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
logger.start(
|
|
43
|
+
logger.start(`Initialize ${options.engine} module`)
|
|
43
44
|
|
|
44
45
|
const defaultLanguage = options.defaultLanguage!
|
|
45
46
|
const fallbackThresholdProbability = options.fallbackThresholdProbability!
|
|
46
47
|
|
|
47
|
-
logger.start(`Initialize ${options.engine} module`)
|
|
48
|
-
|
|
49
48
|
let detectedLanguageProbabilities: LanguageDetectionResults
|
|
50
49
|
|
|
51
50
|
switch (options.engine) {
|
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
|
|
|
@@ -41,8 +41,10 @@ export async function recognize(input: AudioSourceParam, options: RecognitionOpt
|
|
|
41
41
|
logger.end()
|
|
42
42
|
logger.log(``)
|
|
43
43
|
|
|
44
|
+
logger.start(`Resample audio to 16kHz mono`)
|
|
44
45
|
sourceRawAudio = await ensureRawAudio(isolatedRawAudio, 16000, 1)
|
|
45
46
|
} else {
|
|
47
|
+
logger.start(`Resample audio to 16kHz mono`)
|
|
46
48
|
sourceRawAudio = await ensureRawAudio(inputRawAudio, 16000, 1)
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -55,31 +57,34 @@ export async function recognize(input: AudioSourceParam, options: RecognitionOpt
|
|
|
55
57
|
logger.end()
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
logger.start('
|
|
60
|
+
logger.start('Normalize and trim audio')
|
|
59
61
|
|
|
60
62
|
sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
|
|
61
63
|
sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
|
|
62
64
|
|
|
63
65
|
const engine = options.engine!
|
|
64
66
|
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
const { detectedLanguage } = await API.detectSpeechLanguage(sourceRawAudio, options.languageDetection!)
|
|
67
|
+
if (options.language) {
|
|
68
|
+
const languageData = await parseLangIdentifier(options.language)
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
logger.logTitledMessage('Language detected', formatLanguageCodeWithName(detectedLanguage))
|
|
70
|
+
options.language = languageData.Name
|
|
71
71
|
|
|
72
|
-
options.language = detectedLanguage
|
|
73
|
-
} else {
|
|
74
72
|
logger.end()
|
|
73
|
+
logger.logTitledMessage('Language specified', formatLanguageCodeWithName(options.language))
|
|
74
|
+
} else {
|
|
75
|
+
logger.start('No language specified. Detecting speech language')
|
|
76
|
+
const { detectedLanguage } = await API.detectSpeechLanguage(sourceRawAudio, options.languageDetection!)
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
options.language = detectedLanguage
|
|
77
79
|
|
|
78
|
-
logger.
|
|
80
|
+
logger.end()
|
|
81
|
+
logger.logTitledMessage('Language detected', formatLanguageCodeWithName(detectedLanguage))
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
const
|
|
82
|
-
|
|
84
|
+
const languageData = await parseLangIdentifier(options.language)
|
|
85
|
+
|
|
86
|
+
const languageCode = languageData.Name
|
|
87
|
+
const shortLanguageCode = languageData.TwoLetterISOLanguageName
|
|
83
88
|
|
|
84
89
|
let transcript: string
|
|
85
90
|
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'
|
|
@@ -301,7 +301,7 @@ async function synthesizeSegment(text: string, options: SynthesisOptions) {
|
|
|
301
301
|
|
|
302
302
|
const startTimestamp = logger.getTimestamp()
|
|
303
303
|
|
|
304
|
-
logger.start('Prepare for synthesis')
|
|
304
|
+
logger.start('Prepare text for synthesis')
|
|
305
305
|
|
|
306
306
|
const simplifiedText = simplifyPunctuationCharacters(text)
|
|
307
307
|
|
|
@@ -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'
|
|
@@ -43,8 +43,10 @@ export async function translateSpeech(input: AudioSourceParam, options: SpeechTr
|
|
|
43
43
|
logger.end()
|
|
44
44
|
logger.log(``)
|
|
45
45
|
|
|
46
|
+
logger.start(`Resample audio to 16kHz mono`)
|
|
46
47
|
sourceRawAudio = await ensureRawAudio(isolatedRawAudio, 16000, 1)
|
|
47
48
|
} else {
|
|
49
|
+
logger.start(`Resample audio to 16kHz mono`)
|
|
48
50
|
sourceRawAudio = await ensureRawAudio(inputRawAudio, 16000, 1)
|
|
49
51
|
}
|
|
50
52
|
|
|
@@ -57,33 +59,36 @@ export async function translateSpeech(input: AudioSourceParam, options: SpeechTr
|
|
|
57
59
|
logger.end()
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
logger.start('
|
|
62
|
+
logger.start('Normalize and trim audio')
|
|
61
63
|
|
|
62
64
|
sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
|
|
63
65
|
sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
|
|
64
66
|
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
const { detectedLanguage } = await detectSpeechLanguage(sourceRawAudio, options.languageDetection || {})
|
|
67
|
+
if (options.sourceLanguage) {
|
|
68
|
+
const languageData = await parseLangIdentifier(options.sourceLanguage)
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
logger.logTitledMessage('Source language detected', formatLanguageCodeWithName(detectedLanguage))
|
|
70
|
+
options.sourceLanguage = languageData.Name
|
|
71
71
|
|
|
72
|
-
options.sourceLanguage = detectedLanguage
|
|
73
|
-
} else {
|
|
74
72
|
logger.end()
|
|
73
|
+
logger.logTitledMessage('Source language specified', formatLanguageCodeWithName(options.sourceLanguage))
|
|
74
|
+
} else {
|
|
75
|
+
logger.start('No source language specified. Detecting speech language')
|
|
76
|
+
const { detectedLanguage } = await detectSpeechLanguage(sourceRawAudio, options.languageDetection || {})
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
options.sourceLanguage = detectedLanguage
|
|
77
79
|
|
|
78
|
-
logger.
|
|
80
|
+
logger.end()
|
|
81
|
+
logger.logTitledMessage('Source language detected', formatLanguageCodeWithName(detectedLanguage))
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
options.targetLanguage = await normalizeIdentifierToLangaugeCode(options.targetLanguage!)
|
|
85
|
+
|
|
86
|
+
logger.logTitledMessage('Target language', formatLanguageCodeWithName(options.targetLanguage))
|
|
82
87
|
|
|
83
88
|
logger.start('Preprocess audio for translation')
|
|
84
89
|
|
|
85
90
|
const engine = options.engine!
|
|
86
|
-
const sourceLanguage =
|
|
91
|
+
const sourceLanguage = options.sourceLanguage!
|
|
87
92
|
const targetLanguage = options.targetLanguage!
|
|
88
93
|
|
|
89
94
|
let transcript: string
|
|
@@ -119,10 +124,6 @@ export async function translateSpeech(input: AudioSourceParam, options: SpeechTr
|
|
|
119
124
|
|
|
120
125
|
({ transcript, timeline: wordTimeline } = await WhisperSTT.recognize(sourceRawAudio, modelName, modelDir, 'translate', sourceLanguage, whisperOptions))
|
|
121
126
|
|
|
122
|
-
addWordTextOffsetsToTimeline(wordTimeline, transcript);
|
|
123
|
-
|
|
124
|
-
({ segmentTimeline } = await wordTimelineToSegmentSentenceTimeline(wordTimeline, transcript, targetLanguage, 'single', 'preserve'))
|
|
125
|
-
|
|
126
127
|
break
|
|
127
128
|
}
|
|
128
129
|
|
|
@@ -155,9 +156,7 @@ export async function translateSpeech(input: AudioSourceParam, options: SpeechTr
|
|
|
155
156
|
modelName,
|
|
156
157
|
modelPath,
|
|
157
158
|
whisperCppOptions,
|
|
158
|
-
))
|
|
159
|
-
|
|
160
|
-
({ segmentTimeline } = await wordTimelineToSegmentSentenceTimeline(wordTimeline, transcript, targetLanguage, 'single', 'preserve'))
|
|
159
|
+
))
|
|
161
160
|
|
|
162
161
|
break
|
|
163
162
|
}
|
|
@@ -194,13 +193,21 @@ export async function translateSpeech(input: AudioSourceParam, options: SpeechTr
|
|
|
194
193
|
|
|
195
194
|
// If the audio was cropped before recognition, map the timestamps back to the original audio
|
|
196
195
|
if (sourceUncropTimeline && sourceUncropTimeline.length > 0) {
|
|
197
|
-
API.convertCroppedToUncroppedTimeline(segmentTimeline, sourceUncropTimeline)
|
|
198
|
-
|
|
199
196
|
if (wordTimeline) {
|
|
200
197
|
API.convertCroppedToUncroppedTimeline(wordTimeline, sourceUncropTimeline)
|
|
198
|
+
} else if (segmentTimeline) {
|
|
199
|
+
API.convertCroppedToUncroppedTimeline(segmentTimeline, sourceUncropTimeline)
|
|
201
200
|
}
|
|
202
201
|
}
|
|
203
202
|
|
|
203
|
+
if (wordTimeline) {
|
|
204
|
+
addWordTextOffsetsToTimeline(wordTimeline, transcript)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!segmentTimeline) {
|
|
208
|
+
({ segmentTimeline } = await wordTimelineToSegmentSentenceTimeline(wordTimeline!, transcript, targetLanguage, 'single', 'preserve'))
|
|
209
|
+
}
|
|
210
|
+
|
|
204
211
|
logger.log('')
|
|
205
212
|
logger.logDuration(`Total speech translation time`, startTimestamp, chalk.magentaBright)
|
|
206
213
|
|
|
@@ -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'
|
|
@@ -35,8 +35,10 @@ export async function alignTranslation(input: AudioSourceParam, transcript: stri
|
|
|
35
35
|
logger.end()
|
|
36
36
|
logger.log(``)
|
|
37
37
|
|
|
38
|
+
logger.start(`Resample audio to 16kHz mono`)
|
|
38
39
|
sourceRawAudio = await ensureRawAudio(isolatedRawAudio, 16000, 1)
|
|
39
40
|
} else {
|
|
41
|
+
logger.start(`Resample audio to 16kHz mono`)
|
|
40
42
|
sourceRawAudio = await ensureRawAudio(inputRawAudio, 16000, 1)
|
|
41
43
|
}
|
|
42
44
|
|
|
@@ -49,26 +51,35 @@ export async function alignTranslation(input: AudioSourceParam, transcript: stri
|
|
|
49
51
|
logger.end()
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
logger.start('
|
|
54
|
+
logger.start('Normalize and trim audio')
|
|
53
55
|
|
|
54
56
|
sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
|
|
55
57
|
sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
|
|
56
58
|
|
|
59
|
+
logger.end()
|
|
60
|
+
|
|
57
61
|
let sourceLanguage: string
|
|
58
62
|
|
|
59
63
|
if (options.sourceLanguage) {
|
|
60
|
-
|
|
64
|
+
const languageData = await parseLangIdentifier(options.sourceLanguage)
|
|
65
|
+
|
|
66
|
+
sourceLanguage = languageData.Name
|
|
67
|
+
|
|
68
|
+
logger.end()
|
|
69
|
+
logger.logTitledMessage('Source language specified', formatLanguageCodeWithName(sourceLanguage))
|
|
61
70
|
} else {
|
|
62
71
|
logger.start('No source language specified. Detecting speech language')
|
|
63
72
|
const { detectedLanguage } = await API.detectSpeechLanguage(sourceRawAudio, options.languageDetection || {})
|
|
64
73
|
|
|
74
|
+
sourceLanguage = detectedLanguage
|
|
75
|
+
|
|
65
76
|
logger.end()
|
|
66
77
|
logger.logTitledMessage('Source language detected', formatLanguageCodeWithName(detectedLanguage))
|
|
67
|
-
|
|
68
|
-
sourceLanguage = detectedLanguage
|
|
69
78
|
}
|
|
70
79
|
|
|
71
|
-
const targetLanguage =
|
|
80
|
+
const targetLanguage = await normalizeIdentifierToLangaugeCode(options.targetLanguage!)
|
|
81
|
+
|
|
82
|
+
logger.logTitledMessage('Target language', formatLanguageCodeWithName(targetLanguage))
|
|
72
83
|
|
|
73
84
|
let mappedTimeline: Timeline
|
|
74
85
|
|
|
@@ -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
|
|
|
@@ -20,16 +20,15 @@ export async function detectVoiceActivity(input: AudioSourceParam, options: VADO
|
|
|
20
20
|
|
|
21
21
|
const startTimestamp = logger.getTimestamp()
|
|
22
22
|
|
|
23
|
-
logger.start('Prepare for voice activity detection')
|
|
24
|
-
|
|
25
23
|
const inputRawAudio = await ensureRawAudio(input)
|
|
26
24
|
|
|
25
|
+
logger.start(`Resample audio to 16kHz mono`)
|
|
27
26
|
let sourceRawAudio = await ensureRawAudio(inputRawAudio, 16000, 1)
|
|
28
27
|
|
|
29
|
-
options = extendDeep(defaultVADOptions, options)
|
|
30
|
-
|
|
31
28
|
logger.start(`Detect voice activity with ${options.engine}`)
|
|
32
29
|
|
|
30
|
+
options = extendDeep(defaultVADOptions, options)
|
|
31
|
+
|
|
33
32
|
const activityThreshold = options.activityThreshold!
|
|
34
33
|
|
|
35
34
|
let verboseTimeline: Timeline
|
|
@@ -191,38 +190,73 @@ function frameProbabilitiesToTimeline(frameProbabilities: number[], frameDuratio
|
|
|
191
190
|
}
|
|
192
191
|
|
|
193
192
|
export function convertCroppedToUncroppedTimeline(timeline: Timeline, uncropTimeline: Timeline) {
|
|
193
|
+
if (timeline.length === 0) {
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
194
197
|
for (const entry of timeline) {
|
|
195
|
-
|
|
196
|
-
|
|
198
|
+
const {
|
|
199
|
+
mappedStartTime,
|
|
200
|
+
mappedEndTime
|
|
201
|
+
} = mapUsingUncropTimeline(entry.startTime, entry.endTime, uncropTimeline)
|
|
202
|
+
|
|
203
|
+
const mapSubTimeline = (subTimeline: Timeline | undefined) => {
|
|
204
|
+
if (!subTimeline) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const subEntry of subTimeline) {
|
|
209
|
+
subEntry.startTime = Math.min(mappedStartTime + (subEntry.startTime - entry.startTime), mappedEndTime)
|
|
210
|
+
subEntry.endTime = Math.min(mappedStartTime + (subEntry.endTime - entry.startTime), mappedEndTime)
|
|
197
211
|
|
|
198
|
-
|
|
199
|
-
|
|
212
|
+
mapSubTimeline(subEntry.timeline)
|
|
213
|
+
}
|
|
200
214
|
}
|
|
215
|
+
|
|
216
|
+
mapSubTimeline(entry.timeline)
|
|
217
|
+
|
|
218
|
+
entry.startTime = mappedStartTime
|
|
219
|
+
entry.endTime = mappedEndTime
|
|
201
220
|
}
|
|
202
221
|
}
|
|
203
222
|
|
|
204
|
-
|
|
223
|
+
function mapUsingUncropTimeline(startTimeInCroppedAudio: number, endTimeInCroppedAudio: number, uncropTimeline: Timeline) {
|
|
205
224
|
let offsetInCroppedAudio = 0
|
|
206
225
|
|
|
207
|
-
|
|
208
|
-
|
|
226
|
+
if (endTimeInCroppedAudio < startTimeInCroppedAudio) {
|
|
227
|
+
endTimeInCroppedAudio = startTimeInCroppedAudio
|
|
228
|
+
}
|
|
209
229
|
|
|
210
|
-
|
|
230
|
+
let bestOverlapDuration = -1
|
|
231
|
+
let mappedStartTime = -1
|
|
232
|
+
let mappedEndTime = -1
|
|
211
233
|
|
|
212
|
-
|
|
234
|
+
for (const uncropEntry of uncropTimeline) {
|
|
235
|
+
const uncropEntryDuration = uncropEntry.endTime - uncropEntry.startTime
|
|
213
236
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
237
|
+
const overlapStartTime = Math.max(startTimeInCroppedAudio, offsetInCroppedAudio)
|
|
238
|
+
const overlapEndTime = Math.min(endTimeInCroppedAudio, offsetInCroppedAudio + uncropEntryDuration)
|
|
239
|
+
|
|
240
|
+
const overlapDuration = overlapEndTime - overlapStartTime
|
|
241
|
+
|
|
242
|
+
if (overlapDuration >= 0 && overlapDuration > bestOverlapDuration) {
|
|
243
|
+
bestOverlapDuration = overlapDuration
|
|
244
|
+
|
|
245
|
+
mappedStartTime = uncropEntry.startTime + (overlapStartTime - offsetInCroppedAudio)
|
|
246
|
+
mappedEndTime = uncropEntry.startTime + (overlapEndTime - offsetInCroppedAudio)
|
|
220
247
|
}
|
|
221
248
|
|
|
222
|
-
offsetInCroppedAudio +=
|
|
249
|
+
offsetInCroppedAudio += uncropEntryDuration
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (bestOverlapDuration === -1) {
|
|
253
|
+
throw new Error(`No match found in uncrop timeline (should not occur)`)
|
|
223
254
|
}
|
|
224
255
|
|
|
225
|
-
|
|
256
|
+
return {
|
|
257
|
+
mappedStartTime,
|
|
258
|
+
mappedEndTime
|
|
259
|
+
}
|
|
226
260
|
}
|
|
227
261
|
|
|
228
262
|
export interface VADResult {
|
|
@@ -11,8 +11,8 @@ export function encodeRawAudioToWave(rawAudio: RawAudio, bitDepth: BitDepth = 16
|
|
|
11
11
|
return encodeWave(rawAudio, bitDepth, sampleFormat, speakerPositionMask)
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export function decodeWaveToRawAudio(waveFileBuffer: Buffer, ignoreTruncatedChunks =
|
|
15
|
-
return decodeWave(waveFileBuffer, ignoreTruncatedChunks)
|
|
14
|
+
export function decodeWaveToRawAudio(waveFileBuffer: Buffer, ignoreTruncatedChunks = true, ignoreOverflowingDataChunks = true) {
|
|
15
|
+
return decodeWave(waveFileBuffer, ignoreTruncatedChunks, ignoreOverflowingDataChunks)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
package/src/cli/CLILauncher.ts
CHANGED
|
@@ -12,9 +12,8 @@ const scriptArgs = process.argv.slice(2)
|
|
|
12
12
|
const cliScriptPath = resolveToModuleRootDir('dist/cli/CLIStarter.js')
|
|
13
13
|
|
|
14
14
|
const args = [
|
|
15
|
-
'--no-warnings',
|
|
16
|
-
'--no-experimental-fetch',
|
|
17
15
|
'--experimental-wasi-unstable-preview1',
|
|
16
|
+
'--no-warnings',
|
|
18
17
|
cliScriptPath,
|
|
19
18
|
...scriptArgs
|
|
20
19
|
]
|
|
@@ -37,7 +37,11 @@ export async function decodeToChannels(input: string | Buffer, outSampleRate?: n
|
|
|
37
37
|
|
|
38
38
|
const waveAudio = await transcode(input, outputOptions)
|
|
39
39
|
|
|
40
|
-
const
|
|
40
|
+
const logger = new Logger()
|
|
41
|
+
|
|
42
|
+
logger.start(`Convert wave buffer to raw audio`)
|
|
43
|
+
const { rawAudio } = decodeWaveToRawAudio(waveAudio)
|
|
44
|
+
logger.end()
|
|
41
45
|
|
|
42
46
|
return rawAudio
|
|
43
47
|
}
|