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,365 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Import provider factories
|
||||
const { ASRProviderFactory } = require('../utils/providers/asrProviders');
|
||||
const { LLMProviderFactory } = require('../utils/providers/llmProviders');
|
||||
|
||||
// Import services
|
||||
const SpeechToTextService = require('../utils/speechToText');
|
||||
|
||||
// Mock configuration for testing
|
||||
const testConfig = {
|
||||
// API Keys from environment variables
|
||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||
OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY,
|
||||
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
||||
ARK_API_KEY: process.env.ARK_API_KEY,
|
||||
SILICONFLOW_API_KEY: process.env.SILICONFLOW_API_KEY,
|
||||
|
||||
// Provider configurations
|
||||
ASR_PROVIDERS: {
|
||||
openai: {
|
||||
apiUrl: 'https://api.openai.com/v1/audio/transcriptions',
|
||||
model: 'whisper-1',
|
||||
apiKey: 'OPENAI_API_KEY'
|
||||
},
|
||||
siliconflow: {
|
||||
apiUrl: 'https://api.siliconflow.cn/v1/audio/transcriptions',
|
||||
model: 'FunAudioLLM/SenseVoiceSmall',
|
||||
apiKey: 'SILICONFLOW_API_KEY'
|
||||
}
|
||||
},
|
||||
|
||||
LLM_PROVIDERS: {
|
||||
openai: {
|
||||
apiUrl: 'https://api.openai.com/v1/chat/completions',
|
||||
model: 'gpt-5.6-luna',
|
||||
apiKey: 'OPENAI_API_KEY'
|
||||
},
|
||||
'openrouter-gpt': {
|
||||
apiUrl: 'https://openrouter.ai/api/v1/chat/completions',
|
||||
model: 'openai/gpt-5.6-luna',
|
||||
apiKey: 'OPENROUTER_API_KEY'
|
||||
},
|
||||
'openrouter-gemini': {
|
||||
apiUrl: 'https://openrouter.ai/api/v1/chat/completions',
|
||||
model: 'google/gemini-3.5-flash',
|
||||
apiKey: 'OPENROUTER_API_KEY'
|
||||
},
|
||||
ark: {
|
||||
apiUrl: 'https://ark.cn-beijing.volces.com/api/v3/chat/completions',
|
||||
model: 'doubao-seed-1-6-flash-250615',
|
||||
apiKey: 'ARK_API_KEY'
|
||||
}
|
||||
},
|
||||
|
||||
// Audio configuration
|
||||
AUDIO_SAMPLE_RATE: 16000,
|
||||
VISION_MAX_TOKENS: 4096
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a sample audio buffer for testing
|
||||
* This creates a simple sine wave audio buffer
|
||||
*/
|
||||
function createTestAudioBuffer(durationSeconds = 2, sampleRate = 16000) {
|
||||
const numSamples = durationSeconds * sampleRate;
|
||||
const buffer = Buffer.alloc(numSamples * 2); // 16-bit = 2 bytes per sample
|
||||
|
||||
// Generate a simple sine wave at 440Hz
|
||||
const frequency = 440;
|
||||
for (let i = 0; i < numSamples; i++) {
|
||||
const sample = Math.sin(2 * Math.PI * frequency * i / sampleRate) * 0.5;
|
||||
const intSample = Math.round(sample * 32767);
|
||||
buffer.writeInt16LE(intSample, i * 2);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test suite for ASR providers
|
||||
*/
|
||||
describe('ASR Provider Tests', function() {
|
||||
this.timeout(60000); // 60 second timeout for API calls
|
||||
|
||||
const asrProviders = ['openai', 'siliconflow'];
|
||||
|
||||
asrProviders.forEach(providerName => {
|
||||
describe(`${providerName} ASR Provider`, function() {
|
||||
let provider;
|
||||
|
||||
before(function() {
|
||||
// Skip test if API key is not available
|
||||
const apiKeyName = testConfig.ASR_PROVIDERS[providerName].apiKey;
|
||||
if (!testConfig[apiKeyName]) {
|
||||
this.skip();
|
||||
}
|
||||
|
||||
try {
|
||||
provider = ASRProviderFactory.createProvider(providerName, testConfig, testConfig);
|
||||
} catch (error) {
|
||||
console.error(`Failed to create ${providerName} provider:`, error);
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
it('should be created successfully', function() {
|
||||
assert(provider, 'Provider should be created');
|
||||
assert(provider.config, 'Provider should have config');
|
||||
assert(provider.apiKey, 'Provider should have API key');
|
||||
});
|
||||
|
||||
it('should transcribe test audio', async function() {
|
||||
const audioBuffer = createTestAudioBuffer(2); // 2 seconds of audio
|
||||
const tempDir = path.join(__dirname, '../temp');
|
||||
|
||||
// Ensure temp directory exists
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
}
|
||||
|
||||
const result = await provider.transcribe(audioBuffer, tempDir);
|
||||
|
||||
assert(result, 'Should return a result');
|
||||
assert(typeof result.success === 'boolean', 'Should have success field');
|
||||
assert(typeof result.text === 'string', 'Should have text field');
|
||||
assert(result.provider === providerName, 'Should have correct provider name');
|
||||
|
||||
console.log(`${providerName} ASR Result:`, {
|
||||
success: result.success,
|
||||
text: result.text.substring(0, 100) + (result.text.length > 100 ? '...' : ''),
|
||||
language: result.language,
|
||||
provider: result.provider
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SpeechToTextService Integration', function() {
|
||||
asrProviders.forEach(providerName => {
|
||||
it(`should work with ${providerName} provider`, async function() {
|
||||
// Skip test if API key is not available
|
||||
const apiKeyName = testConfig.ASR_PROVIDERS[providerName].apiKey;
|
||||
if (!testConfig[apiKeyName]) {
|
||||
this.skip();
|
||||
}
|
||||
|
||||
// Override config for this test
|
||||
const originalConfig = require('../config');
|
||||
Object.assign(originalConfig, testConfig);
|
||||
originalConfig.ASR_PROVIDER = providerName;
|
||||
|
||||
const sttService = new SpeechToTextService();
|
||||
const audioBuffer = createTestAudioBuffer(2);
|
||||
|
||||
const result = await sttService.transcribeAudio(audioBuffer);
|
||||
|
||||
assert(result, 'Should return a result');
|
||||
assert(typeof result.success === 'boolean', 'Should have success field');
|
||||
assert(typeof result.text === 'string', 'Should have text field');
|
||||
|
||||
console.log(`STT Service with ${providerName}:`, {
|
||||
success: result.success,
|
||||
text: result.text.substring(0, 100) + (result.text.length > 100 ? '...' : ''),
|
||||
provider: result.provider
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Test suite for LLM providers
|
||||
*/
|
||||
describe('LLM Provider Tests', function() {
|
||||
this.timeout(60000); // 60 second timeout for API calls
|
||||
|
||||
const llmProviders = ['openai', 'openrouter-gpt', 'openrouter-gemini', 'ark'];
|
||||
const testMessages = [
|
||||
{ role: 'system', content: 'You are a helpful AI assistant.' },
|
||||
{ role: 'user', content: 'Hello! Please respond with a short greeting.' }
|
||||
];
|
||||
|
||||
llmProviders.forEach(providerName => {
|
||||
describe(`${providerName} LLM Provider`, function() {
|
||||
let provider;
|
||||
|
||||
before(function() {
|
||||
// Skip test if API key is not available
|
||||
const apiKeyName = testConfig.LLM_PROVIDERS[providerName].apiKey;
|
||||
if (!testConfig[apiKeyName]) {
|
||||
this.skip();
|
||||
}
|
||||
|
||||
try {
|
||||
provider = LLMProviderFactory.createProvider(providerName, testConfig, testConfig);
|
||||
} catch (error) {
|
||||
console.error(`Failed to create ${providerName} provider:`, error);
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
it('should be created successfully', function() {
|
||||
assert(provider, 'Provider should be created');
|
||||
assert(provider.config, 'Provider should have config');
|
||||
assert(provider.apiKey, 'Provider should have API key');
|
||||
});
|
||||
|
||||
it('should generate chat completion', async function() {
|
||||
const result = await provider.createChatCompletion(testMessages, {
|
||||
max_tokens: 100
|
||||
});
|
||||
|
||||
assert(result, 'Should return a result');
|
||||
assert(typeof result.success === 'boolean', 'Should have success field');
|
||||
assert(result.provider === providerName.split('-')[0], 'Should have correct provider name');
|
||||
|
||||
if (result.success) {
|
||||
assert(result.response, 'Should have response object');
|
||||
console.log(`${providerName} LLM Result: Success`);
|
||||
} else {
|
||||
console.log(`${providerName} LLM Result:`, result.error);
|
||||
}
|
||||
});
|
||||
|
||||
it('should stream chat completion', async function() {
|
||||
const result = await provider.createChatCompletion(testMessages, {
|
||||
max_tokens: 50,
|
||||
stream: true
|
||||
});
|
||||
|
||||
assert(result, 'Should return a result');
|
||||
|
||||
if (result.success) {
|
||||
assert(result.response, 'Should have response object');
|
||||
assert(result.response.data, 'Should have data stream');
|
||||
|
||||
// Test streaming by collecting some data
|
||||
let receivedData = false;
|
||||
const timeout = setTimeout(() => {
|
||||
if (!receivedData) {
|
||||
console.log(`${providerName}: No data received within timeout`);
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
result.response.data.on('data', (chunk) => {
|
||||
receivedData = true;
|
||||
clearTimeout(timeout);
|
||||
console.log(`${providerName} LLM Streaming: Received data chunk`);
|
||||
});
|
||||
|
||||
result.response.data.on('error', (error) => {
|
||||
clearTimeout(timeout);
|
||||
console.error(`${providerName} LLM Streaming Error:`, error.message);
|
||||
});
|
||||
|
||||
} else {
|
||||
console.log(`${providerName} LLM Streaming Result:`, result.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Integration tests for all provider combinations
|
||||
*/
|
||||
describe('Provider Integration Tests', function() {
|
||||
this.timeout(120000); // 2 minute timeout for integration tests
|
||||
|
||||
const testCombinations = [
|
||||
{ asr: 'openai', llm: 'openai', description: 'OpenAI ASR + OpenAI LLM' },
|
||||
{ asr: 'openai', llm: 'openrouter-gpt', description: 'OpenAI ASR + OpenRouter GPT' },
|
||||
{ asr: 'openai', llm: 'openrouter-gemini', description: 'OpenAI ASR + OpenRouter Gemini' },
|
||||
{ asr: 'openai', llm: 'ark', description: 'OpenAI ASR + ARK Doubao' },
|
||||
{ asr: 'siliconflow', llm: 'openai', description: 'SenseVoice ASR + OpenAI LLM' },
|
||||
{ asr: 'siliconflow', llm: 'openrouter-gpt', description: 'SenseVoice ASR + OpenRouter GPT' },
|
||||
{ asr: 'siliconflow', llm: 'openrouter-gemini', description: 'SenseVoice ASR + OpenRouter Gemini' },
|
||||
{ asr: 'siliconflow', llm: 'ark', description: 'SenseVoice ASR + ARK Doubao' }
|
||||
];
|
||||
|
||||
testCombinations.forEach(combination => {
|
||||
it(`should work with ${combination.description}`, async function() {
|
||||
// Check if API keys are available
|
||||
const asrApiKey = testConfig.ASR_PROVIDERS[combination.asr].apiKey;
|
||||
const llmApiKey = testConfig.LLM_PROVIDERS[combination.llm].apiKey;
|
||||
|
||||
if (!testConfig[asrApiKey] || !testConfig[llmApiKey]) {
|
||||
this.skip();
|
||||
}
|
||||
|
||||
try {
|
||||
// Create providers
|
||||
const asrProvider = ASRProviderFactory.createProvider(combination.asr, testConfig, testConfig);
|
||||
const llmProvider = LLMProviderFactory.createProvider(combination.llm, testConfig, testConfig);
|
||||
|
||||
// Test ASR
|
||||
const audioBuffer = createTestAudioBuffer(2);
|
||||
const tempDir = path.join(__dirname, '../temp');
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
}
|
||||
|
||||
const asrResult = await asrProvider.transcribe(audioBuffer, tempDir);
|
||||
assert(asrResult.success !== undefined, 'ASR should return result');
|
||||
|
||||
// Test LLM with a simple message
|
||||
const messages = [
|
||||
{ role: 'system', content: 'You are a helpful assistant.' },
|
||||
{ role: 'user', content: 'Say hello in one word.' }
|
||||
];
|
||||
|
||||
const llmResult = await llmProvider.createChatCompletion(messages, {
|
||||
max_tokens: 10
|
||||
});
|
||||
|
||||
assert(llmResult.success !== undefined, 'LLM should return result');
|
||||
|
||||
console.log(`Integration Test - ${combination.description}:`, {
|
||||
asr: { success: asrResult.success, provider: asrResult.provider },
|
||||
llm: { success: llmResult.success, provider: llmResult.provider }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Integration test failed for ${combination.description}:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Provider switching tests
|
||||
*/
|
||||
describe('Provider Switching Tests', function() {
|
||||
it('should switch ASR providers dynamically', function() {
|
||||
const originalConfig = require('../config');
|
||||
Object.assign(originalConfig, testConfig);
|
||||
|
||||
const sttService = new SpeechToTextService();
|
||||
const originalProvider = sttService.getProviderInfo();
|
||||
|
||||
// Try switching to different provider
|
||||
const availableProviders = ['openai', 'siliconflow'];
|
||||
const newProvider = availableProviders.find(p => p !== originalProvider.provider);
|
||||
|
||||
if (newProvider && testConfig[testConfig.ASR_PROVIDERS[newProvider].apiKey]) {
|
||||
sttService.switchProvider(newProvider);
|
||||
const newProviderInfo = sttService.getProviderInfo();
|
||||
|
||||
assert(newProviderInfo.provider !== originalProvider.provider, 'Provider should change');
|
||||
console.log('ASR Provider switched from', originalProvider.provider, 'to', newProviderInfo.provider);
|
||||
} else {
|
||||
console.log('Skipping ASR provider switching test - insufficient API keys');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Export test configuration for use in other test files
|
||||
module.exports = {
|
||||
testConfig,
|
||||
createTestAudioBuffer
|
||||
};
|
||||
@@ -0,0 +1,307 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Import ASR provider factory
|
||||
const { ASRProviderFactory } = require('../utils/providers/asrProviders');
|
||||
|
||||
// Test configuration
|
||||
const testConfig = {
|
||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||
SILICONFLOW_API_KEY: process.env.SILICONFLOW_API_KEY,
|
||||
|
||||
ASR_PROVIDERS: {
|
||||
openai: {
|
||||
apiUrl: 'https://api.openai.com/v1/audio/transcriptions',
|
||||
model: 'whisper-1',
|
||||
apiKey: 'OPENAI_API_KEY'
|
||||
},
|
||||
siliconflow: {
|
||||
apiUrl: 'https://api.siliconflow.cn/v1/audio/transcriptions',
|
||||
model: 'FunAudioLLM/SenseVoiceSmall',
|
||||
apiKey: 'SILICONFLOW_API_KEY'
|
||||
}
|
||||
},
|
||||
|
||||
AUDIO_SAMPLE_RATE: 16000
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a test audio buffer with spoken content
|
||||
* This creates a simple sine wave that simulates audio content
|
||||
*/
|
||||
function createTestAudioBuffer(durationSeconds = 3, sampleRate = 16000) {
|
||||
const numSamples = durationSeconds * sampleRate;
|
||||
const buffer = Buffer.alloc(numSamples * 2); // 16-bit = 2 bytes per sample
|
||||
|
||||
// Generate a more complex wave pattern to simulate speech
|
||||
for (let i = 0; i < numSamples; i++) {
|
||||
// Mix multiple frequencies to simulate speech-like content
|
||||
const t = i / sampleRate;
|
||||
const sample =
|
||||
0.3 * Math.sin(2 * Math.PI * 300 * t) + // Base frequency
|
||||
0.2 * Math.sin(2 * Math.PI * 600 * t) + // Harmonic
|
||||
0.1 * Math.sin(2 * Math.PI * 1200 * t) + // Higher harmonic
|
||||
0.05 * (Math.random() - 0.5); // Noise for realism
|
||||
|
||||
const intSample = Math.round(sample * 16000); // Scale to 16-bit range
|
||||
buffer.writeInt16LE(intSample, i * 2);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* ASR Provider Individual Tests
|
||||
*/
|
||||
describe('ASR 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('OpenAI Whisper ASR Provider', function() {
|
||||
let provider;
|
||||
|
||||
before(function() {
|
||||
if (!testConfig.OPENAI_API_KEY) {
|
||||
console.log('⚠️ Skipping OpenAI ASR tests - OPENAI_API_KEY not found');
|
||||
this.skip();
|
||||
}
|
||||
|
||||
try {
|
||||
provider = ASRProviderFactory.createProvider('openai', testConfig, testConfig);
|
||||
console.log('✅ OpenAI ASR Provider created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to create OpenAI ASR provider:', error);
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
it('should initialize with correct configuration', function() {
|
||||
assert(provider, 'Provider should be created');
|
||||
assert.strictEqual(provider.config.model, 'whisper-1', 'Should use whisper-1 model');
|
||||
assert.strictEqual(provider.config.apiUrl, 'https://api.openai.com/v1/audio/transcriptions', 'Should use correct API URL');
|
||||
assert(provider.apiKey, 'Should have API key');
|
||||
console.log('📋 OpenAI ASR Config:', {
|
||||
model: provider.config.model,
|
||||
apiUrl: provider.config.apiUrl
|
||||
});
|
||||
});
|
||||
|
||||
it('should transcribe test audio successfully', async function() {
|
||||
const audioBuffer = createTestAudioBuffer(3); // 3 seconds of test audio
|
||||
console.log('🎵 Testing OpenAI ASR with', audioBuffer.length, 'bytes of audio data');
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await provider.transcribe(audioBuffer, tempDir);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
console.log('📊 OpenAI ASR Results:', {
|
||||
success: result.success,
|
||||
transcriptionTime: `${duration}ms`,
|
||||
textLength: result.text ? result.text.length : 0,
|
||||
language: result.language,
|
||||
confidence: result.confidence,
|
||||
provider: result.provider
|
||||
});
|
||||
|
||||
assert(result, 'Should return a result');
|
||||
assert(typeof result.success === 'boolean', 'Should have success field');
|
||||
assert(typeof result.text === 'string', 'Should have text field');
|
||||
assert.strictEqual(result.provider, 'openai', 'Should identify as openai provider');
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ OpenAI ASR transcription successful');
|
||||
if (result.text.length > 0) {
|
||||
console.log('📝 Transcribed text preview:', result.text.substring(0, 100) + (result.text.length > 100 ? '...' : ''));
|
||||
}
|
||||
} else {
|
||||
console.log('❌ OpenAI ASR transcription failed:', result.error);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle empty audio gracefully', async function() {
|
||||
const emptyBuffer = Buffer.alloc(0);
|
||||
const result = await provider.transcribe(emptyBuffer, tempDir);
|
||||
|
||||
console.log('🔍 OpenAI ASR Empty Audio Test:', {
|
||||
success: result.success,
|
||||
error: result.error,
|
||||
provider: result.provider
|
||||
});
|
||||
|
||||
assert(result, 'Should return a result');
|
||||
assert(typeof result.success === 'boolean', 'Should have success field');
|
||||
// Empty audio should typically fail or return empty text
|
||||
if (!result.success) {
|
||||
assert(result.error, 'Should have error message for empty audio');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('SenseVoice (Siliconflow) ASR Provider', function() {
|
||||
let provider;
|
||||
|
||||
before(function() {
|
||||
if (!testConfig.SILICONFLOW_API_KEY) {
|
||||
console.log('⚠️ Skipping Siliconflow ASR tests - SILICONFLOW_API_KEY not found');
|
||||
this.skip();
|
||||
}
|
||||
|
||||
try {
|
||||
provider = ASRProviderFactory.createProvider('siliconflow', testConfig, testConfig);
|
||||
console.log('✅ Siliconflow ASR Provider created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to create Siliconflow ASR provider:', error);
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
it('should initialize with correct configuration', function() {
|
||||
assert(provider, 'Provider should be created');
|
||||
assert.strictEqual(provider.config.model, 'FunAudioLLM/SenseVoiceSmall', 'Should use SenseVoiceSmall model');
|
||||
assert.strictEqual(provider.config.apiUrl, 'https://api.siliconflow.cn/v1/audio/transcriptions', 'Should use correct API URL');
|
||||
assert(provider.apiKey, 'Should have API key');
|
||||
console.log('📋 Siliconflow ASR Config:', {
|
||||
model: provider.config.model,
|
||||
apiUrl: provider.config.apiUrl
|
||||
});
|
||||
});
|
||||
|
||||
it('should transcribe test audio successfully', async function() {
|
||||
const audioBuffer = createTestAudioBuffer(3); // 3 seconds of test audio
|
||||
console.log('🎵 Testing Siliconflow ASR with', audioBuffer.length, 'bytes of audio data');
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await provider.transcribe(audioBuffer, tempDir);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
console.log('📊 Siliconflow ASR Results:', {
|
||||
success: result.success,
|
||||
transcriptionTime: `${duration}ms`,
|
||||
textLength: result.text ? result.text.length : 0,
|
||||
language: result.language,
|
||||
confidence: result.confidence,
|
||||
provider: result.provider
|
||||
});
|
||||
|
||||
assert(result, 'Should return a result');
|
||||
assert(typeof result.success === 'boolean', 'Should have success field');
|
||||
assert(typeof result.text === 'string', 'Should have text field');
|
||||
assert.strictEqual(result.provider, 'siliconflow', 'Should identify as siliconflow provider');
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ Siliconflow ASR transcription successful');
|
||||
if (result.text.length > 0) {
|
||||
console.log('📝 Transcribed text preview:', result.text.substring(0, 100) + (result.text.length > 100 ? '...' : ''));
|
||||
}
|
||||
} else {
|
||||
console.log('❌ Siliconflow ASR transcription failed:', result.error);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle different audio lengths', async function() {
|
||||
const shortAudio = createTestAudioBuffer(1); // 1 second
|
||||
const longAudio = createTestAudioBuffer(5); // 5 seconds
|
||||
|
||||
console.log('🎵 Testing Siliconflow ASR with different audio lengths');
|
||||
|
||||
const shortResult = await provider.transcribe(shortAudio, tempDir);
|
||||
const longResult = await provider.transcribe(longAudio, tempDir);
|
||||
|
||||
console.log('📊 Audio Length Tests:', {
|
||||
short: { success: shortResult.success, textLength: shortResult.text.length },
|
||||
long: { success: longResult.success, textLength: longResult.text.length }
|
||||
});
|
||||
|
||||
assert(shortResult, 'Should handle short audio');
|
||||
assert(longResult, 'Should handle long audio');
|
||||
assert(typeof shortResult.success === 'boolean', 'Short audio should have success field');
|
||||
assert(typeof longResult.success === 'boolean', 'Long audio should have success field');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ASR Provider Comparison', function() {
|
||||
it('should compare provider performance', async function() {
|
||||
const availableProviders = [];
|
||||
|
||||
if (testConfig.OPENAI_API_KEY) {
|
||||
availableProviders.push('openai');
|
||||
}
|
||||
if (testConfig.SILICONFLOW_API_KEY) {
|
||||
availableProviders.push('siliconflow');
|
||||
}
|
||||
|
||||
if (availableProviders.length < 2) {
|
||||
console.log('⚠️ Skipping provider comparison - need at least 2 providers');
|
||||
this.skip();
|
||||
}
|
||||
|
||||
const audioBuffer = createTestAudioBuffer(3);
|
||||
const results = {};
|
||||
|
||||
console.log('🏁 Comparing ASR provider performance...');
|
||||
|
||||
for (const providerName of availableProviders) {
|
||||
const provider = ASRProviderFactory.createProvider(providerName, testConfig, testConfig);
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await provider.transcribe(audioBuffer, tempDir);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
results[providerName] = {
|
||||
success: result.success,
|
||||
duration: duration,
|
||||
textLength: result.text ? result.text.length : 0,
|
||||
error: result.error
|
||||
};
|
||||
}
|
||||
|
||||
console.log('📊 ASR Provider Performance Comparison:');
|
||||
Object.entries(results).forEach(([provider, result]) => {
|
||||
console.log(` ${provider}:`, {
|
||||
success: result.success ? '✅' : '❌',
|
||||
time: `${result.duration}ms`,
|
||||
textLength: result.textLength,
|
||||
error: result.error || 'none'
|
||||
});
|
||||
});
|
||||
|
||||
// Find fastest successful provider
|
||||
const successfulProviders = Object.entries(results).filter(([_, result]) => result.success);
|
||||
if (successfulProviders.length > 0) {
|
||||
const fastest = successfulProviders.reduce((prev, curr) =>
|
||||
prev[1].duration < curr[1].duration ? prev : curr
|
||||
);
|
||||
console.log(`🏆 Fastest provider: ${fastest[0]} (${fastest[1].duration}ms)`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
after(function() {
|
||||
// Cleanup temp files
|
||||
try {
|
||||
const files = fs.readdirSync(tempDir);
|
||||
files.forEach(file => {
|
||||
if (file.startsWith('audio_')) {
|
||||
fs.unlinkSync(path.join(tempDir, file));
|
||||
}
|
||||
});
|
||||
console.log('🧹 Cleaned up temporary audio files');
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Failed to cleanup temp files:', error.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
testConfig,
|
||||
createTestAudioBuffer
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const { sha256, validateExperimentEvidence } = require('../experiment_validation');
|
||||
|
||||
function fixture(dir) {
|
||||
fs.writeFileSync(path.join(dir, 'input.wav'), Buffer.from('real-input'));
|
||||
fs.writeFileSync(path.join(dir, 'segment.wav'), Buffer.from('real-segment'));
|
||||
fs.writeFileSync(path.join(dir, 'output.mp3'), Buffer.alloc(2000, 1));
|
||||
const real = { execution: 'real', mock: false, probe_only: false, fallback_used: false, latency_seconds: 0.1 };
|
||||
return {
|
||||
schema_version: 1, experiment: '6-3',
|
||||
provenance: { host: { platform: 'test', architecture: 'test' }, runtime: { node: 'test', onnxruntime_node: 'test' } },
|
||||
source_media: { path: 'input.wav', capture_method: 'browser_microphone_over_websocket', sha256: sha256(path.join(dir, 'input.wav')), original_sha256: sha256(path.join(dir, 'input.wav')), sample_rate_hz: 16000, channels: 1, bits_per_sample: 16 },
|
||||
stages: {
|
||||
vad: { ...real, implementation: 'Silero VAD ONNX', model_sha256: 'abc', endpoint_detected: true, forced_endpoint: false, max_silence_ms: 500, observed_trailing_silence_ms: 512, segment_path: 'segment.wav', segment_sha256: sha256(path.join(dir, 'segment.wav')) },
|
||||
asr: { ...real, provider: 'openai', model: 'whisper-1', inference_completed: true, api_request_completed: true, transcript: 'hello' },
|
||||
llm: { ...real, provider: 'openai', model: 'gpt-real', api_request_completed: true, streamed: true, first_token_seconds: 0.1, response: 'Hi.' },
|
||||
tts: { ...real, provider: 'siliconflow', model: 'CosyVoice', api_request_completed: true, first_audio_byte_seconds: 0.1, output_path: 'output.mp3', output_sha256: sha256(path.join(dir, 'output.mp3')), output_bytes: 2000, output_duration_seconds: 1 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('Experiment 6-3 strict evidence gates', () => {
|
||||
let dir;
|
||||
beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'exp6-3-')); });
|
||||
afterEach(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
|
||||
it('accepts direct real media and all four real stages', () => {
|
||||
assert.strictEqual(validateExperimentEvidence(fixture(dir), dir).passed, true);
|
||||
});
|
||||
|
||||
for (const field of ['mock', 'probe_only', 'fallback_used']) {
|
||||
it(`rejects a stage marked ${field}`, () => {
|
||||
const evidence = fixture(dir); evidence.stages.asr[field] = true;
|
||||
assert.strictEqual(validateExperimentEvidence(evidence, dir).passed, false);
|
||||
});
|
||||
}
|
||||
|
||||
it('rejects a forced VAD flush and missing 500ms silence', () => {
|
||||
const evidence = fixture(dir);
|
||||
evidence.stages.vad.forced_endpoint = true;
|
||||
evidence.stages.vad.observed_trailing_silence_ms = 100;
|
||||
assert.strictEqual(validateExperimentEvidence(evidence, dir).passed, false);
|
||||
});
|
||||
|
||||
it('rejects README claims without hashed output media', () => {
|
||||
const evidence = fixture(dir); evidence.stages.tts.output_sha256 = 'claimed-only';
|
||||
assert.strictEqual(validateExperimentEvidence(evidence, dir).passed, false);
|
||||
});
|
||||
|
||||
it('rejects evidence without reproducibility provenance', () => {
|
||||
const evidence = fixture(dir); delete evidence.provenance.runtime.onnxruntime_node;
|
||||
assert.strictEqual(validateExperimentEvidence(evidence, dir).passed, false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,560 @@
|
||||
const assert = require('assert');
|
||||
|
||||
// Import LLM provider factory
|
||||
const { LLMProviderFactory } = require('../utils/providers/llmProviders');
|
||||
|
||||
// Test configuration
|
||||
const testConfig = {
|
||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
|
||||
OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY,
|
||||
ARK_API_KEY: process.env.ARK_API_KEY,
|
||||
SILICONFLOW_API_KEY: process.env.SILICONFLOW_API_KEY,
|
||||
|
||||
LLM_PROVIDERS: {
|
||||
openai: {
|
||||
apiUrl: 'https://api.openai.com/v1/chat/completions',
|
||||
model: 'gpt-5.6-luna',
|
||||
apiKey: 'OPENAI_API_KEY'
|
||||
},
|
||||
'openrouter-gpt': {
|
||||
apiUrl: 'https://openrouter.ai/api/v1/chat/completions',
|
||||
model: 'openai/gpt-5.6-luna',
|
||||
apiKey: 'OPENROUTER_API_KEY'
|
||||
},
|
||||
'openrouter-gemini': {
|
||||
apiUrl: 'https://openrouter.ai/api/v1/chat/completions',
|
||||
model: 'google/gemini-3.5-flash',
|
||||
apiKey: 'OPENROUTER_API_KEY'
|
||||
},
|
||||
ark: {
|
||||
apiUrl: 'https://ark.cn-beijing.volces.com/api/v3/chat/completions',
|
||||
model: 'doubao-seed-1-6-flash-250615',
|
||||
apiKey: 'ARK_API_KEY'
|
||||
}
|
||||
},
|
||||
|
||||
VISION_MAX_TOKENS: 4096
|
||||
};
|
||||
|
||||
// Test messages for different scenarios
|
||||
const testMessages = {
|
||||
simple: [
|
||||
{ role: 'system', content: 'You are a helpful AI assistant.' },
|
||||
{ role: 'user', content: 'Hello! Please respond with exactly the word "SUCCESS" to confirm you are working.' }
|
||||
],
|
||||
conversation: [
|
||||
{ role: 'system', content: 'You are a conversational AI assistant.' },
|
||||
{ role: 'user', content: 'What is the capital of France?' },
|
||||
{ role: 'assistant', content: 'The capital of France is Paris.' },
|
||||
{ role: 'user', content: 'What is its population?' }
|
||||
],
|
||||
creative: [
|
||||
{ role: 'system', content: 'You are a creative writing assistant.' },
|
||||
{ role: 'user', content: 'Write a very short story about a robot learning to paint. Keep it under 50 words.' }
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Collect streaming response data
|
||||
*/
|
||||
async function collectStreamingResponse(response, timeout = 30000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let buffer = '';
|
||||
let accumulatedContent = '';
|
||||
let firstTokenTime = null;
|
||||
let tokenCount = 0;
|
||||
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
reject(new Error('Streaming response timeout'));
|
||||
}, timeout);
|
||||
|
||||
response.data.on('data', (chunk) => {
|
||||
try {
|
||||
if (!firstTokenTime) {
|
||||
firstTokenTime = Date.now();
|
||||
}
|
||||
|
||||
buffer += chunk.toString();
|
||||
const lines = buffer.split('\n');
|
||||
// Keep the last line if it's incomplete
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
if (!trimmedLine || trimmedLine === '[DONE]') continue;
|
||||
if (!trimmedLine.startsWith('data: ')) continue;
|
||||
|
||||
try {
|
||||
const jsonData = JSON.parse(trimmedLine.replace('data: ', ''));
|
||||
const content = jsonData.choices[0]?.delta?.content || '';
|
||||
if (content) {
|
||||
accumulatedContent += content;
|
||||
tokenCount++;
|
||||
}
|
||||
} catch (parseError) {
|
||||
// Skip malformed JSON
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
response.data.on('end', () => {
|
||||
clearTimeout(timeoutHandle);
|
||||
resolve({
|
||||
content: accumulatedContent,
|
||||
tokenCount: tokenCount,
|
||||
firstTokenTime: firstTokenTime,
|
||||
success: true
|
||||
});
|
||||
});
|
||||
|
||||
response.data.on('error', (error) => {
|
||||
clearTimeout(timeoutHandle);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM Provider Individual Tests
|
||||
*/
|
||||
describe('LLM Providers - Individual Testing', function() {
|
||||
this.timeout(120000); // 2 minute timeout for API calls
|
||||
|
||||
describe('OpenAI GPT-4o LLM Provider', function() {
|
||||
let provider;
|
||||
|
||||
before(function() {
|
||||
if (!testConfig.OPENAI_API_KEY) {
|
||||
console.log('⚠️ Skipping OpenAI LLM tests - OPENAI_API_KEY not found');
|
||||
this.skip();
|
||||
}
|
||||
|
||||
try {
|
||||
provider = LLMProviderFactory.createProvider('openai', testConfig, testConfig);
|
||||
console.log('✅ OpenAI LLM Provider created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to create OpenAI LLM provider:', error);
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
it('should initialize with correct configuration', function() {
|
||||
assert(provider, 'Provider should be created');
|
||||
assert.strictEqual(provider.config.model, 'gpt-5.6-luna', 'Should use gpt-5.6-luna model');
|
||||
assert.strictEqual(provider.config.apiUrl, 'https://api.openai.com/v1/chat/completions', 'Should use correct API URL');
|
||||
assert(provider.apiKey, 'Should have API key');
|
||||
console.log('📋 OpenAI LLM Config:', {
|
||||
model: provider.config.model,
|
||||
apiUrl: provider.config.apiUrl
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate simple chat completion', async function() {
|
||||
console.log('🤖 Testing OpenAI LLM with simple message');
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await provider.createChatCompletion(testMessages.simple, {
|
||||
max_tokens: 100
|
||||
});
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
console.log('📊 OpenAI LLM Simple Test:', {
|
||||
success: result.success,
|
||||
responseTime: `${responseTime}ms`,
|
||||
provider: result.provider
|
||||
});
|
||||
|
||||
assert(result, 'Should return a result');
|
||||
assert(typeof result.success === 'boolean', 'Should have success field');
|
||||
assert.strictEqual(result.provider, 'openai', 'Should identify as openai provider');
|
||||
|
||||
if (result.success) {
|
||||
assert(result.response, 'Should have response object');
|
||||
console.log('✅ OpenAI LLM simple completion successful');
|
||||
} else {
|
||||
console.log('❌ OpenAI LLM simple completion failed:', result.error);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle streaming chat completion', async function() {
|
||||
console.log('🌊 Testing OpenAI LLM streaming');
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await provider.createChatCompletion(testMessages.simple, {
|
||||
max_tokens: 50,
|
||||
stream: true
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.log('❌ OpenAI LLM streaming setup failed:', result.error);
|
||||
assert(false, 'Streaming setup should succeed');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const streamResult = await collectStreamingResponse(result.response);
|
||||
const totalTime = Date.now() - startTime;
|
||||
const timeToFirstToken = streamResult.firstTokenTime ? streamResult.firstTokenTime - startTime : 0;
|
||||
|
||||
console.log('📊 OpenAI LLM Streaming Results:', {
|
||||
success: streamResult.success,
|
||||
totalTime: `${totalTime}ms`,
|
||||
timeToFirstToken: `${timeToFirstToken}ms`,
|
||||
tokenCount: streamResult.tokenCount,
|
||||
contentLength: streamResult.content.length
|
||||
});
|
||||
|
||||
assert(streamResult.success, 'Streaming should be successful');
|
||||
assert(streamResult.content.length > 0, 'Should receive content');
|
||||
assert(streamResult.tokenCount > 0, 'Should receive tokens');
|
||||
|
||||
if (streamResult.content.length > 0) {
|
||||
console.log('📝 Generated content preview:', streamResult.content.substring(0, 100) + (streamResult.content.length > 100 ? '...' : ''));
|
||||
}
|
||||
|
||||
console.log('✅ OpenAI LLM streaming successful');
|
||||
} catch (streamError) {
|
||||
console.error('❌ OpenAI LLM streaming failed:', streamError.message);
|
||||
assert(false, 'Streaming should not fail: ' + streamError.message);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle conversation context', async function() {
|
||||
console.log('💬 Testing OpenAI LLM with conversation context');
|
||||
|
||||
const result = await provider.createChatCompletion(testMessages.conversation, {
|
||||
max_tokens: 100
|
||||
});
|
||||
|
||||
console.log('📊 OpenAI LLM Conversation Test:', {
|
||||
success: result.success,
|
||||
provider: result.provider
|
||||
});
|
||||
|
||||
assert(result, 'Should return a result');
|
||||
assert(typeof result.success === 'boolean', 'Should have success field');
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ OpenAI LLM conversation handling successful');
|
||||
} else {
|
||||
console.log('❌ OpenAI LLM conversation handling failed:', result.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('OpenRouter GPT LLM Provider', function() {
|
||||
let provider;
|
||||
|
||||
before(function() {
|
||||
if (!testConfig.OPENROUTER_API_KEY) {
|
||||
console.log('⚠️ Skipping OpenRouter GPT tests - OPENROUTER_API_KEY not found');
|
||||
this.skip();
|
||||
}
|
||||
|
||||
try {
|
||||
provider = LLMProviderFactory.createProvider('openrouter-gpt', testConfig, testConfig);
|
||||
console.log('✅ OpenRouter GPT Provider created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to create OpenRouter GPT provider:', error);
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
it('should initialize with correct configuration', function() {
|
||||
assert(provider, 'Provider should be created');
|
||||
assert.strictEqual(provider.config.model, 'openai/gpt-5.6-luna', 'Should use openai/gpt-5.6-luna model');
|
||||
assert.strictEqual(provider.config.apiUrl, 'https://openrouter.ai/api/v1/chat/completions', 'Should use OpenRouter API URL');
|
||||
assert(provider.apiKey, 'Should have API key');
|
||||
console.log('📋 OpenRouter GPT Config:', {
|
||||
model: provider.config.model,
|
||||
apiUrl: provider.config.apiUrl
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate chat completion via OpenRouter', async function() {
|
||||
console.log('🤖 Testing OpenRouter GPT');
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await provider.createChatCompletion(testMessages.simple, {
|
||||
max_tokens: 100
|
||||
});
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
console.log('📊 OpenRouter GPT Test:', {
|
||||
success: result.success,
|
||||
responseTime: `${responseTime}ms`,
|
||||
provider: result.provider
|
||||
});
|
||||
|
||||
assert(result, 'Should return a result');
|
||||
assert(typeof result.success === 'boolean', 'Should have success field');
|
||||
assert.strictEqual(result.provider, 'openrouter', 'Should identify as openrouter provider');
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ OpenRouter GPT completion successful');
|
||||
} else {
|
||||
console.log('❌ OpenRouter GPT completion failed:', result.error);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle streaming via OpenRouter', async function() {
|
||||
console.log('🌊 Testing OpenRouter GPT streaming');
|
||||
|
||||
const result = await provider.createChatCompletion(testMessages.simple, {
|
||||
max_tokens: 50,
|
||||
stream: true
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.log('❌ OpenRouter GPT streaming setup failed:', result.error);
|
||||
return; // Don't fail the test, just log
|
||||
}
|
||||
|
||||
try {
|
||||
const streamResult = await collectStreamingResponse(result.response);
|
||||
|
||||
console.log('📊 OpenRouter GPT Streaming:', {
|
||||
success: streamResult.success,
|
||||
tokenCount: streamResult.tokenCount,
|
||||
contentLength: streamResult.content.length
|
||||
});
|
||||
|
||||
if (streamResult.success) {
|
||||
console.log('✅ OpenRouter GPT streaming successful');
|
||||
}
|
||||
} catch (streamError) {
|
||||
console.log('❌ OpenRouter GPT streaming error:', streamError.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('OpenRouter Gemini LLM Provider', function() {
|
||||
let provider;
|
||||
|
||||
before(function() {
|
||||
if (!testConfig.OPENROUTER_API_KEY) {
|
||||
console.log('⚠️ Skipping OpenRouter Gemini tests - OPENROUTER_API_KEY not found');
|
||||
this.skip();
|
||||
}
|
||||
|
||||
try {
|
||||
provider = LLMProviderFactory.createProvider('openrouter-gemini', testConfig, testConfig);
|
||||
console.log('✅ OpenRouter Gemini Provider created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to create OpenRouter Gemini provider:', error);
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
it('should initialize with correct configuration', function() {
|
||||
assert(provider, 'Provider should be created');
|
||||
assert.strictEqual(provider.config.model, 'google/gemini-3.5-flash', 'Should use gemini-3.5-flash model');
|
||||
assert.strictEqual(provider.config.apiUrl, 'https://openrouter.ai/api/v1/chat/completions', 'Should use OpenRouter API URL');
|
||||
assert(provider.apiKey, 'Should have API key');
|
||||
console.log('📋 OpenRouter Gemini Config:', {
|
||||
model: provider.config.model,
|
||||
apiUrl: provider.config.apiUrl
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate chat completion with Gemini', async function() {
|
||||
console.log('🤖 Testing OpenRouter Gemini');
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await provider.createChatCompletion(testMessages.simple, {
|
||||
max_tokens: 100
|
||||
});
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
console.log('📊 OpenRouter Gemini Test:', {
|
||||
success: result.success,
|
||||
responseTime: `${responseTime}ms`,
|
||||
provider: result.provider
|
||||
});
|
||||
|
||||
assert(result, 'Should return a result');
|
||||
assert(typeof result.success === 'boolean', 'Should have success field');
|
||||
assert.strictEqual(result.provider, 'openrouter', 'Should identify as openrouter provider');
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ OpenRouter Gemini completion successful');
|
||||
} else {
|
||||
console.log('❌ OpenRouter Gemini completion failed:', result.error);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle creative tasks with Gemini', async function() {
|
||||
console.log('🎨 Testing OpenRouter Gemini creative task');
|
||||
|
||||
const result = await provider.createChatCompletion(testMessages.creative, {
|
||||
max_tokens: 150
|
||||
});
|
||||
|
||||
console.log('📊 OpenRouter Gemini Creative Test:', {
|
||||
success: result.success,
|
||||
provider: result.provider
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ OpenRouter Gemini creative task successful');
|
||||
} else {
|
||||
console.log('❌ OpenRouter Gemini creative task failed:', result.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('ARK Doubao LLM Provider', function() {
|
||||
let provider;
|
||||
|
||||
before(function() {
|
||||
if (!testConfig.ARK_API_KEY) {
|
||||
console.log('⚠️ Skipping ARK Doubao tests - ARK_API_KEY not found');
|
||||
this.skip();
|
||||
}
|
||||
|
||||
try {
|
||||
provider = LLMProviderFactory.createProvider('ark', testConfig, testConfig);
|
||||
console.log('✅ ARK Doubao Provider created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to create ARK Doubao provider:', error);
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
|
||||
it('should initialize with correct configuration', function() {
|
||||
assert(provider, 'Provider should be created');
|
||||
assert.strictEqual(provider.config.model, 'doubao-seed-1-6-flash-250615', 'Should use doubao model');
|
||||
assert.strictEqual(provider.config.apiUrl, 'https://ark.cn-beijing.volces.com/api/v3/chat/completions', 'Should use ARK API URL');
|
||||
assert(provider.apiKey, 'Should have API key');
|
||||
console.log('📋 ARK Doubao Config:', {
|
||||
model: provider.config.model,
|
||||
apiUrl: provider.config.apiUrl
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate chat completion with Doubao', async function() {
|
||||
console.log('🤖 Testing ARK Doubao');
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await provider.createChatCompletion(testMessages.simple, {
|
||||
max_tokens: 100
|
||||
});
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
console.log('📊 ARK Doubao Test:', {
|
||||
success: result.success,
|
||||
responseTime: `${responseTime}ms`,
|
||||
provider: result.provider
|
||||
});
|
||||
|
||||
assert(result, 'Should return a result');
|
||||
assert(typeof result.success === 'boolean', 'Should have success field');
|
||||
assert.strictEqual(result.provider, 'ark', 'Should identify as ark provider');
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ ARK Doubao completion successful');
|
||||
} else {
|
||||
console.log('❌ ARK Doubao completion failed:', result.error);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle Chinese language tasks', async function() {
|
||||
console.log('🇨🇳 Testing ARK Doubao with Chinese');
|
||||
|
||||
const chineseMessages = [
|
||||
{ role: 'system', content: '你是一个有用的AI助手。' },
|
||||
{ role: 'user', content: '请用一句话介绍北京。' }
|
||||
];
|
||||
|
||||
const result = await provider.createChatCompletion(chineseMessages, {
|
||||
max_tokens: 100
|
||||
});
|
||||
|
||||
console.log('📊 ARK Doubao Chinese Test:', {
|
||||
success: result.success,
|
||||
provider: result.provider
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ ARK Doubao Chinese handling successful');
|
||||
} else {
|
||||
console.log('❌ ARK Doubao Chinese handling failed:', result.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('LLM Provider Performance Comparison', function() {
|
||||
it('should compare provider response times', async function() {
|
||||
const availableProviders = [];
|
||||
|
||||
if (testConfig.OPENAI_API_KEY) availableProviders.push('openai');
|
||||
if (testConfig.OPENROUTER_API_KEY) {
|
||||
availableProviders.push('openrouter-gpt', 'openrouter-gemini');
|
||||
}
|
||||
if (testConfig.ARK_API_KEY) availableProviders.push('ark');
|
||||
|
||||
if (availableProviders.length < 2) {
|
||||
console.log('⚠️ Skipping provider comparison - need at least 2 providers');
|
||||
this.skip();
|
||||
}
|
||||
|
||||
const results = {};
|
||||
console.log('🏁 Comparing LLM provider performance...');
|
||||
|
||||
for (const providerName of availableProviders) {
|
||||
try {
|
||||
const provider = LLMProviderFactory.createProvider(providerName, testConfig, testConfig);
|
||||
|
||||
const startTime = Date.now();
|
||||
const result = await provider.createChatCompletion(testMessages.simple, {
|
||||
max_tokens: 50
|
||||
});
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
results[providerName] = {
|
||||
success: result.success,
|
||||
duration: duration,
|
||||
error: result.error
|
||||
};
|
||||
} catch (error) {
|
||||
results[providerName] = {
|
||||
success: false,
|
||||
duration: 0,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
console.log('📊 LLM Provider Performance Comparison:');
|
||||
Object.entries(results).forEach(([provider, result]) => {
|
||||
console.log(` ${provider}:`, {
|
||||
success: result.success ? '✅' : '❌',
|
||||
time: `${result.duration}ms`,
|
||||
error: result.error || 'none'
|
||||
});
|
||||
});
|
||||
|
||||
// Find fastest successful provider
|
||||
const successfulProviders = Object.entries(results).filter(([_, result]) => result.success);
|
||||
if (successfulProviders.length > 0) {
|
||||
const fastest = successfulProviders.reduce((prev, curr) =>
|
||||
prev[1].duration < curr[1].duration ? prev : curr
|
||||
);
|
||||
console.log(`🏆 Fastest LLM provider: ${fastest[0]} (${fastest[1].duration}ms)`);
|
||||
}
|
||||
|
||||
// At least one provider should work
|
||||
assert(successfulProviders.length > 0, 'At least one LLM provider should be successful');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
testConfig,
|
||||
testMessages,
|
||||
collectStreamingResponse
|
||||
};
|
||||
@@ -0,0 +1,424 @@
|
||||
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
|
||||
};
|
||||
Reference in New Issue
Block a user