forge3d / app.py
VISHAL18for4's picture
Upload app.py
3d502e6 verified
Raw History Blame Contribute Delete
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 ──────────────────────────────────────────────────
@app.post("/api/smart-build")
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 ────────────────────────────────────────────────────────
@app.post("/api/groq")
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())
@app.post("/api/openrouter")
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)
@app.post("/api/nemotron")
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)
@app.post("/api/animation")
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)
@app.post("/api/rig")
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)
@app.post("/api/image-gen")
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})
@app.post("/api/texture-gen")
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})
@app.get("/api/status")
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")
@app.get("/")
async def root(): return FileResponse("static/homepage.html")
@app.get("/editor")
async def editor(): return FileResponse("static/index.html")