| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213 |
- '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');
- const { FmodeQiweiClient } = require('../mcp/src/providers/fmode-agent-transport');
- const { readFmodeVoiceToken, readQiweiAuthToken } = require('../mcp/src/core/credentials');
- 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() {
- const emptySources = { processEnv: {}, fileEnv: {}, claudeEnv: {}, fmodeConfig: {} };
- assert.equal(readFmodeVoiceToken(
- { authToken: 'parse-session-token' },
- { ...emptySources, processEnv: { FMODE_API_TOKEN: 'sk-fmode-voice' } },
- ), 'sk-fmode-voice');
- assert.equal(readFmodeVoiceToken({ authToken: 'parse-session-token' }, emptySources), '');
- assert.equal(readFmodeVoiceToken(
- {},
- { ...emptySources, processEnv: { ANTHROPIC_AUTH_TOKEN: 'sk-ant-upstream', ANTHROPIC_BASE_URL: 'https://api.anthropic.com' } },
- ), '');
- assert.equal(readFmodeVoiceToken(
- {},
- { ...emptySources, processEnv: { FMODE_API_TOKEN: 'sk-other-provider', FMODE_API_BASE: 'https://api.openai.com' } },
- ), '');
- assert.equal(readQiweiAuthToken({ authToken: 'parse-session-token' }), 'parse-session-token');
- const sessionOnlyVoice = new VoiceCloneService({
- config: { authToken: 'parse-session-token' },
- qiwei: { context: () => ({ token: 'parse-session-token' }) },
- });
- assert.equal(sessionOnlyVoice.status().configured, false);
- assert.throws(() => sessionOnlyVoice.requireReady(), /https:\/\/api\.fmode\.cn\/keys\//);
- 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: () => ({ token: 'parse-session-token', 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://server.fmode.cn/api/voice/indextts2',
- authToken: 'sk-fmode-smoke-token',
- model: 'fmode-voice',
- },
- });
- 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 = {}) => {
- assert.equal(url, 'https://server.fmode.cn/api/voice/indextts2');
- assert.equal(options.headers.Authorization, 'Bearer sk-fmode-smoke-token');
- 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;
- }
- const uploadClient = new FmodeQiweiClient({
- authToken: 'fmode-upload-token',
- uid: 'upload-account',
- guid: 'upload-guid',
- apiBase: 'https://server.fmode.cn/api/qiwei',
- });
- assert.equal(uploadClient.context().token, 'fmode-upload-token');
- const silkPath = path.join(root, 'upload.silk');
- fs.writeFileSync(silkPath, Buffer.from('silk-smoke'));
- global.fetch = async (url, options = {}) => {
- assert.equal(url, 'https://server.fmode.cn/api/qiwei/doFileApi');
- assert.equal(options.headers.Authorization, 'Bearer fmode-upload-token');
- assert.equal(options.body.get('uid'), 'upload-account');
- assert.equal(options.body.get('guid'), 'upload-guid');
- assert.equal(options.body.get('method'), '/cloud/cdnBigUpload');
- assert.equal(options.body.get('fileType'), '5');
- assert.equal(options.body.get('file').name, 'upload.silk');
- return new Response(JSON.stringify({
- code: 200,
- data: { data: { fileId: 'file-upload', fileAesKey: 'aes-upload', fileSize: 10 } },
- }), { status: 200, headers: { 'Content-Type': 'application/json' } });
- };
- try {
- assert.deepEqual(await uploadClient.uploadVoiceFile(silkPath), {
- fileId: 'file-upload',
- fileAesKey: 'aes-upload',
- fileSize: 10,
- });
- } finally {
- global.fetch = originalFetch;
- }
- service.revoke();
- assert.equal(service.status().enrolled, false);
- console.log('[ok] Fmode voice synthesis, doFileApi upload, tone, enrollment, SILK and send orchestration');
- }
- main()
- .catch(error => {
- console.error(error);
- process.exitCode = 1;
- })
- .finally(() => fs.rmSync(root, { recursive: true, force: true }));
|