136 lines
4.7 KiB
JavaScript
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,
|
|
),
|
|
);
|