ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
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
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
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
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
|
||||
};
|
||||
Reference in New Issue
Block a user