echogarden 1.4.4 → 1.5.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.
Files changed (139) hide show
  1. package/data/schemas/options.json +267 -19
  2. package/dist/alignment/{TextAlignment.d.ts → SemanticTextAlignment.d.ts} +4 -2
  3. package/dist/alignment/SemanticTextAlignment.js +336 -0
  4. package/dist/alignment/SemanticTextAlignment.js.map +1 -0
  5. package/dist/alignment/SpeechAlignment.d.ts +2 -1
  6. package/dist/alignment/SpeechAlignment.js +105 -37
  7. package/dist/alignment/SpeechAlignment.js.map +1 -1
  8. package/dist/api/API.d.ts +7 -2
  9. package/dist/api/API.js +7 -2
  10. package/dist/api/API.js.map +1 -1
  11. package/dist/api/APIOptions.d.ts +4 -1
  12. package/dist/api/Alignment.d.ts +1 -1
  13. package/dist/api/Alignment.js +13 -5
  14. package/dist/api/Alignment.js.map +1 -1
  15. package/dist/api/LanguageDetectionCommon.d.ts +6 -0
  16. package/dist/api/LanguageDetectionCommon.js +2 -0
  17. package/dist/api/LanguageDetectionCommon.js.map +1 -0
  18. package/dist/api/Recognition.js.map +1 -1
  19. package/dist/api/{LanguageDetection.d.ts → SpeechLanguageDetection.d.ts} +1 -25
  20. package/dist/api/{LanguageDetection.js → SpeechLanguageDetection.js} +1 -68
  21. package/dist/api/SpeechLanguageDetection.js.map +1 -0
  22. package/dist/api/{Translation.js → SpeechTranslation.js} +3 -3
  23. package/dist/api/SpeechTranslation.js.map +1 -0
  24. package/dist/api/Synthesis.js +4 -4
  25. package/dist/api/TextLanguageDetection.d.ts +21 -0
  26. package/dist/api/TextLanguageDetection.js +67 -0
  27. package/dist/api/TextLanguageDetection.js.map +1 -0
  28. package/dist/api/TextTranslation.d.ts +5 -0
  29. package/dist/api/TextTranslation.js +4 -0
  30. package/dist/api/TextTranslation.js.map +1 -0
  31. package/dist/api/TimelineTranslationAlignment.d.ts +23 -0
  32. package/dist/api/TimelineTranslationAlignment.js +92 -0
  33. package/dist/api/TimelineTranslationAlignment.js.map +1 -0
  34. package/dist/api/TranscriptAndTranslationAlignment.d.ts +35 -0
  35. package/dist/api/TranscriptAndTranslationAlignment.js +78 -0
  36. package/dist/api/TranscriptAndTranslationAlignment.js.map +1 -0
  37. package/dist/api/TranslationAlignment.d.ts +4 -3
  38. package/dist/api/TranslationAlignment.js +9 -8
  39. package/dist/api/TranslationAlignment.js.map +1 -1
  40. package/dist/api/VoiceActivityDetection.js +16 -1
  41. package/dist/api/VoiceActivityDetection.js.map +1 -1
  42. package/dist/cli/CLI.d.ts +27 -7
  43. package/dist/cli/CLI.js +205 -34
  44. package/dist/cli/CLI.js.map +1 -1
  45. package/dist/codecs/FFMpegTranscoder.js +7 -0
  46. package/dist/codecs/FFMpegTranscoder.js.map +1 -1
  47. package/dist/dsp/FFT.d.ts +1 -1
  48. package/dist/dsp/FFT.js +6 -0
  49. package/dist/dsp/FFT.js.map +1 -1
  50. package/dist/dsp/KWeightingFilter.js +1 -1
  51. package/dist/dsp/KWeightingFilter.js.map +1 -1
  52. package/dist/dsp/MelSpectogram.d.ts +3 -2
  53. package/dist/dsp/MelSpectogram.js +14 -8
  54. package/dist/dsp/MelSpectogram.js.map +1 -1
  55. package/dist/math/VectorMath.d.ts +5 -5
  56. package/dist/math/VectorMath.js +4 -4
  57. package/dist/math/VectorMath.js.map +1 -1
  58. package/dist/recognition/WhisperCppSTT.d.ts +1 -1
  59. package/dist/recognition/WhisperCppSTT.js +2 -2
  60. package/dist/recognition/WhisperCppSTT.js.map +1 -1
  61. package/dist/recognition/WhisperSTT.js +9 -6
  62. package/dist/recognition/WhisperSTT.js.map +1 -1
  63. package/dist/server/Client.d.ts +3 -2
  64. package/dist/server/Client.js.map +1 -1
  65. package/dist/server/Worker.d.ts +3 -2
  66. package/dist/server/Worker.js +3 -2
  67. package/dist/server/Worker.js.map +1 -1
  68. package/dist/source-separation/MDXNetSourceSeparation.js +1 -1
  69. package/dist/source-separation/MDXNetSourceSeparation.js.map +1 -1
  70. package/dist/speech-embeddings/WavToVec2BertFeatureEmbeddings.d.ts +13 -0
  71. package/dist/speech-embeddings/WavToVec2BertFeatureEmbeddings.js +68 -0
  72. package/dist/speech-embeddings/WavToVec2BertFeatureEmbeddings.js.map +1 -0
  73. package/dist/speech-language-detection/SileroLanguageDetection.d.ts +1 -1
  74. package/dist/speech-language-detection/SileroLanguageDetection.js +1 -1
  75. package/dist/speech-language-detection/SileroLanguageDetection.js.map +1 -1
  76. package/dist/tests/Test.js +0 -31
  77. package/dist/tests/Test.js.map +1 -1
  78. package/dist/text-language-detection/FastTextLanguageDetection.d.ts +1 -1
  79. package/dist/text-language-detection/FastTextLanguageDetection.js.map +1 -1
  80. package/dist/text-language-detection/TinyLDLanguageDetection.d.ts +1 -1
  81. package/dist/text-language-detection/TinyLDLanguageDetection.js.map +1 -1
  82. package/dist/text-translation/NLLBTextTranslation.js +1 -1
  83. package/dist/text-translation/NLLBTextTranslation.js.map +1 -1
  84. package/dist/utilities/Locale.d.ts +1 -1
  85. package/dist/utilities/Locale.js +1 -1
  86. package/dist/utilities/OnnxUtilities.d.ts +1 -1
  87. package/dist/utilities/PackageManager.js +6 -4
  88. package/dist/utilities/PackageManager.js.map +1 -1
  89. package/dist/utilities/Timeline.d.ts +1 -0
  90. package/dist/utilities/Timeline.js +12 -0
  91. package/dist/utilities/Timeline.js.map +1 -1
  92. package/docs/API.md +81 -3
  93. package/docs/CLI.md +51 -1
  94. package/docs/Engines.md +24 -0
  95. package/docs/Options.md +33 -1
  96. package/package.json +8 -8
  97. package/src/alignment/SemanticTextAlignment.ts +467 -0
  98. package/src/alignment/SpeechAlignment.ts +179 -52
  99. package/src/api/API.ts +18 -2
  100. package/src/api/APIOptions.ts +14 -1
  101. package/src/api/Alignment.ts +31 -9
  102. package/src/api/LanguageDetectionCommon.ts +7 -0
  103. package/src/api/Recognition.ts +2 -0
  104. package/src/api/{LanguageDetection.ts → SpeechLanguageDetection.ts} +1 -119
  105. package/src/api/{Translation.ts → SpeechTranslation.ts} +2 -2
  106. package/src/api/Synthesis.ts +4 -4
  107. package/src/api/TextLanguageDetection.ts +116 -0
  108. package/src/api/TextTranslation.ts +9 -0
  109. package/src/api/TimelineTranslationAlignment.ts +162 -0
  110. package/src/api/TranscriptAndTranslationAlignment.ts +164 -0
  111. package/src/api/TranslationAlignment.ts +12 -10
  112. package/src/api/VoiceActivityDetection.ts +24 -3
  113. package/src/cli/CLI.ts +276 -34
  114. package/src/codecs/FFMpegTranscoder.ts +6 -0
  115. package/src/dsp/FFT.ts +8 -2
  116. package/src/dsp/KWeightingFilter.ts +1 -1
  117. package/src/dsp/MelSpectogram.ts +17 -8
  118. package/src/math/VectorMath.ts +9 -9
  119. package/src/recognition/WhisperCppSTT.ts +2 -2
  120. package/src/recognition/WhisperSTT.ts +10 -6
  121. package/src/server/Client.ts +3 -2
  122. package/src/server/Worker.ts +3 -2
  123. package/src/source-separation/MDXNetSourceSeparation.ts +1 -1
  124. package/src/speech-embeddings/WavToVec2BertFeatureEmbeddings.ts +107 -0
  125. package/src/speech-language-detection/SileroLanguageDetection.ts +2 -1
  126. package/src/tests/Test.ts +1 -38
  127. package/src/text-language-detection/FastTextLanguageDetection.ts +1 -1
  128. package/src/text-language-detection/TinyLDLanguageDetection.ts +1 -1
  129. package/src/text-translation/NLLBTextTranslation.ts +1 -1
  130. package/src/utilities/Locale.ts +1 -1
  131. package/src/utilities/OnnxUtilities.ts +1 -1
  132. package/src/utilities/PackageManager.ts +7 -4
  133. package/src/utilities/Timeline.ts +14 -0
  134. package/dist/alignment/TextAlignment.js +0 -156
  135. package/dist/alignment/TextAlignment.js.map +0 -1
  136. package/dist/api/LanguageDetection.js.map +0 -1
  137. package/dist/api/Translation.js.map +0 -1
  138. package/src/alignment/TextAlignment.ts +0 -234
  139. /package/dist/api/{Translation.d.ts → SpeechTranslation.d.ts} +0 -0
package/src/cli/CLI.ts CHANGED
@@ -219,17 +219,21 @@ const help = [
219
219
  `${executableName} ${chalk.magentaBright('speak-url')} url [output files...] [options...]`,
220
220
  ` Speak the HTML document on the given URL\n`,
221
221
  `${executableName} ${chalk.magentaBright('speak-wikipedia')} articleName [output files...] [options...]`,
222
- ` Speak the given wikipedia article, language edition can be specified by --language=<langCode>\n`,
222
+ ` Speak the given Wikipedia article. Language edition can be specified by --language=<langCode>\n`,
223
223
  `${executableName} ${chalk.magentaBright('transcribe')} audioFile [output files...] [options...]`,
224
- ` Transcribe audio file\n`,
225
- `${executableName} ${chalk.magentaBright('align')} audioFile referenceFile [output files...] [options...]`,
226
- ` Align audio file to the reference transcript file\n`,
227
- `${executableName} ${chalk.magentaBright('translate-speech')} inputFile [output files...] [options...]`,
224
+ ` Transcribe a spoken audio file\n`,
225
+ `${executableName} ${chalk.magentaBright('align')} audioFile transcriptFile [output files...] [options...]`,
226
+ ` Align spoken audio file to its transcript\n`,
227
+ `${executableName} ${chalk.magentaBright('translate-speech')} audioFile [output files...] [options...]`,
228
228
  ` Transcribe audio file directly to a different language\n`,
229
- `${executableName} ${chalk.magentaBright('align-translation')} audioFile referenceFile [output files...] [options...]`,
230
- ` Align audio file to the reference translated transcript file\n`,
229
+ `${executableName} ${chalk.magentaBright('align-translation')} audioFile translatedTranscriptFile [output files...] [options...]`,
230
+ ` Align spoken audio file to its translated transcript\n`,
231
+ `${executableName} ${chalk.magentaBright('align-transcript-and-translation')} audioFile transcriptFile translatedTranscriptFile [output files...] [options...]`,
232
+ ` Align spoken audio file to both its transcript and its translated transcript using a two-stage approach.\n`,
233
+ `${executableName} ${chalk.magentaBright('align-timeline-translation')} timelineFile translatedFile [output files...] [options...]`,
234
+ ` Align a given timeline file to its translated text\n`,
231
235
  `${executableName} ${chalk.magentaBright('detect-speech-language')} audioFile [output files...] [options...]`,
232
- ` Detect language of audio file\n`,
236
+ ` Detect language of spoken audio file\n`,
233
237
  `${executableName} ${chalk.magentaBright('detect-text-language')} inputFile [output files...] [options...]`,
234
238
  ` Detect language of textual file\n`,
235
239
  `${executableName} ${chalk.magentaBright('detect-voice-activity')} audioFile [output files...] [options...]`,
@@ -285,6 +289,16 @@ async function startWithArgs(operationData: CLIOperationData) {
285
289
  break
286
290
  }
287
291
 
292
+ case 'align-transcript-and-translation': {
293
+ await alignTranscriptAndTranslation(operationData)
294
+ break
295
+ }
296
+
297
+ case 'align-timeline-translation': {
298
+ await alignTimelineTranslation(operationData)
299
+ break
300
+ }
301
+
288
302
  case 'detect-language': {
289
303
  await detectLanguage(operationData, 'auto')
290
304
  break
@@ -352,7 +366,7 @@ async function startWithArgs(operationData: CLIOperationData) {
352
366
  }
353
367
  }
354
368
 
355
- async function speak(operationData: CLIOperationData) {
369
+ export async function speak(operationData: CLIOperationData) {
356
370
  const logger = new Logger()
357
371
 
358
372
  const { operationArgs, operation, operationOptionsLookup, cliOptions } = operationData
@@ -424,8 +438,8 @@ async function speak(operationData: CLIOperationData) {
424
438
  textSegments = splitToParagraphs(textContent, 'single', 'preserve')
425
439
  } else if (sourceFileExtension == 'srt' || sourceFileExtension == 'vtt') {
426
440
  const fileContent = await readFile(sourceFile, { encoding: 'utf-8' })
427
- textSegments = subtitlesToTimeline(fileContent).map(entry => entry.text)
428
- //textSegments = [subtitlesToText(fileContent)]
441
+ //textSegments = subtitlesToTimeline(fileContent).map(entry => entry.text)
442
+ textSegments = [subtitlesToText(fileContent)]
429
443
  } else if (sourceFileExtension == 'xml' || sourceFileExtension == 'ssml') {
430
444
  options.ssml = true
431
445
  textSegments = [fileContent]
@@ -510,7 +524,7 @@ async function speak(operationData: CLIOperationData) {
510
524
  logger.end()
511
525
  }
512
526
 
513
- async function transcribe(operationData: CLIOperationData) {
527
+ export async function transcribe(operationData: CLIOperationData) {
514
528
  const logger = new Logger()
515
529
 
516
530
  const { operationArgs, operationOptionsLookup, cliOptions } = operationData
@@ -572,7 +586,7 @@ async function transcribe(operationData: CLIOperationData) {
572
586
  }
573
587
  }
574
588
 
575
- async function align(operationData: CLIOperationData) {
589
+ export async function align(operationData: CLIOperationData) {
576
590
  const logger = new Logger()
577
591
 
578
592
  const { operationArgs, operationOptionsLookup, cliOptions } = operationData
@@ -668,7 +682,7 @@ async function align(operationData: CLIOperationData) {
668
682
  }
669
683
  }
670
684
 
671
- async function alignTranslation(operationData: CLIOperationData) {
685
+ export async function alignTranslation(operationData: CLIOperationData) {
672
686
  const logger = new Logger()
673
687
 
674
688
  const { operationArgs, operationOptionsLookup, cliOptions } = operationData
@@ -677,7 +691,7 @@ async function alignTranslation(operationData: CLIOperationData) {
677
691
  const outputFilenames = operationArgs.slice(2)
678
692
 
679
693
  if (audioFilename == undefined) {
680
- throw new Error(`align-translation requires an argument containing the audio file path.`)
694
+ throw new Error(`align-translation requires a first argument containing the audio file path.`)
681
695
  }
682
696
 
683
697
  if (!existsSync(audioFilename)) {
@@ -706,7 +720,7 @@ async function alignTranslation(operationData: CLIOperationData) {
706
720
  } else if (referenceFileExtension == 'srt' || referenceFileExtension == 'vtt') {
707
721
  text = subtitlesToText(fileContent)
708
722
  } else {
709
- throw new Error(`align only supports reference files with extensions 'txt', 'html', 'htm', 'srt' or 'vtt'`)
723
+ throw new Error(`align-translation only supports reference files with extensions 'txt', 'html', 'htm', 'srt' or 'vtt'`)
710
724
  }
711
725
 
712
726
  if (cliOptions.play == null) {
@@ -721,8 +735,9 @@ async function alignTranslation(operationData: CLIOperationData) {
721
735
  const {
722
736
  timeline,
723
737
  wordTimeline,
724
- transcript,
725
- language,
738
+ translatedTranscript,
739
+ sourceLanguage,
740
+ targetLanguage,
726
741
  inputRawAudio,
727
742
  isolatedRawAudio,
728
743
  backgroundRawAudio } = await API.alignTranslation(audioFilename, text, options)
@@ -737,10 +752,139 @@ async function alignTranslation(operationData: CLIOperationData) {
737
752
  const segmentAudio = sliceRawAudioByTime(inputRawAudio, segmentEntry.startTime, segmentEntry.endTime)
738
753
  const sentenceTimeline = addTimeOffsetToTimeline(segmentEntry.timeline!, -segmentEntry.startTime)
739
754
 
740
- await writeOutputFilesForSegment(outputFilenames, segmentIndex, timeline.length, segmentAudio, sentenceTimeline, segmentEntry.text, language, allowOverwrite)
755
+ await writeOutputFilesForSegment(outputFilenames, segmentIndex, timeline.length, segmentAudio, sentenceTimeline, segmentEntry.text, targetLanguage, allowOverwrite)
756
+ }
757
+ }
758
+
759
+ for (const outputFilename of outputFilenames) {
760
+ const partPatternMatch = outputFilename.match(filenamePlaceholderPattern)
761
+
762
+ if (partPatternMatch) {
763
+ continue
764
+ }
765
+
766
+ const fileSaver = getFileSaver(outputFilename, allowOverwrite)
767
+
768
+ await fileSaver(inputRawAudio, timeline, translatedTranscript, options.subtitles)
769
+
770
+ await writeSourceSeparationOutputIfNeeded(outputFilename, isolatedRawAudio, backgroundRawAudio, allowOverwrite, true)
771
+ }
772
+
773
+ logger.end()
774
+
775
+ if (cliOptions.play) {
776
+ let audioToPlay: RawAudio
777
+
778
+ if (isolatedRawAudio) {
779
+ audioToPlay = isolatedRawAudio
780
+ } else {
781
+ audioToPlay = inputRawAudio
782
+ }
783
+
784
+ const normalizedAudioToPlay = normalizeAudioLevel(audioToPlay)
785
+
786
+ await playAudioWithWordTimeline(normalizedAudioToPlay, wordTimeline, translatedTranscript)
787
+ }
788
+ }
789
+
790
+ export async function alignTranscriptAndTranslation(operationData: CLIOperationData) {
791
+ const logger = new Logger()
792
+
793
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData
794
+
795
+ const audioFilename = operationArgs[0]
796
+ const outputFilenames = operationArgs.slice(3)
797
+
798
+ if (audioFilename == undefined) {
799
+ throw new Error(`align-transcript-and-translation requires a first argument containing the audio file path.`)
800
+ }
801
+
802
+ if (!existsSync(audioFilename)) {
803
+ throw new Error(`The given source file '${audioFilename}' was not found.`)
804
+ }
805
+
806
+ const nativeTranscriptFilePath = operationArgs[1]
807
+
808
+ if (nativeTranscriptFilePath == undefined) {
809
+ throw new Error(`align-transcript-and-translation requires a second argument containing the native language transcript file path.`)
810
+ }
811
+
812
+ if (!existsSync(nativeTranscriptFilePath)) {
813
+ throw new Error(`The given transcript file '${nativeTranscriptFilePath}' was not found.`)
814
+ }
815
+
816
+ const translatedTranscriptFilePath = operationArgs[2]
817
+
818
+ if (translatedTranscriptFilePath == undefined) {
819
+ throw new Error(`align-transcript-and-translation requires a third argument containing the translated language transcript file path.`)
820
+ }
821
+
822
+ if (!existsSync(translatedTranscriptFilePath)) {
823
+ throw new Error(`The given translated transcript file '${nativeTranscriptFilePath}' was not found.`)
824
+ }
825
+
826
+ let transcript: string
827
+
828
+ {
829
+ const nativeTranscriptFileExtension = getLowercaseFileExtension(nativeTranscriptFilePath)
830
+ const fileContent = await readFile(nativeTranscriptFilePath, { encoding: 'utf-8' })
831
+
832
+
833
+ if (nativeTranscriptFileExtension == 'txt') {
834
+ transcript = fileContent
835
+ } else if (nativeTranscriptFileExtension == 'html' || nativeTranscriptFileExtension == 'htm') {
836
+ transcript = await convertHtmlToText(fileContent)
837
+ } else if (nativeTranscriptFileExtension == 'srt' || nativeTranscriptFileExtension == 'vtt') {
838
+ transcript = subtitlesToText(fileContent)
839
+ } else {
840
+ throw new Error(`align-transcript-and-translation only supports transcript files with extensions 'txt', 'html', 'htm', 'srt' or 'vtt'`)
741
841
  }
742
842
  }
743
843
 
844
+ let translatedTranscript: string
845
+
846
+ {
847
+ const translatedTranscriptFileExtension = getLowercaseFileExtension(translatedTranscriptFilePath)
848
+ const fileContent = await readFile(translatedTranscriptFilePath, { encoding: 'utf-8' })
849
+
850
+ if (translatedTranscriptFileExtension == 'txt') {
851
+ translatedTranscript = fileContent
852
+ } else if (translatedTranscriptFileExtension == 'html' || translatedTranscriptFileExtension == 'htm') {
853
+ translatedTranscript = await convertHtmlToText(fileContent)
854
+ } else if (translatedTranscriptFileExtension == 'srt' || translatedTranscriptFileExtension == 'vtt') {
855
+ translatedTranscript = subtitlesToText(fileContent)
856
+ } else {
857
+ throw new Error(`align-transcript-and-translation only supports transcript files with extensions 'txt', 'html', 'htm', 'srt' or 'vtt'`)
858
+ }
859
+ }
860
+
861
+ if (cliOptions.play == null) {
862
+ cliOptions.play = outputFilenames.length === 0
863
+ }
864
+
865
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'TranscriptAndTranslationAlignmentOptions')
866
+
867
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault)
868
+ const { includesPlaceholderPattern } = await checkOutputFilenames(outputFilenames, true, true, true)
869
+
870
+ const {
871
+ timeline,
872
+ wordTimeline,
873
+
874
+ translatedTimeline,
875
+ translatedWordTimeline,
876
+
877
+ sourceLanguage,
878
+ targetLanguage,
879
+
880
+ inputRawAudio,
881
+ isolatedRawAudio,
882
+ backgroundRawAudio } = await API.alignTranscriptAndTranslation(audioFilename, transcript, translatedTranscript, options)
883
+
884
+ if (outputFilenames.length > 0) {
885
+ logger.start('\nWrite output files')
886
+ }
887
+
744
888
  for (const outputFilename of outputFilenames) {
745
889
  const partPatternMatch = outputFilename.match(filenamePlaceholderPattern)
746
890
 
@@ -753,6 +897,16 @@ async function alignTranslation(operationData: CLIOperationData) {
753
897
  await fileSaver(inputRawAudio, timeline, transcript, options.subtitles)
754
898
 
755
899
  await writeSourceSeparationOutputIfNeeded(outputFilename, isolatedRawAudio, backgroundRawAudio, allowOverwrite, true)
900
+
901
+ const fileExtension = getLowercaseFileExtension(outputFilename)
902
+
903
+ if (['json', 'txt', 'srt', 'vtt'].includes(fileExtension)) {
904
+ const pathWithoutExtension = outputFilename.substring(0, outputFilename.lastIndexOf('.'))
905
+ const translatedOutputPath = `${pathWithoutExtension}.translated.${fileExtension}`
906
+
907
+ const translatedFileSaver = getFileSaver(translatedOutputPath, allowOverwrite)
908
+ await translatedFileSaver(inputRawAudio, translatedTimeline, translatedTranscript, options.subtitles)
909
+ }
756
910
  }
757
911
 
758
912
  logger.end()
@@ -768,11 +922,99 @@ async function alignTranslation(operationData: CLIOperationData) {
768
922
 
769
923
  const normalizedAudioToPlay = normalizeAudioLevel(audioToPlay)
770
924
 
771
- await playAudioWithWordTimeline(normalizedAudioToPlay, wordTimeline, transcript)
925
+ await playAudioWithWordTimeline(normalizedAudioToPlay, translatedWordTimeline, translatedTranscript)
926
+ }
927
+ }
928
+
929
+ export async function alignTimelineTranslation(operationData: CLIOperationData) {
930
+ const logger = new Logger()
931
+
932
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData
933
+
934
+ const timelineFilename = operationArgs[0]
935
+ const outputFilenames = operationArgs.slice(2)
936
+
937
+ if (timelineFilename == undefined) {
938
+ throw new Error(`align-timeline-translation requires a first argument containing the timeline file path.`)
939
+ }
940
+
941
+ if (getLowercaseFileExtension(timelineFilename) != 'json') {
942
+ throw new Error(`align-timeline-translation only supports timeline files with extension 'json'`)
943
+ }
944
+
945
+ if (!existsSync(timelineFilename)) {
946
+ throw new Error(`The given timeline file '${timelineFilename}' was not found.`)
947
+ }
948
+
949
+ const timeline = await readAndParseJsonFile(timelineFilename) as Timeline
950
+
951
+ const translationFilePath = operationArgs[1]
952
+
953
+ if (translationFilePath == undefined) {
954
+ throw new Error(`align-timeline-translation requires a second argument containing the translated reference file path.`)
955
+ }
956
+
957
+ if (!existsSync(translationFilePath)) {
958
+ throw new Error(`The given reference file '${translationFilePath}' was not found.`)
959
+ }
960
+
961
+ const translationFileExtension = getLowercaseFileExtension(translationFilePath)
962
+ const translationFileContent = await readFile(translationFilePath, { encoding: 'utf-8' })
963
+
964
+ let translationText: string
965
+
966
+ if (translationFileExtension == 'txt') {
967
+ translationText = translationFileContent
968
+ } else if (translationFileExtension == 'html' || translationFileExtension == 'htm') {
969
+ translationText = await convertHtmlToText(translationFileContent)
970
+ } else if (translationFileExtension == 'srt' || translationFileExtension == 'vtt') {
971
+ translationText = subtitlesToText(translationFileContent)
972
+ } else {
973
+ throw new Error(`align only supports reference files with extensions 'txt', 'html', 'htm', 'srt' or 'vtt'`)
974
+ }
975
+
976
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'TimelineTranslationAlignmentOptions')
977
+
978
+ const {
979
+ timeline: translationTimeline,
980
+ wordTimeline: translationWordTimeline,
981
+ rawAudio
982
+ } = await API.alignTimelineTranslation(timeline, translationText, options)
983
+
984
+ if (outputFilenames.length > 0) {
985
+ logger.start('\nWrite output files')
986
+ }
987
+
988
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault)
989
+
990
+ for (const outputFilename of outputFilenames) {
991
+ const partPatternMatch = outputFilename.match(filenamePlaceholderPattern)
992
+
993
+ if (partPatternMatch) {
994
+ continue
995
+ }
996
+
997
+ const fileSaver = getFileSaver(outputFilename, allowOverwrite)
998
+
999
+ await fileSaver(getEmptyRawAudio(1, 16000), translationTimeline, translationText, options.subtitles)
1000
+ }
1001
+
1002
+ logger.end()
1003
+
1004
+ if (cliOptions.play && rawAudio) {
1005
+ const normalizedAudioToPlay = normalizeAudioLevel(rawAudio)
1006
+
1007
+ let transcriptToPlay: string
1008
+ let timelineToPlay: Timeline
1009
+
1010
+ transcriptToPlay = translationText
1011
+ timelineToPlay = translationWordTimeline
1012
+
1013
+ await playAudioWithWordTimeline(normalizedAudioToPlay, timelineToPlay, transcriptToPlay)
772
1014
  }
773
1015
  }
774
1016
 
775
- async function translateSpeech(operationData: CLIOperationData) {
1017
+ export async function translateSpeech(operationData: CLIOperationData) {
776
1018
  const logger = new Logger()
777
1019
 
778
1020
  const { operationArgs, operationOptionsLookup, cliOptions } = operationData
@@ -859,7 +1101,7 @@ async function translateSpeech(operationData: CLIOperationData) {
859
1101
  }
860
1102
  }
861
1103
 
862
- async function detectLanguage(operationData: CLIOperationData, mode: 'speech' | 'text' | 'auto') {
1104
+ export async function detectLanguage(operationData: CLIOperationData, mode: 'speech' | 'text' | 'auto') {
863
1105
  const logger = new Logger()
864
1106
 
865
1107
  const { operationArgs, operationOptionsLookup, cliOptions } = operationData
@@ -936,7 +1178,7 @@ async function detectLanguage(operationData: CLIOperationData, mode: 'speech' |
936
1178
  logger.end()
937
1179
  }
938
1180
 
939
- async function detectVoiceActivity(operationData: CLIOperationData) {
1181
+ export async function detectVoiceActivity(operationData: CLIOperationData) {
940
1182
  const logger = new Logger()
941
1183
 
942
1184
  const { operationArgs, operationOptionsLookup, cliOptions } = operationData
@@ -1005,7 +1247,7 @@ async function detectVoiceActivity(operationData: CLIOperationData) {
1005
1247
  }
1006
1248
  }
1007
1249
 
1008
- async function denoise(operationData: CLIOperationData) {
1250
+ export async function denoise(operationData: CLIOperationData) {
1009
1251
  const logger = new Logger()
1010
1252
 
1011
1253
  const { operationArgs, operationOptionsLookup, cliOptions } = operationData
@@ -1050,7 +1292,7 @@ async function denoise(operationData: CLIOperationData) {
1050
1292
  }
1051
1293
  }
1052
1294
 
1053
- async function isolate(operationData: CLIOperationData) {
1295
+ export async function isolate(operationData: CLIOperationData) {
1054
1296
  const logger = new Logger()
1055
1297
 
1056
1298
  const { operationArgs, operationOptionsLookup, cliOptions } = operationData
@@ -1093,7 +1335,7 @@ async function isolate(operationData: CLIOperationData) {
1093
1335
  }
1094
1336
  }
1095
1337
 
1096
- async function listEngines(operationData: CLIOperationData) {
1338
+ export async function listEngines(operationData: CLIOperationData) {
1097
1339
  const logger = new Logger()
1098
1340
 
1099
1341
  const { operationArgs } = operationData
@@ -1206,7 +1448,7 @@ async function listEngines(operationData: CLIOperationData) {
1206
1448
  }
1207
1449
  }
1208
1450
 
1209
- async function listTTSVoices(operationData: CLIOperationData) {
1451
+ export async function listTTSVoices(operationData: CLIOperationData) {
1210
1452
  const logger = new Logger()
1211
1453
 
1212
1454
  const { operationArgs, operationOptionsLookup, cliOptions } = operationData
@@ -1268,7 +1510,7 @@ async function listTTSVoices(operationData: CLIOperationData) {
1268
1510
  logger.end()
1269
1511
  }
1270
1512
 
1271
- async function installPackages(operationData: CLIOperationData) {
1513
+ export async function installPackages(operationData: CLIOperationData) {
1272
1514
  const logger = new Logger()
1273
1515
 
1274
1516
  const { operationArgs } = operationData
@@ -1299,7 +1541,7 @@ async function installPackages(operationData: CLIOperationData) {
1299
1541
  }
1300
1542
  }
1301
1543
 
1302
- async function uninstallPackages(operationData: CLIOperationData) {
1544
+ export async function uninstallPackages(operationData: CLIOperationData) {
1303
1545
  const logger = new Logger()
1304
1546
 
1305
1547
  const { operationArgs } = operationData
@@ -1326,7 +1568,7 @@ async function uninstallPackages(operationData: CLIOperationData) {
1326
1568
  }
1327
1569
  }
1328
1570
 
1329
- async function listPackages(operationData: CLIOperationData) {
1571
+ export async function listPackages(operationData: CLIOperationData) {
1330
1572
  const logger = new Logger()
1331
1573
 
1332
1574
  const packagesDir = await ensureAndGetPackagesDir()
@@ -1358,7 +1600,7 @@ async function listPackages(operationData: CLIOperationData) {
1358
1600
  logger.log(installedPackageNamesFormatted.join('\n'))
1359
1601
  }
1360
1602
 
1361
- async function serve(operationData: CLIOperationData) {
1603
+ export async function serve(operationData: CLIOperationData) {
1362
1604
  const { operationOptionsLookup } = operationData
1363
1605
 
1364
1606
  const options = await optionsLookupToTypedObject(operationOptionsLookup, 'ServerOptions')
@@ -1500,7 +1742,7 @@ async function optionsLookupToTypedObject<K extends keyof APIOptions>(cliOptions
1500
1742
  }
1501
1743
 
1502
1744
  let cachedOptionsSchema: any
1503
- export async function getOptionsSchema() {
1745
+ async function getOptionsSchema() {
1504
1746
  if (!cachedOptionsSchema) {
1505
1747
  cachedOptionsSchema = await readAndParseJsonFile(resolveToModuleRootDir('data/schemas/options.json'))
1506
1748
  }
@@ -1508,7 +1750,7 @@ export async function getOptionsSchema() {
1508
1750
  return cachedOptionsSchema
1509
1751
  }
1510
1752
 
1511
- export async function checkOutputFilenames(outputFilenames: string[], acceptMediaOutputs: boolean, acceptMetadataOutputs: boolean, acceptSubtitleOutputs: boolean) {
1753
+ async function checkOutputFilenames(outputFilenames: string[], acceptMediaOutputs: boolean, acceptMetadataOutputs: boolean, acceptSubtitleOutputs: boolean) {
1512
1754
  const supportedFileExtensions: string[] = []
1513
1755
 
1514
1756
  if (acceptMediaOutputs) {
@@ -1562,7 +1804,7 @@ export async function checkOutputFilenames(outputFilenames: string[], acceptMedi
1562
1804
  return { includesPlaceholderPattern }
1563
1805
  }
1564
1806
 
1565
- export async function writeOutputFilesForSegment(outputFilenames: string[], index: number, total: number, audio: RawAudio, timeline: Timeline, text: string, language: string, allowOverwrite: boolean) {
1807
+ async function writeOutputFilesForSegment(outputFilenames: string[], index: number, total: number, audio: RawAudio, timeline: Timeline, text: string, language: string, allowOverwrite: boolean) {
1566
1808
  const digitCount = Math.max((total + 1).toString().length, 2)
1567
1809
 
1568
1810
  const segmentWords = (await splitToWords(text, language)).filter(text => wordCharacterPattern.test(text))
@@ -7,6 +7,7 @@ import { commandExists, logToStderr } from '../utilities/Utilities.js'
7
7
  import path from 'node:path'
8
8
  import { loadPackage } from '../utilities/PackageManager.js'
9
9
  import { getGlobalOption } from '../api/GlobalOptions.js'
10
+ import { existsSync } from '../utilities/FileSystem.js'
10
11
 
11
12
  const log = logToStderr
12
13
 
@@ -67,6 +68,11 @@ async function transcode_CLI(ffmpegCommand: string, input: string | Buffer, outp
67
68
 
68
69
  if (Buffer.isBuffer(input)) {
69
70
  process.stdin.end(input)
71
+ } else if (typeof input === 'string') {
72
+ if (!existsSync(input)) {
73
+ reject(`Audio file was not found: ${input}`)
74
+ return
75
+ }
70
76
  }
71
77
 
72
78
  const stdoutChunks: Buffer[] = []
package/src/dsp/FFT.ts CHANGED
@@ -48,7 +48,7 @@ export async function stftr(samples: Float32Array, fftOrder: number, windowSize:
48
48
  }
49
49
 
50
50
  binsBufferRef.clear()
51
-
51
+
52
52
  m._kiss_fftr(statePtr, frameBufferRef.address, binsBufferRef.address)
53
53
 
54
54
  const bins = binsBufferRef.view.slice(0, fftOrder + 2)
@@ -229,6 +229,12 @@ export function getWindowWeights(windowType: WindowType, windowSize: number) {
229
229
  //weights[i] = 0.54 - (0.46 * Math.cos(2 * Math.PI * (i / (windowSize - 1))))
230
230
  weights[i] = 0.54 - (0.46 * Math.cos(innerFactor * i))
231
231
  }
232
+ } else if (windowType == 'povey') {
233
+ const hannWeights = getWindowWeights('hann', windowSize)
234
+
235
+ for (let i = 0; i < windowSize; i++) {
236
+ weights[i] = hannWeights[i] ** 0.85
237
+ }
232
238
  } else {
233
239
  throw new Error(`Unsupported window function type: ${windowType}`)
234
240
  }
@@ -255,4 +261,4 @@ export async function testFFT1(rawAudio: RawAudio) {
255
261
  return recoveredRawAudio
256
262
  }
257
263
 
258
- export type WindowType = 'hann' | 'hamming'
264
+ export type WindowType = 'hann' | 'hamming' | 'povey'
@@ -10,7 +10,7 @@ export class KWeightingFilter {
10
10
 
11
11
  if (useStandard44100Filters) {
12
12
  // These parameter values are taken from ITU-R BS.1770-2
13
- // and designed only for a 44100 Hz sampling rate:
13
+ // and designed only for a 44100 Hz sample rate:
14
14
  this.highShelfFilter = new BiquadFilter({
15
15
  b0: 1.53512485958697,
16
16
  b1: -2.69169618940638,
@@ -2,7 +2,7 @@ import { RawAudio } from '../audio/AudioUtilities.js'
2
2
  import { Logger } from '../utilities/Logger.js'
3
3
  import * as FFT from './FFT.js'
4
4
 
5
- export async function computeMelSpectogram(rawAudio: RawAudio, fftOrder: number, windowSize: number, hopLength: number, filterbankCount: number, lowerFrequencyHz: number, upperFrequencyHz: number) {
5
+ export async function computeMelSpectogram(rawAudio: RawAudio, fftOrder: number, windowSize: number, hopLength: number, filterbankCount: number, lowerFrequencyHz: number, upperFrequencyHz: number, windowType: FFT.WindowType = 'hann') {
6
6
  const logger = new Logger()
7
7
 
8
8
  logger.start('Compute mel filterbank')
@@ -18,15 +18,15 @@ export async function computeMelSpectogram(rawAudio: RawAudio, fftOrder: number,
18
18
 
19
19
  logger.end()
20
20
 
21
- return computeMelSpectogramUsingFilterbanks(rawAudio, fftOrder, windowSize, hopLength, melFilterbanks)
21
+ return computeMelSpectogramUsingFilterbanks(rawAudio, fftOrder, windowSize, hopLength, melFilterbanks, windowType)
22
22
  }
23
23
 
24
- export async function computeMelSpectogramUsingFilterbanks(rawAudio: RawAudio, fftOrder: number, windowSize: number, hopLength: number, filterbanks: Filterbank[]) {
24
+ export async function computeMelSpectogramUsingFilterbanks(rawAudio: RawAudio, fftOrder: number, windowSize: number, hopLength: number, filterbanks: Filterbank[], windowType: FFT.WindowType = 'hann') {
25
25
  const logger = new Logger()
26
26
 
27
27
  logger.start('Compute short-time FFTs')
28
28
  const audioSamples = rawAudio.audioChannels[0]
29
- const fftFrames = await FFT.stftr(audioSamples, fftOrder, windowSize, hopLength, 'hann')
29
+ const fftFrames = await FFT.stftr(audioSamples, fftOrder, windowSize, hopLength, windowType)
30
30
 
31
31
  logger.start('Convert FFT frames to a mel spectogram')
32
32
  const melSpectogram = fftFramesToMelSpectogram(fftFrames, filterbanks)
@@ -52,17 +52,26 @@ export function powerSpectrumToMelSpectrum(powerSpectrum: Float32Array, filterba
52
52
  const filterbankStartIndex = filterbank.startIndex
53
53
  const filterbankWeights = filterbank.weights
54
54
 
55
- if (filterbankStartIndex == -1) {
55
+ if (filterbankStartIndex === -1) {
56
56
  continue
57
57
  }
58
58
 
59
- let bandValue = 0
59
+ let melBandValue = 0
60
60
 
61
61
  for (let i = 0; i < filterbankWeights.length; i++) {
62
- bandValue += filterbankWeights[i] * powerSpectrum[filterbankStartIndex + i]
62
+ const powerSpectrumIndex = filterbankStartIndex + i
63
+
64
+ if (powerSpectrumIndex >= powerSpectrum.length) {
65
+ break
66
+ }
67
+
68
+ const weight = filterbankWeights[i]
69
+ const powerSpectrumValue = powerSpectrum[powerSpectrumIndex]
70
+
71
+ melBandValue += weight * powerSpectrumValue
63
72
  }
64
73
 
65
- melSpectrum[melBandIndex] = bandValue
74
+ melSpectrum[melBandIndex] = melBandValue
66
75
  }
67
76
 
68
77
  return melSpectrum
@@ -177,7 +177,7 @@ export function scaleToSumTo1(vector: number[]) {
177
177
  return scaledVector
178
178
  }
179
179
 
180
- export function normalizeVector(vector: number[], kind: 'population' | 'sample' = 'population') {
180
+ export function normalizeVector(vector: ArrayLike<number>, kind: 'population' | 'sample' = 'population') {
181
181
  if (vector.length == 0) {
182
182
  throw new Error('Vector is empty')
183
183
  }
@@ -329,15 +329,15 @@ export function varianceOfVectors(vectors: number[][], kind: 'population' | 'sam
329
329
  return result
330
330
  }
331
331
 
332
- export function meanOfVector(vector: number[]) {
332
+ export function meanOfVector(vector: ArrayLike<number>) {
333
333
  if (vector.length == 0) {
334
- throw new Error('Vector is empty')
334
+ return 0
335
335
  }
336
336
 
337
337
  return sumVector(vector) / vector.length
338
338
  }
339
339
 
340
- export function medianOfVector(vector: number[]) {
340
+ export function medianOfVector(vector: ArrayLike<number>) {
341
341
  if (vector.length == 0) {
342
342
  throw new Error('Vector is empty')
343
343
  }
@@ -345,13 +345,13 @@ export function medianOfVector(vector: number[]) {
345
345
  return vector[Math.floor(vector.length / 2)]
346
346
  }
347
347
 
348
- export function stdDeviationOfVector(vector: number[], kind: 'population' | 'sample' = 'population', mean?: number) {
348
+ export function stdDeviationOfVector(vector: ArrayLike<number>, kind: 'population' | 'sample' = 'population', mean?: number) {
349
349
  return Math.sqrt(varianceOfVector(vector, kind, mean))
350
350
  }
351
351
 
352
- export function varianceOfVector(vector: number[], kind: 'population' | 'sample' = 'population', mean?: number) {
352
+ export function varianceOfVector(vector: ArrayLike<number>, kind: 'population' | 'sample' = 'population', mean?: number) {
353
353
  if (vector.length == 0) {
354
- throw new Error('Vector is empty')
354
+ return 0
355
355
  }
356
356
 
357
357
  const sampleSizeMetric = kind == 'population' || vector.length == 1 ? vector.length : vector.length - 1
@@ -362,8 +362,8 @@ export function varianceOfVector(vector: number[], kind: 'population' | 'sample'
362
362
 
363
363
  let result = 0.0
364
364
 
365
- for (const value of vector) {
366
- result += (value - mean) ** 2
365
+ for (let i = 0; i < vector.length; i++) {
366
+ result += (vector[i] - mean) ** 2
367
367
  }
368
368
 
369
369
  return result / sampleSizeMetric