209 lines
7.6 KiB
JavaScript
209 lines
7.6 KiB
JavaScript
import http from 'node:http'
|
|
import { API_HOST, API_PORT, FAL_API_KEY, HUNYUAN_API_BASE, RODIN_API_KEY, TRIPO_API_KEY } from './server/config.mjs'
|
|
import { assertLocalDiagnosticsRequest, readJsonBody, sendJson, setCorsHeaders } from './server/http-utils.mjs'
|
|
import { createRequestId, logEvent, readRecentLogs, summarizeError, summarizePayload } from './server/logger.mjs'
|
|
import { importLocalModel, proxyModel, serveLocalModel } from './server/model-store.mjs'
|
|
import { createFalTask, getFalHealth, getFalTask } from './server/providers/fal.mjs'
|
|
import { createHunyuanTask, getHunyuanHealth, getHunyuanTask } from './server/providers/hunyuan.mjs'
|
|
import { createRodinTask, getRodinHealth, getRodinTask } from './server/providers/rodin.mjs'
|
|
import { createTripoTask, getTripoHealth, getTripoTask } from './server/providers/tripo.mjs'
|
|
import { analyzeAssetImage, getVisionHealth } from './server/providers/vision.mjs'
|
|
|
|
const DEFAULT_GENERATION_PROVIDER = 'rodin'
|
|
|
|
const server = http.createServer(async (request, response) => {
|
|
const requestId = createRequestId()
|
|
const startedAt = Date.now()
|
|
let url = null
|
|
|
|
try {
|
|
setCorsHeaders(response)
|
|
response.setHeader('X-Request-Id', requestId)
|
|
|
|
if (request.method === 'OPTIONS') {
|
|
response.writeHead(204)
|
|
response.end()
|
|
return
|
|
}
|
|
|
|
url = new URL(request.url, `http://${request.headers.host}`)
|
|
await logEvent('info', 'http.request', {
|
|
requestId,
|
|
method: request.method,
|
|
path: url.pathname,
|
|
query: Object.fromEntries(url.searchParams.entries()),
|
|
})
|
|
|
|
if (request.method === 'GET' && url.pathname === '/api/3d/health') {
|
|
const payload = {
|
|
ok: true,
|
|
providers: {
|
|
tripo: getTripoHealth(),
|
|
rodin: getRodinHealth(),
|
|
hunyuan: getHunyuanHealth(),
|
|
fal: getFalHealth(),
|
|
vision: getVisionHealth(),
|
|
},
|
|
}
|
|
sendJson(response, 200, payload)
|
|
await logEvent('info', 'http.response', { requestId, path: url.pathname, status: 200, durationMs: Date.now() - startedAt })
|
|
return
|
|
}
|
|
|
|
if (request.method === 'GET' && url.pathname === '/api/3d/logs') {
|
|
assertLocalDiagnosticsRequest(request)
|
|
const payload = await readRecentLogs(url.searchParams.get('limit') || 100)
|
|
sendJson(response, 200, payload)
|
|
await logEvent('info', 'http.response', { requestId, path: url.pathname, status: 200, durationMs: Date.now() - startedAt, entries: payload.entries.length })
|
|
return
|
|
}
|
|
|
|
if (request.method === 'POST' && url.pathname === '/api/3d/analyze') {
|
|
const payload = await readJsonBody(request)
|
|
await logEvent('info', 'asset.analyze.start', {
|
|
requestId,
|
|
payload: summarizePayload(payload),
|
|
})
|
|
const insight = await analyzeAssetImage(payload)
|
|
|
|
sendJson(response, 200, insight)
|
|
await logEvent('info', 'asset.analyze.success', {
|
|
requestId,
|
|
provider: insight.provider,
|
|
configured: insight.configured,
|
|
status: insight.status,
|
|
categoryId: insight.categoryId,
|
|
durationMs: Date.now() - startedAt,
|
|
})
|
|
return
|
|
}
|
|
|
|
if (request.method === 'POST' && url.pathname === '/api/3d/generate') {
|
|
const payload = await readJsonBody(request)
|
|
const provider = payload.provider || DEFAULT_GENERATION_PROVIDER
|
|
await logEvent('info', 'generation.create.start', {
|
|
requestId,
|
|
provider,
|
|
payload: summarizePayload(payload),
|
|
})
|
|
const task = await createGenerationTask(provider, payload)
|
|
|
|
sendJson(response, 200, task)
|
|
await logEvent('info', 'generation.create.success', {
|
|
requestId,
|
|
provider,
|
|
taskId: task.taskId,
|
|
status: task.status,
|
|
durationMs: Date.now() - startedAt,
|
|
})
|
|
return
|
|
}
|
|
|
|
if (request.method === 'GET' && url.pathname.startsWith('/api/3d/status/')) {
|
|
const taskId = decodeURIComponent(url.pathname.replace('/api/3d/status/', ''))
|
|
const provider = url.searchParams.get('provider') || DEFAULT_GENERATION_PROVIDER
|
|
const task = await getGenerationTask(provider, taskId)
|
|
|
|
sendJson(response, 200, task)
|
|
await logEvent('info', 'generation.status', {
|
|
requestId,
|
|
provider,
|
|
taskId,
|
|
status: task.status,
|
|
progress: task.progress,
|
|
hasModelUrl: Boolean(task.modelUrl),
|
|
error: task.error,
|
|
durationMs: Date.now() - startedAt,
|
|
})
|
|
return
|
|
}
|
|
|
|
if (request.method === 'GET' && url.pathname === '/api/3d/model') {
|
|
await proxyModel(url, response)
|
|
await logEvent('info', 'model.proxy.success', { requestId, durationMs: Date.now() - startedAt })
|
|
return
|
|
}
|
|
|
|
if (request.method === 'POST' && url.pathname === '/api/3d/local-model') {
|
|
const model = await importLocalModel(request, url)
|
|
sendJson(response, 200, model)
|
|
await logEvent('info', 'model.import.success', {
|
|
requestId,
|
|
taskId: model.taskId,
|
|
modelUrl: model.modelUrl,
|
|
fileName: model.fileName,
|
|
durationMs: Date.now() - startedAt,
|
|
})
|
|
return
|
|
}
|
|
|
|
if (request.method === 'GET' && url.pathname.startsWith('/api/3d/local-model/')) {
|
|
await serveLocalModel(url, response)
|
|
await logEvent('info', 'model.local.success', { requestId, path: url.pathname, durationMs: Date.now() - startedAt })
|
|
return
|
|
}
|
|
|
|
sendJson(response, 404, { error: 'Not found' })
|
|
await logEvent('warn', 'http.not_found', { requestId, path: url.pathname, durationMs: Date.now() - startedAt })
|
|
} catch (error) {
|
|
if (response.headersSent) {
|
|
await logEvent('error', 'http.stream_error', {
|
|
requestId,
|
|
path: url?.pathname,
|
|
durationMs: Date.now() - startedAt,
|
|
error: summarizeError(error),
|
|
})
|
|
response.destroy(error)
|
|
return
|
|
}
|
|
|
|
const status = error.status || 500
|
|
sendJson(response, status, {
|
|
error: error.message || 'Server error',
|
|
detail: error.detail,
|
|
})
|
|
await logEvent(status >= 500 ? 'error' : 'warn', 'http.error', {
|
|
requestId,
|
|
method: request.method,
|
|
path: url?.pathname,
|
|
status,
|
|
durationMs: Date.now() - startedAt,
|
|
error: summarizeError(error),
|
|
})
|
|
}
|
|
})
|
|
|
|
server.listen(API_PORT, API_HOST, () => {
|
|
console.log(`Bio demo API running at http://${API_HOST}:${API_PORT}`)
|
|
console.log(TRIPO_API_KEY ? 'Tripo API key loaded from environment.' : 'TRIPO_API_KEY is missing. Add it to .env.local.')
|
|
console.log(RODIN_API_KEY ? 'Rodin API key loaded from environment.' : 'RODIN_API_KEY is missing. Add it to .env.local.')
|
|
console.log(FAL_API_KEY ? 'Fal API key loaded from environment.' : 'FAL_API_KEY is missing. Add it to .env.local.')
|
|
console.log(getVisionHealth().configured ? 'Vision analysis provider configured.' : 'Vision analysis is not configured. Add OPENAI_API_KEY to .env.local.')
|
|
console.log(`Hunyuan3D local provider: ${HUNYUAN_API_BASE}`)
|
|
logEvent('info', 'api.start', {
|
|
host: API_HOST,
|
|
port: API_PORT,
|
|
providers: {
|
|
tripo: Boolean(TRIPO_API_KEY),
|
|
rodin: Boolean(RODIN_API_KEY),
|
|
fal: Boolean(FAL_API_KEY),
|
|
hunyuan: Boolean(HUNYUAN_API_BASE),
|
|
vision: getVisionHealth().configured,
|
|
},
|
|
})
|
|
})
|
|
|
|
function createGenerationTask(provider, payload) {
|
|
if (provider === 'hunyuan') return createHunyuanTask(payload)
|
|
if (provider === 'fal') return createFalTask(payload)
|
|
if (provider === 'tripo') return createTripoTask(payload)
|
|
return createRodinTask(payload)
|
|
}
|
|
|
|
function getGenerationTask(provider, taskId) {
|
|
if (provider === 'hunyuan') return getHunyuanTask(taskId)
|
|
if (provider === 'fal') return getFalTask(taskId)
|
|
if (provider === 'tripo') return getTripoTask(taskId)
|
|
return getRodinTask(taskId)
|
|
}
|