whispermix 1.5.2 → 1.6.4

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 CHANGED
@@ -75,6 +75,31 @@ whisperGroq.fromStream(audioStream)
75
75
 
76
76
  **Note:** Stream transcription is only available for API-based models (OpenAI and Groq). Local models (Whisper and Parakeet) require file input.
77
77
 
78
+ ### ⏱️ Word-Level Timestamps
79
+
80
+ By default, WhisperMix returns only the transcribed text. To include the start and end time for each word, pass `wordTimestamps: true` per call:
81
+
82
+ ```javascript
83
+ const result = await whisperGroq.fromFile('path/to/audio.mp3', {
84
+ wordTimestamps: true
85
+ });
86
+
87
+ console.log(result.text);
88
+ console.log(result.words);
89
+ // [{ word: 'Hello', start: 0.12, end: 0.48 }, ...]
90
+ ```
91
+
92
+ You can also enable it on the instance:
93
+
94
+ ```javascript
95
+ const whisper = new WhisperMix({
96
+ model: 'openai/whisper-1',
97
+ wordTimestamps: true
98
+ });
99
+
100
+ const result = await whisper.fromStream(audioStream);
101
+ ```
102
+
78
103
  ### 🐦 Parakeet local models
79
104
 
80
105
  Parakeet TDT v3 local models are downloaded once and cached in:
@@ -135,21 +160,25 @@ Creates a new WhisperMix instance.
135
160
  - `options.bottleneck`: (Optional) Configuration for Bottleneck rate limiting (API models only).
136
161
  - `options.chunkSize`: (Optional) The size in seconds of the chunks to split the audio into. Default is 890 seconds.
137
162
  - `options.language`: (Optional) Language for local Whisper model. Defaults to 'auto' for automatic detection.
138
- ### `whisper.fromFile(filePath)`
163
+ - `options.wordTimestamps`: (Optional) Return `{ text, words }` instead of plain text, where each word has `start` and `end` times in seconds.
164
+
165
+ ### `whisper.fromFile(filePath, options)`
139
166
 
140
167
  Transcribes audio from a file.
141
168
 
142
169
  - `filePath`: Path to the audio file.
170
+ - `options.wordTimestamps`: (Optional) Return word-level timestamps for this call.
143
171
 
144
- Returns a Promise that resolves with the transcription text.
172
+ Returns a Promise that resolves with the transcription text, or `{ text, words }` when `wordTimestamps` is enabled.
145
173
 
146
- ### `whisper.fromStream(audioStream)`
174
+ ### `whisper.fromStream(audioStream, options)`
147
175
 
148
176
  Transcribes audio from a stream.
149
177
 
150
178
  - `audioStream`: A readable stream of the audio data.
179
+ - `options.wordTimestamps`: (Optional) Return word-level timestamps for this call.
151
180
 
152
- Returns a Promise that resolves with the transcription text.
181
+ Returns a Promise that resolves with the transcription text, or `{ text, words }` when `wordTimestamps` is enabled.
153
182
 
154
183
  ## 📄 License
155
184
 
package/index.js CHANGED
@@ -49,6 +49,7 @@ const PARAKEET_LAYOUTS = {
49
49
  },
50
50
  },
51
51
  };
52
+ const PARAKEET_DEFAULT_CHUNK_SIZE = 40;
52
53
 
53
54
  class WhisperMix {
54
55
  constructor(setup = {}) {
@@ -118,6 +119,9 @@ class WhisperMix {
118
119
  this.showProgress = this.showProgress || false;
119
120
  this.localBackend = this.config.backend || 'transformers';
120
121
  this.layout = this.config.layout;
122
+ if (setup.chunkSize === undefined && this.localBackend === 'onnx-asr-web') {
123
+ this.chunkSize = PARAKEET_DEFAULT_CHUNK_SIZE;
124
+ }
121
125
  this.transcriber = null;
122
126
  this._onnxAsrNodeModule = null;
123
127
  this._warnedParakeetLanguage = false;
@@ -125,7 +129,8 @@ class WhisperMix {
125
129
  this.limiter = new Bottleneck(this.bottleneck);
126
130
  }
127
131
 
128
- async fromFile(filePath) {
132
+ async fromFile(filePath, options = {}) {
133
+ const transcriptionOptions = this._resolveTranscriptionOptions(options);
129
134
  const absolutePath = path.resolve(filePath);
130
135
 
131
136
  // Check if file exists
@@ -142,14 +147,10 @@ class WhisperMix {
142
147
 
143
148
  if (duration <= this.chunkSize) {
144
149
  // Process as a single file
145
- if (this.isLocal) {
146
- return this._transcribeLocalFile(absolutePath);
147
- } else {
148
- return this.fromStream(fs.createReadStream(absolutePath));
149
- }
150
+ return this._transcribeFilePath(absolutePath, transcriptionOptions);
150
151
  } else {
151
152
  // Split audio and process chunks
152
- let accumulatedTranscription = "";
153
+ const transcriptions = [];
153
154
  const numChunks = Math.ceil(duration / this.chunkSize);
154
155
 
155
156
  for (let i = 0; i < numChunks; i++) {
@@ -171,14 +172,8 @@ class WhisperMix {
171
172
  .run();
172
173
  });
173
174
 
174
- let transcription;
175
- if (this.isLocal) {
176
- transcription = await this._transcribeLocalFile(chunkPath);
177
- } else {
178
- const chunkStream = fs.createReadStream(chunkPath);
179
- transcription = await this.fromStream(chunkStream);
180
- }
181
- accumulatedTranscription += (transcription + " ").trimStart();
175
+ const transcription = await this._transcribeFilePath(chunkPath, transcriptionOptions);
176
+ transcriptions.push(this._offsetTranscription(transcription, startTime));
182
177
 
183
178
  // Clean up chunk immediately after processing
184
179
  try {
@@ -187,7 +182,7 @@ class WhisperMix {
187
182
  console.warn(`Could not delete chunk ${chunkPath}:`, unlinkErr);
188
183
  }
189
184
  }
190
- return accumulatedTranscription.trim();
185
+ return this._mergeTranscriptions(transcriptions, transcriptionOptions, duration);
191
186
  }
192
187
  } finally {
193
188
  // Clean up the temporary directory
@@ -199,10 +194,18 @@ class WhisperMix {
199
194
  }
200
195
  }
201
196
 
202
- async fromStream(audioStream) {
197
+ async _transcribeFilePath(filePath, options) {
198
+ if (this.isLocal) {
199
+ return this._transcribeLocalFile(filePath, options);
200
+ }
201
+ return this.fromStream(fs.createReadStream(filePath), options);
202
+ }
203
+
204
+ async fromStream(audioStream, options = {}) {
203
205
  if (this.isLocal) {
204
206
  throw new Error('fromStream is not supported for local Whisper model. Use fromFile instead.');
205
207
  }
208
+ const transcriptionOptions = this._resolveTranscriptionOptions(options);
206
209
 
207
210
  const chunks = [];
208
211
  for await (const chunk of audioStream) {
@@ -210,10 +213,10 @@ class WhisperMix {
210
213
  }
211
214
  const buffer = Buffer.concat(chunks);
212
215
 
213
- return this.limiter.schedule(() => this._makeRequest(buffer));
216
+ return this.limiter.schedule(() => this._makeRequest(buffer, transcriptionOptions));
214
217
  }
215
218
 
216
- async _makeRequest(buffer) {
219
+ async _makeRequest(buffer, options = {}) {
217
220
  try {
218
221
  const blob = new Blob([buffer], { type: 'audio/mpeg' });
219
222
  const formData = new FormData();
@@ -222,6 +225,10 @@ class WhisperMix {
222
225
  if (this.language) {
223
226
  formData.append('language', this.language);
224
227
  }
228
+ if (options.wordTimestamps) {
229
+ formData.append('response_format', 'verbose_json');
230
+ formData.append('timestamp_granularities[]', 'word');
231
+ }
225
232
 
226
233
  const response = await fetch(this.apiUrl, {
227
234
  method: 'POST',
@@ -237,13 +244,13 @@ class WhisperMix {
237
244
  throw responseData;
238
245
  }
239
246
 
240
- return responseData.text.trim();
247
+ return this._formatApiTranscription(responseData, options);
241
248
  } catch (error) {
242
249
  throw error?.message || error;
243
250
  }
244
251
  }
245
252
 
246
- async _transcribeLocalFile(filePath) {
253
+ async _transcribeLocalFile(filePath, options = {}) {
247
254
  try {
248
255
  // Read the audio file as a buffer
249
256
  const buffer = fs.readFileSync(filePath);
@@ -282,11 +289,11 @@ class WhisperMix {
282
289
  this._warnedParakeetLanguage = true;
283
290
  }
284
291
  const result = await transcriber.transcribeSamples(audioData, 16000);
285
- const text = result?.text || result?.utterance_text;
292
+ const text = result?.text ?? result?.utterance_text;
286
293
  if (typeof text !== 'string') {
287
294
  throw new Error('Parakeet transcription returned no text output.');
288
295
  }
289
- return text.trim();
296
+ return this._formatLocalTranscription(text, result?.words, options);
290
297
  }
291
298
 
292
299
  // Pass the processed audio data
@@ -296,14 +303,162 @@ class WhisperMix {
296
303
  if (this.language !== undefined) {
297
304
  transcriberOptions.language = this.language;
298
305
  }
306
+ if (options.wordTimestamps) {
307
+ transcriberOptions.return_timestamps = 'word';
308
+ }
299
309
  const result = await transcriber(audioData, transcriberOptions);
300
- return result.text.trim();
310
+ const words = options.wordTimestamps ? this._wordsFromTransformersChunks(result.chunks) : undefined;
311
+ return this._formatLocalTranscription(result.text, words, options);
301
312
  } catch (error) {
302
313
  const cacheHint = this.localBackend === 'onnx-asr-web'
303
314
  ? this._getParakeetCacheDir(this.layout)
304
315
  : `${env.cacheDir}${this.modelName}/`;
305
- throw new Error(`Local transcription failed: ${error.message}. If this happened after an interrupted download, remove the model cache at ${cacheHint} and try again.`);
316
+ const chunkHint = this.localBackend === 'onnx-asr-web' && /bad_alloc/i.test(error.message)
317
+ ? ` Parakeet local models should use short chunks; try chunkSize: ${PARAKEET_DEFAULT_CHUNK_SIZE} or less.`
318
+ : '';
319
+ throw new Error(`Local transcription failed: ${error.message}.${chunkHint} If this happened after an interrupted download, remove the model cache at ${cacheHint} and try again.`);
320
+ }
321
+ }
322
+
323
+ _resolveTranscriptionOptions(options = {}) {
324
+ return {
325
+ wordTimestamps: Boolean(options.wordTimestamps ?? this.wordTimestamps),
326
+ };
327
+ }
328
+
329
+ _formatApiTranscription(responseData, options) {
330
+ const text = this._formatText(responseData.text);
331
+ if (!options.wordTimestamps) {
332
+ return text;
333
+ }
334
+ return {
335
+ ...responseData,
336
+ text,
337
+ words: this._normalizeWords(responseData.words),
338
+ };
339
+ }
340
+
341
+ _formatLocalTranscription(text, words, options) {
342
+ const formattedText = this._formatText(text);
343
+ if (!options.wordTimestamps) {
344
+ return formattedText;
345
+ }
346
+ return {
347
+ text: formattedText,
348
+ words: this._normalizeWords(words),
349
+ };
350
+ }
351
+
352
+ _formatText(text) {
353
+ if (typeof text !== 'string') {
354
+ throw new Error('Transcription returned no text output.');
355
+ }
356
+ return text.trim();
357
+ }
358
+
359
+ _wordsFromTransformersChunks(chunks) {
360
+ if (!Array.isArray(chunks)) {
361
+ return undefined;
362
+ }
363
+ return chunks.map((chunk) => ({
364
+ word: chunk.text,
365
+ timestamp: chunk.timestamp,
366
+ }));
367
+ }
368
+
369
+ _normalizeWords(words) {
370
+ if (!Array.isArray(words)) {
371
+ throw new Error('Word timestamps were requested, but the selected backend did not return word timestamps.');
372
+ }
373
+
374
+ return words
375
+ .map((word) => {
376
+ if (!word || typeof word !== 'object') {
377
+ throw new Error('Word timestamp response contains an invalid word entry.');
378
+ }
379
+
380
+ const text = typeof word.word === 'string' ? word.word : word.text;
381
+ const start = typeof word.start === 'number' ? word.start : word.timestamp?.[0];
382
+ const end = typeof word.end === 'number' ? word.end : word.timestamp?.[1];
383
+
384
+ if (typeof text !== 'string' || typeof start !== 'number' || typeof end !== 'number') {
385
+ throw new Error('Word timestamp response contains an invalid word entry.');
386
+ }
387
+
388
+ return {
389
+ word: text.trim(),
390
+ start,
391
+ end,
392
+ };
393
+ })
394
+ .filter((word) => word.word.length > 0);
395
+ }
396
+
397
+ _mergeTranscriptions(transcriptions, options, duration) {
398
+ const text = transcriptions
399
+ .map((transcription) => this._transcriptionText(transcription))
400
+ .filter((chunkText) => chunkText.length > 0)
401
+ .join(' ')
402
+ .trim();
403
+
404
+ if (!options.wordTimestamps) {
405
+ return text;
406
+ }
407
+
408
+ const words = transcriptions.flatMap((transcription) => {
409
+ if (!Array.isArray(transcription.words)) {
410
+ throw new Error('Word timestamps were requested, but a chunk returned no word timestamps.');
411
+ }
412
+ return transcription.words;
413
+ });
414
+ const segments = transcriptions.flatMap((transcription) => (
415
+ Array.isArray(transcription.segments) ? transcription.segments : []
416
+ ));
417
+ const result = { text, words, duration };
418
+
419
+ if (segments.length > 0) {
420
+ result.segments = segments;
421
+ }
422
+
423
+ return result;
424
+ }
425
+
426
+ _transcriptionText(transcription) {
427
+ if (typeof transcription === 'string') {
428
+ return transcription.trim();
429
+ }
430
+ return this._formatText(transcription.text);
431
+ }
432
+
433
+ _offsetTranscription(transcription, offsetSeconds) {
434
+ if (typeof transcription === 'string' || offsetSeconds === 0) {
435
+ return transcription;
436
+ }
437
+
438
+ return {
439
+ ...transcription,
440
+ words: Array.isArray(transcription.words)
441
+ ? this._offsetTimedItems(transcription.words, offsetSeconds)
442
+ : transcription.words,
443
+ segments: Array.isArray(transcription.segments)
444
+ ? this._offsetTimedItems(transcription.segments, offsetSeconds)
445
+ : transcription.segments,
446
+ };
447
+ }
448
+
449
+ _offsetTimedItems(items, offsetSeconds) {
450
+ return items.map((item) => ({
451
+ ...item,
452
+ start: this._offsetTime(item.start, offsetSeconds),
453
+ end: this._offsetTime(item.end, offsetSeconds),
454
+ }));
455
+ }
456
+
457
+ _offsetTime(value, offsetSeconds) {
458
+ if (typeof value !== 'number') {
459
+ return value;
306
460
  }
461
+ return Number((value + offsetSeconds).toFixed(3));
307
462
  }
308
463
 
309
464
  async _getLocalTranscriber() {
package/package.json CHANGED
@@ -1,62 +1,61 @@
1
1
  {
2
- "name": "whispermix",
3
- "description": "🎙️ WhisperMix is a versatile module for transcribing audio using OpenAI’s Whisper or Groq’s Whisper v3 model.",
4
- "version": "1.5.2",
5
- "type": "module",
6
- "keywords": [
7
- "whisper",
8
- "openai",
9
- "groq",
10
- "transcription",
11
- "speech-to-text",
12
- "audio",
13
- "voice",
14
- "ai",
15
- "machine-learning",
16
- "nlp",
17
- "natural-language-processing",
18
- "audio-processing",
19
- "voice-recognition",
20
- "speech-recognition",
21
- "api-wrapper",
22
- "whisper-large-v3",
23
- "whisper-1",
24
- "streaming",
25
- "file-processing",
26
- "multilingual",
27
- "bottleneck",
28
- "chunk",
29
- "clasen",
30
- "local",
31
- "xenova",
32
- "parakeet",
33
- "nvidia",
34
- "onnx"
35
- ],
36
- "repository": {
37
- "type": "git",
38
- "url": "git+https://github.com/clasen/WhisperMix.git"
39
- },
40
- "main": "index.js",
41
- "scripts": {
42
- "test": "echo \"Error: no test specified\" && exit 1"
43
- },
44
- "author": "Martin Clasen",
45
- "license": "MIT",
46
- "bugs": {
47
- "url": "https://github.com/clasen/WhisperMix/issues"
48
- },
49
- "dependencies": {
50
- "@huggingface/transformers": "^4.2.0",
51
- "audio-decode": "^2.2.3",
52
- "bottleneck": "^2.19.5",
53
- "fluent-ffmpeg": "^2.1.3",
54
- "get-audio-duration": "^4.0.1",
55
- "onnx-asr-web": "^0.1.3"
56
- },
57
- "devDependencies": {
58
- "node-addon-api": "^8.7.0",
59
- "node-gyp": "^12.3.0"
60
- },
61
- "packageManager": "pnpm@11.1.2"
62
- }
2
+ "name": "whispermix",
3
+ "description": "🎙️ WhisperMix is a versatile module for transcribing audio using OpenAI’s Whisper or Groq’s Whisper v3 model.",
4
+ "version": "1.6.4",
5
+ "type": "module",
6
+ "keywords": [
7
+ "whisper",
8
+ "openai",
9
+ "groq",
10
+ "transcription",
11
+ "speech-to-text",
12
+ "audio",
13
+ "voice",
14
+ "ai",
15
+ "machine-learning",
16
+ "nlp",
17
+ "natural-language-processing",
18
+ "audio-processing",
19
+ "voice-recognition",
20
+ "speech-recognition",
21
+ "api-wrapper",
22
+ "whisper-large-v3",
23
+ "whisper-1",
24
+ "streaming",
25
+ "file-processing",
26
+ "multilingual",
27
+ "bottleneck",
28
+ "chunk",
29
+ "clasen",
30
+ "local",
31
+ "xenova",
32
+ "parakeet",
33
+ "nvidia",
34
+ "onnx"
35
+ ],
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/clasen/WhisperMix.git"
39
+ },
40
+ "main": "index.js",
41
+ "author": "Martin Clasen",
42
+ "license": "MIT",
43
+ "bugs": {
44
+ "url": "https://github.com/clasen/WhisperMix/issues"
45
+ },
46
+ "dependencies": {
47
+ "@huggingface/transformers": "^4.2.0",
48
+ "audio-decode": "^2.2.3",
49
+ "bottleneck": "^2.19.5",
50
+ "fluent-ffmpeg": "^2.1.3",
51
+ "get-audio-duration": "^4.0.1",
52
+ "onnx-asr-web": "^0.1.3"
53
+ },
54
+ "devDependencies": {
55
+ "node-addon-api": "^8.7.0",
56
+ "node-gyp": "^12.3.0"
57
+ },
58
+ "scripts": {
59
+ "test": "echo \"Error: no test specified\" && exit 1"
60
+ }
61
+ }
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: whispermix
3
+ description: "Transcribe audio to text with WhisperMix in Node.js (OpenAI, Groq, Whisper local or Parakeet local)."
4
+ ---
5
+
6
+ # WhisperMix
7
+
8
+ WhisperMix transcribes audio to text with one API and multiple backends.
9
+
10
+ Use this skill when the user wants speech-to-text from an audio file (`.mp3`, `.wav`, `.m4a`, `.ogg`, `.flac`, `.webm`) in a Node.js project.
11
+
12
+ Do not use for TTS, translation, or live microphone streaming.
13
+
14
+ ## Choose a model (quick)
15
+
16
+ - **Cloud, best quality/speed:** `groq/whisper-large-v3` or `openai/whisper-1`
17
+ - **Local, fastest startup:** `efederici/parakeet-tdt-0.6b-v3-int4`
18
+ - **Local, better language control:** `xenova/whisper-large-v3`
19
+
20
+ Important limits:
21
+ - Local models are file-only (`fromFile`), no `fromStream`.
22
+ - API models need `OPENAI_API_KEY` or `GROQ_API_KEY`.
23
+ - `ffmpeg` is required for long files.
24
+
25
+ ## Basic usage
26
+
27
+ Install:
28
+
29
+ ```bash
30
+ npm install whispermix
31
+ ```
32
+
33
+ File transcription:
34
+
35
+ ```javascript
36
+ import WhisperMix from 'whispermix';
37
+
38
+ const w = new WhisperMix({ model: 'groq/whisper-large-v3' });
39
+ const text = await w.fromFile('audio.mp3');
40
+ console.log(text);
41
+ ```
42
+
43
+ Word timestamps:
44
+
45
+ ```javascript
46
+ import WhisperMix from 'whispermix';
47
+
48
+ const w = new WhisperMix({ model: 'openai/whisper-1' });
49
+ const result = await w.fromFile('audio.mp3', { wordTimestamps: true });
50
+ console.log(result.text);
51
+ console.log(result.words);
52
+ ```
53
+
54
+ ## Troubleshooting
55
+
56
+ - `OPENAI_API_KEY` / `GROQ_API_KEY is not set`: set env var or use a local model.
57
+ - `fromStream not supported`: use `fromFile` with local models.
58
+ - `Cannot find ffmpeg`: install `ffmpeg` and retry.
package/SKILL.md DELETED
@@ -1,88 +0,0 @@
1
- ---
2
- name: whispermix
3
- description: Transcribe audio to text using WhisperMix, a Node.js wrapper around OpenAI Whisper, Groq Whisper Large v3, local Whisper (xenova) and local Parakeet TDT v3 models. Use when the user asks to "transcribe audio", "speech to text", "convert audio/voice to text", mentions an audio file (.mp3, .wav, .m4a, .ogg, .flac, .webm) to turn into text, or names any of these models/providers: Whisper, OpenAI Whisper, Groq Whisper, Whisper Large v3, Parakeet, NVIDIA Parakeet, xenova/whisper, onnx-asr. Also use when the user wants to pick between a cloud API and a local on-device transcription model in a Node.js project.
4
- ---
5
-
6
- # WhisperMix
7
-
8
- Single Node.js entry point for audio transcription. Picks one of four backends behind the same API: OpenAI Whisper, Groq Whisper Large v3, local Whisper (xenova), or local Parakeet TDT v3. Handles long files by chunking and rate-limits API calls automatically.
9
-
10
- Do NOT use for TTS, live microphone streaming, or translation. WhisperMix is one-way: a complete audio file/stream → text.
11
-
12
- ## Model selection (decide first)
13
-
14
- | Need | Pick |
15
- |---|---|
16
- | Offline, lowest latency | `istupakov/parakeet-tdt-0.6b-v3` |
17
- | Offline, smallest footprint (~410 MB) | `efederici/parakeet-tdt-0.6b-v3-int4` |
18
- | Highest accuracy, cloud | `groq/whisper-large-v3` (fast) or `openai/whisper-1` |
19
- | Offline + per-language control | `xenova/whisper-large-v3` or `xenova/whisper-base` |
20
- | Node stream input (not a file) | API only: `openai/whisper-1` or `groq/whisper-large-v3` |
21
-
22
- Constraints to surface before coding:
23
- - Local models (`xenova/*`, `*/parakeet-tdt-0.6b-v3*`) accept **files only**, not streams.
24
- - `language` option applies only to local Whisper. Parakeet is multilingual and ignores it.
25
- - API models need `OPENAI_API_KEY` or `GROQ_API_KEY` in the environment.
26
- - First Parakeet run downloads weights to `~/.cache/whispermix/parakeet/<modelKey>/` — warn the user.
27
- - Requires `ffmpeg` on `PATH` for long-audio chunking (>15 min split automatically).
28
-
29
- ## API
30
-
31
- ```bash
32
- npm install whispermix
33
- ```
34
-
35
- ESM only. If the consumer is CommonJS, use `const WhisperMix = (await import('whispermix')).default;`.
36
-
37
- ```javascript
38
- import WhisperMix from 'whispermix';
39
-
40
- const w = new WhisperMix({ model: '<modelKey>' });
41
- const text = await w.fromFile('path/to/audio.mp3');
42
- // API models only:
43
- const text2 = await w.fromStream(fs.createReadStream('path/to/audio.mp3'));
44
- ```
45
-
46
- Constructor options:
47
- - `model` (required) — see selection table.
48
- - `language` — local Whisper only, e.g. `'spanish'`. Default `'auto'`.
49
- - `chunkSize` — seconds per chunk for long audio. Default `890` (~14m50s).
50
- - `bottleneck` — Bottleneck config for API models. Defaults: `minTime: 3000`, `maxConcurrent: 1`, `reservoir: 18`, `reservoirRefreshAmount: 18`, `reservoirRefreshInterval: 60000`.
51
- - `showProgress` — boolean, prints chunk/decoding progress.
52
-
53
- ## Examples
54
-
55
- Cheapest local:
56
-
57
- ```javascript
58
- import WhisperMix from 'whispermix';
59
- const w = new WhisperMix({ model: 'efederici/parakeet-tdt-0.6b-v3-int4', showProgress: true });
60
- console.log(await w.fromFile('meeting.wav'));
61
- ```
62
-
63
- Groq with custom rate limit:
64
-
65
- ```javascript
66
- import WhisperMix from 'whispermix';
67
- const w = new WhisperMix({
68
- model: 'groq/whisper-large-v3',
69
- bottleneck: { minTime: 4000, maxConcurrent: 1 },
70
- });
71
- console.log(await w.fromFile('podcast.mp3'));
72
- ```
73
-
74
- Local Whisper, fixed language:
75
-
76
- ```javascript
77
- import WhisperMix from 'whispermix';
78
- const w = new WhisperMix({ model: 'xenova/whisper-large-v3', language: 'spanish' });
79
- console.log(await w.fromFile('entrevista.m4a'));
80
- ```
81
-
82
- ## Troubleshooting
83
-
84
- - **`OPENAI_API_KEY`/`GROQ_API_KEY is not set`** — export the key, or switch to a local model.
85
- - **`fromStream` not supported** — local models are file-only. Use `fromFile`, or switch to an API model.
86
- - **`Cannot find ffmpeg`** — install it (`brew install ffmpeg` / `apt install ffmpeg`).
87
- - **First Parakeet call hangs** — weights downloading; enable `showProgress: true`.
88
- - **`ERR_REQUIRE_ESM`** — WhisperMix is ESM-only; use dynamic `import()` from CommonJS.