@moxxy/plugin-stt-whisper 0.28.0 → 0.29.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/dist/audio.d.ts.map +1 -1
- package/dist/audio.js +4 -2
- package/dist/audio.js.map +1 -1
- package/package.json +4 -4
- package/src/audio.ts +4 -2
- package/src/whisper.test.ts +25 -7
package/dist/audio.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio.d.ts","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"audio.d.ts","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAiB,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEnE;;;;;iEAKiE;AACjE,OAAO,EAAE,sBAAsB,EAAE,CAAC;AAElC;;;;;;;;;0EAS0E;AAC1E,eAAO,MAAM,wBAAwB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAYlE,CAAC;AAEL,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,UAAU,GAAG,WAAW,EAC/B,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,cAAc,CAAC,EAAE,MAAM,GACtB,qBAAqB,CAgCvB;AAWD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,GAAG,WAAW,EAAE,UAAU,SAAS,GAAG,UAAU,CAgC7F"}
|
package/dist/audio.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* (filename inference from MIME, raw PCM16 → WAV wrapping). Centralizing
|
|
6
6
|
* them here keeps the wrapper plugins thin.
|
|
7
7
|
*/
|
|
8
|
-
import { MOXXY_PCM16_24KHZ_MIME } from '@moxxy/sdk';
|
|
8
|
+
import { assertDefined, MOXXY_PCM16_24KHZ_MIME } from '@moxxy/sdk';
|
|
9
9
|
/** A custom MIME tag used by the TUI voice recorder to flag raw PCM16
|
|
10
10
|
* mono @ 24kHz bytes (ffmpeg's `-f s16le` output) so the transcriber
|
|
11
11
|
* knows to wrap them in a WAV header before upload. Public so other
|
|
@@ -61,7 +61,9 @@ export function normalizeWhisperUpload(audio, mimeType, filenamePrefix) {
|
|
|
61
61
|
// Guard the runtime type so `.toLowerCase()` can't throw on hostile input —
|
|
62
62
|
// anything non-string degrades to the default rather than crashing the path.
|
|
63
63
|
const rawMime = typeof mimeType === 'string' ? mimeType : '';
|
|
64
|
-
const
|
|
64
|
+
const firstSegment = (rawMime || 'audio/wav').toLowerCase().split(';')[0];
|
|
65
|
+
assertDefined(firstSegment, 'String.split always yields at least one element');
|
|
66
|
+
const mt = firstSegment.trim();
|
|
65
67
|
if (mt === MOXXY_PCM16_24KHZ_MIME) {
|
|
66
68
|
return {
|
|
67
69
|
bytes: pcm16MonoToWav(bytes, 24_000),
|
package/dist/audio.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio.js","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"audio.js","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEnE;;;;;iEAKiE;AACjE,OAAO,EAAE,sBAAsB,EAAE,CAAC;AAElC;;;;;;;;;0EAS0E;AAC1E,MAAM,CAAC,MAAM,wBAAwB;AACnC,uCAAuC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAA2B,EAAE;IACnG,WAAW,EAAE,WAAW;IACxB,YAAY,EAAE,YAAY;IAC1B,YAAY,EAAE,WAAW;IACzB,WAAW,EAAE,WAAW;IACxB,WAAW,EAAE,WAAW;IACxB,aAAa,EAAE,WAAW;IAC1B,YAAY,EAAE,YAAY;IAC1B,WAAW,EAAE,WAAW;IACxB,WAAW,EAAE,WAAW;IACxB,YAAY,EAAE,YAAY;CAC3B,CAAC,CAAC;AAQL;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,sBAAsB,CACpC,KAA+B,EAC/B,QAA4B,EAC5B,cAAuB;IAEvB,MAAM,KAAK,GAAG,KAAK,YAAY,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC;IAC1E,0EAA0E;IAC1E,yEAAyE;IACzE,wEAAwE;IACxE,EAAE;IACF,4EAA4E;IAC5E,8EAA8E;IAC9E,yEAAyE;IACzE,4EAA4E;IAC5E,6EAA6E;IAC7E,MAAM,OAAO,GAAG,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7D,MAAM,YAAY,GAAG,CAAC,OAAO,IAAI,WAAW,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1E,aAAa,CAAC,YAAY,EAAE,iDAAiD,CAAC,CAAC;IAC/E,MAAM,EAAE,GAAG,YAAY,CAAC,IAAI,EAAE,CAAC;IAC/B,IAAI,EAAE,KAAK,sBAAsB,EAAE,CAAC;QAClC,OAAO;YACL,KAAK,EAAE,cAAc,CAAC,KAAK,EAAE,MAAM,CAAC;YACpC,QAAQ,EAAE,WAAW;YACrB,QAAQ,EAAE,MAAM,CAAC,cAAc,EAAE,KAAK,CAAC,IAAI,WAAW;SACvD,CAAC;IACJ,CAAC;IACD,wEAAwE;IACxE,yEAAyE;IACzE,uDAAuD;IACvD,MAAM,MAAM,GAAG,wBAAwB,CAAC,EAAE,CAAC,CAAC;IAC5C,MAAM,WAAW,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC;IACtE,OAAO;QACL,KAAK;QACL,QAAQ,EAAE,EAAE;QACZ,QAAQ,EAAE,MAAM,CAAC,cAAc,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC,IAAI,WAAW;KACpE,CAAC;AACJ,CAAC;AAED,SAAS,MAAM,CAAC,MAA0B,EAAE,GAAW;IACrD,OAAO,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AACjD,CAAC;AAED,SAAS,KAAK,CAAC,QAAgB;IAC7B,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACtC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AACpD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,GAA6B,EAAE,UAAU,GAAG,MAAM;IAC/E,MAAM,GAAG,GAAG,GAAG,YAAY,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC;IAClE,uEAAuE;IACvE,yEAAyE;IACzE,MAAM,IAAI,GAAG,GAAG,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAC7E,uEAAuE;IACvE,0EAA0E;IAC1E,IAAI,IAAI,CAAC,UAAU,GAAG,UAAU,GAAG,EAAE,EAAE,CAAC;QACtC,MAAM,IAAI,UAAU,CAClB,8CAA8C,IAAI,CAAC,UAAU,eAAe,UAAU,GAAG,EAAE,IAAI,CAChG,CAAC;IACJ,CAAC;IACD,MAAM,WAAW,GAAG,EAAE,CAAC;IACvB,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,WAAW,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC;IAC1D,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAEtC,UAAU,CAAC,GAAG,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;IAC3B,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAC9C,UAAU,CAAC,GAAG,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;IAC3B,UAAU,CAAC,GAAG,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC;IAC5B,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;IAC7B,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;IAC5B,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;IAC5B,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC;IACrC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,UAAU,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;IACzC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;IAC5B,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;IAC7B,UAAU,CAAC,GAAG,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC;IAC5B,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAC1C,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAE3B,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,UAAU,CAAC,MAAkB,EAAE,MAAc,EAAE,KAAa;IACnE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACzC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAC3C,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moxxy/plugin-stt-whisper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "OpenAI Whisper Transcriber for moxxy. Audio bytes → text + segments. Plugs into the session's TranscriberRegistry; channels with audio input (Telegram voice, HTTP multipart) consume it transparently.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"moxxy",
|
|
@@ -44,14 +44,14 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"openai": "^4.80.0",
|
|
47
|
-
"@moxxy/sdk": "0.
|
|
47
|
+
"@moxxy/sdk": "0.29.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/node": "^22.10.0",
|
|
51
51
|
"typescript": "^5.7.3",
|
|
52
52
|
"vitest": "^2.1.8",
|
|
53
|
-
"@moxxy/
|
|
54
|
-
"@moxxy/
|
|
53
|
+
"@moxxy/tsconfig": "0.0.0",
|
|
54
|
+
"@moxxy/vitest-preset": "0.0.0"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"build": "tsc -p tsconfig.json",
|
package/src/audio.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* them here keeps the wrapper plugins thin.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { MOXXY_PCM16_24KHZ_MIME } from '@moxxy/sdk';
|
|
9
|
+
import { assertDefined, MOXXY_PCM16_24KHZ_MIME } from '@moxxy/sdk';
|
|
10
10
|
|
|
11
11
|
/** A custom MIME tag used by the TUI voice recorder to flag raw PCM16
|
|
12
12
|
* mono @ 24kHz bytes (ffmpeg's `-f s16le` output) so the transcriber
|
|
@@ -75,7 +75,9 @@ export function normalizeWhisperUpload(
|
|
|
75
75
|
// Guard the runtime type so `.toLowerCase()` can't throw on hostile input —
|
|
76
76
|
// anything non-string degrades to the default rather than crashing the path.
|
|
77
77
|
const rawMime = typeof mimeType === 'string' ? mimeType : '';
|
|
78
|
-
const
|
|
78
|
+
const firstSegment = (rawMime || 'audio/wav').toLowerCase().split(';')[0];
|
|
79
|
+
assertDefined(firstSegment, 'String.split always yields at least one element');
|
|
80
|
+
const mt = firstSegment.trim();
|
|
79
81
|
if (mt === MOXXY_PCM16_24KHZ_MIME) {
|
|
80
82
|
return {
|
|
81
83
|
bytes: pcm16MonoToWav(bytes, 24_000),
|
package/src/whisper.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { APIError, APIConnectionError, APIUserAbortError } from 'openai';
|
|
|
4
4
|
import { WhisperTranscriber } from './whisper.js';
|
|
5
5
|
import { buildWhisperPlugin } from './index.js';
|
|
6
6
|
import { MOXXY_PCM16_24KHZ_MIME, normalizeWhisperUpload, pcm16MonoToWav } from './audio.js';
|
|
7
|
+
import { assertDefined } from '@moxxy/sdk';
|
|
7
8
|
|
|
8
9
|
const fakeOpenAI = (impl: (req: unknown) => unknown): OpenAI =>
|
|
9
10
|
({
|
|
@@ -52,7 +53,9 @@ describe('WhisperTranscriber', () => {
|
|
|
52
53
|
const client = { audio: { transcriptions: { create } } } as unknown as OpenAI;
|
|
53
54
|
const t = new WhisperTranscriber({ client, language: 'pl' });
|
|
54
55
|
await t.transcribe(new Uint8Array(), { mimeType: 'audio/ogg', prompt: 'jargon-list' });
|
|
55
|
-
const
|
|
56
|
+
const call = create.mock.calls[0];
|
|
57
|
+
assertDefined(call, 'transcribe invokes the OpenAI create() exactly once');
|
|
58
|
+
const req = call[0] as { language: string; prompt: string; response_format: string };
|
|
56
59
|
expect(req.language).toBe('pl');
|
|
57
60
|
expect(req.prompt).toBe('jargon-list');
|
|
58
61
|
expect(req.response_format).toBe('verbose_json');
|
|
@@ -63,7 +66,9 @@ describe('WhisperTranscriber', () => {
|
|
|
63
66
|
const client = { audio: { transcriptions: { create } } } as unknown as OpenAI;
|
|
64
67
|
const t = new WhisperTranscriber({ client, language: 'pl' });
|
|
65
68
|
await t.transcribe(new Uint8Array(), { language: 'en' });
|
|
66
|
-
const
|
|
69
|
+
const call = create.mock.calls[0];
|
|
70
|
+
assertDefined(call, 'transcribe invokes the OpenAI create() exactly once');
|
|
71
|
+
const req = call[0] as { language: string };
|
|
67
72
|
expect(req.language).toBe('en');
|
|
68
73
|
});
|
|
69
74
|
|
|
@@ -73,14 +78,19 @@ describe('WhisperTranscriber', () => {
|
|
|
73
78
|
const t = new WhisperTranscriber({ client, model: 'gpt-4o-transcribe' });
|
|
74
79
|
const out = await t.transcribe(new Uint8Array(), { mimeType: 'audio/wav' });
|
|
75
80
|
expect(out.text).toBe('plain');
|
|
76
|
-
const
|
|
81
|
+
const call = create.mock.calls[0];
|
|
82
|
+
assertDefined(call, 'transcribe invokes the OpenAI create() exactly once');
|
|
83
|
+
const req = call[0] as { response_format?: string };
|
|
77
84
|
expect(req.response_format).toBeUndefined();
|
|
78
85
|
});
|
|
79
86
|
|
|
80
87
|
it('plugin registers a transcriber whose createClient yields the right name', () => {
|
|
81
88
|
const plugin = buildWhisperPlugin({});
|
|
82
89
|
expect(plugin.transcribers).toHaveLength(1);
|
|
83
|
-
const
|
|
90
|
+
const transcribers = plugin.transcribers;
|
|
91
|
+
assertDefined(transcribers, 'buildWhisperPlugin always registers transcribers');
|
|
92
|
+
const def = transcribers[0];
|
|
93
|
+
assertDefined(def, 'the plugin registers at least one transcriber');
|
|
84
94
|
expect(def.name).toBe('openai-whisper-1');
|
|
85
95
|
expect(def.displayName).toBe('OpenAI whisper-1');
|
|
86
96
|
const inst = def.createClient({ apiKey: 'sk-test' });
|
|
@@ -95,7 +105,9 @@ describe('WhisperTranscriber', () => {
|
|
|
95
105
|
const raw = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0]);
|
|
96
106
|
await t.transcribe(raw, { mimeType: MOXXY_PCM16_24KHZ_MIME });
|
|
97
107
|
|
|
98
|
-
const
|
|
108
|
+
const call = create.mock.calls[0];
|
|
109
|
+
assertDefined(call, 'transcribe invokes the OpenAI create() exactly once');
|
|
110
|
+
const req = call[0] as { file: File };
|
|
99
111
|
expect(req.file.type).toBe('audio/wav');
|
|
100
112
|
const header = new Uint8Array(await req.file.arrayBuffer());
|
|
101
113
|
const ascii = (s: number, e: number): string =>
|
|
@@ -108,7 +120,11 @@ describe('WhisperTranscriber', () => {
|
|
|
108
120
|
|
|
109
121
|
it('plugin honors a non-default model name', () => {
|
|
110
122
|
const plugin = buildWhisperPlugin({ model: 'gpt-4o-mini-transcribe' });
|
|
111
|
-
|
|
123
|
+
const transcribers = plugin.transcribers;
|
|
124
|
+
assertDefined(transcribers, 'buildWhisperPlugin always registers transcribers');
|
|
125
|
+
const def = transcribers[0];
|
|
126
|
+
assertDefined(def, 'the plugin registers at least one transcriber');
|
|
127
|
+
expect(def.name).toBe('openai-gpt-4o-mini-transcribe');
|
|
112
128
|
});
|
|
113
129
|
|
|
114
130
|
it('throws PROVIDER_UNKNOWN_RESPONSE when verbose_json lacks a string text', async () => {
|
|
@@ -180,7 +196,9 @@ describe('WhisperTranscriber', () => {
|
|
|
180
196
|
mimeType: 123 as unknown as string,
|
|
181
197
|
});
|
|
182
198
|
expect(result.text).toBe('ok');
|
|
183
|
-
const
|
|
199
|
+
const call = create.mock.calls[0];
|
|
200
|
+
assertDefined(call, 'transcribe invokes the OpenAI create() exactly once');
|
|
201
|
+
const req = call[0] as { file: File };
|
|
184
202
|
// Defaults to the audio/wav fallback filename rather than crashing.
|
|
185
203
|
expect(req.file.name).toBe('audio.wav');
|
|
186
204
|
});
|