echogarden 0.12.2 → 1.0.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 (334) hide show
  1. package/README.md +15 -14
  2. package/data/schemas/options.json +398 -111
  3. package/dist/alignment/DTWMfccSequenceAlignment.d.ts +1 -1
  4. package/dist/alignment/DTWMfccSequenceAlignment.js +8 -8
  5. package/dist/alignment/DTWSequenceAlignment.d.ts +1 -1
  6. package/dist/alignment/DTWSequenceAlignment.js +1 -1
  7. package/dist/alignment/DTWSequenceAlignmentWindowed.d.ts +1 -1
  8. package/dist/alignment/DTWSequenceAlignmentWindowed.js +2 -2
  9. package/dist/alignment/LevenshteinSequenceAlignment.d.ts +1 -1
  10. package/dist/alignment/LevenshteinSequenceAlignment.js +1 -1
  11. package/dist/alignment/SpeechAlignment.d.ts +11 -6
  12. package/dist/alignment/SpeechAlignment.js +113 -71
  13. package/dist/alignment/SpeechAlignment.js.map +1 -1
  14. package/dist/api/API.d.ts +13 -12
  15. package/dist/api/API.js +14 -13
  16. package/dist/api/API.js.map +1 -1
  17. package/dist/api/APIOptions.d.ts +5 -4
  18. package/dist/api/Alignment.d.ts +15 -9
  19. package/dist/api/Alignment.js +92 -74
  20. package/dist/api/Alignment.js.map +1 -1
  21. package/dist/api/Common.js +1 -1
  22. package/dist/api/Denoising.d.ts +6 -6
  23. package/dist/api/Denoising.js +23 -23
  24. package/dist/api/Denoising.js.map +1 -1
  25. package/dist/api/LanguageDetection.d.ts +19 -12
  26. package/dist/api/LanguageDetection.js +88 -38
  27. package/dist/api/LanguageDetection.js.map +1 -1
  28. package/dist/api/Recognition.d.ts +16 -6
  29. package/dist/api/Recognition.js +129 -55
  30. package/dist/api/Recognition.js.map +1 -1
  31. package/dist/api/SourceSeparation.d.ts +17 -0
  32. package/dist/api/SourceSeparation.js +61 -0
  33. package/dist/api/SourceSeparation.js.map +1 -0
  34. package/dist/api/Synthesis.d.ts +18 -18
  35. package/dist/api/Synthesis.js +191 -164
  36. package/dist/api/Synthesis.js.map +1 -1
  37. package/dist/api/Translation.d.ts +19 -8
  38. package/dist/api/Translation.js +132 -35
  39. package/dist/api/Translation.js.map +1 -1
  40. package/dist/api/Vad.d.ts +10 -5
  41. package/dist/api/Vad.js +76 -38
  42. package/dist/api/Vad.js.map +1 -1
  43. package/dist/audio/AudioBufferConversion.d.ts +1 -1
  44. package/dist/audio/AudioBufferConversion.js +4 -4
  45. package/dist/audio/AudioPlayer.d.ts +1 -1
  46. package/dist/audio/AudioPlayer.js +26 -26
  47. package/dist/audio/AudioPlayer.js.map +1 -1
  48. package/dist/audio/AudioRecorder.d.ts +1 -1
  49. package/dist/audio/AudioRecorder.js +5 -5
  50. package/dist/audio/AudioUtilities.d.ts +13 -9
  51. package/dist/audio/AudioUtilities.js +86 -24
  52. package/dist/audio/AudioUtilities.js.map +1 -1
  53. package/dist/cli/CLI.d.ts +3 -3
  54. package/dist/cli/CLI.js +271 -162
  55. package/dist/cli/CLI.js.map +1 -1
  56. package/dist/cli/CLIConfigFile.js +8 -8
  57. package/dist/cli/CLILauncher.js +6 -6
  58. package/dist/cli/CLIOptionsSchema.js +2 -2
  59. package/dist/cli/CLIParser.js +5 -5
  60. package/dist/cli/CLIStarter.js +4 -4
  61. package/dist/codecs/FFMpegTranscoder.d.ts +2 -2
  62. package/dist/codecs/FFMpegTranscoder.js +37 -37
  63. package/dist/codecs/FFMpegTranscoder.js.map +1 -1
  64. package/dist/codecs/TIMITCodec.js +5 -5
  65. package/dist/codecs/WaveCodec.d.ts +1 -1
  66. package/dist/codecs/WaveCodec.js +22 -22
  67. package/dist/denoising/RNNoise.d.ts +1 -1
  68. package/dist/denoising/RNNoise.js +9 -9
  69. package/dist/dsp/BiquadFilter.d.ts +3 -2
  70. package/dist/dsp/BiquadFilter.js +18 -11
  71. package/dist/dsp/BiquadFilter.js.map +1 -1
  72. package/dist/dsp/DecayingPeakEstimator.d.ts +16 -0
  73. package/dist/dsp/DecayingPeakEstimator.js +23 -0
  74. package/dist/dsp/DecayingPeakEstimator.js.map +1 -0
  75. package/dist/dsp/FFT.d.ts +8 -4
  76. package/dist/dsp/FFT.js +76 -30
  77. package/dist/dsp/FFT.js.map +1 -1
  78. package/dist/dsp/KWeightingFilter.d.ts +9 -0
  79. package/dist/dsp/KWeightingFilter.js +40 -0
  80. package/dist/dsp/KWeightingFilter.js.map +1 -0
  81. package/dist/dsp/LoudnessEstimator.d.ts +21 -0
  82. package/dist/dsp/LoudnessEstimator.js +47 -0
  83. package/dist/dsp/LoudnessEstimator.js.map +1 -0
  84. package/dist/dsp/MFCC.d.ts +2 -2
  85. package/dist/dsp/MFCC.js +15 -15
  86. package/dist/dsp/MelSpectogram.d.ts +1 -1
  87. package/dist/dsp/MelSpectogram.js +6 -6
  88. package/dist/dsp/Rubberband.d.ts +11 -11
  89. package/dist/dsp/Rubberband.js +27 -27
  90. package/dist/dsp/Sonic.d.ts +1 -1
  91. package/dist/dsp/Sonic.js +3 -3
  92. package/dist/dsp/SpeexResampler.d.ts +1 -1
  93. package/dist/dsp/SpeexResampler.js +2 -2
  94. package/dist/math/VectorMath.d.ts +12 -8
  95. package/dist/math/VectorMath.js +35 -32
  96. package/dist/math/VectorMath.js.map +1 -1
  97. package/dist/nlp/ChineseSegmentation.js +2 -2
  98. package/dist/nlp/CompromiseNLP.js +3 -3
  99. package/dist/nlp/EspeakPhonemizer.js +30 -30
  100. package/dist/nlp/IPA.js +20 -20
  101. package/dist/nlp/JapaneseSegmentation.js +6 -6
  102. package/dist/nlp/Lexicon.d.ts +1 -1
  103. package/dist/nlp/Lexicon.js +7 -7
  104. package/dist/nlp/Segmentation.d.ts +3 -0
  105. package/dist/nlp/Segmentation.js +21 -14
  106. package/dist/nlp/Segmentation.js.map +1 -1
  107. package/dist/nlp/TextNormalizer.js +16 -16
  108. package/dist/recognition/AmazonTranscribeSTT.d.ts +2 -2
  109. package/dist/recognition/AmazonTranscribeSTT.js +13 -14
  110. package/dist/recognition/AmazonTranscribeSTT.js.map +1 -1
  111. package/dist/recognition/AzureCognitiveServicesSTT.js +5 -6
  112. package/dist/recognition/AzureCognitiveServicesSTT.js.map +1 -1
  113. package/dist/recognition/GoogleCloudSTT.d.ts +3 -3
  114. package/dist/recognition/GoogleCloudSTT.js +18 -18
  115. package/dist/recognition/OpenAICloudSTT.d.ts +19 -0
  116. package/dist/recognition/OpenAICloudSTT.js +81 -0
  117. package/dist/recognition/OpenAICloudSTT.js.map +1 -0
  118. package/dist/recognition/SileroSTT.d.ts +2 -2
  119. package/dist/recognition/SileroSTT.js +25 -25
  120. package/dist/recognition/VoskSTT.d.ts +2 -2
  121. package/dist/recognition/VoskSTT.js +8 -8
  122. package/dist/recognition/WhisperCppSTT.d.ts +88 -0
  123. package/dist/recognition/WhisperCppSTT.js +332 -0
  124. package/dist/recognition/WhisperCppSTT.js.map +1 -0
  125. package/dist/recognition/WhisperSTT.d.ts +49 -25
  126. package/dist/recognition/WhisperSTT.js +626 -481
  127. package/dist/recognition/WhisperSTT.js.map +1 -1
  128. package/dist/server/Client.d.ts +1 -1
  129. package/dist/server/Client.js +22 -22
  130. package/dist/server/Server.js +9 -9
  131. package/dist/server/Server.js.map +1 -1
  132. package/dist/server/Worker.d.ts +22 -22
  133. package/dist/server/Worker.js +36 -36
  134. package/dist/server/Worker.js.map +1 -1
  135. package/dist/server/WorkerStarter.js +2 -2
  136. package/dist/source-separation/MDXNetSourceSeparation.d.ts +11 -0
  137. package/dist/source-separation/MDXNetSourceSeparation.js +161 -0
  138. package/dist/source-separation/MDXNetSourceSeparation.js.map +1 -0
  139. package/dist/speech-language-detection/SileroLanguageDetection.d.ts +1 -1
  140. package/dist/speech-language-detection/SileroLanguageDetection.js +7 -7
  141. package/dist/subtitles/Subtitles.d.ts +10 -0
  142. package/dist/subtitles/Subtitles.js +2 -2
  143. package/dist/subtitles/Subtitles.js.map +1 -1
  144. package/dist/synthesis/AwsPollyTTS.d.ts +1 -1
  145. package/dist/synthesis/AwsPollyTTS.js +12 -12
  146. package/dist/synthesis/AzureCognitiveServicesTTS.js +7 -7
  147. package/dist/synthesis/CoquiServerTTS.js +10 -10
  148. package/dist/synthesis/CoquiServerTTS.js.map +1 -1
  149. package/dist/synthesis/ElevenlabsTTS.d.ts +23 -0
  150. package/dist/synthesis/ElevenlabsTTS.js +103 -0
  151. package/dist/synthesis/ElevenlabsTTS.js.map +1 -0
  152. package/dist/synthesis/EspeakTTS.d.ts +6 -5
  153. package/dist/synthesis/EspeakTTS.js +81 -69
  154. package/dist/synthesis/EspeakTTS.js.map +1 -1
  155. package/dist/synthesis/FliteTTS.d.ts +3 -3
  156. package/dist/synthesis/FliteTTS.js +154 -154
  157. package/dist/synthesis/FliteTTS.js.map +1 -1
  158. package/dist/synthesis/GoogleCloudTTS.d.ts +3 -3
  159. package/dist/synthesis/GoogleCloudTTS.js +17 -17
  160. package/dist/synthesis/GoogleCloudTTS.js.map +1 -1
  161. package/dist/synthesis/GoogleTranslateTTS.d.ts +1 -1
  162. package/dist/synthesis/GoogleTranslateTTS.js +103 -103
  163. package/dist/synthesis/MicrosoftEdgeTTS.d.ts +2 -2
  164. package/dist/synthesis/MicrosoftEdgeTTS.js +74 -74
  165. package/dist/synthesis/OpenAICloudTTS.d.ts +13 -0
  166. package/dist/synthesis/OpenAICloudTTS.js +169 -0
  167. package/dist/synthesis/OpenAICloudTTS.js.map +1 -0
  168. package/dist/synthesis/SamTTS.js +3 -3
  169. package/dist/synthesis/SapiTTS.d.ts +3 -3
  170. package/dist/synthesis/SapiTTS.js +26 -26
  171. package/dist/synthesis/StreamlabsPollyTTS.d.ts +2 -2
  172. package/dist/synthesis/StreamlabsPollyTTS.js +27 -27
  173. package/dist/synthesis/SvoxPicoTTS.d.ts +2 -2
  174. package/dist/synthesis/SvoxPicoTTS.js +65 -65
  175. package/dist/synthesis/SvoxPicoTTS.js.map +1 -1
  176. package/dist/synthesis/VitsTTS.d.ts +3 -3
  177. package/dist/synthesis/VitsTTS.js +378 -378
  178. package/dist/synthesis/VitsTTS.js.map +1 -1
  179. package/dist/tests/Test.js +2 -2
  180. package/dist/utilities/Compression.d.ts +5 -0
  181. package/dist/utilities/Compression.js +29 -13
  182. package/dist/utilities/Compression.js.map +1 -1
  183. package/dist/utilities/FileDownloader.d.ts +1 -1
  184. package/dist/utilities/FileDownloader.js +16 -16
  185. package/dist/utilities/FileSystem.js +7 -7
  186. package/dist/utilities/Locale.d.ts +7 -7
  187. package/dist/utilities/Locale.js +15 -15
  188. package/dist/utilities/Logger.js +3 -3
  189. package/dist/utilities/ObjectUtilities.js +19 -19
  190. package/dist/utilities/OpenPromise.js +2 -2
  191. package/dist/utilities/OpenPromise.js.map +1 -1
  192. package/dist/utilities/PackageManager.js +31 -0
  193. package/dist/utilities/PackageManager.js.map +1 -1
  194. package/dist/utilities/PathUtilities.js +8 -8
  195. package/dist/utilities/RandomGenerator.js +2 -2
  196. package/dist/utilities/SmoothEstimator.d.ts +8 -0
  197. package/dist/utilities/SmoothEstimator.js +25 -0
  198. package/dist/utilities/SmoothEstimator.js.map +1 -0
  199. package/dist/utilities/TarballMaker.js +8 -8
  200. package/dist/utilities/Timeline.d.ts +3 -2
  201. package/dist/utilities/Timeline.js +11 -11
  202. package/dist/utilities/Timeline.js.map +1 -1
  203. package/dist/utilities/Timer.js +4 -4
  204. package/dist/utilities/Utilities.d.ts +4 -0
  205. package/dist/utilities/Utilities.js +38 -15
  206. package/dist/utilities/Utilities.js.map +1 -1
  207. package/dist/utilities/WasmMemoryManager.js +7 -7
  208. package/dist/utilities/WebReader.js +23 -23
  209. package/dist/utilities/WikipediaReader.js +2 -2
  210. package/dist/voice-activity-detection/AdaptiveGateVAD.d.ts +28 -0
  211. package/dist/voice-activity-detection/AdaptiveGateVAD.js +138 -0
  212. package/dist/voice-activity-detection/AdaptiveGateVAD.js.map +1 -0
  213. package/dist/voice-activity-detection/SileroVAD.d.ts +1 -1
  214. package/dist/voice-activity-detection/SileroVAD.js +5 -5
  215. package/dist/voice-activity-detection/SileroVAD.js.map +1 -1
  216. package/dist/voice-activity-detection/WebRtcVAD.d.ts +1 -1
  217. package/dist/voice-activity-detection/WebRtcVAD.js +4 -4
  218. package/docs/API.md +29 -11
  219. package/docs/CLI.md +31 -7
  220. package/docs/Contributing.md +38 -0
  221. package/docs/Development.md +91 -19
  222. package/docs/Engines.md +28 -16
  223. package/docs/Licenses.md +4 -1
  224. package/docs/Options.md +159 -79
  225. package/docs/Releases.md +262 -0
  226. package/docs/Server.md +7 -7
  227. package/docs/Tasklist.md +96 -76
  228. package/docs/Technical.md +4 -4
  229. package/package.json +13 -14
  230. package/src/alignment/DTWMfccSequenceAlignment.ts +9 -9
  231. package/src/alignment/DTWSequenceAlignment.ts +2 -2
  232. package/src/alignment/DTWSequenceAlignmentWindowed.ts +3 -3
  233. package/src/alignment/LevenshteinSequenceAlignment.ts +2 -2
  234. package/src/alignment/SpeechAlignment.ts +195 -86
  235. package/src/api/API.ts +14 -13
  236. package/src/api/APIOptions.ts +12 -11
  237. package/src/api/Alignment.ts +148 -92
  238. package/src/api/Common.ts +1 -1
  239. package/src/api/Denoising.ts +28 -28
  240. package/src/api/LanguageDetection.ts +135 -48
  241. package/src/api/Recognition.ts +198 -59
  242. package/src/api/SourceSeparation.ts +99 -0
  243. package/src/api/Synthesis.ts +217 -181
  244. package/src/api/Translation.ts +193 -40
  245. package/src/api/Vad.ts +110 -41
  246. package/src/audio/AudioBufferConversion.ts +4 -4
  247. package/src/audio/AudioPlayer.ts +27 -27
  248. package/src/audio/AudioRecorder.ts +5 -5
  249. package/src/audio/AudioUtilities.ts +107 -24
  250. package/src/cli/CLI.ts +313 -164
  251. package/src/cli/CLIConfigFile.ts +8 -8
  252. package/src/cli/CLILauncher.ts +6 -6
  253. package/src/cli/CLIOptionsSchema.ts +2 -2
  254. package/src/cli/CLIParser.ts +5 -5
  255. package/src/cli/CLIStarter.ts +4 -4
  256. package/src/codecs/FFMpegTranscoder.ts +38 -38
  257. package/src/codecs/TIMITCodec.ts +5 -5
  258. package/src/codecs/WaveCodec.ts +22 -22
  259. package/src/denoising/RNNoise.ts +9 -9
  260. package/src/dsp/BiquadFilter.ts +19 -11
  261. package/src/dsp/DecayingPeakEstimator.ts +35 -0
  262. package/src/dsp/FFT.ts +103 -35
  263. package/src/dsp/KWeightingFilter.ts +43 -0
  264. package/src/dsp/LoudnessEstimator.ts +74 -0
  265. package/src/dsp/MFCC.ts +15 -15
  266. package/src/dsp/MelSpectogram.ts +7 -7
  267. package/src/dsp/Rubberband.ts +38 -38
  268. package/src/dsp/Sonic.ts +4 -4
  269. package/src/dsp/SpeexResampler.ts +2 -2
  270. package/src/math/VectorMath.ts +42 -33
  271. package/src/nlp/ChineseSegmentation.ts +3 -3
  272. package/src/nlp/CompromiseNLP.ts +3 -3
  273. package/src/nlp/EspeakPhonemizer.ts +30 -30
  274. package/src/nlp/IPA.ts +20 -20
  275. package/src/nlp/JapaneseSegmentation.ts +6 -6
  276. package/src/nlp/Lexicon.ts +8 -8
  277. package/src/nlp/Segmentation.ts +23 -14
  278. package/src/nlp/TextNormalizer.ts +16 -16
  279. package/src/recognition/AmazonTranscribeSTT.ts +16 -17
  280. package/src/recognition/AzureCognitiveServicesSTT.ts +8 -6
  281. package/src/recognition/GoogleCloudSTT.ts +21 -21
  282. package/src/recognition/OpenAICloudSTT.ts +142 -0
  283. package/src/recognition/SileroSTT.ts +26 -26
  284. package/src/recognition/VoskSTT.ts +10 -10
  285. package/src/recognition/WhisperCppSTT.ts +555 -0
  286. package/src/recognition/WhisperSTT.ts +760 -507
  287. package/src/server/Client.ts +23 -23
  288. package/src/server/Server.ts +9 -9
  289. package/src/server/Worker.ts +53 -53
  290. package/src/server/WorkerStarter.ts +2 -2
  291. package/src/source-separation/MDXNetSourceSeparation.ts +228 -0
  292. package/src/speech-language-detection/SileroLanguageDetection.ts +8 -8
  293. package/src/subtitles/Subtitles.ts +3 -3
  294. package/src/synthesis/AwsPollyTTS.ts +14 -14
  295. package/src/synthesis/AzureCognitiveServicesTTS.ts +10 -10
  296. package/src/synthesis/CoquiServerTTS.ts +10 -10
  297. package/src/synthesis/ElevenlabsTTS.ts +137 -0
  298. package/src/synthesis/EspeakTTS.ts +90 -71
  299. package/src/synthesis/FliteTTS.ts +157 -157
  300. package/src/synthesis/GoogleCloudTTS.ts +19 -19
  301. package/src/synthesis/GoogleTranslateTTS.ts +104 -104
  302. package/src/synthesis/MicrosoftEdgeTTS.ts +80 -80
  303. package/src/synthesis/OpenAICloudTTS.ts +196 -0
  304. package/src/synthesis/SamTTS.ts +3 -3
  305. package/src/synthesis/SapiTTS.ts +29 -29
  306. package/src/synthesis/StreamlabsPollyTTS.ts +29 -29
  307. package/src/synthesis/SvoxPicoTTS.ts +67 -67
  308. package/src/synthesis/VitsTTS.ts +380 -380
  309. package/src/tests/Test.ts +4 -4
  310. package/src/utilities/Compression.ts +34 -13
  311. package/src/utilities/FileDownloader.ts +19 -19
  312. package/src/utilities/FileSystem.ts +7 -7
  313. package/src/utilities/Locale.ts +22 -22
  314. package/src/utilities/Logger.ts +4 -4
  315. package/src/utilities/ObjectUtilities.ts +19 -19
  316. package/src/utilities/OpenPromise.ts +2 -2
  317. package/src/utilities/PackageManager.ts +40 -0
  318. package/src/utilities/PathUtilities.ts +8 -8
  319. package/src/utilities/RandomGenerator.ts +3 -3
  320. package/src/utilities/SmoothEstimator.ts +35 -0
  321. package/src/utilities/TarballMaker.ts +9 -9
  322. package/src/utilities/Timeline.ts +15 -13
  323. package/src/utilities/Timer.ts +4 -4
  324. package/src/utilities/Utilities.ts +49 -15
  325. package/src/utilities/WasmMemoryManager.ts +7 -7
  326. package/src/utilities/WebReader.ts +23 -23
  327. package/src/utilities/WikipediaReader.ts +2 -2
  328. package/src/voice-activity-detection/AdaptiveGateVAD.ts +202 -0
  329. package/src/voice-activity-detection/SileroVAD.ts +5 -5
  330. package/src/voice-activity-detection/WebRtcVAD.ts +5 -5
  331. package/dist/synthesis/ElevenLabsTTS.d.ts +0 -8
  332. package/dist/synthesis/ElevenLabsTTS.js +0 -82
  333. package/dist/synthesis/ElevenLabsTTS.js.map +0 -1
  334. package/src/synthesis/ElevenLabsTTS.ts +0 -104
package/src/cli/CLI.ts CHANGED
@@ -1,20 +1,20 @@
1
1
  import * as API from '../api/API.js'
2
2
  import { CLIArguments, parseCLIArguments } from './CLIParser.js'
3
- import { convertHtmlToText, formatIntegerWithLeadingZeros, formatListWithQuotedElements, getWithDefault, logToStderr, setupUnhandledExceptionListeners, splitFilenameOnExtendedExtension, stringifyAndFormatJson } from "../utilities/Utilities.js"
4
- import { getOptionTypeFromSchema, SchemaTypeDefinition } from "./CLIOptionsSchema.js"
5
- import { ParsedConfigFile, parseConfigFile, parseJSONConfigFile } from "./CLIConfigFile.js"
3
+ import { convertHtmlToText, formatIntegerWithLeadingZeros, formatListWithQuotedElements, getWithDefault, logToStderr, setupUnhandledExceptionListeners, splitFilenameOnExtendedExtension, stringifyAndFormatJson } from '../utilities/Utilities.js'
4
+ import { getOptionTypeFromSchema, SchemaTypeDefinition } from './CLIOptionsSchema.js'
5
+ import { ParsedConfigFile, parseConfigFile, parseJSONConfigFile } from './CLIConfigFile.js'
6
6
 
7
7
  import chalk from 'chalk'
8
- import { RawAudio, applyGainDecibels, encodeWaveBuffer, getEmptyRawAudio, getRawAudioDuration, normalizeAudioLevel, sliceRawAudioByTime } from "../audio/AudioUtilities.js"
9
- import { SubtitlesConfig, subtitlesToText, subtitlesToTimeline, timelineToSubtitles } from "../subtitles/Subtitles.js"
10
- import { Logger, resetActiveLogger } from "../utilities/Logger.js"
8
+ import { RawAudio, applyGainDecibels, encodeRawAudioToWave, getEmptyRawAudio, getRawAudioDuration, normalizeAudioLevel, sliceRawAudioByTime } from '../audio/AudioUtilities.js'
9
+ import { SubtitlesConfig, subtitlesToText, subtitlesToTimeline, timelineToSubtitles } from '../subtitles/Subtitles.js'
10
+ import { Logger, resetActiveLogger } from '../utilities/Logger.js'
11
11
  import { isMainThread, parentPort } from 'node:worker_threads'
12
- import { encodeFromChannels, getDefaultFFMpegOptionsForSpeech } from "../codecs/FFMpegTranscoder.js"
13
- import path, { parse as parsePath } from "node:path"
14
- import { splitToParagraphs, splitToWords, wordCharacterPattern } from "../nlp/Segmentation.js"
15
- import { playAudioSamples, playAudioWithWordTimeline } from "../audio/AudioPlayer.js"
16
- import { extendDeep } from "../utilities/ObjectUtilities.js"
17
- import { Timeline, TimelineEntry, addTimeOffsetToTimeline, addWordTextOffsetsToTimeline, roundTimelineProperties, wordTimelineToSegmentSentenceTimeline } from "../utilities/Timeline.js"
12
+ import { encodeFromChannels, getDefaultFFMpegOptionsForSpeech } from '../codecs/FFMpegTranscoder.js'
13
+ import path, { parse as parsePath } from 'node:path'
14
+ import { splitToParagraphs, splitToWords, wordCharacterPattern } from '../nlp/Segmentation.js'
15
+ import { playAudioSamples, playAudioWithWordTimeline } from '../audio/AudioPlayer.js'
16
+ import { deepClone, extendDeep } from '../utilities/ObjectUtilities.js'
17
+ import { Timeline, TimelineEntry, addTimeOffsetToTimeline, addWordTextOffsetsToTimeline, roundTimelineProperties, wordTimelineToSegmentSentenceTimeline } from '../utilities/Timeline.js'
18
18
  import { ensureDir, existsSync, readAndParseJsonFile, readFile, readdir, writeFileSafe } from '../utilities/FileSystem.js'
19
19
  import { formatLanguageCodeWithName, getShortLanguageCode } from '../utilities/Locale.js'
20
20
  import { APIOptions } from '../api/APIOptions.js'
@@ -37,7 +37,7 @@ async function startIfInWorkerThread() {
37
37
 
38
38
  const initOpenPromise = new OpenPromise<void>()
39
39
 
40
- parentPort.once("message", (message) => {
40
+ parentPort.once('message', (message) => {
41
41
  if (message.name == 'init') {
42
42
  process.stderr.isTTY = message.stdErrIsTTY
43
43
  process.stderr.hasColors = () => message.hasColors
@@ -62,44 +62,44 @@ export async function start(processArgs: string[]) {
62
62
  let args: CLIArguments
63
63
 
64
64
  try {
65
- const packageData = await readAndParseJsonFile(resolveToModuleRootDir("package.json"))
65
+ const packageData = await readAndParseJsonFile(resolveToModuleRootDir('package.json'))
66
66
 
67
67
  logger.log(chalk.magentaBright(`Echogarden v${packageData.version}\n`))
68
68
 
69
69
  const command = processArgs[0]
70
70
 
71
- if (!command || command == "help") {
72
- logger.log(`Supported operations:\n\n${commandHelp.join("\n")}`)
71
+ if (!command || command == 'help') {
72
+ logger.log(`Supported operations:\n\n${commandHelp.join('\n')}`)
73
73
  process.exit(0)
74
74
  }
75
75
 
76
- if (command == "--help") {
76
+ if (command == '--help') {
77
77
  logger.log(`There's no command called '--help'. Did you mean to run 'echogarden help'?`)
78
78
  process.exit(0)
79
79
  }
80
80
 
81
81
  args = parseCLIArguments(command, processArgs.slice(1))
82
82
 
83
- if (!args.options.has("config")) {
83
+ if (!args.options.has('config')) {
84
84
  const defaultConfigFile = `./${appName}.config`
85
- const defaultJsonConfigFile = defaultConfigFile + ".json"
85
+ const defaultJsonConfigFile = defaultConfigFile + '.json'
86
86
 
87
87
  if (existsSync(defaultConfigFile)) {
88
- args.options.set("config", defaultConfigFile)
88
+ args.options.set('config', defaultConfigFile)
89
89
  } else if (existsSync(defaultJsonConfigFile)) {
90
- args.options.set("config", defaultJsonConfigFile)
90
+ args.options.set('config', defaultJsonConfigFile)
91
91
  }
92
92
  }
93
93
 
94
- if (args.options.has("config")) {
95
- const configFilePath = args.options.get("config")!
96
- args.options.delete("config")
94
+ if (args.options.has('config')) {
95
+ const configFilePath = args.options.get('config')!
96
+ args.options.delete('config')
97
97
 
98
98
  let parsedOptionFile: ParsedConfigFile
99
99
 
100
- if (configFilePath.endsWith(".config")) {
100
+ if (configFilePath.endsWith('.config')) {
101
101
  parsedOptionFile = await parseConfigFile(configFilePath)
102
- } else if (configFilePath.endsWith(".config.json")) {
102
+ } else if (configFilePath.endsWith('.config.json')) {
103
103
  parsedOptionFile = await parseJSONConfigFile(configFilePath)
104
104
  } else {
105
105
  throw new Error(`Specified config file '${configFilePath}' doesn't have a supported extension. Should be either '.config' or '.config.json'`)
@@ -107,8 +107,8 @@ export async function start(processArgs: string[]) {
107
107
 
108
108
  let sectionName = args.command
109
109
 
110
- if (sectionName.startsWith("speak-")) {
111
- sectionName = "speak"
110
+ if (sectionName.startsWith('speak-')) {
111
+ sectionName = 'speak'
112
112
  }
113
113
 
114
114
  const newOptions = parsedOptionFile.get(sectionName) || new Map<string, string>()
@@ -174,7 +174,9 @@ const commandHelp = [
174
174
  `${executableName} ${chalk.magentaBright('detect-voice-activity')} audioFile [output files...] [options...]`,
175
175
  ` Detect voice activity in audio file\n`,
176
176
  `${executableName} ${chalk.magentaBright('denoise')} audioFile [output files...] [options...]`,
177
- ` Denoise audio file\n`,
177
+ ` Apply speech denoising to audio file\n`,
178
+ `${executableName} ${chalk.magentaBright('isolate')} audioFile [output files...] [options...]`,
179
+ ` Extract isolated voice track from an audio file\n`,
178
180
  `${executableName} ${chalk.magentaBright('list-engines')} operation`,
179
181
  ` List available engines for the specified operation\n`,
180
182
  `${executableName} ${chalk.magentaBright('list-voices')} tts-engine [output files...] [options...]`,
@@ -219,17 +221,17 @@ async function startWithArgs(parsedArgs: CLIArguments) {
219
221
  }
220
222
 
221
223
  case 'detect-language': {
222
- await detectLanguage(parsedArgs.commandArgs, parsedArgs.options, "auto")
224
+ await detectLanguage(parsedArgs.commandArgs, parsedArgs.options, 'auto')
223
225
  break
224
226
  }
225
227
 
226
228
  case 'detect-speech-language': {
227
- await detectLanguage(parsedArgs.commandArgs, parsedArgs.options, "speech")
229
+ await detectLanguage(parsedArgs.commandArgs, parsedArgs.options, 'speech')
228
230
  break
229
231
  }
230
232
 
231
233
  case 'detect-text-language': {
232
- await detectLanguage(parsedArgs.commandArgs, parsedArgs.options, "text")
234
+ await detectLanguage(parsedArgs.commandArgs, parsedArgs.options, 'text')
233
235
  break
234
236
  }
235
237
 
@@ -243,6 +245,11 @@ async function startWithArgs(parsedArgs: CLIArguments) {
243
245
  break
244
246
  }
245
247
 
248
+ case 'isolate': {
249
+ await isolate(parsedArgs.commandArgs, parsedArgs.options)
250
+ break
251
+ }
252
+
246
253
  case 'list-engines': {
247
254
  await listEngines(parsedArgs.commandArgs, parsedArgs.options)
248
255
  break
@@ -280,7 +287,7 @@ async function startWithArgs(parsedArgs: CLIArguments) {
280
287
  }
281
288
  }
282
289
 
283
- type SpeakCommand = "speak" | "speak-file" | "speak-url" | "speak-wikipedia"
290
+ type SpeakCommand = 'speak' | 'speak-file' | 'speak-url' | 'speak-wikipedia'
284
291
 
285
292
  async function speak(command: SpeakCommand, commandArgs: string[], cliOptions: Map<string, string>) {
286
293
  const logger = new Logger()
@@ -289,13 +296,13 @@ async function speak(command: SpeakCommand, commandArgs: string[], cliOptions: M
289
296
  const outputFilenames = commandArgs.slice(1)
290
297
 
291
298
  if (mainArg == undefined) {
292
- if (command == "speak") {
299
+ if (command == 'speak') {
293
300
  throw new Error(`'speak' requires an argument containing the text to speak.`)
294
- } else if (command == "speak-file") {
301
+ } else if (command == 'speak-file') {
295
302
  throw new Error(`'speak-file' requires an argument containing the file to speak.`)
296
- } else if (command == "speak-url") {
303
+ } else if (command == 'speak-url') {
297
304
  throw new Error(`'speak-url' requires an argument containing the url to speak.`)
298
- } else if (command == "speak-wikipedia") {
305
+ } else if (command == 'speak-wikipedia') {
299
306
  throw new Error(`'speak-wikipedia' requires an argument containing the name of the Wikipedia article to speak.`)
300
307
  }
301
308
 
@@ -310,10 +317,10 @@ async function speak(command: SpeakCommand, commandArgs: string[], cliOptions: M
310
317
  cliOptions.set('play', `${outputFilenames.length == 0}`)
311
318
  }
312
319
 
313
- const options: API.SynthesisOptions = await cliOptionsMapToOptionsObject(cliOptions, "SynthesisOptions", additionalOptionsSchema)
320
+ const options: API.SynthesisOptions = await cliOptionsMapToOptionsObject(cliOptions, 'SynthesisOptions', additionalOptionsSchema)
314
321
 
315
322
  const allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
316
- const { includesPartPattern } = await checkOutputFilenames(outputFilenames, true, true, true)
323
+ const { includesPlaceholderPattern } = await checkOutputFilenames(outputFilenames, true, true, true)
317
324
 
318
325
  let plainText: string | undefined = undefined
319
326
  let textSegments: string[]
@@ -321,7 +328,7 @@ async function speak(command: SpeakCommand, commandArgs: string[], cliOptions: M
321
328
  const plainTextParagraphBreaks = options.plainText?.paragraphBreaks || API.defaultSynthesisOptions.plainText!.paragraphBreaks!
322
329
  const plainTextWhitespace = options.plainText?.whitespace || API.defaultSynthesisOptions.plainText!.whitespace!
323
330
 
324
- if (command == "speak") {
331
+ if (command == 'speak') {
325
332
  if (options.ssml) {
326
333
  textSegments = [mainArg]
327
334
  } else {
@@ -329,7 +336,7 @@ async function speak(command: SpeakCommand, commandArgs: string[], cliOptions: M
329
336
  }
330
337
 
331
338
  plainText = mainArg
332
- } else if (command == "speak-file") {
339
+ } else if (command == 'speak-file') {
333
340
  const sourceFile = mainArg
334
341
 
335
342
  if (!existsSync(sourceFile)) {
@@ -339,59 +346,59 @@ async function speak(command: SpeakCommand, commandArgs: string[], cliOptions: M
339
346
  const sourceFileExtension = getLowercaseFileExtension(sourceFile)
340
347
  const fileContent = await readFile(sourceFile, { encoding: 'utf-8' })
341
348
 
342
- if (options.ssml && sourceFileExtension != "xml" && sourceFileExtension != "ssml") {
349
+ if (options.ssml && sourceFileExtension != 'xml' && sourceFileExtension != 'ssml') {
343
350
  throw new Error(`SSML option is set, but source file doesn't have an 'xml' or 'ssml' extension.`)
344
351
  }
345
352
 
346
- if (sourceFileExtension == "txt") {
353
+ if (sourceFileExtension == 'txt') {
347
354
  textSegments = splitToParagraphs(fileContent, plainTextParagraphBreaks, plainTextWhitespace)
348
355
 
349
356
  plainText = fileContent
350
- } else if (sourceFileExtension == "html" || sourceFileExtension == "htm") {
357
+ } else if (sourceFileExtension == 'html' || sourceFileExtension == 'htm') {
351
358
  const textContent = await convertHtmlToText(fileContent)
352
359
  textSegments = splitToParagraphs(textContent, 'single', 'preserve')
353
- } else if (sourceFileExtension == "srt" || sourceFileExtension == "vtt") {
360
+ } else if (sourceFileExtension == 'srt' || sourceFileExtension == 'vtt') {
354
361
  const fileContent = await readFile(sourceFile, { encoding: 'utf-8' })
355
362
  textSegments = subtitlesToTimeline(fileContent).map(entry => entry.text)
356
- } else if (sourceFileExtension == "xml" || sourceFileExtension == "ssml") {
363
+ } else if (sourceFileExtension == 'xml' || sourceFileExtension == 'ssml') {
357
364
  options.ssml = true
358
365
  textSegments = [fileContent]
359
366
  } else {
360
367
  throw new Error(`'speak-file' only supports inputs with extensions 'txt', 'html', 'htm', 'xml', 'ssml', 'srt', 'vtt'`)
361
368
  }
362
- } else if (command == "speak-url") {
369
+ } else if (command == 'speak-url') {
363
370
  if (options.ssml) {
364
371
  throw new Error(`speak-url doesn't provide SSML inputs`)
365
372
  }
366
373
 
367
374
  const url = mainArg
368
375
 
369
- if (!url.startsWith("http://") && !url.startsWith("https://")) {
376
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
370
377
  throw new Error(`'${url}' is not a valid URL. Only 'http://' and 'https://' protocols are supported`)
371
378
  }
372
379
 
373
- const { fetchDocumentText } = await import("../utilities/WebReader.js")
380
+ const { fetchDocumentText } = await import('../utilities/WebReader.js')
374
381
  const textContent = await fetchDocumentText(url)
375
382
 
376
383
  textSegments = splitToParagraphs(textContent, 'single', 'preserve')
377
- } else if (command == "speak-wikipedia") {
384
+ } else if (command == 'speak-wikipedia') {
378
385
  if (options.ssml) {
379
386
  throw new Error(`speak-wikipedia doesn't provide SSML inputs`)
380
387
  }
381
388
 
382
- const { parseWikipediaArticle } = await import("../utilities/WikipediaReader.js")
389
+ const { parseWikipediaArticle } = await import('../utilities/WikipediaReader.js')
383
390
  if (!options.language) {
384
- options.language = "en"
391
+ options.language = 'en'
385
392
  }
386
393
 
387
394
  textSegments = await parseWikipediaArticle(mainArg, getShortLanguageCode(options.language))
388
395
  } else {
389
- throw new Error("Invalid command")
396
+ throw new Error('Invalid command')
390
397
  }
391
398
 
392
399
  async function onSegment(segmentData: API.SynthesisSegmentEventData) {
393
- if (includesPartPattern) {
394
- logger.start("Writing output files for segment")
400
+ if (includesPlaceholderPattern) {
401
+ logger.start('Write output files for segment')
395
402
  }
396
403
 
397
404
  await writeOutputFilesForSegment(outputFilenames, segmentData.index, segmentData.total, segmentData.audio as RawAudio, segmentData.timeline, segmentData.transcript, segmentData.language, allowOverwrite)
@@ -420,18 +427,18 @@ async function speak(command: SpeakCommand, commandArgs: string[], cliOptions: M
420
427
  }
421
428
 
422
429
  if (outputFilenames.length > 0) {
423
- logger.start("\nWriting output files")
430
+ logger.start('\nWrite output files')
424
431
  }
425
432
 
426
- for (const outputFile of outputFilenames) {
427
- const partPatternMatch = outputFile.match(segmentFilenamePattern)
433
+ for (const outputFilename of outputFilenames) {
434
+ const placeholderPatternMatch = outputFilename.match(filenamePlaceholderPattern)
428
435
 
429
- if (partPatternMatch) {
436
+ if (placeholderPatternMatch) {
430
437
  continue
431
438
  }
432
439
 
433
- const fileSaver = getFileSaver(outputFile, allowOverwrite)
434
- await fileSaver(synthesizedAudio as RawAudio, timeline, textSegments.join("\n\n"), options.subtitles)
440
+ const fileSaver = getFileSaver(outputFilename, allowOverwrite)
441
+ await fileSaver(synthesizedAudio as RawAudio, timeline, textSegments.join('\n\n'), options.subtitles)
435
442
  }
436
443
 
437
444
  logger.end()
@@ -459,35 +466,45 @@ async function transcribe(commandArgs: string[], cliOptions: Map<string, string>
459
466
  cliOptions.set('play', `${outputFilenames.length == 0}`)
460
467
  }
461
468
 
462
- const options: API.RecognitionOptions = await cliOptionsMapToOptionsObject(cliOptions, "RecognitionOptions", additionalOptionsSchema)
469
+ const options: API.RecognitionOptions = await cliOptionsMapToOptionsObject(cliOptions, 'RecognitionOptions', additionalOptionsSchema)
463
470
 
464
471
  const allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
465
- const { includesPartPattern } = await checkOutputFilenames(outputFilenames, true, true, true)
472
+ const { includesPlaceholderPattern } = await checkOutputFilenames(outputFilenames, true, true, true)
466
473
 
467
- const { transcript, timeline, wordTimeline, inputRawAudio, language } = await API.recognize(sourceFilename, options)
474
+ const { transcript, timeline, wordTimeline, language, inputRawAudio, isolatedRawAudio, backgroundRawAudio } = await API.recognize(sourceFilename, options)
468
475
 
469
476
  if (outputFilenames.length > 0) {
470
- logger.start("\nWriting output files")
477
+ logger.start('\nWrite output files')
471
478
  }
472
479
 
473
- for (const outputFile of outputFilenames) {
474
- const partPatternMatch = outputFile.match(segmentFilenamePattern)
480
+ for (const outputFilename of outputFilenames) {
481
+ const partPatternMatch = outputFilename.match(filenamePlaceholderPattern)
475
482
 
476
483
  if (partPatternMatch) {
477
484
  continue
478
485
  }
479
486
 
480
- const fileSaver = getFileSaver(outputFile, allowOverwrite)
487
+ const fileSaver = getFileSaver(outputFilename, allowOverwrite)
481
488
 
482
489
  await fileSaver(inputRawAudio, timeline, transcript, options.subtitles)
490
+
491
+ await writeSourceSeparationOutputIfNeeded(outputFilename, isolatedRawAudio, backgroundRawAudio, allowOverwrite, true)
483
492
  }
484
493
 
485
494
  logger.end()
486
495
 
487
496
  if ((options as any).play) {
488
- const normalizedAudio = normalizeAudioLevel(inputRawAudio)
497
+ let audioToPlay: RawAudio
498
+
499
+ if (isolatedRawAudio) {
500
+ audioToPlay = isolatedRawAudio
501
+ } else {
502
+ audioToPlay = inputRawAudio
503
+ }
504
+
505
+ const normalizedAudioToPlay = normalizeAudioLevel(audioToPlay)
489
506
 
490
- await playAudioWithWordTimeline(normalizedAudio, wordTimeline, transcript)
507
+ await playAudioWithWordTimeline(normalizedAudioToPlay, wordTimeline, transcript)
491
508
  }
492
509
  }
493
510
 
@@ -520,11 +537,11 @@ async function align(commandArgs: string[], cliOptions: Map<string, string>) {
520
537
 
521
538
  let text: string
522
539
 
523
- if (referenceFileExtension == "txt") {
540
+ if (referenceFileExtension == 'txt') {
524
541
  text = fileContent
525
- } else if (referenceFileExtension == "html" || referenceFileExtension == "htm") {
542
+ } else if (referenceFileExtension == 'html' || referenceFileExtension == 'htm') {
526
543
  text = await convertHtmlToText(fileContent)
527
- } else if (referenceFileExtension == "srt" || referenceFileExtension == "vtt") {
544
+ } else if (referenceFileExtension == 'srt' || referenceFileExtension == 'vtt') {
528
545
  text = subtitlesToText(fileContent)
529
546
  } else {
530
547
  throw new Error(`align only supports reference files with extensions 'txt', 'html', 'htm', 'srt' or 'vtt'`)
@@ -538,18 +555,18 @@ async function align(commandArgs: string[], cliOptions: Map<string, string>) {
538
555
  cliOptions.set('play', `${outputFilenames.length == 0}`)
539
556
  }
540
557
 
541
- const options: API.AlignmentOptions = await cliOptionsMapToOptionsObject(cliOptions, "AlignmentOptions", additionalOptionsSchema)
558
+ const options: API.AlignmentOptions = await cliOptionsMapToOptionsObject(cliOptions, 'AlignmentOptions', additionalOptionsSchema)
542
559
 
543
560
  const allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
544
- const { includesPartPattern } = await checkOutputFilenames(outputFilenames, true, true, true)
561
+ const { includesPlaceholderPattern } = await checkOutputFilenames(outputFilenames, true, true, true)
545
562
 
546
- const { timeline, wordTimeline, transcript, language, inputRawAudio } = await API.align(audioFilename, text, options)
563
+ const { timeline, wordTimeline, transcript, language, inputRawAudio, isolatedRawAudio, backgroundRawAudio } = await API.align(audioFilename, text, options)
547
564
 
548
565
  if (outputFilenames.length > 0) {
549
- logger.start("\nWriting output files")
566
+ logger.start('\nWrite output files')
550
567
  }
551
568
 
552
- if (includesPartPattern) {
569
+ if (includesPlaceholderPattern) {
553
570
  for (let segmentIndex = 0; segmentIndex < timeline.length; segmentIndex++) {
554
571
  const segmentEntry = timeline[segmentIndex]
555
572
  const segmentAudio = sliceRawAudioByTime(inputRawAudio, segmentEntry.startTime, segmentEntry.endTime)
@@ -559,24 +576,34 @@ async function align(commandArgs: string[], cliOptions: Map<string, string>) {
559
576
  }
560
577
  }
561
578
 
562
- for (const outputFile of outputFilenames) {
563
- const partPatternMatch = outputFile.match(segmentFilenamePattern)
579
+ for (const outputFilename of outputFilenames) {
580
+ const partPatternMatch = outputFilename.match(filenamePlaceholderPattern)
564
581
 
565
582
  if (partPatternMatch) {
566
583
  continue
567
584
  }
568
585
 
569
- const fileSaver = getFileSaver(outputFile, allowOverwrite)
586
+ const fileSaver = getFileSaver(outputFilename, allowOverwrite)
570
587
 
571
588
  await fileSaver(inputRawAudio, timeline, transcript, options.subtitles)
589
+
590
+ await writeSourceSeparationOutputIfNeeded(outputFilename, isolatedRawAudio, backgroundRawAudio, allowOverwrite, true)
572
591
  }
573
592
 
574
593
  logger.end()
575
594
 
576
595
  if ((options as any).play) {
577
- const normalizedAudio = normalizeAudioLevel(inputRawAudio)
596
+ let audioToPlay: RawAudio
578
597
 
579
- await playAudioWithWordTimeline(normalizedAudio, wordTimeline, transcript)
598
+ if (isolatedRawAudio) {
599
+ audioToPlay = isolatedRawAudio
600
+ } else {
601
+ audioToPlay = inputRawAudio
602
+ }
603
+
604
+ const normalizedAudioToPlay = normalizeAudioLevel(audioToPlay)
605
+
606
+ await playAudioWithWordTimeline(normalizedAudioToPlay, wordTimeline, transcript)
580
607
  }
581
608
  }
582
609
 
@@ -602,40 +629,74 @@ async function translateSpeech(commandArgs: string[], cliOptions: Map<string, st
602
629
  cliOptions.set('play', `${outputFilenames.length == 0}`)
603
630
  }
604
631
 
605
- const options: API.SpeechTranslationOptions = await cliOptionsMapToOptionsObject(cliOptions, "SpeechTranslationOptions", additionalOptionsSchema)
632
+ const options: API.SpeechTranslationOptions = await cliOptionsMapToOptionsObject(cliOptions, 'SpeechTranslationOptions', additionalOptionsSchema)
606
633
 
607
634
  const allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
608
635
 
609
636
  await checkOutputFilenames(outputFilenames, true, true, true)
610
637
 
611
- const { transcript, timeline, wordTimeline, sourceLanguage, targetLanguage, inputRawAudio } = await API.translateSpeech(inputFilename, options)
638
+ const { transcript, timeline, wordTimeline, sourceLanguage, targetLanguage, inputRawAudio, isolatedRawAudio, backgroundRawAudio } = await API.translateSpeech(inputFilename, options)
612
639
 
613
640
  if (outputFilenames.length > 0) {
614
- logger.start("\nWriting output files")
641
+ logger.start('\nWrite output files')
615
642
  }
616
643
 
617
- for (const outputFile of outputFilenames) {
618
- const partPatternMatch = outputFile.match(segmentFilenamePattern)
644
+ for (const outputFilename of outputFilenames) {
645
+ const partPatternMatch = outputFilename.match(filenamePlaceholderPattern)
619
646
 
620
647
  if (partPatternMatch) {
621
648
  continue
622
649
  }
623
650
 
624
- const fileSaver = getFileSaver(outputFile, allowOverwrite)
651
+ const fileSaver = getFileSaver(outputFilename, allowOverwrite)
625
652
 
626
653
  await fileSaver(inputRawAudio, timeline, transcript, options.subtitles)
654
+
655
+ await writeSourceSeparationOutputIfNeeded(outputFilename, isolatedRawAudio, backgroundRawAudio, allowOverwrite, true)
627
656
  }
628
657
 
629
658
  logger.end()
630
659
 
631
660
  if ((options as any).play) {
632
- const normalizedAudio = normalizeAudioLevel(inputRawAudio)
661
+ let audioToPlay: RawAudio
662
+
663
+ if (isolatedRawAudio) {
664
+ audioToPlay = isolatedRawAudio
665
+ } else {
666
+ audioToPlay = inputRawAudio
667
+ }
668
+
669
+ const normalizedAudioToPlay = normalizeAudioLevel(audioToPlay)
633
670
 
634
- await playAudioWithWordTimeline(normalizedAudio, wordTimeline, transcript)
671
+ let transcriptToPlay: string
672
+ let timelineToPlay: Timeline
673
+
674
+ if (wordTimeline) {
675
+ transcriptToPlay = transcript
676
+ timelineToPlay = wordTimeline
677
+ } else {
678
+ timelineToPlay = timeline.map(entry => ({
679
+ type: 'word',
680
+ text: entry.text.trim(),
681
+ startTime: entry.startTime,
682
+ endTime: entry.endTime
683
+ }))
684
+
685
+ transcriptToPlay = ''
686
+
687
+ for (const entry of timelineToPlay) {
688
+ transcriptToPlay += entry.text
689
+ transcriptToPlay += ' '
690
+ }
691
+
692
+ transcriptToPlay = transcriptToPlay.trim()
693
+ }
694
+
695
+ await playAudioWithWordTimeline(normalizedAudioToPlay, timelineToPlay, transcriptToPlay)
635
696
  }
636
697
  }
637
698
 
638
- async function detectLanguage(commandArgs: string[], cliOptions: Map<string, string>, mode: "speech" | "text" | "auto") {
699
+ async function detectLanguage(commandArgs: string[], cliOptions: Map<string, string>, mode: 'speech' | 'text' | 'auto') {
639
700
  const logger = new Logger()
640
701
 
641
702
  const inputFilePath = commandArgs[0]
@@ -649,13 +710,13 @@ async function detectLanguage(commandArgs: string[], cliOptions: Map<string, str
649
710
  additionalOptionsSchema.set('overwrite', { type: 'boolean' })
650
711
 
651
712
  const inputFileExtension = getLowercaseFileExtension(inputFilePath)
652
- const supportedInputTextFormats = ["txt", "srt", "vtt"]
713
+ const supportedInputTextFormats = ['txt', 'srt', 'vtt']
653
714
 
654
715
  let results: API.LanguageDetectionResults
655
716
 
656
717
  let allowOverwrite: boolean
657
718
 
658
- if (mode == "text" || (mode == "auto" && supportedInputTextFormats.includes(inputFileExtension))) {
719
+ if (mode == 'text' || (mode == 'auto' && supportedInputTextFormats.includes(inputFileExtension))) {
659
720
  if (inputFilePath == undefined) {
660
721
  throw new Error(`detect-text-language requires an argument containing the input file path.`)
661
722
  }
@@ -664,14 +725,14 @@ async function detectLanguage(commandArgs: string[], cliOptions: Map<string, str
664
725
  throw new Error(`'detect-text-language' doesn't support input file extension '${inputFileExtension}'`)
665
726
  }
666
727
 
667
- const options: API.TextLanguageDetectionOptions = await cliOptionsMapToOptionsObject(cliOptions, "TextLanguageDetectionOptions", additionalOptionsSchema)
728
+ const options: API.TextLanguageDetectionOptions = await cliOptionsMapToOptionsObject(cliOptions, 'TextLanguageDetectionOptions', additionalOptionsSchema)
668
729
  allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
669
730
 
670
731
  await checkOutputFilenames(outputFilenames, false, true, false)
671
732
 
672
- let text = await readFile(inputFilePath, { encoding: "utf-8" })
733
+ let text = await readFile(inputFilePath, { encoding: 'utf-8' })
673
734
 
674
- if (inputFileExtension == "srt" || inputFileExtension == "vtt") {
735
+ if (inputFileExtension == 'srt' || inputFileExtension == 'vtt') {
675
736
  text = subtitlesToText(text)
676
737
  }
677
738
 
@@ -683,7 +744,7 @@ async function detectLanguage(commandArgs: string[], cliOptions: Map<string, str
683
744
  throw new Error(`detect-speech-language requires an argument containing the input audio file path.`)
684
745
  }
685
746
 
686
- const options: API.SpeechLanguageDetectionOptions = await cliOptionsMapToOptionsObject(cliOptions, "SpeechLanguageDetectionOptions", additionalOptionsSchema)
747
+ const options: API.SpeechLanguageDetectionOptions = await cliOptionsMapToOptionsObject(cliOptions, 'SpeechLanguageDetectionOptions', additionalOptionsSchema)
687
748
  allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
688
749
 
689
750
  await checkOutputFilenames(outputFilenames, false, true, false)
@@ -694,19 +755,19 @@ async function detectLanguage(commandArgs: string[], cliOptions: Map<string, str
694
755
  }
695
756
 
696
757
  if (outputFilenames.length > 0) {
697
- logger.start("\nWriting output files")
758
+ logger.start('\nWrite output files')
698
759
 
699
- const resultsAsText = results.map(result => `${formatLanguageCodeWithName(result.language)}: ${result.probability.toFixed(5)}`).join("\n")
760
+ const resultsAsText = results.map(result => `${formatLanguageCodeWithName(result.language)}: ${result.probability.toFixed(5)}`).join('\n')
700
761
 
701
- for (const outputFile of outputFilenames) {
702
- const fileSaver = getFileSaver(outputFile, allowOverwrite)
762
+ for (const outputFilename of outputFilenames) {
763
+ const fileSaver = getFileSaver(outputFilename, allowOverwrite)
703
764
 
704
765
  await fileSaver(getEmptyRawAudio(0, 0), results as any, resultsAsText)
705
766
  }
706
767
  } else {
707
- const resultsAsText = results.slice(0, 10).map(result => `${formatLanguageCodeWithName(result.language)}: ${result.probability.toFixed(5)}`).join("\n")
768
+ const resultsAsText = results.slice(0, 10).map(result => `${formatLanguageCodeWithName(result.language)}: ${result.probability.toFixed(5)}`).join('\n')
708
769
 
709
- logger.log("")
770
+ logger.log('')
710
771
  logger.log(resultsAsText)
711
772
  }
712
773
 
@@ -735,28 +796,40 @@ async function detectVoiceActivity(commandArgs: string[], cliOptions: Map<string
735
796
  cliOptions.set('play', `${outputFilenames.length == 0}`)
736
797
  }
737
798
 
738
- const options: API.VADOptions = await cliOptionsMapToOptionsObject(cliOptions, "VADOptions", additionalOptionsSchema)
799
+ const options: API.VADOptions = await cliOptionsMapToOptionsObject(cliOptions, 'VADOptions', additionalOptionsSchema)
739
800
 
740
801
  const allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
741
802
 
742
- await checkOutputFilenames(outputFilenames, true, true, false)
803
+ await checkOutputFilenames(outputFilenames, true, true, true)
743
804
 
744
- const { timeline, inputRawAudio } = await API.detectVoiceActivity(audioFilename, options)
805
+ let { timeline, verboseTimeline, inputRawAudio, croppedRawAudio } = await API.detectVoiceActivity(audioFilename, options)
745
806
 
746
807
  if (outputFilenames.length > 0) {
747
- logger.start("\nWriting output files")
808
+ logger.start('\nWrite output files')
748
809
  }
749
810
 
750
- for (const outputFile of outputFilenames) {
751
- const partPatternMatch = outputFile.match(segmentFilenamePattern)
811
+ for (const outputFilename of outputFilenames) {
812
+ const partPatternMatch = outputFilename.match(filenamePlaceholderPattern)
752
813
 
753
814
  if (partPatternMatch) {
754
815
  continue
755
816
  }
756
817
 
757
- const fileSaver = getFileSaver(outputFile, allowOverwrite)
818
+ const fileSaver = getFileSaver(outputFilename, allowOverwrite)
819
+
820
+ await fileSaver(inputRawAudio, timeline, '', { maxAddedDuration: 0 })
821
+
822
+ const fileExtension = getLowercaseFileExtension(outputFilename)
823
+
824
+ if (supportedOutputMediaFileExtensions.includes(fileExtension)) {
825
+ const pathWithoutExtension = outputFilename.substring(0, outputFilename.lastIndexOf('.'))
826
+
827
+ const isolatedOutputFilePath = `${pathWithoutExtension}.cropped.${fileExtension}`
758
828
 
759
- await fileSaver(inputRawAudio, timeline, "")
829
+ const fileSaver = getFileSaver(isolatedOutputFilePath, allowOverwrite)
830
+
831
+ await fileSaver(croppedRawAudio, [], '')
832
+ }
760
833
  }
761
834
 
762
835
  logger.end()
@@ -764,8 +837,8 @@ async function detectVoiceActivity(commandArgs: string[], cliOptions: Map<string
764
837
  if ((options as any).play) {
765
838
  const normalizedAudio = normalizeAudioLevel(inputRawAudio)
766
839
 
767
- const timelineToPlay = timeline.map(entry => {
768
- return {...entry, type: "word" } as TimelineEntry
840
+ const timelineToPlay = verboseTimeline.map(entry => {
841
+ return {...entry, type: 'word' } as TimelineEntry
769
842
  })
770
843
 
771
844
  await playAudioWithWordTimeline(normalizedAudio, timelineToPlay)
@@ -794,7 +867,7 @@ async function denoise(commandArgs: string[], cliOptions: Map<string, string>) {
794
867
  cliOptions.set('play', `${outputFilenames.length == 0}`)
795
868
  }
796
869
 
797
- const options: API.DenoisingOptions = await cliOptionsMapToOptionsObject(cliOptions, "DenoisingOptions", additionalOptionsSchema)
870
+ const options: API.DenoisingOptions = await cliOptionsMapToOptionsObject(cliOptions, 'DenoisingOptions', additionalOptionsSchema)
798
871
 
799
872
  const allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
800
873
 
@@ -803,13 +876,13 @@ async function denoise(commandArgs: string[], cliOptions: Map<string, string>) {
803
876
  const { denoisedAudio } = await API.denoise(audioFilename, options)
804
877
 
805
878
  if (outputFilenames.length > 0) {
806
- logger.start("\nWriting output files")
879
+ logger.start('\nWrite output files')
807
880
  }
808
881
 
809
- for (const filename of outputFilenames) {
810
- const fileSaver = getFileSaver(filename, allowOverwrite)
882
+ for (const outputFilename of outputFilenames) {
883
+ const fileSaver = getFileSaver(outputFilename, allowOverwrite)
811
884
 
812
- await fileSaver(denoisedAudio, [], "")
885
+ await fileSaver(denoisedAudio, [], '')
813
886
  }
814
887
 
815
888
  logger.end()
@@ -819,6 +892,51 @@ async function denoise(commandArgs: string[], cliOptions: Map<string, string>) {
819
892
  }
820
893
  }
821
894
 
895
+ async function isolate(commandArgs: string[], cliOptions: Map<string, string>) {
896
+ const logger = new Logger()
897
+
898
+ const audioFilename = commandArgs[0]
899
+ const outputFilenames = commandArgs.slice(1)
900
+
901
+ if (audioFilename == undefined) {
902
+ throw new Error(`'isolate' requires an argument containing the audio file path.`)
903
+ }
904
+
905
+ if (!existsSync(audioFilename)) {
906
+ throw new Error(`The given source audio file '${audioFilename}' was not found.`)
907
+ }
908
+
909
+ const additionalOptionsSchema = new Map<string, SchemaTypeDefinition>()
910
+ additionalOptionsSchema.set('play', { type: 'boolean' })
911
+ additionalOptionsSchema.set('overwrite', { type: 'boolean' })
912
+
913
+ if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
914
+ cliOptions.set('play', `${outputFilenames.length == 0}`)
915
+ }
916
+
917
+ const options: API.SourceSeparationOptions = await cliOptionsMapToOptionsObject(cliOptions, 'SourceSeparationOptions', additionalOptionsSchema)
918
+
919
+ const allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
920
+
921
+ await checkOutputFilenames(outputFilenames, true, false, false)
922
+
923
+ const { inputRawAudio, isolatedRawAudio, backgroundRawAudio } = await API.isolate(audioFilename, options)
924
+
925
+ if (outputFilenames.length > 0) {
926
+ logger.start('\nWrite output files')
927
+ }
928
+
929
+ for (let outputFilename of outputFilenames) {
930
+ await writeSourceSeparationOutputIfNeeded(outputFilename, isolatedRawAudio, backgroundRawAudio, allowOverwrite, false)
931
+ }
932
+
933
+ logger.end()
934
+
935
+ if ((options as any).play) {
936
+ await playAudioSamples(isolatedRawAudio)
937
+ }
938
+ }
939
+
822
940
  async function listEngines(commandArgs: string[], cliOptions: Map<string, string>) {
823
941
  const logger = new Logger()
824
942
 
@@ -840,6 +958,12 @@ async function listEngines(commandArgs: string[], cliOptions: Map<string, string
840
958
  break
841
959
  }
842
960
 
961
+ case 'list-voices': {
962
+ engines = API.synthesisEngines
963
+
964
+ break
965
+ }
966
+
843
967
  case 'transcribe': {
844
968
  engines = API.recognitionEngines
845
969
 
@@ -888,8 +1012,8 @@ async function listEngines(commandArgs: string[], cliOptions: Map<string, string
888
1012
  break
889
1013
  }
890
1014
 
891
- case 'list-voices': {
892
- engines = API.synthesisEngines
1015
+ case 'isolate': {
1016
+ engines = API.sourceSeparationEngines
893
1017
 
894
1018
  break
895
1019
  }
@@ -913,7 +1037,7 @@ async function listEngines(commandArgs: string[], cliOptions: Map<string, string
913
1037
  logger.logTitledMessage('Type', engine.type)
914
1038
 
915
1039
  if (index < engines.length - 1) {
916
- logger.log("")
1040
+ logger.log('')
917
1041
  }
918
1042
  }
919
1043
  }
@@ -926,9 +1050,9 @@ async function listTTSVoices(commandArgs: string[], cliOptions: Map<string, stri
926
1050
 
927
1051
  if (!targetEngine) {
928
1052
  const optionsSchema = await getOptionsSchema()
929
- const { enum: ttsEnginesEnum } = getOptionTypeFromSchema(["VoiceListRequestOptions", "engine"], optionsSchema)
1053
+ const { enum: ttsEnginesEnum } = getOptionTypeFromSchema(['VoiceListRequestOptions', 'engine'], optionsSchema)
930
1054
 
931
- throw new Error(`list-voices requires an argument specifying one of these supported engines:\n${ttsEnginesEnum!.join(", ")}`)
1055
+ throw new Error(`list-voices requires an argument specifying one of these supported engines:\n${ttsEnginesEnum!.join(', ')}`)
932
1056
  }
933
1057
 
934
1058
  const additionalOptionsSchema = new Map<string, SchemaTypeDefinition>()
@@ -936,7 +1060,7 @@ async function listTTSVoices(commandArgs: string[], cliOptions: Map<string, stri
936
1060
 
937
1061
  cliOptions.set('engine', targetEngine)
938
1062
 
939
- const options: API.VoiceListRequestOptions = await cliOptionsMapToOptionsObject(cliOptions, "VoiceListRequestOptions", additionalOptionsSchema)
1063
+ const options: API.VoiceListRequestOptions = await cliOptionsMapToOptionsObject(cliOptions, 'VoiceListRequestOptions', additionalOptionsSchema)
940
1064
 
941
1065
  const allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
942
1066
 
@@ -946,7 +1070,7 @@ async function listTTSVoices(commandArgs: string[], cliOptions: Map<string, stri
946
1070
 
947
1071
  const voiceListText = voiceList.map(entry => {
948
1072
  const nameText = entry.name
949
- const languagesNamesText = entry.languages.map(language => formatLanguageCodeWithName(language)).join(", ")
1073
+ const languagesNamesText = entry.languages.map(language => formatLanguageCodeWithName(language)).join(', ')
950
1074
  const genderText = entry.gender
951
1075
 
952
1076
  let entryText = `${chalk.cyanBright('Identifier')}: ${chalk.magentaBright(nameText)}\n${chalk.cyanBright('Languages')}: ${languagesNamesText}\n${chalk.cyanBright('Gender')}: ${genderText}`
@@ -958,10 +1082,10 @@ async function listTTSVoices(commandArgs: string[], cliOptions: Map<string, stri
958
1082
  }
959
1083
 
960
1084
  return entryText
961
- }).join("\n\n")
1085
+ }).join('\n\n')
962
1086
 
963
1087
  if (outputFilenames.length > 0) {
964
- logger.start("\nWriting output files")
1088
+ logger.start('\nWrite output files')
965
1089
 
966
1090
  for (const filename of outputFilenames) {
967
1091
  const fileSaver = getFileSaver(filename, allowOverwrite)
@@ -982,7 +1106,7 @@ async function installPackages(commandArgs: string[], cliOptions: Map<string, st
982
1106
  const logger = new Logger()
983
1107
 
984
1108
  if (commandArgs.length == 0) {
985
- throw new Error("No package names specified")
1109
+ throw new Error('No package names specified')
986
1110
  }
987
1111
 
988
1112
  const failedPackageNames: string[] = []
@@ -1011,7 +1135,7 @@ async function uninstallPackages(commandArgs: string[], cliOptions: Map<string,
1011
1135
  const logger = new Logger()
1012
1136
 
1013
1137
  if (commandArgs.length == 0) {
1014
- throw new Error("No package names specified")
1138
+ throw new Error('No package names specified')
1015
1139
  }
1016
1140
 
1017
1141
  const failedPackageNames: string[] = []
@@ -1061,11 +1185,11 @@ async function listPackages(commandArgs: string[], cliOptions: Map<string, strin
1061
1185
 
1062
1186
  logger.log(`Total of ${installedPackageNamesFormatted.length} packages installed in '${packagesDir}'\n`)
1063
1187
 
1064
- logger.log(installedPackageNamesFormatted.join("\n"))
1188
+ logger.log(installedPackageNamesFormatted.join('\n'))
1065
1189
  }
1066
1190
 
1067
1191
  async function serve(commandArgs: string[], cliOptions: Map<string, string>) {
1068
- const options: ServerOptions = await cliOptionsMapToOptionsObject(cliOptions, "ServerOptions")
1192
+ const options: ServerOptions = await cliOptionsMapToOptionsObject(cliOptions, 'ServerOptions')
1069
1193
 
1070
1194
  async function onServerStarted(serverOptions: ServerOptions) {
1071
1195
  // Run a test routine (early development)
@@ -1075,6 +1199,31 @@ async function serve(commandArgs: string[], cliOptions: Map<string, string>) {
1075
1199
  await startServer(options, onServerStarted)
1076
1200
  }
1077
1201
 
1202
+ async function writeSourceSeparationOutputIfNeeded(outputFilename: string, isolatedRawAudio: RawAudio | undefined, backgroundRawAudio: RawAudio | undefined, allowOverwrite: boolean, prefixIsolated: boolean) {
1203
+ // Write source separation output if needed
1204
+ const fileExtension = getLowercaseFileExtension(outputFilename)
1205
+
1206
+ if (isolatedRawAudio && backgroundRawAudio && supportedOutputMediaFileExtensions.includes(fileExtension)) {
1207
+ const pathWithoutExtension = outputFilename.substring(0, outputFilename.lastIndexOf('.'))
1208
+
1209
+ {
1210
+ const isolatedOutputFilePath = prefixIsolated ? `${pathWithoutExtension}.isolated.${fileExtension}` : outputFilename
1211
+
1212
+ const fileSaver = getFileSaver(isolatedOutputFilePath, allowOverwrite)
1213
+
1214
+ await fileSaver(isolatedRawAudio, [], '')
1215
+ }
1216
+
1217
+ {
1218
+ const backgroundOutputFilePath = `${pathWithoutExtension}.background.${fileExtension}`
1219
+
1220
+ const fileSaver = getFileSaver(backgroundOutputFilePath, allowOverwrite)
1221
+
1222
+ await fileSaver(backgroundRawAudio, [], '')
1223
+ }
1224
+ }
1225
+ }
1226
+
1078
1227
  async function cliOptionsMapToOptionsObject(cliOptionsMap: Map<string, string>, optionsRoot: keyof APIOptions, additionalOptionsSchema?: Map<string, SchemaTypeDefinition>) {
1079
1228
  const optionsSchema = await getOptionsSchema()
1080
1229
  const resultingObj: any = {}
@@ -1100,7 +1249,7 @@ async function cliOptionsMapToOptionsObject(cliOptionsMap: Map<string, string>,
1100
1249
  for (let [key, value] of cliOptionsMap) {
1101
1250
  let isNegated = false
1102
1251
 
1103
- if (key.startsWith("no-")) {
1252
+ if (key.startsWith('no-')) {
1104
1253
  isNegated = true
1105
1254
  key = key.slice(3)
1106
1255
 
@@ -1109,7 +1258,7 @@ async function cliOptionsMapToOptionsObject(cliOptionsMap: Map<string, string>,
1109
1258
  }
1110
1259
  }
1111
1260
 
1112
- const path = key.split(".")
1261
+ const path = key.split('.')
1113
1262
 
1114
1263
  let optionType: string | undefined
1115
1264
  let optionEnum: any[] | undefined
@@ -1169,7 +1318,7 @@ async function cliOptionsMapToOptionsObject(cliOptionsMap: Map<string, string>,
1169
1318
  }
1170
1319
 
1171
1320
  if (optionEnum && !optionEnum.includes(parsedValue)) {
1172
- throw new Error(`The property '${key}' must be one of ${optionEnum.join(", ")}`)
1321
+ throw new Error(`The property '${key}' must be one of ${optionEnum.join(', ')}`)
1173
1322
  }
1174
1323
 
1175
1324
  setValueAtPath(path, parsedValue)
@@ -1181,7 +1330,7 @@ async function cliOptionsMapToOptionsObject(cliOptionsMap: Map<string, string>,
1181
1330
  let cachedOptionsSchema: any
1182
1331
  export async function getOptionsSchema() {
1183
1332
  if (!cachedOptionsSchema) {
1184
- cachedOptionsSchema = await readAndParseJsonFile(resolveToModuleRootDir("data/schemas/options.json"))
1333
+ cachedOptionsSchema = await readAndParseJsonFile(resolveToModuleRootDir('data/schemas/options.json'))
1185
1334
  }
1186
1335
 
1187
1336
  return cachedOptionsSchema
@@ -1202,13 +1351,13 @@ export async function checkOutputFilenames(outputFilenames: string[], acceptMedi
1202
1351
  supportedFileExtensions.push(...supportedSubtitleFileExtensions)
1203
1352
  }
1204
1353
 
1205
- let includesPartPattern = false
1354
+ let includesPlaceholderPattern = false
1206
1355
 
1207
1356
  for (const outputFilename of outputFilenames) {
1208
1357
  const fileExtension = getLowercaseFileExtension(outputFilename)
1209
1358
 
1210
1359
  if (!supportedFileExtensions.includes(fileExtension)) {
1211
- let errorText = ""
1360
+ let errorText = ''
1212
1361
  errorText += `\nThe specified output path '${outputFilename}' doesn't have a supported file extension.\n`
1213
1362
  errorText += `\nSupported extensions are:\n`
1214
1363
 
@@ -1220,25 +1369,25 @@ export async function checkOutputFilenames(outputFilenames: string[], acceptMedi
1220
1369
  errorText += `${formatListWithQuotedElements(supportedMetadataFileExtensions)} for metadata output files.\n`
1221
1370
  }
1222
1371
 
1223
- if (acceptMetadataOutputs) {
1372
+ if (acceptSubtitleOutputs) {
1224
1373
  errorText += `${formatListWithQuotedElements(supportedSubtitleFileExtensions)} for subtitle output files.\n`
1225
1374
  }
1226
1375
 
1227
1376
  throw new Error(errorText)
1228
1377
  }
1229
1378
 
1230
- const partPatternMatch = outputFilename.match(segmentFilenamePattern)
1379
+ const placeholderPatternMatch = outputFilename.match(filenamePlaceholderPattern)
1231
1380
 
1232
- if (partPatternMatch) {
1233
- if (partPatternMatch[1] != "segment") {
1234
- throw new Error(`Invalid square bracket pattern: '${partPatternMatch[1]}'. Square bracket output filename pattern currently only supports the value 'segment'. For example: '/segment/[segment].wav'`)
1381
+ if (placeholderPatternMatch) {
1382
+ if (placeholderPatternMatch[1] != 'segment') {
1383
+ throw new Error(`Invalid placeholder pattern: '${placeholderPatternMatch[1]}'. Placeholder output filename pattern currently only supports 'segment'. For example: '/out/[segment].wav'`)
1235
1384
  }
1236
1385
 
1237
- includesPartPattern = true
1386
+ includesPlaceholderPattern = true
1238
1387
  }
1239
1388
  }
1240
1389
 
1241
- return { includesPartPattern }
1390
+ return { includesPlaceholderPattern }
1242
1391
  }
1243
1392
 
1244
1393
  export async function writeOutputFilesForSegment(outputFilenames: string[], index: number, total: number, audio: RawAudio, timeline: Timeline, text: string, language: string, allowOverwrite: boolean) {
@@ -1246,7 +1395,7 @@ export async function writeOutputFilesForSegment(outputFilenames: string[], inde
1246
1395
 
1247
1396
  const segmentWords = (await splitToWords(text, language)).filter(text => wordCharacterPattern.test(text))
1248
1397
 
1249
- const segmentJoinedWords = segmentWords.join(" ").trim()
1398
+ const segmentJoinedWords = segmentWords.join(' ').trim()
1250
1399
 
1251
1400
  let initialText: string
1252
1401
 
@@ -1255,17 +1404,17 @@ export async function writeOutputFilesForSegment(outputFilenames: string[], inde
1255
1404
  if (segmentJoinedWords.length < maxLength) {
1256
1405
  initialText = segmentJoinedWords.substring(0, maxLength).trim()
1257
1406
  } else {
1258
- initialText = segmentJoinedWords.substring(0, maxLength - 4).trim() + ".. "
1407
+ initialText = segmentJoinedWords.substring(0, maxLength - 4).trim() + '.. '
1259
1408
  }
1260
1409
 
1261
1410
  for (const outputFilename of outputFilenames) {
1262
- const partPatternMatch = outputFilename.match(segmentFilenamePattern)
1411
+ const placeholderPatternMatch = outputFilename.match(filenamePlaceholderPattern)
1263
1412
 
1264
- if (!partPatternMatch) {
1413
+ if (!placeholderPatternMatch) {
1265
1414
  continue
1266
1415
  }
1267
1416
 
1268
- const segmentFilename = outputFilename.replace(segmentFilenamePattern, `${formatIntegerWithLeadingZeros(index + 1, digitCount)} - ${initialText}.$2`)
1417
+ const segmentFilename = outputFilename.replace(filenamePlaceholderPattern, `${formatIntegerWithLeadingZeros(index + 1, digitCount)} - ${initialText}.$2`)
1269
1418
 
1270
1419
  const fileSaver = getFileSaver(segmentFilename, allowOverwrite)
1271
1420
  await fileSaver(audio, timeline, text)
@@ -1277,13 +1426,13 @@ type FileSaver = (audio: RawAudio, timeline: Timeline, text: string, subtitlesCo
1277
1426
  function getFileSaver(outputFilePath: string, allowOverwrite: boolean): FileSaver {
1278
1427
  const parsedPath = parsePath(outputFilePath)
1279
1428
 
1280
- const fileDir = parsedPath.dir || "./"
1429
+ const fileDir = parsedPath.dir || './'
1281
1430
 
1282
1431
  if (!allowOverwrite) {
1283
1432
  const filenameParts = splitFilenameOnExtendedExtension(parsedPath.base)
1284
1433
 
1285
1434
  for (let i = 1; existsSync(outputFilePath); i++) {
1286
- outputFilePath = path.join(parsedPath.dir, `${filenameParts[0]} (${i})`)
1435
+ outputFilePath = path.join(parsedPath.dir, `${filenameParts[0]}_${formatIntegerWithLeadingZeros(i, 3)}`)
1287
1436
 
1288
1437
  if (filenameParts[1]) {
1289
1438
  outputFilePath += `.${filenameParts[1]}`
@@ -1295,24 +1444,24 @@ function getFileSaver(outputFilePath: string, allowOverwrite: boolean): FileSave
1295
1444
 
1296
1445
  let fileSaver: FileSaver
1297
1446
 
1298
- if (fileExtension == "txt") {
1447
+ if (fileExtension == 'txt') {
1299
1448
  fileSaver = async (audio, timeline, text) => {
1300
1449
  await ensureDir(fileDir)
1301
- return writeFileSafe(outputFilePath, text, { encoding: "utf-8" })
1450
+ return writeFileSafe(outputFilePath, text, { encoding: 'utf-8' })
1302
1451
  }
1303
- } else if (fileExtension == "json") {
1452
+ } else if (fileExtension == 'json') {
1304
1453
  fileSaver = async (audio, timeline, text) => {
1305
1454
  await ensureDir(fileDir)
1306
1455
 
1307
1456
  const roundedTimeline = roundTimelineProperties(timeline)
1308
1457
  return writeFileSafe(outputFilePath, stringifyAndFormatJson(roundedTimeline))
1309
1458
  }
1310
- } else if (fileExtension == "srt") {
1459
+ } else if (fileExtension == 'srt') {
1311
1460
  fileSaver = async (audio, timeline, text, subtitlesConfig) => {
1312
1461
  await ensureDir(fileDir)
1313
1462
 
1314
1463
  const extraSubtitleConfigOptions: SubtitlesConfig = {
1315
- format: "srt",
1464
+ format: 'srt',
1316
1465
  originalText: text,
1317
1466
  totalDuration: getRawAudioDuration(audio)
1318
1467
  }
@@ -1323,12 +1472,12 @@ function getFileSaver(outputFilePath: string, allowOverwrite: boolean): FileSave
1323
1472
 
1324
1473
  return writeFileSafe(outputFilePath, subtitles)
1325
1474
  }
1326
- } else if (fileExtension == "vtt") {
1475
+ } else if (fileExtension == 'vtt') {
1327
1476
  fileSaver = async (audio, timeline, text, subtitlesConfig) => {
1328
1477
  await ensureDir(fileDir)
1329
1478
 
1330
1479
  const extraSubtitleConfigOptions: SubtitlesConfig = {
1331
- format: "webvtt",
1480
+ format: 'webvtt',
1332
1481
  originalText: text,
1333
1482
  totalDuration: getRawAudioDuration(audio)
1334
1483
  }
@@ -1339,11 +1488,11 @@ function getFileSaver(outputFilePath: string, allowOverwrite: boolean): FileSave
1339
1488
 
1340
1489
  return writeFileSafe(outputFilePath, subtitles)
1341
1490
  }
1342
- } else if (fileExtension == "wav") {
1491
+ } else if (fileExtension == 'wav') {
1343
1492
  fileSaver = async (audio) => {
1344
1493
  await ensureDir(fileDir)
1345
1494
 
1346
- return writeFileSafe(outputFilePath, encodeWaveBuffer(audio))
1495
+ return writeFileSafe(outputFilePath, encodeRawAudioToWave(audio))
1347
1496
  }
1348
1497
  } else if (supportedOutputMediaFileExtensions.includes(fileExtension)) {
1349
1498
  fileSaver = async (audio) => {
@@ -1358,7 +1507,7 @@ function getFileSaver(outputFilePath: string, allowOverwrite: boolean): FileSave
1358
1507
  return
1359
1508
  }
1360
1509
  } else {
1361
- throw new Error("Unsupported file extension")
1510
+ throw new Error('Unsupported output file extension')
1362
1511
  }
1363
1512
 
1364
1513
  return fileSaver
@@ -1366,9 +1515,9 @@ function getFileSaver(outputFilePath: string, allowOverwrite: boolean): FileSave
1366
1515
 
1367
1516
  const supportedMetadataFileExtensions = ['txt', 'json']
1368
1517
  const supportedSubtitleFileExtensions = ['srt', 'vtt']
1369
- const supportedOutputMediaFileExtensions = ["wav", "mp3", "opus", "m4a", "ogg", "flac"]
1518
+ const supportedOutputMediaFileExtensions = ['wav', 'mp3', 'opus', 'm4a', 'ogg', 'flac']
1370
1519
 
1371
- const segmentFilenamePattern = /\[(.*)\]\.(.*)$/
1520
+ const filenamePlaceholderPattern = /\[(.*)\]\.(.*)$/
1372
1521
 
1373
1522
  const overwriteByDefault = false
1374
1523