'use strict'; const assert = require('assert/strict'); const fs = require('fs'); const os = require('os'); const path = require('path'); const root = fs.mkdtempSync(path.join(os.tmpdir(), 'qiwei-voice-clone-smoke-')); process.env.QIWEI_OUTPUTS_DIR = path.join(root, 'outputs'); const { VoiceCloneService, inferVoiceTone, resolveTone, readResponseBuffer, } = require('../mcp/src/core/voice-clone-service'); const { __testing: voiceToolTesting } = require('../mcp/src/tools/qiwei-voice-run'); function wavBuffer({ duration = 1, sampleRate = 16000, frequency = 220 } = {}) { const samples = Math.floor(duration * sampleRate); const dataSize = samples * 2; const buffer = Buffer.alloc(44 + dataSize); buffer.write('RIFF', 0); buffer.writeUInt32LE(36 + dataSize, 4); buffer.write('WAVEfmt ', 8); buffer.writeUInt32LE(16, 16); buffer.writeUInt16LE(1, 20); buffer.writeUInt16LE(1, 22); buffer.writeUInt32LE(sampleRate, 24); buffer.writeUInt32LE(sampleRate * 2, 28); buffer.writeUInt16LE(2, 32); buffer.writeUInt16LE(16, 34); buffer.write('data', 36); buffer.writeUInt32LE(dataSize, 40); for (let index = 0; index < samples; index += 1) { const value = Math.round(Math.sin((2 * Math.PI * frequency * index) / sampleRate) * 4000); buffer.writeInt16LE(value, 44 + index * 2); } return buffer; } async function main() { assert.equal(inferVoiceTone('非常抱歉给您带来了不好的体验').id, 'apology'); assert.equal(inferVoiceTone('恭喜,您的申请已经审核通过').id, 'friendly'); assert.equal(inferVoiceTone('提醒您,请于明天下午前提交资料').id, 'reminder'); assert.equal(inferVoiceTone('理解您的心情,这段时间辛苦了').id, 'empathetic'); assert.equal(inferVoiceTone('资料已经收到,我稍后回复您').id, 'natural'); assert.equal(resolveTone('natural', '恭喜').automatic, false); assert.throws(() => resolveTone('angry', '测试'), /不支持/); assert.equal(voiceToolTesting.isPrivateAddress('127.0.0.1'), true); assert.equal(voiceToolTesting.isPrivateAddress('10.0.0.8'), true); assert.equal(voiceToolTesting.isPrivateAddress('8.8.8.8'), false); await assert.rejects(() => voiceToolTesting.fetchVoice('http://example.com/voice.wav'), /只允许使用/); await assert.rejects( () => readResponseBuffer(new Response('small', { headers: { 'Content-Length': String(51 * 1024 * 1024) } }), 50 * 1024 * 1024), /响应过大/ ); const referencePath = path.join(root, 'reference.wav'); fs.writeFileSync(referencePath, wavBuffer({ duration: 6 })); const calls = []; let sendSuccess = true; const qiwei = { context: () => ({ uid: 'account-smoke', guid: 'guid-smoke' }), isConfigured: () => true, async uploadVoiceFile(filePath) { calls.push({ type: 'upload', filePath }); assert.equal(fs.existsSync(filePath), true); return { fileId: 'file-smoke', fileAesKey: 'aes-smoke', fileSize: fs.statSync(filePath).size }; }, async sendVoice(toId, media) { calls.push({ type: 'send', toId, media }); return sendSuccess ? { isSendSuccess: 1, msgType: 16, msgServerId: 1001 } : { isSendSuccess: 0 }; }, }; const service = new VoiceCloneService({ qiwei, config: { endpoint: 'https://tts.example/indextts2', apiKey: 'test-key', model: 'IndexTeam/IndexTTS-2', }, }); await service.enroll({ filePath: referencePath, originalName: '本人录音.wav', mime: 'audio/wav' }); const status = service.status(); assert.equal(status.enrolled, true); assert.equal(status.previewEnabled, false); assert.equal(status.profile.sampleRate, 16000); assert.ok(status.profile.duration >= 5.9 && status.profile.duration <= 6.1); await service.enroll({ filePath: referencePath, originalName: '本人录音-更新.wav', mime: 'audio/wav' }); assert.equal(service.status().enrolled, true); const originalFetch = global.fetch; const generated = wavBuffer({ duration: 1.2, sampleRate: 22050, frequency: 330 }); global.fetch = async (_url, options = {}) => { const payload = JSON.parse(options.body.get('payload')); assert.equal(payload.use_random, false); assert.equal([0, 3].includes(payload.emo_control_method), true); if (payload.emo_control_method === 3) assert.equal(payload.emo_text.includes('歉意'), true); assert.equal(options.body.get('stream_mode'), 'true'); return new Response(generated, { status: 200, headers: { 'Content-Type': 'audio/wav' } }); }; try { await assert.rejects( service.synthesizeAndSend({ text: '未经确认', tone: 'natural', toId: 'contact-smoke' }), /必须获得人工确认/ ); const result = await service.synthesizeAndSend({ text: '非常抱歉,我们马上为您处理。', tone: 'auto', toId: 'contact-smoke', confirmed: true, }); assert.equal(result.tone.id, 'apology'); assert.ok(result.duration >= 1.1 && result.duration <= 1.3); assert.equal(calls[0].type, 'upload'); assert.equal(calls[1].type, 'send'); assert.equal(calls[1].toId, 'contact-smoke'); assert.equal(calls[1].media.voiceTime, 1); assert.equal(fs.existsSync(path.join(result.runDir, 'speech.wav')), true); assert.equal(fs.existsSync(path.join(result.runDir, 'speech.silk')), false); assert.match(path.basename(result.runDir), /-clone-[a-f0-9]{8}$/); const manifest = JSON.parse(fs.readFileSync(path.join(result.runDir, 'manifest.json'), 'utf8')); assert.equal(manifest.outcome, 'sent'); assert.equal(Object.hasOwn(manifest, 'text'), false); sendSuccess = false; await assert.rejects( service.synthesizeAndSend({ text: '这次发送应失败', tone: 'natural', toId: 'contact-smoke', confirmed: true }), /未明确确认/ ); const retainedWavs = fs.readdirSync(path.dirname(result.runDir), { withFileTypes: true }) .filter(item => item.isDirectory()) .map(item => path.join(path.dirname(result.runDir), item.name, 'speech.wav')) .filter(filePath => fs.existsSync(filePath)); assert.deepEqual(retainedWavs, [result.audioPath]); } finally { global.fetch = originalFetch; } service.revoke(); assert.equal(service.status().enrolled, false); console.log('[ok] voice clone tone, enrollment, retained sent WAV, SILK and send orchestration'); } main() .catch(error => { console.error(error); process.exitCode = 1; }) .finally(() => fs.rmSync(root, { recursive: true, force: true }));