recmp3-cli 1.0.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/LICENSE +661 -0
- package/LICENSE-COMMERCIAL.md +25 -0
- package/README.md +255 -0
- package/dist/chunk-7NR5CU7W.js +46 -0
- package/dist/chunk-DDXRBIWU.js +40 -0
- package/dist/chunk-FNZ6ZCOK.js +51 -0
- package/dist/chunk-NUWDWBJQ.js +63 -0
- package/dist/chunk-NY5EJT5D.js +127 -0
- package/dist/chunk-XGHYROLT.js +481 -0
- package/dist/index.js +1960 -0
- package/dist/keychain-CLYHGYS2.js +3 -0
- package/dist/linux-pulse-AROLYZNB.js +183 -0
- package/dist/local-whisper-VH26RX7Y.js +4 -0
- package/dist/mac-avfoundation-COPCFRZT.js +136 -0
- package/dist/registry-D5SOVUEJ.js +5 -0
- package/dist/windows-dshow-O2GU4ZLR.js +150 -0
- package/package.json +75 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { LocalWhisperProvider } from './chunk-NY5EJT5D.js';
|
|
3
|
+
import { log } from './chunk-DDXRBIWU.js';
|
|
4
|
+
import { ConfigError, TranscriptionError, NetworkError } from './chunk-NUWDWBJQ.js';
|
|
5
|
+
import { existsSync, readFileSync } from 'fs';
|
|
6
|
+
import { mkdir, writeFile, readFile } from 'fs/promises';
|
|
7
|
+
import { join, dirname, basename, extname } from 'path';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
|
|
11
|
+
function xdgConfigHome() {
|
|
12
|
+
return process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
13
|
+
}
|
|
14
|
+
function xdgDataHome() {
|
|
15
|
+
return process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
|
|
16
|
+
}
|
|
17
|
+
function appPaths() {
|
|
18
|
+
const platform = process.platform;
|
|
19
|
+
if (platform === "darwin") {
|
|
20
|
+
const appSupport = join(
|
|
21
|
+
homedir(),
|
|
22
|
+
"Library",
|
|
23
|
+
"Application Support",
|
|
24
|
+
"recmp3"
|
|
25
|
+
);
|
|
26
|
+
return {
|
|
27
|
+
config: join(homedir(), "Library", "Preferences", "recmp3"),
|
|
28
|
+
data: appSupport,
|
|
29
|
+
recordings: join(appSupport, "recordings"),
|
|
30
|
+
transcripts: join(appSupport, "transcripts")
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (platform === "win32") {
|
|
34
|
+
const appData = process.env.APPDATA ?? join(homedir(), "AppData", "Roaming");
|
|
35
|
+
const base = join(appData, "recmp3");
|
|
36
|
+
return {
|
|
37
|
+
config: base,
|
|
38
|
+
data: base,
|
|
39
|
+
recordings: join(base, "recordings"),
|
|
40
|
+
transcripts: join(base, "transcripts")
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const config = join(xdgConfigHome(), "recmp3");
|
|
44
|
+
const data = join(xdgDataHome(), "recmp3");
|
|
45
|
+
return {
|
|
46
|
+
config,
|
|
47
|
+
data,
|
|
48
|
+
recordings: join(data, "recordings"),
|
|
49
|
+
transcripts: join(data, "transcripts")
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
var paths = appPaths();
|
|
53
|
+
var configFilePath = join(paths.config, "config.json");
|
|
54
|
+
var RecmpConfigSchema = z.object({
|
|
55
|
+
version: z.literal(1).default(1),
|
|
56
|
+
provider: z.object({
|
|
57
|
+
default: z.enum(["groq", "openai", "local-whisper"]).default("groq"),
|
|
58
|
+
groq: z.object({
|
|
59
|
+
model: z.string().default("whisper-large-v3-turbo"),
|
|
60
|
+
baseUrl: z.string().optional(),
|
|
61
|
+
timeoutMs: z.number().optional()
|
|
62
|
+
}).optional(),
|
|
63
|
+
openai: z.object({
|
|
64
|
+
model: z.string().default("whisper-1"),
|
|
65
|
+
baseUrl: z.string().optional(),
|
|
66
|
+
timeoutMs: z.number().optional()
|
|
67
|
+
}).optional(),
|
|
68
|
+
local: z.object({
|
|
69
|
+
binPath: z.string().optional(),
|
|
70
|
+
modelPath: z.string().optional(),
|
|
71
|
+
language: z.string().optional()
|
|
72
|
+
}).optional()
|
|
73
|
+
}).default({}),
|
|
74
|
+
audio: z.object({
|
|
75
|
+
source: z.string().default("default"),
|
|
76
|
+
sampleRate: z.literal(16e3).default(16e3),
|
|
77
|
+
channels: z.literal(1).default(1),
|
|
78
|
+
format: z.literal("wav").default("wav")
|
|
79
|
+
}).default({}),
|
|
80
|
+
output: z.object({
|
|
81
|
+
recordingDir: z.string().optional(),
|
|
82
|
+
namePrefix: z.string().default("rec"),
|
|
83
|
+
keepAudio: z.boolean().default(true),
|
|
84
|
+
saveTranscriptToFile: z.boolean().default(true)
|
|
85
|
+
}).default({}),
|
|
86
|
+
transcription: z.object({
|
|
87
|
+
defaultLanguage: z.string().optional(),
|
|
88
|
+
chunking: z.object({
|
|
89
|
+
enabled: z.boolean().default(true),
|
|
90
|
+
chunkSeconds: z.number().default(600)
|
|
91
|
+
}).default({})
|
|
92
|
+
}).default({}),
|
|
93
|
+
ui: z.object({
|
|
94
|
+
clipboardOnTranscribe: z.boolean().default(true),
|
|
95
|
+
printOnTranscribe: z.boolean().default(true),
|
|
96
|
+
color: z.enum(["auto", "always", "never"]).default("auto")
|
|
97
|
+
}).default({}),
|
|
98
|
+
consent: z.object({
|
|
99
|
+
uploadsAcknowledged: z.boolean().default(false),
|
|
100
|
+
acknowledgedAt: z.string().optional()
|
|
101
|
+
}).default({})
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// src/config/load.ts
|
|
105
|
+
var _config = null;
|
|
106
|
+
function applyEnvOverrides(config) {
|
|
107
|
+
const clone = structuredClone(config);
|
|
108
|
+
if (process.env.RECMP3_PROVIDER) {
|
|
109
|
+
const p = process.env.RECMP3_PROVIDER;
|
|
110
|
+
if (p === "groq" || p === "openai") clone.provider.default = p;
|
|
111
|
+
}
|
|
112
|
+
if (process.env.RECMP3_MODEL) {
|
|
113
|
+
const model = process.env.RECMP3_MODEL;
|
|
114
|
+
const provider = clone.provider.default;
|
|
115
|
+
if (provider === "groq") {
|
|
116
|
+
clone.provider.groq = { ...clone.provider.groq, model };
|
|
117
|
+
} else if (provider === "openai") {
|
|
118
|
+
clone.provider.openai = { ...clone.provider.openai, model };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (process.env.RECMP3_SOURCE) {
|
|
122
|
+
clone.audio.source = process.env.RECMP3_SOURCE;
|
|
123
|
+
}
|
|
124
|
+
if (process.env.RECMP3_LANG) {
|
|
125
|
+
clone.transcription.defaultLanguage = process.env.RECMP3_LANG;
|
|
126
|
+
}
|
|
127
|
+
if (process.env.RECMP3_OUTDIR) {
|
|
128
|
+
clone.output.recordingDir = process.env.RECMP3_OUTDIR;
|
|
129
|
+
}
|
|
130
|
+
if (process.env.RECMP3_WHISPER_BIN) {
|
|
131
|
+
clone.provider.local = {
|
|
132
|
+
...clone.provider.local,
|
|
133
|
+
binPath: process.env.RECMP3_WHISPER_BIN
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (process.env.RECMP3_WHISPER_MODEL) {
|
|
137
|
+
clone.provider.local = {
|
|
138
|
+
...clone.provider.local,
|
|
139
|
+
modelPath: process.env.RECMP3_WHISPER_MODEL
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return clone;
|
|
143
|
+
}
|
|
144
|
+
async function loadConfig() {
|
|
145
|
+
if (_config) return _config;
|
|
146
|
+
if (existsSync(".env")) {
|
|
147
|
+
const { config: dotenvConfig } = await import('dotenv');
|
|
148
|
+
dotenvConfig({ path: join(process.cwd(), ".env") });
|
|
149
|
+
}
|
|
150
|
+
let raw = {};
|
|
151
|
+
if (existsSync(configFilePath)) {
|
|
152
|
+
try {
|
|
153
|
+
raw = JSON.parse(readFileSync(configFilePath, "utf-8"));
|
|
154
|
+
} catch {
|
|
155
|
+
raw = {};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const parsed = RecmpConfigSchema.safeParse(raw);
|
|
159
|
+
const base = parsed.success ? parsed.data : RecmpConfigSchema.parse({});
|
|
160
|
+
if (!base.output.recordingDir) {
|
|
161
|
+
base.output.recordingDir = paths.recordings;
|
|
162
|
+
}
|
|
163
|
+
_config = applyEnvOverrides(base);
|
|
164
|
+
return _config;
|
|
165
|
+
}
|
|
166
|
+
function resetConfigCache() {
|
|
167
|
+
_config = null;
|
|
168
|
+
}
|
|
169
|
+
async function saveConfig(config) {
|
|
170
|
+
await mkdir(dirname(configFilePath), { recursive: true });
|
|
171
|
+
await writeFile(
|
|
172
|
+
configFilePath,
|
|
173
|
+
`${JSON.stringify(config, null, 2)}
|
|
174
|
+
`,
|
|
175
|
+
"utf-8"
|
|
176
|
+
);
|
|
177
|
+
_config = config;
|
|
178
|
+
}
|
|
179
|
+
var ENV_VAR = {
|
|
180
|
+
groq: "GROQ_API_KEY",
|
|
181
|
+
openai: "OPENAI_API_KEY"
|
|
182
|
+
};
|
|
183
|
+
async function getApiKey(provider) {
|
|
184
|
+
const fromEnv = process.env[ENV_VAR[provider]];
|
|
185
|
+
if (fromEnv) return fromEnv;
|
|
186
|
+
const { getSecret } = await import('./keychain-CLYHGYS2.js');
|
|
187
|
+
return getSecret(ENV_VAR[provider]);
|
|
188
|
+
}
|
|
189
|
+
var GROQ_BASE_URL = "https://api.groq.com/openai/v1";
|
|
190
|
+
var MAX_FILE_SIZE = 25 * 1024 * 1024;
|
|
191
|
+
var SUPPORTED_FORMATS = [
|
|
192
|
+
"flac",
|
|
193
|
+
"m4a",
|
|
194
|
+
"mp3",
|
|
195
|
+
"mp4",
|
|
196
|
+
"mpeg",
|
|
197
|
+
"mpga",
|
|
198
|
+
"oga",
|
|
199
|
+
"ogg",
|
|
200
|
+
"wav",
|
|
201
|
+
"webm"
|
|
202
|
+
];
|
|
203
|
+
function getMimeType(filePath) {
|
|
204
|
+
const ext = extname(filePath).toLowerCase().slice(1);
|
|
205
|
+
const mimes = {
|
|
206
|
+
wav: "audio/wav",
|
|
207
|
+
mp3: "audio/mpeg",
|
|
208
|
+
mp4: "audio/mp4",
|
|
209
|
+
m4a: "audio/mp4",
|
|
210
|
+
ogg: "audio/ogg",
|
|
211
|
+
oga: "audio/ogg",
|
|
212
|
+
flac: "audio/flac",
|
|
213
|
+
webm: "audio/webm"
|
|
214
|
+
};
|
|
215
|
+
return mimes[ext] ?? "audio/wav";
|
|
216
|
+
}
|
|
217
|
+
var GroqProvider = class {
|
|
218
|
+
constructor(config) {
|
|
219
|
+
this.config = config;
|
|
220
|
+
this.baseUrl = config.baseUrl ?? GROQ_BASE_URL;
|
|
221
|
+
this.timeoutMs = config.timeoutMs ?? 12e4;
|
|
222
|
+
}
|
|
223
|
+
config;
|
|
224
|
+
name = "groq";
|
|
225
|
+
maxFileSizeBytes = MAX_FILE_SIZE;
|
|
226
|
+
supportedFormats = SUPPORTED_FORMATS;
|
|
227
|
+
baseUrl;
|
|
228
|
+
timeoutMs;
|
|
229
|
+
async transcribe(input) {
|
|
230
|
+
const {
|
|
231
|
+
audioPath,
|
|
232
|
+
language,
|
|
233
|
+
prompt,
|
|
234
|
+
responseFormat = "verbose_json",
|
|
235
|
+
signal
|
|
236
|
+
} = input;
|
|
237
|
+
const t0 = Date.now();
|
|
238
|
+
log.info(
|
|
239
|
+
`Transcribing with Groq (${this.config.model}): ${basename(audioPath)}`
|
|
240
|
+
);
|
|
241
|
+
const audioBuffer = await readFile(audioPath);
|
|
242
|
+
const mimeType = getMimeType(audioPath);
|
|
243
|
+
const filename = basename(audioPath);
|
|
244
|
+
const form = new FormData();
|
|
245
|
+
const blob = new Blob([audioBuffer], { type: mimeType });
|
|
246
|
+
form.append("file", blob, filename);
|
|
247
|
+
form.append("model", this.config.model);
|
|
248
|
+
form.append("response_format", responseFormat);
|
|
249
|
+
if (language) form.append("language", language);
|
|
250
|
+
if (prompt) form.append("prompt", prompt);
|
|
251
|
+
const controller = new AbortController();
|
|
252
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
253
|
+
const combinedSignal = signal ?? controller.signal;
|
|
254
|
+
let response;
|
|
255
|
+
try {
|
|
256
|
+
response = await fetch(`${this.baseUrl}/audio/transcriptions`, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers: {
|
|
259
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
260
|
+
},
|
|
261
|
+
body: form,
|
|
262
|
+
signal: combinedSignal
|
|
263
|
+
});
|
|
264
|
+
} catch (err) {
|
|
265
|
+
clearTimeout(timeout);
|
|
266
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
267
|
+
throw new TranscriptionError(
|
|
268
|
+
`Transcription timed out after ${Math.round(this.timeoutMs / 1e3)}s.`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
throw new NetworkError(
|
|
272
|
+
`Network error connecting to Groq: ${err instanceof Error ? err.message : String(err)}`
|
|
273
|
+
);
|
|
274
|
+
} finally {
|
|
275
|
+
clearTimeout(timeout);
|
|
276
|
+
}
|
|
277
|
+
if (!response.ok) {
|
|
278
|
+
const body = await response.text().catch(() => "");
|
|
279
|
+
throw new TranscriptionError(
|
|
280
|
+
`Groq API error ${response.status}: ${body || response.statusText}`,
|
|
281
|
+
response.status
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
const raw = await response.json();
|
|
285
|
+
const text = typeof raw === "string" ? raw : raw.text ?? "";
|
|
286
|
+
const latencyMs = Date.now() - t0;
|
|
287
|
+
log.info(`Transcription complete in ${(latencyMs / 1e3).toFixed(1)}s`);
|
|
288
|
+
return {
|
|
289
|
+
text: text.trim(),
|
|
290
|
+
language: raw.language,
|
|
291
|
+
durationSec: raw.duration,
|
|
292
|
+
segments: raw.segments?.map((s) => ({
|
|
293
|
+
startSec: s.start,
|
|
294
|
+
endSec: s.end,
|
|
295
|
+
text: s.text
|
|
296
|
+
})),
|
|
297
|
+
raw,
|
|
298
|
+
provider: "groq",
|
|
299
|
+
model: this.config.model,
|
|
300
|
+
latencyMs
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
async ping() {
|
|
304
|
+
const t0 = Date.now();
|
|
305
|
+
try {
|
|
306
|
+
const response = await fetch(`${this.baseUrl}/models`, {
|
|
307
|
+
headers: { Authorization: `Bearer ${this.config.apiKey}` },
|
|
308
|
+
signal: AbortSignal.timeout(1e4)
|
|
309
|
+
});
|
|
310
|
+
if (!response.ok) {
|
|
311
|
+
return { ok: false, error: `HTTP ${response.status}` };
|
|
312
|
+
}
|
|
313
|
+
return { ok: true, latencyMs: Date.now() - t0 };
|
|
314
|
+
} catch (err) {
|
|
315
|
+
return {
|
|
316
|
+
ok: false,
|
|
317
|
+
error: err instanceof Error ? err.message : String(err)
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
var OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
323
|
+
var MAX_FILE_SIZE2 = 25 * 1024 * 1024;
|
|
324
|
+
var SUPPORTED_FORMATS2 = [
|
|
325
|
+
"flac",
|
|
326
|
+
"m4a",
|
|
327
|
+
"mp3",
|
|
328
|
+
"mp4",
|
|
329
|
+
"mpeg",
|
|
330
|
+
"mpga",
|
|
331
|
+
"oga",
|
|
332
|
+
"ogg",
|
|
333
|
+
"wav",
|
|
334
|
+
"webm"
|
|
335
|
+
];
|
|
336
|
+
function getMimeType2(filePath) {
|
|
337
|
+
const ext = extname(filePath).toLowerCase().slice(1);
|
|
338
|
+
const mimes = {
|
|
339
|
+
wav: "audio/wav",
|
|
340
|
+
mp3: "audio/mpeg",
|
|
341
|
+
m4a: "audio/mp4",
|
|
342
|
+
ogg: "audio/ogg",
|
|
343
|
+
flac: "audio/flac"
|
|
344
|
+
};
|
|
345
|
+
return mimes[ext] ?? "audio/wav";
|
|
346
|
+
}
|
|
347
|
+
var OpenAIProvider = class {
|
|
348
|
+
constructor(config) {
|
|
349
|
+
this.config = config;
|
|
350
|
+
this.baseUrl = config.baseUrl ?? OPENAI_BASE_URL;
|
|
351
|
+
this.timeoutMs = config.timeoutMs ?? 12e4;
|
|
352
|
+
}
|
|
353
|
+
config;
|
|
354
|
+
name = "openai";
|
|
355
|
+
maxFileSizeBytes = MAX_FILE_SIZE2;
|
|
356
|
+
supportedFormats = SUPPORTED_FORMATS2;
|
|
357
|
+
baseUrl;
|
|
358
|
+
timeoutMs;
|
|
359
|
+
async transcribe(input) {
|
|
360
|
+
const {
|
|
361
|
+
audioPath,
|
|
362
|
+
language,
|
|
363
|
+
prompt,
|
|
364
|
+
responseFormat = "verbose_json",
|
|
365
|
+
signal
|
|
366
|
+
} = input;
|
|
367
|
+
const t0 = Date.now();
|
|
368
|
+
log.info(
|
|
369
|
+
`Transcribing with OpenAI (${this.config.model}): ${basename(audioPath)}`
|
|
370
|
+
);
|
|
371
|
+
const audioBuffer = await readFile(audioPath);
|
|
372
|
+
const form = new FormData();
|
|
373
|
+
const blob = new Blob([audioBuffer], { type: getMimeType2(audioPath) });
|
|
374
|
+
form.append("file", blob, basename(audioPath));
|
|
375
|
+
form.append("model", this.config.model);
|
|
376
|
+
form.append("response_format", responseFormat);
|
|
377
|
+
if (language) form.append("language", language);
|
|
378
|
+
if (prompt) form.append("prompt", prompt);
|
|
379
|
+
const controller = new AbortController();
|
|
380
|
+
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
381
|
+
let response;
|
|
382
|
+
try {
|
|
383
|
+
response = await fetch(`${this.baseUrl}/audio/transcriptions`, {
|
|
384
|
+
method: "POST",
|
|
385
|
+
headers: { Authorization: `Bearer ${this.config.apiKey}` },
|
|
386
|
+
body: form,
|
|
387
|
+
signal: signal ?? controller.signal
|
|
388
|
+
});
|
|
389
|
+
} catch (err) {
|
|
390
|
+
clearTimeout(timeout);
|
|
391
|
+
throw new NetworkError(
|
|
392
|
+
`Network error connecting to OpenAI: ${err instanceof Error ? err.message : String(err)}`
|
|
393
|
+
);
|
|
394
|
+
} finally {
|
|
395
|
+
clearTimeout(timeout);
|
|
396
|
+
}
|
|
397
|
+
if (!response.ok) {
|
|
398
|
+
const body = await response.text().catch(() => "");
|
|
399
|
+
throw new TranscriptionError(
|
|
400
|
+
`OpenAI API error ${response.status}: ${body || response.statusText}`,
|
|
401
|
+
response.status
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const raw = await response.json();
|
|
405
|
+
return {
|
|
406
|
+
text: (typeof raw === "string" ? raw : raw.text ?? "").trim(),
|
|
407
|
+
language: raw.language,
|
|
408
|
+
durationSec: raw.duration,
|
|
409
|
+
segments: raw.segments?.map((s) => ({
|
|
410
|
+
startSec: s.start,
|
|
411
|
+
endSec: s.end,
|
|
412
|
+
text: s.text
|
|
413
|
+
})),
|
|
414
|
+
raw,
|
|
415
|
+
provider: "openai",
|
|
416
|
+
model: this.config.model,
|
|
417
|
+
latencyMs: Date.now() - t0
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
async ping() {
|
|
421
|
+
const t0 = Date.now();
|
|
422
|
+
try {
|
|
423
|
+
const r = await fetch(`${this.baseUrl}/models`, {
|
|
424
|
+
headers: { Authorization: `Bearer ${this.config.apiKey}` },
|
|
425
|
+
signal: AbortSignal.timeout(1e4)
|
|
426
|
+
});
|
|
427
|
+
return r.ok ? { ok: true, latencyMs: Date.now() - t0 } : { ok: false, error: `HTTP ${r.status}` };
|
|
428
|
+
} catch (err) {
|
|
429
|
+
return {
|
|
430
|
+
ok: false,
|
|
431
|
+
error: err instanceof Error ? err.message : String(err)
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// src/transcription/registry.ts
|
|
438
|
+
function providerUploads(name) {
|
|
439
|
+
return name === "groq" || name === "openai";
|
|
440
|
+
}
|
|
441
|
+
async function createProvider(config) {
|
|
442
|
+
const providerName = config.provider.default;
|
|
443
|
+
if (providerName === "groq") {
|
|
444
|
+
const apiKey = await getApiKey("groq");
|
|
445
|
+
if (!apiKey) {
|
|
446
|
+
throw new ConfigError(
|
|
447
|
+
"GROQ_API_KEY is not set.\n Set it with: export GROQ_API_KEY=your_key\n Or run: recmp3 config set-key groq --key your_key"
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
const groqConfig = config.provider.groq;
|
|
451
|
+
return new GroqProvider({
|
|
452
|
+
apiKey,
|
|
453
|
+
model: groqConfig?.model ?? "whisper-large-v3-turbo",
|
|
454
|
+
baseUrl: groqConfig?.baseUrl,
|
|
455
|
+
timeoutMs: groqConfig?.timeoutMs
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
if (providerName === "openai") {
|
|
459
|
+
const apiKey = await getApiKey("openai");
|
|
460
|
+
if (!apiKey) {
|
|
461
|
+
throw new ConfigError(
|
|
462
|
+
"OPENAI_API_KEY is not set.\n Set it with: export OPENAI_API_KEY=your_key\n Or run: recmp3 config set-key openai --key your_key"
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
const openaiConfig = config.provider.openai;
|
|
466
|
+
return new OpenAIProvider({
|
|
467
|
+
apiKey,
|
|
468
|
+
model: openaiConfig?.model ?? "whisper-1",
|
|
469
|
+
baseUrl: openaiConfig?.baseUrl,
|
|
470
|
+
timeoutMs: openaiConfig?.timeoutMs
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
if (providerName === "local-whisper") {
|
|
474
|
+
return new LocalWhisperProvider(config.provider.local ?? {});
|
|
475
|
+
}
|
|
476
|
+
throw new ConfigError(
|
|
477
|
+
`Unknown provider: "${providerName}". Valid options: groq, openai, local-whisper.`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export { RecmpConfigSchema, configFilePath, createProvider, getApiKey, loadConfig, paths, providerUploads, resetConfigCache, saveConfig };
|