voice-to-text-converter
Version:
A modern, lightweight Node.js package for speech-to-text conversion with support for multiple engines
417 lines • 14.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VoiceToText = void 0;
const events_1 = require("events");
const types_1 = require("./types");
const engines_1 = require("./engines");
const audio_1 = require("./utils/audio");
/**
* Main VoiceToText converter class
*/
class VoiceToText extends events_1.EventEmitter {
constructor(options = {}) {
super();
this.currentEngine = null;
this.isInitialized = false;
this.options = {
enableFallback: true,
enginePriority: ['web-speech', 'vosk', 'google-cloud'],
debug: false,
...options
};
if (this.options.debug) {
console.log('VoiceToText initialized with options:', this.options);
}
}
/**
* Initialize the voice-to-text converter
*/
async initialize() {
if (this.isInitialized) {
return;
}
try {
// Select and initialize the best available engine
const engine = await this.selectEngine();
if (!engine) {
throw new Error('No speech recognition engines are available');
}
this.currentEngine = engine;
this.setupEngineEvents();
this.isInitialized = true;
if (this.options.debug) {
console.log('VoiceToText initialized successfully');
}
}
catch (error) {
throw new Error(`Failed to initialize VoiceToText: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Convert speech from microphone to text
*/
async fromMicrophone(options = {}) {
await this.ensureInitialized();
const audioConfig = {
source: 'microphone',
duration: options.duration,
deviceId: options.deviceId
};
const recognitionConfig = {
sampleRate: options.sampleRate || this.options.defaultRecognitionConfig?.sampleRate,
continuous: true,
interimResults: true,
...this.options.defaultRecognitionConfig
};
await this.currentEngine.start(audioConfig, recognitionConfig);
}
/**
* Convert speech from audio file to text
*/
async fromFile(filePath, config) {
await this.ensureInitialized();
try {
(0, audio_1.validateAudioFile)(filePath);
const mergedConfig = {
...this.options.defaultRecognitionConfig,
...config
};
return await this.currentEngine.processFile(filePath, mergedConfig);
}
catch (error) {
throw new Error(`Failed to process file: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Convert speech from audio stream to text
*/
async fromStream(stream, config) {
await this.ensureInitialized();
try {
const mergedConfig = {
...this.options.defaultRecognitionConfig,
...config
};
return await this.currentEngine.processStream(stream, mergedConfig);
}
catch (error) {
throw new Error(`Failed to process stream: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Start continuous speech recognition
*/
async startListening(audioConfig, config) {
await this.ensureInitialized();
const mergedConfig = {
continuous: true,
interimResults: true,
...this.options.defaultRecognitionConfig,
...config
};
await this.currentEngine.start(audioConfig, mergedConfig);
}
/**
* Stop speech recognition
*/
async stopListening() {
if (this.currentEngine && this.currentEngine.isActive) {
await this.currentEngine.stop();
}
}
/**
* Abort speech recognition
*/
async abort() {
if (this.currentEngine) {
await this.currentEngine.abort();
}
}
/**
* Check if currently listening/recording
*/
get isListening() {
return this.currentEngine?.isActive || false;
}
/**
* Get current engine information
*/
getCurrentEngine() {
if (!this.currentEngine) {
return null;
}
// Determine engine type (simplified approach)
const engineType = this.currentEngine.constructor.name.toLowerCase().replace('engine', '');
return {
type: engineType,
available: this.currentEngine.isAvailable(),
capabilities: (0, engines_1.getEngineCapabilities)(engineType)
};
}
/**
* Switch to a different engine
*/
async switchEngine(engineConfig) {
if (this.currentEngine?.isActive) {
await this.currentEngine.stop();
}
try {
const newEngine = (0, engines_1.createEngine)(engineConfig);
if (!newEngine.isAvailable()) {
throw new Error(`Engine ${engineConfig.engine} is not available`);
}
this.currentEngine = newEngine;
this.setupEngineEvents();
if (this.options.debug) {
console.log(`Switched to engine: ${engineConfig.engine}`);
}
}
catch (error) {
throw new Error(`Failed to switch engine: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Get available engines in current environment
*/
static getAvailableEngines() {
return (0, engines_1.getAvailableEngines)();
}
/**
* Check if a specific engine is available
*/
static isEngineAvailable(engineType) {
return (0, engines_1.isEngineAvailable)(engineType);
}
/**
* Get engine capabilities
*/
static getEngineCapabilities(engineType) {
return (0, engines_1.getEngineCapabilities)(engineType);
}
/**
* Get browser support information
*/
static getBrowserSupport() {
if (typeof window === 'undefined') {
return {
webSpeechAPI: false,
mediaRecorder: false,
getUserMedia: false,
browser: { name: 'Node.js', version: process.version }
};
}
const webSpeechAPI = !!(window.SpeechRecognition || window.webkitSpeechRecognition);
const mediaRecorder = !!window.MediaRecorder;
const getUserMedia = !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
// Simple browser detection
const userAgent = navigator.userAgent;
let browserName = 'Unknown';
let browserVersion = 'Unknown';
if (userAgent.includes('Chrome')) {
browserName = 'Chrome';
const match = userAgent.match(/Chrome\/(\d+)/);
browserVersion = match ? match[1] : 'Unknown';
}
else if (userAgent.includes('Firefox')) {
browserName = 'Firefox';
const match = userAgent.match(/Firefox\/(\d+)/);
browserVersion = match ? match[1] : 'Unknown';
}
else if (userAgent.includes('Safari')) {
browserName = 'Safari';
const match = userAgent.match(/Version\/(\d+)/);
browserVersion = match ? match[1] : 'Unknown';
}
else if (userAgent.includes('Edge')) {
browserName = 'Edge';
const match = userAgent.match(/Edge\/(\d+)/);
browserVersion = match ? match[1] : 'Unknown';
}
return {
webSpeechAPI,
mediaRecorder,
getUserMedia,
browser: { name: browserName, version: browserVersion }
};
}
/**
* Create a quick instance for one-time use
*/
static async quickTranscribe(source, options = {}) {
const converter = new VoiceToText({
defaultEngine: options.engine ? { engine: options.engine } : undefined,
defaultRecognitionConfig: {
language: options.language,
...options.config
}
});
await converter.initialize();
try {
if (source === 'microphone') {
return new Promise((resolve, reject) => {
const results = [];
converter.on('result', (result) => {
if (result.isFinal) {
results.push(result);
}
});
converter.on('end', () => resolve(results));
converter.on('error', reject);
converter.fromMicrophone({ duration: options.duration || 5000 }).catch(reject);
});
}
else if (typeof source === 'string') {
return await converter.fromFile(source, options.config);
}
else {
return await converter.fromStream(source, options.config);
}
}
finally {
await converter.cleanup();
}
}
/**
* Clean up resources
*/
async cleanup() {
if (this.currentEngine) {
if (this.currentEngine.isActive) {
await this.currentEngine.abort();
}
this.currentEngine.removeAllListeners();
}
this.removeAllListeners();
this.isInitialized = false;
this.currentEngine = null;
}
/**
* Ensure the converter is initialized
*/
async ensureInitialized() {
if (!this.isInitialized) {
await this.initialize();
}
}
/**
* Select the best available engine
*/
async selectEngine() {
// If a default engine is specified, try it first
if (this.options.defaultEngine) {
try {
const engine = (0, engines_1.createEngine)(this.options.defaultEngine);
if (engine.isAvailable()) {
return engine;
}
}
catch (error) {
if (this.options.debug) {
console.warn(`Default engine ${this.options.defaultEngine.engine} failed:`, error);
}
}
}
// Try engines in priority order
const availableEngines = (0, engines_1.getAvailableEngines)();
const enginePriority = this.options.enginePriority || ['web-speech', 'vosk', 'google-cloud'];
for (const engineType of enginePriority) {
if (availableEngines.includes(engineType)) {
try {
const engineConfig = { engine: engineType };
// Add default configuration for specific engines
if (engineType === 'google-cloud' && this.options.defaultEngine?.apiKey) {
engineConfig.apiKey = this.options.defaultEngine.apiKey;
engineConfig.projectId = this.options.defaultEngine.projectId;
}
else if (engineType === 'vosk' && this.options.defaultEngine?.modelPath) {
engineConfig.modelPath = this.options.defaultEngine.modelPath;
}
const engine = (0, engines_1.createEngine)(engineConfig);
if (engine.isAvailable()) {
if (this.options.debug) {
console.log(`Selected engine: ${engineType}`);
}
return engine;
}
}
catch (error) {
if (this.options.debug) {
console.warn(`Engine ${engineType} failed:`, error);
}
}
}
}
return null;
}
/**
* Set up event forwarding from engine to main class
*/
setupEngineEvents() {
if (!this.currentEngine) {
return;
}
// Forward all events from engine
this.currentEngine.on('start', () => this.emit('start'));
this.currentEngine.on('end', () => this.emit('end'));
this.currentEngine.on('result', (result) => this.emit('result', result));
this.currentEngine.on('error', (error) => {
if (this.options.enableFallback && !this.isListening) {
this.handleEngineFallback(error).catch(fallbackError => {
this.emit('error', fallbackError);
});
}
else {
this.emit('error', error);
}
});
this.currentEngine.on('audiostart', () => this.emit('audiostart'));
this.currentEngine.on('audioend', () => this.emit('audioend'));
this.currentEngine.on('soundstart', () => this.emit('soundstart'));
this.currentEngine.on('soundend', () => this.emit('soundend'));
this.currentEngine.on('speechstart', () => this.emit('speechstart'));
this.currentEngine.on('speechend', () => this.emit('speechend'));
}
/**
* Handle engine fallback on error
*/
async handleEngineFallback(error) {
if (this.options.debug) {
console.log('Attempting engine fallback due to error:', error.message);
}
const currentEngineType = this.getCurrentEngine()?.type;
const availableEngines = (0, engines_1.getAvailableEngines)();
const remainingEngines = availableEngines.filter(engine => engine !== currentEngineType);
for (const engineType of remainingEngines) {
try {
await this.switchEngine({ engine: engineType });
if (this.options.debug) {
console.log(`Successfully switched to fallback engine: ${engineType}`);
}
return;
}
catch (fallbackError) {
if (this.options.debug) {
console.warn(`Fallback engine ${engineType} also failed:`, fallbackError);
}
}
}
throw new types_1.SpeechRecognitionError(types_1.SpeechRecognitionErrorType.ENGINE_ERROR, `All engines failed. Original error: ${error.message}`, error);
}
/**
* Type-safe event emitter methods
*/
emit(event, ...args) {
return super.emit(event, ...args);
}
on(event, listener) {
return super.on(event, listener);
}
once(event, listener) {
return super.once(event, listener);
}
off(event, listener) {
return super.off(event, listener);
}
}
exports.VoiceToText = VoiceToText;
//# sourceMappingURL=voice-to-text.js.map