voice-to-text-converter
Version:
A modern, lightweight Node.js package for speech-to-text conversion with support for multiple engines
502 lines • 20.7 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.GoogleCloudEngine = 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"));
/**
* Google Cloud Speech-to-Text engine
*/
class GoogleCloudEngine extends base_engine_1.BaseSpeechRecognitionEngine {
constructor(apiKey, projectId) {
super();
this.client = null;
this.apiKey = null;
this.projectId = null;
this.streamingRecognizeStream = null;
this.apiKey = apiKey || null;
this.projectId = projectId || null;
}
/**
* Check if Google Cloud Speech is available
*/
isAvailable() {
try {
require.resolve('@google-cloud/speech');
return true;
}
catch {
return false;
}
}
/**
* Get supported languages
*/
getSupportedLanguages() {
return [
'af-ZA', 'am-ET', 'ar-AE', 'ar-BH', 'ar-DZ', 'ar-EG', 'ar-IQ', 'ar-IL', 'ar-JO', 'ar-KW',
'ar-LB', 'ar-LY', 'ar-MA', 'ar-OM', 'ar-PS', 'ar-QA', 'ar-SA', 'ar-SY', 'ar-TN', 'ar-YE',
'az-AZ', 'bg-BG', 'bn-BD', 'bn-IN', 'bs-BA', 'ca-ES', 'cs-CZ', 'da-DK', 'de-AT', 'de-CH',
'de-DE', 'el-GR', 'en-AU', 'en-CA', 'en-GB', 'en-GH', 'en-HK', 'en-IE', 'en-IN', 'en-KE',
'en-NG', 'en-NZ', 'en-PH', 'en-PK', 'en-SG', 'en-TZ', 'en-US', 'en-ZA', 'es-AR', 'es-BO',
'es-CL', 'es-CO', 'es-CR', 'es-DO', 'es-EC', 'es-ES', 'es-GT', 'es-HN', 'es-MX', 'es-NI',
'es-PA', 'es-PE', 'es-PR', 'es-PY', 'es-SV', 'es-UY', 'es-VE', 'et-EE', 'eu-ES', 'fa-IR',
'fi-FI', 'fil-PH', 'fr-BE', 'fr-CA', 'fr-CH', 'fr-FR', 'gl-ES', 'gu-IN', 'he-IL', 'hi-IN',
'hr-HR', 'hu-HU', 'hy-AM', 'id-ID', 'is-IS', 'it-CH', 'it-IT', 'ja-JP', 'jv-ID', 'ka-GE',
'kk-KZ', 'km-KH', 'kn-IN', 'ko-KR', 'lo-LA', 'lt-LT', 'lv-LV', 'mk-MK', 'ml-IN', 'mn-MN',
'mr-IN', 'ms-MY', 'mt-MT', 'my-MM', 'ne-NP', 'nl-BE', 'nl-NL', 'no-NO', 'pa-Guru-IN',
'pl-PL', 'pt-BR', 'pt-PT', 'ro-RO', 'ru-RU', 'si-LK', 'sk-SK', 'sl-SI', 'sq-AL', 'sr-RS',
'su-ID', 'sv-SE', 'sw-KE', 'sw-TZ', 'ta-IN', 'ta-LK', 'ta-MY', 'ta-SG', 'te-IN', 'th-TH',
'tr-TR', 'uk-UA', 'ur-IN', 'ur-PK', 'uz-UZ', 'vi-VN', 'yue-Hant-HK', 'zh-CN', 'zh-TW',
'zu-ZA'
];
}
/**
* Initialize Google Cloud Speech client
*/
async initializeClient() {
if (!this.isAvailable()) {
throw new Error('Google Cloud Speech module is not installed. Install it with: npm install @google-cloud/speech');
}
try {
const speech = require('@google-cloud/speech');
const clientOptions = {};
if (this.apiKey) {
clientOptions.apiKey = this.apiKey;
}
if (this.projectId) {
clientOptions.projectId = this.projectId;
}
this.client = new speech.SpeechClient(clientOptions);
}
catch (error) {
throw new Error(`Failed to initialize Google Cloud Speech client: ${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.initializeClient();
if (!this.client) {
throw new Error('Google Cloud Speech client not initialized');
}
this.setRecordingState(true);
this.emit('start');
if (audioConfig.source === 'microphone') {
await this.startStreamingRecognition(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 Google Cloud Speech recognition: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
}
}
/**
* Start streaming recognition for microphone input
*/
async startStreamingRecognition(audioConfig) {
if (!this.client) {
throw new Error('Client not initialized');
}
try {
const request = {
config: this.buildSpeechConfig(),
interimResults: this.config.interimResults || false,
};
this.streamingRecognizeStream = this.client.streamingRecognize()
.on('error', (error) => {
this.emitError(types_1.SpeechRecognitionErrorType.ENGINE_ERROR, `Streaming recognition error: ${error.message}`, error);
})
.on('data', (data) => {
if (data.results && data.results.length > 0) {
const result = data.results[0];
const alternative = result.alternatives[0];
if (alternative) {
const speechResult = {
transcript: alternative.transcript,
confidence: alternative.confidence || 1.0,
isFinal: result.isFinal,
alternatives: result.alternatives?.map((alt) => ({
transcript: alt.transcript,
confidence: alt.confidence || 1.0
}))
};
this.emitResult(speechResult);
}
}
});
// Write initial config
this.streamingRecognizeStream.write(request);
// Set up 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', (chunk) => {
if (this.streamingRecognizeStream && this.isRecording) {
this.streamingRecognizeStream.write({ audioContent: chunk });
}
});
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);
}
recording.stream().on('end', () => {
if (this.streamingRecognizeStream) {
this.streamingRecognizeStream.end();
}
this.emit('audioend');
this.emit('end');
this.setRecordingState(false);
});
}
catch (error) {
this.emitError(types_1.SpeechRecognitionErrorType.AUDIO_CAPTURE, `Failed to start streaming recognition: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
}
}
/**
* Process audio file
*/
async processAudioFile(filePath) {
if (!this.client) {
throw new Error('Client not initialized');
}
try {
(0, audio_1.validateAudioFile)(filePath);
const audioBytes = fs.readFileSync(filePath).toString('base64');
const request = {
audio: {
content: audioBytes,
},
config: this.buildSpeechConfig(),
};
this.emit('audiostart');
const [response] = await this.client.recognize(request);
if (response.results) {
for (const result of response.results) {
const alternative = result.alternatives[0];
if (alternative) {
const speechResult = {
transcript: alternative.transcript,
confidence: alternative.confidence || 1.0,
isFinal: true,
alternatives: result.alternatives?.map((alt) => ({
transcript: alt.transcript,
confidence: alt.confidence || 1.0
}))
};
this.emitResult(speechResult);
}
}
}
this.emit('audioend');
this.emit('end');
this.setRecordingState(false);
}
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) {
if (!this.client) {
throw new Error('Client not initialized');
}
return new Promise((resolve, reject) => {
const chunks = [];
stream.on('data', (chunk) => {
chunks.push(chunk);
});
stream.on('end', async () => {
try {
const audioBuffer = Buffer.concat(chunks);
const audioBytes = audioBuffer.toString('base64');
const request = {
audio: {
content: audioBytes,
},
config: this.buildSpeechConfig(),
};
this.emit('audiostart');
const [response] = await this.client.recognize(request);
if (response.results) {
for (const result of response.results) {
const alternative = result.alternatives[0];
if (alternative) {
const speechResult = {
transcript: alternative.transcript,
confidence: alternative.confidence || 1.0,
isFinal: true,
alternatives: result.alternatives?.map((alt) => ({
transcript: alt.transcript,
confidence: alt.confidence || 1.0
}))
};
this.emitResult(speechResult);
}
}
}
this.emit('audioend');
this.emit('end');
this.setRecordingState(false);
resolve();
}
catch (error) {
this.emitError(types_1.SpeechRecognitionErrorType.ENGINE_ERROR, `Failed to process audio stream: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
reject(error);
}
});
stream.on('error', (error) => {
this.emitError(types_1.SpeechRecognitionErrorType.ENGINE_ERROR, `Stream processing error: ${error.message}`, error);
reject(error);
});
});
}
/**
* Build Google Cloud Speech configuration
*/
buildSpeechConfig() {
return {
encoding: this.mapEncodingFormat(this.config.encoding || 'LINEAR16'),
sampleRateHertz: this.config.sampleRate || 16000,
languageCode: this.config.language || 'en-US',
maxAlternatives: this.config.maxAlternatives || 1,
profanityFilter: false,
enableWordTimeOffsets: true,
enableAutomaticPunctuation: true,
model: 'latest_long',
useEnhanced: true
};
}
/**
* Map encoding format to Google Cloud format
*/
mapEncodingFormat(encoding) {
const encodingMap = {
'LINEAR16': 'LINEAR16',
'FLAC': 'FLAC',
'MULAW': 'MULAW',
'AMR': 'AMR',
'AMR_WB': 'AMR_WB',
'OGG_OPUS': 'OGG_OPUS',
'SPEEX_WITH_HEADER_BYTE': 'SPEEX_WITH_HEADER_BYTE'
};
return encodingMap[encoding] || 'LINEAR16';
}
/**
* Stop speech recognition
*/
async stop() {
this.setRecordingState(false);
if (this.streamingRecognizeStream) {
this.streamingRecognizeStream.end();
this.streamingRecognizeStream = null;
}
}
/**
* Abort speech recognition
*/
async abort() {
this.setRecordingState(false);
if (this.streamingRecognizeStream) {
this.streamingRecognizeStream.destroy();
this.streamingRecognizeStream = null;
}
this.cleanup();
}
/**
* Process audio file directly
*/
async processFile(filePath, config) {
await this.initializeClient();
if (!this.client) {
throw new Error('Google Cloud Speech client not initialized');
}
const results = [];
const mergedConfig = this.getConfig(config);
try {
(0, audio_1.validateAudioFile)(filePath);
const audioBytes = fs.readFileSync(filePath).toString('base64');
const request = {
audio: {
content: audioBytes,
},
config: {
encoding: this.mapEncodingFormat(mergedConfig.encoding || 'LINEAR16'),
sampleRateHertz: mergedConfig.sampleRate || 16000,
languageCode: mergedConfig.language || 'en-US',
maxAlternatives: mergedConfig.maxAlternatives || 1,
profanityFilter: false,
enableWordTimeOffsets: true,
enableAutomaticPunctuation: true,
model: 'latest_long',
useEnhanced: true
},
};
const [response] = await this.client.recognize(request);
if (response.results) {
for (const result of response.results) {
const alternative = result.alternatives[0];
if (alternative) {
results.push({
transcript: alternative.transcript,
confidence: alternative.confidence || 1.0,
isFinal: true,
alternatives: result.alternatives?.map((alt) => ({
transcript: alt.transcript,
confidence: alt.confidence || 1.0
}))
});
}
}
}
return results;
}
catch (error) {
throw new Error(`Failed to process file: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Process audio stream directly
*/
async processStream(stream, config) {
await this.initializeClient();
if (!this.client) {
throw new Error('Google Cloud Speech client not initialized');
}
const results = [];
const mergedConfig = this.getConfig(config);
return new Promise((resolve, reject) => {
const chunks = [];
stream.on('data', (chunk) => {
chunks.push(chunk);
});
stream.on('end', async () => {
try {
const audioBuffer = Buffer.concat(chunks);
const audioBytes = audioBuffer.toString('base64');
const request = {
audio: {
content: audioBytes,
},
config: {
encoding: this.mapEncodingFormat(mergedConfig.encoding || 'LINEAR16'),
sampleRateHertz: mergedConfig.sampleRate || 16000,
languageCode: mergedConfig.language || 'en-US',
maxAlternatives: mergedConfig.maxAlternatives || 1,
profanityFilter: false,
enableWordTimeOffsets: true,
enableAutomaticPunctuation: true,
model: 'latest_long',
useEnhanced: true
},
};
const [response] = await this.client.recognize(request);
if (response.results) {
for (const result of response.results) {
const alternative = result.alternatives[0];
if (alternative) {
results.push({
transcript: alternative.transcript,
confidence: alternative.confidence || 1.0,
isFinal: true,
alternatives: result.alternatives?.map((alt) => ({
transcript: alt.transcript,
confidence: alt.confidence || 1.0
}))
});
}
}
}
resolve(results);
}
catch (error) {
reject(error);
}
});
stream.on('error', reject);
});
}
/**
* Set API credentials
*/
setCredentials(apiKey, projectId) {
this.apiKey = apiKey;
if (projectId) {
this.projectId = projectId;
}
this.client = null; // Reset client to reinitialize with new credentials
}
/**
* Clean up resources
*/
cleanup() {
if (this.streamingRecognizeStream) {
this.streamingRecognizeStream.destroy();
this.streamingRecognizeStream = null;
}
super.cleanup();
}
}
exports.GoogleCloudEngine = GoogleCloudEngine;
//# sourceMappingURL=google-cloud-engine.js.map