Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
277 lines
7.9 KiB
JavaScript
277 lines
7.9 KiB
JavaScript
const axios = require('axios');
|
|
const FormData = require('form-data');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
/**
|
|
* Base ASR Provider class
|
|
*/
|
|
class BaseASRProvider {
|
|
constructor(config, apiKey) {
|
|
this.config = config;
|
|
this.apiKey = apiKey;
|
|
}
|
|
|
|
/**
|
|
* Convert raw audio buffer to WAV format
|
|
* @param {Buffer} audioBuffer - Raw PCM audio data
|
|
* @param {Object} options - Audio format options
|
|
* @returns {Buffer} WAV formatted audio data
|
|
*/
|
|
createWavBuffer(audioBuffer, options = {}) {
|
|
const sampleRate = options.sampleRate || 16000;
|
|
const channels = options.channels || 1;
|
|
const bitsPerSample = options.bitsPerSample || 16;
|
|
|
|
const byteRate = sampleRate * channels * bitsPerSample / 8;
|
|
const blockAlign = channels * bitsPerSample / 8;
|
|
const dataSize = audioBuffer.length;
|
|
const fileSize = 36 + dataSize;
|
|
|
|
const header = Buffer.alloc(44);
|
|
|
|
// RIFF header
|
|
header.write('RIFF', 0);
|
|
header.writeUInt32LE(fileSize, 4);
|
|
header.write('WAVE', 8);
|
|
|
|
// fmt chunk
|
|
header.write('fmt ', 12);
|
|
header.writeUInt32LE(16, 16); // PCM format chunk size
|
|
header.writeUInt16LE(1, 20); // PCM format
|
|
header.writeUInt16LE(channels, 22);
|
|
header.writeUInt32LE(sampleRate, 24);
|
|
header.writeUInt32LE(byteRate, 28);
|
|
header.writeUInt16LE(blockAlign, 32);
|
|
header.writeUInt16LE(bitsPerSample, 34);
|
|
|
|
// data chunk
|
|
header.write('data', 36);
|
|
header.writeUInt32LE(dataSize, 40);
|
|
|
|
return Buffer.concat([header, audioBuffer]);
|
|
}
|
|
|
|
async transcribe(audioBuffer, options = {}) {
|
|
throw new Error('transcribe method must be implemented by subclass');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* OpenAI Whisper ASR Provider
|
|
*/
|
|
class OpenAIASRProvider extends BaseASRProvider {
|
|
async transcribe(audioBuffer, tempDir, options = {}) {
|
|
try {
|
|
// Create WAV buffer
|
|
const wavBuffer = this.createWavBuffer(audioBuffer, {
|
|
sampleRate: 16000,
|
|
channels: 1,
|
|
bitsPerSample: 16
|
|
});
|
|
|
|
// Create temporary file
|
|
const tempFileName = `audio_${Date.now()}_${Math.random().toString(36).substring(2)}.wav`;
|
|
const tempFilePath = path.join(tempDir, tempFileName);
|
|
|
|
// Write audio to temporary file
|
|
fs.writeFileSync(tempFilePath, wavBuffer);
|
|
|
|
try {
|
|
// Create form data
|
|
const formData = new FormData();
|
|
formData.append('file', fs.createReadStream(tempFilePath));
|
|
formData.append('model', this.config.model);
|
|
formData.append('response_format', 'json');
|
|
|
|
if (options.language) {
|
|
formData.append('language', options.language);
|
|
}
|
|
|
|
if (options.prompt) {
|
|
formData.append('prompt', options.prompt);
|
|
}
|
|
|
|
// Make API request
|
|
const response = await axios({
|
|
method: 'post',
|
|
url: this.config.apiUrl,
|
|
data: formData,
|
|
headers: {
|
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
...formData.getHeaders()
|
|
},
|
|
timeout: 30000 // 30 second timeout
|
|
});
|
|
|
|
const result = {
|
|
success: true,
|
|
text: response.data.text || '',
|
|
language: response.data.language || 'unknown',
|
|
duration: response.data.duration || 0,
|
|
confidence: response.data.confidence || 1.0,
|
|
requestId: response.headers?.['x-request-id'] || response.headers?.['request-id'] || null,
|
|
responseModel: response.data.model || this.config.model,
|
|
timestamp: Date.now(),
|
|
provider: 'openai'
|
|
};
|
|
|
|
console.log('OpenAI ASR Result:', {
|
|
text: result.text,
|
|
language: result.language,
|
|
duration: result.duration
|
|
});
|
|
|
|
return result;
|
|
|
|
} finally {
|
|
// Clean up temporary file
|
|
try {
|
|
fs.unlinkSync(tempFilePath);
|
|
} catch (cleanupError) {
|
|
console.warn('Failed to cleanup temp file:', cleanupError.message);
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('OpenAI ASR error:', error.response?.data || error.message);
|
|
|
|
return {
|
|
success: false,
|
|
text: '',
|
|
error: error.response?.data?.error?.message || error.message,
|
|
timestamp: Date.now(),
|
|
provider: 'openai'
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* SenseVoice via Siliconflow ASR Provider
|
|
*/
|
|
class SiliconflowASRProvider extends BaseASRProvider {
|
|
async transcribe(audioBuffer, tempDir, options = {}) {
|
|
try {
|
|
// Create WAV buffer
|
|
const wavBuffer = this.createWavBuffer(audioBuffer, {
|
|
sampleRate: 16000,
|
|
channels: 1,
|
|
bitsPerSample: 16
|
|
});
|
|
|
|
// Create temporary file
|
|
const tempFileName = `audio_${Date.now()}_${Math.random().toString(36).substring(2)}.wav`;
|
|
const tempFilePath = path.join(tempDir, tempFileName);
|
|
|
|
// Write audio to temporary file
|
|
fs.writeFileSync(tempFilePath, wavBuffer);
|
|
|
|
try {
|
|
// Create form data
|
|
const formData = new FormData();
|
|
formData.append('file', fs.createReadStream(tempFilePath));
|
|
formData.append('model', this.config.model);
|
|
formData.append('response_format', 'json');
|
|
|
|
// SenseVoice specific parameters
|
|
if (options.language) {
|
|
formData.append('language', options.language);
|
|
} else {
|
|
// SenseVoice supports auto language detection
|
|
formData.append('language', 'auto');
|
|
}
|
|
|
|
if (options.prompt) {
|
|
formData.append('prompt', options.prompt);
|
|
}
|
|
|
|
// Make API request
|
|
const response = await axios({
|
|
method: 'post',
|
|
url: this.config.apiUrl,
|
|
data: formData,
|
|
headers: {
|
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
...formData.getHeaders()
|
|
},
|
|
timeout: 30000 // 30 second timeout
|
|
});
|
|
|
|
const result = {
|
|
success: true,
|
|
text: response.data.text || '',
|
|
language: response.data.language || 'unknown',
|
|
duration: response.data.duration || 0,
|
|
confidence: response.data.confidence || 1.0,
|
|
requestId: response.headers?.['x-request-id'] || response.headers?.['request-id'] || null,
|
|
responseModel: response.data.model || this.config.model,
|
|
timestamp: Date.now(),
|
|
provider: 'siliconflow'
|
|
};
|
|
|
|
console.log('SenseVoice ASR Result:', {
|
|
text: result.text,
|
|
language: result.language,
|
|
duration: result.duration
|
|
});
|
|
|
|
return result;
|
|
|
|
} finally {
|
|
// Clean up temporary file
|
|
try {
|
|
fs.unlinkSync(tempFilePath);
|
|
} catch (cleanupError) {
|
|
console.warn('Failed to cleanup temp file:', cleanupError.message);
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('SenseVoice ASR error:', error.response?.data || error.message);
|
|
|
|
return {
|
|
success: false,
|
|
text: '',
|
|
error: error.response?.data?.error?.message || error.message,
|
|
timestamp: Date.now(),
|
|
provider: 'siliconflow'
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ASR Provider Factory
|
|
*/
|
|
class ASRProviderFactory {
|
|
static createProvider(providerName, config, globalConfig) {
|
|
const providerConfig = config.ASR_PROVIDERS[providerName];
|
|
if (!providerConfig) {
|
|
throw new Error(`ASR provider ${providerName} not found in configuration`);
|
|
}
|
|
|
|
// Get API key from global config
|
|
const apiKey = globalConfig[providerConfig.apiKey];
|
|
if (!apiKey) {
|
|
throw new Error(`API key ${providerConfig.apiKey} not found in configuration`);
|
|
}
|
|
|
|
switch (providerName) {
|
|
case 'openai':
|
|
return new OpenAIASRProvider(providerConfig, apiKey);
|
|
case 'siliconflow':
|
|
return new SiliconflowASRProvider(providerConfig, apiKey);
|
|
default:
|
|
throw new Error(`Unsupported ASR provider: ${providerName}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
BaseASRProvider,
|
|
OpenAIASRProvider,
|
|
SiliconflowASRProvider,
|
|
ASRProviderFactory
|
|
};
|