@moxxy/plugin-stt-local 0.44.0 → 0.46.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/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/local-stt.d.ts +10 -11
- package/dist/local-stt.d.ts.map +1 -1
- package/dist/local-stt.js +25 -38
- package/dist/local-stt.js.map +1 -1
- package/dist/models.d.ts +23 -28
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +52 -31
- package/dist/models.js.map +1 -1
- package/package.json +10 -6
- package/src/host-protocol.test.ts +1 -1
- package/src/index.ts +5 -1
- package/src/live.test.ts +4 -4
- package/src/local-stt.test.ts +26 -25
- package/src/local-stt.ts +41 -47
- package/src/models.test.ts +54 -33
- package/src/models.ts +63 -68
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Plugin } from '@moxxy/sdk';
|
|
2
2
|
import { type LocalWhisperOptions } from './local-stt.js';
|
|
3
3
|
export { LocalWhisperTranscriber, createLocalWhisperTranscriber, shutdownLocalStt, LOCAL_WHISPER_TRANSCRIBER_NAME, type LocalWhisperOptions, type WhisperModelPaths, } from './local-stt.js';
|
|
4
|
-
export { MODEL_CATALOG, DEFAULT_MODEL_ID, modelIds, findModel, requireModel, type WhisperModelEntry, type WhisperModelId, } from './models.js';
|
|
4
|
+
export { MODEL_CATALOG, DEFAULT_MODEL_ID, modelIds, findModel, requireModel, modelFiles, modelBytes, modelDir, defaultModelsDir, type WhisperModelEntry, type WhisperModelId, } from './models.js';
|
|
5
5
|
export { pcm16ToFloat32, downmixToMono, resampleLinear, parseWav, isRiffWave, TARGET_SAMPLE_RATE, type ParsedWav, } from './audio.js';
|
|
6
6
|
export { decodeToMono16k, type DecodeOptions } from './decode.js';
|
|
7
7
|
export { decodeViaFfmpeg, missingFfmpegError, __resetFfmpegProbeForTest, } from './ffmpeg.js';
|
|
@@ -9,7 +9,7 @@ export { HostClient, type HostClientLike, type ForkLike, type ChildHandle } from
|
|
|
9
9
|
export { sherpaPlatformPackage, resolveSherpaLibDir, sherpaEnv, libraryPathVar, } from './platform.js';
|
|
10
10
|
export interface BuildLocalSttPluginOptions {
|
|
11
11
|
/** Build-time defaults / test seams for the transcriber (injected
|
|
12
|
-
* `
|
|
12
|
+
* `ensureModelFilesImpl`, `hostFactory`, `fetchImpl`, `modelsDir`, `log`, …). */
|
|
13
13
|
readonly defaults?: LocalWhisperOptions;
|
|
14
14
|
}
|
|
15
15
|
/**
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,KAAK,MAAM,EAAE,MAAM,YAAY,CAAC;AAE1E,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,uBAAuB,EACvB,6BAA6B,EAC7B,gBAAgB,EAChB,8BAA8B,EAC9B,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,GACvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,KAAK,iBAAiB,EACtB,KAAK,cAAc,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,cAAc,EACd,aAAa,EACb,cAAc,EACd,QAAQ,EACR,UAAU,EACV,kBAAkB,EAClB,KAAK,SAAS,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,yBAAyB,GAC1B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,UAAU,EAAE,KAAK,cAAc,EAAE,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpG,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,SAAS,EACT,cAAc,GACf,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,0BAA0B;IACzC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,KAAK,MAAM,EAAE,MAAM,YAAY,CAAC;AAE1E,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,uBAAuB,EACvB,6BAA6B,EAC7B,gBAAgB,EAChB,8BAA8B,EAC9B,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,GACvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,UAAU,EACV,UAAU,EACV,QAAQ,EACR,gBAAgB,EAChB,KAAK,iBAAiB,EACtB,KAAK,cAAc,GACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,cAAc,EACd,aAAa,EACb,cAAc,EACd,QAAQ,EACR,UAAU,EACV,kBAAkB,EAClB,KAAK,SAAS,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,yBAAyB,GAC1B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,UAAU,EAAE,KAAK,cAAc,EAAE,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpG,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,SAAS,EACT,cAAc,GACf,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,0BAA0B;IACzC;sFACkF;IAClF,QAAQ,CAAC,QAAQ,CAAC,EAAE,mBAAmB,CAAC;CACzC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,0BAA+B,GAAG,MAAM,CAgBjF;;AAsBD,wBAAqC"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { definePlugin, defineTranscriber } from '@moxxy/sdk';
|
|
2
2
|
import { createLocalWhisperTranscriber, LOCAL_WHISPER_TRANSCRIBER_NAME, shutdownLocalStt, } from './local-stt.js';
|
|
3
3
|
export { LocalWhisperTranscriber, createLocalWhisperTranscriber, shutdownLocalStt, LOCAL_WHISPER_TRANSCRIBER_NAME, } from './local-stt.js';
|
|
4
|
-
export { MODEL_CATALOG, DEFAULT_MODEL_ID, modelIds, findModel, requireModel, } from './models.js';
|
|
4
|
+
export { MODEL_CATALOG, DEFAULT_MODEL_ID, modelIds, findModel, requireModel, modelFiles, modelBytes, modelDir, defaultModelsDir, } from './models.js';
|
|
5
5
|
export { pcm16ToFloat32, downmixToMono, resampleLinear, parseWav, isRiffWave, TARGET_SAMPLE_RATE, } from './audio.js';
|
|
6
6
|
export { decodeToMono16k } from './decode.js';
|
|
7
7
|
export { decodeViaFfmpeg, missingFfmpegError, __resetFfmpegProbeForTest, } from './ffmpeg.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAe,MAAM,YAAY,CAAC;AAE1E,OAAO,EACL,6BAA6B,EAC7B,8BAA8B,EAC9B,gBAAgB,GAEjB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,uBAAuB,EACvB,6BAA6B,EAC7B,gBAAgB,EAChB,8BAA8B,GAG/B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,QAAQ,EACR,SAAS,EACT,YAAY,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAe,MAAM,YAAY,CAAC;AAE1E,OAAO,EACL,6BAA6B,EAC7B,8BAA8B,EAC9B,gBAAgB,GAEjB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,uBAAuB,EACvB,6BAA6B,EAC7B,gBAAgB,EAChB,8BAA8B,GAG/B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,UAAU,EACV,UAAU,EACV,QAAQ,EACR,gBAAgB,GAGjB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,cAAc,EACd,aAAa,EACb,cAAc,EACd,QAAQ,EACR,UAAU,EACV,kBAAkB,GAEnB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,eAAe,EAAsB,MAAM,aAAa,CAAC;AAClE,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,yBAAyB,GAC1B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,UAAU,EAAwD,MAAM,kBAAkB,CAAC;AACpG,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EACnB,SAAS,EACT,cAAc,GACf,MAAM,eAAe,CAAC;AAQvB;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAmC,EAAE;IACvE,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC;IACrC,OAAO,YAAY,CAAC;QAClB,IAAI,EAAE,yBAAyB;QAC/B,OAAO,EAAE,OAAO;QAChB,YAAY,EAAE;YACZ,iBAAiB,CAAC;gBAChB,IAAI,EAAE,8BAA8B;gBACpC,WAAW,EAAE,uCAAuC;gBACpD,YAAY,EAAE,CAAC,MAA+B,EAAE,EAAE,CAChD,6BAA6B,CAAC,EAAE,GAAG,QAAQ,EAAE,GAAG,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC;aAC7E,CAAC;SACH;QACD,iEAAiE;QACjE,KAAK,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,gBAAgB,EAAE,EAAE;KAChD,CAAC,CAAC;AACL,CAAC;AAED;;;uEAGuE;AACvE,SAAS,eAAe,CAAC,MAA+B;IACtD,MAAM,GAAG,GAA4E,EAAE,CAAC;IACxF,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,KAAK;QAAE,GAAG,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAC/E,IAAI,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ;QAAE,GAAG,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IACxE,IAAI,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ;QAAE,GAAG,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IAC3F,IACE,OAAO,MAAM,CAAC,UAAU,KAAK,QAAQ;QACrC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC;QACnC,MAAM,CAAC,UAAU,GAAG,CAAC,EACrB,CAAC;QACD,GAAG,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;IACrC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,0EAA0E;AAC1E,eAAe,mBAAmB,EAAE,CAAC"}
|
package/dist/local-stt.d.ts
CHANGED
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
* The `local-whisper` Transcriber: fully local, on-device speech-to-text.
|
|
3
3
|
*
|
|
4
4
|
* On the first transcription for a model it downloads + verifies that model's
|
|
5
|
-
* Whisper
|
|
5
|
+
* Whisper files (once, via @moxxy/model-fetch), then decodes the inbound audio
|
|
6
6
|
* to Float32 mono @ 16 kHz IN-PROCESS (raw PCM / WAV) or via ffmpeg (compressed
|
|
7
7
|
* containers) and hands the samples to the sherpa sidecar ({@link HostClient}).
|
|
8
8
|
* No key, no network at transcription time.
|
|
9
9
|
*
|
|
10
|
-
* Every collaborator is injectable (`
|
|
10
|
+
* Every collaborator is injectable (`ensureModelFilesImpl`, `hostFactory`, `log`,
|
|
11
11
|
* `modelsDir`, `fetchImpl`, `spawnImpl`) so the whole flow is unit-testable
|
|
12
12
|
* without the native addon, a real download, or ffmpeg.
|
|
13
13
|
*/
|
|
14
14
|
import type { spawn } from 'node:child_process';
|
|
15
15
|
import { type TranscribeOptions, type Transcriber, type TranscriptionResult } from '@moxxy/sdk';
|
|
16
|
-
import {
|
|
16
|
+
import { ensureModelFiles, type FetchLike } from '@moxxy/model-fetch';
|
|
17
17
|
import { type HostClientLike } from './host-client.js';
|
|
18
18
|
/** The single registered transcriber name. */
|
|
19
19
|
export declare const LOCAL_WHISPER_TRANSCRIBER_NAME = "local-whisper";
|
|
@@ -24,7 +24,7 @@ export interface WhisperModelPaths {
|
|
|
24
24
|
readonly tokens: string;
|
|
25
25
|
}
|
|
26
26
|
export interface LocalWhisperOptions {
|
|
27
|
-
/** Model id
|
|
27
|
+
/** Model id. The only one is `turbo`, also the default. */
|
|
28
28
|
readonly model?: string;
|
|
29
29
|
/** Default Whisper language tag (BCP-47-ish, e.g. `en`, `pl`). Per-call
|
|
30
30
|
* `TranscribeOptions.language` overrides it; omit to let Whisper auto-detect. */
|
|
@@ -38,9 +38,9 @@ export interface LocalWhisperOptions {
|
|
|
38
38
|
/** Absolute path to the built sidecar entry. Default: `sidecar.js` beside
|
|
39
39
|
* this module (i.e. `dist/sidecar.js` in a published build). */
|
|
40
40
|
readonly sidecarPath?: string;
|
|
41
|
-
/** Injected model
|
|
42
|
-
readonly
|
|
43
|
-
/** Injected `fetch`, threaded into
|
|
41
|
+
/** Injected model download (tests). Defaults to @moxxy/model-fetch `ensureModelFiles`. */
|
|
42
|
+
readonly ensureModelFilesImpl?: typeof ensureModelFiles;
|
|
43
|
+
/** Injected `fetch`, threaded into the model download (tests). */
|
|
44
44
|
readonly fetchImpl?: FetchLike;
|
|
45
45
|
/** Injected sidecar host factory (tests). Defaults to a real {@link HostClient}. */
|
|
46
46
|
readonly hostFactory?: () => HostClientLike;
|
|
@@ -59,7 +59,7 @@ export declare class LocalWhisperTranscriber implements Transcriber {
|
|
|
59
59
|
private readonly modelsDir;
|
|
60
60
|
private readonly provider;
|
|
61
61
|
private readonly sidecarPath;
|
|
62
|
-
private readonly
|
|
62
|
+
private readonly ensureModelFilesImpl;
|
|
63
63
|
private readonly fetchImpl;
|
|
64
64
|
private readonly hostFactory;
|
|
65
65
|
private readonly spawnImpl;
|
|
@@ -72,11 +72,10 @@ export declare class LocalWhisperTranscriber implements Transcriber {
|
|
|
72
72
|
/** Kill this transcriber's sidecar (if any) and drop registration. */
|
|
73
73
|
shutdown(): void;
|
|
74
74
|
private getHost;
|
|
75
|
-
/** Resolve on-disk paths for the model, downloading
|
|
75
|
+
/** Resolve on-disk paths for the model, downloading its files once. */
|
|
76
76
|
private ensureModelFiles;
|
|
77
77
|
/** A progress callback that logs a single start line then ~every-10% ticks,
|
|
78
|
-
*
|
|
79
|
-
* present (ensureModel emits only `done`). */
|
|
78
|
+
* but stays silent when the model is already present (nothing to fetch). */
|
|
80
79
|
private makeProgressLogger;
|
|
81
80
|
/** Build a real sidecar-backed host, resolving the platform loader path.
|
|
82
81
|
* Fails clearly when no sherpa binary exists for this platform/arch. */
|
package/dist/local-stt.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-stt.d.ts","sourceRoot":"","sources":["../src/local-stt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAEhD,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACzB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"local-stt.d.ts","sourceRoot":"","sources":["../src/local-stt.ts"],"names":[],"mappings":"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"}
|
package/dist/local-stt.js
CHANGED
|
@@ -2,24 +2,23 @@
|
|
|
2
2
|
* The `local-whisper` Transcriber: fully local, on-device speech-to-text.
|
|
3
3
|
*
|
|
4
4
|
* On the first transcription for a model it downloads + verifies that model's
|
|
5
|
-
* Whisper
|
|
5
|
+
* Whisper files (once, via @moxxy/model-fetch), then decodes the inbound audio
|
|
6
6
|
* to Float32 mono @ 16 kHz IN-PROCESS (raw PCM / WAV) or via ffmpeg (compressed
|
|
7
7
|
* containers) and hands the samples to the sherpa sidecar ({@link HostClient}).
|
|
8
8
|
* No key, no network at transcription time.
|
|
9
9
|
*
|
|
10
|
-
* Every collaborator is injectable (`
|
|
10
|
+
* Every collaborator is injectable (`ensureModelFilesImpl`, `hostFactory`, `log`,
|
|
11
11
|
* `modelsDir`, `fetchImpl`, `spawnImpl`) so the whole flow is unit-testable
|
|
12
12
|
* without the native addon, a real download, or ffmpeg.
|
|
13
13
|
*/
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { MoxxyError, } from '@moxxy/sdk';
|
|
17
|
-
import {
|
|
18
|
-
import { ensureModel, } from '@moxxy/model-fetch';
|
|
17
|
+
import { ensureModelFiles, } from '@moxxy/model-fetch';
|
|
19
18
|
import { TARGET_SAMPLE_RATE } from './audio.js';
|
|
20
19
|
import { decodeToMono16k } from './decode.js';
|
|
21
20
|
import { HostClient } from './host-client.js';
|
|
22
|
-
import { DEFAULT_MODEL_ID, requireModel } from './models.js';
|
|
21
|
+
import { DEFAULT_MODEL_ID, defaultModelsDir, modelBytes, modelDir, modelFiles, requireModel, } from './models.js';
|
|
23
22
|
import { resolveSherpaLibDir, sherpaEnv, sherpaPlatformPackage } from './platform.js';
|
|
24
23
|
/** The single registered transcriber name. */
|
|
25
24
|
export const LOCAL_WHISPER_TRANSCRIBER_NAME = 'local-whisper';
|
|
@@ -41,7 +40,7 @@ export class LocalWhisperTranscriber {
|
|
|
41
40
|
modelsDir;
|
|
42
41
|
provider;
|
|
43
42
|
sidecarPath;
|
|
44
|
-
|
|
43
|
+
ensureModelFilesImpl;
|
|
45
44
|
fetchImpl;
|
|
46
45
|
hostFactory;
|
|
47
46
|
spawnImpl;
|
|
@@ -58,10 +57,10 @@ export class LocalWhisperTranscriber {
|
|
|
58
57
|
opts.numThreads && Number.isInteger(opts.numThreads) && opts.numThreads > 0
|
|
59
58
|
? opts.numThreads
|
|
60
59
|
: DEFAULT_NUM_THREADS;
|
|
61
|
-
this.modelsDir = opts.modelsDir ??
|
|
60
|
+
this.modelsDir = opts.modelsDir ?? defaultModelsDir();
|
|
62
61
|
this.provider = opts.provider ?? 'cpu';
|
|
63
62
|
this.sidecarPath = opts.sidecarPath;
|
|
64
|
-
this.
|
|
63
|
+
this.ensureModelFilesImpl = opts.ensureModelFilesImpl ?? ensureModelFiles;
|
|
65
64
|
this.fetchImpl = opts.fetchImpl;
|
|
66
65
|
this.hostFactory = opts.hostFactory ?? (() => this.defaultHost());
|
|
67
66
|
this.spawnImpl = opts.spawnImpl;
|
|
@@ -114,14 +113,13 @@ export class LocalWhisperTranscriber {
|
|
|
114
113
|
this.host ??= this.hostFactory();
|
|
115
114
|
return this.host;
|
|
116
115
|
}
|
|
117
|
-
/** Resolve on-disk paths for the model, downloading
|
|
116
|
+
/** Resolve on-disk paths for the model, downloading its files once. */
|
|
118
117
|
async ensureModelFiles(signal) {
|
|
119
|
-
const dir =
|
|
118
|
+
const dir = modelDir(this.model, this.modelsDir);
|
|
120
119
|
let inflight = this.ensuring;
|
|
121
120
|
if (!inflight) {
|
|
122
|
-
inflight = this.
|
|
123
|
-
|
|
124
|
-
sha256: this.model.sha256,
|
|
121
|
+
inflight = this.ensureModelFilesImpl({
|
|
122
|
+
files: modelFiles(this.model),
|
|
125
123
|
dir,
|
|
126
124
|
...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),
|
|
127
125
|
...(signal ? { signal } : {}),
|
|
@@ -135,40 +133,29 @@ export class LocalWhisperTranscriber {
|
|
|
135
133
|
});
|
|
136
134
|
}
|
|
137
135
|
await inflight;
|
|
138
|
-
const root = path.join(dir, this.model.archiveRootDir);
|
|
139
136
|
return {
|
|
140
|
-
encoder: path.join(
|
|
141
|
-
decoder: path.join(
|
|
142
|
-
tokens: path.join(
|
|
137
|
+
encoder: path.join(dir, this.model.encoder.name),
|
|
138
|
+
decoder: path.join(dir, this.model.decoder.name),
|
|
139
|
+
tokens: path.join(dir, this.model.tokens.name),
|
|
143
140
|
};
|
|
144
141
|
}
|
|
145
142
|
/** A progress callback that logs a single start line then ~every-10% ticks,
|
|
146
|
-
*
|
|
147
|
-
* present (ensureModel emits only `done`). */
|
|
143
|
+
* but stays silent when the model is already present (nothing to fetch). */
|
|
148
144
|
makeProgressLogger() {
|
|
149
145
|
let started = false;
|
|
150
|
-
let extracting = false;
|
|
151
146
|
let lastPct = -1;
|
|
152
147
|
return (p) => {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (pct >= lastPct + 10) {
|
|
161
|
-
lastPct = pct - (pct % 10);
|
|
162
|
-
this.log(`stt-local: downloading ${this.model.id} … ${lastPct}%`);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
148
|
+
const pct = p.totalBytes > 0 ? Math.floor((p.receivedBytes / p.totalBytes) * 100) : 0;
|
|
149
|
+
if (!started) {
|
|
150
|
+
if (pct >= 100)
|
|
151
|
+
return;
|
|
152
|
+
started = true;
|
|
153
|
+
const mb = Math.round(modelBytes(this.model) / 1_000_000);
|
|
154
|
+
this.log(`stt-local: first use — downloading Whisper ${this.model.id} (~${mb} MB) to ${this.modelsDir}, one-time`);
|
|
165
155
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
this.log(`stt-local:
|
|
169
|
-
}
|
|
170
|
-
else if (p.phase === 'done' && started) {
|
|
171
|
-
this.log(`stt-local: ${this.model.id} ready.`);
|
|
156
|
+
if (pct >= lastPct + 10) {
|
|
157
|
+
lastPct = pct - (pct % 10);
|
|
158
|
+
this.log(lastPct >= 100 ? `stt-local: ${this.model.id} ready.` : `stt-local: downloading ${this.model.id} … ${lastPct}%`);
|
|
172
159
|
}
|
|
173
160
|
};
|
|
174
161
|
}
|
package/dist/local-stt.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-stt.js","sourceRoot":"","sources":["../src/local-stt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,OAAO,EACL,UAAU,GAIX,MAAM,YAAY,CAAC;AACpB,OAAO,
|
|
1
|
+
{"version":3,"file":"local-stt.js","sourceRoot":"","sources":["../src/local-stt.ts"],"names":[],"mappings":"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"}
|
package/dist/models.d.ts
CHANGED
|
@@ -1,42 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The pinned Whisper model catalog.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* variants we don't use) and `<id>-tokens.txt`.
|
|
4
|
+
* The one model is Whisper large-v3 turbo, the multilingual sherpa-onnx int8
|
|
5
|
+
* export. Its three files download as they are from Hugging Face, pinned to a
|
|
6
|
+
* fixed revision and checked against their size and sha256 — byte for byte the
|
|
7
|
+
* files the project's `asr-models` GitHub release packs into a `.tar.bz2`
|
|
8
|
+
* (verified 2026-10-24), but with no archive to unpack: unpacking it in
|
|
9
|
+
* JavaScript took about 67 s against 9 s for the download.
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* sweet spot for Polish, `base` the balanced default, `tiny` the fastest.
|
|
11
|
+
* Multilingual (not an `.en` export), so Polish and every other Whisper
|
|
12
|
+
* language work.
|
|
15
13
|
*/
|
|
16
|
-
|
|
14
|
+
import type { ModelFile } from '@moxxy/model-fetch/files';
|
|
15
|
+
export type WhisperModelId = 'turbo';
|
|
17
16
|
export interface WhisperModelEntry {
|
|
18
|
-
/** Stable model id (also the `model` config value). */
|
|
17
|
+
/** Stable model id (also the `model` config value and its directory name). */
|
|
19
18
|
readonly id: WhisperModelId;
|
|
20
19
|
/** Human label for UI surfaces / the first-use notice. */
|
|
21
20
|
readonly label: string;
|
|
22
|
-
|
|
23
|
-
readonly
|
|
24
|
-
|
|
25
|
-
readonly sha256: string;
|
|
26
|
-
/** Top directory the archive extracts to (`sherpa-onnx-whisper-<id>`). */
|
|
27
|
-
readonly archiveRootDir: string;
|
|
28
|
-
/** Encoder ONNX filename inside `archiveRootDir` (`<id>-encoder.onnx`). */
|
|
29
|
-
readonly encoderFile: string;
|
|
30
|
-
/** Decoder ONNX filename inside `archiveRootDir` (`<id>-decoder.onnx`). */
|
|
31
|
-
readonly decoderFile: string;
|
|
32
|
-
/** Tokens filename inside `archiveRootDir` (`<id>-tokens.txt`). */
|
|
33
|
-
readonly tokensFile: string;
|
|
34
|
-
/** Approximate download size in MB (for the one-time first-use notice). */
|
|
35
|
-
readonly approxMb: number;
|
|
21
|
+
readonly encoder: ModelFile;
|
|
22
|
+
readonly decoder: ModelFile;
|
|
23
|
+
readonly tokens: ModelFile;
|
|
36
24
|
}
|
|
37
25
|
export declare const MODEL_CATALOG: readonly WhisperModelEntry[];
|
|
38
|
-
/**
|
|
39
|
-
|
|
26
|
+
/** Every file of a model, in download order. */
|
|
27
|
+
export declare function modelFiles(entry: WhisperModelEntry): ModelFile[];
|
|
28
|
+
/** The model's download size in bytes. */
|
|
29
|
+
export declare function modelBytes(entry: WhisperModelEntry): number;
|
|
30
|
+
/** The models root: `~/.moxxy/models/stt`, MOXXY_HOME-aware. */
|
|
31
|
+
export declare function defaultModelsDir(): string;
|
|
32
|
+
/** Where a model's files live under the models root. */
|
|
33
|
+
export declare function modelDir(entry: WhisperModelEntry, modelsDir: string): string;
|
|
34
|
+
/** The model used when the caller doesn't pick one — the only one there is. */
|
|
40
35
|
export declare const DEFAULT_MODEL_ID: WhisperModelId;
|
|
41
36
|
/** All valid model ids, for error messages and validation. */
|
|
42
37
|
export declare function modelIds(): string[];
|
package/dist/models.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAE1D,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC;AAErC,MAAM,WAAW,iBAAiB;IAChC,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,EAAE,cAAc,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;CAC5B;AAKD,eAAO,MAAM,aAAa,EAAE,SAAS,iBAAiB,EAuBrD,CAAC;AAEF,gDAAgD;AAChD,wBAAgB,UAAU,CAAC,KAAK,EAAE,iBAAiB,GAAG,SAAS,EAAE,CAEhE;AAED,0CAA0C;AAC1C,wBAAgB,UAAU,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAE3D;AAED,gEAAgE;AAChE,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,wDAAwD;AACxD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE5E;AAED,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,EAAE,cAAwB,CAAC;AAExD,8DAA8D;AAC9D,wBAAgB,QAAQ,IAAI,MAAM,EAAE,CAEnC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAEnE;AAED;uEACuE;AACvE,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,iBAAiB,CAWzE"}
|
package/dist/models.js
CHANGED
|
@@ -1,42 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The pinned Whisper model catalog.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* variants we don't use) and `<id>-tokens.txt`.
|
|
4
|
+
* The one model is Whisper large-v3 turbo, the multilingual sherpa-onnx int8
|
|
5
|
+
* export. Its three files download as they are from Hugging Face, pinned to a
|
|
6
|
+
* fixed revision and checked against their size and sha256 — byte for byte the
|
|
7
|
+
* files the project's `asr-models` GitHub release packs into a `.tar.bz2`
|
|
8
|
+
* (verified 2026-10-24), but with no archive to unpack: unpacking it in
|
|
9
|
+
* JavaScript took about 67 s against 9 s for the download.
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* sweet spot for Polish, `base` the balanced default, `tiny` the fastest.
|
|
11
|
+
* Multilingual (not an `.en` export), so Polish and every other Whisper
|
|
12
|
+
* language work.
|
|
15
13
|
*/
|
|
14
|
+
import path from 'node:path';
|
|
16
15
|
import { MoxxyError } from '@moxxy/sdk';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return {
|
|
21
|
-
id,
|
|
22
|
-
label,
|
|
23
|
-
url: `${RELEASE_BASE}/${archiveRootDir}.tar.bz2`,
|
|
24
|
-
sha256,
|
|
25
|
-
archiveRootDir,
|
|
26
|
-
encoderFile: `${id}-encoder.onnx`,
|
|
27
|
-
decoderFile: `${id}-decoder.onnx`,
|
|
28
|
-
tokensFile: `${id}-tokens.txt`,
|
|
29
|
-
approxMb,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
16
|
+
import { moxxyPath } from '@moxxy/sdk/server';
|
|
17
|
+
const TURBO_REVISION = '2ca6ff69fc878651b770880507669577ac41c2ff';
|
|
18
|
+
const TURBO_BASE = `https://huggingface.co/csukuangfj/sherpa-onnx-whisper-turbo/resolve/${TURBO_REVISION}`;
|
|
32
19
|
export const MODEL_CATALOG = [
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
20
|
+
{
|
|
21
|
+
id: 'turbo',
|
|
22
|
+
label: 'Whisper turbo (multilingual, large-v3 turbo)',
|
|
23
|
+
encoder: {
|
|
24
|
+
name: 'turbo-encoder.int8.onnx',
|
|
25
|
+
url: `${TURBO_BASE}/turbo-encoder.int8.onnx`,
|
|
26
|
+
bytes: 674_716_297,
|
|
27
|
+
sha256: 'b02dcdf54f348741e93fe732b67d933c8dcb6735655f710640143081db38878b',
|
|
28
|
+
},
|
|
29
|
+
decoder: {
|
|
30
|
+
name: 'turbo-decoder.int8.onnx',
|
|
31
|
+
url: `${TURBO_BASE}/turbo-decoder.int8.onnx`,
|
|
32
|
+
bytes: 361_080_764,
|
|
33
|
+
sha256: '20accd02388482eb3a46bd615631adfdc85e1eb2c7db9ea3f02a40ffe6b81547',
|
|
34
|
+
},
|
|
35
|
+
tokens: {
|
|
36
|
+
name: 'turbo-tokens.txt',
|
|
37
|
+
url: `${TURBO_BASE}/turbo-tokens.txt`,
|
|
38
|
+
bytes: 816_730,
|
|
39
|
+
sha256: 'b34b360dbb493e781e479794586d661700670d65564001f23024971d1f2fa126',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
36
42
|
];
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
/** Every file of a model, in download order. */
|
|
44
|
+
export function modelFiles(entry) {
|
|
45
|
+
return [entry.encoder, entry.decoder, entry.tokens];
|
|
46
|
+
}
|
|
47
|
+
/** The model's download size in bytes. */
|
|
48
|
+
export function modelBytes(entry) {
|
|
49
|
+
return modelFiles(entry).reduce((sum, file) => sum + file.bytes, 0);
|
|
50
|
+
}
|
|
51
|
+
/** The models root: `~/.moxxy/models/stt`, MOXXY_HOME-aware. */
|
|
52
|
+
export function defaultModelsDir() {
|
|
53
|
+
return moxxyPath('models', 'stt');
|
|
54
|
+
}
|
|
55
|
+
/** Where a model's files live under the models root. */
|
|
56
|
+
export function modelDir(entry, modelsDir) {
|
|
57
|
+
return path.join(modelsDir, entry.id);
|
|
58
|
+
}
|
|
59
|
+
/** The model used when the caller doesn't pick one — the only one there is. */
|
|
60
|
+
export const DEFAULT_MODEL_ID = 'turbo';
|
|
40
61
|
/** All valid model ids, for error messages and validation. */
|
|
41
62
|
export function modelIds() {
|
|
42
63
|
return MODEL_CATALOG.map((m) => m.id);
|
package/dist/models.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"models.js","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"models.js","sourceRoot":"","sources":["../src/models.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAe9C,MAAM,cAAc,GAAG,0CAA0C,CAAC;AAClE,MAAM,UAAU,GAAG,uEAAuE,cAAc,EAAE,CAAC;AAE3G,MAAM,CAAC,MAAM,aAAa,GAAiC;IACzD;QACE,EAAE,EAAE,OAAO;QACX,KAAK,EAAE,8CAA8C;QACrD,OAAO,EAAE;YACP,IAAI,EAAE,yBAAyB;YAC/B,GAAG,EAAE,GAAG,UAAU,0BAA0B;YAC5C,KAAK,EAAE,WAAW;YAClB,MAAM,EAAE,kEAAkE;SAC3E;QACD,OAAO,EAAE;YACP,IAAI,EAAE,yBAAyB;YAC/B,GAAG,EAAE,GAAG,UAAU,0BAA0B;YAC5C,KAAK,EAAE,WAAW;YAClB,MAAM,EAAE,kEAAkE;SAC3E;QACD,MAAM,EAAE;YACN,IAAI,EAAE,kBAAkB;YACxB,GAAG,EAAE,GAAG,UAAU,mBAAmB;YACrC,KAAK,EAAE,OAAO;YACd,MAAM,EAAE,kEAAkE;SAC3E;KACF;CACF,CAAC;AAEF,gDAAgD;AAChD,MAAM,UAAU,UAAU,CAAC,KAAwB;IACjD,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;AACtD,CAAC;AAED,0CAA0C;AAC1C,MAAM,UAAU,UAAU,CAAC,KAAwB;IACjD,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AACtE,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,gBAAgB;IAC9B,OAAO,SAAS,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;AACpC,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,QAAQ,CAAC,KAAwB,EAAE,SAAiB;IAClE,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;AACxC,CAAC;AAED,+EAA+E;AAC/E,MAAM,CAAC,MAAM,gBAAgB,GAAmB,OAAO,CAAC;AAExD,8DAA8D;AAC9D,MAAM,UAAU,QAAQ;IACtB,OAAO,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,EAAU;IAClC,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;AAChD,CAAC;AAED;uEACuE;AACvE,MAAM,UAAU,YAAY,CAAC,EAAU,EAAE,KAAa;IACpD,MAAM,KAAK,GAAG,SAAS,CAAC,EAAE,CAAC,CAAC;IAC5B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,UAAU,CAAC;YACnB,IAAI,EAAE,gBAAgB;YACtB,OAAO,EAAE,+BAA+B,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,KAAK,IAAI;YACxE,IAAI,EAAE,iBAAiB,QAAQ,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;YAC/C,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE;SAC9B,CAAC,CAAC;IACL,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moxxy/plugin-stt-local",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Fully local, on-device speech-to-text Transcriber for moxxy. Runs multilingual Whisper (English + Polish) via sherpa-onnx in a forked sidecar process;
|
|
3
|
+
"version": "0.46.0",
|
|
4
|
+
"description": "Fully local, on-device speech-to-text Transcriber for moxxy. Runs multilingual Whisper (English + Polish) via sherpa-onnx in a forked sidecar process; the Whisper turbo model downloads once on first use, pinned and sha256-verified. No API key, no network at transcription time. Plugs into the session TranscriberRegistry — the TUI and channel voice notes consume it transparently.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"moxxy",
|
|
7
7
|
"agent",
|
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
".": {
|
|
35
35
|
"types": "./dist/index.d.ts",
|
|
36
36
|
"import": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./models": {
|
|
39
|
+
"types": "./dist/models.d.ts",
|
|
40
|
+
"import": "./dist/models.js"
|
|
37
41
|
}
|
|
38
42
|
},
|
|
39
43
|
"files": [
|
|
@@ -48,15 +52,15 @@
|
|
|
48
52
|
},
|
|
49
53
|
"dependencies": {
|
|
50
54
|
"sherpa-onnx-node": "^1.13.8",
|
|
51
|
-
"@moxxy/
|
|
52
|
-
"@moxxy/
|
|
55
|
+
"@moxxy/sdk": "0.46.0",
|
|
56
|
+
"@moxxy/model-fetch": "0.2.0"
|
|
53
57
|
},
|
|
54
58
|
"devDependencies": {
|
|
55
59
|
"@types/node": "^22.20.5",
|
|
56
60
|
"typescript": "^6.0.3",
|
|
57
61
|
"vitest": "^5.0.3",
|
|
58
|
-
"@moxxy/
|
|
59
|
-
"@moxxy/
|
|
62
|
+
"@moxxy/vitest-preset": "0.0.0",
|
|
63
|
+
"@moxxy/tsconfig": "0.0.0"
|
|
60
64
|
},
|
|
61
65
|
"scripts": {
|
|
62
66
|
"build": "tsc -p tsconfig.json",
|
|
@@ -137,7 +137,7 @@ describe('createMessageHandler', () => {
|
|
|
137
137
|
await handle(req({ id: 1 }));
|
|
138
138
|
await handle(req({ id: 2 }));
|
|
139
139
|
await handle(
|
|
140
|
-
req({ id: 3, modelKey: '/models/
|
|
140
|
+
req({ id: 3, modelKey: '/models/turbo/turbo-encoder.int8.onnx', encoder: '/models/turbo/turbo-encoder.int8.onnx' }),
|
|
141
141
|
);
|
|
142
142
|
expect(loads).toBe(1); // module loaded once
|
|
143
143
|
expect(s.constructed).toBe(2); // one recognizer per distinct modelKey
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,10 @@ export {
|
|
|
21
21
|
modelIds,
|
|
22
22
|
findModel,
|
|
23
23
|
requireModel,
|
|
24
|
+
modelFiles,
|
|
25
|
+
modelBytes,
|
|
26
|
+
modelDir,
|
|
27
|
+
defaultModelsDir,
|
|
24
28
|
type WhisperModelEntry,
|
|
25
29
|
type WhisperModelId,
|
|
26
30
|
} from './models.js';
|
|
@@ -49,7 +53,7 @@ export {
|
|
|
49
53
|
|
|
50
54
|
export interface BuildLocalSttPluginOptions {
|
|
51
55
|
/** Build-time defaults / test seams for the transcriber (injected
|
|
52
|
-
* `
|
|
56
|
+
* `ensureModelFilesImpl`, `hostFactory`, `fetchImpl`, `modelsDir`, `log`, …). */
|
|
53
57
|
readonly defaults?: LocalWhisperOptions;
|
|
54
58
|
}
|
|
55
59
|
|
package/src/live.test.ts
CHANGED
|
@@ -14,8 +14,8 @@ import { removeDir } from '@moxxy/vitest-preset/fs';
|
|
|
14
14
|
const SIDECAR_PATH = fileURLToPath(new URL('../dist/sidecar.js', import.meta.url));
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* Opt-in end-to-end test that REALLY downloads the
|
|
18
|
-
* the native sherpa sidecar — off by default (it fetches ~
|
|
17
|
+
* Opt-in end-to-end test that REALLY downloads the turbo Whisper model and runs
|
|
18
|
+
* the native sherpa sidecar — off by default (it fetches ~1 GB and needs the
|
|
19
19
|
* platform binary). Run manually with:
|
|
20
20
|
*
|
|
21
21
|
* MOXXY_STT_LOCAL_LIVE=1 pnpm -F @moxxy/plugin-stt-local test
|
|
@@ -64,9 +64,9 @@ afterAll(async () => {
|
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
describe.skipIf(!LIVE)('local STT (live, real download + native sidecar)', () => {
|
|
67
|
-
it('downloads the
|
|
67
|
+
it('downloads the turbo model and transcribes a generated WAV fixture', async () => {
|
|
68
68
|
const stt = createLocalWhisperTranscriber({
|
|
69
|
-
model: '
|
|
69
|
+
model: 'turbo',
|
|
70
70
|
modelsDir,
|
|
71
71
|
sidecarPath: SIDECAR_PATH,
|
|
72
72
|
});
|
package/src/local-stt.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as path from 'node:path';
|
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
3
|
|
|
4
4
|
import { MOXXY_PCM16_24KHZ_MIME } from '@moxxy/sdk';
|
|
5
|
-
import {
|
|
5
|
+
import type { ensureModelFiles } from '@moxxy/model-fetch';
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
createLocalWhisperTranscriber,
|
|
@@ -49,14 +49,14 @@ function fakeHost(result: TranscribeResult = { text: 'hello world' }): {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/** A fake
|
|
53
|
-
function fakeEnsure(): { impl: typeof
|
|
54
|
-
const
|
|
55
|
-
const impl = (async (opts: Parameters<typeof
|
|
56
|
-
|
|
52
|
+
/** A fake ensureModelFiles recording each model it was asked to fetch. */
|
|
53
|
+
function fakeEnsure(): { impl: typeof ensureModelFiles; calls: Parameters<typeof ensureModelFiles>[0][] } {
|
|
54
|
+
const calls: Parameters<typeof ensureModelFiles>[0][] = [];
|
|
55
|
+
const impl = (async (opts: Parameters<typeof ensureModelFiles>[0]) => {
|
|
56
|
+
calls.push(opts);
|
|
57
57
|
return { dir: opts.dir, skipped: false };
|
|
58
|
-
}) as
|
|
59
|
-
return { impl,
|
|
58
|
+
}) as typeof ensureModelFiles;
|
|
59
|
+
return { impl, calls };
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function makeTranscriber(
|
|
@@ -71,7 +71,7 @@ function makeTranscriber(
|
|
|
71
71
|
const ensure = fakeEnsure();
|
|
72
72
|
const transcriber = createLocalWhisperTranscriber({
|
|
73
73
|
modelsDir: '/models/stt',
|
|
74
|
-
|
|
74
|
+
ensureModelFilesImpl: ensure.impl,
|
|
75
75
|
hostFactory: () => host.host,
|
|
76
76
|
log: () => {},
|
|
77
77
|
...over,
|
|
@@ -80,28 +80,29 @@ function makeTranscriber(
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
describe('LocalWhisperTranscriber.transcribe', () => {
|
|
83
|
-
it('defaults to the
|
|
83
|
+
it('defaults to the turbo model, decodes raw PCM, and returns the transcript', async () => {
|
|
84
84
|
const { transcriber, host, ensure } = makeTranscriber();
|
|
85
85
|
const out = await transcriber.transcribe(AUDIO, { mimeType: MOXXY_PCM16_24KHZ_MIME });
|
|
86
86
|
expect(out.text).toBe('hello world');
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
const modelRoot = path.join('/models/stt', 'turbo');
|
|
88
|
+
expect(ensure.calls.map((call) => call.dir)).toEqual([modelRoot]);
|
|
89
|
+
expect(ensure.calls[0]!.files.map((file) => file.name)).toEqual([
|
|
90
|
+
'turbo-encoder.int8.onnx',
|
|
91
|
+
'turbo-decoder.int8.onnx',
|
|
92
|
+
'turbo-tokens.txt',
|
|
89
93
|
]);
|
|
90
94
|
const req = host.reqs[0]!;
|
|
91
|
-
expect(req.encoder).
|
|
92
|
-
expect(req.decoder).
|
|
93
|
-
expect(req.tokens).
|
|
95
|
+
expect(req.encoder).toBe(path.join(modelRoot, 'turbo-encoder.int8.onnx'));
|
|
96
|
+
expect(req.decoder).toBe(path.join(modelRoot, 'turbo-decoder.int8.onnx'));
|
|
97
|
+
expect(req.tokens).toBe(path.join(modelRoot, 'turbo-tokens.txt'));
|
|
94
98
|
expect(req.sampleRate).toBe(16_000);
|
|
95
99
|
expect(req.samples.length).toBe(160); // resampled 24k → 16k
|
|
96
100
|
expect(req.task).toBe('transcribe');
|
|
97
101
|
expect(out.durationSec).toBeCloseTo(160 / 16_000, 6);
|
|
98
102
|
});
|
|
99
103
|
|
|
100
|
-
it('
|
|
101
|
-
|
|
102
|
-
await transcriber.transcribe(AUDIO, { mimeType: MOXXY_PCM16_24KHZ_MIME });
|
|
103
|
-
expect(ensure.urls[0]).toContain('sherpa-onnx-whisper-small.tar.bz2');
|
|
104
|
-
expect(host.reqs[0]!.encoder).toContain('small-encoder.onnx');
|
|
104
|
+
it('rejects a removed model (small) left in config, before any download', () => {
|
|
105
|
+
expect(() => makeTranscriber({ model: 'small' })).toThrow(/Unknown local Whisper model "small"/);
|
|
105
106
|
});
|
|
106
107
|
|
|
107
108
|
it('passes a per-call language hint through to the recognizer', async () => {
|
|
@@ -145,14 +146,14 @@ describe('LocalWhisperTranscriber.transcribe', () => {
|
|
|
145
146
|
await transcriber.transcribe(AUDIO, { mimeType: MOXXY_PCM16_24KHZ_MIME });
|
|
146
147
|
await transcriber.transcribe(AUDIO, { mimeType: MOXXY_PCM16_24KHZ_MIME });
|
|
147
148
|
await transcriber.transcribe(AUDIO, { mimeType: MOXXY_PCM16_24KHZ_MIME });
|
|
148
|
-
expect(ensure.
|
|
149
|
+
expect(ensure.calls.length).toBe(1);
|
|
149
150
|
});
|
|
150
151
|
|
|
151
152
|
it('returns empty text for empty/silent audio without touching the model', async () => {
|
|
152
153
|
const { transcriber, host, ensure } = makeTranscriber();
|
|
153
154
|
const out = await transcriber.transcribe(new Uint8Array(0), { mimeType: MOXXY_PCM16_24KHZ_MIME });
|
|
154
155
|
expect(out.text).toBe('');
|
|
155
|
-
expect(ensure.
|
|
156
|
+
expect(ensure.calls.length).toBe(0);
|
|
156
157
|
expect(host.reqs.length).toBe(0);
|
|
157
158
|
});
|
|
158
159
|
|
|
@@ -202,14 +203,14 @@ describe('buildLocalSttPlugin', () => {
|
|
|
202
203
|
const plugin = buildLocalSttPlugin({
|
|
203
204
|
defaults: {
|
|
204
205
|
modelsDir: '/models/stt',
|
|
205
|
-
|
|
206
|
+
ensureModelFilesImpl: ensure.impl,
|
|
206
207
|
hostFactory: () => host.host,
|
|
207
208
|
log: () => {},
|
|
208
209
|
},
|
|
209
210
|
});
|
|
210
|
-
const inst = plugin.transcribers![0]!.createClient({ model: '
|
|
211
|
+
const inst = plugin.transcribers![0]!.createClient({ model: 'turbo', language: 'pl' });
|
|
211
212
|
await inst.transcribe(AUDIO, { mimeType: MOXXY_PCM16_24KHZ_MIME });
|
|
212
|
-
expect(ensure.
|
|
213
|
+
expect(ensure.calls[0]!.dir).toBe(path.join('/models/stt', 'turbo'));
|
|
213
214
|
expect(host.reqs[0]!.language).toBe('pl');
|
|
214
215
|
});
|
|
215
216
|
|
package/src/local-stt.ts
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* The `local-whisper` Transcriber: fully local, on-device speech-to-text.
|
|
3
3
|
*
|
|
4
4
|
* On the first transcription for a model it downloads + verifies that model's
|
|
5
|
-
* Whisper
|
|
5
|
+
* Whisper files (once, via @moxxy/model-fetch), then decodes the inbound audio
|
|
6
6
|
* to Float32 mono @ 16 kHz IN-PROCESS (raw PCM / WAV) or via ffmpeg (compressed
|
|
7
7
|
* containers) and hands the samples to the sherpa sidecar ({@link HostClient}).
|
|
8
8
|
* No key, no network at transcription time.
|
|
9
9
|
*
|
|
10
|
-
* Every collaborator is injectable (`
|
|
10
|
+
* Every collaborator is injectable (`ensureModelFilesImpl`, `hostFactory`, `log`,
|
|
11
11
|
* `modelsDir`, `fetchImpl`, `spawnImpl`) so the whole flow is unit-testable
|
|
12
12
|
* without the native addon, a real download, or ffmpeg.
|
|
13
13
|
*/
|
|
@@ -22,18 +22,25 @@ import {
|
|
|
22
22
|
type Transcriber,
|
|
23
23
|
type TranscriptionResult,
|
|
24
24
|
} from '@moxxy/sdk';
|
|
25
|
-
import { moxxyPath } from '@moxxy/sdk/server';
|
|
26
25
|
import {
|
|
27
|
-
|
|
28
|
-
type
|
|
29
|
-
type EnsureModelResult,
|
|
26
|
+
ensureModelFiles,
|
|
27
|
+
type EnsureModelFilesResult,
|
|
30
28
|
type FetchLike,
|
|
29
|
+
type ModelFilesProgress,
|
|
31
30
|
} from '@moxxy/model-fetch';
|
|
32
31
|
|
|
33
32
|
import { TARGET_SAMPLE_RATE } from './audio.js';
|
|
34
33
|
import { decodeToMono16k } from './decode.js';
|
|
35
34
|
import { HostClient, type HostClientLike } from './host-client.js';
|
|
36
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
DEFAULT_MODEL_ID,
|
|
37
|
+
defaultModelsDir,
|
|
38
|
+
modelBytes,
|
|
39
|
+
modelDir,
|
|
40
|
+
modelFiles,
|
|
41
|
+
requireModel,
|
|
42
|
+
type WhisperModelEntry,
|
|
43
|
+
} from './models.js';
|
|
37
44
|
import { resolveSherpaLibDir, sherpaEnv, sherpaPlatformPackage } from './platform.js';
|
|
38
45
|
|
|
39
46
|
/** The single registered transcriber name. */
|
|
@@ -49,7 +56,7 @@ export interface WhisperModelPaths {
|
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
export interface LocalWhisperOptions {
|
|
52
|
-
/** Model id
|
|
59
|
+
/** Model id. The only one is `turbo`, also the default. */
|
|
53
60
|
readonly model?: string;
|
|
54
61
|
/** Default Whisper language tag (BCP-47-ish, e.g. `en`, `pl`). Per-call
|
|
55
62
|
* `TranscribeOptions.language` overrides it; omit to let Whisper auto-detect. */
|
|
@@ -63,9 +70,9 @@ export interface LocalWhisperOptions {
|
|
|
63
70
|
/** Absolute path to the built sidecar entry. Default: `sidecar.js` beside
|
|
64
71
|
* this module (i.e. `dist/sidecar.js` in a published build). */
|
|
65
72
|
readonly sidecarPath?: string;
|
|
66
|
-
/** Injected model
|
|
67
|
-
readonly
|
|
68
|
-
/** Injected `fetch`, threaded into
|
|
73
|
+
/** Injected model download (tests). Defaults to @moxxy/model-fetch `ensureModelFiles`. */
|
|
74
|
+
readonly ensureModelFilesImpl?: typeof ensureModelFiles;
|
|
75
|
+
/** Injected `fetch`, threaded into the model download (tests). */
|
|
69
76
|
readonly fetchImpl?: FetchLike;
|
|
70
77
|
/** Injected sidecar host factory (tests). Defaults to a real {@link HostClient}. */
|
|
71
78
|
readonly hostFactory?: () => HostClientLike;
|
|
@@ -94,7 +101,7 @@ export class LocalWhisperTranscriber implements Transcriber {
|
|
|
94
101
|
private readonly modelsDir: string;
|
|
95
102
|
private readonly provider: string;
|
|
96
103
|
private readonly sidecarPath: string | undefined;
|
|
97
|
-
private readonly
|
|
104
|
+
private readonly ensureModelFilesImpl: typeof ensureModelFiles;
|
|
98
105
|
private readonly fetchImpl: FetchLike | undefined;
|
|
99
106
|
private readonly hostFactory: () => HostClientLike;
|
|
100
107
|
private readonly spawnImpl: typeof spawn | undefined;
|
|
@@ -102,7 +109,7 @@ export class LocalWhisperTranscriber implements Transcriber {
|
|
|
102
109
|
|
|
103
110
|
private host: HostClientLike | null = null;
|
|
104
111
|
/** In-flight model download promise — de-dupes concurrent first-use. */
|
|
105
|
-
private ensuring: Promise<
|
|
112
|
+
private ensuring: Promise<EnsureModelFilesResult> | null = null;
|
|
106
113
|
|
|
107
114
|
constructor(opts: LocalWhisperOptions = {}) {
|
|
108
115
|
// Validate the configured model up front so a bad config fails clearly at
|
|
@@ -113,10 +120,10 @@ export class LocalWhisperTranscriber implements Transcriber {
|
|
|
113
120
|
opts.numThreads && Number.isInteger(opts.numThreads) && opts.numThreads > 0
|
|
114
121
|
? opts.numThreads
|
|
115
122
|
: DEFAULT_NUM_THREADS;
|
|
116
|
-
this.modelsDir = opts.modelsDir ??
|
|
123
|
+
this.modelsDir = opts.modelsDir ?? defaultModelsDir();
|
|
117
124
|
this.provider = opts.provider ?? 'cpu';
|
|
118
125
|
this.sidecarPath = opts.sidecarPath;
|
|
119
|
-
this.
|
|
126
|
+
this.ensureModelFilesImpl = opts.ensureModelFilesImpl ?? ensureModelFiles;
|
|
120
127
|
this.fetchImpl = opts.fetchImpl;
|
|
121
128
|
this.hostFactory = opts.hostFactory ?? (() => this.defaultHost());
|
|
122
129
|
this.spawnImpl = opts.spawnImpl;
|
|
@@ -184,14 +191,13 @@ export class LocalWhisperTranscriber implements Transcriber {
|
|
|
184
191
|
return this.host;
|
|
185
192
|
}
|
|
186
193
|
|
|
187
|
-
/** Resolve on-disk paths for the model, downloading
|
|
194
|
+
/** Resolve on-disk paths for the model, downloading its files once. */
|
|
188
195
|
private async ensureModelFiles(signal?: AbortSignal): Promise<WhisperModelPaths> {
|
|
189
|
-
const dir =
|
|
196
|
+
const dir = modelDir(this.model, this.modelsDir);
|
|
190
197
|
let inflight = this.ensuring;
|
|
191
198
|
if (!inflight) {
|
|
192
|
-
inflight = this.
|
|
193
|
-
|
|
194
|
-
sha256: this.model.sha256,
|
|
199
|
+
inflight = this.ensureModelFilesImpl({
|
|
200
|
+
files: modelFiles(this.model),
|
|
195
201
|
dir,
|
|
196
202
|
...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),
|
|
197
203
|
...(signal ? { signal } : {}),
|
|
@@ -204,41 +210,29 @@ export class LocalWhisperTranscriber implements Transcriber {
|
|
|
204
210
|
});
|
|
205
211
|
}
|
|
206
212
|
await inflight;
|
|
207
|
-
const root = path.join(dir, this.model.archiveRootDir);
|
|
208
213
|
return {
|
|
209
|
-
encoder: path.join(
|
|
210
|
-
decoder: path.join(
|
|
211
|
-
tokens: path.join(
|
|
214
|
+
encoder: path.join(dir, this.model.encoder.name),
|
|
215
|
+
decoder: path.join(dir, this.model.decoder.name),
|
|
216
|
+
tokens: path.join(dir, this.model.tokens.name),
|
|
212
217
|
};
|
|
213
218
|
}
|
|
214
219
|
|
|
215
220
|
/** A progress callback that logs a single start line then ~every-10% ticks,
|
|
216
|
-
*
|
|
217
|
-
|
|
218
|
-
private makeProgressLogger(): (p: EnsureModelProgress) => void {
|
|
221
|
+
* but stays silent when the model is already present (nothing to fetch). */
|
|
222
|
+
private makeProgressLogger(): (p: ModelFilesProgress) => void {
|
|
219
223
|
let started = false;
|
|
220
|
-
let extracting = false;
|
|
221
224
|
let lastPct = -1;
|
|
222
225
|
return (p) => {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
lastPct = pct - (pct % 10);
|
|
234
|
-
this.log(`stt-local: downloading ${this.model.id} … ${lastPct}%`);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
} else if (p.phase === 'extracting' && !extracting) {
|
|
238
|
-
extracting = true;
|
|
239
|
-
this.log(`stt-local: extracting ${this.model.id} …`);
|
|
240
|
-
} else if (p.phase === 'done' && started) {
|
|
241
|
-
this.log(`stt-local: ${this.model.id} ready.`);
|
|
226
|
+
const pct = p.totalBytes > 0 ? Math.floor((p.receivedBytes / p.totalBytes) * 100) : 0;
|
|
227
|
+
if (!started) {
|
|
228
|
+
if (pct >= 100) return;
|
|
229
|
+
started = true;
|
|
230
|
+
const mb = Math.round(modelBytes(this.model) / 1_000_000);
|
|
231
|
+
this.log(`stt-local: first use — downloading Whisper ${this.model.id} (~${mb} MB) to ${this.modelsDir}, one-time`);
|
|
232
|
+
}
|
|
233
|
+
if (pct >= lastPct + 10) {
|
|
234
|
+
lastPct = pct - (pct % 10);
|
|
235
|
+
this.log(lastPct >= 100 ? `stt-local: ${this.model.id} ready.` : `stt-local: downloading ${this.model.id} … ${lastPct}%`);
|
|
242
236
|
}
|
|
243
237
|
};
|
|
244
238
|
}
|
package/src/models.test.ts
CHANGED
|
@@ -1,58 +1,79 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
1
3
|
import { describe, expect, it } from 'vitest';
|
|
2
4
|
import {
|
|
3
5
|
DEFAULT_MODEL_ID,
|
|
4
|
-
MODEL_CATALOG,
|
|
5
6
|
findModel,
|
|
7
|
+
modelBytes,
|
|
8
|
+
modelDir,
|
|
9
|
+
modelFiles,
|
|
6
10
|
modelIds,
|
|
7
11
|
requireModel,
|
|
8
12
|
} from './models.js';
|
|
9
13
|
|
|
10
14
|
describe('MODEL_CATALOG', () => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
const HF = 'https://huggingface.co/csukuangfj/sherpa-onnx-whisper-turbo/resolve/2ca6ff69fc878651b770880507669577ac41c2ff';
|
|
16
|
+
|
|
17
|
+
it('pins only Whisper turbo, as its three int8 files on a fixed Hugging Face revision', () => {
|
|
18
|
+
expect(modelIds()).toEqual(['turbo']);
|
|
19
|
+
const turbo = findModel('turbo')!;
|
|
20
|
+
expect(turbo.encoder).toEqual({
|
|
21
|
+
name: 'turbo-encoder.int8.onnx',
|
|
22
|
+
url: `${HF}/turbo-encoder.int8.onnx`,
|
|
23
|
+
bytes: 674_716_297,
|
|
24
|
+
sha256: 'b02dcdf54f348741e93fe732b67d933c8dcb6735655f710640143081db38878b',
|
|
25
|
+
});
|
|
26
|
+
expect(turbo.decoder).toEqual({
|
|
27
|
+
name: 'turbo-decoder.int8.onnx',
|
|
28
|
+
url: `${HF}/turbo-decoder.int8.onnx`,
|
|
29
|
+
bytes: 361_080_764,
|
|
30
|
+
sha256: '20accd02388482eb3a46bd615631adfdc85e1eb2c7db9ea3f02a40ffe6b81547',
|
|
31
|
+
});
|
|
32
|
+
expect(turbo.tokens).toEqual({
|
|
33
|
+
name: 'turbo-tokens.txt',
|
|
34
|
+
url: `${HF}/turbo-tokens.txt`,
|
|
35
|
+
bytes: 816_730,
|
|
36
|
+
sha256: 'b34b360dbb493e781e479794586d661700670d65564001f23024971d1f2fa126',
|
|
37
|
+
});
|
|
24
38
|
});
|
|
25
39
|
|
|
26
|
-
it('
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
'
|
|
32
|
-
);
|
|
33
|
-
expect(
|
|
34
|
-
|
|
40
|
+
it('lists the files of a model and their total size', () => {
|
|
41
|
+
const turbo = findModel('turbo')!;
|
|
42
|
+
expect(modelFiles(turbo).map((file) => file.name)).toEqual([
|
|
43
|
+
'turbo-encoder.int8.onnx',
|
|
44
|
+
'turbo-decoder.int8.onnx',
|
|
45
|
+
'turbo-tokens.txt',
|
|
46
|
+
]);
|
|
47
|
+
expect(modelBytes(turbo)).toBe(674_716_297 + 361_080_764 + 816_730);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('keeps each model in its own directory under the models root', () => {
|
|
51
|
+
expect(modelDir(findModel('turbo')!, '/home/me/.moxxy/models/stt')).toBe(
|
|
52
|
+
path.join('/home/me/.moxxy/models/stt', 'turbo'),
|
|
35
53
|
);
|
|
36
54
|
});
|
|
37
55
|
|
|
38
|
-
it('defaults to
|
|
39
|
-
expect(DEFAULT_MODEL_ID).toBe('
|
|
56
|
+
it('defaults to turbo', () => {
|
|
57
|
+
expect(DEFAULT_MODEL_ID).toBe('turbo');
|
|
40
58
|
expect(findModel(DEFAULT_MODEL_ID)).toBeDefined();
|
|
41
59
|
});
|
|
42
60
|
});
|
|
43
61
|
|
|
44
62
|
describe('requireModel', () => {
|
|
45
63
|
it('returns the catalog entry for a known id', () => {
|
|
46
|
-
expect(requireModel('
|
|
64
|
+
expect(requireModel('turbo', 'model').id).toBe('turbo');
|
|
47
65
|
});
|
|
48
66
|
|
|
49
|
-
it('
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
it('rejects the removed tiny/base/small models with CONFIG_INVALID naming turbo', () => {
|
|
68
|
+
for (const removed of ['tiny', 'base', 'small']) {
|
|
69
|
+
try {
|
|
70
|
+
requireModel(removed, 'model');
|
|
71
|
+
expect.unreachable(`${removed} should be rejected`);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
expect((err as { code?: string }).code).toBe('CONFIG_INVALID');
|
|
74
|
+
expect((err as Error).message).toMatch(/Unknown local Whisper model/);
|
|
75
|
+
expect((err as { hint?: string }).hint).toBe('Valid models: turbo.');
|
|
76
|
+
}
|
|
56
77
|
}
|
|
57
78
|
});
|
|
58
79
|
});
|
package/src/models.ts
CHANGED
|
@@ -1,90 +1,85 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The pinned Whisper model catalog.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* variants we don't use) and `<id>-tokens.txt`.
|
|
4
|
+
* The one model is Whisper large-v3 turbo, the multilingual sherpa-onnx int8
|
|
5
|
+
* export. Its three files download as they are from Hugging Face, pinned to a
|
|
6
|
+
* fixed revision and checked against their size and sha256 — byte for byte the
|
|
7
|
+
* files the project's `asr-models` GitHub release packs into a `.tar.bz2`
|
|
8
|
+
* (verified 2026-10-24), but with no archive to unpack: unpacking it in
|
|
9
|
+
* JavaScript took about 67 s against 9 s for the download.
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* sweet spot for Polish, `base` the balanced default, `tiny` the fastest.
|
|
11
|
+
* Multilingual (not an `.en` export), so Polish and every other Whisper
|
|
12
|
+
* language work.
|
|
15
13
|
*/
|
|
16
14
|
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
|
|
17
17
|
import { MoxxyError } from '@moxxy/sdk';
|
|
18
|
+
import { moxxyPath } from '@moxxy/sdk/server';
|
|
19
|
+
import type { ModelFile } from '@moxxy/model-fetch/files';
|
|
18
20
|
|
|
19
|
-
export type WhisperModelId = '
|
|
21
|
+
export type WhisperModelId = 'turbo';
|
|
20
22
|
|
|
21
23
|
export interface WhisperModelEntry {
|
|
22
|
-
/** Stable model id (also the `model` config value). */
|
|
24
|
+
/** Stable model id (also the `model` config value and its directory name). */
|
|
23
25
|
readonly id: WhisperModelId;
|
|
24
26
|
/** Human label for UI surfaces / the first-use notice. */
|
|
25
27
|
readonly label: string;
|
|
26
|
-
|
|
27
|
-
readonly
|
|
28
|
-
|
|
29
|
-
readonly sha256: string;
|
|
30
|
-
/** Top directory the archive extracts to (`sherpa-onnx-whisper-<id>`). */
|
|
31
|
-
readonly archiveRootDir: string;
|
|
32
|
-
/** Encoder ONNX filename inside `archiveRootDir` (`<id>-encoder.onnx`). */
|
|
33
|
-
readonly encoderFile: string;
|
|
34
|
-
/** Decoder ONNX filename inside `archiveRootDir` (`<id>-decoder.onnx`). */
|
|
35
|
-
readonly decoderFile: string;
|
|
36
|
-
/** Tokens filename inside `archiveRootDir` (`<id>-tokens.txt`). */
|
|
37
|
-
readonly tokensFile: string;
|
|
38
|
-
/** Approximate download size in MB (for the one-time first-use notice). */
|
|
39
|
-
readonly approxMb: number;
|
|
28
|
+
readonly encoder: ModelFile;
|
|
29
|
+
readonly decoder: ModelFile;
|
|
30
|
+
readonly tokens: ModelFile;
|
|
40
31
|
}
|
|
41
32
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
function whisperModel(
|
|
45
|
-
id: WhisperModelId,
|
|
46
|
-
label: string,
|
|
47
|
-
sha256: string,
|
|
48
|
-
approxMb: number,
|
|
49
|
-
): WhisperModelEntry {
|
|
50
|
-
const archiveRootDir = `sherpa-onnx-whisper-${id}`;
|
|
51
|
-
return {
|
|
52
|
-
id,
|
|
53
|
-
label,
|
|
54
|
-
url: `${RELEASE_BASE}/${archiveRootDir}.tar.bz2`,
|
|
55
|
-
sha256,
|
|
56
|
-
archiveRootDir,
|
|
57
|
-
encoderFile: `${id}-encoder.onnx`,
|
|
58
|
-
decoderFile: `${id}-decoder.onnx`,
|
|
59
|
-
tokensFile: `${id}-tokens.txt`,
|
|
60
|
-
approxMb,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
33
|
+
const TURBO_REVISION = '2ca6ff69fc878651b770880507669577ac41c2ff';
|
|
34
|
+
const TURBO_BASE = `https://huggingface.co/csukuangfj/sherpa-onnx-whisper-turbo/resolve/${TURBO_REVISION}`;
|
|
63
35
|
|
|
64
36
|
export const MODEL_CATALOG: readonly WhisperModelEntry[] = [
|
|
65
|
-
|
|
66
|
-
'
|
|
67
|
-
'Whisper
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
37
|
+
{
|
|
38
|
+
id: 'turbo',
|
|
39
|
+
label: 'Whisper turbo (multilingual, large-v3 turbo)',
|
|
40
|
+
encoder: {
|
|
41
|
+
name: 'turbo-encoder.int8.onnx',
|
|
42
|
+
url: `${TURBO_BASE}/turbo-encoder.int8.onnx`,
|
|
43
|
+
bytes: 674_716_297,
|
|
44
|
+
sha256: 'b02dcdf54f348741e93fe732b67d933c8dcb6735655f710640143081db38878b',
|
|
45
|
+
},
|
|
46
|
+
decoder: {
|
|
47
|
+
name: 'turbo-decoder.int8.onnx',
|
|
48
|
+
url: `${TURBO_BASE}/turbo-decoder.int8.onnx`,
|
|
49
|
+
bytes: 361_080_764,
|
|
50
|
+
sha256: '20accd02388482eb3a46bd615631adfdc85e1eb2c7db9ea3f02a40ffe6b81547',
|
|
51
|
+
},
|
|
52
|
+
tokens: {
|
|
53
|
+
name: 'turbo-tokens.txt',
|
|
54
|
+
url: `${TURBO_BASE}/turbo-tokens.txt`,
|
|
55
|
+
bytes: 816_730,
|
|
56
|
+
sha256: 'b34b360dbb493e781e479794586d661700670d65564001f23024971d1f2fa126',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
83
59
|
];
|
|
84
60
|
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
|
|
61
|
+
/** Every file of a model, in download order. */
|
|
62
|
+
export function modelFiles(entry: WhisperModelEntry): ModelFile[] {
|
|
63
|
+
return [entry.encoder, entry.decoder, entry.tokens];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The model's download size in bytes. */
|
|
67
|
+
export function modelBytes(entry: WhisperModelEntry): number {
|
|
68
|
+
return modelFiles(entry).reduce((sum, file) => sum + file.bytes, 0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The models root: `~/.moxxy/models/stt`, MOXXY_HOME-aware. */
|
|
72
|
+
export function defaultModelsDir(): string {
|
|
73
|
+
return moxxyPath('models', 'stt');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Where a model's files live under the models root. */
|
|
77
|
+
export function modelDir(entry: WhisperModelEntry, modelsDir: string): string {
|
|
78
|
+
return path.join(modelsDir, entry.id);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The model used when the caller doesn't pick one — the only one there is. */
|
|
82
|
+
export const DEFAULT_MODEL_ID: WhisperModelId = 'turbo';
|
|
88
83
|
|
|
89
84
|
/** All valid model ids, for error messages and validation. */
|
|
90
85
|
export function modelIds(): string[] {
|