feat: 支持 MiniMax 音色克隆和 2.8 语音模型
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
#!/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,
|
||||
),
|
||||
);
|
||||
Reference in New Issue
Block a user