Fix Vosklet browser cache handling and recognizer event bridge

- fix broken cached model loading
- cache validated TAR models instead of gzip responses
- add proper fireEv bridge for wasm/js events
- fix recognizer initialization callbacks
- fix Embind waveform return type handling
- stabilize acceptWaveform in minified builds
- add optional debug logging
This commit is contained in:
2026-05-12 19:25:31 +02:00
parent 166ecc501b
commit 90e4390d1a
9 changed files with 1800 additions and 108 deletions

1649
Vosklet.js

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,4 +1,5 @@
#include "CommonModel.h" #include "CommonModel.h"
#include "FireEv.h"
CommonModel::CommonModel(int index, bool normalMdl, int tarStart, int tarSize) : normalMdl{normalMdl}, index{index} CommonModel::CommonModel(int index, bool normalMdl, int tarStart, int tarSize) : normalMdl{normalMdl}, index{index}
{ {

18
src/FireEv.cc Normal file
View File

@@ -0,0 +1,18 @@
#include <emscripten.h>
EM_JS(void, fireEv, (int idx, const char *msgPtr), {
const msg = msgPtr ? UTF8ToString(msgPtr) : null;
const obj =
globalThis.__voskletObjs &&
globalThis.__voskletObjs[idx];
if (!obj) {
console.error("fireEv: unknown object index", idx);
return;
}
obj.dispatchEvent(new CustomEvent("", {
detail: msg
}));
});

12
src/FireEv.h Normal file
View File

@@ -0,0 +1,12 @@
// FireEv.h
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
void fireEv(int idx, const char *msgPtr);
#ifdef __cplusplus
}
#endif

View File

@@ -1,40 +1,54 @@
#include "Recognizer.h" #include "Recognizer.h"
#include "FireEv.h"
#include <atomic> #include <atomic>
const char *recognizerInitErr{"Unable to initialize recognizer"}; const char *recognizerInitErr{"Unable to initialize recognizer"};
Recognizer::Recognizer(int index, float sampleRate, CommonModel *model) : rec{vosk_recognizer_new(std::get<VoskModel *>(model->mdl), sampleRate)} Recognizer::Recognizer(int index, float sampleRate, CommonModel *model) : rec{vosk_recognizer_new(std::get<VoskModel *>(model->mdl), sampleRate)}
{ {
if (rec == nullptr) if (rec == nullptr)
{
fireEv(index, recognizerInitErr); fireEv(index, recognizerInitErr);
} else
fireEv(index);
} }
Recognizer::Recognizer(int index, float sampleRate, CommonModel *model, CommonModel *spkModel) : rec{vosk_recognizer_new_spk(std::get<VoskModel *>(model->mdl), sampleRate, std::get<VoskSpkModel *>(spkModel->mdl))} Recognizer::Recognizer(int index, float sampleRate, CommonModel *model, CommonModel *spkModel) : rec{vosk_recognizer_new_spk(std::get<VoskModel *>(model->mdl), sampleRate, std::get<VoskSpkModel *>(spkModel->mdl))}
{ {
if (rec == nullptr) if (rec == nullptr)
{
fireEv(index, recognizerInitErr); fireEv(index, recognizerInitErr);
} else
fireEv(index);
} }
Recognizer::Recognizer(int index, float sampleRate, CommonModel *model, const std::string &grm, int) : rec{vosk_recognizer_new_grm(std::get<VoskModel *>(model->mdl), sampleRate, grm.c_str())} Recognizer::Recognizer(int index, float sampleRate, CommonModel *model, const std::string &grm, int) : rec{vosk_recognizer_new_grm(std::get<VoskModel *>(model->mdl), sampleRate, grm.c_str())}
{ {
if (rec == nullptr) if (rec == nullptr)
{
fireEv(index, recognizerInitErr); fireEv(index, recognizerInitErr);
} else
fireEv(index);
} }
const char *Recognizer::acceptWaveform(int start, int len) std::string Recognizer::acceptWaveform(int start, int len)
{ {
switch (vosk_recognizer_accept_waveform_f(rec, reinterpret_cast<float *>(start), len)) const char *res = nullptr;
switch (vosk_recognizer_accept_waveform_f(
rec,
reinterpret_cast<float *>(start),
len))
{ {
case 0: case 0:
return vosk_recognizer_partial_result(rec); res = vosk_recognizer_partial_result(rec);
break; break;
case 1: case 1:
return vosk_recognizer_result(rec); res = vosk_recognizer_result(rec);
break;
default:
res = "";
break;
} }
return nullptr;
return res ? std::string(res) : std::string();
} }
void Recognizer::reset() void Recognizer::reset()
{ {

View File

@@ -11,7 +11,7 @@ struct Recognizer
Recognizer(int index, float sampleRate, CommonModel *model); Recognizer(int index, float sampleRate, CommonModel *model);
Recognizer(int index, float sampleRate, CommonModel *model, CommonModel *spkModel); Recognizer(int index, float sampleRate, CommonModel *model, CommonModel *spkModel);
Recognizer(int index, float sampleRate, CommonModel *model, const std::string &grm, int); Recognizer(int index, float sampleRate, CommonModel *model, const std::string &grm, int);
const char *acceptWaveform(int start, int len); std::string acceptWaveform(int start, int len);
void reset(); void reset();
void setEndpointerMode(VoskEndpointerMode mode); void setEndpointerMode(VoskEndpointerMode mode);
void setEndpointerDelays(float tStartMax, float tEnd, float tMax); void setEndpointerDelays(float tStartMax, float tEnd, float tMax);

View File

@@ -7,6 +7,22 @@ if (ENVIRONMENT_IS_WEB) {
// 'var' to expose this outside the if // 'var' to expose this outside the if
var objs = []; var objs = [];
globalThis.__voskletObjs = objs;
const VOSKLET_DEBUG = false;
function voskLog(...args) {
if (VOSKLET_DEBUG) {
console.log(...args);
}
}
function voskWarn(...args) {
if (VOSKLET_DEBUG) {
console.warn(...args);
}
}
var events = ['status', 'partialResult', 'result']; var events = ['status', 'partialResult', 'result'];
let _cache = caches.open('Vosklet'); let _cache = caches.open('Vosklet');
let processorURL = URL.createObjectURL(new Blob(['(', (() => { let processorURL = URL.createObjectURL(new Blob(['(', (() => {
@@ -39,8 +55,31 @@ if (ENVIRONMENT_IS_WEB) {
delete() { delete() {
this.obj.delete(); this.obj.delete();
} }
static isGzip(bytes) {
return bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
}
static isTar(bytes) {
return (
bytes.length > 262 &&
bytes[257] === 0x75 &&
bytes[258] === 0x73 &&
bytes[259] === 0x74 &&
bytes[260] === 0x61 &&
bytes[261] === 0x72
);
}
static async gunzipArrayBuffer(buffer) {
const ds = new DecompressionStream('gzip');
const stream = new Response(buffer).body.pipeThrough(ds);
return await new Response(stream).arrayBuffer();
}
static async mk(url, storepath, id, normalMdl) { static async mk(url, storepath, id, normalMdl) {
let mdl = new CommonModel(); let mdl = new CommonModel();
let result = new Promise((resolve, reject) => { let result = new Promise((resolve, reject) => {
mdl.addEventListener('', ev => { mdl.addEventListener('', ev => {
if (!ev.detail) { if (!ev.detail) {
@@ -50,59 +89,151 @@ if (ENVIRONMENT_IS_WEB) {
else reject(ev.detail); else reject(ev.detail);
}, { once: true }); }, { once: true });
}); });
let cache = await caches.open('Vosklet');
let req = (await cache.keys(storepath, { ignoreSearch: true }))[0];
let res;
if (typeof req == 'undefined' || req.url.split('?')[1] != id) {
// Caching already handled explicitly const cache = await caches.open('Vosklet');
res = await fetch(url, { cache: 'no-store' }); const cacheKey = storepath + '?' + id;
if (!res.ok) throw 'Unable to fetch model, status: ' + res.status;
res = new Response(res.body.pipeThrough(new DecompressionStream('gzip'))); let res = await cache.match(cacheKey);
await cache.put(storepath + '?' + id, res.clone()); let tar;
if (res) {
tar = await res.arrayBuffer();
if (tar.byteLength === 0 || !CommonModel.isTar(new Uint8Array(tar))) {
voskWarn('Vosklet: kaputter Cache-Eintrag, lösche...');
await cache.delete(cacheKey);
tar = null;
}
} }
else res = await cache.match(req);
let tar = await res.arrayBuffer(); if (!tar) {
voskLog("Vosklet: fetch start");
let fetchRes = await fetch(url, { cache: 'no-store' });
voskLog("Vosklet: fetch ok", fetchRes.status);
if (!fetchRes.ok) {
throw 'Unable to fetch model, status: ' + fetchRes.status;
}
let buf = await fetchRes.arrayBuffer();
voskLog("Vosklet: arrayBuffer size", buf.byteLength);
let bytes = new Uint8Array(buf);
voskLog("Vosklet: gzip?", CommonModel.isGzip(bytes));
voskLog("Vosklet: tar?", CommonModel.isTar(bytes));
if (bytes.byteLength === 0) {
throw 'Vosklet: Modell-Download ergab 0 Bytes.';
}
if (CommonModel.isGzip(bytes)) {
buf = await CommonModel.gunzipArrayBuffer(buf);
bytes = new Uint8Array(buf);
}
if (!CommonModel.isTar(bytes)) {
throw 'Vosklet: Modell ist kein gültiges USTAR/TAR.';
}
voskLog("Vosklet: cache put start");
await cache.put(
cacheKey,
new Response(buf, {
headers: {
'Content-Type': 'application/x-tar',
'X-Vosklet-Format': 'tar',
'X-Vosklet-Model-Id': String(id)
}
})
);
voskLog("Vosklet: cache put done");
tar = buf;
}
voskLog("Vosklet: malloc start", tar.byteLength);
let tarStart = _malloc(tar.byteLength); let tarStart = _malloc(tar.byteLength);
voskLog("Vosklet: malloc done", tarStart);
voskLog("Vosklet: HEAPU8.set start");
HEAPU8.set(new Uint8Array(tar), tarStart); HEAPU8.set(new Uint8Array(tar), tarStart);
mdl.obj = new Module['CommonModel'](objs.length - 1, normalMdl, tarStart, tar.byteLength); voskLog("Vosklet: HEAPU8.set done");
voskLog("Vosklet: CommonModel constructor start");
mdl.obj = new Module['CommonModel'](
objs.length - 1,
normalMdl,
tarStart,
tar.byteLength
);
voskLog("Vosklet: CommonModel constructor returned");
return result; return result;
} }
} }
class Recognizer extends EventTarget { class Recognizer extends EventTarget {
constructor() { constructor() {
super(); super();
objs.push(this); objs.push(this);
this['acceptWaveform'] = (audioData) => {
let start = _malloc(audioData.length * 4);
HEAPF32.set(audioData, start / 4);
return this.obj['acceptWaveform'](start, audioData.length);
};
} }
acceptWaveform(audioData) {
let start = _malloc(audioData.length * 4);
HEAPF32.set(audioData, start / 4);
return UTF8ToString(this.obj['acceptWaveform'](start, audioData.length));
}
delete() { delete() {
this.obj.delete(); this.obj.delete();
} }
static async mk(model, sampleRate, mode, grammar, spkModel) { static async mk(model, sampleRate, mode, grammar, spkModel) {
let rec = new Recognizer(); let rec = new Recognizer();
let result = new Promise((resolve, reject) => { let result = new Promise((resolve, reject) => {
rec.addEventListener('', ev => { rec.addEventListener('', ev => {
if (!ev.detail) resolve(rec); if (!ev.detail) {
else reject(ev.detail); resolve(rec);
} else {
reject(ev.detail);
}
}, { once: true }); }, { once: true });
}) });
switch (mode) { switch (mode) {
case 1: case 1:
rec.obj = new Module['Recognizer'](objs.length - 1, sampleRate, model); rec.obj = new Module['Recognizer'](
objs.length - 1,
sampleRate,
model
);
break; break;
case 2: case 2:
rec.obj = new Module['Recognizer'](objs.length - 1, sampleRate, model, spkModel); rec.obj = new Module['Recognizer'](
objs.length - 1,
sampleRate,
model,
spkModel
);
break; break;
default: default:
rec.obj = new Module['Recognizer'](objs.length - 1, sampleRate, model, grammar, 0); rec.obj = new Module['Recognizer'](
objs.length - 1,
sampleRate,
model,
grammar,
0
);
} }
return result; return result;
} }
} }
Module = { Module = {
'getModelCache': () => _cache, 'getModelCache': () => _cache,

View File

@@ -37,17 +37,18 @@ VOSK=$(realpath vosk)
OPENFST=$(realpath openfst) OPENFST=$(realpath openfst)
OPENBLAS=$(realpath openblas) OPENBLAS=$(realpath openblas)
SHARED_FLAGS="-g0 -O3 -flto -msimd128 -matomics -mreference-types -mextended-const -msign-ext -mmutable-globals" #SHARED_FLAGS="-g0 -O3 -flto -msimd128 -matomics -mreference-types -mextended-const -msign-ext -mmutable-globals"
SHARED_FLAGS=" -msimd128 -matomics -mreference-types -mextended-const -msign-ext -mmutable-globals"
if [ ! -f "$OPENFST/lib/libfst.a" ]; then if [ ! -f "$OPENFST/lib/libfst.a" ]; then
rm -rf /tmp/openfst && rm -rf /tmp/openfst &&
wget https://www.openfst.org/twiki/pub/FST/FstDownload/openfst-1.8.4.tar.gz -O /tmp/openfst.tgz && wget https://www.openfst.org/twiki/pub/FST/FstDownload/openfst-1.8.4.tar.gz -O /tmp/openfst.tgz &&
mkdir /tmp/openfst && mkdir /tmp/openfst &&
tar -xzf /tmp/openfst.tgz -C /tmp/openfst --strip-component 1 && tar --no-same-owner -xzf /tmp/openfst.tgz -C /tmp/openfst --strip-component 1 &&
cd /tmp/openfst && cd /tmp/openfst &&
autoreconf -is && autoreconf -is &&
CXXFLAGS="$SHARED_FLAGS -O3 -fno-rtti" emconfigure ./configure --prefix="$OPENFST" --enable-static --disable-shared --enable-ngram-fsts --disable-bin && CXXFLAGS="$SHARED_FLAGS -O3 -fno-rtti" emconfigure ./configure --prefix="$OPENFST" --enable-static --disable-shared --enable-ngram-fsts --disable-bin &&
emmake make -j"$JOBS" install > /dev/null && emmake make -j"$JOBS" install
fi fi
if [ ! -f "$OPENBLAS/lib/libopenblas.a" ]; then if [ ! -f "$OPENBLAS/lib/libopenblas.a" ]; then
rm -rf /tmp/openblas && rm -rf /tmp/openblas &&
@@ -56,10 +57,10 @@ if [ ! -f "$OPENBLAS/lib/libopenblas.a" ]; then
git apply "$SRC"/OpenBLAS.patch && git apply "$SRC"/OpenBLAS.patch &&
# Change HOSTCC to the default C compiler on your machine # Change HOSTCC to the default C compiler on your machine
openblasFlags="CC=emcc HOSTCC=clang-20 TARGET=RISCV64_GENERIC USE_THREAD=0 NO_SHARED=1 BINARY=32 BUILD_SINGLE=1 BUILD_DOUBLE=1 BUILD_BFLOAT16=0 BUILD_COMPLEX16=0 BUILD_COMPLEX=0" openblasFlags="CC=emcc HOSTCC=clang-19 TARGET=RISCV64_GENERIC USE_THREAD=0 NO_SHARED=1 BINARY=32 BUILD_SINGLE=1 BUILD_DOUBLE=1 BUILD_BFLOAT16=0 BUILD_COMPLEX16=0 BUILD_COMPLEX=0"
openblasCFlags="$SHARED_FLAGS -fno-exceptions -fno-rtti -Wno-implicit-function-declaration -Wno-unused-function -Wno-unused-but-set-variable" openblasCFlags="$SHARED_FLAGS -fno-exceptions -fno-rtti -Wno-implicit-function-declaration -Wno-unused-function -Wno-unused-but-set-variable"
make $openblasFlags CFLAGS="$openblasCFlags" PREFIX="$OPENBLAS" -j"$JOBS" > /dev/null && make $openblasFlags CFLAGS="$openblasCFlags" PREFIX="$OPENBLAS" -j"$JOBS" > /dev/null &&
make $openblasFlags CFLAGS="$openblasCFlags" PREFIX="$OPENBLAS" -j"$JOBS" install && make $openblasFlags CFLAGS="$openblasCFlags" PREFIX="$OPENBLAS" -j"$JOBS" install
fi fi
if [ ! -f "$KALDI/src/kaldi.mk" ]; then if [ ! -f "$KALDI/src/kaldi.mk" ]; then
@@ -82,7 +83,7 @@ if [ ! -f "$VOSK/src/vosk.a" ]; then
fi fi
cd "$SRC" && cd "$SRC" &&
voskletFiles="Util.o CommonModel.o Recognizer.o Bindings.o" voskletFiles="Util.o CommonModel.o Recognizer.o Bindings.o FireEv.o"
voskletFlags="$SHARED_FLAGS -fno-rtti -sSTRICT -sWASM_WORKERS" voskletFlags="$SHARED_FLAGS -fno-rtti -sSTRICT -sWASM_WORKERS"
voskletLDFlags="-sWASMFS -sMODULARIZE -sTEXTDECODER=2 -sEVAL_CTORS=2 -sALLOW_UNIMPLEMENTED_SYSCALLS -sINITIAL_MEMORY=$INITIAL_MEMORY -sALLOW_MEMORY_GROWTH -sPOLYFILL=0 -sEXIT_RUNTIME=0 -sINVOKE_RUN=0 -sSUPPORT_LONGJMP=0 -sINCOMING_MODULE_JS_API=wasmMemory,instantiateWasm,wasm -sEXPORT_NAME=loadVosklet -sMALLOC=emmalloc -sENVIRONMENT=web,worker -L$KALDI/src -l:online2/kaldi-online2.a -l:decoder/kaldi-decoder.a -l:ivector/kaldi-ivector.a -l:gmm/kaldi-gmm.a -l:tree/kaldi-tree.a -l:feat/kaldi-feat.a -l:cudamatrix/kaldi-cudamatrix.a -l:lat/kaldi-lat.a -l:lm/kaldi-lm.a -l:rnnlm/kaldi-rnnlm.a -l:hmm/kaldi-hmm.a -l:nnet3/kaldi-nnet3.a -l:transform/kaldi-transform.a -l:matrix/kaldi-matrix.a -l:fstext/kaldi-fstext.a -l:util/kaldi-util.a -l:base/kaldi-base.a -L$OPENFST/lib -l:libfst.a -l:libfstngram.a -L$OPENBLAS -l:lib/libopenblas.a -L$VOSK/src -l:vosk.a -lembind --no-entry --closure 1 --pre-js Wrapper.js" voskletLDFlags="-sWASMFS -sMODULARIZE -sTEXTDECODER=2 -sEVAL_CTORS=2 -sALLOW_UNIMPLEMENTED_SYSCALLS -sINITIAL_MEMORY=$INITIAL_MEMORY -sALLOW_MEMORY_GROWTH -sPOLYFILL=0 -sEXIT_RUNTIME=0 -sINVOKE_RUN=0 -sSUPPORT_LONGJMP=0 -sINCOMING_MODULE_JS_API=wasmMemory,instantiateWasm,wasm -sEXPORT_NAME=loadVosklet -sMALLOC=emmalloc -sENVIRONMENT=web,worker -L$KALDI/src -l:online2/kaldi-online2.a -l:decoder/kaldi-decoder.a -l:ivector/kaldi-ivector.a -l:gmm/kaldi-gmm.a -l:tree/kaldi-tree.a -l:feat/kaldi-feat.a -l:cudamatrix/kaldi-cudamatrix.a -l:lat/kaldi-lat.a -l:lm/kaldi-lm.a -l:rnnlm/kaldi-rnnlm.a -l:hmm/kaldi-hmm.a -l:nnet3/kaldi-nnet3.a -l:transform/kaldi-transform.a -l:matrix/kaldi-matrix.a -l:fstext/kaldi-fstext.a -l:util/kaldi-util.a -l:base/kaldi-base.a -L$OPENFST/lib -l:libfst.a -l:libfstngram.a -L$OPENBLAS -l:lib/libopenblas.a -L$VOSK/src -l:vosk.a -lembind --no-entry --closure 1 --pre-js Wrapper.js"