185 lines
8.1 KiB
JavaScript
185 lines
8.1 KiB
JavaScript
import { getModelById, getVideoModelById, getI2IModelById, getI2VModelById, getV2VModelById, getLipSyncModelById } from './models.js';
|
|
|
|
const BASE_URL = 'https://api.muapi.ai';
|
|
|
|
async function pollForResult(requestId, key, maxAttempts = 900, interval = 2000) {
|
|
const pollUrl = `${BASE_URL}/api/v1/predictions/${requestId}/result`;
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
await new Promise(resolve => setTimeout(resolve, interval));
|
|
try {
|
|
const response = await fetch(pollUrl, {
|
|
headers: { 'Content-Type': 'application/json', 'x-api-key': key }
|
|
});
|
|
if (!response.ok) {
|
|
const errText = await response.text();
|
|
if (response.status >= 500) continue;
|
|
throw new Error(`Poll Failed: ${response.status} - ${errText.slice(0, 100)}`);
|
|
}
|
|
const data = await response.json();
|
|
const status = data.status?.toLowerCase();
|
|
if (status === 'completed' || status === 'succeeded' || status === 'success') return data;
|
|
if (status === 'failed' || status === 'error') throw new Error(`Generation failed: ${data.error || 'Unknown error'}`);
|
|
} catch (error) {
|
|
if (attempt === maxAttempts) throw error;
|
|
}
|
|
}
|
|
throw new Error('Generation timed out after polling.');
|
|
}
|
|
|
|
async function submitAndPoll(endpoint, payload, key, onRequestId, maxAttempts = 60) {
|
|
const url = `${BASE_URL}/api/v1/${endpoint}`;
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'x-api-key': key },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
if (!response.ok) {
|
|
const errText = await response.text();
|
|
throw new Error(`API Request Failed: ${response.status} ${response.statusText} - ${errText.slice(0, 100)}`);
|
|
}
|
|
const submitData = await response.json();
|
|
const requestId = submitData.request_id || submitData.id;
|
|
if (!requestId) return submitData;
|
|
if (onRequestId) onRequestId(requestId);
|
|
const result = await pollForResult(requestId, key, maxAttempts);
|
|
const outputUrl = result.outputs?.[0] || result.url || result.output?.url;
|
|
return { ...result, url: outputUrl };
|
|
}
|
|
|
|
export async function generateImage(apiKey, params) {
|
|
const modelInfo = getModelById(params.model);
|
|
const endpoint = modelInfo?.endpoint || params.model;
|
|
const payload = { prompt: params.prompt };
|
|
if (params.aspect_ratio) payload.aspect_ratio = params.aspect_ratio;
|
|
if (params.resolution) payload.resolution = params.resolution;
|
|
if (params.quality) payload.quality = params.quality;
|
|
if (params.image_url) { payload.image_url = params.image_url; payload.strength = params.strength || 0.6; }
|
|
else payload.image_url = null;
|
|
if (params.seed && params.seed !== -1) payload.seed = params.seed;
|
|
return submitAndPoll(endpoint, payload, apiKey, params.onRequestId, 60);
|
|
}
|
|
|
|
export async function generateI2I(apiKey, params) {
|
|
const modelInfo = getI2IModelById(params.model);
|
|
const endpoint = modelInfo?.endpoint || params.model;
|
|
const payload = {};
|
|
if (params.prompt) payload.prompt = params.prompt;
|
|
const imageField = modelInfo?.imageField || 'image_url';
|
|
const imagesList = params.images_list?.length > 0 ? params.images_list : (params.image_url ? [params.image_url] : null);
|
|
if (imagesList) {
|
|
if (imageField === 'images_list') payload.images_list = imagesList;
|
|
else payload[imageField] = imagesList[0];
|
|
}
|
|
if (params.aspect_ratio) payload.aspect_ratio = params.aspect_ratio;
|
|
if (params.resolution) payload.resolution = params.resolution;
|
|
if (params.quality) payload.quality = params.quality;
|
|
return submitAndPoll(endpoint, payload, apiKey, params.onRequestId, 60);
|
|
}
|
|
|
|
export async function generateVideo(apiKey, params) {
|
|
const modelInfo = getVideoModelById(params.model);
|
|
const endpoint = modelInfo?.endpoint || params.model;
|
|
const payload = {};
|
|
if (params.prompt) payload.prompt = params.prompt;
|
|
if (params.aspect_ratio) payload.aspect_ratio = params.aspect_ratio;
|
|
if (params.duration) payload.duration = params.duration;
|
|
if (params.resolution) payload.resolution = params.resolution;
|
|
if (params.quality) payload.quality = params.quality;
|
|
if (params.mode) payload.mode = params.mode;
|
|
if (params.image_url) payload.image_url = params.image_url;
|
|
return submitAndPoll(endpoint, payload, apiKey, params.onRequestId, 900);
|
|
}
|
|
|
|
export async function generateI2V(apiKey, params) {
|
|
const modelInfo = getI2VModelById(params.model);
|
|
const endpoint = modelInfo?.endpoint || params.model;
|
|
const payload = {};
|
|
if (params.prompt) payload.prompt = params.prompt;
|
|
const imageField = modelInfo?.imageField || 'image_url';
|
|
if (params.image_url) {
|
|
if (imageField === 'images_list') payload.images_list = [params.image_url];
|
|
else payload[imageField] = params.image_url;
|
|
}
|
|
if (params.aspect_ratio) payload.aspect_ratio = params.aspect_ratio;
|
|
if (params.duration) payload.duration = params.duration;
|
|
if (params.resolution) payload.resolution = params.resolution;
|
|
if (params.quality) payload.quality = params.quality;
|
|
if (params.mode) payload.mode = params.mode;
|
|
return submitAndPoll(endpoint, payload, apiKey, params.onRequestId, 900);
|
|
}
|
|
|
|
export async function processLipSync(apiKey, params) {
|
|
const modelInfo = getLipSyncModelById(params.model);
|
|
const endpoint = modelInfo?.endpoint || params.model;
|
|
const payload = {};
|
|
if (params.audio_url) payload.audio_url = params.audio_url;
|
|
if (params.image_url) payload.image_url = params.image_url;
|
|
if (params.video_url) payload.video_url = params.video_url;
|
|
if (params.prompt) payload.prompt = params.prompt;
|
|
if (params.resolution) payload.resolution = params.resolution;
|
|
if (params.seed !== undefined && params.seed !== -1) payload.seed = params.seed;
|
|
return submitAndPoll(endpoint, payload, apiKey, params.onRequestId, 900);
|
|
}
|
|
|
|
export function uploadFile(apiKey, file, onProgress) {
|
|
return new Promise((resolve, reject) => {
|
|
const url = `${BASE_URL}/api/v1/upload_file`;
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open('POST', url);
|
|
xhr.setRequestHeader('x-api-key', apiKey);
|
|
|
|
if (onProgress) {
|
|
xhr.upload.onprogress = (event) => {
|
|
if (event.lengthComputable) {
|
|
const percentComplete = Math.round((event.loaded / event.total) * 100);
|
|
onProgress(percentComplete);
|
|
}
|
|
};
|
|
}
|
|
|
|
xhr.onload = () => {
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
try {
|
|
const data = JSON.parse(xhr.responseText);
|
|
const fileUrl = data.url || data.file_url || data.data?.url;
|
|
if (!fileUrl) {
|
|
reject(new Error('No URL returned from file upload'));
|
|
} else {
|
|
resolve(fileUrl);
|
|
}
|
|
} catch (e) {
|
|
reject(new Error('Failed to parse upload response'));
|
|
}
|
|
} else {
|
|
let detail = xhr.statusText;
|
|
try {
|
|
const errObj = JSON.parse(xhr.responseText);
|
|
detail = errObj.detail || detail;
|
|
} catch (e) {
|
|
// fallback to statusText
|
|
}
|
|
reject(new Error(`File upload failed: ${xhr.status} - ${detail}`));
|
|
}
|
|
};
|
|
|
|
xhr.onerror = () => reject(new Error('Network error during file upload'));
|
|
xhr.send(formData);
|
|
});
|
|
}
|
|
|
|
export async function getUserBalance(apiKey) {
|
|
const response = await fetch(`${BASE_URL}/api/v1/account/balance`, {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'x-api-key': apiKey
|
|
}
|
|
});
|
|
if (!response.ok) {
|
|
const errText = await response.text();
|
|
throw new Error(`Failed to fetch balance: ${response.status} - ${errText.slice(0, 100)}`);
|
|
}
|
|
return await response.json();
|
|
}
|