listener-ai 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/dist/cli.js +291 -62
- package/dist/configService.js +4 -0
- package/dist/geminiService.js +163 -81
- package/dist/main.js +89 -20
- package/dist/outputService.js +132 -2
- package/package.json +9 -7
package/README.md
CHANGED
|
@@ -41,8 +41,18 @@ listener config set notionDatabaseId <your-id>
|
|
|
41
41
|
```bash
|
|
42
42
|
listener recording.mp3 # Transcribe to default output dir
|
|
43
43
|
listener recording.m4a --output ./ # Transcribe to current directory
|
|
44
|
-
listener
|
|
44
|
+
listener transcript recording.wav # Print transcript to stdout (no summary)
|
|
45
|
+
listener transcript recording.wav -o out.txt
|
|
46
|
+
# Write transcript to a file
|
|
47
|
+
listener transcript recording.wav --prompt "Translate to English while transcribing"
|
|
48
|
+
# Override the default transcription instruction
|
|
49
|
+
listener config list # Show all config values (secrets masked)
|
|
50
|
+
listener config get <key> # Print one config value
|
|
51
|
+
listener config set <key> <value> # Set a config value
|
|
52
|
+
listener config unset <key> # Clear a config value (falls back to default)
|
|
45
53
|
listener config path # Print config file path
|
|
54
|
+
listener --version # Print CLI version
|
|
55
|
+
listener --help # Show usage
|
|
46
56
|
```
|
|
47
57
|
|
|
48
58
|
Supported formats: mp3, m4a, wav, ogg, flac, aac, wma, opus, webm
|
package/dist/cli.js
CHANGED
|
@@ -57,32 +57,54 @@ const SUPPORTED_EXTENSIONS = new Set([
|
|
|
57
57
|
'.opus',
|
|
58
58
|
'.webm',
|
|
59
59
|
]);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
60
|
+
const VERSION = (() => {
|
|
61
|
+
try {
|
|
62
|
+
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
63
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
64
|
+
return pkg.version ?? 'unknown';
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return 'unknown';
|
|
68
|
+
}
|
|
69
|
+
})();
|
|
70
|
+
const USAGE_TEXT = 'Usage: listener <file> [--output <dir>] Transcribe an audio file into a meeting note\n' +
|
|
71
|
+
' listener transcript <file> [--output <path>] [--prompt <text>]\n' +
|
|
72
|
+
' Transcribe to plain text only (no summary)\n' +
|
|
73
|
+
' listener list [--limit <n>] List past transcriptions\n' +
|
|
74
|
+
' listener show <ref> Print summary to stdout\n' +
|
|
75
|
+
' listener export <ref> [<path>] [--json] [--transcript]\n' +
|
|
76
|
+
' Export transcription\n' +
|
|
77
|
+
' listener search <query> [--limit <n>] [--transcript] [--field <name>]\n' +
|
|
78
|
+
' Search past transcriptions\n' +
|
|
79
|
+
' listener merge <ref1> <ref2> [<ref3>...] [--title <t>]\n' +
|
|
80
|
+
' Concat the source audio of two or more notes,\n' +
|
|
81
|
+
' re-transcribe end-to-end, and save as a new note\n' +
|
|
82
|
+
' listener ask <question> [--ref <ref>]\n' +
|
|
83
|
+
' Ask the AI agent about saved meetings or settings\n' +
|
|
84
|
+
' listener config list|get|set|unset|path\n' +
|
|
85
|
+
' Manage configuration\n' +
|
|
86
|
+
'\n' +
|
|
87
|
+
'<ref> is a number from `listener list` or a folder name.\n' +
|
|
88
|
+
'\n' +
|
|
89
|
+
'Options:\n' +
|
|
90
|
+
' --output, -o <path>\n' +
|
|
91
|
+
' Parent directory for the output folder (transcribe);\n' +
|
|
92
|
+
' destination file or directory (transcript)\n' +
|
|
93
|
+
' --prompt <text> Override the default transcription instruction (transcript)\n' +
|
|
94
|
+
' --limit <n> Max results (0 = all, default: 20)\n' +
|
|
95
|
+
' --json Export as JSON instead of markdown\n' +
|
|
96
|
+
' --transcript Include transcript body (export: append; search: widen scope)\n' +
|
|
97
|
+
' --field <name> Restrict search to one of: title, summary, keyPoints, actionItems, transcript, all\n' +
|
|
98
|
+
' --version, -V Print CLI version\n' +
|
|
99
|
+
' --help, -h Show this help message\n';
|
|
100
|
+
function usageError() {
|
|
101
|
+
process.stderr.write(USAGE_TEXT);
|
|
84
102
|
process.exit(1);
|
|
85
103
|
}
|
|
104
|
+
function showHelp() {
|
|
105
|
+
process.stdout.write(USAGE_TEXT);
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
86
108
|
const KNOWN_CONFIG_KEYS = [
|
|
87
109
|
'geminiApiKey',
|
|
88
110
|
'geminiModel',
|
|
@@ -90,23 +112,124 @@ const KNOWN_CONFIG_KEYS = [
|
|
|
90
112
|
'notionApiKey',
|
|
91
113
|
'notionDatabaseId',
|
|
92
114
|
'autoMode',
|
|
115
|
+
'meetingDetection',
|
|
116
|
+
'displayDetection',
|
|
93
117
|
'globalShortcut',
|
|
118
|
+
'knownWords',
|
|
119
|
+
'summaryPrompt',
|
|
120
|
+
'maxRecordingMinutes',
|
|
121
|
+
'recordingReminderMinutes',
|
|
94
122
|
'minRecordingSeconds',
|
|
123
|
+
'recordSystemAudio',
|
|
124
|
+
'slackWebhookUrl',
|
|
125
|
+
'slackAutoShare',
|
|
95
126
|
];
|
|
127
|
+
function isSensitiveKey(key) {
|
|
128
|
+
const lk = key.toLowerCase();
|
|
129
|
+
return lk.includes('key') || lk.includes('webhook');
|
|
130
|
+
}
|
|
96
131
|
function maskValue(key, value) {
|
|
97
132
|
if (value == null || value === '')
|
|
98
133
|
return '(not set)';
|
|
99
|
-
if (
|
|
100
|
-
|
|
134
|
+
if (Array.isArray(value)) {
|
|
135
|
+
if (value.length === 0)
|
|
136
|
+
return '(none)';
|
|
137
|
+
const joined = value.map((x) => String(x)).join(', ');
|
|
138
|
+
return joined.length > 60 ? `${joined.slice(0, 57)}...` : joined;
|
|
139
|
+
}
|
|
140
|
+
const str = String(value);
|
|
141
|
+
if (isSensitiveKey(key)) {
|
|
142
|
+
return str.length > 4 ? `****${str.slice(-4)}` : '****';
|
|
143
|
+
}
|
|
144
|
+
if (str.length > 60)
|
|
145
|
+
return `${str.slice(0, 57)}...`;
|
|
146
|
+
return str;
|
|
147
|
+
}
|
|
148
|
+
function parseBool(key, v) {
|
|
149
|
+
if (v !== 'true' && v !== 'false') {
|
|
150
|
+
process.stderr.write(`Error: ${key} must be "true" or "false"\n`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
return v === 'true';
|
|
154
|
+
}
|
|
155
|
+
function parseNonNegInt(key, v) {
|
|
156
|
+
const n = Number.parseInt(v, 10);
|
|
157
|
+
if (Number.isNaN(n) || n < 0 || String(n) !== v.trim()) {
|
|
158
|
+
process.stderr.write(`Error: ${key} must be a non-negative integer\n`);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
return n;
|
|
162
|
+
}
|
|
163
|
+
function parseKnownWords(v) {
|
|
164
|
+
return v
|
|
165
|
+
.split(',')
|
|
166
|
+
.map((s) => s.trim())
|
|
167
|
+
.filter((s) => s.length > 0);
|
|
168
|
+
}
|
|
169
|
+
function applyConfigSet(config, key, value) {
|
|
170
|
+
switch (key) {
|
|
171
|
+
case 'geminiApiKey':
|
|
172
|
+
config.setGeminiApiKey(value);
|
|
173
|
+
return;
|
|
174
|
+
case 'geminiModel':
|
|
175
|
+
config.setGeminiModel(value);
|
|
176
|
+
return;
|
|
177
|
+
case 'geminiFlashModel':
|
|
178
|
+
config.setGeminiFlashModel(value);
|
|
179
|
+
return;
|
|
180
|
+
case 'notionApiKey':
|
|
181
|
+
config.setNotionApiKey(value);
|
|
182
|
+
return;
|
|
183
|
+
case 'notionDatabaseId':
|
|
184
|
+
config.setNotionDatabaseId(value);
|
|
185
|
+
return;
|
|
186
|
+
case 'autoMode':
|
|
187
|
+
config.setAutoMode(parseBool('autoMode', value));
|
|
188
|
+
return;
|
|
189
|
+
case 'meetingDetection':
|
|
190
|
+
config.updateConfig({ meetingDetection: parseBool('meetingDetection', value) });
|
|
191
|
+
return;
|
|
192
|
+
case 'displayDetection':
|
|
193
|
+
config.setDisplayDetection(parseBool('displayDetection', value));
|
|
194
|
+
return;
|
|
195
|
+
case 'globalShortcut':
|
|
196
|
+
config.setGlobalShortcut(value);
|
|
197
|
+
return;
|
|
198
|
+
case 'knownWords':
|
|
199
|
+
config.setKnownWords(parseKnownWords(value));
|
|
200
|
+
return;
|
|
201
|
+
case 'summaryPrompt':
|
|
202
|
+
config.setSummaryPrompt(value);
|
|
203
|
+
return;
|
|
204
|
+
case 'maxRecordingMinutes':
|
|
205
|
+
config.setMaxRecordingMinutes(parseNonNegInt('maxRecordingMinutes', value));
|
|
206
|
+
return;
|
|
207
|
+
case 'recordingReminderMinutes':
|
|
208
|
+
config.setRecordingReminderMinutes(parseNonNegInt('recordingReminderMinutes', value));
|
|
209
|
+
return;
|
|
210
|
+
case 'minRecordingSeconds':
|
|
211
|
+
config.setMinRecordingSeconds(parseNonNegInt('minRecordingSeconds', value));
|
|
212
|
+
return;
|
|
213
|
+
case 'recordSystemAudio':
|
|
214
|
+
config.setRecordSystemAudio(parseBool('recordSystemAudio', value));
|
|
215
|
+
return;
|
|
216
|
+
case 'slackWebhookUrl':
|
|
217
|
+
config.setSlackWebhookUrl(value);
|
|
218
|
+
return;
|
|
219
|
+
case 'slackAutoShare':
|
|
220
|
+
config.setSlackAutoShare(parseBool('slackAutoShare', value));
|
|
221
|
+
return;
|
|
101
222
|
}
|
|
102
|
-
return value;
|
|
103
223
|
}
|
|
104
224
|
function handleConfig(subArgs) {
|
|
105
225
|
const dataPath = (0, dataPath_1.getDataPath)();
|
|
106
226
|
const config = new configService_1.ConfigService(dataPath);
|
|
107
227
|
const sub = subArgs[0];
|
|
108
|
-
if (!sub
|
|
109
|
-
|
|
228
|
+
if (!sub) {
|
|
229
|
+
usageError();
|
|
230
|
+
}
|
|
231
|
+
if (sub === '--help' || sub === '-h') {
|
|
232
|
+
showHelp();
|
|
110
233
|
}
|
|
111
234
|
if (sub === 'path') {
|
|
112
235
|
process.stdout.write(`${config.getConfigPath()}\n`);
|
|
@@ -116,8 +239,7 @@ function handleConfig(subArgs) {
|
|
|
116
239
|
const all = config.getAllConfig();
|
|
117
240
|
for (const key of KNOWN_CONFIG_KEYS) {
|
|
118
241
|
const raw = all[key];
|
|
119
|
-
|
|
120
|
-
process.stdout.write(`${key}=${display}\n`);
|
|
242
|
+
process.stdout.write(`${key}=${maskValue(key, raw)}\n`);
|
|
121
243
|
}
|
|
122
244
|
return;
|
|
123
245
|
}
|
|
@@ -134,7 +256,12 @@ function handleConfig(subArgs) {
|
|
|
134
256
|
}
|
|
135
257
|
const all = config.getAllConfig();
|
|
136
258
|
const val = all[key];
|
|
137
|
-
|
|
259
|
+
if (Array.isArray(val)) {
|
|
260
|
+
process.stdout.write(`${val.join(',')}\n`);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
process.stdout.write(`${val ?? ''}\n`);
|
|
264
|
+
}
|
|
138
265
|
return;
|
|
139
266
|
}
|
|
140
267
|
if (sub === 'set') {
|
|
@@ -149,35 +276,27 @@ function handleConfig(subArgs) {
|
|
|
149
276
|
process.stderr.write(`Known keys: ${KNOWN_CONFIG_KEYS.join(', ')}\n`);
|
|
150
277
|
process.exit(1);
|
|
151
278
|
}
|
|
152
|
-
|
|
153
|
-
geminiApiKey: (v) => config.setGeminiApiKey(v),
|
|
154
|
-
geminiModel: (v) => config.setGeminiModel(v),
|
|
155
|
-
geminiFlashModel: (v) => config.setGeminiFlashModel(v),
|
|
156
|
-
notionApiKey: (v) => config.setNotionApiKey(v),
|
|
157
|
-
notionDatabaseId: (v) => config.setNotionDatabaseId(v),
|
|
158
|
-
autoMode: (v) => {
|
|
159
|
-
if (v !== 'true' && v !== 'false') {
|
|
160
|
-
process.stderr.write('Error: autoMode must be "true" or "false"\n');
|
|
161
|
-
process.exit(1);
|
|
162
|
-
}
|
|
163
|
-
config.setAutoMode(v === 'true');
|
|
164
|
-
},
|
|
165
|
-
globalShortcut: (v) => config.setGlobalShortcut(v),
|
|
166
|
-
minRecordingSeconds: (v) => {
|
|
167
|
-
const n = Number.parseInt(v, 10);
|
|
168
|
-
if (Number.isNaN(n) || n < 0 || String(n) !== v.trim()) {
|
|
169
|
-
process.stderr.write('Error: minRecordingSeconds must be a non-negative integer (0 disables)\n');
|
|
170
|
-
process.exit(1);
|
|
171
|
-
}
|
|
172
|
-
config.setMinRecordingSeconds(n);
|
|
173
|
-
},
|
|
174
|
-
};
|
|
175
|
-
setters[key](value);
|
|
279
|
+
applyConfigSet(config, key, value);
|
|
176
280
|
process.stderr.write(`Set ${key}\n`);
|
|
177
281
|
return;
|
|
178
282
|
}
|
|
283
|
+
if (sub === 'unset') {
|
|
284
|
+
const key = subArgs[1];
|
|
285
|
+
if (!key) {
|
|
286
|
+
process.stderr.write('Error: Missing key. Usage: listener config unset <key>\n');
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
if (!KNOWN_CONFIG_KEYS.includes(key)) {
|
|
290
|
+
process.stderr.write(`Error: Unknown key: ${key}\n`);
|
|
291
|
+
process.stderr.write(`Known keys: ${KNOWN_CONFIG_KEYS.join(', ')}\n`);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
config.unsetKey(key);
|
|
295
|
+
process.stderr.write(`Unset ${key}\n`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
179
298
|
process.stderr.write(`Error: Unknown config command: ${sub}\n`);
|
|
180
|
-
|
|
299
|
+
usageError();
|
|
181
300
|
}
|
|
182
301
|
async function resolveRef(ref, dataPath) {
|
|
183
302
|
if (/^\d+$/.test(ref)) {
|
|
@@ -312,6 +431,8 @@ async function handleExport(args) {
|
|
|
312
431
|
/* ignore */
|
|
313
432
|
}
|
|
314
433
|
}
|
|
434
|
+
const liveNotes = (0, outputService_1.parseLiveNotesField)(meta.liveNotes);
|
|
435
|
+
const highlights = (0, outputService_1.parseHighlightsField)(meta.highlights);
|
|
315
436
|
const obj = {
|
|
316
437
|
title: meta.title || '',
|
|
317
438
|
transcribedAt: meta.transcribedAt || '',
|
|
@@ -319,6 +440,8 @@ async function handleExport(args) {
|
|
|
319
440
|
keyPoints: meta.keyPoints || [],
|
|
320
441
|
actionItems: meta.actionItems || [],
|
|
321
442
|
customFields,
|
|
443
|
+
...(liveNotes ? { liveNotes } : {}),
|
|
444
|
+
...(highlights ? { highlights } : {}),
|
|
322
445
|
};
|
|
323
446
|
if (includeTranscript) {
|
|
324
447
|
obj.transcript = meta.transcript || '';
|
|
@@ -548,10 +671,112 @@ async function handleAsk(args) {
|
|
|
548
671
|
}
|
|
549
672
|
}
|
|
550
673
|
}
|
|
674
|
+
async function handleTranscript(args) {
|
|
675
|
+
let filePath;
|
|
676
|
+
let outputArg;
|
|
677
|
+
let promptText;
|
|
678
|
+
for (let i = 0; i < args.length; i++) {
|
|
679
|
+
const a = args[i];
|
|
680
|
+
if ((a === '--output' || a === '-o') && i + 1 < args.length) {
|
|
681
|
+
outputArg = args[++i];
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
if (a === '--prompt' && i + 1 < args.length) {
|
|
685
|
+
promptText = args[++i];
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (a.startsWith('-')) {
|
|
689
|
+
process.stderr.write(`Error: Unknown option: ${a}\n`);
|
|
690
|
+
process.exit(1);
|
|
691
|
+
}
|
|
692
|
+
if (filePath) {
|
|
693
|
+
process.stderr.write(`Error: Unexpected argument: ${a}\n`);
|
|
694
|
+
process.exit(1);
|
|
695
|
+
}
|
|
696
|
+
filePath = a;
|
|
697
|
+
}
|
|
698
|
+
if (!filePath) {
|
|
699
|
+
process.stderr.write('Error: No audio file specified. Usage: listener transcript <file> [--output <path>] [--prompt <text>]\n');
|
|
700
|
+
process.exit(1);
|
|
701
|
+
}
|
|
702
|
+
filePath = path.resolve(filePath);
|
|
703
|
+
if (!fs.existsSync(filePath)) {
|
|
704
|
+
process.stderr.write(`Error: File not found: ${filePath}\n`);
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
708
|
+
if (!SUPPORTED_EXTENSIONS.has(ext)) {
|
|
709
|
+
process.stderr.write(`Error: Unsupported file type: ${ext}\n`);
|
|
710
|
+
process.stderr.write(`Supported formats: ${[...SUPPORTED_EXTENSIONS].join(', ')}\n`);
|
|
711
|
+
process.exit(1);
|
|
712
|
+
}
|
|
713
|
+
const dataPath = (0, dataPath_1.getDataPath)();
|
|
714
|
+
const config = new configService_1.ConfigService(dataPath);
|
|
715
|
+
const apiKey = config.getGeminiApiKey();
|
|
716
|
+
if (!apiKey) {
|
|
717
|
+
process.stderr.write('Error: Gemini API key not found.\n' +
|
|
718
|
+
'Set GEMINI_API_KEY env var or configure via the Listener.AI app.\n');
|
|
719
|
+
process.exit(1);
|
|
720
|
+
}
|
|
721
|
+
// Resolve --output before the expensive transcription so we fail fast on a
|
|
722
|
+
// bad path. Existing directory => <dir>/<basename>.transcript.md.
|
|
723
|
+
// Anything else => the path itself, treated as a file.
|
|
724
|
+
let outputPath;
|
|
725
|
+
if (outputArg) {
|
|
726
|
+
const resolved = path.resolve(outputArg);
|
|
727
|
+
let isDir = false;
|
|
728
|
+
try {
|
|
729
|
+
isDir = fs.statSync(resolved).isDirectory();
|
|
730
|
+
}
|
|
731
|
+
catch {
|
|
732
|
+
// ENOENT or similar: treat as a file path, validated below.
|
|
733
|
+
}
|
|
734
|
+
if (isDir) {
|
|
735
|
+
outputPath = path.join(resolved, `${path.basename(filePath, ext)}.transcript.md`);
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
outputPath = resolved;
|
|
739
|
+
const parent = path.dirname(outputPath);
|
|
740
|
+
if (!fs.existsSync(parent)) {
|
|
741
|
+
process.stderr.write(`Error: Output directory does not exist: ${parent}\n`);
|
|
742
|
+
process.exit(1);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
const gemini = new geminiService_1.GeminiService({
|
|
747
|
+
apiKey,
|
|
748
|
+
dataPath,
|
|
749
|
+
knownWords: config.getKnownWords(),
|
|
750
|
+
proModel: config.getGeminiModel(),
|
|
751
|
+
flashModel: config.getGeminiFlashModel(),
|
|
752
|
+
});
|
|
753
|
+
process.stderr.write(`Processing: ${filePath}\n`);
|
|
754
|
+
const result = await gemini.transcribeAudio(filePath, (_percent, message) => {
|
|
755
|
+
process.stderr.write(` ${message}\n`);
|
|
756
|
+
}, undefined, undefined, { transcriptOnly: true, transcriptionPrompt: promptText });
|
|
757
|
+
if (outputPath) {
|
|
758
|
+
fs.writeFileSync(outputPath, result.transcript, 'utf-8');
|
|
759
|
+
process.stderr.write('Done.\n');
|
|
760
|
+
process.stdout.write(`${outputPath}\n`);
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
// Wait for the OS to drain the write before returning, so multi-MB
|
|
764
|
+
// transcripts piped to a slow consumer are not truncated on process exit.
|
|
765
|
+
const out = result.transcript.endsWith('\n') ? result.transcript : `${result.transcript}\n`;
|
|
766
|
+
await new Promise((resolve) => process.stdout.write(out, () => resolve()));
|
|
767
|
+
}
|
|
768
|
+
}
|
|
551
769
|
async function main() {
|
|
552
770
|
const args = process.argv.slice(2);
|
|
553
|
-
if (args.
|
|
554
|
-
|
|
771
|
+
if (args.includes('--version') || args.includes('-V')) {
|
|
772
|
+
process.stdout.write(`listener ${VERSION}\n`);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
776
|
+
showHelp();
|
|
777
|
+
}
|
|
778
|
+
if (args.length === 0) {
|
|
779
|
+
usageError();
|
|
555
780
|
}
|
|
556
781
|
if (args[0] === 'config') {
|
|
557
782
|
handleConfig(args.slice(1));
|
|
@@ -581,16 +806,20 @@ async function main() {
|
|
|
581
806
|
await handleAsk(args.slice(1));
|
|
582
807
|
return;
|
|
583
808
|
}
|
|
809
|
+
if (args[0] === 'transcript') {
|
|
810
|
+
await handleTranscript(args.slice(1));
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
584
813
|
// Parse arguments
|
|
585
814
|
let filePath;
|
|
586
815
|
let outputDir;
|
|
587
816
|
for (let i = 0; i < args.length; i++) {
|
|
588
|
-
if (args[i] === '--output' && i + 1 < args.length) {
|
|
817
|
+
if ((args[i] === '--output' || args[i] === '-o') && i + 1 < args.length) {
|
|
589
818
|
outputDir = args[++i];
|
|
590
819
|
}
|
|
591
820
|
else if (args[i].startsWith('-')) {
|
|
592
821
|
process.stderr.write(`Error: Unknown option: ${args[i]}\n`);
|
|
593
|
-
|
|
822
|
+
usageError();
|
|
594
823
|
}
|
|
595
824
|
else {
|
|
596
825
|
filePath = args[i];
|
|
@@ -598,7 +827,7 @@ async function main() {
|
|
|
598
827
|
}
|
|
599
828
|
if (!filePath) {
|
|
600
829
|
process.stderr.write('Error: No audio file specified.\n');
|
|
601
|
-
|
|
830
|
+
usageError();
|
|
602
831
|
}
|
|
603
832
|
// Resolve to absolute path
|
|
604
833
|
filePath = path.resolve(filePath);
|
package/dist/configService.js
CHANGED