105 lines
3.5 KiB
JavaScript
105 lines
3.5 KiB
JavaScript
import {
|
|
GENERATION_POLL_INTERVAL_MS,
|
|
GENERATION_PROVIDER_OPTIONS,
|
|
GENERATION_TIMEOUT_MS,
|
|
MODEL_API_BASE,
|
|
} from '../config/appConfig.js'
|
|
|
|
export function apiUrl(path) {
|
|
if (/^https?:\/\//i.test(path)) return path
|
|
const normalized = path.startsWith('/') ? path : `/${path}`
|
|
if (!normalized.startsWith('/api/')) return normalized
|
|
return `${MODEL_API_BASE.replace(/\/$/, '')}${normalized}`
|
|
}
|
|
|
|
export function delay(ms) {
|
|
return new Promise((resolve) => {
|
|
window.setTimeout(resolve, ms)
|
|
})
|
|
}
|
|
|
|
export async function readApiResponse(response) {
|
|
const payload = await response.json().catch(() => ({}))
|
|
if (!response.ok || payload.error) {
|
|
throw new Error(payload.error || `Request failed with ${response.status}`)
|
|
}
|
|
return payload
|
|
}
|
|
|
|
export function getProviderPlan(provider) {
|
|
return provider === 'auto' ? ['rodin', 'tripo', 'fal', 'hunyuan', 'cinematic'] : [provider || 'rodin']
|
|
}
|
|
|
|
export function getProviderLabel(provider) {
|
|
if (provider === 'local') return 'Local'
|
|
if (provider === 'cinematic') return 'JS Depth'
|
|
if (provider === 'reference') return 'Khronos Reference'
|
|
return GENERATION_PROVIDER_OPTIONS.find((item) => item.id === provider)?.label ?? 'Hyper3D'
|
|
}
|
|
|
|
export async function create3dGeneration({ provider, imageDataUrl, fileName, prompt, modelId }) {
|
|
const response = await fetch(apiUrl('/api/3d/generate'), {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ provider, imageDataUrl, fileName, prompt, modelId }),
|
|
})
|
|
|
|
return readApiResponse(response)
|
|
}
|
|
|
|
export async function analyzeAssetImage({ imageDataUrl, fileName }) {
|
|
const response = await fetch(apiUrl('/api/3d/analyze'), {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ imageDataUrl, fileName }),
|
|
})
|
|
|
|
return readApiResponse(response)
|
|
}
|
|
|
|
export async function uploadLocal3dModel(file) {
|
|
const response = await fetch(apiUrl(`/api/3d/local-model?fileName=${encodeURIComponent(file.name)}`), {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': file.type || 'model/gltf-binary' },
|
|
body: file,
|
|
})
|
|
|
|
return readApiResponse(response)
|
|
}
|
|
|
|
export async function get3dApiHealth() {
|
|
const response = await fetch(apiUrl('/api/3d/health'))
|
|
return readApiResponse(response)
|
|
}
|
|
|
|
export async function get3dServerLogs(limit = 100) {
|
|
const response = await fetch(apiUrl(`/api/3d/logs?limit=${encodeURIComponent(limit)}`))
|
|
return readApiResponse(response)
|
|
}
|
|
|
|
export async function get3dGenerationStatus(taskId, provider) {
|
|
const response = await fetch(apiUrl(`/api/3d/status/${encodeURIComponent(taskId)}?provider=${encodeURIComponent(provider || 'rodin')}`))
|
|
return readApiResponse(response)
|
|
}
|
|
|
|
export async function waitFor3dModel(taskId, provider, onStatus) {
|
|
const deadline = Date.now() + GENERATION_TIMEOUT_MS
|
|
|
|
while (Date.now() < deadline) {
|
|
await delay(GENERATION_POLL_INTERVAL_MS)
|
|
const status = await get3dGenerationStatus(taskId, provider)
|
|
onStatus?.(status)
|
|
|
|
if (['success', 'completed', 'complete', 'done'].includes(String(status.status).toLowerCase())) {
|
|
if (!status.modelUrl) throw new Error(`${getProviderLabel(provider)} finished but no GLB model URL was returned.`)
|
|
return status
|
|
}
|
|
|
|
if (['failed', 'error', 'cancelled', 'canceled'].includes(String(status.status).toLowerCase())) {
|
|
throw new Error(status.error || `${getProviderLabel(provider)} generation failed.`)
|
|
}
|
|
}
|
|
|
|
throw new Error(`${getProviderLabel(provider)} generation timed out.`)
|
|
}
|