Spaces:
Running
Running
Download app/static/index.html from townbox/projection-texturing: direct link, hf CLI and curl.
- Browser
- Download file 6.53 kB
-
https://huggingface.co/spaces/townbox/projection-texturing/resolve/main/app/static/index.html
- Command line
-
hf download hf://spaces/townbox/projection-texturing/app/static/index.html
-
curl -L -o index.html https://huggingface.co/spaces/townbox/projection-texturing/resolve/main/app/static/index.html
6.53 kB
| <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> | |