Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ if (EMSCRIPTEN)
add_subdirectory(bench.wasm)
elseif(CMAKE_JS_VERSION)
add_subdirectory(addon.node)
add_subdirectory(stream.node)
else()
add_subdirectory(cli)
add_subdirectory(bench)
Expand Down
44 changes: 44 additions & 0 deletions examples/stream.node/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
cmake_minimum_required(VERSION 3.13)
project(stream_addon)

set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/../../cmake ${CMAKE_MODULE_PATH})

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_definitions(-DNAPI_VERSION=4)
include_directories(${CMAKE_JS_INC})

set(TARGET stream.node)

add_library(${TARGET} SHARED
addon.cpp
whisper-stream.cpp
)

set_target_properties(${TARGET} PROPERTIES
PREFIX ""
SUFFIX ".node"
)
include(DefaultTargetOptions)

execute_process(COMMAND node -p "require('node-addon-api').include"
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
OUTPUT_VARIABLE NODE_ADD_ON_API_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE)
string(REPLACE "\"" "" NODE_ADD_ON_API_DIR ${NODE_ADD_ON_API_DIR})
string(REPLACE "\\\\" "/" NODE_ADD_ON_API_DIR ${NODE_ADD_ON_API_DIR})
target_include_directories(${TARGET} PRIVATE ${NODE_ADD_ON_API_DIR})

target_link_libraries(${TARGET}
whisper
common
${CMAKE_THREAD_LIBS_INIT}
)

if(MSVC AND CMAKE_NODEJS_DEF AND CMAKE_JS_NODEJS_TARGET)
target_link_libraries(${TARGET} ${CMAKE_JS_NODEJS_TARGET})
elseif(CMAKE_JS_LIB)
execute_process(COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_JS_LIB} ${CMAKE_NODEJS_LIB_TARGET})
target_link_libraries(${TARGET} ${CMAKE_NODEJS_LIB_TARGET})
endif()
114 changes: 114 additions & 0 deletions examples/stream.node/addon.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#include "addon.h" // Your header file for WhisperStreamWrapper
#include "whisper-stream.h" // Your header file for the WhisperStream class

// NOTE: The N-API wrapper handles errors by throwing JS exceptions, so this macro is not needed.
// #define CHECK_STATUS(env, status, msg) ...

// --- Implementation of the Wrapper ---

Napi::Object WhisperStreamWrapper::Init(Napi::Env env, Napi::Object exports) {
Napi::Function func = DefineClass(env, "WhisperStream", {
InstanceMethod("startModel", &WhisperStreamWrapper::startModel),
InstanceMethod("processChunk", &WhisperStreamWrapper::ProcessChunk),
InstanceMethod("freeModel", &WhisperStreamWrapper::freeModel),
});

exports.Set("WhisperStream", func);
return exports;
}

WhisperStreamWrapper::WhisperStreamWrapper(const Napi::CallbackInfo& info) : Napi::ObjectWrap<WhisperStreamWrapper>(info) {
}

Napi::Value WhisperStreamWrapper::startModel(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();

if (info.Length() < 1 || !info[0].IsObject()) {
Napi::TypeError::New(env, "Expected a configuration object").ThrowAsJavaScriptException();
return env.Null();
}

Napi::Object js_params = info[0].As<Napi::Object>();
StreamParams params;

if (js_params.Has("modelPath")) {
params.model = js_params.Get("modelPath").As<Napi::String>();
} else {
Napi::TypeError::New(env, "Missing required parameter 'model'").ThrowAsJavaScriptException();
return env.Null();
}

if (js_params.Has("language")) params.language = js_params.Get("language").As<Napi::String>();
if (js_params.Has("nThreads")) params.n_threads = js_params.Get("nThreads").As<Napi::Number>();
if (js_params.Has("stepMs")) params.step_ms = js_params.Get("stepMs").As<Napi::Number>();
if (js_params.Has("lengthMs")) params.length_ms = js_params.Get("lengthMs").As<Napi::Number>();
if (js_params.Has("keepMs")) params.keep_ms = js_params.Get("keepMs").As<Napi::Number>();
if (js_params.Has("maxTokens")) params.max_tokens = js_params.Get("maxTokens").As<Napi::Number>();
if (js_params.Has("audioCtx")) params.audio_ctx = js_params.Get("audioCtx").As<Napi::Number>();
if (js_params.Has("vadThold")) params.vad_thold = js_params.Get("vadThold").As<Napi::Number>();
if (js_params.Has("beamSize")) params.beam_size = js_params.Get("beamSize").As<Napi::Number>();
if (js_params.Has("freqThold")) params.freq_thold = js_params.Get("freqThold").As<Napi::Number>();
if (js_params.Has("translate")) params.translate = js_params.Get("translate").As<Napi::Boolean>();
if (js_params.Has("noFallback")) params.no_fallback = js_params.Get("noFallback").As<Napi::Boolean>();
if (js_params.Has("printSpecial")) params.print_special = js_params.Get("printSpecial").As<Napi::Boolean>();
if (js_params.Has("noContext")) params.no_context = js_params.Get("noContext").As<Napi::Boolean>();
if (js_params.Has("noTimestamps")) params.no_timestamps = js_params.Get("noTimestamps").As<Napi::Boolean>();
if (js_params.Has("tinydiarize")) params.tinydiarize = js_params.Get("tinydiarize").As<Napi::Boolean>();
if (js_params.Has("saveAudio")) params.save_audio = js_params.Get("saveAudio").As<Napi::Boolean>();
if (js_params.Has("useGpu")) params.use_gpu = js_params.Get("useGpu").As<Napi::Boolean>();
if (js_params.Has("flashAttn")) params.flash_attn = js_params.Get("flashAttn").As<Napi::Boolean>();

if (this->whisperStream_) {
delete this->whisperStream_;
}

try {
this->whisperStream_ = new WhisperStream(params);
this->whisperStream_->init();
} catch (const std::runtime_error& e) {
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
return env.Null();
}

return env.Undefined();
}

Napi::Value WhisperStreamWrapper::ProcessChunk(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();

if (!this->whisperStream_) {
Napi::Error::New(env, "Model not started. Call startModel() first.").ThrowAsJavaScriptException();
return env.Null();
}

if (info.Length() < 1 || !info[0].IsTypedArray() || info[0].As<Napi::TypedArray>().TypedArrayType() != napi_float32_array) {
Napi::TypeError::New(env, "Argument must be a Float32Array").ThrowAsJavaScriptException();
return env.Null();
}

Napi::Float32Array pcmf32_array = info[0].As<Napi::Float32Array>();
std::vector<float> pcmf32_new(pcmf32_array.Data(), pcmf32_array.Data() + pcmf32_array.ElementLength());

TranscriptionResult result = this->whisperStream_->process(pcmf32_new);

Napi::Object resultObj = Napi::Object::New(env);
resultObj.Set("text", Napi::String::New(env, result.text));
resultObj.Set("isFinal", Napi::Boolean::New(env, result.final));

return resultObj;
}

Napi::Value WhisperStreamWrapper::freeModel(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (this->whisperStream_) {
delete this->whisperStream_;
this->whisperStream_ = nullptr;
}
return env.Undefined();
}

Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
return WhisperStreamWrapper::Init(env, exports);
}

NODE_API_MODULE(whisper, InitAll)
17 changes: 17 additions & 0 deletions examples/stream.node/addon.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#pragma once

#include <napi.h>
#include "whisper-stream.h"

class WhisperStreamWrapper : public Napi::ObjectWrap<WhisperStreamWrapper> {
public:
static Napi::Object Init(Napi::Env env, Napi::Object exports);
WhisperStreamWrapper(const Napi::CallbackInfo& info);

private:
Napi::Value startModel(const Napi::CallbackInfo& info);
Napi::Value ProcessChunk(const Napi::CallbackInfo& info);
Napi::Value freeModel(const Napi::CallbackInfo& info);

WhisperStream* whisperStream_ = nullptr;
};
83 changes: 83 additions & 0 deletions examples/stream.node/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const path = require('path');
const os = require('os');
const portAudio = require('naudiodon2');

const addonPath = path.join(__dirname, '..', '..', 'build', 'Release', 'stream.node');

const { WhisperStream } = require(addonPath);

const modelPath = path.join(__dirname, '..', '..', 'models', 'ggml-base.en.bin');
const SAMPLE_RATE = 16000;

// --- Main Application ---
async function main() {
const whisper = new WhisperStream();
let pendingText = ''; // Buffer for the current unconfirmed text

console.log('Loading model...');
whisper.startModel({
modelPath: modelPath,
language: 'en',
nThreads: 4,
stepMs: 3000,
lengthMs: 10000,
keepMs: 200,
useGpu: true,
});
console.log('Model loaded.');

const ai = new portAudio.AudioIO({
inOptions: {
channelCount: 1,
sampleFormat: portAudio.SampleFormatFloat32,
sampleRate: SAMPLE_RATE,
deviceId: -1,
closeOnError: true,
}
});

ai.on('data', (chunk) => {
const floatCount = chunk.length / Float32Array.BYTES_PER_ELEMENT;
const float32 = new Float32Array(chunk.buffer, chunk.byteOffset, floatCount);

try {
const result = whisper.processChunk(float32);
if (!result || !result.text) return;

const { text, isFinal } = result;

if (isFinal) {
process.stdout.write(`\r${text}\n`);
pendingText = ''; // Reset for the next utterance
} else {
pendingText = text;
// '\r' moves cursor to the start, '\x1B[K' clears the rest of the line.
process.stdout.write(`\r${pendingText}\x1B[K`);
}
} catch (err) {
console.error('Error during processing:', err);
}
});

ai.on('error', (err) => console.error('Audio input error:', err));

ai.start();
console.log('Recording from microphone. Speak now.');
process.stdout.write('> ');

const shutdown = () => {
console.log('\nShutting down...');
ai.quit(() => {
whisper.freeModel();
process.exit(0);
});
};

process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
}

main().catch((err) => {
console.error('An unexpected error occurred:', err);
process.exit(1);
});
16 changes: 16 additions & 0 deletions examples/stream.node/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "stream.node",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "node index_naudiodon.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"naudiodon2": "^2.3.6"
},
"devDependencies": {
"cmake-js": "^7.3.1",
"node-addon-api": "^5.1.0"
}
}
Loading