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
424 lines
16 KiB
JavaScript
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
|
|
};
|