web-asr-core
Version:
WebASR Core - Browser-based speech processing with VAD, WakeWord and Whisper - Unified all-in-one version
317 lines • 13.1 kB
JavaScript
/**
* VAD(語音活動檢測)服務
*
* 提供無狀態的語音活動檢測服務,用於檢測音訊塊中的語音活動。
* 使用 Silero VAD v6 模型進行高精度語音檢測。
*
* @fileoverview VAD 語音活動檢測服務實現
* @author WebASRCore Team
*/
import { createSession, createTensor } from '../runtime/ort';
import { ConfigManager } from '../utils/config-manager';
import { ortService } from './ort';
/**
* VAD 事件發射器
*
* @description 用於發送 VAD 相關事件,外部可以監聽這些事件進行相應處理
* 事件類型:
* - 'speech-start': 語音開始 { timestamp: number }
* - 'speech-end': 語音結束 { timestamp: number }
* - 'processing-error': 處理錯誤 { error: Error, context: string }
*/
export const vadEvents = new EventTarget();
/**
* 載入 VAD 模型會話
*
* @description 從指定 URL 載入 Silero VAD v6 模型並建立 ONNX Runtime 會話
* @param modelUrl - VAD 模型的 URL 路徑(可選,預設使用 ConfigManager 設定)
* @param sessionOptions - 可選的會話配置選項
* @param config - 可選的配置管理器實例
* @returns Promise<InferenceSession> - ONNX Runtime 推理會話
* @throws Error - 當模型載入失敗時拋出錯誤
*
* @example
* ```typescript
* // 使用預設配置
* const session = await loadVadSession();
*
* // 使用自訂路徑
* const session = await loadVadSession('./models/custom_vad.onnx');
*
* // 使用自訂配置管理器
* const config = new ConfigManager();
* config.vad.modelPath = './models/my_vad.onnx';
* const session = await loadVadSession(undefined, undefined, config);
* ```
*/
export async function loadVadSession(modelUrl, sessionOptions, config) {
const cfg = config || ConfigManager.getInstance();
const url = modelUrl || cfg.vad.modelPath;
// 初始化 ORT 服務
await ortService.initialize();
// 如果啟用 Web Worker,預載入模型,指定為 vad 類型以使用 WebGPU
if (cfg.onnx.useWebWorker) {
await ortService.preloadModelInWorker('vad', url, 'vad');
}
// 使用優化的 ORT 服務創建會話,指定為 vad 類型以使用 WebGPU
return await ortService.createSession(url, sessionOptions, 'vad');
}
/**
* 創建初始 VAD 狀態
*
* @description 建立 VAD 處理所需的初始狀態,包括 LSTM 狀態和上下文樣本
* @param config - 可選的配置管理器實例
* @returns VadState - 初始化的 VAD 狀態物件
*
* @example
* ```typescript
* // 使用預設配置
* const vadState = createVadState();
* console.log(vadState.isSpeechActive); // false
*
* // 使用自訂配置
* const config = new ConfigManager();
* config.vad.contextSize = 128;
* const vadState = createVadState(config);
* ```
*/
export function createVadState(config) {
const cfg = config || new ConfigManager();
// Silero VAD v6 的 LSTM 狀態維度:[2, 1, 128]
// 第一個維度用於 h 和 c 狀態
const stateSize = 2 * 1 * 128;
return {
state: new Float32Array(stateSize), // 零初始化
contextSamples: new Float32Array(cfg.vad.contextSize), // 上下文樣本
hangoverCounter: 0, // 延遲計數器
isSpeechActive: false, // 語音活動狀態
};
}
/**
* 透過 VAD 處理音訊塊
*
* @description 使用 Silero VAD v6 模型處理單個音訊塊,檢測語音活動
* @param session - VAD 模型的 ONNX Runtime 會話
* @param prevState - 前一個 VAD 狀態
* @param audio - 音訊塊(Float32Array)- 應為 16kHz 的樣本
* @param params - VAD 參數配置
* @param config - 可選的配置管理器實例
* @returns Promise<VadResult> - 檢測結果和更新後的狀態
* @throws Error - 當處理失敗時拋出錯誤
*
* @example
* ```typescript
* const result = await processVad(session, vadState, audioChunk, vadParams);
* console.log(`語音檢測: ${result.detected}, 分數: ${result.score}`);
* vadState = result.state; // 更新狀態
* ```
*/
export async function processVad(session, prevState, audio, params, config) {
const cfg = config || ConfigManager.getInstance();
try {
// 如果啟用 Web Worker,使用 Worker 執行推理
if (cfg.onnx.useWebWorker) {
try {
// 準備完整的輸入數據(上下文 + 新音訊)
const windowSize = cfg.vad.windowSize;
const contextSize = cfg.vad.contextSize;
const effectiveWindowSize = windowSize + contextSize;
const fullInput = new Float32Array(effectiveWindowSize);
fullInput.set(prevState.contextSamples, 0); // 前 64 個上下文樣本
fullInput.set(audio.slice(0, windowSize), contextSize); // 當前 512 個音訊樣本
const result = await ortService.runInferenceInWorker('vad', 'vad', cfg.vad.modelPath, fullInput);
// 檢查 Worker 是否返回有效結果
if (!result || !result.result || result.error) {
throw new Error(`Worker inference failed: ${result?.error || 'Invalid result'}`);
}
// 更新狀態
const newContextSamples = new Float32Array(cfg.vad.contextSize);
const startIdx = cfg.vad.windowSize - cfg.vad.contextSize;
newContextSamples.set(audio.slice(startIdx, startIdx + cfg.vad.contextSize));
let isSpeechActive = prevState.isSpeechActive;
let hangoverCounter = prevState.hangoverCounter;
if (result.result.isSpeech) {
// 檢測語音開始事件
if (!prevState.isSpeechActive) {
vadEvents.dispatchEvent(new CustomEvent('speech-start', {
detail: { timestamp: Date.now() }
}));
}
isSpeechActive = true;
hangoverCounter = params.hangoverFrames;
}
else if (isSpeechActive) {
hangoverCounter -= 1;
if (hangoverCounter <= 0) {
// 檢測語音結束事件
vadEvents.dispatchEvent(new CustomEvent('speech-end', {
detail: { timestamp: Date.now() }
}));
isSpeechActive = false;
}
}
const state = {
state: prevState.state, // Worker 內部管理狀態
contextSamples: newContextSamples,
hangoverCounter,
isSpeechActive
};
return {
detected: result.result.isSpeech,
score: result.result.probability,
state
};
}
catch (error) {
console.warn('[VAD] Worker inference failed, falling back to main thread:', error);
// 發出處理錯誤事件
vadEvents.dispatchEvent(new CustomEvent('processing-error', {
detail: {
error: error,
context: 'worker-inference'
}
}));
// 如果 Worker 失敗,繼續使用主執行緒
}
}
// Silero VAD v6 模型輸入規格:
// - input: [1, 576] (64 個上下文樣本 + 512 個新樣本)
// - state: [2, 1, 128] (LSTM 狀態)
// - sr: [1] (採樣率,int64 格式)
const windowSize = cfg.vad.windowSize; // 時間視窗(預設 512 = 32ms @ 16kHz)
const contextSize = cfg.vad.contextSize; // 上下文視窗(預設 64 = 4ms)
const effectiveWindowSize = windowSize + contextSize; // 總計樣本數
// 準備模型輸入:組合上下文樣本與當前音訊塊
const inputData = new Float32Array(effectiveWindowSize);
inputData.set(prevState.contextSamples, 0); // 設置前 64 個上下文樣本
inputData.set(audio.slice(0, windowSize), contextSize); // 設置當前 512 個音訊樣本
// 建立輸入張量 "input": [1, 576] (float32)
const inputTensor = createTensor('float32', inputData, [1, effectiveWindowSize]);
// 建立狀態張量 "state": [2, 1, 128] (float32)
const stateTensor = createTensor('float32', prevState.state, [2, 1, 128]);
// 建立採樣率張量 "sr": [1] (int64) - 使用 BigInt64Array
const srTensor = createTensor('int64', new BigInt64Array([BigInt(params.sampleRate)]), [1]);
// 組織模型輸入參數
const feeds = {
input: inputTensor,
state: stateTensor,
sr: srTensor,
};
// 執行 ONNX 模型推論
const results = await session.run(feeds);
// 提取語音檢測分數輸出
const outputData = results.output;
const score = outputData.data[0];
// 提取更新後的 LSTM 狀態 (stateN)
const stateN = results.stateN;
const newState = new Float32Array(stateN.data);
// 保存音訊塊尾部 64 個樣本作為下次處理的上下文
const newContextSamples = new Float32Array(contextSize);
const startIdx = windowSize - contextSize; // 計算起始索引:512 - 64 = 448
newContextSamples.set(audio.slice(startIdx, startIdx + contextSize));
// 判斷語音活動狀態
let isSpeechActive = prevState.isSpeechActive;
let hangoverCounter = prevState.hangoverCounter;
const vadDetected = score > params.threshold;
if (vadDetected) {
// 檢測到語音 - 激活狀態並重置延遲計數器
if (!prevState.isSpeechActive) {
// 發出語音開始事件
vadEvents.dispatchEvent(new CustomEvent('speech-start', {
detail: { timestamp: Date.now() }
}));
}
isSpeechActive = true;
hangoverCounter = params.hangoverFrames;
}
else if (isSpeechActive) {
// 未檢測到語音但仍處於活動狀態 - 遞減延遲計數器
hangoverCounter -= 1;
if (hangoverCounter <= 0) {
// 發出語音結束事件
vadEvents.dispatchEvent(new CustomEvent('speech-end', {
detail: { timestamp: Date.now() }
}));
isSpeechActive = false;
}
}
// 返回檢測結果與更新的狀態
const state = {
state: newState,
contextSamples: newContextSamples,
hangoverCounter,
isSpeechActive
};
return {
detected: vadDetected,
score,
state
};
}
catch (error) {
// 發出處理錯誤事件
vadEvents.dispatchEvent(new CustomEvent('processing-error', {
detail: {
error: error,
context: 'processVad'
}
}));
throw error; // 重新拋出錯誤以保持原有行為
}
}
/**
* 批次處理多個音訊塊的輔助函數
*
* @description 依序處理多個音訊塊,並維護狀態的連續性
* @param session - VAD 模型的 ONNX Runtime 會話
* @param chunks - 要處理的音訊塊陣列
* @param initialState - 初始 VAD 狀態
* @param params - VAD 參數配置
* @param config - 可選的配置管理器實例
* @returns Promise<VadResult[]> - 每個音訊塊對應的檢測結果陣列
*
* @example
* ```typescript
* const chunks = [chunk1, chunk2, chunk3];
* const results = await processVadChunks(session, chunks, vadState, vadParams);
* console.log(`處理了 ${results.length} 個音訊塊`);
* ```
*/
export async function processVadChunks(session, chunks, initialState, params, config) {
const results = [];
let state = initialState;
// 依序處理每個音訊塊,保持狀態連續性
for (const chunk of chunks) {
const result = await processVad(session, state, chunk, params, config);
results.push(result);
state = result.state; // 更新狀態以供下一個塊使用
}
return results;
}
/**
* 創建預設的 VAD 參數
*
* @description 從 ConfigManager 創建預設的 VAD 參數配置
* @param config - 可選的配置管理器實例
* @returns VadParams - VAD 參數配置
*
* @example
* ```typescript
* // 使用預設配置
* const params = createDefaultVadParams();
*
* // 使用自訂配置
* const config = new ConfigManager();
* config.vad.threshold = 0.6;
* const params = createDefaultVadParams(config);
* ```
*/
export function createDefaultVadParams(config) {
const cfg = config || new ConfigManager();
return {
threshold: cfg.vad.threshold,
hangoverFrames: cfg.vad.hangoverFrames,
sampleRate: cfg.vad.sampleRate,
};
}
//# sourceMappingURL=vad.js.map