feat(rsplat): default to image upload + switch job queue to Hunyuan3D
- Default upload tab is now "Generate from Image" - Entire dotted drop area is clickable to open file browser - Update process3DGenJob to use Hunyuan3D v2.1 via fal.ai queue API (was still using old Trellis endpoint) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d950354dfd
commit
fd63c2bc6f
|
|
@ -723,20 +723,20 @@ async function sendSplatEmail(job: Gen3DJob) {
|
||||||
|
|
||||||
async function process3DGenJob(job: Gen3DJob) {
|
async function process3DGenJob(job: Gen3DJob) {
|
||||||
job.status = "processing";
|
job.status = "processing";
|
||||||
try {
|
const falHeaders = { Authorization: `Key ${FAL_KEY}`, "Content-Type": "application/json" };
|
||||||
const res = await fetch("https://fal.run/fal-ai/trellis", {
|
const MODEL = "fal-ai/hunyuan3d-v21";
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Key ${FAL_KEY}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ image_url: job.imageUrl }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
try {
|
||||||
const errText = await res.text();
|
// 1. Submit to fal.ai queue
|
||||||
console.error("[3d-gen] fal.ai error:", res.status, errText);
|
const submitRes = await fetch(`https://queue.fal.run/${MODEL}`, {
|
||||||
let detail = "3D generation failed";
|
method: "POST",
|
||||||
|
headers: falHeaders,
|
||||||
|
body: JSON.stringify({ input_image_url: job.imageUrl, textured_mesh: true, octree_resolution: 256 }),
|
||||||
|
});
|
||||||
|
if (!submitRes.ok) {
|
||||||
|
const errText = await submitRes.text();
|
||||||
|
console.error("[3d-gen] fal.ai submit error:", submitRes.status, errText);
|
||||||
|
let detail = "3D generation failed to start";
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(errText);
|
const parsed = JSON.parse(errText);
|
||||||
if (parsed.detail) {
|
if (parsed.detail) {
|
||||||
|
|
@ -750,9 +750,48 @@ async function process3DGenJob(job: Gen3DJob) {
|
||||||
job.completedAt = Date.now();
|
job.completedAt = Date.now();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const { request_id } = await submitRes.json() as { request_id: string };
|
||||||
|
|
||||||
const data = await res.json();
|
// 2. Poll for completion (up to 5 min)
|
||||||
const modelUrl = data.glb_url || data.model_mesh?.url || data.output?.url;
|
const deadline = Date.now() + 300_000;
|
||||||
|
let completed = false;
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
await new Promise((r) => setTimeout(r, 3000));
|
||||||
|
const statusRes = await fetch(
|
||||||
|
`https://queue.fal.run/${MODEL}/requests/${request_id}/status`,
|
||||||
|
{ headers: falHeaders },
|
||||||
|
);
|
||||||
|
if (!statusRes.ok) continue;
|
||||||
|
const status = await statusRes.json() as { status: string };
|
||||||
|
if (status.status === "COMPLETED") { completed = true; break; }
|
||||||
|
if (status.status === "FAILED") {
|
||||||
|
job.status = "failed";
|
||||||
|
job.error = "3D generation failed on fal.ai";
|
||||||
|
job.completedAt = Date.now();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!completed) {
|
||||||
|
job.status = "failed";
|
||||||
|
job.error = "3D generation timed out";
|
||||||
|
job.completedAt = Date.now();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fetch result
|
||||||
|
const resultRes = await fetch(
|
||||||
|
`https://queue.fal.run/${MODEL}/requests/${request_id}`,
|
||||||
|
{ headers: falHeaders },
|
||||||
|
);
|
||||||
|
if (!resultRes.ok) {
|
||||||
|
job.status = "failed";
|
||||||
|
job.error = "Failed to retrieve 3D model";
|
||||||
|
job.completedAt = Date.now();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await resultRes.json();
|
||||||
|
const modelUrl = data.model_glb?.url || data.model_glb_pbr?.url;
|
||||||
if (!modelUrl) {
|
if (!modelUrl) {
|
||||||
job.status = "failed";
|
job.status = "failed";
|
||||||
job.error = "No 3D model returned";
|
job.error = "No 3D model returned";
|
||||||
|
|
@ -760,6 +799,7 @@ async function process3DGenJob(job: Gen3DJob) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Download and save
|
||||||
const modelRes = await fetch(modelUrl);
|
const modelRes = await fetch(modelUrl);
|
||||||
if (!modelRes.ok) {
|
if (!modelRes.ok) {
|
||||||
job.status = "failed";
|
job.status = "failed";
|
||||||
|
|
@ -769,14 +809,13 @@ async function process3DGenJob(job: Gen3DJob) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelBuf = await modelRes.arrayBuffer();
|
const modelBuf = await modelRes.arrayBuffer();
|
||||||
const ext = modelUrl.includes(".ply") ? "ply" : "glb";
|
const filename = `splat-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.glb`;
|
||||||
const filename = `splat-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.${ext}`;
|
|
||||||
const dir = resolve(process.env.FILES_DIR || "./data/files", "generated");
|
const dir = resolve(process.env.FILES_DIR || "./data/files", "generated");
|
||||||
await Bun.write(resolve(dir, filename), modelBuf);
|
await Bun.write(resolve(dir, filename), modelBuf);
|
||||||
|
|
||||||
job.status = "complete";
|
job.status = "complete";
|
||||||
job.resultUrl = `/data/files/generated/${filename}`;
|
job.resultUrl = `/data/files/generated/${filename}`;
|
||||||
job.resultFormat = ext;
|
job.resultFormat = "glb";
|
||||||
job.completedAt = Date.now();
|
job.completedAt = Date.now();
|
||||||
console.log(`[3d-gen] Job ${job.id} complete: ${job.resultUrl}`);
|
console.log(`[3d-gen] Job ${job.id} complete: ${job.resultUrl}`);
|
||||||
|
|
||||||
|
|
@ -1171,7 +1210,7 @@ app.post("/api/image-stage", async (c) => {
|
||||||
return c.json({ url: `${PUBLIC_ORIGIN}/data/files/generated/${filename}` });
|
return c.json({ url: `${PUBLIC_ORIGIN}/data/files/generated/${filename}` });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Image-to-3D via fal.ai Trellis (async job queue)
|
// Image-to-3D via fal.ai Hunyuan3D v2.1 (async job queue)
|
||||||
app.post("/api/3d-gen", async (c) => {
|
app.post("/api/3d-gen", async (c) => {
|
||||||
if (!FAL_KEY) return c.json({ error: "FAL_KEY not configured" }, 503);
|
if (!FAL_KEY) return c.json({ error: "FAL_KEY not configured" }, 503);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue