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 +33 -4
- package/index.js +180 -25
- package/package.json +60 -61
- package/skills/sfxmix/SKILL.md +58 -0
- package/SKILL.md +0 -88
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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.
|