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.
Files changed (71) hide show
  1. package/data/tables/lcid-table.json +7427 -7427
  2. package/dist/alignment/TextAlignment.d.ts +16 -0
  3. package/dist/alignment/TextAlignment.js +63 -0
  4. package/dist/alignment/TextAlignment.js.map +1 -0
  5. package/dist/api/Alignment.js +11 -4
  6. package/dist/api/Alignment.js.map +1 -1
  7. package/dist/api/Denoising.js +0 -1
  8. package/dist/api/Denoising.js.map +1 -1
  9. package/dist/api/LanguageDetection.js +2 -2
  10. package/dist/api/LanguageDetection.js.map +1 -1
  11. package/dist/api/Recognition.js +15 -11
  12. package/dist/api/Recognition.js.map +1 -1
  13. package/dist/api/Synthesis.js +11 -15
  14. package/dist/api/Synthesis.js.map +1 -1
  15. package/dist/api/Translation.js +24 -15
  16. package/dist/api/Translation.js.map +1 -1
  17. package/dist/api/TranslationAlignment.js +12 -5
  18. package/dist/api/TranslationAlignment.js.map +1 -1
  19. package/dist/api/VoiceActivityDetection.d.ts +0 -4
  20. package/dist/api/VoiceActivityDetection.js +43 -20
  21. package/dist/api/VoiceActivityDetection.js.map +1 -1
  22. package/dist/audio/AudioUtilities.d.ts +1 -1
  23. package/dist/audio/AudioUtilities.js +2 -2
  24. package/dist/audio/AudioUtilities.js.map +1 -1
  25. package/dist/cli/CLILauncher.js +1 -2
  26. package/dist/cli/CLILauncher.js.map +1 -1
  27. package/dist/codecs/FFMpegTranscoder.js +4 -1
  28. package/dist/codecs/FFMpegTranscoder.js.map +1 -1
  29. package/dist/codecs/WaveCodec.d.ts +1 -1
  30. package/dist/codecs/WaveCodec.js +13 -5
  31. package/dist/codecs/WaveCodec.js.map +1 -1
  32. package/dist/dsp/SpeexResampler.js +49 -21
  33. package/dist/dsp/SpeexResampler.js.map +1 -1
  34. package/dist/recognition/WhisperSTT.js +4 -4
  35. package/dist/recognition/WhisperSTT.js.map +1 -1
  36. package/dist/synthesis/CoquiServerTTS.js +1 -1
  37. package/dist/synthesis/CoquiServerTTS.js.map +1 -1
  38. package/dist/synthesis/VitsTTS.js +1 -1
  39. package/dist/synthesis/VitsTTS.js.map +1 -1
  40. package/dist/text-translation/NLLBTextTranslation.d.ts +1 -0
  41. package/dist/text-translation/NLLBTextTranslation.js +237 -0
  42. package/dist/text-translation/NLLBTextTranslation.js.map +1 -0
  43. package/dist/utilities/Locale.d.ts +14 -8
  44. package/dist/utilities/Locale.js +52 -11
  45. package/dist/utilities/Locale.js.map +1 -1
  46. package/dist/utilities/PackageManager.js +2 -0
  47. package/dist/utilities/PackageManager.js.map +1 -1
  48. package/docs/Development.md +1 -1
  49. package/docs/Server.md +1 -1
  50. package/docs/Tasklist.md +11 -6
  51. package/package.json +12 -11
  52. package/src/alignment/TextAlignment.ts +96 -0
  53. package/src/api/Alignment.ts +19 -4
  54. package/src/api/Denoising.ts +0 -2
  55. package/src/api/LanguageDetection.ts +2 -3
  56. package/src/api/Recognition.ts +18 -13
  57. package/src/api/Synthesis.ts +13 -21
  58. package/src/api/Translation.ts +29 -22
  59. package/src/api/TranslationAlignment.ts +17 -6
  60. package/src/api/VoiceActivityDetection.ts +56 -22
  61. package/src/audio/AudioUtilities.ts +2 -2
  62. package/src/cli/CLILauncher.ts +1 -2
  63. package/src/codecs/FFMpegTranscoder.ts +5 -1
  64. package/src/codecs/WaveCodec.ts +15 -5
  65. package/src/dsp/SpeexResampler.ts +62 -23
  66. package/src/recognition/WhisperSTT.ts +4 -4
  67. package/src/synthesis/CoquiServerTTS.ts +1 -1
  68. package/src/synthesis/VitsTTS.ts +2 -2
  69. package/src/text-translation/NLLBTextTranslation.ts +252 -0
  70. package/src/utilities/Locale.ts +84 -22
  71. 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
+ }
@@ -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, 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'
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('Prepare for alignment')
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
- language = normalizeLanguageCode(options.language!)
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
- logger.logTitledMessage('Language detected', formatLanguageCodeWithName(detectedLanguage))
96
+
97
+ logger.logTitledMessage('Language detected', formatLanguageCodeWithName(language))
83
98
  }
84
99
 
85
100
  language = getDefaultDialectForLanguageCodeIfPossible(language)
@@ -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('Prepare for speech language detection')
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) {
@@ -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
 
@@ -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('Prepare for recognition')
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 (!options.language) { // && options.engine != 'whisper') {
66
- logger.start('No language specified. Detecting speech language')
67
- const { detectedLanguage } = await API.detectSpeechLanguage(sourceRawAudio, options.languageDetection!)
67
+ if (options.language) {
68
+ const languageData = await parseLangIdentifier(options.language)
68
69
 
69
- logger.end()
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
- const specifiedLanguageFormatted = formatLanguageCodeWithName(getShortLanguageCode(normalizeLanguageCode(options.language)))
78
+ options.language = detectedLanguage
77
79
 
78
- logger.logTitledMessage('Language specified', specifiedLanguageFormatted)
80
+ logger.end()
81
+ logger.logTitledMessage('Language detected', formatLanguageCodeWithName(detectedLanguage))
79
82
  }
80
83
 
81
- const languageCode = normalizeLanguageCode(options.language)
82
- const shortLanguageCode = getShortLanguageCode(languageCode)
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
@@ -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
- 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'
@@ -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('Prepare for speech translation')
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 (!options.sourceLanguage) {
66
- logger.start('No source language specified. Detecting speech language')
67
- const { detectedLanguage } = await detectSpeechLanguage(sourceRawAudio, options.languageDetection || {})
67
+ if (options.sourceLanguage) {
68
+ const languageData = await parseLangIdentifier(options.sourceLanguage)
68
69
 
69
- logger.end()
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
- const specifiedLanguageFormatted = formatLanguageCodeWithName(getShortLanguageCode(normalizeLanguageCode(options.sourceLanguage)))
78
+ options.sourceLanguage = detectedLanguage
77
79
 
78
- logger.logTitledMessage('Source language', specifiedLanguageFormatted)
80
+ logger.end()
81
+ logger.logTitledMessage('Source language detected', formatLanguageCodeWithName(detectedLanguage))
79
82
  }
80
83
 
81
- logger.logTitledMessage('Target language', formatLanguageCodeWithName(getShortLanguageCode(normalizeLanguageCode(options.targetLanguage!))))
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 = normalizeLanguageCode(options.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, 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'
@@ -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('Prepare for alignment')
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
- sourceLanguage = normalizeLanguageCode(options.sourceLanguage!)
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 = normalizeLanguageCode(options.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
- entry.startTime = mapTimestampUsingUncropTimeline(entry.startTime, uncropTimeline).time
196
- entry.endTime = mapTimestampUsingUncropTimeline(entry.endTime, uncropTimeline).time
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
- if (entry.timeline) {
199
- convertCroppedToUncroppedTimeline(entry.timeline, uncropTimeline)
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
- export function mapTimestampUsingUncropTimeline(timeInCroppedAudio: number, uncropTimeline: Timeline) {
223
+ function mapUsingUncropTimeline(startTimeInCroppedAudio: number, endTimeInCroppedAudio: number, uncropTimeline: Timeline) {
205
224
  let offsetInCroppedAudio = 0
206
225
 
207
- for (let i = 0; i < uncropTimeline.length; i++) {
208
- const entry = uncropTimeline[i]
226
+ if (endTimeInCroppedAudio < startTimeInCroppedAudio) {
227
+ endTimeInCroppedAudio = startTimeInCroppedAudio
228
+ }
209
229
 
210
- const entryDuration = entry.endTime - entry.startTime
230
+ let bestOverlapDuration = -1
231
+ let mappedStartTime = -1
232
+ let mappedEndTime = -1
211
233
 
212
- const endOffset = offsetInCroppedAudio + entryDuration
234
+ for (const uncropEntry of uncropTimeline) {
235
+ const uncropEntryDuration = uncropEntry.endTime - uncropEntry.startTime
213
236
 
214
- if ((i === uncropTimeline.length - 1) ||
215
- (timeInCroppedAudio >= offsetInCroppedAudio && timeInCroppedAudio < endOffset)) {
216
- return {
217
- time: entry.startTime + (timeInCroppedAudio - offsetInCroppedAudio),
218
- entryIndex: i
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 += entryDuration
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
- throw new Error(`Should not be reached`)
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 = false) {
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
  ////////////////////////////////////////////////////////////////////////////////////////////////
@@ -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 { rawAudio } = decodeWaveToRawAudio(waveAudio, true)
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
  }