voice-to-text-converter
Version:
A modern, lightweight Node.js package for speech-to-text conversion with support for multiple engines
459 lines • 17.1 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.VoskEngine = void 0;
const base_engine_1 = require("./base-engine");
const types_1 = require("../types");
const audio_1 = require("../utils/audio");
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
/**
* Vosk engine for offline speech recognition
*/
class VoskEngine extends base_engine_1.BaseSpeechRecognitionEngine {
constructor(modelPath) {
super();
this.vosk = null;
this.model = null;
this.recognizer = null;
this.modelPath = null;
this.modelPath = modelPath || null;
}
/**
* Check if Vosk is available
*/
isAvailable() {
try {
// Try to require vosk module
require.resolve('vosk');
return true;
}
catch {
return false;
}
}
/**
* Get supported languages based on available models
*/
getSupportedLanguages() {
// This would typically scan available model directories
// For now, return common languages that have Vosk models
return [
'en-US', 'en-GB', 'en-IN',
'es-ES', 'es-MX',
'fr-FR',
'de-DE',
'it-IT',
'pt-BR',
'ru-RU',
'ja-JP',
'ko-KR',
'zh-CN',
'ar-SA',
'hi-IN',
'tr-TR',
'pl-PL',
'nl-NL',
'sv-SE',
'da-DK',
'no-NO',
'fi-FI'
];
}
/**
* Initialize Vosk module and model
*/
async initializeVosk(modelPath) {
if (!this.isAvailable()) {
throw new Error('Vosk module is not installed. Install it with: npm install vosk');
}
try {
// Dynamic import of vosk module
this.vosk = require('vosk');
this.vosk.setLogLevel(-1); // Disable logging
// Use provided model path or default
const resolvedModelPath = modelPath || this.modelPath;
if (!resolvedModelPath) {
throw new Error('Model path must be provided for Vosk engine');
}
if (!fs.existsSync(resolvedModelPath)) {
throw new Error(`Vosk model not found at: ${resolvedModelPath}`);
}
// Load model
this.model = new this.vosk.Model(resolvedModelPath);
}
catch (error) {
throw new Error(`Failed to initialize Vosk: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Start speech recognition
*/
async start(audioConfig, recognitionConfig) {
this.validateAudioConfig(audioConfig);
this.config = this.getConfig(recognitionConfig);
this.validateConfig(this.config);
if (this.isRecording) {
await this.stop();
}
try {
await this.initializeVosk();
if (!this.model || !this.vosk) {
throw new Error('Vosk not properly initialized');
}
// Create recognizer
this.recognizer = new this.vosk.KaldiRecognizer(this.model, this.config.sampleRate || 16000);
this.setRecordingState(true);
this.emit('start');
if (audioConfig.source === 'microphone') {
await this.startMicrophoneRecognition(audioConfig);
}
else if (audioConfig.source === 'file') {
await this.processAudioFile(audioConfig.filePath);
}
else if (audioConfig.source === 'stream') {
await this.processAudioStream(audioConfig.audioStream);
}
}
catch (error) {
this.emitError(types_1.SpeechRecognitionErrorType.ENGINE_ERROR, `Failed to start Vosk recognition: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
}
}
/**
* Start microphone recognition
*/
async startMicrophoneRecognition(audioConfig) {
try {
// Use node-record-lpcm16 for microphone recording
const recorder = require('node-record-lpcm16');
const recordingOptions = {
sampleRate: this.config.sampleRate || 16000,
channels: 1,
audioType: 'raw',
device: audioConfig.deviceId || null
};
const recording = recorder.record(recordingOptions);
this.emit('audiostart');
recording.stream().on('data', (data) => {
if (this.recognizer && this.isRecording) {
const hasResult = this.recognizer.acceptWaveform(data);
if (hasResult) {
const result = JSON.parse(this.recognizer.result());
if (result.text) {
this.emitResult({
transcript: result.text,
confidence: result.confidence || 1.0,
isFinal: true
});
}
}
else if (this.config.interimResults) {
const partialResult = JSON.parse(this.recognizer.partialResult());
if (partialResult.partial) {
this.emitResult({
transcript: partialResult.partial,
confidence: 0.5,
isFinal: false
});
}
}
}
});
recording.stream().on('error', (error) => {
this.emitError(types_1.SpeechRecognitionErrorType.AUDIO_CAPTURE, `Microphone recording error: ${error.message}`, error);
});
// Stop recording after specified duration
if (audioConfig.duration) {
setTimeout(() => {
recording.stop();
this.stop().catch(console.error);
}, audioConfig.duration);
}
// Handle recording end
recording.stream().on('end', () => {
if (this.recognizer) {
const finalResult = JSON.parse(this.recognizer.finalResult());
if (finalResult.text) {
this.emitResult({
transcript: finalResult.text,
confidence: finalResult.confidence || 1.0,
isFinal: true
});
}
}
this.emit('audioend');
this.emit('end');
this.setRecordingState(false);
});
}
catch (error) {
this.emitError(types_1.SpeechRecognitionErrorType.AUDIO_CAPTURE, `Failed to start microphone recording: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
}
}
/**
* Process audio file
*/
async processAudioFile(filePath) {
try {
(0, audio_1.validateAudioFile)(filePath);
const stream = (0, audio_1.createAudioFileStream)(filePath);
await this.processAudioStream(stream);
}
catch (error) {
this.emitError(types_1.SpeechRecognitionErrorType.ENGINE_ERROR, `Failed to process audio file: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
}
}
/**
* Process audio stream
*/
async processAudioStream(stream) {
return new Promise((resolve, reject) => {
if (!this.recognizer) {
reject(new Error('Recognizer not initialized'));
return;
}
this.emit('audiostart');
stream.on('data', (chunk) => {
if (this.recognizer && this.isRecording) {
const hasResult = this.recognizer.acceptWaveform(chunk);
if (hasResult) {
const result = JSON.parse(this.recognizer.result());
if (result.text) {
this.emitResult({
transcript: result.text,
confidence: result.confidence || 1.0,
isFinal: true
});
}
}
else if (this.config.interimResults) {
const partialResult = JSON.parse(this.recognizer.partialResult());
if (partialResult.partial) {
this.emitResult({
transcript: partialResult.partial,
confidence: 0.5,
isFinal: false
});
}
}
}
});
stream.on('end', () => {
if (this.recognizer) {
const finalResult = JSON.parse(this.recognizer.finalResult());
if (finalResult.text) {
this.emitResult({
transcript: finalResult.text,
confidence: finalResult.confidence || 1.0,
isFinal: true
});
}
}
this.emit('audioend');
this.emit('end');
this.setRecordingState(false);
resolve();
});
stream.on('error', (error) => {
this.emitError(types_1.SpeechRecognitionErrorType.ENGINE_ERROR, `Stream processing error: ${error.message}`, error);
reject(error);
});
});
}
/**
* Stop speech recognition
*/
async stop() {
this.setRecordingState(false);
if (this.recognizer) {
try {
// Get final result before stopping
const finalResult = JSON.parse(this.recognizer.finalResult());
if (finalResult.text) {
this.emitResult({
transcript: finalResult.text,
confidence: finalResult.confidence || 1.0,
isFinal: true
});
}
}
catch (error) {
console.error('Error getting final result:', error);
}
}
}
/**
* Abort speech recognition
*/
async abort() {
this.setRecordingState(false);
this.cleanup();
}
/**
* Process audio file directly
*/
async processFile(filePath, config) {
const results = [];
await this.initializeVosk();
if (!this.model || !this.vosk) {
throw new Error('Vosk not properly initialized');
}
const mergedConfig = this.getConfig(config);
const recognizer = new this.vosk.KaldiRecognizer(this.model, mergedConfig.sampleRate || 16000);
try {
(0, audio_1.validateAudioFile)(filePath);
const stream = (0, audio_1.createAudioFileStream)(filePath);
return new Promise((resolve, reject) => {
stream.on('data', (chunk) => {
const hasResult = recognizer.acceptWaveform(chunk);
if (hasResult) {
const result = JSON.parse(recognizer.result());
if (result.text) {
results.push({
transcript: result.text,
confidence: result.confidence || 1.0,
isFinal: true
});
}
}
});
stream.on('end', () => {
const finalResult = JSON.parse(recognizer.finalResult());
if (finalResult.text) {
results.push({
transcript: finalResult.text,
confidence: finalResult.confidence || 1.0,
isFinal: true
});
}
recognizer.free();
resolve(results);
});
stream.on('error', reject);
});
}
catch (error) {
recognizer.free();
throw error;
}
}
/**
* Process audio stream directly
*/
async processStream(stream, config) {
const results = [];
await this.initializeVosk();
if (!this.model || !this.vosk) {
throw new Error('Vosk not properly initialized');
}
const mergedConfig = this.getConfig(config);
const recognizer = new this.vosk.KaldiRecognizer(this.model, mergedConfig.sampleRate || 16000);
return new Promise((resolve, reject) => {
stream.on('data', (chunk) => {
const hasResult = recognizer.acceptWaveform(chunk);
if (hasResult) {
const result = JSON.parse(recognizer.result());
if (result.text) {
results.push({
transcript: result.text,
confidence: result.confidence || 1.0,
isFinal: true
});
}
}
});
stream.on('end', () => {
const finalResult = JSON.parse(recognizer.finalResult());
if (finalResult.text) {
results.push({
transcript: finalResult.text,
confidence: finalResult.confidence || 1.0,
isFinal: true
});
}
recognizer.free();
resolve(results);
});
stream.on('error', (error) => {
recognizer.free();
reject(error);
});
});
}
/**
* Clean up resources
*/
cleanup() {
if (this.recognizer) {
try {
this.recognizer.free();
}
catch (error) {
console.error('Error freeing recognizer:', error);
}
this.recognizer = null;
}
this.model = null;
this.vosk = null;
super.cleanup();
}
/**
* Set model path
*/
setModelPath(modelPath) {
this.modelPath = modelPath;
}
/**
* Get current model path
*/
getModelPath() {
return this.modelPath;
}
/**
* Download and setup Vosk model (helper method)
*/
static async downloadModel(language, modelSize = 'small') {
// This would implement model downloading logic
// For now, just return expected path
const modelName = `vosk-model-${language}-${modelSize}`;
const modelPath = path.join(process.cwd(), 'models', modelName);
if (!fs.existsSync(modelPath)) {
throw new Error(`Model not found at ${modelPath}. Please download it manually from https://alphacephei.com/vosk/models`);
}
return modelPath;
}
}
exports.VoskEngine = VoskEngine;
//# sourceMappingURL=vosk-engine.js.map