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/src/cli/CLI.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as API from '../api/API.js'
2
- import { CLIArguments, parseCLIArguments } from './CLIParser.js'
3
- import { convertHtmlToText, formatIntegerWithLeadingZeros, formatListWithQuotedElements, getWithDefault, logToStderr, setupUnhandledExceptionListeners, splitFilenameOnExtendedExtension, stringifyAndFormatJson } from '../utilities/Utilities.js'
2
+ import { parseCLIArguments } from './CLIParser.js'
3
+ import { clip, convertHtmlToText, formatIntegerWithLeadingZeros, formatListWithQuotedElements, getWithDefault, logToStderr, setupUnhandledExceptionListeners, splitFilenameOnExtendedExtension, stringifyAndFormatJson } from '../utilities/Utilities.js'
4
4
  import { getOptionTypeFromSchema, SchemaTypeDefinition } from './CLIOptionsSchema.js'
5
5
  import { ParsedConfigFile, parseConfigFile, parseJSONConfigFile } from './CLIConfigFile.js'
6
6
 
@@ -25,6 +25,7 @@ import { ServerOptions, startServer } from '../server/Server.js'
25
25
  import { OpenPromise } from '../utilities/OpenPromise.js'
26
26
  import JSON5 from 'json5'
27
27
  import { getLowercaseFileExtension, resolveToModuleRootDir } from '../utilities/PathUtilities.js'
28
+ import { CLIOptions, CLIOptionsKeys } from './CLIOptions.js'
28
29
 
29
30
  //const log = logToStderr
30
31
 
@@ -56,92 +57,150 @@ async function startIfInWorkerThread() {
56
57
  start(process.argv.slice(2))
57
58
  }
58
59
 
60
+ type CLIOperationData = {
61
+ operation: string
62
+ operationArgs: string[]
63
+
64
+ globalOptions: API.GlobalOptions
65
+ cliOptions: CLIOptions
66
+
67
+ operationOptionsLookup: Map<string, string>
68
+ }
69
+
59
70
  export async function start(processArgs: string[]) {
60
71
  const logger = new Logger()
61
72
 
62
- let args: CLIArguments
73
+ const operationData: CLIOperationData = {
74
+ operation: '',
75
+ operationArgs: [],
76
+
77
+ globalOptions: {},
78
+ cliOptions: {},
79
+
80
+ operationOptionsLookup: new Map<string, string>(),
81
+ }
63
82
 
64
83
  try {
65
84
  const packageData = await readAndParseJsonFile(resolveToModuleRootDir('package.json'))
66
85
 
67
86
  logger.log(chalk.magentaBright(`Echogarden v${packageData.version}\n`))
68
87
 
69
- const command = processArgs[0]
88
+ const operation = processArgs[0]
70
89
 
71
- if (!command || command == 'help') {
72
- logger.log(`Supported operations:\n\n${commandHelp.join('\n')}`)
90
+ if (!operation || operation == 'help') {
91
+ logger.log(`Supported operations:\n\n${help.join('\n')}`)
73
92
  process.exit(0)
74
93
  }
75
94
 
76
- if (command == '--help') {
77
- logger.log(`There's no command called '--help'. Did you mean to run 'echogarden help'?`)
78
- process.exit(0)
95
+ if (operation == '--help' || operation == '-h') {
96
+ logger.log(`There's no operation called '${operation}'. Did you mean to run 'echogarden help'?`)
97
+ process.exit(1)
79
98
  }
80
99
 
81
- args = parseCLIArguments(command, processArgs.slice(1))
100
+ if (operation.startsWith('-')) {
101
+ logger.log(`Operation name '${operation}' is invalid. It cannot start with a hyphen.`)
102
+ process.exit(1)
103
+ }
104
+
105
+ const { operationArgs, parsedArgumentsLookup } = parseCLIArguments(processArgs.slice(1))
82
106
 
83
- if (!args.options.has('config')) {
107
+ if (!parsedArgumentsLookup.has('config')) {
84
108
  const defaultConfigFile = `./${appName}.config`
85
109
  const defaultJsonConfigFile = defaultConfigFile + '.json'
86
110
 
87
111
  if (existsSync(defaultConfigFile)) {
88
- args.options.set('config', defaultConfigFile)
112
+ parsedArgumentsLookup.set('config', defaultConfigFile)
89
113
  } else if (existsSync(defaultJsonConfigFile)) {
90
- args.options.set('config', defaultJsonConfigFile)
114
+ parsedArgumentsLookup.set('config', defaultJsonConfigFile)
91
115
  }
92
116
  }
93
117
 
94
- if (args.options.has('config')) {
95
- const configFilePath = args.options.get('config')!
96
- args.options.delete('config')
118
+ if (parsedArgumentsLookup.has('config')) {
119
+ const configFilePath = parsedArgumentsLookup.get('config')!
120
+ parsedArgumentsLookup.delete('config')
97
121
 
98
- let parsedOptionFile: ParsedConfigFile
122
+ let parsedConfigFile: ParsedConfigFile
99
123
 
100
124
  if (configFilePath.endsWith('.config')) {
101
- parsedOptionFile = await parseConfigFile(configFilePath)
125
+ parsedConfigFile = await parseConfigFile(configFilePath)
102
126
  } else if (configFilePath.endsWith('.config.json')) {
103
- parsedOptionFile = await parseJSONConfigFile(configFilePath)
127
+ parsedConfigFile = await parseJSONConfigFile(configFilePath)
104
128
  } else {
105
129
  throw new Error(`Specified config file '${configFilePath}' doesn't have a supported extension. Should be either '.config' or '.config.json'`)
106
130
  }
107
131
 
108
- let sectionName = args.command
132
+ let sectionName = operation
109
133
 
110
134
  if (sectionName.startsWith('speak-')) {
111
135
  sectionName = 'speak'
112
136
  }
113
137
 
114
- const newOptions = parsedOptionFile.get(sectionName) || new Map<string, string>()
138
+ const globalOptionsLookup = new Map<string, string>()
139
+ const cliOptionsLookup = new Map<string, string>()
140
+ const operationsOptionsLookup = new Map<string, string>()
141
+
142
+ if (parsedConfigFile.has('global')) {
143
+ for (const [key, value] of parsedConfigFile.get('global')!) {
144
+ globalOptionsLookup.set(key, value)
145
+ }
146
+ }
115
147
 
116
- for (const [key, value] of args.options) {
117
- newOptions.set(key, value)
148
+ if (parsedConfigFile.has('cli')) {
149
+ for (const [key, value] of parsedConfigFile.get('cli')!) {
150
+ cliOptionsLookup.set(key, value)
151
+ }
152
+ }
153
+
154
+ if (parsedConfigFile.has(sectionName)) {
155
+ for (const [key, value] of parsedConfigFile.get(sectionName)!) {
156
+ operationsOptionsLookup.set(key, value)
157
+ }
118
158
  }
119
159
 
120
- args.options = newOptions
160
+ const globalOptionsKeys = API.listGlobalOptions()
161
+ const cliOptionsKeys = CLIOptionsKeys
162
+
163
+ for (const [key, value] of parsedArgumentsLookup) {
164
+ if (globalOptionsKeys.includes(key)) {
165
+ globalOptionsLookup.set(key, value)
166
+ } else if (cliOptionsKeys.includes(key as any)) {
167
+ cliOptionsLookup.set(key, value)
168
+ } else {
169
+ operationsOptionsLookup.set(key, value)
170
+ }
171
+ }
172
+
173
+ operationData.operation = operation
174
+ operationData.operationArgs = operationArgs
175
+
176
+ operationData.globalOptions = await optionsLookupToTypedObject(globalOptionsLookup, 'GlobalOptions')
177
+ operationData.cliOptions = await optionsLookupToTypedObject(cliOptionsLookup, 'CLIOptions')
178
+ operationData.operationOptionsLookup = operationsOptionsLookup
121
179
  }
122
180
  } catch (e: any) {
123
181
  resetActiveLogger()
124
182
 
125
- logger.logTitledMessage(`Error`, e.message, chalk.redBright)
183
+ logger.logTitledMessage(`Error`, e.message, chalk.redBright, 'error')
126
184
  process.exit(1)
127
185
  }
128
186
 
129
- let debugMode = false
130
- if (args.options.has('debug')) {
131
- args.options.delete('debug')
187
+ for (const key in operationData.globalOptions) {
188
+ const value = (operationData.globalOptions as any)[key]
132
189
 
133
- debugMode = true
190
+ API.setGlobalOption(key as any, value)
134
191
  }
135
192
 
193
+ const debugMode = operationData.cliOptions.debug || false
194
+
136
195
  try {
137
- await startWithArgs(args)
196
+ await startWithArgs(operationData)
138
197
  } catch (e: any) {
139
198
  resetActiveLogger()
140
199
 
141
200
  if (debugMode) {
142
- logger.log(e)
201
+ logger.log(e, 'error')
143
202
  } else {
144
- logger.logTitledMessage(`Error`, e.message, chalk.redBright)
203
+ logger.logTitledMessage(`Error`, e.message, chalk.redBright, 'error')
145
204
  }
146
205
 
147
206
  process.exit(1)
@@ -152,7 +211,7 @@ export async function start(processArgs: string[]) {
152
211
 
153
212
  const executableName = `${chalk.cyanBright('echogarden')}`
154
213
 
155
- const commandHelp = [
214
+ const help = [
156
215
  `${executableName} ${chalk.magentaBright('speak')} text [output files...] [options...]`,
157
216
  ` Speak the given text\n`,
158
217
  `${executableName} ${chalk.magentaBright('speak-file')} inputFile [output files...] [options...]`,
@@ -192,124 +251,123 @@ const commandHelp = [
192
251
  `${executableName} ${chalk.magentaBright('serve')} [options...]`,
193
252
  ` Start a server\n`,
194
253
  `Options reference: ${chalk.blueBright('https://bit.ly/echogarden-options')}`
195
- //` ${chalk.blueBright('https://github.com/echogarden-project/echogarden/blob/main/docs/Options.md')}`
196
254
  ]
197
255
 
198
- async function startWithArgs(parsedArgs: CLIArguments) {
256
+ async function startWithArgs(operationData: CLIOperationData) {
199
257
  const logger = new Logger()
200
258
 
201
- switch (parsedArgs.command) {
259
+ switch (operationData.operation) {
202
260
  case 'speak':
203
261
  case 'speak-file':
204
262
  case 'speak-url':
205
263
  case 'speak-wikipedia': {
206
- await speak(parsedArgs.command, parsedArgs.commandArgs, parsedArgs.options)
264
+ await speak(operationData)
207
265
  break
208
266
  }
209
267
 
210
268
  case 'transcribe': {
211
- await transcribe(parsedArgs.commandArgs, parsedArgs.options)
269
+ await transcribe(operationData)
212
270
  break
213
271
  }
214
272
 
215
273
  case 'align': {
216
- await align(parsedArgs.commandArgs, parsedArgs.options)
274
+ await align(operationData)
217
275
  break
218
276
  }
219
277
 
220
278
  case 'translate-speech': {
221
- await translateSpeech(parsedArgs.commandArgs, parsedArgs.options)
279
+ await translateSpeech(operationData)
222
280
  break
223
281
  }
224
282
 
225
283
  case 'align-translation': {
226
- await alignTranslation(parsedArgs.commandArgs, parsedArgs.options)
284
+ await alignTranslation(operationData)
227
285
  break
228
286
  }
229
287
 
230
288
  case 'detect-language': {
231
- await detectLanguage(parsedArgs.commandArgs, parsedArgs.options, 'auto')
289
+ await detectLanguage(operationData, 'auto')
232
290
  break
233
291
  }
234
292
 
235
293
  case 'detect-speech-language': {
236
- await detectLanguage(parsedArgs.commandArgs, parsedArgs.options, 'speech')
294
+ await detectLanguage(operationData, 'speech')
237
295
  break
238
296
  }
239
297
 
240
298
  case 'detect-text-language': {
241
- await detectLanguage(parsedArgs.commandArgs, parsedArgs.options, 'text')
299
+ await detectLanguage(operationData, 'text')
242
300
  break
243
301
  }
244
302
 
245
303
  case 'detect-voice-activity': {
246
- await detectVoiceActivity(parsedArgs.commandArgs, parsedArgs.options)
304
+ await detectVoiceActivity(operationData)
247
305
  break
248
306
  }
249
307
 
250
308
  case 'denoise': {
251
- await denoise(parsedArgs.commandArgs, parsedArgs.options)
309
+ await denoise(operationData)
252
310
  break
253
311
  }
254
312
 
255
313
  case 'isolate': {
256
- await isolate(parsedArgs.commandArgs, parsedArgs.options)
314
+ await isolate(operationData)
257
315
  break
258
316
  }
259
317
 
260
318
  case 'list-engines': {
261
- await listEngines(parsedArgs.commandArgs, parsedArgs.options)
319
+ await listEngines(operationData)
262
320
  break
263
321
  }
264
322
 
265
323
  case 'list-voices': {
266
- await listTTSVoices(parsedArgs.commandArgs, parsedArgs.options)
324
+ await listTTSVoices(operationData)
267
325
  break
268
326
  }
269
327
 
270
328
  case 'install': {
271
- await installPackages(parsedArgs.commandArgs, parsedArgs.options)
329
+ await installPackages(operationData)
272
330
  break
273
331
  }
274
332
 
275
333
  case 'uninstall': {
276
- await uninstallPackages(parsedArgs.commandArgs, parsedArgs.options)
334
+ await uninstallPackages(operationData)
277
335
  break
278
336
  }
279
337
 
280
338
  case 'list-packages': {
281
- await listPackages(parsedArgs.commandArgs, parsedArgs.options)
339
+ await listPackages(operationData)
282
340
  break
283
341
  }
284
342
 
285
343
  case 'serve': {
286
- await serve(parsedArgs.commandArgs, parsedArgs.options)
344
+ await serve(operationData)
287
345
  break
288
346
  }
289
347
 
290
348
  default: {
291
- logger.logTitledMessage(`Unknown command`, parsedArgs.command, chalk.redBright)
349
+ logger.logTitledMessage(`Unknown operation`, operationData.operation, chalk.redBright, 'error')
292
350
  process.exit(1)
293
351
  }
294
352
  }
295
353
  }
296
354
 
297
- type SpeakCommand = 'speak' | 'speak-file' | 'speak-url' | 'speak-wikipedia'
298
-
299
- async function speak(command: SpeakCommand, commandArgs: string[], cliOptions: Map<string, string>) {
355
+ async function speak(operationData: CLIOperationData) {
300
356
  const logger = new Logger()
301
357
 
302
- const mainArg = commandArgs[0]
303
- const outputFilenames = commandArgs.slice(1)
358
+ const { operationArgs, operation, operationOptionsLookup, cliOptions } = operationData
359
+
360
+ const mainArg = operationArgs[0]
361
+ const outputFilenames = operationArgs.slice(1)
304
362
 
305
363
  if (mainArg == undefined) {
306
- if (command == 'speak') {
364
+ if (operation == 'speak') {
307
365
  throw new Error(`'speak' requires an argument containing the text to speak.`)
308
- } else if (command == 'speak-file') {
366
+ } else if (operation == 'speak-file') {
309
367
  throw new Error(`'speak-file' requires an argument containing the file to speak.`)
310
- } else if (command == 'speak-url') {
368
+ } else if (operation == 'speak-url') {
311
369
  throw new Error(`'speak-url' requires an argument containing the url to speak.`)
312
- } else if (command == 'speak-wikipedia') {
370
+ } else if (operation == 'speak-wikipedia') {
313
371
  throw new Error(`'speak-wikipedia' requires an argument containing the name of the Wikipedia article to speak.`)
314
372
  }
315
373
 
@@ -320,13 +378,13 @@ async function speak(command: SpeakCommand, commandArgs: string[], cliOptions: M
320
378
  additionalOptionsSchema.set('play', { type: 'boolean' })
321
379
  additionalOptionsSchema.set('overwrite', { type: 'boolean' })
322
380
 
323
- if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
324
- cliOptions.set('play', `${outputFilenames.length == 0}`)
381
+ if (cliOptions.play == null) {
382
+ cliOptions.play = outputFilenames.length === 0
325
383
  }
326
384
 
327
- const options: API.SynthesisOptions = await cliOptionsMapToOptionsObject(cliOptions, 'SynthesisOptions', additionalOptionsSchema)
385
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'SynthesisOptions', additionalOptionsSchema)
328
386
 
329
- const allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
387
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault)
330
388
  const { includesPlaceholderPattern } = await checkOutputFilenames(outputFilenames, true, true, true)
331
389
 
332
390
  let plainText: string | undefined = undefined
@@ -335,7 +393,7 @@ async function speak(command: SpeakCommand, commandArgs: string[], cliOptions: M
335
393
  const plainTextParagraphBreaks = options.plainText?.paragraphBreaks || API.defaultSynthesisOptions.plainText!.paragraphBreaks!
336
394
  const plainTextWhitespace = options.plainText?.whitespace || API.defaultSynthesisOptions.plainText!.whitespace!
337
395
 
338
- if (command == 'speak') {
396
+ if (operation == 'speak') {
339
397
  if (options.ssml) {
340
398
  textSegments = [mainArg]
341
399
  } else {
@@ -343,7 +401,7 @@ async function speak(command: SpeakCommand, commandArgs: string[], cliOptions: M
343
401
  }
344
402
 
345
403
  plainText = mainArg
346
- } else if (command == 'speak-file') {
404
+ } else if (operation == 'speak-file') {
347
405
  const sourceFile = mainArg
348
406
 
349
407
  if (!existsSync(sourceFile)) {
@@ -373,9 +431,9 @@ async function speak(command: SpeakCommand, commandArgs: string[], cliOptions: M
373
431
  } else {
374
432
  throw new Error(`'speak-file' only supports inputs with extensions 'txt', 'html', 'htm', 'xml', 'ssml', 'srt', 'vtt'`)
375
433
  }
376
- } else if (command == 'speak-url') {
434
+ } else if (operation == 'speak-url') {
377
435
  if (options.ssml) {
378
- throw new Error(`speak-url doesn't provide SSML inputs`)
436
+ throw new Error(`speak-url doesn't accept SSML inputs`)
379
437
  }
380
438
 
381
439
  const url = mainArg
@@ -388,7 +446,7 @@ async function speak(command: SpeakCommand, commandArgs: string[], cliOptions: M
388
446
  const textContent = await fetchDocumentText(url)
389
447
 
390
448
  textSegments = splitToParagraphs(textContent, 'single', 'preserve')
391
- } else if (command == 'speak-wikipedia') {
449
+ } else if (operation == 'speak-wikipedia') {
392
450
  if (options.ssml) {
393
451
  throw new Error(`speak-wikipedia doesn't provide SSML inputs`)
394
452
  }
@@ -400,7 +458,7 @@ async function speak(command: SpeakCommand, commandArgs: string[], cliOptions: M
400
458
 
401
459
  textSegments = await parseWikipediaArticle(mainArg, getShortLanguageCode(options.language))
402
460
  } else {
403
- throw new Error('Invalid command')
461
+ throw new Error(`Invalid operation specified: '${operation}'`)
404
462
  }
405
463
 
406
464
  async function onSegment(segmentData: API.SynthesisSegmentEventData) {
@@ -412,7 +470,7 @@ async function speak(command: SpeakCommand, commandArgs: string[], cliOptions: M
412
470
 
413
471
  logger.end()
414
472
 
415
- if ((options as any).play) {
473
+ if (cliOptions.play) {
416
474
  let gainAmount = -3 - segmentData.peakDecibelsSoFar
417
475
  //gainAmount = Math.min(gainAmount, 0)
418
476
 
@@ -451,11 +509,13 @@ async function speak(command: SpeakCommand, commandArgs: string[], cliOptions: M
451
509
  logger.end()
452
510
  }
453
511
 
454
- async function transcribe(commandArgs: string[], cliOptions: Map<string, string>) {
512
+ async function transcribe(operationData: CLIOperationData) {
455
513
  const logger = new Logger()
456
514
 
457
- const sourceFilename = commandArgs[0]
458
- const outputFilenames = commandArgs.slice(1)
515
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData
516
+
517
+ const sourceFilename = operationArgs[0]
518
+ const outputFilenames = operationArgs.slice(1)
459
519
 
460
520
  if (sourceFilename == undefined) {
461
521
  throw new Error(`'transcribe' requires an argument containing the source file name.`)
@@ -465,17 +525,13 @@ async function transcribe(commandArgs: string[], cliOptions: Map<string, string>
465
525
  throw new Error(`The given source audio file '${sourceFilename}' was not found.`)
466
526
  }
467
527
 
468
- const additionalOptionsSchema = new Map<string, SchemaTypeDefinition>()
469
- additionalOptionsSchema.set('play', { type: 'boolean' })
470
- additionalOptionsSchema.set('overwrite', { type: 'boolean' })
471
-
472
- if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
473
- cliOptions.set('play', `${outputFilenames.length == 0}`)
528
+ if (cliOptions.play == null) {
529
+ cliOptions.play = outputFilenames.length === 0
474
530
  }
475
531
 
476
- const options: API.RecognitionOptions = await cliOptionsMapToOptionsObject(cliOptions, 'RecognitionOptions', additionalOptionsSchema)
532
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'RecognitionOptions')
477
533
 
478
- const allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
534
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault)
479
535
  const { includesPlaceholderPattern } = await checkOutputFilenames(outputFilenames, true, true, true)
480
536
 
481
537
  const { transcript, timeline, wordTimeline, language, inputRawAudio, isolatedRawAudio, backgroundRawAudio } = await API.recognize(sourceFilename, options)
@@ -500,7 +556,7 @@ async function transcribe(commandArgs: string[], cliOptions: Map<string, string>
500
556
 
501
557
  logger.end()
502
558
 
503
- if ((options as any).play) {
559
+ if (cliOptions.play) {
504
560
  let audioToPlay: RawAudio
505
561
 
506
562
  if (isolatedRawAudio) {
@@ -515,11 +571,13 @@ async function transcribe(commandArgs: string[], cliOptions: Map<string, string>
515
571
  }
516
572
  }
517
573
 
518
- async function align(commandArgs: string[], cliOptions: Map<string, string>) {
574
+ async function align(operationData: CLIOperationData) {
519
575
  const logger = new Logger()
520
576
 
521
- const audioFilename = commandArgs[0]
522
- const outputFilenames = commandArgs.slice(2)
577
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData
578
+
579
+ const audioFilename = operationArgs[0]
580
+ const outputFilenames = operationArgs.slice(2)
523
581
 
524
582
  if (audioFilename == undefined) {
525
583
  throw new Error(`align requires an argument containing the audio file path.`)
@@ -529,7 +587,7 @@ async function align(commandArgs: string[], cliOptions: Map<string, string>) {
529
587
  throw new Error(`The given source file '${audioFilename}' was not found.`)
530
588
  }
531
589
 
532
- const alignmentReferenceFile = commandArgs[1]
590
+ const alignmentReferenceFile = operationArgs[1]
533
591
 
534
592
  if (alignmentReferenceFile == undefined) {
535
593
  throw new Error(`align requires a second argument containing the alignment reference file path.`)
@@ -554,17 +612,12 @@ async function align(commandArgs: string[], cliOptions: Map<string, string>) {
554
612
  throw new Error(`align only supports reference files with extensions 'txt', 'html', 'htm', 'srt' or 'vtt'`)
555
613
  }
556
614
 
557
- const additionalOptionsSchema = new Map<string, SchemaTypeDefinition>()
558
- additionalOptionsSchema.set('play', { type: 'boolean' })
559
- additionalOptionsSchema.set('overwrite', { type: 'boolean' })
560
-
561
- if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
562
- cliOptions.set('play', `${outputFilenames.length == 0}`)
615
+ if (cliOptions.play == null) {
616
+ cliOptions.play = outputFilenames.length === 0
563
617
  }
618
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'AlignmentOptions')
564
619
 
565
- const options: API.AlignmentOptions = await cliOptionsMapToOptionsObject(cliOptions, 'AlignmentOptions', additionalOptionsSchema)
566
-
567
- const allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
620
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault)
568
621
  const { includesPlaceholderPattern } = await checkOutputFilenames(outputFilenames, true, true, true)
569
622
 
570
623
  const { timeline, wordTimeline, transcript, language, inputRawAudio, isolatedRawAudio, backgroundRawAudio } = await API.align(audioFilename, text, options)
@@ -599,7 +652,7 @@ async function align(commandArgs: string[], cliOptions: Map<string, string>) {
599
652
 
600
653
  logger.end()
601
654
 
602
- if ((options as any).play) {
655
+ if (cliOptions.play) {
603
656
  let audioToPlay: RawAudio
604
657
 
605
658
  if (isolatedRawAudio) {
@@ -614,11 +667,13 @@ async function align(commandArgs: string[], cliOptions: Map<string, string>) {
614
667
  }
615
668
  }
616
669
 
617
- async function alignTranslation(commandArgs: string[], cliOptions: Map<string, string>) {
670
+ async function alignTranslation(operationData: CLIOperationData) {
618
671
  const logger = new Logger()
619
672
 
620
- const audioFilename = commandArgs[0]
621
- const outputFilenames = commandArgs.slice(2)
673
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData
674
+
675
+ const audioFilename = operationArgs[0]
676
+ const outputFilenames = operationArgs.slice(2)
622
677
 
623
678
  if (audioFilename == undefined) {
624
679
  throw new Error(`align-translation requires an argument containing the audio file path.`)
@@ -628,7 +683,7 @@ async function alignTranslation(commandArgs: string[], cliOptions: Map<string, s
628
683
  throw new Error(`The given source file '${audioFilename}' was not found.`)
629
684
  }
630
685
 
631
- const alignmentReferenceFile = commandArgs[1]
686
+ const alignmentReferenceFile = operationArgs[1]
632
687
 
633
688
  if (alignmentReferenceFile == undefined) {
634
689
  throw new Error(`align-translation requires a second argument containing the translated reference file path.`)
@@ -653,17 +708,13 @@ async function alignTranslation(commandArgs: string[], cliOptions: Map<string, s
653
708
  throw new Error(`align only supports reference files with extensions 'txt', 'html', 'htm', 'srt' or 'vtt'`)
654
709
  }
655
710
 
656
- const additionalOptionsSchema = new Map<string, SchemaTypeDefinition>()
657
- additionalOptionsSchema.set('play', { type: 'boolean' })
658
- additionalOptionsSchema.set('overwrite', { type: 'boolean' })
659
-
660
- if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
661
- cliOptions.set('play', `${outputFilenames.length == 0}`)
711
+ if (cliOptions.play == null) {
712
+ cliOptions.play = outputFilenames.length === 0
662
713
  }
663
714
 
664
- const options: API.TranslationAlignmentOptions = await cliOptionsMapToOptionsObject(cliOptions, 'TranslationAlignmentOptions', additionalOptionsSchema)
715
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'TranslationAlignmentOptions')
665
716
 
666
- const allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
717
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault)
667
718
  const { includesPlaceholderPattern } = await checkOutputFilenames(outputFilenames, true, true, true)
668
719
 
669
720
  const {
@@ -705,7 +756,7 @@ async function alignTranslation(commandArgs: string[], cliOptions: Map<string, s
705
756
 
706
757
  logger.end()
707
758
 
708
- if ((options as any).play) {
759
+ if (cliOptions.play) {
709
760
  let audioToPlay: RawAudio
710
761
 
711
762
  if (isolatedRawAudio) {
@@ -720,11 +771,13 @@ async function alignTranslation(commandArgs: string[], cliOptions: Map<string, s
720
771
  }
721
772
  }
722
773
 
723
- async function translateSpeech(commandArgs: string[], cliOptions: Map<string, string>) {
774
+ async function translateSpeech(operationData: CLIOperationData) {
724
775
  const logger = new Logger()
725
776
 
726
- const inputFilename = commandArgs[0]
727
- const outputFilenames = commandArgs.slice(1)
777
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData
778
+
779
+ const inputFilename = operationArgs[0]
780
+ const outputFilenames = operationArgs.slice(1)
728
781
 
729
782
  if (inputFilename == undefined) {
730
783
  throw new Error(`translate-speech requires an argument containing the input file path.`)
@@ -734,17 +787,13 @@ async function translateSpeech(commandArgs: string[], cliOptions: Map<string, st
734
787
  throw new Error(`The given input file '${inputFilename}' was not found.`)
735
788
  }
736
789
 
737
- const additionalOptionsSchema = new Map<string, SchemaTypeDefinition>()
738
- additionalOptionsSchema.set('play', { type: 'boolean' })
739
- additionalOptionsSchema.set('overwrite', { type: 'boolean' })
740
-
741
- if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
742
- cliOptions.set('play', `${outputFilenames.length == 0}`)
790
+ if (cliOptions.play == null) {
791
+ cliOptions.play = outputFilenames.length === 0
743
792
  }
744
793
 
745
- const options: API.SpeechTranslationOptions = await cliOptionsMapToOptionsObject(cliOptions, 'SpeechTranslationOptions', additionalOptionsSchema)
794
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'SpeechTranslationOptions')
746
795
 
747
- const allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
796
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault)
748
797
 
749
798
  await checkOutputFilenames(outputFilenames, true, true, true)
750
799
 
@@ -770,7 +819,7 @@ async function translateSpeech(commandArgs: string[], cliOptions: Map<string, st
770
819
 
771
820
  logger.end()
772
821
 
773
- if ((options as any).play) {
822
+ if (cliOptions.play) {
774
823
  let audioToPlay: RawAudio
775
824
 
776
825
  if (isolatedRawAudio) {
@@ -809,19 +858,18 @@ async function translateSpeech(commandArgs: string[], cliOptions: Map<string, st
809
858
  }
810
859
  }
811
860
 
812
- async function detectLanguage(commandArgs: string[], cliOptions: Map<string, string>, mode: 'speech' | 'text' | 'auto') {
861
+ async function detectLanguage(operationData: CLIOperationData, mode: 'speech' | 'text' | 'auto') {
813
862
  const logger = new Logger()
814
863
 
815
- const inputFilePath = commandArgs[0]
816
- const outputFilenames = commandArgs.slice(1)
864
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData
865
+
866
+ const inputFilePath = operationArgs[0]
867
+ const outputFilenames = operationArgs.slice(1)
817
868
 
818
869
  if (!existsSync(inputFilePath)) {
819
870
  throw new Error(`The given input file '${inputFilePath}' was not found.`)
820
871
  }
821
872
 
822
- const additionalOptionsSchema = new Map<string, SchemaTypeDefinition>()
823
- additionalOptionsSchema.set('overwrite', { type: 'boolean' })
824
-
825
873
  const inputFileExtension = getLowercaseFileExtension(inputFilePath)
826
874
  const supportedInputTextFormats = ['txt', 'srt', 'vtt']
827
875
 
@@ -838,8 +886,8 @@ async function detectLanguage(commandArgs: string[], cliOptions: Map<string, str
838
886
  throw new Error(`'detect-text-language' doesn't support input file extension '${inputFileExtension}'`)
839
887
  }
840
888
 
841
- const options: API.TextLanguageDetectionOptions = await cliOptionsMapToOptionsObject(cliOptions, 'TextLanguageDetectionOptions', additionalOptionsSchema)
842
- allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
889
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'TextLanguageDetectionOptions')
890
+ allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault)
843
891
 
844
892
  await checkOutputFilenames(outputFilenames, false, true, false)
845
893
 
@@ -857,8 +905,8 @@ async function detectLanguage(commandArgs: string[], cliOptions: Map<string, str
857
905
  throw new Error(`detect-speech-language requires an argument containing the input audio file path.`)
858
906
  }
859
907
 
860
- const options: API.SpeechLanguageDetectionOptions = await cliOptionsMapToOptionsObject(cliOptions, 'SpeechLanguageDetectionOptions', additionalOptionsSchema)
861
- allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
908
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'SpeechLanguageDetectionOptions')
909
+ allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault)
862
910
 
863
911
  await checkOutputFilenames(outputFilenames, false, true, false)
864
912
 
@@ -880,18 +928,20 @@ async function detectLanguage(commandArgs: string[], cliOptions: Map<string, str
880
928
  } else {
881
929
  const resultsAsText = results.slice(0, 10).map(result => `${formatLanguageCodeWithName(result.language)}: ${result.probability.toFixed(5)}`).join('\n')
882
930
 
883
- logger.log('')
884
- logger.log(resultsAsText)
931
+ logger.log('', 'output')
932
+ logger.log(resultsAsText, 'output')
885
933
  }
886
934
 
887
935
  logger.end()
888
936
  }
889
937
 
890
- async function detectVoiceActivity(commandArgs: string[], cliOptions: Map<string, string>) {
938
+ async function detectVoiceActivity(operationData: CLIOperationData) {
891
939
  const logger = new Logger()
892
940
 
893
- const audioFilename = commandArgs[0]
894
- const outputFilenames = commandArgs.slice(1)
941
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData
942
+
943
+ const audioFilename = operationArgs[0]
944
+ const outputFilenames = operationArgs.slice(1)
895
945
 
896
946
  if (audioFilename == undefined) {
897
947
  throw new Error(`detect-voice-activity requires an argument containing the audio file path.`)
@@ -901,17 +951,13 @@ async function detectVoiceActivity(commandArgs: string[], cliOptions: Map<string
901
951
  throw new Error(`The given source audio file '${audioFilename}' was not found.`)
902
952
  }
903
953
 
904
- const additionalOptionsSchema = new Map<string, SchemaTypeDefinition>()
905
- additionalOptionsSchema.set('play', { type: 'boolean' })
906
- additionalOptionsSchema.set('overwrite', { type: 'boolean' })
907
-
908
- if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
909
- cliOptions.set('play', `${outputFilenames.length == 0}`)
954
+ if (cliOptions.play == null) {
955
+ cliOptions.play = outputFilenames.length === 0
910
956
  }
911
957
 
912
- const options: API.VADOptions = await cliOptionsMapToOptionsObject(cliOptions, 'VADOptions', additionalOptionsSchema)
958
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'VADOptions')
913
959
 
914
- const allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
960
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault)
915
961
 
916
962
  await checkOutputFilenames(outputFilenames, true, true, true)
917
963
 
@@ -947,7 +993,7 @@ async function detectVoiceActivity(commandArgs: string[], cliOptions: Map<string
947
993
 
948
994
  logger.end()
949
995
 
950
- if ((options as any).play) {
996
+ if (cliOptions.play) {
951
997
  const normalizedAudio = normalizeAudioLevel(inputRawAudio)
952
998
 
953
999
  const timelineToPlay = verboseTimeline.map(entry => {
@@ -958,11 +1004,13 @@ async function detectVoiceActivity(commandArgs: string[], cliOptions: Map<string
958
1004
  }
959
1005
  }
960
1006
 
961
- async function denoise(commandArgs: string[], cliOptions: Map<string, string>) {
1007
+ async function denoise(operationData: CLIOperationData) {
962
1008
  const logger = new Logger()
963
1009
 
964
- const audioFilename = commandArgs[0]
965
- const outputFilenames = commandArgs.slice(1)
1010
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData
1011
+
1012
+ const audioFilename = operationArgs[0]
1013
+ const outputFilenames = operationArgs.slice(1)
966
1014
 
967
1015
  if (audioFilename == undefined) {
968
1016
  throw new Error(`'denoise' requires an argument containing the audio file path.`)
@@ -972,17 +1020,13 @@ async function denoise(commandArgs: string[], cliOptions: Map<string, string>) {
972
1020
  throw new Error(`The given source audio file '${audioFilename}' was not found.`)
973
1021
  }
974
1022
 
975
- const additionalOptionsSchema = new Map<string, SchemaTypeDefinition>()
976
- additionalOptionsSchema.set('play', { type: 'boolean' })
977
- additionalOptionsSchema.set('overwrite', { type: 'boolean' })
978
-
979
- if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
980
- cliOptions.set('play', `${outputFilenames.length == 0}`)
1023
+ if (cliOptions.play == null) {
1024
+ cliOptions.play = outputFilenames.length === 0
981
1025
  }
982
1026
 
983
- const options: API.DenoisingOptions = await cliOptionsMapToOptionsObject(cliOptions, 'DenoisingOptions', additionalOptionsSchema)
1027
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'DenoisingOptions')
984
1028
 
985
- const allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
1029
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault)
986
1030
 
987
1031
  await checkOutputFilenames(outputFilenames, true, false, false)
988
1032
 
@@ -1000,16 +1044,18 @@ async function denoise(commandArgs: string[], cliOptions: Map<string, string>) {
1000
1044
 
1001
1045
  logger.end()
1002
1046
 
1003
- if ((options as any).play) {
1047
+ if (cliOptions.play) {
1004
1048
  await playAudioSamples(denoisedAudio)
1005
1049
  }
1006
1050
  }
1007
1051
 
1008
- async function isolate(commandArgs: string[], cliOptions: Map<string, string>) {
1052
+ async function isolate(operationData: CLIOperationData) {
1009
1053
  const logger = new Logger()
1010
1054
 
1011
- const audioFilename = commandArgs[0]
1012
- const outputFilenames = commandArgs.slice(1)
1055
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData
1056
+
1057
+ const audioFilename = operationArgs[0]
1058
+ const outputFilenames = operationArgs.slice(1)
1013
1059
 
1014
1060
  if (audioFilename == undefined) {
1015
1061
  throw new Error(`'isolate' requires an argument containing the audio file path.`)
@@ -1019,17 +1065,13 @@ async function isolate(commandArgs: string[], cliOptions: Map<string, string>) {
1019
1065
  throw new Error(`The given source audio file '${audioFilename}' was not found.`)
1020
1066
  }
1021
1067
 
1022
- const additionalOptionsSchema = new Map<string, SchemaTypeDefinition>()
1023
- additionalOptionsSchema.set('play', { type: 'boolean' })
1024
- additionalOptionsSchema.set('overwrite', { type: 'boolean' })
1025
-
1026
- if (!cliOptions.has('play') && !cliOptions.has('no-play')) {
1027
- cliOptions.set('play', `${outputFilenames.length == 0}`)
1068
+ if (cliOptions.play == null) {
1069
+ cliOptions.play = outputFilenames.length === 0
1028
1070
  }
1029
1071
 
1030
- const options: API.SourceSeparationOptions = await cliOptionsMapToOptionsObject(cliOptions, 'SourceSeparationOptions', additionalOptionsSchema)
1072
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'SourceSeparationOptions')
1031
1073
 
1032
- const allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
1074
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault)
1033
1075
 
1034
1076
  await checkOutputFilenames(outputFilenames, true, false, false)
1035
1077
 
@@ -1045,18 +1087,20 @@ async function isolate(commandArgs: string[], cliOptions: Map<string, string>) {
1045
1087
 
1046
1088
  logger.end()
1047
1089
 
1048
- if ((options as any).play) {
1090
+ if (cliOptions.play) {
1049
1091
  await playAudioSamples(isolatedRawAudio)
1050
1092
  }
1051
1093
  }
1052
1094
 
1053
- async function listEngines(commandArgs: string[], cliOptions: Map<string, string>) {
1095
+ async function listEngines(operationData: CLIOperationData) {
1054
1096
  const logger = new Logger()
1055
1097
 
1056
- const targetOperation = commandArgs[0]
1098
+ const { operationArgs } = operationData
1099
+
1100
+ const targetOperation = operationArgs[0]
1057
1101
 
1058
1102
  if (!targetOperation) {
1059
- throw new Error(`The 'list-engines' command requires an argument specifying the operation to list engines for, like 'echogarden list-engines transcribe'.`)
1103
+ throw new Error(`The 'list-engines' operation requires an argument specifying the operation to list engines for, like 'echogarden list-engines transcribe'.`)
1060
1104
  }
1061
1105
 
1062
1106
  let engines: API.EngineMetadata[]
@@ -1150,22 +1194,24 @@ async function listEngines(commandArgs: string[], cliOptions: Map<string, string
1150
1194
  }
1151
1195
 
1152
1196
  for (const [index, engine] of engines.entries()) {
1153
- logger.logTitledMessage('Identifier', chalk.magentaBright(engine.id))
1154
- logger.logTitledMessage('Name', engine.name)
1155
- logger.logTitledMessage('Description', engine.description)
1156
- logger.logTitledMessage('Type', engine.type)
1197
+ logger.logTitledMessage('Identifier', chalk.magentaBright(engine.id), undefined, 'output')
1198
+ logger.logTitledMessage('Name', engine.name, undefined, 'output')
1199
+ logger.logTitledMessage('Description', engine.description, undefined, 'output')
1200
+ logger.logTitledMessage('Type', engine.type, undefined, 'output')
1157
1201
 
1158
1202
  if (index < engines.length - 1) {
1159
- logger.log('')
1203
+ logger.log('', 'output')
1160
1204
  }
1161
1205
  }
1162
1206
  }
1163
1207
 
1164
- async function listTTSVoices(commandArgs: string[], cliOptions: Map<string, string>) {
1208
+ async function listTTSVoices(operationData: CLIOperationData) {
1165
1209
  const logger = new Logger()
1166
1210
 
1167
- const targetEngine = commandArgs[0]
1168
- const outputFilenames = commandArgs.slice(1)
1211
+ const { operationArgs, operationOptionsLookup, cliOptions } = operationData
1212
+
1213
+ const targetEngine = operationArgs[0]
1214
+ const outputFilenames = operationArgs.slice(1)
1169
1215
 
1170
1216
  if (!targetEngine) {
1171
1217
  const optionsSchema = await getOptionsSchema()
@@ -1177,11 +1223,11 @@ async function listTTSVoices(commandArgs: string[], cliOptions: Map<string, stri
1177
1223
  const additionalOptionsSchema = new Map<string, SchemaTypeDefinition>()
1178
1224
  additionalOptionsSchema.set('overwrite', { type: 'boolean' })
1179
1225
 
1180
- cliOptions.set('engine', targetEngine)
1226
+ operationOptionsLookup.set('engine', targetEngine)
1181
1227
 
1182
- const options: API.VoiceListRequestOptions = await cliOptionsMapToOptionsObject(cliOptions, 'VoiceListRequestOptions', additionalOptionsSchema)
1228
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'VoiceListRequestOptions')
1183
1229
 
1184
- const allowOverwrite = getWithDefault((options as any).overwrite, overwriteByDefault)
1230
+ const allowOverwrite = getWithDefault(cliOptions.overwrite, overwriteByDefault)
1185
1231
 
1186
1232
  await checkOutputFilenames(outputFilenames, false, true, false)
1187
1233
 
@@ -1215,67 +1261,71 @@ async function listTTSVoices(commandArgs: string[], cliOptions: Map<string, stri
1215
1261
  await fileSaver(getEmptyRawAudio(0, 0), voiceList as any, voiceListTextWithoutColors)
1216
1262
  }
1217
1263
  } else {
1218
- logger.log(voiceListText)
1264
+ logger.log(voiceListText, 'output')
1219
1265
  }
1220
1266
 
1221
1267
  logger.end()
1222
1268
  }
1223
1269
 
1224
- async function installPackages(commandArgs: string[], cliOptions: Map<string, string>) {
1270
+ async function installPackages(operationData: CLIOperationData) {
1225
1271
  const logger = new Logger()
1226
1272
 
1227
- if (commandArgs.length == 0) {
1273
+ const { operationArgs } = operationData
1274
+
1275
+ if (operationArgs.length == 0) {
1228
1276
  throw new Error('No package names specified')
1229
1277
  }
1230
1278
 
1231
1279
  const failedPackageNames: string[] = []
1232
1280
 
1233
- for (const packageName of commandArgs) {
1281
+ for (const packageName of operationArgs) {
1234
1282
  try {
1235
1283
  await loadPackage(packageName)
1236
1284
  } catch (e) {
1237
1285
  resetActiveLogger()
1238
1286
 
1239
- logger.log(`Failed installing package ${packageName}: ${e}`)
1287
+ logger.logTitledMessage(`Failed installing package ${packageName}`, e, chalk.redBright, 'error')
1240
1288
  failedPackageNames.push(packageName)
1241
1289
  }
1242
1290
  }
1243
1291
 
1244
1292
  if (failedPackageNames.length > 0) {
1245
1293
  if (failedPackageNames.length == 1) {
1246
- logger.log(`The package ${failedPackageNames[0]} failed to install`)
1294
+ logger.log(`The package ${failedPackageNames[0]} failed to install`, 'error')
1247
1295
  } else {
1248
- logger.log(`The packages ${failedPackageNames.join(', ')} failed to install`)
1296
+ logger.log(`The packages ${failedPackageNames.join(', ')} failed to install`, 'error')
1249
1297
  }
1250
1298
  }
1251
1299
  }
1252
1300
 
1253
- async function uninstallPackages(commandArgs: string[], cliOptions: Map<string, string>) {
1301
+ async function uninstallPackages(operationData: CLIOperationData) {
1254
1302
  const logger = new Logger()
1255
1303
 
1256
- if (commandArgs.length == 0) {
1304
+ const { operationArgs } = operationData
1305
+
1306
+ if (operationArgs.length == 0) {
1257
1307
  throw new Error('No package names specified')
1258
1308
  }
1259
1309
 
1260
1310
  const failedPackageNames: string[] = []
1261
1311
 
1262
- for (const packageName of commandArgs) {
1312
+ for (const packageName of operationArgs) {
1263
1313
  try {
1264
1314
  await removePackage(packageName)
1265
1315
  } catch (e) {
1266
1316
  resetActiveLogger()
1267
1317
 
1268
- logger.log(`Failed uninstalling package ${packageName}: ${e}`)
1318
+ logger.logTitledMessage(`Failed uninstalling package ${packageName}`, e, chalk.redBright, 'error')
1269
1319
  failedPackageNames.push(packageName)
1270
1320
  }
1271
1321
  }
1272
1322
 
1273
1323
  if (failedPackageNames.length > 0) {
1274
- logger.log(`The packages ${failedPackageNames.join(', ')} failed to uninstall`)
1324
+ logger.log(`The packages ${failedPackageNames.join(', ')} failed to uninstall`, 'error')
1275
1325
  }
1276
1326
  }
1277
1327
 
1278
- async function listPackages(commandArgs: string[], cliOptions: Map<string, string>) {
1328
+ async function listPackages(operationData: CLIOperationData) {
1279
1329
  const logger = new Logger()
1280
1330
 
1281
1331
  const packagesDir = await ensureAndGetPackagesDir()
@@ -1302,13 +1352,15 @@ async function listPackages(commandArgs: string[], cliOptions: Map<string, strin
1302
1352
 
1303
1353
  installedPackageNamesFormatted.sort()
1304
1354
 
1305
- logger.log(`Total of ${installedPackageNamesFormatted.length} packages installed in '${packagesDir}'\n`)
1306
-
1355
+ logger.log(`Total of ${installedPackageNamesFormatted.length} packages installed in '${packagesDir}'`)
1356
+ logger.log(``)
1307
1357
  logger.log(installedPackageNamesFormatted.join('\n'))
1308
1358
  }
1309
1359
 
1310
- async function serve(commandArgs: string[], cliOptions: Map<string, string>) {
1311
- const options: ServerOptions = await cliOptionsMapToOptionsObject(cliOptions, 'ServerOptions')
1360
+ async function serve(operationData: CLIOperationData) {
1361
+ const { operationOptionsLookup } = operationData
1362
+
1363
+ const options = await optionsLookupToTypedObject(operationOptionsLookup, 'ServerOptions')
1312
1364
 
1313
1365
  async function onServerStarted(serverOptions: ServerOptions) {
1314
1366
  // Run a test routine (early development)
@@ -1343,7 +1395,7 @@ async function writeSourceSeparationOutputIfNeeded(outputFilename: string, isola
1343
1395
  }
1344
1396
  }
1345
1397
 
1346
- async function cliOptionsMapToOptionsObject(cliOptionsMap: Map<string, string>, optionsRoot: keyof APIOptions, additionalOptionsSchema?: Map<string, SchemaTypeDefinition>) {
1398
+ async function optionsLookupToTypedObject<K extends keyof APIOptions>(cliOptionsMap: Map<string, string>, optionsRoot: K, additionalOptionsSchema?: Map<string, SchemaTypeDefinition>): Promise<APIOptions[K]> {
1347
1399
  const optionsSchema = await getOptionsSchema()
1348
1400
  const resultingObj: any = {}
1349
1401