Skip to content
Draft
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
123 changes: 123 additions & 0 deletions samples/js/automatic_speech_recognition/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Automatic speech recognition sample (JavaScript)

This example showcases inference of speech recognition models. The application doesn't have many configuration options to encourage the reader to explore and modify the source code. For example, change the device for inference to GPU. The sample features `ASRPipeline` and uses an audio file in wav format as an input source. Audio conversion is performed by a custom helper in `wav_utils.js` (PCM16 mono/stereo at 16 kHz) to align numerical behavior with the C++ and Python sample paths.

`ASRPipeline` dispatches to the model implementation based on the model's `config.json` (for example Whisper or Qwen3-ASR).

## Download and convert the model and tokenizers

The `--upgrade-strategy eager` option is needed to ensure `optimum-intel` is upgraded to the latest version.

It's not required to install [../../export-requirements.txt](../../export-requirements.txt) for deployment if the model has already been exported.

```sh
pip install --upgrade-strategy eager -r <GENAI_ROOT_DIR>/samples/requirements.txt
optimum-cli export openvino --trust-remote-code --model openai/whisper-base whisper-base
```

## Prepare audio file

Prepare audio file in wav format with sampling rate 16k Hz.

You can download example audio file: https://storage.openvinotoolkit.org/models_contrib/speech/2021.2/librispeech_s5/how_are_you_doing_today.wav

## Run

From the `samples/js` directory, install dependencies (if not already done):

```bash
npm install
```

If you use the master branch, you may need to [build openvino-genai-node from source](../../src/js/README.md#build-bindings) first.

Run the sample:

```bash
node automatic_speech_recognition/automatic_speech_recognition.js whisper-base how_are_you_doing_today.wav
```

Optional third argument is the device (default: CPU):

```bash
node automatic_speech_recognition/automatic_speech_recognition.js whisper-base how_are_you_doing_today.wav GPU
```

Output:

```
How are you doing today?
timestamps: [0.00, 2.00] text: How are you doing today?
[0.00, 0.xx]:
[0.xx, 0.xx]: How
...
```

Refer to the [Supported Models](https://openvinotoolkit.github.io/openvino.genai/docs/supported-models/#speech-recognition-models-whisper-based) for more details.

# ASR pipeline usage

```javascript
import { ASRPipeline } from 'openvino-genai-node';
import { readFileSync } from 'node:fs';
import { decode } from 'node-wav';

const pipeline = await ASRPipeline(modelDir, "CPU");
const rawSpeechBuffer = readFileSync(audioFilePath);
const rawSpeech = decode(rawSpeechBuffer).channelData[0];
const result = await pipeline.generate(rawSpeech);
console.log(result.texts[0]);
// How are you doing today?
```

### Transcription

Multilingual models predict the language of the source audio automatically.

If the source audio language is known in advance, it can be specified in the generation config:

```javascript
// In the form of "<|en|>" for Whisper models, "English" for Qwen3-ASR models.
const generationConfig = { language: "<|en|>", task: "transcribe" };
const result = await pipeline.generate(rawSpeech, { generationConfig });
```

### Translation

By default, Whisper performs the task of speech transcription, where the source audio language is the same as the target text language. To perform speech translation, where the target text is in English, set the task to "translate":

```javascript
const generationConfig = { task: "translate" };
const result = await pipeline.generate(rawSpeech, { generationConfig });
```

### Timestamps prediction

The model can predict timestamps. For sentence-level timestamps, pass the `return_timestamps` argument. The result exposes `chunks` grouped per input, so use `chunks[0]` for a single audio input:

```javascript
const generationConfig = { return_timestamps: true, language: "<|en|>", task: "transcribe" };
const result = await pipeline.generate(rawSpeech, { generationConfig });
for (const chunk of result.chunks?.[0] ?? []) {
console.log(`timestamps: [${chunk.startTs.toFixed(2)}, ${chunk.endTs.toFixed(2)}] text: ${chunk.text}`);
}
```

### Word-level timestamps

Word-level timestamps are supported by Whisper models. Pass `word_timestamps: true` in the pipeline constructor, then in the generation config:

```javascript
const pipeline = await ASRPipeline(modelDir, "CPU", { word_timestamps: true });
const generationConfig = { return_timestamps: true, word_timestamps: true, language: "<|en|>", task: "transcribe" };
const result = await pipeline.generate(rawSpeech, { generationConfig });
for (const word of result.words?.[0] ?? []) {
console.log(`[${word.startTs.toFixed(2)}, ${word.endTs.toFixed(2)}]: ${word.text}`);
}
```

### Initial prompt and hotwords

The ASR pipeline has `initial_prompt` and `hotwords` generate arguments for Whisper models:
* `initial_prompt`: initial prompt tokens passed as a previous transcription (after `<|startofprev|>` token) to the first processing window
* `hotwords`: hotwords tokens passed as a previous transcription (after `<|startofprev|>` token) to all processing windows
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (C) 2023-2026 Intel Corporation
// SPDX-License-Identifier: Apache-2.0

import { basename } from 'node:path';
import yargs from 'yargs/yargs';
import { hideBin } from 'yargs/helpers';
import { ASRPipeline } from 'openvino-genai-node';
import { readAudio } from './wav_utils.js';

/**
* Parse CLI arguments, run ASR inference and print transcription output.
* @returns {Promise<void>}
*/
async function main() {
const argv = yargs(hideBin(process.argv))
.scriptName(basename(process.argv[1]))
.command(
'$0 <model_dir> <audio_file> [device]',
'Run automatic speech recognition on an audio file',
(yargsBuilder) =>
yargsBuilder
.positional('model_dir', {
type: 'string',
describe: 'Path to the converted ASR model directory',
demandOption: true,
})
.positional('audio_file', {
type: 'string',
describe: 'Path to the WAV audio file',
demandOption: true,
})
.positional('device', {
type: 'string',
describe: 'Device to run the model on (e.g. CPU, GPU)',
default: 'CPU',
}),
)
.strict()
.help()
.parse();

const modelDir = argv.model_dir;
const wavFilePath = argv.audio_file;
const device = argv.device;

const properties = {};
if (device === 'NPU' || device.startsWith('GPU')) {
// Cache compiled models on disk for GPU and NPU to save time on the next run.
properties['CACHE_DIR'] = 'asr_cache';
}
// Word timestamps supported by Whisper models only.
// Must be passed to the ASRPipeline constructor as a property.
properties.word_timestamps = true;

const pipeline = await ASRPipeline(modelDir, device, properties);

// If the language is known in advance it can be specified in the generation config.
// In the form of "<|en|>" for Whisper models. Supported by multilingual models only.
// In the form of "English" for Qwen3-ASR models.
// Whisper model parameters (task, return_timestamps, word_timestamps) are ignored for Qwen3-ASR models.
const generationConfig = {
language: '<|en|>',
task: 'transcribe',
return_timestamps: true,
word_timestamps: true,
};

// Pipeline expects normalized audio with a sample rate of 16 kHz.
const audioTensor = await readAudio(wavFilePath);
const result = await pipeline.generate(audioTensor, { generationConfig });

console.log(result.texts?.[0] ?? '');

if (result.chunks?.[0]?.length) {
for (const chunk of result.chunks[0]) {
console.log(`timestamps: [${chunk.startTs.toFixed(2)}, ${chunk.endTs.toFixed(2)}] text: ${chunk.text}`);
}
}

if (result.words?.[0]?.length) {
for (const word of result.words[0]) {
console.log(`[${word.startTs.toFixed(2)}, ${word.endTs.toFixed(2)}]: ${word.text}`);
}
}
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
102 changes: 102 additions & 0 deletions samples/js/automatic_speech_recognition/wav_utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (C) 2023-2026 Intel Corporation
// SPDX-License-Identifier: Apache-2.0

import { readFile } from 'node:fs/promises';

function parseWavPcm16Mono(buffer) {
if (buffer.length < 44) {
throw new Error('Invalid WAV payload: file is too small.');
}

if (buffer.toString('ascii', 0, 4) !== 'RIFF' || buffer.toString('ascii', 8, 12) !== 'WAVE') {
throw new Error('Invalid WAV payload: RIFF/WAVE header is missing.');
}

const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
let offset = 12;

let audioFormat;
let channels;
let sampleRate;
let bitsPerSample;
let dataOffset;
let dataSize;

while (offset + 8 <= buffer.length) {
const chunkId = buffer.toString('ascii', offset, offset + 4);
const chunkSize = view.getUint32(offset + 4, true);
const chunkDataOffset = offset + 8;

if (chunkDataOffset + chunkSize > buffer.length) {
throw new Error('Invalid WAV payload: malformed chunk size.');
}

if (chunkId === 'fmt ') {
if (chunkSize < 16) {
throw new Error('Invalid WAV payload: fmt chunk is too small.');
}
audioFormat = view.getUint16(chunkDataOffset, true);
channels = view.getUint16(chunkDataOffset + 2, true);
sampleRate = view.getUint32(chunkDataOffset + 4, true);
bitsPerSample = view.getUint16(chunkDataOffset + 14, true);
} else if (chunkId === 'data') {
dataOffset = chunkDataOffset;
dataSize = chunkSize;
}

offset = chunkDataOffset + chunkSize + (chunkSize % 2);
}

if (audioFormat !== 1) {
throw new Error('Unsupported WAV format: only PCM is supported.');
}

if (channels !== 1 && channels !== 2) {
throw new Error('WAV file must be mono or stereo.');
}

if (sampleRate !== 16000) {
throw new Error(`WAV file must be 16 kHz, but got ${sampleRate}.`);
}

if (bitsPerSample !== 16) {
throw new Error(`Unsupported WAV bit depth: ${bitsPerSample}. Only 16-bit PCM is supported.`);
}

if (dataOffset === undefined || dataSize === undefined) {
throw new Error('Invalid WAV payload: missing data chunk.');
}

const bytesPerFrame = channels * 2;
const frameCount = Math.floor(dataSize / bytesPerFrame);
const mono = new Float32Array(frameCount);

for (let index = 0; index < frameCount; index++) {
const frameOffset = dataOffset + index * bytesPerFrame;
if (channels === 1) {
const sample = view.getInt16(frameOffset, true);
mono[index] = sample / 32768.0;
} else {
const left = view.getInt16(frameOffset, true);
const right = view.getInt16(frameOffset + 2, true);
mono[index] = (left + right) / 65536.0;
}
}

return mono;
}

/**
* Read WAV file and convert to 16kHz mono Float32Array for the ASR pipeline.
* @param {string} audioPath
* @returns {Promise<Float32Array>}
*/
export async function readAudio(audioPath) {
const wavBuffer = await readFile(audioPath);

if (wavBuffer.length === 0) {
throw new Error('Audio file is empty.');
}

return parseWavPcm16Mono(wavBuffer);
}
6 changes: 5 additions & 1 deletion site/docs/bindings/node-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description: Node.js bindings provide JavaScript/TypeScript API.
OpenVINO GenAI provides Node.js bindings that enable you to use generative AI pipelines in JavaScript and TypeScript applications.

:::warning API Coverage
Node.js bindings currently provide a subset of the full OpenVINO GenAI API available in C++ and Python. The focus is on core text generation (`LLMPipeline`), vision language models (`VLMPipeline`), text embedding (`TextEmbeddingPipeline`), text reranking (`TextRerankPipeline`), speech recognition (`WhisperPipeline`), speech generation (`Text2SpeechPipeline`), and image generation (`Text2ImagePipeline`, `Image2ImagePipeline`, `InpaintingPipeline`) functionality.
Node.js bindings currently provide a subset of the full OpenVINO GenAI API available in C++ and Python. The focus is on core text generation (`LLMPipeline`), vision language models (`VLMPipeline`), text embedding (`TextEmbeddingPipeline`), text reranking (`TextRerankPipeline`), speech recognition (`WhisperPipeline`, `ASRPipeline`), speech generation (`Text2SpeechPipeline`), and image generation (`Text2ImagePipeline`, `Image2ImagePipeline`, `InpaintingPipeline`) functionality.
:::

## Supported Pipelines and Features
Expand All @@ -33,6 +33,10 @@ Node.js bindings currently support:
- `WhisperPipeline`: Speech recognition from audio input
- Streaming support
- Timestamps extraction
- `ASRPipeline`: Automatic speech recognition dispatching to Whisper or Qwen3-ASR models
- Streaming support
- Segment- and word-level timestamps extraction
- Detected language reporting
- `Text2SpeechPipeline`: Speech generation from text
- Optional speaker embedding support
- Batch generation
Expand Down
2 changes: 2 additions & 0 deletions src/js/include/addon.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ struct AddonData {
Napi::FunctionReference vlm_pipeline;
Napi::FunctionReference text_rerank_pipeline;
Napi::FunctionReference whisper_pipeline;
Napi::FunctionReference asr_pipeline;
Napi::FunctionReference text2image_pipeline;
Napi::FunctionReference image2image_pipeline;
Napi::FunctionReference inpainting_pipeline;
Napi::FunctionReference tokenizer;
Napi::FunctionReference perf_metrics;
Napi::FunctionReference vlm_perf_metrics;
Napi::FunctionReference whisper_perf_metrics;
Napi::FunctionReference asr_perf_metrics;
Napi::FunctionReference text2speech_pipeline;
Napi::FunctionReference text2speech_perf_metrics;
Napi::FunctionReference text2image_perf_metrics;
Expand Down
31 changes: 31 additions & 0 deletions src/js/include/asr_pipeline/init_worker.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (C) 2023-2026 Intel Corporation
// SPDX-License-Identifier: Apache-2.0

#pragma once

#include <atomic>
#include <filesystem>
#include <napi.h>

#include "openvino/genai/automatic_speech_recognition/pipeline.hpp"

class ASRInitWorker : public Napi::AsyncWorker {
public:
ASRInitWorker(Napi::Function& callback,
std::shared_ptr<ov::genai::ASRPipeline>& pipe,
std::shared_ptr<std::atomic<bool>> is_initializing,
std::filesystem::path model_path,
std::string device,
ov::AnyMap properties);
virtual ~ASRInitWorker() {}
void Execute() override;
void OnOK() override;
void OnError(const Napi::Error& e) override;

private:
std::shared_ptr<ov::genai::ASRPipeline>& pipe;
std::shared_ptr<std::atomic<bool>> is_initializing;
std::filesystem::path model_path;
std::string device;
ov::AnyMap properties;
};
Loading
Loading