Files
liqiang b119135836
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
ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
2026-08-20 13:12:50 +00:00

424 lines
16 KiB
JavaScript

const assert = require('assert');
const axios = require('axios');
const fs = require('fs');
const path = require('path');
// Test configuration
const testConfig = {
SILICONFLOW_API_KEY: process.env.SILICONFLOW_API_KEY,
TTS_PROVIDERS: {
siliconflow: {
apiUrl: 'https://api.siliconflow.cn/v1/audio/speech',
model: 'FunAudioLLM/CosyVoice2-0.5B',
voice: 'FunAudioLLM/CosyVoice2-0.5B:diana',
apiKey: 'SILICONFLOW_API_KEY'
}
}
};
// Test texts for different scenarios
const testTexts = {
simple: 'Hello, this is a simple text-to-speech test.',
multilingual: 'Hello world. 你好世界. こんにちは世界.',
punctuation: 'Testing punctuation: question? exclamation! comma, period.',
numbers: 'The year is 2025, and the time is 12:34 PM.',
long: 'This is a longer text to test the text-to-speech synthesis capability. It contains multiple sentences and should demonstrate the natural flow of speech generation. The quality and naturalness of the audio output will be evaluated.'
};
/**
* TTS Provider for CosyVoice2 via Siliconflow
*/
class SiliconflowTTSProvider {
constructor(config, apiKey) {
this.config = config;
this.apiKey = apiKey;
}
async synthesize(text, options = {}) {
try {
const response = await axios({
method: 'post',
url: this.config.apiUrl,
data: {
model: this.config.model,
input: text,
voice: options.voice || this.config.voice,
response_format: options.format || 'mp3',
sample_rate: options.sampleRate || 32000,
stream: options.stream || false,
speed: options.speed || 1.0,
gain: options.gain || 0
},
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
responseType: 'arraybuffer',
timeout: 60000 // 60 second timeout
});
const result = {
success: true,
audioData: Buffer.from(response.data),
format: options.format || 'mp3',
sampleRate: options.sampleRate || 32000,
provider: 'siliconflow',
timestamp: Date.now()
};
return result;
} catch (error) {
console.error('Siliconflow TTS error:', error.response?.data || error.message);
return {
success: false,
audioData: null,
error: error.response?.data?.error?.message || error.message,
provider: 'siliconflow',
timestamp: Date.now()
};
}
}
}
/**
* TTS Provider Individual Tests
*/
describe('TTS Providers - Individual Testing', function() {
this.timeout(120000); // 2 minute timeout for API calls
const tempDir = path.join(__dirname, '../temp');
before(function() {
// Ensure temp directory exists
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
});
describe('CosyVoice2 (Siliconflow) TTS Provider', function() {
let provider;
before(function() {
if (!testConfig.SILICONFLOW_API_KEY) {
console.log('⚠️ Skipping Siliconflow TTS tests - SILICONFLOW_API_KEY not found');
this.skip();
}
try {
const providerConfig = testConfig.TTS_PROVIDERS.siliconflow;
provider = new SiliconflowTTSProvider(providerConfig, testConfig.SILICONFLOW_API_KEY);
console.log('✅ Siliconflow TTS Provider created successfully');
} catch (error) {
console.error('❌ Failed to create Siliconflow TTS provider:', error);
this.skip();
}
});
it('should initialize with correct configuration', function() {
assert(provider, 'Provider should be created');
assert.strictEqual(provider.config.model, 'FunAudioLLM/CosyVoice2-0.5B', 'Should use CosyVoice2 model');
assert.strictEqual(provider.config.voice, 'FunAudioLLM/CosyVoice2-0.5B:diana', 'Should use diana voice');
assert.strictEqual(provider.config.apiUrl, 'https://api.siliconflow.cn/v1/audio/speech', 'Should use correct API URL');
assert(provider.apiKey, 'Should have API key');
console.log('📋 Siliconflow TTS Config:', {
model: provider.config.model,
voice: provider.config.voice,
apiUrl: provider.config.apiUrl
});
});
it('should synthesize simple text to speech', async function() {
console.log('🎵 Testing Siliconflow TTS with simple text');
const startTime = Date.now();
const result = await provider.synthesize(testTexts.simple);
const synthesisTime = Date.now() - startTime;
console.log('📊 Siliconflow TTS Simple Test:', {
success: result.success,
synthesisTime: `${synthesisTime}ms`,
audioSize: result.audioData ? `${result.audioData.length} bytes` : 'none',
format: result.format,
sampleRate: result.sampleRate,
provider: result.provider
});
assert(result, 'Should return a result');
assert(typeof result.success === 'boolean', 'Should have success field');
assert.strictEqual(result.provider, 'siliconflow', 'Should identify as siliconflow provider');
if (result.success) {
assert(result.audioData, 'Should have audio data');
assert(result.audioData.length > 0, 'Audio data should not be empty');
assert.strictEqual(result.format, 'mp3', 'Should return MP3 format');
// Save test audio file
const testAudioPath = path.join(tempDir, `tts_simple_${Date.now()}.mp3`);
fs.writeFileSync(testAudioPath, result.audioData);
console.log('💾 Saved test audio to:', testAudioPath);
console.log('✅ Siliconflow TTS simple synthesis successful');
} else {
console.log('❌ Siliconflow TTS simple synthesis failed:', result.error);
}
});
it('should handle multilingual text', async function() {
console.log('🌍 Testing Siliconflow TTS with multilingual text');
const result = await provider.synthesize(testTexts.multilingual);
console.log('📊 Siliconflow TTS Multilingual Test:', {
success: result.success,
audioSize: result.audioData ? `${result.audioData.length} bytes` : 'none',
provider: result.provider
});
assert(result, 'Should return a result');
assert(typeof result.success === 'boolean', 'Should have success field');
if (result.success) {
assert(result.audioData, 'Should have audio data for multilingual text');
assert(result.audioData.length > 0, 'Multilingual audio data should not be empty');
// Save multilingual test audio
const testAudioPath = path.join(tempDir, `tts_multilingual_${Date.now()}.mp3`);
fs.writeFileSync(testAudioPath, result.audioData);
console.log('💾 Saved multilingual audio to:', testAudioPath);
console.log('✅ Siliconflow TTS multilingual synthesis successful');
} else {
console.log('❌ Siliconflow TTS multilingual synthesis failed:', result.error);
}
});
it('should handle different audio formats and settings', async function() {
console.log('⚙️ Testing Siliconflow TTS with different settings');
const settings = [
{ format: 'mp3', sampleRate: 24000, speed: 1.0 },
{ format: 'mp3', sampleRate: 32000, speed: 1.2 },
{ format: 'mp3', sampleRate: 16000, speed: 0.8 }
];
for (const setting of settings) {
const result = await provider.synthesize(testTexts.simple, setting);
console.log(`📊 TTS Settings Test (${setting.format}, ${setting.sampleRate}Hz, ${setting.speed}x):`, {
success: result.success,
audioSize: result.audioData ? `${result.audioData.length} bytes` : 'none',
error: result.error || 'none'
});
assert(result, 'Should return a result');
assert(typeof result.success === 'boolean', 'Should have success field');
if (result.success) {
assert(result.audioData, 'Should have audio data');
assert(result.audioData.length > 0, 'Audio data should not be empty');
// Save test audio with settings info
const filename = `tts_settings_${setting.sampleRate}hz_${setting.speed}x_${Date.now()}.mp3`;
const testAudioPath = path.join(tempDir, filename);
fs.writeFileSync(testAudioPath, result.audioData);
console.log(`💾 Saved settings test audio to: ${testAudioPath}`);
}
}
console.log('✅ Siliconflow TTS settings variation tests completed');
});
it('should handle punctuation and numbers correctly', async function() {
console.log('🔢 Testing Siliconflow TTS with punctuation and numbers');
const punctuationResult = await provider.synthesize(testTexts.punctuation);
const numbersResult = await provider.synthesize(testTexts.numbers);
console.log('📊 Siliconflow TTS Punctuation Test:', {
success: punctuationResult.success,
audioSize: punctuationResult.audioData ? `${punctuationResult.audioData.length} bytes` : 'none'
});
console.log('📊 Siliconflow TTS Numbers Test:', {
success: numbersResult.success,
audioSize: numbersResult.audioData ? `${numbersResult.audioData.length} bytes` : 'none'
});
assert(punctuationResult, 'Should handle punctuation');
assert(numbersResult, 'Should handle numbers');
if (punctuationResult.success && numbersResult.success) {
console.log('✅ Siliconflow TTS punctuation and numbers handling successful');
}
});
it('should handle longer text synthesis', async function() {
console.log('📝 Testing Siliconflow TTS with longer text');
const startTime = Date.now();
const result = await provider.synthesize(testTexts.long);
const synthesisTime = Date.now() - startTime;
console.log('📊 Siliconflow TTS Long Text Test:', {
success: result.success,
synthesisTime: `${synthesisTime}ms`,
audioSize: result.audioData ? `${result.audioData.length} bytes` : 'none',
textLength: testTexts.long.length
});
assert(result, 'Should return a result');
assert(typeof result.success === 'boolean', 'Should have success field');
if (result.success) {
assert(result.audioData, 'Should have audio data for long text');
assert(result.audioData.length > 0, 'Long text audio data should not be empty');
// Long text should produce more audio data
const expectedMinSize = 50000; // Roughly 50KB minimum for longer text
if (result.audioData.length > expectedMinSize) {
console.log('✅ Long text produced appropriate amount of audio data');
}
// Save long text audio
const testAudioPath = path.join(tempDir, `tts_long_${Date.now()}.mp3`);
fs.writeFileSync(testAudioPath, result.audioData);
console.log('💾 Saved long text audio to:', testAudioPath);
console.log('✅ Siliconflow TTS long text synthesis successful');
} else {
console.log('❌ Siliconflow TTS long text synthesis failed:', result.error);
}
});
it('should handle empty or invalid text gracefully', async function() {
console.log('🔍 Testing Siliconflow TTS with edge cases');
const emptyResult = await provider.synthesize('');
const spaceResult = await provider.synthesize(' ');
const specialResult = await provider.synthesize('!@#$%^&*()');
console.log('📊 Siliconflow TTS Edge Cases:', {
empty: { success: emptyResult.success, error: emptyResult.error || 'none' },
spaces: { success: spaceResult.success, error: spaceResult.error || 'none' },
special: { success: specialResult.success, error: specialResult.error || 'none' }
});
// These should either work or fail gracefully
assert(emptyResult, 'Should handle empty string');
assert(spaceResult, 'Should handle whitespace');
assert(specialResult, 'Should handle special characters');
console.log('✅ Siliconflow TTS edge cases handled');
});
it('should measure synthesis performance metrics', async function() {
console.log('🏁 Testing Siliconflow TTS performance metrics');
const testText = testTexts.simple;
const iterations = 3;
const results = [];
for (let i = 0; i < iterations; i++) {
const startTime = Date.now();
const result = await provider.synthesize(testText);
const duration = Date.now() - startTime;
if (result.success) {
results.push({
duration: duration,
audioSize: result.audioData.length,
success: true
});
} else {
results.push({
duration: duration,
audioSize: 0,
success: false,
error: result.error
});
}
}
const successfulResults = results.filter(r => r.success);
if (successfulResults.length > 0) {
const avgDuration = successfulResults.reduce((sum, r) => sum + r.duration, 0) / successfulResults.length;
const avgAudioSize = successfulResults.reduce((sum, r) => sum + r.audioSize, 0) / successfulResults.length;
const minDuration = Math.min(...successfulResults.map(r => r.duration));
const maxDuration = Math.max(...successfulResults.map(r => r.duration));
console.log('📊 Siliconflow TTS Performance Metrics:', {
successRate: `${successfulResults.length}/${iterations}`,
avgSynthesisTime: `${Math.round(avgDuration)}ms`,
minSynthesisTime: `${minDuration}ms`,
maxSynthesisTime: `${maxDuration}ms`,
avgAudioSize: `${Math.round(avgAudioSize)} bytes`,
textLength: testText.length
});
// Performance expectations
assert(avgDuration < 10000, 'Average synthesis time should be under 10 seconds');
assert(avgAudioSize > 1000, 'Should produce reasonable amount of audio data');
console.log('✅ Siliconflow TTS performance metrics acceptable');
} else {
console.log('❌ No successful TTS synthesis results for performance testing');
}
assert(successfulResults.length > 0, 'At least one synthesis attempt should succeed');
});
});
describe('TTS Provider Integration', function() {
it('should integrate with live audio system', function() {
if (!testConfig.SILICONFLOW_API_KEY) {
console.log('⚠️ Skipping TTS integration test - SILICONFLOW_API_KEY not found');
this.skip();
}
// Simulate the TTS configuration that would be used in the live system
const liveConfig = {
TTS_API_URL: 'https://api.siliconflow.cn/v1/audio/speech',
TTS_PROVIDERS: testConfig.TTS_PROVIDERS,
TTS_PROVIDER: 'siliconflow',
SILICONFLOW_API_KEY: testConfig.SILICONFLOW_API_KEY
};
assert(liveConfig.TTS_API_URL, 'Should have TTS API URL');
assert(liveConfig.TTS_PROVIDERS.siliconflow, 'Should have Siliconflow TTS provider config');
assert(liveConfig.SILICONFLOW_API_KEY, 'Should have Siliconflow API key');
console.log('📋 Live System TTS Integration Config:', {
provider: liveConfig.TTS_PROVIDER,
model: liveConfig.TTS_PROVIDERS.siliconflow.model,
voice: liveConfig.TTS_PROVIDERS.siliconflow.voice,
hasApiKey: !!liveConfig.SILICONFLOW_API_KEY
});
console.log('✅ TTS provider integration configuration valid');
});
});
after(function() {
// Cleanup temp audio files
try {
const files = fs.readdirSync(tempDir);
let cleanedCount = 0;
files.forEach(file => {
if (file.startsWith('tts_') && file.endsWith('.mp3')) {
fs.unlinkSync(path.join(tempDir, file));
cleanedCount++;
}
});
console.log(`🧹 Cleaned up ${cleanedCount} temporary TTS audio files`);
} catch (error) {
console.warn('⚠️ Failed to cleanup temp TTS files:', error.message);
}
});
});
module.exports = {
testConfig,
testTexts,
SiliconflowTTSProvider
};