Refactor server response handling and add frontend HTML
Browse files- Removed the inline HTML landing page from `server.py` and replaced it with a static `index.html` file in the `frontend` directory.
- Updated the root endpoint to serve the new HTML file.
- Enhanced the health check response to include the application version.
- Cleaned up unused imports in `server.py`.
- frontend/index.html +83 -0
- server.py +3 -88
frontend/index.html
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 6 |
+
<title>TRELLIS.2 API</title>
|
| 7 |
+
<style>
|
| 8 |
+
*{margin:0;padding:0;box-sizing:border-box}
|
| 9 |
+
:root{--bg:#0e0e10;--card:#18181b;--border:#27272a;--text:#e4e4e7;--dim:#71717a;--accent:#a78bfa;--green:#4ade80;--red:#f87171}
|
| 10 |
+
body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:2rem}
|
| 11 |
+
.wrap{max-width:480px;width:100%}
|
| 12 |
+
h1{font-size:1.6rem;font-weight:700;letter-spacing:-.02em;margin-bottom:.25rem}
|
| 13 |
+
.version{color:var(--dim);font-size:.85rem;margin-bottom:1.5rem}
|
| 14 |
+
.grid{display:grid;grid-template-columns:1fr 1fr;gap:.75rem;margin-bottom:1.5rem}
|
| 15 |
+
.stat{background:var(--card);border:1px solid var(--border);border-radius:.75rem;padding:1rem}
|
| 16 |
+
.stat-label{font-size:.7rem;text-transform:uppercase;letter-spacing:.08em;color:var(--dim);margin-bottom:.35rem}
|
| 17 |
+
.stat-value{font-size:1.5rem;font-weight:600;font-variant-numeric:tabular-nums}
|
| 18 |
+
.stat-value.ready{color:var(--green)}
|
| 19 |
+
.stat-value.not-ready{color:var(--red)}
|
| 20 |
+
.wide{grid-column:1/-1}
|
| 21 |
+
.endpoints{background:var(--card);border:1px solid var(--border);border-radius:.75rem;padding:1rem;margin-bottom:1rem}
|
| 22 |
+
.endpoints h2{font-size:.75rem;text-transform:uppercase;letter-spacing:.08em;color:var(--dim);margin-bottom:.6rem}
|
| 23 |
+
.ep{display:flex;justify-content:space-between;align-items:center;padding:.35rem 0;border-bottom:1px solid var(--border);font-size:.82rem}
|
| 24 |
+
.ep:last-child{border-bottom:none}
|
| 25 |
+
.ep-method{font-weight:600;color:var(--accent);min-width:3.2rem}
|
| 26 |
+
.ep-path{color:var(--text);font-family:ui-monospace,monospace;font-size:.78rem}
|
| 27 |
+
.pulse{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:.5rem;vertical-align:middle;animation:blink 2s ease-in-out infinite}
|
| 28 |
+
@keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
|
| 29 |
+
.footer{text-align:center;color:var(--dim);font-size:.7rem;margin-top:1rem}
|
| 30 |
+
</style>
|
| 31 |
+
</head>
|
| 32 |
+
<body>
|
| 33 |
+
<div class="wrap">
|
| 34 |
+
<h1>TRELLIS.2</h1>
|
| 35 |
+
<div class="version">Image-to-3D API · <span id="version">...</span></div>
|
| 36 |
+
<div class="grid">
|
| 37 |
+
<div class="stat wide">
|
| 38 |
+
<div class="stat-label">Worker Pool</div>
|
| 39 |
+
<div class="stat-value" id="pool"><span class="pulse" id="pulse"></span><span id="pool-text">...</span></div>
|
| 40 |
+
</div>
|
| 41 |
+
<div class="stat">
|
| 42 |
+
<div class="stat-label">Active Jobs</div>
|
| 43 |
+
<div class="stat-value"><span id="active">-</span><span style="color:var(--dim);font-size:.85rem"> / <span id="max">-</span></span></div>
|
| 44 |
+
</div>
|
| 45 |
+
<div class="stat">
|
| 46 |
+
<div class="stat-label">Queued</div>
|
| 47 |
+
<div class="stat-value" id="queued">-</div>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
<div class="endpoints">
|
| 51 |
+
<h2>Endpoints</h2>
|
| 52 |
+
<div class="ep"><span class="ep-method">POST</span><span class="ep-path">/v1/image-to-glb</span></div>
|
| 53 |
+
<div class="ep"><span class="ep-method">GET</span><span class="ep-path">/v1/jobs/{job_id}</span></div>
|
| 54 |
+
<div class="ep"><span class="ep-method">GET</span><span class="ep-path">/v1/jobs/{job_id}/result</span></div>
|
| 55 |
+
<div class="ep"><span class="ep-method">GET</span><span class="ep-path">/healthz</span></div>
|
| 56 |
+
</div>
|
| 57 |
+
<div class="footer">auto-refreshes every 3 s</div>
|
| 58 |
+
</div>
|
| 59 |
+
<script>
|
| 60 |
+
async function poll() {
|
| 61 |
+
try {
|
| 62 |
+
const response = await fetch("/healthz");
|
| 63 |
+
const data = await response.json();
|
| 64 |
+
const ok = data.worker_pool_ready;
|
| 65 |
+
document.getElementById("version").textContent = `v${data.version ?? "1.2.0"}`;
|
| 66 |
+
document.getElementById("pool-text").textContent = ok ? "Ready" : "Not Ready";
|
| 67 |
+
document.getElementById("pool").className = `stat-value ${ok ? "ready" : "not-ready"}`;
|
| 68 |
+
document.getElementById("pulse").style.background = ok ? "var(--green)" : "var(--red)";
|
| 69 |
+
document.getElementById("active").textContent = data.active_jobs;
|
| 70 |
+
document.getElementById("max").textContent = data.max_concurrent_jobs;
|
| 71 |
+
document.getElementById("queued").textContent = data.queued_jobs;
|
| 72 |
+
} catch {
|
| 73 |
+
document.getElementById("pool-text").textContent = "Unreachable";
|
| 74 |
+
document.getElementById("pool").className = "stat-value not-ready";
|
| 75 |
+
document.getElementById("pulse").style.background = "var(--red)";
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
poll();
|
| 80 |
+
setInterval(poll, 3000);
|
| 81 |
+
</script>
|
| 82 |
+
</body>
|
| 83 |
+
</html>
|
server.py
CHANGED
|
@@ -15,7 +15,7 @@ from typing import Annotated, Any
|
|
| 15 |
|
| 16 |
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
|
| 17 |
from fastapi.middleware.cors import CORSMiddleware
|
| 18 |
-
from fastapi.responses import FileResponse,
|
| 19 |
from PIL import Image, UnidentifiedImageError
|
| 20 |
|
| 21 |
import runtime_env # noqa: F401
|
|
@@ -477,6 +477,7 @@ async def healthz() -> dict[str, object]:
|
|
| 477 |
queue_state = state.queue.snapshot()
|
| 478 |
return {
|
| 479 |
"ok": state.executor is not None,
|
|
|
|
| 480 |
"worker_pool_ready": state.executor is not None,
|
| 481 |
"max_concurrent_jobs": MAX_CONCURRENT_JOBS,
|
| 482 |
"active_jobs": queue_state["active_jobs"],
|
|
@@ -485,95 +486,9 @@ async def healthz() -> dict[str, object]:
|
|
| 485 |
}
|
| 486 |
|
| 487 |
|
| 488 |
-
_LANDING_HTML = """\
|
| 489 |
-
<!DOCTYPE html>
|
| 490 |
-
<html lang="en">
|
| 491 |
-
<head>
|
| 492 |
-
<meta charset="utf-8">
|
| 493 |
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 494 |
-
<title>TRELLIS.2 API</title>
|
| 495 |
-
<style>
|
| 496 |
-
*{margin:0;padding:0;box-sizing:border-box}
|
| 497 |
-
:root{--bg:#0e0e10;--card:#18181b;--border:#27272a;--text:#e4e4e7;--dim:#71717a;--accent:#a78bfa;--green:#4ade80;--red:#f87171;--yellow:#facc15}
|
| 498 |
-
body{font-family:system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:2rem}
|
| 499 |
-
.wrap{max-width:480px;width:100%}
|
| 500 |
-
h1{font-size:1.6rem;font-weight:700;letter-spacing:-.02em;margin-bottom:.25rem}
|
| 501 |
-
.version{color:var(--dim);font-size:.85rem;margin-bottom:1.5rem}
|
| 502 |
-
.grid{display:grid;grid-template-columns:1fr 1fr;gap:.75rem;margin-bottom:1.5rem}
|
| 503 |
-
.stat{background:var(--card);border:1px solid var(--border);border-radius:.75rem;padding:1rem}
|
| 504 |
-
.stat-label{font-size:.7rem;text-transform:uppercase;letter-spacing:.08em;color:var(--dim);margin-bottom:.35rem}
|
| 505 |
-
.stat-value{font-size:1.5rem;font-weight:600;font-variant-numeric:tabular-nums}
|
| 506 |
-
.stat-value.ready{color:var(--green)}
|
| 507 |
-
.stat-value.not-ready{color:var(--red)}
|
| 508 |
-
.wide{grid-column:1/-1}
|
| 509 |
-
.endpoints{background:var(--card);border:1px solid var(--border);border-radius:.75rem;padding:1rem;margin-bottom:1rem}
|
| 510 |
-
.endpoints h2{font-size:.75rem;text-transform:uppercase;letter-spacing:.08em;color:var(--dim);margin-bottom:.6rem}
|
| 511 |
-
.ep{display:flex;justify-content:space-between;align-items:center;padding:.35rem 0;border-bottom:1px solid var(--border);font-size:.82rem}
|
| 512 |
-
.ep:last-child{border-bottom:none}
|
| 513 |
-
.ep-method{font-weight:600;color:var(--accent);min-width:3.2rem}
|
| 514 |
-
.ep-path{color:var(--text);font-family:ui-monospace,monospace;font-size:.78rem}
|
| 515 |
-
.pulse{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:.5rem;vertical-align:middle;animation:blink 2s ease-in-out infinite}
|
| 516 |
-
@keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
|
| 517 |
-
.footer{text-align:center;color:var(--dim);font-size:.7rem;margin-top:1rem}
|
| 518 |
-
</style>
|
| 519 |
-
</head>
|
| 520 |
-
<body>
|
| 521 |
-
<div class="wrap">
|
| 522 |
-
<h1>TRELLIS.2</h1>
|
| 523 |
-
<div class="version">Image-to-3D API · v""" + app.version + """</div>
|
| 524 |
-
<div class="grid">
|
| 525 |
-
<div class="stat wide">
|
| 526 |
-
<div class="stat-label">Worker Pool</div>
|
| 527 |
-
<div class="stat-value" id="pool"><span class="pulse" id="pulse"></span><span id="pool-text">...</span></div>
|
| 528 |
-
</div>
|
| 529 |
-
<div class="stat">
|
| 530 |
-
<div class="stat-label">Active Jobs</div>
|
| 531 |
-
<div class="stat-value"><span id="active">-</span><span style="color:var(--dim);font-size:.85rem"> / <span id="max">-</span></span></div>
|
| 532 |
-
</div>
|
| 533 |
-
<div class="stat">
|
| 534 |
-
<div class="stat-label">Queued</div>
|
| 535 |
-
<div class="stat-value" id="queued">-</div>
|
| 536 |
-
</div>
|
| 537 |
-
</div>
|
| 538 |
-
<div class="endpoints">
|
| 539 |
-
<h2>Endpoints</h2>
|
| 540 |
-
<div class="ep"><span class="ep-method">POST</span><span class="ep-path">/v1/image-to-glb</span></div>
|
| 541 |
-
<div class="ep"><span class="ep-method">GET</span><span class="ep-path">/v1/jobs/{job_id}</span></div>
|
| 542 |
-
<div class="ep"><span class="ep-method">GET</span><span class="ep-path">/v1/jobs/{job_id}/result</span></div>
|
| 543 |
-
<div class="ep"><span class="ep-method">GET</span><span class="ep-path">/healthz</span></div>
|
| 544 |
-
</div>
|
| 545 |
-
<div class="footer">auto-refreshes every 3 s</div>
|
| 546 |
-
</div>
|
| 547 |
-
<script>
|
| 548 |
-
async function poll(){
|
| 549 |
-
try{
|
| 550 |
-
const r=await fetch("/healthz");
|
| 551 |
-
const d=await r.json();
|
| 552 |
-
const ok=d.worker_pool_ready;
|
| 553 |
-
document.getElementById("pool-text").textContent=ok?"Ready":"Not Ready";
|
| 554 |
-
const pv=document.getElementById("pool");
|
| 555 |
-
pv.className="stat-value "+(ok?"ready":"not-ready");
|
| 556 |
-
document.getElementById("pulse").style.background=ok?"var(--green)":"var(--red)";
|
| 557 |
-
document.getElementById("active").textContent=d.active_jobs;
|
| 558 |
-
document.getElementById("max").textContent=d.max_concurrent_jobs;
|
| 559 |
-
document.getElementById("queued").textContent=d.queued_jobs;
|
| 560 |
-
}catch(e){
|
| 561 |
-
document.getElementById("pool-text").textContent="Unreachable";
|
| 562 |
-
document.getElementById("pool").className="stat-value not-ready";
|
| 563 |
-
document.getElementById("pulse").style.background="var(--red)";
|
| 564 |
-
}
|
| 565 |
-
}
|
| 566 |
-
poll();
|
| 567 |
-
setInterval(poll,3000);
|
| 568 |
-
</script>
|
| 569 |
-
</body>
|
| 570 |
-
</html>
|
| 571 |
-
"""
|
| 572 |
-
|
| 573 |
-
|
| 574 |
@app.get("/")
|
| 575 |
async def root():
|
| 576 |
-
return
|
| 577 |
|
| 578 |
|
| 579 |
@app.post("/v1/image-to-glb")
|
|
|
|
| 15 |
|
| 16 |
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
|
| 17 |
from fastapi.middleware.cors import CORSMiddleware
|
| 18 |
+
from fastapi.responses import FileResponse, JSONResponse
|
| 19 |
from PIL import Image, UnidentifiedImageError
|
| 20 |
|
| 21 |
import runtime_env # noqa: F401
|
|
|
|
| 477 |
queue_state = state.queue.snapshot()
|
| 478 |
return {
|
| 479 |
"ok": state.executor is not None,
|
| 480 |
+
"version": app.version,
|
| 481 |
"worker_pool_ready": state.executor is not None,
|
| 482 |
"max_concurrent_jobs": MAX_CONCURRENT_JOBS,
|
| 483 |
"active_jobs": queue_state["active_jobs"],
|
|
|
|
| 486 |
}
|
| 487 |
|
| 488 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
@app.get("/")
|
| 490 |
async def root():
|
| 491 |
+
return FileResponse(ROOT_DIR / "frontend" / "index.html")
|
| 492 |
|
| 493 |
|
| 494 |
@app.post("/v1/image-to-glb")
|