easyai-ai-gateway/scripts/voice-clone-e2e.mjs

136 lines
4.7 KiB
JavaScript

#!/usr/bin/env node
const baseURL = (process.env.GATEWAY_BASE_URL || 'http://localhost:8080').replace(/\/+$/, '');
const apiKey = process.env.GATEWAY_API_KEY || process.env.EASYAI_GATEWAY_API_KEY;
const cloneModel = process.env.GATEWAY_VOICE_CLONE_MODEL || 'MiniMax-Voice-Clone';
const speechModel = process.env.GATEWAY_TTS_MODEL || 'speech-2.6-turbo';
const voiceId =
process.env.VOICE_CLONE_ID || `voice_clone_${Date.now().toString(36)}`;
const audioURL =
process.env.VOICE_CLONE_AUDIO_URL ||
`${baseURL}/static/simulation/audio.wav`;
const marker = `voice-clone-e2e-${Date.now().toString(36)}`;
if (!apiKey) {
throw new Error('Set GATEWAY_API_KEY or EASYAI_GATEWAY_API_KEY');
}
function assert(condition, message) {
if (!condition) throw new Error(message);
}
async function request(path, init = {}) {
const res = await fetch(`${baseURL}${path}`, {
...init,
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
...(init.headers || {}),
},
});
const text = await res.text();
const body = text ? JSON.parse(text) : {};
if (!res.ok) {
throw new Error(`${init.method || 'GET'} ${path} failed ${res.status}: ${text}`);
}
return body;
}
async function postAsyncTask(path, body) {
const accepted = await request(path, {
method: 'POST',
headers: { 'X-Async': 'true' },
body: JSON.stringify(body),
});
const taskId = accepted.taskId || accepted.task?.id;
assert(taskId, `Expected async task id from ${path}`);
return pollTask(taskId);
}
async function pollTask(taskId, timeoutMs = 120000) {
const started = Date.now();
while (Date.now() - started < timeoutMs) {
const task = await request(`/api/v1/tasks/${taskId}`);
if (task.status === 'succeeded') return task;
if (task.status === 'failed') {
throw new Error(`Task ${taskId} failed: ${task.errorMessage || task.error || JSON.stringify(task)}`);
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
throw new Error(`Timed out waiting for task ${taskId}`);
}
const cloneTask = await postAsyncTask('/v1/voice_clone', {
model: cloneModel,
voice_id: voiceId,
audio_url: audioURL,
text: 'hello voice clone preview',
preview_model: process.env.VOICE_CLONE_PREVIEW_MODEL || 'speech-2.8-hd',
runMode: 'simulation',
simulation: true,
integrationTestMarker: `${marker}-clone`,
});
const cloneResult = cloneTask.result || {};
const clonedVoice = cloneResult.cloned_voice || cloneResult.clonedVoice;
assert(cloneResult.status === 'success', `Unexpected clone result: ${JSON.stringify(cloneResult)}`);
assert((cloneResult.voice_id || clonedVoice?.voiceId || clonedVoice?.voice_id) === voiceId, 'Clone voice_id mismatch');
assert(clonedVoice?.platformId || clonedVoice?.platform_id, 'Clone result missing platform binding');
const listResult = await request('/v1/voice_clone/voices');
const voices = listResult.items || listResult.data || [];
const listedVoice = voices.find((item) => item.voiceId === voiceId || item.voice_id === voiceId);
assert(listedVoice, 'Cloned voice is missing from voice list');
assert(
(listedVoice.platformId || listedVoice.platform_id) ===
(clonedVoice.platformId || clonedVoice.platform_id),
'Listed voice platform binding mismatch',
);
const speechTask = await postAsyncTask('/v1/speech/generations', {
model: speechModel,
text: 'hello from cloned voice',
cloned_voice_id: clonedVoice.id,
runMode: 'simulation',
simulation: true,
integrationTestMarker: `${marker}-speech`,
});
const speechResult = speechTask.result || {};
assert(speechResult.status === 'success', `Unexpected speech result: ${JSON.stringify(speechResult)}`);
const speechAttemptPlatformId = speechTask.attempts?.[0]?.platformId;
assert(speechAttemptPlatformId, 'Speech task is missing attempt platformId');
assert(
speechAttemptPlatformId === (clonedVoice.platformId || clonedVoice.platform_id),
`Speech used ${speechAttemptPlatformId}, expected cloned voice platform ${clonedVoice.platformId || clonedVoice.platform_id}`,
);
if (process.env.GATEWAY_CROSS_PLATFORM_TTS_MODEL) {
try {
await postAsyncTask('/v1/speech/generations', {
model: process.env.GATEWAY_CROSS_PLATFORM_TTS_MODEL,
text: 'this should not cross platform',
cloned_voice_id: clonedVoice.id,
runMode: 'simulation',
simulation: true,
});
throw new Error('Cross-platform TTS request unexpectedly succeeded');
} catch (error) {
if (String(error?.message || '').includes('unexpectedly succeeded')) throw error;
}
}
console.log(
JSON.stringify(
{
ok: true,
voiceId,
clonedVoiceId: clonedVoice.id,
platformId: clonedVoice.platformId || clonedVoice.platform_id,
cloneTaskId: cloneTask.id,
speechTaskId: speechTask.id,
},
null,
2,
),
);