Spaces:
Sleeping
Sleeping
Download app.py from VISHAL18for4/forge3d: direct link, hf CLI and curl.
- Browser
- Download file 17.2 kB
-
https://huggingface.co/spaces/VISHAL18for4/forge3d/resolve/main/app.py
- Command line
-
hf download hf://spaces/VISHAL18for4/forge3d/app.py
-
curl -L -o app.py https://huggingface.co/spaces/VISHAL18for4/forge3d/resolve/main/app.py
17.2 kB
| import os, httpx, asyncio, json | |
| from fastapi import FastAPI, Request | |
| from fastapi.responses import FileResponse, JSONResponse | |
| from fastapi.staticfiles import StaticFiles | |
| app = FastAPI() | |
| GROQ_KEY = os.getenv("GROQ_API_KEY","") | |
| NEMOTRON_KEY = os.getenv("NEMOTRON_API_KEY","") # free at build.nvidia.com | |
| OPENROUTER_KEY = os.getenv("OPENROUTER_API_KEY","") | |
| # ββ NVIDIA NIM (free β build.nvidia.com, no credit card) βββββββββββββββββββββ | |
| NVIDIA_BASE = "https://integrate.api.nvidia.com/v1" | |
| NVIDIA_MODEL = "nvidia/llama-3.3-nemotron-super-49b-v1.5" # best free coding model | |
| NVIDIA_MODEL_B = "nvidia/llama-3.3-nemotron-super-49b-v1" # fallback | |
| CUSTOM_SKILLS="" | |
| try: | |
| with open("SKILLS.md") as f: CUSTOM_SKILLS=f.read() | |
| print("SKILLS.md loaded") | |
| except: pass | |
| # ββ WORKING FREE MODELS (verified June 2026) βββββββββββββββββββββββββββββββββ | |
| OR_MODELS = [ | |
| "meta-llama/llama-3.3-70b-instruct:free", | |
| "nvidia/llama-3.3-nemotron-super-49b-v1:free", | |
| "qwen/qwen-2.5-72b-instruct:free", | |
| "meta-llama/llama-3.1-8b-instruct:free", | |
| ] | |
| # ββ PLANNER SYSTEM PROMPT βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PLANNER_SYS = """You are a 3D modelling researcher for FORGE3D. | |
| When given an object name, output a detailed build blueprint. | |
| FORMAT (plain text, no markdown): | |
| OBJECT: [name] | |
| PARTS: [count] | |
| COMPONENTS: | |
| - [part]: [Three.js geometry] [size] [color hex] [metalness] [roughness] | |
| - (list every single visible part, surface detail, joint, panel) | |
| MATERIALS: | |
| - [name]: color, metalness, roughness | |
| ANIMATION: [what moves and how] | |
| SCALE: [size in Three.js units] | |
| Be extremely specific. List EVERY part. No vague descriptions. | |
| Output ONLY the blueprint, nothing else.""" | |
| # ββ CODE GENERATION SYSTEM PROMPT ββββββββββββββββββββββββββββββββββββββββββββ | |
| BASE = """You are FORGE3D AI β expert Three.js 3D sculptor. | |
| Output ONLY raw JavaScript. No markdown. No backticks. No comments. No explanations. | |
| GLOBALS (already exist, NEVER redeclare): | |
| THREE SCENE OBJS CAM SEL toast(msg) | |
| EVERY mesh needs: | |
| mesh.castShadow=true; mesh.receiveShadow=true; | |
| SCENE.add(mesh); | |
| OBJS.push({mesh,light:null,name:'Label',type:'MESH',vis:true,kf:{}}); | |
| MATERIALS always need both: | |
| new THREE.MeshStandardMaterial({color:'#hex',metalness:0-1,roughness:0-1}) | |
| ANIMATION pattern: | |
| const _t0=Date.now(); | |
| (function _loop(){ requestAnimationFrame(_loop); const _t=(Date.now()-_t0)*0.001; })(); | |
| RIGGING: Every joint = THREE.Group at pivot + child mesh offset: | |
| const jnt=new THREE.Group(); jnt.position.set(x,y,z); | |
| const mesh=new THREE.Mesh(geo,mat); mesh.position.y=-halfLen; | |
| jnt.add(mesh); parent.add(jnt); | |
| DETAIL RULES: | |
| - Characters: 40+ parts, joint groups, bevel rings (TorusGeometry), panel lines | |
| - Vehicles: 30+ parts, wheel pivot groups, ExtrudeGeometry panels | |
| - Props: 20+ parts, no single-box shortcuts | |
| - NEVER build a complex object as 1-2 boxes | |
| MATERIAL PRESETS: | |
| Chrome:{color:'#d4d4d4',metalness:0.98,roughness:0.02} | |
| Gold:{color:'#FFD700',metalness:0.92,roughness:0.08} | |
| DarkSteel:{color:'#1e2530',metalness:0.82,roughness:0.28} | |
| Rubber:{color:'#0a0a0a',metalness:0.05,roughness:0.95} | |
| Glass:{color:'#aaddff',metalness:0,roughness:0,transparent:true,opacity:0.2} | |
| """ + (f"\nCUSTOM SKILLS:\n{CUSTOM_SKILLS}" if CUSTOM_SKILLS else "") | |
| DETAIL_SYS = BASE + "\nBUILD MODE: 30-80 parts. Use joint groups. Use ExtrudeGeometry for panels. High segment counts (32+)." | |
| SCENE_SYS = BASE + "\nSCENE MODE: 20-50 objects. Add ground plane. Add fog. Add 3 colored PointLights." | |
| ANIM_SYS = BASE + "\nANIM MODE: Drive bones via group.rotation. Walking = sin waves PI-offset. End with drawTL();applyKF();" | |
| RIG_SYS = BASE + "\nRIG MODE: Every movable part = THREE.Group at pivot. Ball-socket joints. TorusGeometry bevels. ExtrudeGeometry panels." | |
| # ββ API HELPERS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def groq_call(messages, sys_prompt, tokens=4096, temp=0.12): | |
| if not GROQ_KEY: return None | |
| for attempt in range(3): | |
| try: | |
| async with httpx.AsyncClient(timeout=90) as c: | |
| r = await c.post( | |
| "https://api.groq.com/openai/v1/chat/completions", | |
| headers={"Authorization":f"Bearer {GROQ_KEY}","Content-Type":"application/json"}, | |
| json={"model":"llama-3.3-70b-versatile", | |
| "messages":[{"role":"system","content":sys_prompt}]+messages, | |
| "max_tokens":tokens,"temperature":temp,"stream":False}) | |
| if r.status_code == 429: | |
| await asyncio.sleep(5*(attempt+1)); continue | |
| if r.status_code == 200: | |
| return r | |
| print(f"Groq {r.status_code}: {r.text[:100]}") | |
| return None | |
| except Exception as e: | |
| print(f"Groq attempt {attempt}: {e}") | |
| await asyncio.sleep(2) | |
| return None | |
| async def or_call(messages, sys_prompt, tokens=4096, temp=0.12, models=None): | |
| if not OPENROUTER_KEY: return None, "No OR key" | |
| for model in (models or OR_MODELS): | |
| try: | |
| async with httpx.AsyncClient(timeout=120) as c: | |
| r = await c.post( | |
| "https://openrouter.ai/api/v1/chat/completions", | |
| headers={"Authorization":f"Bearer {OPENROUTER_KEY}", | |
| "Content-Type":"application/json", | |
| "HTTP-Referer":"https://huggingface.co","X-Title":"FORGE3D"}, | |
| json={"model":model, | |
| "messages":[{"role":"system","content":sys_prompt}]+messages, | |
| "max_tokens":tokens,"temperature":temp}) | |
| d = r.json() | |
| if r.status_code==200 and "choices" in d: | |
| d["_model_used"]=model; return d, None | |
| if r.status_code in [404,400]: | |
| print(f"OR {model} unavailable ({r.status_code})") | |
| continue | |
| if r.status_code in [429,503]: | |
| print(f"OR {model} rate limited, skipping") | |
| continue | |
| except Exception as e: | |
| print(f"OR {model}: {e}"); continue | |
| return None, "All OR models failed/unavailable" | |
| async def nvidia_call(messages, sys_prompt, tokens=4096, temp=0.1): | |
| """ | |
| NVIDIA NIM free API β best coding model available for free. | |
| Get key at build.nvidia.com (free dev account, no credit card) | |
| Set as HuggingFace secret: NEMOTRON_API_KEY | |
| Model: Nemotron Super 49B (49B params, 70B-level quality) | |
| """ | |
| if not NEMOTRON_KEY: return None | |
| for model in [NVIDIA_MODEL, NVIDIA_MODEL_B]: | |
| try: | |
| async with httpx.AsyncClient(timeout=120) as c: | |
| r = await c.post( | |
| f"{NVIDIA_BASE}/chat/completions", | |
| headers={"Authorization":f"Bearer {NEMOTRON_KEY}", | |
| "Content-Type":"application/json"}, | |
| json={"model":model, | |
| "messages":[{"role":"system","content":sys_prompt}]+messages, | |
| "max_tokens":tokens,"temperature":temp, | |
| "top_p":0.95,"stream":False}) | |
| if r.status_code==200: | |
| d=r.json(); d["_model_used"]=model | |
| print(f"NVIDIA {model} OK"); return d | |
| print(f"NVIDIA {model} {r.status_code}: {r.text[:100]}") | |
| if r.status_code in [404,400]: continue | |
| except Exception as e: print(f"NVIDIA: {e}") | |
| return None | |
| def get_text(d) -> str: | |
| """Safely extract text from an API response dict.""" | |
| if not d: return "" | |
| try: return d["choices"][0]["message"]["content"] or "" | |
| except: return "" | |
| def get_text_from_response(r) -> str: | |
| """Safely extract text from an httpx Response object.""" | |
| if r is None: return "" | |
| try: return r.json()["choices"][0]["message"]["content"] or "" | |
| except: return "" | |
| # ββ SMART BUILD: Plan β Code ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def smart_build(req: Request): | |
| try: | |
| b = await req.json() | |
| user_msg = b.get("prompt","").strip() | |
| tokens = b.get("max_tokens",4096) | |
| if not user_msg: | |
| return JSONResponse({"error":"No prompt"},status_code=400) | |
| # STEP 1: Plan with Groq (fast, reliable) | |
| plan_text = "" | |
| plan_r = await groq_call( | |
| [{"role":"user","content":f'Object to build in 3D: "{user_msg}"\nWrite the blueprint.'}], | |
| PLANNER_SYS, tokens=800, temp=0.2) | |
| if plan_r: | |
| plan_text = get_text_from_response(plan_r) | |
| if not plan_text: | |
| plan_text = f"Build a detailed 3D model of: {user_msg}. Use 30+ parts. Be specific about every component." | |
| # STEP 2: Build code from plan | |
| build_prompt = f"""Build this 3D model in Three.js. Follow the blueprint exactly. Use ALL listed parts. | |
| {plan_text} | |
| Output ONLY raw JavaScript. No markdown. No explanations. Minimum 30 parts.""" | |
| # Try OR first, then Groq fallback | |
| code_text = "" | |
| model_used = "" | |
| if OPENROUTER_KEY: | |
| d, _ = await or_call([{"role":"user","content":build_prompt}], DETAIL_SYS, tokens) | |
| if d: | |
| code_text = get_text(d) | |
| model_used = d.get("_model_used","openrouter") | |
| if not code_text and GROQ_KEY: | |
| r = await groq_call([{"role":"user","content":build_prompt}], DETAIL_SYS, tokens) | |
| if r: | |
| code_text = get_text_from_response(r) | |
| model_used = "groq-fallback" | |
| if not code_text: | |
| return JSONResponse({"error":"All AI models failed β check API keys"},status_code=503) | |
| # Clean code | |
| code_text = code_text.strip() | |
| for fence in ["```javascript","```js","```"]: | |
| code_text = code_text.replace(fence,"") | |
| code_text = code_text.strip() | |
| return JSONResponse({ | |
| "choices":[{"message":{"content":code_text}}], | |
| "_model_used": model_used, | |
| "_plan": plan_text, | |
| "_pipeline": "smart-build" | |
| }) | |
| except Exception as e: | |
| import traceback | |
| print(f"smart-build error: {traceback.format_exc()}") | |
| return JSONResponse({"error":f"Server error: {str(e)}"},status_code=500) | |
| # ββ STANDARD ENDPOINTS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def proxy_groq(req: Request): | |
| if not GROQ_KEY: return JSONResponse({"error":"GROQ_API_KEY not set"},status_code=500) | |
| b = await req.json() | |
| r = await groq_call(b.get("messages",[]), BASE, b.get("max_tokens",4096)) | |
| if r is None: return JSONResponse({"error":"Groq unavailable"},status_code=503) | |
| return JSONResponse(r.json()) | |
| async def proxy_openrouter(req: Request): | |
| b = await req.json(); msgs=b.get("messages",[]); tokens=b.get("max_tokens",4096) | |
| if OPENROUTER_KEY: | |
| d,_ = await or_call(msgs, DETAIL_SYS, tokens) | |
| if d: return JSONResponse(d) | |
| if GROQ_KEY: | |
| r = await groq_call(msgs, DETAIL_SYS, tokens) | |
| if r: | |
| d=r.json(); d["_fallback"]="groq"; return JSONResponse(d) | |
| return JSONResponse({"error":"No API keys"},status_code=500) | |
| async def proxy_nemotron(req: Request): | |
| """Uses NVIDIA Nemotron Super 49B β best free coding model.""" | |
| b = await req.json(); msgs=b.get("messages",[]); tokens=b.get("max_tokens",4096) | |
| if NEMOTRON_KEY: | |
| d = await nvidia_call(msgs, SCENE_SYS, tokens) | |
| if d: return JSONResponse(d) | |
| if OPENROUTER_KEY: | |
| d,_ = await or_call(msgs, SCENE_SYS, tokens) | |
| if d: return JSONResponse(d) | |
| if GROQ_KEY: | |
| r = await groq_call(msgs, SCENE_SYS, tokens) | |
| if r: | |
| d=r.json(); d["_fallback"]="groq"; return JSONResponse(d) | |
| return JSONResponse({"error":"No API keys β set NEMOTRON_API_KEY at build.nvidia.com"},status_code=500) | |
| async def proxy_animation(req: Request): | |
| b = await req.json(); msgs=b.get("messages",[]); tokens=b.get("max_tokens",4096) | |
| if OPENROUTER_KEY: | |
| d,_ = await or_call(msgs, ANIM_SYS, tokens) | |
| if d: return JSONResponse(d) | |
| if GROQ_KEY: | |
| r = await groq_call(msgs, ANIM_SYS, tokens, 0.08) | |
| if r: return JSONResponse(r.json()) | |
| return JSONResponse({"error":"No API keys"},status_code=500) | |
| async def proxy_rig(req: Request): | |
| b = await req.json(); msgs=b.get("messages",[]); tokens=b.get("max_tokens",4096) | |
| if OPENROUTER_KEY: | |
| d,_ = await or_call(msgs, RIG_SYS, tokens, 0.10) | |
| if d: return JSONResponse(d) | |
| if GROQ_KEY: | |
| r = await groq_call(msgs, RIG_SYS, tokens, 0.10) | |
| if r: | |
| d=r.json(); d["_fallback"]="groq"; return JSONResponse(d) | |
| return JSONResponse({"error":"No API keys"},status_code=500) | |
| async def image_gen(req: Request): | |
| import urllib.parse | |
| b=await req.json(); prompt=b.get("prompt","art"); w=b.get("w",512); h=b.get("h",512) | |
| url=f"https://image.pollinations.ai/prompt/{urllib.parse.quote(prompt)}?width={w}&height={h}&nologo=true&enhance=true" | |
| return JSONResponse({"url":url,"prompt":prompt}) | |
| async def texture_gen(req: Request): | |
| import urllib.parse | |
| b=await req.json(); desc=b.get("description","metal") | |
| url=f"https://image.pollinations.ai/prompt/{urllib.parse.quote('seamless tileable '+desc+' texture top-down no shadows')}?width=512&height=512&nologo=true" | |
| code=f"""const _o=OBJS.find(o=>o.mesh===SEL?.mesh)||OBJS[OBJS.length-1]; | |
| if(_o){{new THREE.TextureLoader().load('{url}',t=>{{t.wrapS=t.wrapT=THREE.RepeatWrapping;t.repeat.set(4,4);_o.mesh.material.map=t;_o.mesh.material.needsUpdate=true;}});toast('Texture: {desc}');}}""" | |
| return JSONResponse({"code":code,"url":url}) | |
| async def status(): | |
| res={} | |
| if GROQ_KEY: | |
| try: | |
| async with httpx.AsyncClient(timeout=8) as c: | |
| r=await c.post("https://api.groq.com/openai/v1/chat/completions", | |
| headers={"Authorization":f"Bearer {GROQ_KEY}","Content-Type":"application/json"}, | |
| json={"model":"llama-3.3-70b-versatile","messages":[{"role":"user","content":"hi"}],"max_tokens":3}) | |
| res["groq"]="β Groq Ready" if r.status_code==200 else f"β Groq {r.status_code}" | |
| except Exception as e: res["groq"]=f"β Groq: {str(e)[:30]}" | |
| else: res["groq"]="β Missing GROQ_API_KEY" | |
| if NEMOTRON_KEY: | |
| try: | |
| async with httpx.AsyncClient(timeout=15) as c: | |
| r = await c.post(f"{NVIDIA_BASE}/chat/completions", | |
| headers={"Authorization":f"Bearer {NEMOTRON_KEY}","Content-Type":"application/json"}, | |
| json={"model":NVIDIA_MODEL,"messages":[{"role":"user","content":"hi"}],"max_tokens":3}) | |
| if r.status_code==200: | |
| res["nemotron"]=f"β NVIDIA Nemotron Super 49B Ready" | |
| else: | |
| res["nemotron"]=f"β NVIDIA {r.status_code}: {r.text[:80]}" | |
| except Exception as e: res["nemotron"]=f"β NVIDIA: {str(e)[:50]}" | |
| else: | |
| res["nemotron"]="β οΈ No NEMOTRON_API_KEY β get FREE key at build.nvidia.com" | |
| if OPENROUTER_KEY: | |
| try: | |
| async with httpx.AsyncClient(timeout=10) as c: | |
| r=await c.post("https://openrouter.ai/api/v1/chat/completions", | |
| headers={"Authorization":f"Bearer {OPENROUTER_KEY}","Content-Type":"application/json", | |
| "HTTP-Referer":"https://huggingface.co","X-Title":"FORGE3D"}, | |
| json={"model":"meta-llama/llama-3.3-70b-instruct:free","messages":[{"role":"user","content":"hi"}],"max_tokens":3}) | |
| res["openrouter"]="β OpenRouter Ready" if r.status_code==200 else f"β οΈ OR {r.status_code}" | |
| except Exception as e: res["openrouter"]=f"β OR: {str(e)[:40]}" | |
| else: res["openrouter"]="β Missing OPENROUTER_API_KEY" | |
| res["smart_build"]="β /api/smart-build β PlanβBuild pipeline active" | |
| res["pollinations"]="β Pollinations (free textures)" | |
| res["skills"]="β SKILLS.md loaded" if CUSTOM_SKILLS else "βΉοΈ No SKILLS.md" | |
| return res | |
| app.mount("/static",StaticFiles(directory="static"),name="static") | |
| async def root(): return FileResponse("static/homepage.html") | |
| async def editor(): return FileResponse("static/index.html") | |