Mike0021's picture
Default backend: Qwen-Image-2.1-Turbo (hugging-apps/qwen-qwen-image-2-1-turbo); full Qwen-Image-2.1 and Fibo stay selectable
4de8d8b verified
Raw History Blame Contribute Delete
6.53 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>projtex: projection texturing API</title>
<style>
:root { --bg:#f7f7f5; --fg:#1b1b1b; --muted:#5d5d5d; --card:#fff; --line:#dcdcd8; --accent:#2f6f4f; --code:#f0f0ec; }
@media (prefers-color-scheme: dark) { :root { --bg:#141414; --fg:#ececec; --muted:#a9a9a9; --card:#1d1d1d; --line:#323232; --accent:#7fc59f; --code:#232323; } }
* { box-sizing: border-box; }
body { margin:0 auto; max-width: 980px; padding: 20px 16px 60px; background:var(--bg); color:var(--fg);
font: 15px/1.55 system-ui, -apple-system, Segoe UI, sans-serif; }
h1 { font-size: 26px; margin: 8px 0 4px; } h2 { font-size: 18px; margin: 28px 0 8px; }
p.lead { color: var(--muted); margin-top: 0; }
a { color: var(--accent); }
pre, code { font: 13px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; background: var(--code); border-radius: 6px; }
pre { padding: 12px; overflow-x: auto; } code { padding: 1px 4px; }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px; }
.card { background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: 12px; }
.card b { display:block; margin-bottom: 4px; }
img.hero { width: 100%; border-radius: 8px; border: 1px solid var(--line); }
form { display: grid; gap: 8px; background: var(--card); border: 1px solid var(--line); border-radius: 8px; padding: 12px; }
label { font-size: 13px; color: var(--muted); }
input, select, textarea, button { font: inherit; padding: 6px 8px; border-radius: 6px; border: 1px solid var(--line); background: var(--bg); color: var(--fg); width: 100%; }
button { background: var(--accent); color: #fff; border: 0; cursor: pointer; width: auto; padding: 8px 16px; }
.row { display:grid; grid-template-columns: repeat(auto-fit, minmax(160px,1fr)); gap: 8px; }
#out img { max-width: 100%; border-radius: 6px; margin-top: 8px; }
#log { white-space: pre-wrap; }
</style>
</head>
<body>
<h1>projtex: projection texturing API</h1>
<p class="lead">Send a 3D mesh and a prompt, get back a textured GLB. Images are generated from virtual cameras by
Qwen-Image-2.1-Turbo (default), the full Qwen-Image-2.1 or Bria Fibo 2.0 Edit Turbo. They are projected onto the geometry, blended
in texture space and baked to the mesh's UVs. Agents: the API is asynchronous and every response says what to do next.</p>
<div class="cards">
<div class="card"><b>Agent guide</b><a href="llms.txt">/llms.txt</a>: workflow, parameters, quality loop</div>
<div class="card"><b>REST + OpenAPI</b><a href="docs">/docs</a> (<a href="openapi.json">openapi.json</a>)</div>
<div class="card"><b>MCP</b><code>POST /mcp</code> (streamable HTTP), 13 tools</div>
<div class="card"><b>Free dry run</b><code>"backend": "mock"</code>: whole pipeline, no GPU quota</div>
</div>
<h2>Quick start</h2>
<pre id="curl"></pre>
<h2>MCP client config</h2>
<pre id="mcpcfg"></pre>
<p class="lead">Generation spends the <b>caller's</b> ZeroGPU quota (about 30 s per view). Send your Hugging Face token as
<code>X-HF-Token</code> so your own quota is used. It is kept in memory for the job only and never stored or logged.</p>
<h2>Try it</h2>
<form id="f">
<div class="row">
<div><label>Sample mesh</label><select id="sample"><option>lantern</option><option>avocado</option><option>head</option></select></div>
<div><label>Backend</label><select id="backend"><option value="mock">mock (free dry run)</option><option value="qwen-turbo" selected>qwen-turbo (default)</option><option value="qwen-space">qwen-space (full Qwen-Image-2.1)</option><option value="fibo-space">fibo-space (non-commercial)</option></select></div>
<div><label>Views</label><select id="preset"><option>minimal</option><option selected>balanced</option><option>dense</option></select></div>
</div>
<div><label>Prompt (materials and colours)</label><input id="prompt" value="black wrought iron with rust, weathered oak post with grain, warm frosted glass"></div>
<div><button type="submit">Texture it</button></div>
</form>
<div id="out"><pre id="log">No job yet.</pre></div>
<h2>Examples</h2>
<img class="hero" src="static/hero.jpg" alt="Input meshes and textured results" loading="lazy">
<script>
const base = location.origin;
document.getElementById('curl').textContent =
`# 1. upload your mesh (a GLB from Blender or three.js) -> upload_id
curl -s --data-binary @house.glb -H 'content-type: model/gltf-binary' ${base}/api/uploads
# 2. create a job (returns immediately); or use {"sample": "lantern"} instead of upload_id
curl -s -X POST ${base}/api/jobs -H 'content-type: application/json' -H "X-HF-Token: $HF_TOKEN" \\
-d '{"upload_id":"up_...","prompt":"a tidy suburban house","part_prompts":{"Walls":"white clapboard","Roof":"grey shingles"}}'
# 3. poll until state is "done", then follow "next" and download files.textured_glb
curl -s ${base}/api/jobs/<job_id>`;
document.getElementById('mcpcfg').textContent = JSON.stringify({mcpServers: {projtex: {type: "http", url: base + "/mcp",
headers: {"X-HF-Token": "hf_..."}}}}, null, 2);
const log = document.getElementById('log'), out = document.getElementById('out');
document.getElementById('f').onsubmit = async (e) => {
e.preventDefault();
const body = {sample: sample.value, backend: backend.value, preset: preset.value, prompt: prompt.value};
let r = await fetch('api/jobs', {method: 'POST', headers: {'content-type': 'application/json'}, body: JSON.stringify(body)});
let st = await r.json();
if (!r.ok) { log.textContent = JSON.stringify(st, null, 2); return; }
out.querySelectorAll('img,a.dl').forEach(n => n.remove());
while (st.state === 'queued' || st.state === 'running') {
const p = st.progress || {};
log.textContent = `job ${st.job_id}: ${st.state}, ${p.step || ''}` + (p.total ? ` (view ${p.done + 1}/${p.total}: ${p.view})` : '') +
`\nquota: ${st.quota_identity}`;
await new Promise(res => setTimeout(res, 4000));
st = await (await fetch('api/jobs/' + st.job_id)).json();
}
log.textContent = JSON.stringify({state: st.state, summary: st.summary, error: st.error, next: st.next}, null, 2);
if (st.files && st.files.contact_sheet) {
const img = document.createElement('img'); img.src = st.files.contact_sheet; out.appendChild(img);
const a = document.createElement('a'); a.className = 'dl'; a.href = st.files.textured_glb; a.textContent = 'Download textured GLB';
out.appendChild(a);
}
};
</script>
</body>
</html>