echogarden 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/data/schemas/options.json +56 -1
  2. package/dist/alignment/DTWSequenceAlignmentWindowed.js +6 -1
  3. package/dist/alignment/DTWSequenceAlignmentWindowed.js.map +1 -1
  4. package/dist/alignment/SpeechAlignment.js +1 -1
  5. package/dist/alignment/SpeechAlignment.js.map +1 -1
  6. package/dist/api/APIOptions.d.ts +3 -0
  7. package/dist/api/Alignment.js +2 -1
  8. package/dist/api/Alignment.js.map +1 -1
  9. package/dist/api/GlobalOptions.d.ts +10 -0
  10. package/dist/api/GlobalOptions.js +21 -3
  11. package/dist/api/GlobalOptions.js.map +1 -1
  12. package/dist/cli/CLI.js +224 -191
  13. package/dist/cli/CLI.js.map +1 -1
  14. package/dist/cli/CLIConfigFile.js +7 -7
  15. package/dist/cli/CLIConfigFile.js.map +1 -1
  16. package/dist/cli/CLIOptions.d.ts +7 -0
  17. package/dist/cli/CLIOptions.js +2 -0
  18. package/dist/cli/CLIOptions.js.map +1 -0
  19. package/dist/cli/CLIParser.d.ts +5 -6
  20. package/dist/cli/CLIParser.js +16 -12
  21. package/dist/cli/CLIParser.js.map +1 -1
  22. package/dist/recognition/WhisperSTT.js +34 -17
  23. package/dist/recognition/WhisperSTT.js.map +1 -1
  24. package/dist/utilities/FileDownloader.js +3 -1
  25. package/dist/utilities/FileDownloader.js.map +1 -1
  26. package/dist/utilities/Logger.d.ts +5 -4
  27. package/dist/utilities/Logger.js +19 -8
  28. package/dist/utilities/Logger.js.map +1 -1
  29. package/dist/utilities/PackageManager.js +3 -2
  30. package/dist/utilities/PackageManager.js.map +1 -1
  31. package/dist/utilities/Utilities.d.ts +1 -1
  32. package/dist/utilities/Utilities.js +9 -9
  33. package/dist/utilities/Utilities.js.map +1 -1
  34. package/docs/API.md +3 -10
  35. package/docs/CLI.md +88 -77
  36. package/docs/Contributing.md +4 -4
  37. package/docs/Engines.md +1 -1
  38. package/docs/Options.md +25 -2
  39. package/docs/Releases.md +8 -8
  40. package/docs/Tasklist.md +17 -19
  41. package/docs/Technical.md +2 -2
  42. package/package.json +3 -3
  43. package/src/alignment/DTWSequenceAlignmentWindowed.ts +7 -1
  44. package/src/alignment/SpeechAlignment.ts +1 -1
  45. package/src/api/APIOptions.ts +3 -0
  46. package/src/api/Alignment.ts +4 -2
  47. package/src/api/GlobalOptions.ts +33 -5
  48. package/src/cli/CLI.ts +257 -205
  49. package/src/cli/CLIConfigFile.ts +7 -7
  50. package/src/cli/CLIOptions.ts +8 -0
  51. package/src/cli/CLIParser.ts +20 -17
  52. package/src/recognition/WhisperSTT.ts +38 -17
  53. package/src/utilities/FileDownloader.ts +5 -1
  54. package/src/utilities/Logger.ts +22 -8
  55. package/src/utilities/PackageManager.ts +4 -3
  56. package/src/utilities/Utilities.ts +9 -9
package/dist/cli/CLI.js CHANGED
@@ -23,6 +23,7 @@ import { startServer } from '../server/Server.js';
23
23
  import { OpenPromise } from '../utilities/OpenPromise.js';
24
24
  import JSON5 from 'json5';
25
25
  import { getLowercaseFileExtension, resolveToModuleRootDir } from '../utilities/PathUtilities.js';
26
+ import { CLIOptionsKeys } from './CLIOptions.js';
26
27
  //const log = logToStderr
27
28
  async function startIfInWorkerThread() {
28
29
  if (isMainThread || !parentPort) {
@@ -46,81 +47,122 @@ async function startIfInWorkerThread() {
46
47
  }
47
48
  export async function start(processArgs) {
48
49
  const logger = new Logger();
49
- let args;
50
+ const operationData = {
51
+ operation: '',
52
+ operationArgs: [],
53
+ globalOptions: {},
54
+ cliOptions: {},
55
+ operationOptionsLookup: new Map(),
56
+ };
50
57
  try {
51
58
  const packageData = await readAndParseJsonFile(resolveToModuleRootDir('package.json'));
52
59
  logger.log(chalk.magentaBright(`Echogarden v${packageData.version}\n`));
53
- const command = processArgs[0];
54
- if (!command || command == 'help') {
55
- logger.log(`Supported operations:\n\n${commandHelp.join('\n')}`);
60
+ const operation = processArgs[0];
61
+ if (!operation || operation == 'help') {
62
+ logger.log(`Supported operations:\n\n${help.join('\n')}`);
56
63
  process.exit(0);
57
64
  }
58
- if (command == '--help') {
59
- logger.log(`There's no command called '--help'. Did you mean to run 'echogarden help'?`);
60
- process.exit(0);
65
+ if (operation == '--help' || operation == '-h') {
66
+ logger.log(`There's no operation called '${operation}'. Did you mean to run 'echogarden help'?`);
67
+ process.exit(1);
61
68
  }
62
- args = parseCLIArguments(command, processArgs.slice(1));
63
- if (!args.options.has('config')) {
69
+ if (operation.startsWith('-')) {
70
+ logger.log(`Operation name '${operation}' is invalid. It cannot start with a hyphen.`);
71
+ process.exit(1);
72
+ }
73
+ const { operationArgs, parsedArgumentsLookup } = parseCLIArguments(processArgs.slice(1));
74
+ if (!parsedArgumentsLookup.has('config')) {
64
75
  const defaultConfigFile = `./${appName}.config`;
65
76
  const defaultJsonConfigFile = defaultConfigFile + '.json';
66
77
  if (existsSync(defaultConfigFile)) {
67
- args.options.set('config', defaultConfigFile);
78
+ parsedArgumentsLookup.set('config', defaultConfigFile);
68
79
  }
69
80
  else if (existsSync(defaultJsonConfigFile)) {
70
- args.options.set('config', defaultJsonConfigFile);
81
+ parsedArgumentsLookup.set('config', defaultJsonConfigFile);
71
82
  }
72
83
  }
73
- if (args.options.has('config')) {
74
- const configFilePath = args.options.get('config');
75
- args.options.delete('config');
76
- let parsedOptionFile;
84
+ if (parsedArgumentsLookup.has('config')) {
85
+ const configFilePath = parsedArgumentsLookup.get('config');
86
+ parsedArgumentsLookup.delete('config');
87
+ let parsedConfigFile;
77
88
  if (configFilePath.endsWith('.config')) {
78
- parsedOptionFile = await parseConfigFile(configFilePath);
89
+ parsedConfigFile = await parseConfigFile(configFilePath);
79
90
  }
80
91
  else if (configFilePath.endsWith('.config.json')) {
81
- parsedOptionFile = await parseJSONConfigFile(configFilePath);
92
+ parsedConfigFile = await parseJSONConfigFile(configFilePath);
82
93
  }
83
94
  else {
84
95
  throw new Error(`Specified config file '${configFilePath}' doesn't have a supported extension. Should be either '.config' or '.config.json'`);
85
96
  }
86
- let sectionName = args.command;
97
+ let sectionName = operation;
87
98
  if (sectionName.startsWith('speak-')) {
88
99
  sectionName = 'speak';
89
100
  }
90
- const newOptions = parsedOptionFile.get(sectionName) || new Map();
91
- for (const [key, value] of args.options) {
92
- newOptions.set(key, value);
101
+ const globalOptionsLookup = new Map();
102
+ const cliOptionsLookup = new Map();
103
+ const operationsOptionsLookup = new Map();
104
+ if (parsedConfigFile.has('global')) {
105
+ for (const [key, value] of parsedConfigFile.get('global')) {
106
+ globalOptionsLookup.set(key, value);
107
+ }
108
+ }
109
+ if (parsedConfigFile.has('cli')) {
110
+ for (const [key, value] of parsedConfigFile.get('cli')) {
111
+ cliOptionsLookup.set(key, value);
112
+ }
113
+ }
114
+ if (parsedConfigFile.has(sectionName)) {
115
+ for (const [key, value] of parsedConfigFile.get(sectionName)) {
116
+ operationsOptionsLookup.set(key, value);
117
+ }
118
+ }
119
+ const globalOptionsKeys = API.listGlobalOptions();
120
+ const cliOptionsKeys = CLIOptionsKeys;
121
+ for (const [key, value] of parsedArgumentsLookup) {
122
+ if (globalOptionsKeys.includes(key)) {
123
+ globalOptionsLookup.set(key, value);
124
+ }
125
+ else if (cliOptionsKeys.includes(key)) {
126
+ cliOptionsLookup.set(key, value);
127
+ }
128
+ else {
129
+ operationsOptionsLookup.set(key, value);
130
+ }
93
131
  }
94
- args.options = newOptions;
132
+ operationData.operation = operation;
133
+ operationData.operationArgs = operationArgs;
134
+ operationData.globalOptions = await optionsLookupToTypedObject(globalOptionsLookup, 'GlobalOptions');
135
+ operationData.cliOptions = await optionsLookupToTypedObject(cliOptionsLookup, 'CLIOptions');
136
+ operationData.operationOptionsLookup = operationsOptionsLookup;
95
137
  }
96
138
  }
97
139
  catch (e) {
98
140
  resetActiveLogger();
99
- logger.logTitledMessage(`Error`, e.message, chalk.redBright);
141
+ logger.logTitledMessage(`Error`, e.message, chalk.redBright, 'error');
100
142
  process.exit(1);
101
143
  }
102
- let debugMode = false;
103
- if (args.options.has('debug')) {
104
- args.options.delete('debug');
105
- debugMode = true;
144
+ for (const key in operationData.globalOptions) {
145
+ const value = operationData.globalOptions[key];
146
+ API.setGlobalOption(key, value);
106
147
  }
148
+ const debugMode = operationData.cliOptions.debug || false;
107
149
  try {
108
- await startWithArgs(args);
150
+ await startWithArgs(operationData);
109
151
  }
110
152
  catch (e) {
111
153
  resetActiveLogger();
112
154
  if (debugMode) {
113
- logger.log(e);
155
+ logger.log(e, 'error');
114
156
  }
115
157
  else {
116
- logger.logTitledMessage(`Error`, e.message, chalk.redBright);
158
+ logger.logTitledMessage(`Error`, e.message, chalk.redBright, 'error');
117
159
  }
118
160
  process.exit(1);
119
161
  }
120
162
  process.exit(0);
121
163
  }
122
164
  const executableName = `${chalk.cyanBright('echogarden')}`;
123
- const commandHelp = [
165
+ const help = [
124
166
  `${executableName} ${chalk.magentaBright('speak')} text [output files...] [options...]`,
125
167
  ` Speak the given text\n`,
126
168
  `${executableName} ${chalk.magentaBright('speak-file')} inputFile [output files...] [options...]`,
@@ -160,103 +202,103 @@ const commandHelp = [
160
202
  `${executableName} ${chalk.magentaBright('serve')} [options...]`,
161
203
  ` Start a server\n`,
162
204
  `Options reference: ${chalk.blueBright('https://bit.ly/echogarden-options')}`
163
- //` ${chalk.blueBright('https://github.com/echogarden-project/echogarden/blob/main/docs/Options.md')}`
164
205
  ];
165
- async function startWithArgs(parsedArgs) {
206
+ async function startWithArgs(operationData) {
166
207
  const logger = new Logger();
167
- switch (parsedArgs.command) {
208
+ switch (operationData.operation) {
168
209
  case 'speak':
169
210
  case 'speak-file':
170
211
  case 'speak-url':
171
212
  case 'speak-wikipedia': {
172
- await speak(parsedArgs.command, parsedArgs.commandArgs, parsedArgs.options);
213
+ await speak(operationData);
173
214
  break;
174
215
  }
175
216
  case 'transcribe': {
176
- await transcribe(parsedArgs.commandArgs, parsedArgs.options);
217
+ await transcribe(operationData);
177
218
  break;
178
219
  }
179
220
  case 'align': {
180
- await align(parsedArgs.commandArgs, parsedArgs.options);
221
+ await align(operationData);
181
222
  break;
182
223
  }
183
224
  case 'translate-speech': {
184
- await translateSpeech(parsedArgs.commandArgs, parsedArgs.options);
225
+ await translateSpeech(operationData);
185
226
  break;
186
227
  }
187
228
  case 'align-translation': {
188
- await alignTranslation(parsedArgs.commandArgs, parsedArgs.options);
229
+ await alignTranslation(operationData);
189
230
  break;
190
231
  }
191
232
  case 'detect-language': {
192
- await detectLanguage(parsedArgs.commandArgs, parsedArgs.options, 'auto');
233
+ await detectLanguage(operationData, 'auto');
193
234
  break;
194
235
  }
195
236
  case 'detect-speech-language': {
196
- await detectLanguage(parsedArgs.commandArgs, parsedArgs.options, 'speech');
237
+ await detectLanguage(operationData, 'speech');
197
238
  break;
198
239
  }
199
240
  case 'detect-text-language': {
200
- await detectLanguage(parsedArgs.commandArgs, parsedArgs.options, 'text');
241
+ await detectLanguage(operationData, 'text');
201
242
  break;
202
243
  }
203
244
  case 'detect-voice-activity': {
204
- await detectVoiceActivity(parsedArgs.commandArgs, parsedArgs.options);
245
+ await detectVoiceActivity(operationData);
205
246
  break;
206
247
  }
207
248
  case 'denoise': {
208
- await denoise(parsedArgs.commandArgs, parsedArgs.options);
249
+ await denoise(operationData);
209
250
  break;
210
251
  }
211
252
  case 'isolate': {
212
- await isolate(parsedArgs.commandArgs, parsedArgs.options);
253
+ await isolate(operationData);
213
254
  break;
214
255
  }
215
256
  case 'list-engines': {
216
- await listEngines(parsedArgs.commandArgs, parsedArgs.options);
257
+ await listEngines(operationData);
217
258
  break;
218
259
  }
219
260
  case 'list-voices': {
220
- await listTTSVoices(parsedArgs.commandArgs, parsedArgs.options);
261
+ await listTTSVoices(operationData);
221
262
  break;
222
263
  }
223
264
  case 'install': {
224
- await installPackages(parsedArgs.commandArgs, parsedArgs.options);
265
+ await installPackages(operationData);
225
266
  break;
226
267
  }
227
268
  case 'uninstall': {
228
- await uninstallPackages(parsedArgs.commandArgs, parsedArgs.options);
269
+ await uninstallPackages(operationData);
229
270
  break;
230
271
  }
231
272
  case 'list-packages': {
232
- await listPackages(parsedArgs.commandArgs, parsedArgs.options);
273
+ await listPackages(operationData);
233
274
  break;
234
275
  }
235
276
  case 'serve': {
236
- await serve(parsedArgs.commandArgs, parsedArgs.options);
277
+ await serve(operationData);
237
278
  break;
238
279
  }
239
280
  default: {
240
- logger.logTitledMessage(`Unknown command`, parsedArgs.command, chalk.redBright);
281
+ logger.logTitledMessage(`Unknown operation`, operationData.operation, chalk.redBright, 'error');
241
282
  process.exit(1);
242
283
  }
243
284
  }
244
285
  }
245
- async function speak(command, commandArgs, cliOptions) {
286
+ async function speak(operationData) {
246
287
  const logger = new Logger();
247
- const mainArg = commandArgs[0];
248
- const outputFilenames = commandArgs.slice(1);
288
+ const { operationArgs, operation, operationOptionsLookup, cliOptions } = operationData;
289
+ const mainArg = operationArgs[0];
290
+ const outputFilenames = operationArgs.slice(1);
249
291
  if (mainArg == undefined) {
250
- if (command == 'speak') {
292
+ if (operation == 'speak') {
251
293
  throw new Error(`'speak' requires an argument containing the text to speak.`);
252
294
  }
253
- else if (command == 'speak-file') {
295
+ else if (operation == 'speak-file') {
254
296
  throw new Error(`'speak-file' requires an argument containing the file to speak.`);
255
297
  }
256
- else if (command == 'speak-url') {
298
+ else if (operation == 'speak-url') {
257
299
  throw new Error(`'speak-url' requires an argument containing the url to speak.`);
258
300
  }
259
- else if (command == 'speak-wikipedia') {
301
+ else if (operation == 'speak-wikipedia') {
260
302
  throw new Error(`'speak-wikipedia' requires an argument containing the name of the Wikipedia article to speak.`);
261
303
  }
262
304
  return;
@@ -264,17 +306,17 @@ async function speak(command, commandArgs, cliOptions) {
264
306
  const additionalOptionsSchema = new Map();
265
307
  additionalOptionsSchema.set('play', { type: 'boolean' });
266
308
  additionalOptionsSchema.set('overwrite', { type: 'boolean' });
267
- if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
268
- cliOptions.set('play', `${outputFilenames.length == 0}`);
309
+ if (cliOptions.play == null) {
310
+ cliOptions.play = outputFilenames.length === 0;
269
311
  }
270
- const options = await cliOptionsMapToOptionsObject(cliOptions, 'SynthesisOptions', additionalOptionsSchema);
271
- const allowOverwrite = getWithDefault(options.overwrite, overwriteByDefault);
312
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'SynthesisOptions', additionalOptionsSchema);
313
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault);
272
314
  const { includesPlaceholderPattern } = await checkOutputFilenames(outputFilenames, true, true, true);
273
315
  let plainText = undefined;
274
316
  let textSegments;
275
317
  const plainTextParagraphBreaks = options.plainText?.paragraphBreaks || API.defaultSynthesisOptions.plainText.paragraphBreaks;
276
318
  const plainTextWhitespace = options.plainText?.whitespace || API.defaultSynthesisOptions.plainText.whitespace;
277
- if (command == 'speak') {
319
+ if (operation == 'speak') {
278
320
  if (options.ssml) {
279
321
  textSegments = [mainArg];
280
322
  }
@@ -283,7 +325,7 @@ async function speak(command, commandArgs, cliOptions) {
283
325
  }
284
326
  plainText = mainArg;
285
327
  }
286
- else if (command == 'speak-file') {
328
+ else if (operation == 'speak-file') {
287
329
  const sourceFile = mainArg;
288
330
  if (!existsSync(sourceFile)) {
289
331
  throw new Error(`The given source file '${sourceFile}' was not found.`);
@@ -313,9 +355,9 @@ async function speak(command, commandArgs, cliOptions) {
313
355
  throw new Error(`'speak-file' only supports inputs with extensions 'txt', 'html', 'htm', 'xml', 'ssml', 'srt', 'vtt'`);
314
356
  }
315
357
  }
316
- else if (command == 'speak-url') {
358
+ else if (operation == 'speak-url') {
317
359
  if (options.ssml) {
318
- throw new Error(`speak-url doesn't provide SSML inputs`);
360
+ throw new Error(`speak-url doesn't accept SSML inputs`);
319
361
  }
320
362
  const url = mainArg;
321
363
  if (!url.startsWith('http://') && !url.startsWith('https://')) {
@@ -325,7 +367,7 @@ async function speak(command, commandArgs, cliOptions) {
325
367
  const textContent = await fetchDocumentText(url);
326
368
  textSegments = splitToParagraphs(textContent, 'single', 'preserve');
327
369
  }
328
- else if (command == 'speak-wikipedia') {
370
+ else if (operation == 'speak-wikipedia') {
329
371
  if (options.ssml) {
330
372
  throw new Error(`speak-wikipedia doesn't provide SSML inputs`);
331
373
  }
@@ -336,7 +378,7 @@ async function speak(command, commandArgs, cliOptions) {
336
378
  textSegments = await parseWikipediaArticle(mainArg, getShortLanguageCode(options.language));
337
379
  }
338
380
  else {
339
- throw new Error('Invalid command');
381
+ throw new Error(`Invalid operation specified: '${operation}'`);
340
382
  }
341
383
  async function onSegment(segmentData) {
342
384
  if (includesPlaceholderPattern) {
@@ -344,7 +386,7 @@ async function speak(command, commandArgs, cliOptions) {
344
386
  }
345
387
  await writeOutputFilesForSegment(outputFilenames, segmentData.index, segmentData.total, segmentData.audio, segmentData.timeline, segmentData.transcript, segmentData.language, allowOverwrite);
346
388
  logger.end();
347
- if (options.play) {
389
+ if (cliOptions.play) {
348
390
  let gainAmount = -3 - segmentData.peakDecibelsSoFar;
349
391
  //gainAmount = Math.min(gainAmount, 0)
350
392
  const audioWithAddedGain = applyGainDecibels(segmentData.audio, gainAmount);
@@ -372,24 +414,22 @@ async function speak(command, commandArgs, cliOptions) {
372
414
  }
373
415
  logger.end();
374
416
  }
375
- async function transcribe(commandArgs, cliOptions) {
417
+ async function transcribe(operationData) {
376
418
  const logger = new Logger();
377
- const sourceFilename = commandArgs[0];
378
- const outputFilenames = commandArgs.slice(1);
419
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData;
420
+ const sourceFilename = operationArgs[0];
421
+ const outputFilenames = operationArgs.slice(1);
379
422
  if (sourceFilename == undefined) {
380
423
  throw new Error(`'transcribe' requires an argument containing the source file name.`);
381
424
  }
382
425
  if (!existsSync(sourceFilename)) {
383
426
  throw new Error(`The given source audio file '${sourceFilename}' was not found.`);
384
427
  }
385
- const additionalOptionsSchema = new Map();
386
- additionalOptionsSchema.set('play', { type: 'boolean' });
387
- additionalOptionsSchema.set('overwrite', { type: 'boolean' });
388
- if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
389
- cliOptions.set('play', `${outputFilenames.length == 0}`);
428
+ if (cliOptions.play == null) {
429
+ cliOptions.play = outputFilenames.length === 0;
390
430
  }
391
- const options = await cliOptionsMapToOptionsObject(cliOptions, 'RecognitionOptions', additionalOptionsSchema);
392
- const allowOverwrite = getWithDefault(options.overwrite, overwriteByDefault);
431
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'RecognitionOptions');
432
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault);
393
433
  const { includesPlaceholderPattern } = await checkOutputFilenames(outputFilenames, true, true, true);
394
434
  const { transcript, timeline, wordTimeline, language, inputRawAudio, isolatedRawAudio, backgroundRawAudio } = await API.recognize(sourceFilename, options);
395
435
  if (outputFilenames.length > 0) {
@@ -405,7 +445,7 @@ async function transcribe(commandArgs, cliOptions) {
405
445
  await writeSourceSeparationOutputIfNeeded(outputFilename, isolatedRawAudio, backgroundRawAudio, allowOverwrite, true);
406
446
  }
407
447
  logger.end();
408
- if (options.play) {
448
+ if (cliOptions.play) {
409
449
  let audioToPlay;
410
450
  if (isolatedRawAudio) {
411
451
  audioToPlay = isolatedRawAudio;
@@ -417,17 +457,18 @@ async function transcribe(commandArgs, cliOptions) {
417
457
  await playAudioWithWordTimeline(normalizedAudioToPlay, wordTimeline, transcript);
418
458
  }
419
459
  }
420
- async function align(commandArgs, cliOptions) {
460
+ async function align(operationData) {
421
461
  const logger = new Logger();
422
- const audioFilename = commandArgs[0];
423
- const outputFilenames = commandArgs.slice(2);
462
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData;
463
+ const audioFilename = operationArgs[0];
464
+ const outputFilenames = operationArgs.slice(2);
424
465
  if (audioFilename == undefined) {
425
466
  throw new Error(`align requires an argument containing the audio file path.`);
426
467
  }
427
468
  if (!existsSync(audioFilename)) {
428
469
  throw new Error(`The given source file '${audioFilename}' was not found.`);
429
470
  }
430
- const alignmentReferenceFile = commandArgs[1];
471
+ const alignmentReferenceFile = operationArgs[1];
431
472
  if (alignmentReferenceFile == undefined) {
432
473
  throw new Error(`align requires a second argument containing the alignment reference file path.`);
433
474
  }
@@ -449,14 +490,11 @@ async function align(commandArgs, cliOptions) {
449
490
  else {
450
491
  throw new Error(`align only supports reference files with extensions 'txt', 'html', 'htm', 'srt' or 'vtt'`);
451
492
  }
452
- const additionalOptionsSchema = new Map();
453
- additionalOptionsSchema.set('play', { type: 'boolean' });
454
- additionalOptionsSchema.set('overwrite', { type: 'boolean' });
455
- if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
456
- cliOptions.set('play', `${outputFilenames.length == 0}`);
493
+ if (cliOptions.play == null) {
494
+ cliOptions.play = outputFilenames.length === 0;
457
495
  }
458
- const options = await cliOptionsMapToOptionsObject(cliOptions, 'AlignmentOptions', additionalOptionsSchema);
459
- const allowOverwrite = getWithDefault(options.overwrite, overwriteByDefault);
496
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'AlignmentOptions');
497
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault);
460
498
  const { includesPlaceholderPattern } = await checkOutputFilenames(outputFilenames, true, true, true);
461
499
  const { timeline, wordTimeline, transcript, language, inputRawAudio, isolatedRawAudio, backgroundRawAudio } = await API.align(audioFilename, text, options);
462
500
  if (outputFilenames.length > 0) {
@@ -480,7 +518,7 @@ async function align(commandArgs, cliOptions) {
480
518
  await writeSourceSeparationOutputIfNeeded(outputFilename, isolatedRawAudio, backgroundRawAudio, allowOverwrite, true);
481
519
  }
482
520
  logger.end();
483
- if (options.play) {
521
+ if (cliOptions.play) {
484
522
  let audioToPlay;
485
523
  if (isolatedRawAudio) {
486
524
  audioToPlay = isolatedRawAudio;
@@ -492,17 +530,18 @@ async function align(commandArgs, cliOptions) {
492
530
  await playAudioWithWordTimeline(normalizedAudioToPlay, wordTimeline, transcript);
493
531
  }
494
532
  }
495
- async function alignTranslation(commandArgs, cliOptions) {
533
+ async function alignTranslation(operationData) {
496
534
  const logger = new Logger();
497
- const audioFilename = commandArgs[0];
498
- const outputFilenames = commandArgs.slice(2);
535
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData;
536
+ const audioFilename = operationArgs[0];
537
+ const outputFilenames = operationArgs.slice(2);
499
538
  if (audioFilename == undefined) {
500
539
  throw new Error(`align-translation requires an argument containing the audio file path.`);
501
540
  }
502
541
  if (!existsSync(audioFilename)) {
503
542
  throw new Error(`The given source file '${audioFilename}' was not found.`);
504
543
  }
505
- const alignmentReferenceFile = commandArgs[1];
544
+ const alignmentReferenceFile = operationArgs[1];
506
545
  if (alignmentReferenceFile == undefined) {
507
546
  throw new Error(`align-translation requires a second argument containing the translated reference file path.`);
508
547
  }
@@ -524,14 +563,11 @@ async function alignTranslation(commandArgs, cliOptions) {
524
563
  else {
525
564
  throw new Error(`align only supports reference files with extensions 'txt', 'html', 'htm', 'srt' or 'vtt'`);
526
565
  }
527
- const additionalOptionsSchema = new Map();
528
- additionalOptionsSchema.set('play', { type: 'boolean' });
529
- additionalOptionsSchema.set('overwrite', { type: 'boolean' });
530
- if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
531
- cliOptions.set('play', `${outputFilenames.length == 0}`);
566
+ if (cliOptions.play == null) {
567
+ cliOptions.play = outputFilenames.length === 0;
532
568
  }
533
- const options = await cliOptionsMapToOptionsObject(cliOptions, 'TranslationAlignmentOptions', additionalOptionsSchema);
534
- const allowOverwrite = getWithDefault(options.overwrite, overwriteByDefault);
569
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'TranslationAlignmentOptions');
570
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault);
535
571
  const { includesPlaceholderPattern } = await checkOutputFilenames(outputFilenames, true, true, true);
536
572
  const { timeline, wordTimeline, transcript, language, inputRawAudio, isolatedRawAudio, backgroundRawAudio } = await API.alignTranslation(audioFilename, text, options);
537
573
  if (outputFilenames.length > 0) {
@@ -555,7 +591,7 @@ async function alignTranslation(commandArgs, cliOptions) {
555
591
  await writeSourceSeparationOutputIfNeeded(outputFilename, isolatedRawAudio, backgroundRawAudio, allowOverwrite, true);
556
592
  }
557
593
  logger.end();
558
- if (options.play) {
594
+ if (cliOptions.play) {
559
595
  let audioToPlay;
560
596
  if (isolatedRawAudio) {
561
597
  audioToPlay = isolatedRawAudio;
@@ -567,24 +603,22 @@ async function alignTranslation(commandArgs, cliOptions) {
567
603
  await playAudioWithWordTimeline(normalizedAudioToPlay, wordTimeline, transcript);
568
604
  }
569
605
  }
570
- async function translateSpeech(commandArgs, cliOptions) {
606
+ async function translateSpeech(operationData) {
571
607
  const logger = new Logger();
572
- const inputFilename = commandArgs[0];
573
- const outputFilenames = commandArgs.slice(1);
608
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData;
609
+ const inputFilename = operationArgs[0];
610
+ const outputFilenames = operationArgs.slice(1);
574
611
  if (inputFilename == undefined) {
575
612
  throw new Error(`translate-speech requires an argument containing the input file path.`);
576
613
  }
577
614
  if (!existsSync(inputFilename)) {
578
615
  throw new Error(`The given input file '${inputFilename}' was not found.`);
579
616
  }
580
- const additionalOptionsSchema = new Map();
581
- additionalOptionsSchema.set('play', { type: 'boolean' });
582
- additionalOptionsSchema.set('overwrite', { type: 'boolean' });
583
- if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
584
- cliOptions.set('play', `${outputFilenames.length == 0}`);
617
+ if (cliOptions.play == null) {
618
+ cliOptions.play = outputFilenames.length === 0;
585
619
  }
586
- const options = await cliOptionsMapToOptionsObject(cliOptions, 'SpeechTranslationOptions', additionalOptionsSchema);
587
- const allowOverwrite = getWithDefault(options.overwrite, overwriteByDefault);
620
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'SpeechTranslationOptions');
621
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault);
588
622
  await checkOutputFilenames(outputFilenames, true, true, true);
589
623
  const { transcript, timeline, wordTimeline, sourceLanguage, targetLanguage, inputRawAudio, isolatedRawAudio, backgroundRawAudio } = await API.translateSpeech(inputFilename, options);
590
624
  if (outputFilenames.length > 0) {
@@ -600,7 +634,7 @@ async function translateSpeech(commandArgs, cliOptions) {
600
634
  await writeSourceSeparationOutputIfNeeded(outputFilename, isolatedRawAudio, backgroundRawAudio, allowOverwrite, true);
601
635
  }
602
636
  logger.end();
603
- if (options.play) {
637
+ if (cliOptions.play) {
604
638
  let audioToPlay;
605
639
  if (isolatedRawAudio) {
606
640
  audioToPlay = isolatedRawAudio;
@@ -632,15 +666,14 @@ async function translateSpeech(commandArgs, cliOptions) {
632
666
  await playAudioWithWordTimeline(normalizedAudioToPlay, timelineToPlay, transcriptToPlay);
633
667
  }
634
668
  }
635
- async function detectLanguage(commandArgs, cliOptions, mode) {
669
+ async function detectLanguage(operationData, mode) {
636
670
  const logger = new Logger();
637
- const inputFilePath = commandArgs[0];
638
- const outputFilenames = commandArgs.slice(1);
671
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData;
672
+ const inputFilePath = operationArgs[0];
673
+ const outputFilenames = operationArgs.slice(1);
639
674
  if (!existsSync(inputFilePath)) {
640
675
  throw new Error(`The given input file '${inputFilePath}' was not found.`);
641
676
  }
642
- const additionalOptionsSchema = new Map();
643
- additionalOptionsSchema.set('overwrite', { type: 'boolean' });
644
677
  const inputFileExtension = getLowercaseFileExtension(inputFilePath);
645
678
  const supportedInputTextFormats = ['txt', 'srt', 'vtt'];
646
679
  let results;
@@ -652,8 +685,8 @@ async function detectLanguage(commandArgs, cliOptions, mode) {
652
685
  if (!supportedInputTextFormats.includes(inputFileExtension)) {
653
686
  throw new Error(`'detect-text-language' doesn't support input file extension '${inputFileExtension}'`);
654
687
  }
655
- const options = await cliOptionsMapToOptionsObject(cliOptions, 'TextLanguageDetectionOptions', additionalOptionsSchema);
656
- allowOverwrite = getWithDefault(options.overwrite, overwriteByDefault);
688
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'TextLanguageDetectionOptions');
689
+ allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault);
657
690
  await checkOutputFilenames(outputFilenames, false, true, false);
658
691
  let text = await readFile(inputFilePath, { encoding: 'utf-8' });
659
692
  if (inputFileExtension == 'srt' || inputFileExtension == 'vtt') {
@@ -666,8 +699,8 @@ async function detectLanguage(commandArgs, cliOptions, mode) {
666
699
  if (inputFilePath == undefined) {
667
700
  throw new Error(`detect-speech-language requires an argument containing the input audio file path.`);
668
701
  }
669
- const options = await cliOptionsMapToOptionsObject(cliOptions, 'SpeechLanguageDetectionOptions', additionalOptionsSchema);
670
- allowOverwrite = getWithDefault(options.overwrite, overwriteByDefault);
702
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'SpeechLanguageDetectionOptions');
703
+ allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault);
671
704
  await checkOutputFilenames(outputFilenames, false, true, false);
672
705
  const { detectedLanguage, detectedLanguageProbabilities } = await API.detectSpeechLanguage(inputFilePath, options);
673
706
  results = detectedLanguageProbabilities;
@@ -682,29 +715,27 @@ async function detectLanguage(commandArgs, cliOptions, mode) {
682
715
  }
683
716
  else {
684
717
  const resultsAsText = results.slice(0, 10).map(result => `${formatLanguageCodeWithName(result.language)}: ${result.probability.toFixed(5)}`).join('\n');
685
- logger.log('');
686
- logger.log(resultsAsText);
718
+ logger.log('', 'output');
719
+ logger.log(resultsAsText, 'output');
687
720
  }
688
721
  logger.end();
689
722
  }
690
- async function detectVoiceActivity(commandArgs, cliOptions) {
723
+ async function detectVoiceActivity(operationData) {
691
724
  const logger = new Logger();
692
- const audioFilename = commandArgs[0];
693
- const outputFilenames = commandArgs.slice(1);
725
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData;
726
+ const audioFilename = operationArgs[0];
727
+ const outputFilenames = operationArgs.slice(1);
694
728
  if (audioFilename == undefined) {
695
729
  throw new Error(`detect-voice-activity requires an argument containing the audio file path.`);
696
730
  }
697
731
  if (!existsSync(audioFilename)) {
698
732
  throw new Error(`The given source audio file '${audioFilename}' was not found.`);
699
733
  }
700
- const additionalOptionsSchema = new Map();
701
- additionalOptionsSchema.set('play', { type: 'boolean' });
702
- additionalOptionsSchema.set('overwrite', { type: 'boolean' });
703
- if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
704
- cliOptions.set('play', `${outputFilenames.length == 0}`);
734
+ if (cliOptions.play == null) {
735
+ cliOptions.play = outputFilenames.length === 0;
705
736
  }
706
- const options = await cliOptionsMapToOptionsObject(cliOptions, 'VADOptions', additionalOptionsSchema);
707
- const allowOverwrite = getWithDefault(options.overwrite, overwriteByDefault);
737
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'VADOptions');
738
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault);
708
739
  await checkOutputFilenames(outputFilenames, true, true, true);
709
740
  let { timeline, verboseTimeline, inputRawAudio, croppedRawAudio } = await API.detectVoiceActivity(audioFilename, options);
710
741
  if (outputFilenames.length > 0) {
@@ -726,7 +757,7 @@ async function detectVoiceActivity(commandArgs, cliOptions) {
726
757
  }
727
758
  }
728
759
  logger.end();
729
- if (options.play) {
760
+ if (cliOptions.play) {
730
761
  const normalizedAudio = normalizeAudioLevel(inputRawAudio);
731
762
  const timelineToPlay = verboseTimeline.map(entry => {
732
763
  return { ...entry, type: 'word' };
@@ -734,24 +765,22 @@ async function detectVoiceActivity(commandArgs, cliOptions) {
734
765
  await playAudioWithWordTimeline(normalizedAudio, timelineToPlay);
735
766
  }
736
767
  }
737
- async function denoise(commandArgs, cliOptions) {
768
+ async function denoise(operationData) {
738
769
  const logger = new Logger();
739
- const audioFilename = commandArgs[0];
740
- const outputFilenames = commandArgs.slice(1);
770
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData;
771
+ const audioFilename = operationArgs[0];
772
+ const outputFilenames = operationArgs.slice(1);
741
773
  if (audioFilename == undefined) {
742
774
  throw new Error(`'denoise' requires an argument containing the audio file path.`);
743
775
  }
744
776
  if (!existsSync(audioFilename)) {
745
777
  throw new Error(`The given source audio file '${audioFilename}' was not found.`);
746
778
  }
747
- const additionalOptionsSchema = new Map();
748
- additionalOptionsSchema.set('play', { type: 'boolean' });
749
- additionalOptionsSchema.set('overwrite', { type: 'boolean' });
750
- if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
751
- cliOptions.set('play', `${outputFilenames.length == 0}`);
779
+ if (cliOptions.play == null) {
780
+ cliOptions.play = outputFilenames.length === 0;
752
781
  }
753
- const options = await cliOptionsMapToOptionsObject(cliOptions, 'DenoisingOptions', additionalOptionsSchema);
754
- const allowOverwrite = getWithDefault(options.overwrite, overwriteByDefault);
782
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'DenoisingOptions');
783
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault);
755
784
  await checkOutputFilenames(outputFilenames, true, false, false);
756
785
  const { denoisedAudio } = await API.denoise(audioFilename, options);
757
786
  if (outputFilenames.length > 0) {
@@ -762,28 +791,26 @@ async function denoise(commandArgs, cliOptions) {
762
791
  await fileSaver(denoisedAudio, [], '');
763
792
  }
764
793
  logger.end();
765
- if (options.play) {
794
+ if (cliOptions.play) {
766
795
  await playAudioSamples(denoisedAudio);
767
796
  }
768
797
  }
769
- async function isolate(commandArgs, cliOptions) {
798
+ async function isolate(operationData) {
770
799
  const logger = new Logger();
771
- const audioFilename = commandArgs[0];
772
- const outputFilenames = commandArgs.slice(1);
800
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData;
801
+ const audioFilename = operationArgs[0];
802
+ const outputFilenames = operationArgs.slice(1);
773
803
  if (audioFilename == undefined) {
774
804
  throw new Error(`'isolate' requires an argument containing the audio file path.`);
775
805
  }
776
806
  if (!existsSync(audioFilename)) {
777
807
  throw new Error(`The given source audio file '${audioFilename}' was not found.`);
778
808
  }
779
- const additionalOptionsSchema = new Map();
780
- additionalOptionsSchema.set('play', { type: 'boolean' });
781
- additionalOptionsSchema.set('overwrite', { type: 'boolean' });
782
- if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
783
- cliOptions.set('play', `${outputFilenames.length == 0}`);
809
+ if (cliOptions.play == null) {
810
+ cliOptions.play = outputFilenames.length === 0;
784
811
  }
785
- const options = await cliOptionsMapToOptionsObject(cliOptions, 'SourceSeparationOptions', additionalOptionsSchema);
786
- const allowOverwrite = getWithDefault(options.overwrite, overwriteByDefault);
812
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'SourceSeparationOptions');
813
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault);
787
814
  await checkOutputFilenames(outputFilenames, true, false, false);
788
815
  const { inputRawAudio, isolatedRawAudio, backgroundRawAudio } = await API.isolate(audioFilename, options);
789
816
  if (outputFilenames.length > 0) {
@@ -793,15 +820,16 @@ async function isolate(commandArgs, cliOptions) {
793
820
  await writeSourceSeparationOutputIfNeeded(outputFilename, isolatedRawAudio, backgroundRawAudio, allowOverwrite, false);
794
821
  }
795
822
  logger.end();
796
- if (options.play) {
823
+ if (cliOptions.play) {
797
824
  await playAudioSamples(isolatedRawAudio);
798
825
  }
799
826
  }
800
- async function listEngines(commandArgs, cliOptions) {
827
+ async function listEngines(operationData) {
801
828
  const logger = new Logger();
802
- const targetOperation = commandArgs[0];
829
+ const { operationArgs } = operationData;
830
+ const targetOperation = operationArgs[0];
803
831
  if (!targetOperation) {
804
- throw new Error(`The 'list-engines' command requires an argument specifying the operation to list engines for, like 'echogarden list-engines transcribe'.`);
832
+ throw new Error(`The 'list-engines' operation requires an argument specifying the operation to list engines for, like 'echogarden list-engines transcribe'.`);
805
833
  }
806
834
  let engines;
807
835
  switch (targetOperation) {
@@ -867,19 +895,20 @@ async function listEngines(commandArgs, cliOptions) {
867
895
  }
868
896
  }
869
897
  for (const [index, engine] of engines.entries()) {
870
- logger.logTitledMessage('Identifier', chalk.magentaBright(engine.id));
871
- logger.logTitledMessage('Name', engine.name);
872
- logger.logTitledMessage('Description', engine.description);
873
- logger.logTitledMessage('Type', engine.type);
898
+ logger.logTitledMessage('Identifier', chalk.magentaBright(engine.id), undefined, 'output');
899
+ logger.logTitledMessage('Name', engine.name, undefined, 'output');
900
+ logger.logTitledMessage('Description', engine.description, undefined, 'output');
901
+ logger.logTitledMessage('Type', engine.type, undefined, 'output');
874
902
  if (index < engines.length - 1) {
875
- logger.log('');
903
+ logger.log('', 'output');
876
904
  }
877
905
  }
878
906
  }
879
- async function listTTSVoices(commandArgs, cliOptions) {
907
+ async function listTTSVoices(operationData) {
880
908
  const logger = new Logger();
881
- const targetEngine = commandArgs[0];
882
- const outputFilenames = commandArgs.slice(1);
909
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData;
910
+ const targetEngine = operationArgs[0];
911
+ const outputFilenames = operationArgs.slice(1);
883
912
  if (!targetEngine) {
884
913
  const optionsSchema = await getOptionsSchema();
885
914
  const { enum: ttsEnginesEnum } = getOptionTypeFromSchema(['VoiceListRequestOptions', 'engine'], optionsSchema);
@@ -887,9 +916,9 @@ async function listTTSVoices(commandArgs, cliOptions) {
887
916
  }
888
917
  const additionalOptionsSchema = new Map();
889
918
  additionalOptionsSchema.set('overwrite', { type: 'boolean' });
890
- cliOptions.set('engine', targetEngine);
891
- const options = await cliOptionsMapToOptionsObject(cliOptions, 'VoiceListRequestOptions', additionalOptionsSchema);
892
- const allowOverwrite = getWithDefault(options.overwrite, overwriteByDefault);
919
+ operationOptionsLookup.set('engine', targetEngine);
920
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'VoiceListRequestOptions');
921
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault);
893
922
  await checkOutputFilenames(outputFilenames, false, true, false);
894
923
  const { voiceList } = await API.requestVoiceList(options);
895
924
  const voiceListText = voiceList.map(entry => {
@@ -913,56 +942,58 @@ async function listTTSVoices(commandArgs, cliOptions) {
913
942
  }
914
943
  }
915
944
  else {
916
- logger.log(voiceListText);
945
+ logger.log(voiceListText, 'output');
917
946
  }
918
947
  logger.end();
919
948
  }
920
- async function installPackages(commandArgs, cliOptions) {
949
+ async function installPackages(operationData) {
921
950
  const logger = new Logger();
922
- if (commandArgs.length == 0) {
951
+ const { operationArgs } = operationData;
952
+ if (operationArgs.length == 0) {
923
953
  throw new Error('No package names specified');
924
954
  }
925
955
  const failedPackageNames = [];
926
- for (const packageName of commandArgs) {
956
+ for (const packageName of operationArgs) {
927
957
  try {
928
958
  await loadPackage(packageName);
929
959
  }
930
960
  catch (e) {
931
961
  resetActiveLogger();
932
- logger.log(`Failed installing package ${packageName}: ${e}`);
962
+ logger.logTitledMessage(`Failed installing package ${packageName}`, e, chalk.redBright, 'error');
933
963
  failedPackageNames.push(packageName);
934
964
  }
935
965
  }
936
966
  if (failedPackageNames.length > 0) {
937
967
  if (failedPackageNames.length == 1) {
938
- logger.log(`The package ${failedPackageNames[0]} failed to install`);
968
+ logger.log(`The package ${failedPackageNames[0]} failed to install`, 'error');
939
969
  }
940
970
  else {
941
- logger.log(`The packages ${failedPackageNames.join(', ')} failed to install`);
971
+ logger.log(`The packages ${failedPackageNames.join(', ')} failed to install`, 'error');
942
972
  }
943
973
  }
944
974
  }
945
- async function uninstallPackages(commandArgs, cliOptions) {
975
+ async function uninstallPackages(operationData) {
946
976
  const logger = new Logger();
947
- if (commandArgs.length == 0) {
977
+ const { operationArgs } = operationData;
978
+ if (operationArgs.length == 0) {
948
979
  throw new Error('No package names specified');
949
980
  }
950
981
  const failedPackageNames = [];
951
- for (const packageName of commandArgs) {
982
+ for (const packageName of operationArgs) {
952
983
  try {
953
984
  await removePackage(packageName);
954
985
  }
955
986
  catch (e) {
956
987
  resetActiveLogger();
957
- logger.log(`Failed uninstalling package ${packageName}: ${e}`);
988
+ logger.logTitledMessage(`Failed uninstalling package ${packageName}`, e, chalk.redBright, 'error');
958
989
  failedPackageNames.push(packageName);
959
990
  }
960
991
  }
961
992
  if (failedPackageNames.length > 0) {
962
- logger.log(`The packages ${failedPackageNames.join(', ')} failed to uninstall`);
993
+ logger.log(`The packages ${failedPackageNames.join(', ')} failed to uninstall`, 'error');
963
994
  }
964
995
  }
965
- async function listPackages(commandArgs, cliOptions) {
996
+ async function listPackages(operationData) {
966
997
  const logger = new Logger();
967
998
  const packagesDir = await ensureAndGetPackagesDir();
968
999
  const installedPackageNames = await readdir(packagesDir);
@@ -981,11 +1012,13 @@ async function listPackages(commandArgs, cliOptions) {
981
1012
  }
982
1013
  });
983
1014
  installedPackageNamesFormatted.sort();
984
- logger.log(`Total of ${installedPackageNamesFormatted.length} packages installed in '${packagesDir}'\n`);
1015
+ logger.log(`Total of ${installedPackageNamesFormatted.length} packages installed in '${packagesDir}'`);
1016
+ logger.log(``);
985
1017
  logger.log(installedPackageNamesFormatted.join('\n'));
986
1018
  }
987
- async function serve(commandArgs, cliOptions) {
988
- const options = await cliOptionsMapToOptionsObject(cliOptions, 'ServerOptions');
1019
+ async function serve(operationData) {
1020
+ const { operationOptionsLookup } = operationData;
1021
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'ServerOptions');
989
1022
  async function onServerStarted(serverOptions) {
990
1023
  // Run a test routine (early development)
991
1024
  //await runClientWebSocketTest(serverOptions.port!, serverOptions.secure!)
@@ -1009,7 +1042,7 @@ async function writeSourceSeparationOutputIfNeeded(outputFilename, isolatedRawAu
1009
1042
  }
1010
1043
  }
1011
1044
  }
1012
- async function cliOptionsMapToOptionsObject(cliOptionsMap, optionsRoot, additionalOptionsSchema) {
1045
+ async function optionsLookupToTypedObject(cliOptionsMap, optionsRoot, additionalOptionsSchema) {
1013
1046
  const optionsSchema = await getOptionsSchema();
1014
1047
  const resultingObj = {};
1015
1048
  function setValueAtPath(path, value) {