echogarden 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -1
- package/data/lexicons/heteronyms.json +674 -0
- package/data/schemas/options.json +1057 -0
- package/data/tables/lcid-table.json +7427 -0
- package/dist/alignment/DTWMfccSequenceAlignment.d.ts +2 -0
- package/dist/alignment/DTWMfccSequenceAlignment.js +26 -0
- package/dist/alignment/DTWMfccSequenceAlignment.js.map +1 -0
- package/dist/alignment/DTWSequenceAlignment.d.ts +5 -0
- package/dist/alignment/DTWSequenceAlignment.js +99 -0
- package/dist/alignment/DTWSequenceAlignment.js.map +1 -0
- package/dist/alignment/DTWSequenceAlignmentWindowed.d.ts +5 -0
- package/dist/alignment/DTWSequenceAlignmentWindowed.js +161 -0
- package/dist/alignment/DTWSequenceAlignmentWindowed.js.map +1 -0
- package/dist/alignment/LevenshteinSequenceAlignment.d.ts +5 -0
- package/dist/alignment/LevenshteinSequenceAlignment.js +102 -0
- package/dist/alignment/LevenshteinSequenceAlignment.js.map +1 -0
- package/dist/alignment/SpeechAlignment.d.ts +27 -0
- package/dist/alignment/SpeechAlignment.js +266 -0
- package/dist/alignment/SpeechAlignment.js.map +1 -0
- package/dist/api/API.d.ts +8 -0
- package/dist/api/API.js +10 -0
- package/dist/api/API.js.map +1 -0
- package/dist/api/APIOptions.d.ts +12 -0
- package/dist/api/APIOptions.js +2 -0
- package/dist/api/APIOptions.js.map +1 -0
- package/dist/api/Alignment.d.ts +32 -0
- package/dist/api/Alignment.js +134 -0
- package/dist/api/Alignment.js.map +1 -0
- package/dist/api/Denoising.d.ts +14 -0
- package/dist/api/Denoising.js +66 -0
- package/dist/api/Denoising.js.map +1 -0
- package/dist/api/Globals.d.ts +1 -0
- package/dist/api/Globals.js +2 -0
- package/dist/api/Globals.js.map +1 -0
- package/dist/api/LanguageDetection.d.ts +49 -0
- package/dist/api/LanguageDetection.js +118 -0
- package/dist/api/LanguageDetection.js.map +1 -0
- package/dist/api/Recognition.d.ts +49 -0
- package/dist/api/Recognition.js +162 -0
- package/dist/api/Recognition.js.map +1 -0
- package/dist/api/Synthesis.d.ts +125 -0
- package/dist/api/Synthesis.js +892 -0
- package/dist/api/Synthesis.js.map +1 -0
- package/dist/api/Translation.d.ts +25 -0
- package/dist/api/Translation.js +69 -0
- package/dist/api/Translation.js.map +1 -0
- package/dist/api/Vad.d.ts +25 -0
- package/dist/api/Vad.js +88 -0
- package/dist/api/Vad.js.map +1 -0
- package/dist/audio/AudioBufferConversion.d.ts +15 -0
- package/dist/audio/AudioBufferConversion.js +228 -0
- package/dist/audio/AudioBufferConversion.js.map +1 -0
- package/dist/audio/AudioPlayer.d.ts +9 -0
- package/dist/audio/AudioPlayer.js +232 -0
- package/dist/audio/AudioPlayer.js.map +1 -0
- package/dist/audio/AudioRecorder.d.ts +3 -0
- package/dist/audio/AudioRecorder.js +68 -0
- package/dist/audio/AudioRecorder.js.map +1 -0
- package/dist/audio/AudioUtilities.d.ts +48 -0
- package/dist/audio/AudioUtilities.js +209 -0
- package/dist/audio/AudioUtilities.js.map +1 -0
- package/dist/audio/SoxPath.d.ts +1 -0
- package/dist/audio/SoxPath.js +19 -0
- package/dist/audio/SoxPath.js.map +1 -0
- package/dist/cli/CLI.d.ts +8 -0
- package/dist/cli/CLI.js +860 -0
- package/dist/cli/CLI.js.map +1 -0
- package/dist/cli/CLIConfigFile.d.ts +3 -0
- package/dist/cli/CLIConfigFile.js +62 -0
- package/dist/cli/CLIConfigFile.js.map +1 -0
- package/dist/cli/CLILauncher.d.ts +2 -0
- package/dist/cli/CLILauncher.js +22 -0
- package/dist/cli/CLILauncher.js.map +1 -0
- package/dist/cli/CLIOptionsSchema.d.ts +5 -0
- package/dist/cli/CLIOptionsSchema.js +36 -0
- package/dist/cli/CLIOptionsSchema.js.map +1 -0
- package/dist/cli/CLIParser.d.ts +6 -0
- package/dist/cli/CLIParser.js +34 -0
- package/dist/cli/CLIParser.js.map +1 -0
- package/dist/cli/CLIStarter.d.ts +1 -0
- package/dist/cli/CLIStarter.js +3 -0
- package/dist/cli/CLIStarter.js.map +1 -0
- package/dist/codecs/FFMpegTranscoder.d.ts +20 -0
- package/dist/codecs/FFMpegTranscoder.js +169 -0
- package/dist/codecs/FFMpegTranscoder.js.map +1 -0
- package/dist/codecs/TIMITCodec.d.ts +9 -0
- package/dist/codecs/TIMITCodec.js +14 -0
- package/dist/codecs/TIMITCodec.js.map +1 -0
- package/dist/codecs/WaveCodec.d.ts +19 -0
- package/dist/codecs/WaveCodec.js +208 -0
- package/dist/codecs/WaveCodec.js.map +1 -0
- package/dist/denoising/RNNoise.d.ts +6 -0
- package/dist/denoising/RNNoise.js +68 -0
- package/dist/denoising/RNNoise.js.map +1 -0
- package/dist/dsp/BiquadFilter.d.ts +26 -0
- package/dist/dsp/BiquadFilter.js +399 -0
- package/dist/dsp/BiquadFilter.js.map +1 -0
- package/dist/dsp/FFT.d.ts +9 -0
- package/dist/dsp/FFT.js +135 -0
- package/dist/dsp/FFT.js.map +1 -0
- package/dist/dsp/MFCC.d.ts +25 -0
- package/dist/dsp/MFCC.js +162 -0
- package/dist/dsp/MFCC.js.map +1 -0
- package/dist/dsp/MelSpectogram.d.ts +19 -0
- package/dist/dsp/MelSpectogram.js +102 -0
- package/dist/dsp/MelSpectogram.js.map +1 -0
- package/dist/dsp/Rubberband.d.ts +52 -0
- package/dist/dsp/Rubberband.js +186 -0
- package/dist/dsp/Rubberband.js.map +1 -0
- package/dist/dsp/Sonic.d.ts +2 -0
- package/dist/dsp/Sonic.js +39 -0
- package/dist/dsp/Sonic.js.map +1 -0
- package/dist/dsp/SpeexResampler.d.ts +3 -0
- package/dist/dsp/SpeexResampler.js +53 -0
- package/dist/dsp/SpeexResampler.js.map +1 -0
- package/dist/math/VectorMath.d.ts +70 -0
- package/dist/math/VectorMath.js +564 -0
- package/dist/math/VectorMath.js.map +1 -0
- package/dist/nlp/ChineseSegmentation.d.ts +1 -0
- package/dist/nlp/ChineseSegmentation.js +53 -0
- package/dist/nlp/ChineseSegmentation.js.map +1 -0
- package/dist/nlp/CompromiseNLP.d.ts +15 -0
- package/dist/nlp/CompromiseNLP.js +66 -0
- package/dist/nlp/CompromiseNLP.js.map +1 -0
- package/dist/nlp/EspeakPhonemizer.d.ts +4 -0
- package/dist/nlp/EspeakPhonemizer.js +133 -0
- package/dist/nlp/EspeakPhonemizer.js.map +1 -0
- package/dist/nlp/IPA.d.ts +19 -0
- package/dist/nlp/IPA.js +113 -0
- package/dist/nlp/IPA.js.map +1 -0
- package/dist/nlp/JapaneseSegmentation.d.ts +1 -0
- package/dist/nlp/JapaneseSegmentation.js +40 -0
- package/dist/nlp/JapaneseSegmentation.js.map +1 -0
- package/dist/nlp/Lexicon.d.ts +17 -0
- package/dist/nlp/Lexicon.js +6 -0
- package/dist/nlp/Lexicon.js.map +1 -0
- package/dist/nlp/PhoneConversion.d.ts +6 -0
- package/dist/nlp/PhoneConversion.js +467 -0
- package/dist/nlp/PhoneConversion.js.map +1 -0
- package/dist/nlp/Segmentation.d.ts +41 -0
- package/dist/nlp/Segmentation.js +158 -0
- package/dist/nlp/Segmentation.js.map +1 -0
- package/dist/nlp/TextNormalizer.d.ts +4 -0
- package/dist/nlp/TextNormalizer.js +69 -0
- package/dist/nlp/TextNormalizer.js.map +1 -0
- package/dist/recognition/AmazonTranscribeSTT.d.ts +6 -0
- package/dist/recognition/AmazonTranscribeSTT.js +79 -0
- package/dist/recognition/AmazonTranscribeSTT.js.map +1 -0
- package/dist/recognition/AzureCognitiveServicesSTT.d.ts +7 -0
- package/dist/recognition/AzureCognitiveServicesSTT.js +51 -0
- package/dist/recognition/AzureCognitiveServicesSTT.js.map +1 -0
- package/dist/recognition/GoogleCloudSTT.d.ts +7 -0
- package/dist/recognition/GoogleCloudSTT.js +66 -0
- package/dist/recognition/GoogleCloudSTT.js.map +1 -0
- package/dist/recognition/SileroSTT.d.ts +9 -0
- package/dist/recognition/SileroSTT.js +125 -0
- package/dist/recognition/SileroSTT.js.map +1 -0
- package/dist/recognition/VoskSTT.d.ts +10 -0
- package/dist/recognition/VoskSTT.js +78 -0
- package/dist/recognition/VoskSTT.js.map +1 -0
- package/dist/recognition/WhisperSTT.d.ts +69 -0
- package/dist/recognition/WhisperSTT.js +977 -0
- package/dist/recognition/WhisperSTT.js.map +1 -0
- package/dist/server/Server.d.ts +1 -0
- package/dist/server/Server.js +18 -0
- package/dist/server/Server.js.map +1 -0
- package/dist/speech-language-detection/SileroLanguageDetection.d.ts +9 -0
- package/dist/speech-language-detection/SileroLanguageDetection.js +47 -0
- package/dist/speech-language-detection/SileroLanguageDetection.js.map +1 -0
- package/dist/subtitles/Subtitles.d.ts +25 -0
- package/dist/subtitles/Subtitles.js +286 -0
- package/dist/subtitles/Subtitles.js.map +1 -0
- package/dist/synthesis/AwsPollyTTS.d.ts +7 -0
- package/dist/synthesis/AwsPollyTTS.js +51 -0
- package/dist/synthesis/AwsPollyTTS.js.map +1 -0
- package/dist/synthesis/AzureCognitiveServicesTTS.d.ts +9 -0
- package/dist/synthesis/AzureCognitiveServicesTTS.js +103 -0
- package/dist/synthesis/AzureCognitiveServicesTTS.js.map +1 -0
- package/dist/synthesis/CoquiServerTTS.d.ts +6 -0
- package/dist/synthesis/CoquiServerTTS.js +22 -0
- package/dist/synthesis/CoquiServerTTS.js.map +1 -0
- package/dist/synthesis/ElevenLabsTTS.d.ts +8 -0
- package/dist/synthesis/ElevenLabsTTS.js +48 -0
- package/dist/synthesis/ElevenLabsTTS.js.map +1 -0
- package/dist/synthesis/EspeakTTS.d.ts +46 -0
- package/dist/synthesis/EspeakTTS.js +353 -0
- package/dist/synthesis/EspeakTTS.js.map +1 -0
- package/dist/synthesis/FliteTTS.d.ts +17 -0
- package/dist/synthesis/FliteTTS.js +326 -0
- package/dist/synthesis/FliteTTS.js.map +1 -0
- package/dist/synthesis/GoogleCloudTTS.d.ts +16 -0
- package/dist/synthesis/GoogleCloudTTS.js +72 -0
- package/dist/synthesis/GoogleCloudTTS.js.map +1 -0
- package/dist/synthesis/GoogleTranslateTTS.d.ts +13 -0
- package/dist/synthesis/GoogleTranslateTTS.js +177 -0
- package/dist/synthesis/GoogleTranslateTTS.js.map +1 -0
- package/dist/synthesis/MicrosoftEdgeTTS.d.ts +10 -0
- package/dist/synthesis/MicrosoftEdgeTTS.js +216 -0
- package/dist/synthesis/MicrosoftEdgeTTS.js.map +1 -0
- package/dist/synthesis/SamTTS.d.ts +4 -0
- package/dist/synthesis/SamTTS.js +21 -0
- package/dist/synthesis/SamTTS.js.map +1 -0
- package/dist/synthesis/SapiTTS.d.ts +9 -0
- package/dist/synthesis/SapiTTS.js +211 -0
- package/dist/synthesis/SapiTTS.js.map +1 -0
- package/dist/synthesis/StreamlabsPollyTTS.d.ts +12 -0
- package/dist/synthesis/StreamlabsPollyTTS.js +88 -0
- package/dist/synthesis/StreamlabsPollyTTS.js.map +1 -0
- package/dist/synthesis/SvoxPicoTTS.d.ts +11 -0
- package/dist/synthesis/SvoxPicoTTS.js +236 -0
- package/dist/synthesis/SvoxPicoTTS.js.map +1 -0
- package/dist/synthesis/VitsTTS.d.ts +27 -0
- package/dist/synthesis/VitsTTS.js +359 -0
- package/dist/synthesis/VitsTTS.js.map +1 -0
- package/dist/tests/Test.d.ts +1 -0
- package/dist/tests/Test.js +10 -0
- package/dist/tests/Test.js.map +1 -0
- package/dist/text-language-detection/FastTextLanguageDetection.d.ts +2 -0
- package/dist/text-language-detection/FastTextLanguageDetection.js +38 -0
- package/dist/text-language-detection/FastTextLanguageDetection.js.map +1 -0
- package/dist/text-language-detection/TinyLDLanguageDetection.d.ts +2 -0
- package/dist/text-language-detection/TinyLDLanguageDetection.js +12 -0
- package/dist/text-language-detection/TinyLDLanguageDetection.js.map +1 -0
- package/dist/utilities/BinaryArrayConversion.d.ts +15 -0
- package/dist/utilities/BinaryArrayConversion.js +115 -0
- package/dist/utilities/BinaryArrayConversion.js.map +1 -0
- package/dist/utilities/Compression.d.ts +4 -0
- package/dist/utilities/Compression.js +67 -0
- package/dist/utilities/Compression.js.map +1 -0
- package/dist/utilities/FileDownloader.d.ts +3 -0
- package/dist/utilities/FileDownloader.js +147 -0
- package/dist/utilities/FileDownloader.js.map +1 -0
- package/dist/utilities/FileSystem.d.ts +30 -0
- package/dist/utilities/FileSystem.js +141 -0
- package/dist/utilities/FileSystem.js.map +1 -0
- package/dist/utilities/Hashing.d.ts +10 -0
- package/dist/utilities/Hashing.js +169 -0
- package/dist/utilities/Hashing.js.map +1 -0
- package/dist/utilities/Locale.d.ts +9 -0
- package/dist/utilities/Locale.js +65 -0
- package/dist/utilities/Locale.js.map +1 -0
- package/dist/utilities/Logger.d.ts +11 -0
- package/dist/utilities/Logger.js +49 -0
- package/dist/utilities/Logger.js.map +1 -0
- package/dist/utilities/NdArrayUtilities.d.ts +3 -0
- package/dist/utilities/NdArrayUtilities.js +22 -0
- package/dist/utilities/NdArrayUtilities.js.map +1 -0
- package/dist/utilities/ObjectUtilities.d.ts +4 -0
- package/dist/utilities/ObjectUtilities.js +132 -0
- package/dist/utilities/ObjectUtilities.js.map +1 -0
- package/dist/utilities/OpenPromise.d.ts +6 -0
- package/dist/utilities/OpenPromise.js +12 -0
- package/dist/utilities/OpenPromise.js.map +1 -0
- package/dist/utilities/PackageManager.d.ts +4 -0
- package/dist/utilities/PackageManager.js +46 -0
- package/dist/utilities/PackageManager.js.map +1 -0
- package/dist/utilities/RandomGenerator.d.ts +35 -0
- package/dist/utilities/RandomGenerator.js +149 -0
- package/dist/utilities/RandomGenerator.js.map +1 -0
- package/dist/utilities/TarballMaker.d.ts +4 -0
- package/dist/utilities/TarballMaker.js +50 -0
- package/dist/utilities/TarballMaker.js.map +1 -0
- package/dist/utilities/Timeline.d.ts +20 -0
- package/dist/utilities/Timeline.js +110 -0
- package/dist/utilities/Timeline.js.map +1 -0
- package/dist/utilities/Timer.d.ts +13 -0
- package/dist/utilities/Timer.js +70 -0
- package/dist/utilities/Timer.js.map +1 -0
- package/dist/utilities/Utilities.d.ts +68 -0
- package/dist/utilities/Utilities.js +305 -0
- package/dist/utilities/Utilities.js.map +1 -0
- package/dist/utilities/WasmMemoryManager.d.ts +142 -0
- package/dist/utilities/WasmMemoryManager.js +407 -0
- package/dist/utilities/WasmMemoryManager.js.map +1 -0
- package/dist/utilities/WebReader.d.ts +1 -0
- package/dist/utilities/WebReader.js +47 -0
- package/dist/utilities/WebReader.js.map +1 -0
- package/dist/utilities/WikipediaReader.d.ts +1 -0
- package/dist/utilities/WikipediaReader.js +31 -0
- package/dist/utilities/WikipediaReader.js.map +1 -0
- package/dist/voice-activity-detection/SileroVAD.d.ts +13 -0
- package/dist/voice-activity-detection/SileroVAD.js +58 -0
- package/dist/voice-activity-detection/SileroVAD.js.map +1 -0
- package/dist/voice-activity-detection/WebRtcVAD.d.ts +3 -0
- package/dist/voice-activity-detection/WebRtcVAD.js +53 -0
- package/dist/voice-activity-detection/WebRtcVAD.js.map +1 -0
- package/docs/CLI.md +234 -0
- package/docs/Development.md +3 -0
- package/docs/Engines.md +80 -0
- package/docs/Licenses.md +37 -0
- package/docs/Options.md +192 -0
- package/docs/Roadmap.md +16 -0
- package/docs/Technical.md +72 -0
- package/package.json +119 -19
- package/cli.js +0 -3
package/dist/cli/CLI.js
ADDED
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
import * as API from '../api/API.js';
|
|
2
|
+
import { parseCLIArguments } from './CLIParser.js';
|
|
3
|
+
import { convertHtmlToText, formatIntegerWithLeadingZeros, formatListWithQuotedElements, logToStderr, setupUnhandledExceptionListeners, stringifyAndFormatJson } from "../utilities/Utilities.js";
|
|
4
|
+
import { getOptionTypeFromSchema } from "./CLIOptionsSchema.js";
|
|
5
|
+
import { parseConfigFile, parseJSONConfigFile } from "./CLIConfigFile.js";
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { applyGainDecibels, encodeWaveBuffer, getEmptyRawAudio, normalizeAudioLevel, sliceRawAudioByTime } from "../audio/AudioUtilities.js";
|
|
8
|
+
import { subtitlesToText, subtitlesToTimeline, timelineToSubtitles } from "../subtitles/Subtitles.js";
|
|
9
|
+
import { Logger } from "../utilities/Logger.js";
|
|
10
|
+
import { encodeFromChannels } from "../codecs/FFMpegTranscoder.js";
|
|
11
|
+
import { parse as parsePath } from "node:path";
|
|
12
|
+
import { splitToParagraphs, splitToWords, wordCharacterPattern } from "../nlp/Segmentation.js";
|
|
13
|
+
import { playAudioSamples, playAudioWithTimeline } from "../audio/AudioPlayer.js";
|
|
14
|
+
import { extendDeep } from "../utilities/ObjectUtilities.js";
|
|
15
|
+
import { addTimeOffsetToTimeline, roundTimelineTimestamps, wordTimelineToSegmentSentenceTimeline } from "../utilities/Timeline.js";
|
|
16
|
+
import { ensureDir, existsSync, getFileExtension, readAndParseJsonFile, readFile, readdir, resolveToModuleRootDir, writeFileSafe } from '../utilities/FileSystem.js';
|
|
17
|
+
import { formatLanguageCodeWithName } from '../utilities/Locale.js';
|
|
18
|
+
import { ensureAndGetPackagesDir, loadPackage } from '../utilities/PackageManager.js';
|
|
19
|
+
import { removePackage } from '../utilities/PackageManager.js';
|
|
20
|
+
import { appName } from '../api/Globals.js';
|
|
21
|
+
const log = logToStderr;
|
|
22
|
+
export async function start() {
|
|
23
|
+
setupUnhandledExceptionListeners();
|
|
24
|
+
let args;
|
|
25
|
+
try {
|
|
26
|
+
const processArgs = process.argv.slice(2);
|
|
27
|
+
const command = processArgs[0];
|
|
28
|
+
if (!command) {
|
|
29
|
+
log(`Supported operations:\n\n${commandHelp.join("\n")}`);
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
args = parseCLIArguments(command, processArgs.slice(1));
|
|
33
|
+
if (!args.options.has("config")) {
|
|
34
|
+
const defaultConfigFile = `./${appName}.config`;
|
|
35
|
+
const defaultJsonConfigFile = defaultConfigFile + ".json";
|
|
36
|
+
if (existsSync(defaultConfigFile)) {
|
|
37
|
+
args.options.set("config", defaultConfigFile);
|
|
38
|
+
}
|
|
39
|
+
else if (existsSync(defaultJsonConfigFile)) {
|
|
40
|
+
args.options.set("config", defaultJsonConfigFile);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (args.options.has("config")) {
|
|
44
|
+
const configFilePath = args.options.get("config");
|
|
45
|
+
args.options.delete("config");
|
|
46
|
+
let parsedOptionFile;
|
|
47
|
+
if (configFilePath.endsWith(".config")) {
|
|
48
|
+
parsedOptionFile = await parseConfigFile(configFilePath);
|
|
49
|
+
}
|
|
50
|
+
else if (configFilePath.endsWith(".config.json")) {
|
|
51
|
+
parsedOptionFile = await parseJSONConfigFile(configFilePath);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
throw new Error(`Specified config file '${configFilePath}' doesn't have a supported extension. Should be either '.config' or '.config.json'`);
|
|
55
|
+
}
|
|
56
|
+
let sectionName = args.command;
|
|
57
|
+
if (sectionName.startsWith("speak-")) {
|
|
58
|
+
sectionName = "speak";
|
|
59
|
+
}
|
|
60
|
+
const newOptions = parsedOptionFile.get(sectionName) || new Map();
|
|
61
|
+
for (const [key, value] of args.options) {
|
|
62
|
+
newOptions.set(key, value);
|
|
63
|
+
}
|
|
64
|
+
args.options = newOptions;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
log(`Error: ${e.message}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
let debugMode = false;
|
|
72
|
+
if (args.options.has('debug')) {
|
|
73
|
+
args.options.delete('debug');
|
|
74
|
+
debugMode = true;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
await startWithArgs(args);
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
if (debugMode) {
|
|
81
|
+
log(e);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
log(`Error: ${e.message}`);
|
|
85
|
+
}
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
const executableName = `${chalk.cyanBright('echogarden')}`;
|
|
91
|
+
const commandHelp = [
|
|
92
|
+
`${executableName} ${chalk.magentaBright('speak')} text [output files...] [options...]`,
|
|
93
|
+
` Speak the given text\n`,
|
|
94
|
+
`${executableName} ${chalk.magentaBright('speak-file')} inputFile [output files...] [options...]`,
|
|
95
|
+
` Speak the given text file\n`,
|
|
96
|
+
`${executableName} ${chalk.magentaBright('speak-url')} url [output files...] [options...]`,
|
|
97
|
+
` Speak the given URL\n`,
|
|
98
|
+
`${executableName} ${chalk.magentaBright('speak-wikipedia')} articleName [output files...] [options...]`,
|
|
99
|
+
` Speak the given wikipedia article, language edition can be specified by --language=lang\n`,
|
|
100
|
+
`${executableName} ${chalk.magentaBright('transcribe')} audioFile [output files...] [options...]`,
|
|
101
|
+
` Transcribe audio file\n`,
|
|
102
|
+
`${executableName} ${chalk.magentaBright('align')} audioFile referenceFile [output files...] [options...]`,
|
|
103
|
+
` Align audio file to transcript\n`,
|
|
104
|
+
`${executableName} ${chalk.magentaBright('translate-speech')} inputFile [output files...] [options...]`,
|
|
105
|
+
` Translate audio file\n`,
|
|
106
|
+
`${executableName} ${chalk.magentaBright('detect-speech-language')} audioFile [options...]`,
|
|
107
|
+
` Detect language of audio file\n`,
|
|
108
|
+
`${executableName} ${chalk.magentaBright('detect-text-language')} inputFile [options...]`,
|
|
109
|
+
` Detect language of textual file\n`,
|
|
110
|
+
`${executableName} ${chalk.magentaBright('detect-voice-activity')} audioFile [output files...] [options...]`,
|
|
111
|
+
` Detect voice activity in audio file\n`,
|
|
112
|
+
`${executableName} ${chalk.magentaBright('denoise')} audioFile [output files...] [options...]`,
|
|
113
|
+
` Denoise audio file\n`,
|
|
114
|
+
`${executableName} ${chalk.magentaBright('list-voices')} tts-engine [options...]`,
|
|
115
|
+
` List voices for specified TTS engine\n`,
|
|
116
|
+
`${executableName} ${chalk.magentaBright('install')} [package names...] [options...]`,
|
|
117
|
+
` Install one or more Echogarden packages\n`,
|
|
118
|
+
`${executableName} ${chalk.magentaBright('uninstall')} [package names...] [options...]`,
|
|
119
|
+
` Uninstall one or more Echogarden packages\n`,
|
|
120
|
+
`${executableName} ${chalk.magentaBright('list-packages')} [package names...] [options...]`,
|
|
121
|
+
` List installed Echogarden packages`,
|
|
122
|
+
];
|
|
123
|
+
async function startWithArgs(parsedArgs) {
|
|
124
|
+
switch (parsedArgs.command) {
|
|
125
|
+
case 'speak':
|
|
126
|
+
case 'speak-file':
|
|
127
|
+
case 'speak-url':
|
|
128
|
+
case 'speak-wikipedia': {
|
|
129
|
+
await speak(parsedArgs.command, parsedArgs.commandArgs, parsedArgs.options);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case 'transcribe': {
|
|
133
|
+
await transcribe(parsedArgs.commandArgs, parsedArgs.options);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case 'align': {
|
|
137
|
+
await align(parsedArgs.commandArgs, parsedArgs.options);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case 'translate-speech': {
|
|
141
|
+
await translateSpeech(parsedArgs.commandArgs, parsedArgs.options);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case 'detect-language': {
|
|
145
|
+
await detectLanguage(parsedArgs.commandArgs, parsedArgs.options, "auto");
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case 'detect-speech-language': {
|
|
149
|
+
await detectLanguage(parsedArgs.commandArgs, parsedArgs.options, "speech");
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case 'detect-text-language': {
|
|
153
|
+
await detectLanguage(parsedArgs.commandArgs, parsedArgs.options, "text");
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case 'detect-voice-activity': {
|
|
157
|
+
await detectVoiceActivity(parsedArgs.commandArgs, parsedArgs.options);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case 'denoise': {
|
|
161
|
+
await denoise(parsedArgs.commandArgs, parsedArgs.options);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case 'list-voices': {
|
|
165
|
+
await listTTSVoices(parsedArgs.commandArgs, parsedArgs.options);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case 'install': {
|
|
169
|
+
await installPackages(parsedArgs.commandArgs, parsedArgs.options);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
case 'uninstall': {
|
|
173
|
+
await uninstallPackages(parsedArgs.commandArgs, parsedArgs.options);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
case 'list-packages': {
|
|
177
|
+
await listPackages(parsedArgs.commandArgs, parsedArgs.options);
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
default: {
|
|
181
|
+
log(`Unknown command: ${parsedArgs.command}`);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function speak(command, commandArgs, cliOptions) {
|
|
187
|
+
const progressLogger = new Logger();
|
|
188
|
+
const mainArg = commandArgs[0];
|
|
189
|
+
const outputFilenames = commandArgs.slice(1);
|
|
190
|
+
if (mainArg == undefined) {
|
|
191
|
+
if (command == "speak") {
|
|
192
|
+
throw new Error(`'speak' requires an argument containing the text to speak.`);
|
|
193
|
+
}
|
|
194
|
+
else if (command == "speak-file") {
|
|
195
|
+
throw new Error(`'speak-file' requires an argument containing the file to speak.`);
|
|
196
|
+
}
|
|
197
|
+
else if (command == "speak-url") {
|
|
198
|
+
throw new Error(`'speak-url' requires an argument containing the url to speak.`);
|
|
199
|
+
}
|
|
200
|
+
else if (command == "speak-wikipedia") {
|
|
201
|
+
throw new Error(`'speak-wikipedia' requires an argument containing the name of the Wikipedia article to speak.`);
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
let textSegments;
|
|
206
|
+
if (command == "speak") {
|
|
207
|
+
textSegments = splitToParagraphs(mainArg);
|
|
208
|
+
}
|
|
209
|
+
else if (command == "speak-file") {
|
|
210
|
+
const sourceFile = mainArg;
|
|
211
|
+
if (!existsSync(sourceFile)) {
|
|
212
|
+
throw new Error(`The given source file '${sourceFile}' was not found.`);
|
|
213
|
+
}
|
|
214
|
+
const sourceFileExtension = getFileExtension(sourceFile);
|
|
215
|
+
const fileContent = await readFile(sourceFile, { encoding: 'utf-8' });
|
|
216
|
+
if (sourceFileExtension == "txt") {
|
|
217
|
+
textSegments = splitToParagraphs(fileContent);
|
|
218
|
+
}
|
|
219
|
+
else if (sourceFileExtension == "html" || sourceFileExtension == "htm") {
|
|
220
|
+
const textContent = await convertHtmlToText(fileContent);
|
|
221
|
+
textSegments = splitToParagraphs(textContent);
|
|
222
|
+
}
|
|
223
|
+
else if (sourceFileExtension == "srt" || sourceFileExtension == "vtt") {
|
|
224
|
+
const fileContent = await readFile(sourceFile, { encoding: 'utf-8' });
|
|
225
|
+
textSegments = subtitlesToTimeline(fileContent).map(entry => entry.text);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
throw new Error(`'speak-file' only supports inputs with extensions 'txt', 'html', 'htm', 'srt' or 'vtt'`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else if (command == "speak-url") {
|
|
232
|
+
const url = mainArg;
|
|
233
|
+
if (!url.startsWith("http://") || !url.startsWith("https://")) {
|
|
234
|
+
throw new Error(`'${url}' is not a valid URL. Only 'http://' and 'https://' protocols are supported`);
|
|
235
|
+
}
|
|
236
|
+
const { fetchDocumentText } = await import("../utilities/WebReader.js");
|
|
237
|
+
const urlTextContent = await fetchDocumentText(url);
|
|
238
|
+
textSegments = splitToParagraphs(urlTextContent);
|
|
239
|
+
}
|
|
240
|
+
else if (command == "speak-wikipedia") {
|
|
241
|
+
const { parseWikipediaArticle } = await import("../utilities/WikipediaReader.js");
|
|
242
|
+
let language = cliOptions.get("language");
|
|
243
|
+
if (!language) {
|
|
244
|
+
language = "en";
|
|
245
|
+
cliOptions.set("language", language);
|
|
246
|
+
}
|
|
247
|
+
textSegments = await parseWikipediaArticle(mainArg, language);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
throw new Error("Invalid command");
|
|
251
|
+
}
|
|
252
|
+
const { includesPartPattern } = await checkOutputFilenames(outputFilenames, true, true, true);
|
|
253
|
+
const additionalOptionsSchema = new Map();
|
|
254
|
+
additionalOptionsSchema.set('play', { type: 'boolean' });
|
|
255
|
+
if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
|
|
256
|
+
cliOptions.set('play', `${outputFilenames.length == 0}`);
|
|
257
|
+
}
|
|
258
|
+
const options = await cliOptionsMapToOptionsObject(cliOptions, "SynthesisOptions", additionalOptionsSchema);
|
|
259
|
+
async function onSegment(segmentData) {
|
|
260
|
+
if (includesPartPattern) {
|
|
261
|
+
progressLogger.start("Writing output files for segment");
|
|
262
|
+
}
|
|
263
|
+
await writeOutputFilesForSegment(outputFilenames, segmentData.index, segmentData.total, segmentData.audio, segmentData.timeline, segmentData.transcript, segmentData.language);
|
|
264
|
+
progressLogger.end();
|
|
265
|
+
if (options.play) {
|
|
266
|
+
const audioWithAddedGain = applyGainDecibels(segmentData.audio, -3 - segmentData.peakDecibelsSoFar);
|
|
267
|
+
const segmentWordTimeline = segmentData.timeline.flatMap(sentenceTimeline => sentenceTimeline.timeline);
|
|
268
|
+
await playAudioWithTimeline(audioWithAddedGain, segmentWordTimeline, segmentData.transcript);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const { synthesizedAudio, timeline } = await API.synthesizeSegments(textSegments, options, onSegment, undefined);
|
|
272
|
+
if (outputFilenames.length > 0) {
|
|
273
|
+
progressLogger.start("\nWriting output files");
|
|
274
|
+
}
|
|
275
|
+
for (const outputFile of outputFilenames) {
|
|
276
|
+
const partPatternMatch = outputFile.match(segmentFilenamePattern);
|
|
277
|
+
if (partPatternMatch) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const fileSaver = getFileSaver(outputFile);
|
|
281
|
+
await fileSaver(synthesizedAudio, timeline, textSegments.join("\n\n"));
|
|
282
|
+
}
|
|
283
|
+
progressLogger.end();
|
|
284
|
+
}
|
|
285
|
+
async function transcribe(commandArgs, cliOptions) {
|
|
286
|
+
const progressLogger = new Logger();
|
|
287
|
+
const sourceFilename = commandArgs[0];
|
|
288
|
+
const outputFilenames = commandArgs.slice(1);
|
|
289
|
+
if (sourceFilename == undefined) {
|
|
290
|
+
throw new Error(`'transcribe' requires an argument containing the source file name.`);
|
|
291
|
+
}
|
|
292
|
+
if (!existsSync(sourceFilename)) {
|
|
293
|
+
throw new Error(`The given source audio file '${sourceFilename}' was not found.`);
|
|
294
|
+
}
|
|
295
|
+
const { includesPartPattern } = await checkOutputFilenames(outputFilenames, true, true, true);
|
|
296
|
+
const additionalOptionsSchema = new Map();
|
|
297
|
+
additionalOptionsSchema.set('play', { type: 'boolean' });
|
|
298
|
+
if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
|
|
299
|
+
cliOptions.set('play', `${outputFilenames.length == 0}`);
|
|
300
|
+
}
|
|
301
|
+
const options = await cliOptionsMapToOptionsObject(cliOptions, "RecognitionOptions", additionalOptionsSchema);
|
|
302
|
+
const { transcript, timeline, rawAudio } = await API.recognizeFile(sourceFilename, options);
|
|
303
|
+
if (outputFilenames.length > 0) {
|
|
304
|
+
progressLogger.start("\nWriting output files");
|
|
305
|
+
}
|
|
306
|
+
for (const outputFile of outputFilenames) {
|
|
307
|
+
const partPatternMatch = outputFile.match(segmentFilenamePattern);
|
|
308
|
+
if (partPatternMatch) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const fileSaver = getFileSaver(outputFile);
|
|
312
|
+
await fileSaver(rawAudio, timeline, transcript);
|
|
313
|
+
}
|
|
314
|
+
if (options.play) {
|
|
315
|
+
const normalizedAudio = normalizeAudioLevel(rawAudio);
|
|
316
|
+
await playAudioWithTimeline(normalizedAudio, timeline, transcript);
|
|
317
|
+
}
|
|
318
|
+
progressLogger.end();
|
|
319
|
+
}
|
|
320
|
+
async function align(commandArgs, cliOptions) {
|
|
321
|
+
const progressLogger = new Logger();
|
|
322
|
+
const audioFilename = commandArgs[0];
|
|
323
|
+
const outputFilenames = commandArgs.slice(2);
|
|
324
|
+
if (audioFilename == undefined) {
|
|
325
|
+
throw new Error(`align requires an argument containing the audio file path.`);
|
|
326
|
+
}
|
|
327
|
+
if (!existsSync(audioFilename)) {
|
|
328
|
+
throw new Error(`The given source file '${audioFilename}' was not found.`);
|
|
329
|
+
}
|
|
330
|
+
const alignmentReferenceFile = commandArgs[1];
|
|
331
|
+
if (alignmentReferenceFile == undefined) {
|
|
332
|
+
throw new Error(`align requires a second argument containing the alignment reference file path.`);
|
|
333
|
+
}
|
|
334
|
+
if (!existsSync(alignmentReferenceFile)) {
|
|
335
|
+
throw new Error(`The given reference file '${alignmentReferenceFile}' was not found.`);
|
|
336
|
+
}
|
|
337
|
+
const referenceFileExtension = getFileExtension(alignmentReferenceFile);
|
|
338
|
+
const fileContent = await readFile(alignmentReferenceFile, { encoding: 'utf-8' });
|
|
339
|
+
let text;
|
|
340
|
+
if (referenceFileExtension == "txt") {
|
|
341
|
+
text = fileContent;
|
|
342
|
+
}
|
|
343
|
+
else if (referenceFileExtension == "html" || referenceFileExtension == "htm") {
|
|
344
|
+
text = await convertHtmlToText(fileContent);
|
|
345
|
+
}
|
|
346
|
+
else if (referenceFileExtension == "srt" || referenceFileExtension == "vtt") {
|
|
347
|
+
text = subtitlesToText(fileContent);
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
throw new Error(`align only supports reference files with extensions 'txt', 'html', 'htm', 'srt' or 'vtt'`);
|
|
351
|
+
}
|
|
352
|
+
const additionalOptionsSchema = new Map();
|
|
353
|
+
additionalOptionsSchema.set('play', { type: 'boolean' });
|
|
354
|
+
if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
|
|
355
|
+
cliOptions.set('play', `${outputFilenames.length == 0}`);
|
|
356
|
+
}
|
|
357
|
+
const options = await cliOptionsMapToOptionsObject(cliOptions, "AlignmentOptions", additionalOptionsSchema);
|
|
358
|
+
const { includesPartPattern } = await checkOutputFilenames(outputFilenames, true, true, true);
|
|
359
|
+
const { wordTimeline, rawAudio, transcript, language } = await API.alignFile(audioFilename, text, options);
|
|
360
|
+
const { segmentTimeline } = await wordTimelineToSegmentSentenceTimeline(wordTimeline, transcript, language);
|
|
361
|
+
if (outputFilenames.length > 0) {
|
|
362
|
+
progressLogger.start("\nWriting output files");
|
|
363
|
+
}
|
|
364
|
+
if (includesPartPattern) {
|
|
365
|
+
for (let segmentIndex = 0; segmentIndex < segmentTimeline.length; segmentIndex++) {
|
|
366
|
+
const segmentEntry = segmentTimeline[segmentIndex];
|
|
367
|
+
const segmentAudio = sliceRawAudioByTime(rawAudio, segmentEntry.startTime, segmentEntry.endTime);
|
|
368
|
+
const sentenceTimeline = addTimeOffsetToTimeline(segmentEntry.timeline, -segmentEntry.startTime);
|
|
369
|
+
await writeOutputFilesForSegment(outputFilenames, segmentIndex, segmentTimeline.length, segmentAudio, sentenceTimeline, segmentEntry.text, language);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
for (const outputFile of outputFilenames) {
|
|
373
|
+
const partPatternMatch = outputFile.match(segmentFilenamePattern);
|
|
374
|
+
if (partPatternMatch) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const fileSaver = getFileSaver(outputFile);
|
|
378
|
+
await fileSaver(rawAudio, segmentTimeline, transcript);
|
|
379
|
+
}
|
|
380
|
+
if (options.play) {
|
|
381
|
+
const normalizedAudio = normalizeAudioLevel(rawAudio);
|
|
382
|
+
const wordTimeline = segmentTimeline.flatMap(segmentEntry => segmentEntry.timeline).flatMap(sentenceEntry => sentenceEntry.timeline);
|
|
383
|
+
await playAudioWithTimeline(normalizedAudio, wordTimeline, transcript);
|
|
384
|
+
}
|
|
385
|
+
progressLogger.end();
|
|
386
|
+
}
|
|
387
|
+
async function translateSpeech(commandArgs, cliOptions) {
|
|
388
|
+
const progressLogger = new Logger();
|
|
389
|
+
const inputFilename = commandArgs[0];
|
|
390
|
+
const outputFilenames = commandArgs.slice(1);
|
|
391
|
+
if (inputFilename == undefined) {
|
|
392
|
+
throw new Error(`translate-speech requires an argument containing the input file path.`);
|
|
393
|
+
}
|
|
394
|
+
if (!existsSync(inputFilename)) {
|
|
395
|
+
throw new Error(`The given input file '${inputFilename}' was not found.`);
|
|
396
|
+
}
|
|
397
|
+
await checkOutputFilenames(outputFilenames, true, true, true);
|
|
398
|
+
const additionalOptionsSchema = new Map();
|
|
399
|
+
additionalOptionsSchema.set('play', { type: 'boolean' });
|
|
400
|
+
if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
|
|
401
|
+
cliOptions.set('play', `${outputFilenames.length == 0}`);
|
|
402
|
+
}
|
|
403
|
+
const options = await cliOptionsMapToOptionsObject(cliOptions, "SpeechTranslationOptions", additionalOptionsSchema);
|
|
404
|
+
const { transcript, timeline, rawAudio, sourceLanguage } = await API.translateSpeechFile(inputFilename, options);
|
|
405
|
+
if (outputFilenames.length > 0) {
|
|
406
|
+
progressLogger.start("\nWriting output files");
|
|
407
|
+
}
|
|
408
|
+
for (const outputFile of outputFilenames) {
|
|
409
|
+
const partPatternMatch = outputFile.match(segmentFilenamePattern);
|
|
410
|
+
if (partPatternMatch) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const fileSaver = getFileSaver(outputFile);
|
|
414
|
+
await fileSaver(rawAudio, timeline, transcript);
|
|
415
|
+
}
|
|
416
|
+
if (options.play) {
|
|
417
|
+
const normalizedAudio = normalizeAudioLevel(rawAudio);
|
|
418
|
+
await playAudioWithTimeline(normalizedAudio, timeline, transcript);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async function detectLanguage(commandArgs, cliOptions, mode) {
|
|
422
|
+
const progressLogger = new Logger();
|
|
423
|
+
const inputFilePath = commandArgs[0];
|
|
424
|
+
const outputFilenames = commandArgs.slice(1);
|
|
425
|
+
if (!existsSync(inputFilePath)) {
|
|
426
|
+
throw new Error(`The given input file '${inputFilePath}' was not found.`);
|
|
427
|
+
}
|
|
428
|
+
await checkOutputFilenames(outputFilenames, false, true, false);
|
|
429
|
+
const inputFileExtension = getFileExtension(inputFilePath);
|
|
430
|
+
const supportedInputTextFormats = ["txt", "srt", "vtt"];
|
|
431
|
+
let results;
|
|
432
|
+
if (mode == "text" || (mode == "auto" && supportedInputTextFormats.includes(inputFileExtension))) {
|
|
433
|
+
if (inputFilePath == undefined) {
|
|
434
|
+
throw new Error(`detect-text-language requires an argument containing the input file path.`);
|
|
435
|
+
}
|
|
436
|
+
if (!supportedInputTextFormats.includes(inputFileExtension)) {
|
|
437
|
+
throw new Error(`'detect-text-language' doesn't support input file extension '${inputFileExtension}'`);
|
|
438
|
+
}
|
|
439
|
+
const options = await cliOptionsMapToOptionsObject(cliOptions, "TextLanguageDetectionOptions");
|
|
440
|
+
let text = await readFile(inputFilePath, { encoding: "utf-8" });
|
|
441
|
+
if (inputFileExtension == "srt" || inputFileExtension == "vtt") {
|
|
442
|
+
text = subtitlesToText(text);
|
|
443
|
+
}
|
|
444
|
+
const { detectedLanguage, detectedLanguageProbabilities } = await API.detectTextLanguage(text, options);
|
|
445
|
+
results = detectedLanguageProbabilities;
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
if (inputFilePath == undefined) {
|
|
449
|
+
throw new Error(`detect-speech-language requires an argument containing the input audio file path.`);
|
|
450
|
+
}
|
|
451
|
+
const options = await cliOptionsMapToOptionsObject(cliOptions, "SpeechLanguageDetectionOptions");
|
|
452
|
+
const { detectedLanguage, detectedLanguageProbabilities } = await API.detectSpeechFileLanguage(inputFilePath, options);
|
|
453
|
+
results = detectedLanguageProbabilities;
|
|
454
|
+
}
|
|
455
|
+
if (outputFilenames.length > 0) {
|
|
456
|
+
progressLogger.start("\nWriting output files");
|
|
457
|
+
const resultsAsText = results.map(result => `${formatLanguageCodeWithName(result.language)}: ${result.probability.toFixed(5)}`).join("\n");
|
|
458
|
+
for (const outputFile of outputFilenames) {
|
|
459
|
+
const fileSaver = getFileSaver(outputFile);
|
|
460
|
+
await fileSaver(getEmptyRawAudio(0, 0), results, resultsAsText);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
const resultsAsText = results.slice(0, 10).map(result => `${formatLanguageCodeWithName(result.language)}: ${result.probability.toFixed(5)}`).join("\n");
|
|
465
|
+
log("");
|
|
466
|
+
log(resultsAsText);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
async function detectVoiceActivity(commandArgs, cliOptions) {
|
|
470
|
+
const progressLogger = new Logger();
|
|
471
|
+
const audioFilename = commandArgs[0];
|
|
472
|
+
const outputFilenames = commandArgs.slice(1);
|
|
473
|
+
if (audioFilename == undefined) {
|
|
474
|
+
throw new Error(`detect-voice-activity requires an argument containing the audio file path.`);
|
|
475
|
+
}
|
|
476
|
+
if (!existsSync(audioFilename)) {
|
|
477
|
+
throw new Error(`The given source audio file '${audioFilename}' was not found.`);
|
|
478
|
+
}
|
|
479
|
+
await checkOutputFilenames(outputFilenames, true, true, false);
|
|
480
|
+
const additionalOptionsSchema = new Map();
|
|
481
|
+
additionalOptionsSchema.set('play', { type: 'boolean' });
|
|
482
|
+
if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
|
|
483
|
+
cliOptions.set('play', `${outputFilenames.length == 0}`);
|
|
484
|
+
}
|
|
485
|
+
const options = await cliOptionsMapToOptionsObject(cliOptions, "VADOptions", additionalOptionsSchema);
|
|
486
|
+
const { timeline, rawAudio } = await API.detectFileVoiceActivity(audioFilename, options);
|
|
487
|
+
if (outputFilenames.length > 0) {
|
|
488
|
+
progressLogger.start("\nWriting output files");
|
|
489
|
+
}
|
|
490
|
+
for (const outputFile of outputFilenames) {
|
|
491
|
+
const partPatternMatch = outputFile.match(segmentFilenamePattern);
|
|
492
|
+
if (partPatternMatch) {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
const fileSaver = getFileSaver(outputFile);
|
|
496
|
+
await fileSaver(rawAudio, timeline, "");
|
|
497
|
+
}
|
|
498
|
+
if (options.play) {
|
|
499
|
+
const normalizedAudio = normalizeAudioLevel(rawAudio);
|
|
500
|
+
await playAudioWithTimeline(normalizedAudio, timeline);
|
|
501
|
+
}
|
|
502
|
+
progressLogger.end();
|
|
503
|
+
}
|
|
504
|
+
async function denoise(commandArgs, cliOptions) {
|
|
505
|
+
const progressLogger = new Logger();
|
|
506
|
+
const audioFilename = commandArgs[0];
|
|
507
|
+
const outputFilenames = commandArgs.slice(1);
|
|
508
|
+
if (audioFilename == undefined) {
|
|
509
|
+
throw new Error(`'denoise' requires an argument containing the audio file path.`);
|
|
510
|
+
}
|
|
511
|
+
if (!existsSync(audioFilename)) {
|
|
512
|
+
throw new Error(`The given source audio file '${audioFilename}' was not found.`);
|
|
513
|
+
}
|
|
514
|
+
await checkOutputFilenames(outputFilenames, true, false, false);
|
|
515
|
+
const additionalOptionsSchema = new Map();
|
|
516
|
+
additionalOptionsSchema.set('play', { type: 'boolean' });
|
|
517
|
+
if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
|
|
518
|
+
cliOptions.set('play', `${outputFilenames.length == 0}`);
|
|
519
|
+
}
|
|
520
|
+
const options = await cliOptionsMapToOptionsObject(cliOptions, "DenoisingOptions", additionalOptionsSchema);
|
|
521
|
+
const outputAudio = await API.denoiseFile(audioFilename, options);
|
|
522
|
+
if (outputFilenames.length > 0) {
|
|
523
|
+
progressLogger.start("\nWriting output files");
|
|
524
|
+
}
|
|
525
|
+
for (const filename of outputFilenames) {
|
|
526
|
+
const fileSaver = getFileSaver(filename);
|
|
527
|
+
await fileSaver(outputAudio, [], "");
|
|
528
|
+
}
|
|
529
|
+
if (options.play) {
|
|
530
|
+
await playAudioSamples(outputAudio);
|
|
531
|
+
}
|
|
532
|
+
progressLogger.end();
|
|
533
|
+
}
|
|
534
|
+
async function listTTSVoices(commandArgs, cliOptions) {
|
|
535
|
+
const progressLogger = new Logger();
|
|
536
|
+
const targetEngine = commandArgs[0];
|
|
537
|
+
const outputFilenames = commandArgs.slice(1);
|
|
538
|
+
if (targetEngine == undefined) {
|
|
539
|
+
const optionsSchema = await getOptionsSchema();
|
|
540
|
+
const { enum: ttsEnginesEnum } = getOptionTypeFromSchema(["VoiceListRequestOptions", "engine"], optionsSchema);
|
|
541
|
+
throw new Error(`list-voices requires an argument specifying one of these supported engines:\n${ttsEnginesEnum.join(", ")}`);
|
|
542
|
+
}
|
|
543
|
+
await checkOutputFilenames(outputFilenames, false, true, false);
|
|
544
|
+
cliOptions.set('engine', targetEngine);
|
|
545
|
+
const options = await cliOptionsMapToOptionsObject(cliOptions, "VoiceListRequestOptions");
|
|
546
|
+
const { voiceList } = await API.requestVoiceList(options);
|
|
547
|
+
const voiceListText = voiceList.map(entry => {
|
|
548
|
+
const nameText = entry.name;
|
|
549
|
+
const languagesNamesText = entry.languages.map(language => formatLanguageCodeWithName(language)).join(", ");
|
|
550
|
+
const genderText = entry.gender;
|
|
551
|
+
let entryText = `Name: ${nameText}\nLanguages: ${languagesNamesText}\nGender: ${genderText}`;
|
|
552
|
+
const speakerCount = entry.speakerCount;
|
|
553
|
+
if (speakerCount) {
|
|
554
|
+
entryText += `\nSpeaker count: ${speakerCount}`;
|
|
555
|
+
}
|
|
556
|
+
return entryText;
|
|
557
|
+
}).join("\n\n");
|
|
558
|
+
if (outputFilenames.length > 0) {
|
|
559
|
+
progressLogger.start("\nWriting output files");
|
|
560
|
+
for (const filename of outputFilenames) {
|
|
561
|
+
const fileSaver = getFileSaver(filename);
|
|
562
|
+
await fileSaver(getEmptyRawAudio(0, 0), voiceList, voiceListText);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
log(voiceListText);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
async function installPackages(commandArgs, cliOptions) {
|
|
570
|
+
if (commandArgs.length == 0) {
|
|
571
|
+
throw new Error("No package names specified");
|
|
572
|
+
}
|
|
573
|
+
const failedPackageNames = [];
|
|
574
|
+
for (const packageName of commandArgs) {
|
|
575
|
+
try {
|
|
576
|
+
await loadPackage(packageName);
|
|
577
|
+
}
|
|
578
|
+
catch (e) {
|
|
579
|
+
log(`Failed installing package ${packageName}: ${e}`);
|
|
580
|
+
failedPackageNames.push(packageName);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (failedPackageNames.length > 0) {
|
|
584
|
+
if (failedPackageNames.length == 1) {
|
|
585
|
+
log(`The package ${failedPackageNames[0]} failed to install`);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
log(`The packages ${failedPackageNames.join(', ')} failed to install`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
async function uninstallPackages(commandArgs, cliOptions) {
|
|
593
|
+
if (commandArgs.length == 0) {
|
|
594
|
+
throw new Error("No package names specified");
|
|
595
|
+
}
|
|
596
|
+
const failedPackageNames = [];
|
|
597
|
+
for (const packageName of commandArgs) {
|
|
598
|
+
try {
|
|
599
|
+
await removePackage(packageName);
|
|
600
|
+
}
|
|
601
|
+
catch (e) {
|
|
602
|
+
log(`Failed uninstalling package ${packageName}: ${e}`);
|
|
603
|
+
failedPackageNames.push(packageName);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (failedPackageNames.length > 0) {
|
|
607
|
+
log(`The packages ${failedPackageNames.join(', ')} failed to uninstall`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async function listPackages(commandArgs, cliOptions) {
|
|
611
|
+
if (commandArgs.length > 0) {
|
|
612
|
+
}
|
|
613
|
+
const packagesDir = await ensureAndGetPackagesDir();
|
|
614
|
+
const packageNames = await readdir(packagesDir);
|
|
615
|
+
packageNames.sort();
|
|
616
|
+
log(`Total of ${packageNames.length} packages installed in '${packagesDir}'\n`);
|
|
617
|
+
log(packageNames.join("\n"));
|
|
618
|
+
}
|
|
619
|
+
async function cliOptionsMapToOptionsObject(cliOptionsMap, optionsRoot, additionalOptionsSchema) {
|
|
620
|
+
const optionsSchema = await getOptionsSchema();
|
|
621
|
+
const resultingObj = {};
|
|
622
|
+
function setValueAtPath(path, value) {
|
|
623
|
+
let currentObject = resultingObj;
|
|
624
|
+
for (let keyIndex = 0; keyIndex < path.length; keyIndex++) {
|
|
625
|
+
const key = path[keyIndex];
|
|
626
|
+
if (keyIndex == path.length - 1) {
|
|
627
|
+
currentObject[key] = value;
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
if (!(key in currentObject)) {
|
|
631
|
+
currentObject[key] = {};
|
|
632
|
+
}
|
|
633
|
+
currentObject = currentObject[key];
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
for (let [key, value] of cliOptionsMap) {
|
|
638
|
+
let isNegated = false;
|
|
639
|
+
if (key.startsWith("no-")) {
|
|
640
|
+
isNegated = true;
|
|
641
|
+
key = key.slice(3);
|
|
642
|
+
if (value) {
|
|
643
|
+
throw new Error(`The negated property '${key}' cannot have a value.`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const path = key.split(".");
|
|
647
|
+
let optionType;
|
|
648
|
+
let optionEnum;
|
|
649
|
+
if (additionalOptionsSchema && additionalOptionsSchema.has(key)) {
|
|
650
|
+
({ type: optionType, enum: optionEnum } = additionalOptionsSchema.get(key));
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
const extendedPath = [optionsRoot, ...path];
|
|
654
|
+
({ type: optionType, enum: optionEnum } = getOptionTypeFromSchema(extendedPath, optionsSchema));
|
|
655
|
+
}
|
|
656
|
+
let parsedValue;
|
|
657
|
+
if (optionType == 'boolean') {
|
|
658
|
+
if (value == null || value == '') {
|
|
659
|
+
parsedValue = !isNegated;
|
|
660
|
+
}
|
|
661
|
+
else if (value == 'true') {
|
|
662
|
+
parsedValue = true;
|
|
663
|
+
}
|
|
664
|
+
else if (value == 'false') {
|
|
665
|
+
parsedValue = false;
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
throw new Error(`The property '${key}' is a Boolean, which can receive either 'true' or 'false', not '${value}'.`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
else if (value == null || value == '') {
|
|
672
|
+
throw new Error(`No value was specified for the property '${key}', which has type ${optionType}.`);
|
|
673
|
+
}
|
|
674
|
+
else if (isNegated) {
|
|
675
|
+
throw new Error(`The property '${key}' is not a Boolean, and cannot be negated using the 'not-' prefix.`);
|
|
676
|
+
}
|
|
677
|
+
else if (optionType == 'number') {
|
|
678
|
+
parsedValue = parseFloat(value);
|
|
679
|
+
if (isNaN(parsedValue)) {
|
|
680
|
+
throw new Error(`The property '${key}' is a number. '${value}' cannot be parsed as a number.`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
parsedValue = value;
|
|
685
|
+
}
|
|
686
|
+
if (optionEnum && !optionEnum.includes(parsedValue)) {
|
|
687
|
+
throw new Error(`The property '${key}' must be one of ${optionEnum.join(", ")}`);
|
|
688
|
+
}
|
|
689
|
+
setValueAtPath(path, parsedValue);
|
|
690
|
+
}
|
|
691
|
+
return resultingObj;
|
|
692
|
+
}
|
|
693
|
+
let cachedOptionsSchema;
|
|
694
|
+
export async function getOptionsSchema() {
|
|
695
|
+
if (!cachedOptionsSchema) {
|
|
696
|
+
cachedOptionsSchema = await readAndParseJsonFile(resolveToModuleRootDir("data/schemas/options.json"));
|
|
697
|
+
}
|
|
698
|
+
return cachedOptionsSchema;
|
|
699
|
+
}
|
|
700
|
+
export async function checkOutputFilenames(outputFilenames, acceptMediaOutputs, acceptMetadataOutputs, acceptSubtitleOutputs) {
|
|
701
|
+
const supportedFileExtensions = [];
|
|
702
|
+
if (acceptMediaOutputs) {
|
|
703
|
+
supportedFileExtensions.push(...supportedOutputMediaFileExtensions);
|
|
704
|
+
}
|
|
705
|
+
if (acceptMetadataOutputs) {
|
|
706
|
+
supportedFileExtensions.push(...supportedMetadataFileExtensions);
|
|
707
|
+
}
|
|
708
|
+
if (acceptSubtitleOutputs) {
|
|
709
|
+
supportedFileExtensions.push(...supportedSubtitleFileExtensions);
|
|
710
|
+
}
|
|
711
|
+
let includesPartPattern = false;
|
|
712
|
+
for (const outputFilename of outputFilenames) {
|
|
713
|
+
const fileExtension = getFileExtension(outputFilename);
|
|
714
|
+
if (!supportedFileExtensions.includes(fileExtension)) {
|
|
715
|
+
let errorText = "";
|
|
716
|
+
errorText += `\nThe specified output path '${outputFilename}' doesn't have a supported file extension.\n`;
|
|
717
|
+
errorText += `\nSupported extensions are:\n`;
|
|
718
|
+
if (acceptMediaOutputs) {
|
|
719
|
+
errorText += `${formatListWithQuotedElements(supportedOutputMediaFileExtensions)} for audio output files.\n`;
|
|
720
|
+
}
|
|
721
|
+
if (acceptMetadataOutputs) {
|
|
722
|
+
errorText += `${formatListWithQuotedElements(supportedMetadataFileExtensions)} for metadata output files.\n`;
|
|
723
|
+
}
|
|
724
|
+
if (acceptMetadataOutputs) {
|
|
725
|
+
errorText += `${formatListWithQuotedElements(supportedSubtitleFileExtensions)} for subtitle output files.\n`;
|
|
726
|
+
}
|
|
727
|
+
throw new Error(errorText);
|
|
728
|
+
}
|
|
729
|
+
const partPatternMatch = outputFilename.match(segmentFilenamePattern);
|
|
730
|
+
if (partPatternMatch) {
|
|
731
|
+
if (partPatternMatch[1] != "segment") {
|
|
732
|
+
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'`);
|
|
733
|
+
}
|
|
734
|
+
includesPartPattern = true;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return { includesPartPattern };
|
|
738
|
+
}
|
|
739
|
+
export async function writeOutputFilesForSegment(outputFilenames, index, total, audio, timeline, text, language) {
|
|
740
|
+
const digitCount = Math.max((total + 1).toString().length, 2);
|
|
741
|
+
const segmentWords = (await splitToWords(text, language)).filter(text => wordCharacterPattern.test(text));
|
|
742
|
+
const segmentJoinedWords = segmentWords.join(" ").trim();
|
|
743
|
+
let initialText;
|
|
744
|
+
const maxLength = 50;
|
|
745
|
+
if (segmentJoinedWords.length < maxLength) {
|
|
746
|
+
initialText = segmentJoinedWords.substring(0, maxLength).trim();
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
initialText = segmentJoinedWords.substring(0, maxLength - 4).trim() + ".. ";
|
|
750
|
+
}
|
|
751
|
+
for (const outputFilename of outputFilenames) {
|
|
752
|
+
const partPatternMatch = outputFilename.match(segmentFilenamePattern);
|
|
753
|
+
if (!partPatternMatch) {
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
const segmentFilename = outputFilename.replace(segmentFilenamePattern, `${formatIntegerWithLeadingZeros(index + 1, digitCount)} - ${initialText}.$2`);
|
|
757
|
+
const fileSaver = getFileSaver(segmentFilename);
|
|
758
|
+
await fileSaver(audio, timeline, text);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
function getFileSaver(outputFilePath) {
|
|
762
|
+
const fileDir = parsePath(outputFilePath).dir || "./";
|
|
763
|
+
const fileExtension = getFileExtension(outputFilePath);
|
|
764
|
+
let fileSaver;
|
|
765
|
+
if (fileExtension == "txt") {
|
|
766
|
+
fileSaver = async (audio, timeline, text) => {
|
|
767
|
+
await ensureDir(fileDir);
|
|
768
|
+
return writeFileSafe(outputFilePath, text, { encoding: "utf-8" });
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
else if (fileExtension == "json") {
|
|
772
|
+
fileSaver = async (audio, timeline, text) => {
|
|
773
|
+
await ensureDir(fileDir);
|
|
774
|
+
const roundedTimeline = roundTimelineTimestamps(timeline);
|
|
775
|
+
return writeFileSafe(outputFilePath, stringifyAndFormatJson(roundedTimeline));
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
else if (fileExtension == "srt") {
|
|
779
|
+
fileSaver = async (audio, timeline, text, subtitlesConfig) => {
|
|
780
|
+
await ensureDir(fileDir);
|
|
781
|
+
subtitlesConfig = extendDeep(subtitlesConfig, { format: "srt" });
|
|
782
|
+
const subtitles = timelineToSubtitles(timeline, subtitlesConfig, false);
|
|
783
|
+
return writeFileSafe(outputFilePath, subtitles);
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
else if (fileExtension == "vtt") {
|
|
787
|
+
fileSaver = async (audio, timeline, text, subtitlesConfig) => {
|
|
788
|
+
await ensureDir(fileDir);
|
|
789
|
+
subtitlesConfig = extendDeep(subtitlesConfig, { format: "webvtt" });
|
|
790
|
+
const subtitles = timelineToSubtitles(timeline, subtitlesConfig, false);
|
|
791
|
+
return writeFileSafe(outputFilePath, subtitles);
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
else if (fileExtension == "wav") {
|
|
795
|
+
fileSaver = async (audio) => {
|
|
796
|
+
await ensureDir(fileDir);
|
|
797
|
+
return writeFileSafe(outputFilePath, encodeWaveBuffer(audio));
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
else if (supportedOutputMediaFileExtensions.includes(fileExtension)) {
|
|
801
|
+
fileSaver = async (audio) => {
|
|
802
|
+
let ffmpegOptions;
|
|
803
|
+
if (fileExtension == "mp3") {
|
|
804
|
+
ffmpegOptions = {
|
|
805
|
+
format: "mp3",
|
|
806
|
+
codec: "libmp3lame",
|
|
807
|
+
bitrate: 64,
|
|
808
|
+
customOptions: []
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
else if (fileExtension == "opus") {
|
|
812
|
+
ffmpegOptions = {
|
|
813
|
+
codec: "libopus",
|
|
814
|
+
bitrate: 48,
|
|
815
|
+
customOptions: []
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
else if (fileExtension == "m4a") {
|
|
819
|
+
ffmpegOptions = {
|
|
820
|
+
format: "mp4",
|
|
821
|
+
codec: "aac",
|
|
822
|
+
bitrate: 48,
|
|
823
|
+
customOptions: ["-profile:a", "aac_low", "-movflags", "frag_keyframe+empty_moov"]
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
else if (fileExtension == "ogg") {
|
|
827
|
+
ffmpegOptions = {
|
|
828
|
+
codec: "libvorbis",
|
|
829
|
+
bitrate: 48,
|
|
830
|
+
customOptions: []
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
else if (fileExtension == "flac") {
|
|
834
|
+
ffmpegOptions = {
|
|
835
|
+
format: "flac",
|
|
836
|
+
customOptions: ["-compression_level", "6"]
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
840
|
+
ffmpegOptions = {
|
|
841
|
+
format: fileExtension,
|
|
842
|
+
customOptions: []
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
ffmpegOptions.filename = outputFilePath;
|
|
846
|
+
await ensureDir(fileDir);
|
|
847
|
+
await encodeFromChannels(audio, ffmpegOptions);
|
|
848
|
+
return;
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
throw new Error("Unsupported file extension");
|
|
853
|
+
}
|
|
854
|
+
return fileSaver;
|
|
855
|
+
}
|
|
856
|
+
const supportedMetadataFileExtensions = ['txt', 'json'];
|
|
857
|
+
const supportedSubtitleFileExtensions = ['srt', 'vtt'];
|
|
858
|
+
const supportedOutputMediaFileExtensions = ["wav", "mp3", "opus", "m4a", "ogg", "flac"];
|
|
859
|
+
const segmentFilenamePattern = /\[(.*)\]\.(.*)$/;
|
|
860
|
+
//# sourceMappingURL=CLI.js.map
|