@qvac/bci-whispercpp 0.0.0 → 0.1.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/CHANGELOG.md +76 -0
- package/LICENSE +179 -0
- package/NOTICE +108 -0
- package/README.md +378 -0
- package/addonLogging.d.ts +7 -0
- package/addonLogging.js +6 -0
- package/bci.js +370 -0
- package/binding.js +1 -0
- package/configChecker.js +91 -0
- package/index.d.ts +134 -0
- package/index.js +676 -0
- package/lib/error.js +112 -0
- package/lib/stream.js +207 -0
- package/lib/util.js +15 -0
- package/lib/wer.js +40 -0
- package/package.json +100 -1
- package/prebuilds/android-arm64/qvac__bci-whispercpp.bare +0 -0
- package/prebuilds/darwin-arm64/qvac__bci-whispercpp.bare +0 -0
- package/prebuilds/darwin-arm64/qvac__bci-whispercpp.bare.exports +1439 -0
- package/prebuilds/darwin-x64/qvac__bci-whispercpp.bare +0 -0
- package/prebuilds/darwin-x64/qvac__bci-whispercpp.bare.exports +1654 -0
- package/prebuilds/ios-arm64/qvac__bci-whispercpp.bare +0 -0
- package/prebuilds/ios-arm64/qvac__bci-whispercpp.bare.exports +1439 -0
- package/prebuilds/ios-arm64-simulator/qvac__bci-whispercpp.bare +0 -0
- package/prebuilds/ios-arm64-simulator/qvac__bci-whispercpp.bare.exports +1439 -0
- package/prebuilds/ios-x64-simulator/qvac__bci-whispercpp.bare +0 -0
- package/prebuilds/ios-x64-simulator/qvac__bci-whispercpp.bare.exports +1654 -0
- package/prebuilds/linux-arm64/qvac__bci-whispercpp.bare +0 -0
- package/prebuilds/linux-x64/qvac__bci-whispercpp.bare +0 -0
- package/prebuilds/win32-x64/qvac__bci-whispercpp.bare +0 -0
- package/prebuilds/win32-x64/qvac__bci-whispercpp.bare.exports +0 -0
package/bci.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { QvacErrorAddonBCI, ERR_CODES } = require('./lib/error')
|
|
4
|
+
const { checkConfig } = require('./configChecker')
|
|
5
|
+
|
|
6
|
+
const state = Object.freeze({
|
|
7
|
+
LOADING: 'loading',
|
|
8
|
+
LISTENING: 'listening',
|
|
9
|
+
PROCESSING: 'processing',
|
|
10
|
+
IDLE: 'idle'
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const END_OF_INPUT = 'end of job'
|
|
14
|
+
|
|
15
|
+
// Upper bound on buffered neural-signal bytes between append() calls.
|
|
16
|
+
// Neural data is ~1 MB/s at 512ch * 50 Hz * 4 B, so 500 MB ~= 8 minutes of
|
|
17
|
+
// signal. The bound matches qvac-lib-infer-whispercpp and protects against
|
|
18
|
+
// runaway producers.
|
|
19
|
+
const MAX_BUFFERED_BYTES = 500 * 1024 * 1024
|
|
20
|
+
|
|
21
|
+
function nextSafeId (current) {
|
|
22
|
+
return current >= Number.MAX_SAFE_INTEGER ? 1 : current + 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Low-level interface between the Bare C++ BCI addon and the JS runtime.
|
|
27
|
+
* Accepts neural signal data (Uint8Array) instead of audio.
|
|
28
|
+
*/
|
|
29
|
+
class BCIInterface {
|
|
30
|
+
/**
|
|
31
|
+
* @param {Object} binding - the native binding object
|
|
32
|
+
* @param {Object} configurationParams - configuration for the BCI model
|
|
33
|
+
* @param {Function} outputCb - callback for inference events (Output, JobEnded, Error)
|
|
34
|
+
* @param {Function} [transitionCb] - callback for state changes
|
|
35
|
+
*/
|
|
36
|
+
constructor (binding, configurationParams, outputCb, transitionCb = null) {
|
|
37
|
+
this._binding = binding
|
|
38
|
+
this._outputCb = outputCb
|
|
39
|
+
this._transitionCb = transitionCb
|
|
40
|
+
this._nextJobId = 1
|
|
41
|
+
this._activeJobId = null
|
|
42
|
+
this._bufferedSignal = []
|
|
43
|
+
this._bufferedBytes = 0
|
|
44
|
+
this._state = state.LOADING
|
|
45
|
+
|
|
46
|
+
checkConfig(configurationParams)
|
|
47
|
+
this._handle = this._binding.createInstance(
|
|
48
|
+
this,
|
|
49
|
+
configurationParams,
|
|
50
|
+
this._addonOutputCallback.bind(this),
|
|
51
|
+
transitionCb
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_setState (newState) {
|
|
56
|
+
this._state = newState
|
|
57
|
+
if (this._transitionCb) {
|
|
58
|
+
this._transitionCb(this, newState)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_addonOutputCallback (addon, event, data, error) {
|
|
63
|
+
const isError = typeof error === 'string' && error.length > 0
|
|
64
|
+
const isStats = data && typeof data === 'object' && (
|
|
65
|
+
'totalTime' in data ||
|
|
66
|
+
'tokensPerSecond' in data ||
|
|
67
|
+
'totalWallMs' in data
|
|
68
|
+
)
|
|
69
|
+
const isTranscriptOutput = (
|
|
70
|
+
(Array.isArray(data) && data.length > 0) ||
|
|
71
|
+
(data && typeof data === 'object' && typeof data.text === 'string')
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
let mappedEvent = event
|
|
75
|
+
if (event === 'Error' || isError || String(event).includes('Error')) {
|
|
76
|
+
mappedEvent = 'Error'
|
|
77
|
+
} else if (event === 'JobEnded' || isStats || String(event).includes('RuntimeStats')) {
|
|
78
|
+
mappedEvent = 'JobEnded'
|
|
79
|
+
} else if (event === 'Output' || isTranscriptOutput) {
|
|
80
|
+
mappedEvent = 'Output'
|
|
81
|
+
} else if (Array.isArray(data) && data.length === 0) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const jobId = this._activeJobId
|
|
86
|
+
if (jobId === null || jobId === undefined) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (mappedEvent === 'Output') {
|
|
91
|
+
this._setState(state.PROCESSING)
|
|
92
|
+
|
|
93
|
+
if (this._outputCb != null) {
|
|
94
|
+
const isTranscriptArray = Array.isArray(data) && data.length > 0 &&
|
|
95
|
+
typeof data[0]?.text === 'string'
|
|
96
|
+
const isSingleTranscript = !Array.isArray(data) &&
|
|
97
|
+
data && typeof data === 'object' && typeof data.text === 'string'
|
|
98
|
+
if (isTranscriptArray) {
|
|
99
|
+
for (const segment of data) {
|
|
100
|
+
this._outputCb(addon, 'Output', jobId, [segment], null)
|
|
101
|
+
}
|
|
102
|
+
} else if (isSingleTranscript) {
|
|
103
|
+
this._outputCb(addon, 'Output', jobId, [data], null)
|
|
104
|
+
} else {
|
|
105
|
+
this._outputCb(addon, 'Output', jobId, data, null)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (this._outputCb != null) {
|
|
112
|
+
this._outputCb(addon, mappedEvent, jobId, data, isError ? error : null)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (mappedEvent === 'Error' || mappedEvent === 'JobEnded') {
|
|
116
|
+
this._activeJobId = null
|
|
117
|
+
this._setState(state.LISTENING)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async unload () {
|
|
122
|
+
await this.destroyInstance()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async load (configurationParams) {
|
|
126
|
+
checkConfig(configurationParams)
|
|
127
|
+
await this.destroyInstance()
|
|
128
|
+
this._handle = this._binding.createInstance(
|
|
129
|
+
this,
|
|
130
|
+
configurationParams,
|
|
131
|
+
this._addonOutputCallback.bind(this),
|
|
132
|
+
this._transitionCb
|
|
133
|
+
)
|
|
134
|
+
this._setState(state.LOADING)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async reload (configurationParams) {
|
|
138
|
+
checkConfig(configurationParams)
|
|
139
|
+
await this.cancel()
|
|
140
|
+
|
|
141
|
+
if (typeof this._binding.reload === 'function') {
|
|
142
|
+
await this._binding.reload(this._handle, configurationParams)
|
|
143
|
+
this._setState(state.LOADING)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
await this.load(configurationParams)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async loadWeights (weightsData) {
|
|
151
|
+
try {
|
|
152
|
+
this._binding.loadWeights(this._handle, weightsData)
|
|
153
|
+
} catch (err) {
|
|
154
|
+
throw new QvacErrorAddonBCI({
|
|
155
|
+
code: ERR_CODES.FAILED_TO_LOAD_WEIGHTS,
|
|
156
|
+
adds: err.message,
|
|
157
|
+
cause: err
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async unloadWeights () {
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async activate () {
|
|
167
|
+
try {
|
|
168
|
+
this._binding.activate(this._handle)
|
|
169
|
+
this._setState(state.LISTENING)
|
|
170
|
+
} catch (err) {
|
|
171
|
+
throw new QvacErrorAddonBCI({
|
|
172
|
+
code: ERR_CODES.FAILED_TO_ACTIVATE,
|
|
173
|
+
adds: err.message,
|
|
174
|
+
cause: err
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async cancel (jobId) {
|
|
180
|
+
try {
|
|
181
|
+
await this._binding.cancel(this._handle, jobId)
|
|
182
|
+
this._bufferedSignal = []
|
|
183
|
+
this._bufferedBytes = 0
|
|
184
|
+
this._activeJobId = null
|
|
185
|
+
this._setState(state.LISTENING)
|
|
186
|
+
} catch (err) {
|
|
187
|
+
throw new QvacErrorAddonBCI({
|
|
188
|
+
code: ERR_CODES.FAILED_TO_CANCEL,
|
|
189
|
+
adds: err.message,
|
|
190
|
+
cause: err
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Appends neural signal data to the processing buffer.
|
|
197
|
+
* Send { type: 'end of job' } to trigger processing.
|
|
198
|
+
* @param {Object} data
|
|
199
|
+
* @param {string} data.type - 'neural' or 'end of job'
|
|
200
|
+
* @param {Uint8Array} [data.input] - binary neural signal data
|
|
201
|
+
* @returns {number} job ID
|
|
202
|
+
*/
|
|
203
|
+
async append (data) {
|
|
204
|
+
try {
|
|
205
|
+
if (data?.type === END_OF_INPUT) {
|
|
206
|
+
if (this._bufferedSignal.length === 0) {
|
|
207
|
+
throw new QvacErrorAddonBCI({
|
|
208
|
+
code: ERR_CODES.INVALID_NEURAL_INPUT,
|
|
209
|
+
adds: 'no neural signal data was appended before end-of-job'
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
const currentJobId = this._nextJobId
|
|
213
|
+
const input = this._concatBufferedSignal()
|
|
214
|
+
const previousState = this._state
|
|
215
|
+
const previousJobId = this._activeJobId
|
|
216
|
+
|
|
217
|
+
let accepted = false
|
|
218
|
+
try {
|
|
219
|
+
accepted = this._binding.runJob(this._handle, {
|
|
220
|
+
type: 'neural',
|
|
221
|
+
input
|
|
222
|
+
})
|
|
223
|
+
} catch (err) {
|
|
224
|
+
this._activeJobId = previousJobId
|
|
225
|
+
this._setState(previousState)
|
|
226
|
+
throw err
|
|
227
|
+
}
|
|
228
|
+
if (!accepted) {
|
|
229
|
+
this._activeJobId = previousJobId
|
|
230
|
+
this._setState(previousState)
|
|
231
|
+
throw new QvacErrorAddonBCI({ code: ERR_CODES.JOB_ALREADY_RUNNING })
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this._activeJobId = currentJobId
|
|
235
|
+
this._nextJobId = nextSafeId(this._nextJobId)
|
|
236
|
+
this._bufferedSignal = []
|
|
237
|
+
this._bufferedBytes = 0
|
|
238
|
+
this._setState(state.PROCESSING)
|
|
239
|
+
return currentJobId
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (data?.type === 'neural') {
|
|
243
|
+
if (!(data.input instanceof Uint8Array)) {
|
|
244
|
+
throw new QvacErrorAddonBCI({
|
|
245
|
+
code: ERR_CODES.INVALID_NEURAL_INPUT,
|
|
246
|
+
adds: 'input must be Uint8Array'
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
if (this._bufferedBytes + data.input.byteLength > MAX_BUFFERED_BYTES) {
|
|
250
|
+
throw new QvacErrorAddonBCI({
|
|
251
|
+
code: ERR_CODES.BUFFER_LIMIT_EXCEEDED,
|
|
252
|
+
adds: MAX_BUFFERED_BYTES + ' bytes'
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
this._bufferedSignal.push(data.input)
|
|
256
|
+
this._bufferedBytes += data.input.byteLength
|
|
257
|
+
return this._nextJobId
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
throw new Error(`Unknown append input type: ${data?.type}`)
|
|
261
|
+
} catch (err) {
|
|
262
|
+
if (err instanceof QvacErrorAddonBCI) throw err
|
|
263
|
+
throw new QvacErrorAddonBCI({
|
|
264
|
+
code: ERR_CODES.FAILED_TO_APPEND,
|
|
265
|
+
adds: err.message,
|
|
266
|
+
cause: err
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Run a single batch job directly with neural signal data.
|
|
273
|
+
* @param {Object} data
|
|
274
|
+
* @param {Uint8Array} data.input - binary neural signal data
|
|
275
|
+
*/
|
|
276
|
+
async runJob (data) {
|
|
277
|
+
if (!data || !(data.input instanceof Uint8Array)) {
|
|
278
|
+
throw new QvacErrorAddonBCI({
|
|
279
|
+
code: ERR_CODES.INVALID_NEURAL_INPUT,
|
|
280
|
+
adds: 'runJob input must be a Uint8Array'
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
if (data.input.byteLength === 0) {
|
|
284
|
+
throw new QvacErrorAddonBCI({
|
|
285
|
+
code: ERR_CODES.INVALID_NEURAL_INPUT,
|
|
286
|
+
adds: 'runJob input must not be empty'
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const candidateJobId = this._nextJobId
|
|
291
|
+
const previousState = this._state
|
|
292
|
+
const previousJobId = this._activeJobId
|
|
293
|
+
let accepted = false
|
|
294
|
+
try {
|
|
295
|
+
accepted = this._binding.runJob(this._handle, {
|
|
296
|
+
type: 'neural',
|
|
297
|
+
input: data.input
|
|
298
|
+
})
|
|
299
|
+
} catch (err) {
|
|
300
|
+
this._activeJobId = previousJobId
|
|
301
|
+
this._setState(previousState)
|
|
302
|
+
throw new QvacErrorAddonBCI({
|
|
303
|
+
code: ERR_CODES.FAILED_TO_START_JOB,
|
|
304
|
+
adds: err.message,
|
|
305
|
+
cause: err
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!accepted) {
|
|
310
|
+
this._activeJobId = previousJobId
|
|
311
|
+
this._setState(previousState)
|
|
312
|
+
return false
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
this._activeJobId = candidateJobId
|
|
316
|
+
this._nextJobId = nextSafeId(this._nextJobId)
|
|
317
|
+
this._setState(state.PROCESSING)
|
|
318
|
+
return accepted
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async status () {
|
|
322
|
+
return this._state
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async destroyInstance () {
|
|
326
|
+
if (this._handle === null) {
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
try {
|
|
331
|
+
await this._binding.cancel(this._handle)
|
|
332
|
+
} catch {}
|
|
333
|
+
this._binding.destroyInstance(this._handle)
|
|
334
|
+
this._handle = null
|
|
335
|
+
this._bufferedSignal = []
|
|
336
|
+
this._bufferedBytes = 0
|
|
337
|
+
this._activeJobId = null
|
|
338
|
+
this._setState(state.IDLE)
|
|
339
|
+
} catch (err) {
|
|
340
|
+
throw new QvacErrorAddonBCI({
|
|
341
|
+
code: ERR_CODES.FAILED_TO_DESTROY,
|
|
342
|
+
adds: err.message,
|
|
343
|
+
cause: err
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
_concatBufferedSignal () {
|
|
349
|
+
if (this._bufferedSignal.length === 0) {
|
|
350
|
+
return new Uint8Array()
|
|
351
|
+
}
|
|
352
|
+
if (this._bufferedSignal.length === 1) {
|
|
353
|
+
return this._bufferedSignal[0]
|
|
354
|
+
}
|
|
355
|
+
const totalLength = this._bufferedSignal.reduce(
|
|
356
|
+
(sum, chunk) => sum + chunk.byteLength, 0
|
|
357
|
+
)
|
|
358
|
+
const merged = new Uint8Array(totalLength)
|
|
359
|
+
let offset = 0
|
|
360
|
+
for (const chunk of this._bufferedSignal) {
|
|
361
|
+
merged.set(chunk, offset)
|
|
362
|
+
offset += chunk.byteLength
|
|
363
|
+
}
|
|
364
|
+
return merged
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
BCIInterface.END_OF_INPUT = END_OF_INPUT
|
|
369
|
+
|
|
370
|
+
module.exports = { BCIInterface, END_OF_INPUT, MAX_BUFFERED_BYTES, nextSafeId }
|
package/binding.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require.addon()
|
package/configChecker.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validates BCI addon configuration.
|
|
5
|
+
* @param {Object} configObject
|
|
6
|
+
* @returns {void} or throws if invalid
|
|
7
|
+
*/
|
|
8
|
+
function checkConfig (configObject) {
|
|
9
|
+
const requiredSections = ['whisperConfig', 'contextParams', 'miscConfig']
|
|
10
|
+
|
|
11
|
+
for (const section of requiredSections) {
|
|
12
|
+
if (!configObject[section]) {
|
|
13
|
+
throw new Error(`${section} object is required`)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const validWhisperParams = [
|
|
18
|
+
'n_threads',
|
|
19
|
+
'duration_ms',
|
|
20
|
+
'translate',
|
|
21
|
+
'no_timestamps',
|
|
22
|
+
'single_segment',
|
|
23
|
+
'print_special',
|
|
24
|
+
'print_progress',
|
|
25
|
+
'print_realtime',
|
|
26
|
+
'print_timestamps',
|
|
27
|
+
'language',
|
|
28
|
+
'detect_language',
|
|
29
|
+
'suppress_blank',
|
|
30
|
+
'suppress_nst',
|
|
31
|
+
'temperature',
|
|
32
|
+
'greedy_best_of',
|
|
33
|
+
'beam_search_beam_size'
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const validContextParams = [
|
|
37
|
+
'model',
|
|
38
|
+
'use_gpu',
|
|
39
|
+
'flash_attn',
|
|
40
|
+
'gpu_device'
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
const validMiscParams = [
|
|
44
|
+
'caption_enabled'
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
const validBCIParams = [
|
|
48
|
+
'day_idx'
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
for (const userParam of Object.keys(configObject.whisperConfig)) {
|
|
52
|
+
if (!validWhisperParams.includes(userParam)) {
|
|
53
|
+
throw new Error(`${userParam} is not a valid parameter for whisperConfig`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const userParam of Object.keys(configObject.contextParams)) {
|
|
58
|
+
if (!validContextParams.includes(userParam)) {
|
|
59
|
+
throw new Error(`${userParam} is not a valid parameter for contextParams`)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const userParam of Object.keys(configObject.miscConfig)) {
|
|
64
|
+
if (!validMiscParams.includes(userParam)) {
|
|
65
|
+
throw new Error(`${userParam} is not a valid parameter for miscConfig`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (configObject.miscConfig.caption_enabled !== undefined &&
|
|
69
|
+
typeof configObject.miscConfig.caption_enabled !== 'boolean') {
|
|
70
|
+
throw new Error('miscConfig.caption_enabled must be a boolean')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (configObject.bciConfig) {
|
|
74
|
+
for (const userParam of Object.keys(configObject.bciConfig)) {
|
|
75
|
+
if (!validBCIParams.includes(userParam)) {
|
|
76
|
+
throw new Error(`${userParam} is not a valid parameter for bciConfig`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const dayIdx = configObject.bciConfig.day_idx
|
|
80
|
+
if (dayIdx !== undefined) {
|
|
81
|
+
if (typeof dayIdx !== 'number' || !Number.isFinite(dayIdx) || !Number.isInteger(dayIdx)) {
|
|
82
|
+
throw new Error('bciConfig.day_idx must be a finite integer')
|
|
83
|
+
}
|
|
84
|
+
if (dayIdx < -1) {
|
|
85
|
+
throw new Error('bciConfig.day_idx must be >= -1 (use -1 to enable mel-passthrough mode)')
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { checkConfig }
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import QvacResponse from '@qvac/infer-base/src/QvacResponse'
|
|
2
|
+
import type { LoggerInterface } from '@qvac/logging'
|
|
3
|
+
|
|
4
|
+
declare interface BCIConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Session day index used to select day-specific projection matrices in
|
|
7
|
+
* bci-embedder.bin.
|
|
8
|
+
*
|
|
9
|
+
* - `day_idx >= 0` (default `0`): apply the day projection; values beyond
|
|
10
|
+
* the available range are clamped at the native layer.
|
|
11
|
+
* - `day_idx === -1`: mel passthrough — skip preprocessing and treat
|
|
12
|
+
* the input buffer as pre-computed 512-bin mel features in
|
|
13
|
+
* frame-major layout. Intended for parity testing against the Python
|
|
14
|
+
* reference, not production use.
|
|
15
|
+
*/
|
|
16
|
+
day_idx?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare interface WhisperConfig {
|
|
20
|
+
language?: string
|
|
21
|
+
n_threads?: number
|
|
22
|
+
temperature?: number
|
|
23
|
+
suppress_nst?: boolean
|
|
24
|
+
suppress_blank?: boolean
|
|
25
|
+
duration_ms?: number
|
|
26
|
+
translate?: boolean
|
|
27
|
+
no_timestamps?: boolean
|
|
28
|
+
single_segment?: boolean
|
|
29
|
+
print_special?: boolean
|
|
30
|
+
print_progress?: boolean
|
|
31
|
+
print_realtime?: boolean
|
|
32
|
+
print_timestamps?: boolean
|
|
33
|
+
detect_language?: boolean
|
|
34
|
+
greedy_best_of?: number
|
|
35
|
+
beam_search_beam_size?: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
declare interface BCIWhispercppFiles {
|
|
39
|
+
/** Absolute path to the BCI GGML model file. */
|
|
40
|
+
model: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
declare interface BCIWhispercppArgs {
|
|
44
|
+
files: BCIWhispercppFiles
|
|
45
|
+
logger?: LoggerInterface
|
|
46
|
+
opts?: {
|
|
47
|
+
stats?: boolean
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
declare interface BCIWhispercppConfig {
|
|
52
|
+
whisperConfig?: WhisperConfig
|
|
53
|
+
bciConfig?: BCIConfig
|
|
54
|
+
contextParams?: {
|
|
55
|
+
model?: string
|
|
56
|
+
use_gpu?: boolean
|
|
57
|
+
flash_attn?: boolean
|
|
58
|
+
gpu_device?: number
|
|
59
|
+
}
|
|
60
|
+
miscConfig?: {
|
|
61
|
+
caption_enabled?: boolean
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
declare interface TranscriptSegment {
|
|
66
|
+
text: string
|
|
67
|
+
toAppend: boolean
|
|
68
|
+
start: number
|
|
69
|
+
end: number
|
|
70
|
+
id: number
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
declare interface BCIWhispercppState {
|
|
74
|
+
configLoaded: boolean
|
|
75
|
+
destroyed: boolean
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* BCI neural signal transcription client powered by whisper.cpp.
|
|
80
|
+
*
|
|
81
|
+
* Uses `createJobHandler` + `exclusiveRunQueue` from `@qvac/infer-base` and
|
|
82
|
+
* follows the same lifecycle contract as `TranscriptionWhispercpp` /
|
|
83
|
+
* `LlmLlamacpp`: construct with local file paths, call `load()`, issue
|
|
84
|
+
* `transcribe()` / `transcribeFile()` calls, then `destroy()`.
|
|
85
|
+
*/
|
|
86
|
+
declare class BCIWhispercpp {
|
|
87
|
+
constructor(args: BCIWhispercppArgs, config?: BCIWhispercppConfig)
|
|
88
|
+
|
|
89
|
+
/** Load and activate the model. Must be awaited before `transcribe()`. */
|
|
90
|
+
load(): Promise<void>
|
|
91
|
+
|
|
92
|
+
/** Transcribe a neural signal binary file (convenience wrapper). */
|
|
93
|
+
transcribeFile(filePath: string): Promise<QvacResponse>
|
|
94
|
+
|
|
95
|
+
/** Transcribe a neural signal buffer (batch mode). */
|
|
96
|
+
transcribe(neuralData: Uint8Array): Promise<QvacResponse>
|
|
97
|
+
|
|
98
|
+
/** Cancel the in-flight inference, if any. */
|
|
99
|
+
cancel(): Promise<void>
|
|
100
|
+
|
|
101
|
+
/** Unload the model and release native resources. Instance is reusable. */
|
|
102
|
+
unload(): Promise<void>
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Destroy the instance, unload, and mark as permanently destroyed.
|
|
106
|
+
* Subsequent `load()` calls will throw `MODEL_NOT_LOADED`.
|
|
107
|
+
*/
|
|
108
|
+
destroy(): Promise<void>
|
|
109
|
+
|
|
110
|
+
/** Current lifecycle state. */
|
|
111
|
+
getState(): BCIWhispercppState
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
declare namespace BCIWhispercpp {
|
|
115
|
+
/**
|
|
116
|
+
* Compute Word Error Rate between hypothesis and reference strings.
|
|
117
|
+
* @returns WER as a ratio (0.0 = perfect).
|
|
118
|
+
*/
|
|
119
|
+
function computeWER(hypothesis: string, reference: string): number
|
|
120
|
+
|
|
121
|
+
export {
|
|
122
|
+
BCIWhispercpp as default,
|
|
123
|
+
BCIWhispercpp,
|
|
124
|
+
BCIConfig,
|
|
125
|
+
WhisperConfig,
|
|
126
|
+
BCIWhispercppFiles,
|
|
127
|
+
BCIWhispercppArgs,
|
|
128
|
+
BCIWhispercppConfig,
|
|
129
|
+
BCIWhispercppState,
|
|
130
|
+
TranscriptSegment
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export = BCIWhispercpp
|