forge3d / app.py
VISHAL18for4's picture
Upload app.py
92999dc verified
import os, httpx, asyncio
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","")
OPENROUTER_KEY = os.getenv("OPENROUTER_API_KEY","")
CUSTOM_SKILLS=""
try:
with open("SKILLS.md") as f: CUSTOM_SKILLS=f.read()
print("SKILLS.md loaded")
except: pass
# ── SYSTEM PROMPT ─────────────────────────────────────────────────────────────
BASE="""You are FORGE3D AI, a Three.js expert. Output ONLY raw JavaScript. No markdown, no backticks, no explanations.
CRITICAL RULES (breaking any = crash or invisible objects):
1. NEVER declare: const SCENE, const THREE, const OBJS, const CAM β€” already exist
2. EVERY mesh needs: SCENE.add(mesh) THEN OBJS.push({mesh,light:null,name:'X',type:'MESH',vis:true,kf:{}})
3. Every material: new THREE.MeshStandardMaterial({metalness:X, roughness:Y, color:'#hex'})
4. End always with: toast('description of what was built')
ANIMATION β€” EXACT PATTERN (use this for ANY moving object):
const _t0=Date.now();
(function _loop(){
requestAnimationFrame(_loop);
const _t=(Date.now()-_t0)*0.001;
mesh.position.x = _t * speed; // forward movement
mesh.rotation.y = _t * spinSpeed; // rotation
})();
CAR RECIPE (always build like this, 15+ parts):
const carG=new THREE.Group();
// Body
const bodyM=new THREE.MeshStandardMaterial({color:'#cc2200',metalness:0.7,roughness:0.3});
const body=new THREE.Mesh(new THREE.BoxGeometry(4,1,2),bodyM);
body.position.y=0.8; carG.add(body);
// Cabin
const cabin=new THREE.Mesh(new THREE.BoxGeometry(2.2,0.9,1.8),new THREE.MeshStandardMaterial({color:'#cc2200',metalness:0.7,roughness:0.3}));
cabin.position.set(0,1.7,0); carG.add(cabin);
// Windshield
const wsM=new THREE.MeshStandardMaterial({color:'#aaddff',metalness:0,roughness:0,transparent:true,opacity:0.4});
const ws=new THREE.Mesh(new THREE.BoxGeometry(0.1,0.7,1.6),wsM); ws.position.set(1.05,1.65,0); carG.add(ws);
// 4 wheels
[-1.4,1.4].forEach(x=>[-1.1,1.1].forEach(z=>{
const wh=new THREE.Mesh(new THREE.CylinderGeometry(0.4,0.4,0.3,24),new THREE.MeshStandardMaterial({color:'#111',metalness:0.3,roughness:0.8}));
wh.rotation.z=Math.PI/2; wh.position.set(x,0.4,z); carG.add(wh);
const hub=new THREE.Mesh(new THREE.CylinderGeometry(0.2,0.2,0.32,12),new THREE.MeshStandardMaterial({color:'#aaa',metalness:0.9,roughness:0.1}));
hub.rotation.z=Math.PI/2; hub.position.set(x,0.4,z); carG.add(hub);
}));
// Headlights
[0.9,-0.9].forEach(z=>{
const hl=new THREE.Mesh(new THREE.SphereGeometry(0.15,8,8),new THREE.MeshStandardMaterial({color:'#ffffff',emissive:'#ffffaa',emissiveIntensity:2}));
hl.position.set(2.05,0.85,z); carG.add(hl);
});
carG.castShadow=true;
SCENE.add(carG);
const _carObj={mesh:carG,light:null,name:'Sports Car',type:'MESH',vis:true,kf:{}};
OBJS.push(_carObj);
// ANIMATE β€” car moves forward
const _ct=Date.now();
(function _carLoop(){
requestAnimationFrame(_carLoop);
carG.position.z = ((Date.now()-_ct)*0.001*3)%40 - 20;
})();
const pl=new THREE.PointLight('#ff6600',2,15); pl.position.set(0,3,0); SCENE.add(pl);
OBJS.push({mesh:pl,light:pl,name:'Car Light',type:'LIGHT',vis:true,kf:{}});
toast('Sports car built and driving');
SOLAR SYSTEM RECIPE:
// Sun
const sun=new THREE.Mesh(new THREE.SphereGeometry(2,32,32),new THREE.MeshStandardMaterial({color:'#ff8800',emissive:'#ff4400',emissiveIntensity:2,metalness:0,roughness:1}));
SCENE.add(sun); OBJS.push({mesh:sun,light:null,name:'Sun',type:'MESH',vis:true,kf:{}});
// Planets with orbits
const planets=[{r:0.5,d:5,spd:1.2,col:'#4488ff'},{r:0.8,d:8,spd:0.7,col:'#44ff88'},{r:0.4,d:11,spd:0.4,col:'#ff4444'}];
const _pm=planets.map(p=>{
const m=new THREE.Mesh(new THREE.SphereGeometry(p.r,16,16),new THREE.MeshStandardMaterial({color:p.col,metalness:0.3,roughness:0.6}));
SCENE.add(m); OBJS.push({mesh:m,light:null,name:'Planet',type:'MESH',vis:true,kf:{}});
return {mesh:m,...p};
});
const _st=Date.now();
(function _solarLoop(){
requestAnimationFrame(_solarLoop);
const t=(Date.now()-_st)*0.001;
_pm.forEach(p=>{p.mesh.position.x=Math.cos(t*p.spd)*p.d;p.mesh.position.z=Math.sin(t*p.spd)*p.d;});
})();
SPACESHIP RECIPE (LatheGeometry + details):
const pts=[new THREE.Vector2(0,0),new THREE.Vector2(0.3,0.5),new THREE.Vector2(0.8,1.5),new THREE.Vector2(0.9,2.5),new THREE.Vector2(0.7,3.5),new THREE.Vector2(0.3,4),new THREE.Vector2(0,4.2)];
const hull=new THREE.Mesh(new THREE.LatheGeometry(pts,32),new THREE.MeshStandardMaterial({color:'#334455',metalness:0.8,roughness:0.2}));
SCENE.add(hull); OBJS.push({mesh:hull,light:null,name:'Ship Hull',type:'MESH',vis:true,kf:{}});
""" + (f"\nCUSTOM SKILLS:\n{CUSTOM_SKILLS}" if CUSTOM_SKILLS else "")
SCENE_SYS = BASE + "\nBuild complete environments 15-40 objects. Use loops for repeated elements."
DETAIL_SYS = BASE + "\nBuild hyper-detailed models 20-50 parts. LatheGeometry+ExtrudeGeometry+TubeGeometry every time."
ANIM_SYS = BASE + "\nAnimation specialist: write requestAnimationFrame loops. Moving objects use position changes. Spinning uses rotation. End with drawTL();applyKF();"
# ── API HELPERS ────────────────────────────────────────────────────────────────
OR_MODELS=["deepseek/deepseek-chat-v3-0324:free","meta-llama/llama-3.3-70b-instruct:free",
"nvidia/llama-3.1-nemotron-ultra-253b-v1:free","google/gemma-3-27b-it:free"]
async def groq_call(messages, sys, tokens=3500, temp=0.15):
for attempt in range(2):
async with httpx.AsyncClient(timeout=60) 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}]+messages,
"max_tokens":tokens,"temperature":temp,"stream":False})
if r.status_code==429 and attempt==0: await asyncio.sleep(5); continue
return r
return r
async def or_call(messages, sys, tokens=3500, temp=0.15, models=None):
for model in (models or OR_MODELS):
try:
async with httpx.AsyncClient(timeout=90) 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}]+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]: continue
if r.status_code in [429,503]: await asyncio.sleep(2); continue
except: continue
return None,"All OR models failed"
# ── 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",3500))
return JSONResponse(r.json(),status_code=r.status_code)
@app.post("/api/nemotron")
async def proxy_nemotron(req:Request):
b=await req.json(); msgs=b.get("messages",[]); tokens=b.get("max_tokens",3500)
# NVIDIA requires system="detailed thinking off", rules go in user message
if NEMOTRON_KEY:
nim_msgs=[]
for i,m in enumerate(msgs):
if m["role"]=="user" and i==len(msgs)-1:
nim_msgs.append({"role":"user","content":SCENE_SYS+"\n\n"+m["content"]})
else: nim_msgs.append(m)
try:
async with httpx.AsyncClient(timeout=90) as c:
r=await c.post("https://integrate.api.nvidia.com/v1/chat/completions",
headers={"Authorization":f"Bearer {NEMOTRON_KEY}","Content-Type":"application/json"},
json={"model":"nvidia/llama-3.1-nemotron-ultra-253b-v1",
"messages":[{"role":"system","content":"detailed thinking off"}]+nim_msgs,
"max_tokens":tokens,"temperature":0.15,"stream":False})
if r.status_code==200: return JSONResponse(r.json())
print(f"NVIDIA {r.status_code}: {r.text[:300]}")
except Exception as e: print(f"NVIDIA err: {e}")
# OpenRouter fallback
if OPENROUTER_KEY:
d,_=await or_call(msgs,SCENE_SYS,tokens,0.15,
["nvidia/llama-3.1-nemotron-ultra-253b-v1:free","deepseek/deepseek-chat-v3-0324:free"])
if d: return JSONResponse(d)
if GROQ_KEY:
r=await groq_call(msgs,SCENE_SYS,tokens); d=r.json(); d["_fallback"]="groq"; return JSONResponse(d)
return JSONResponse({"error":"No API keys"},status_code=500)
@app.post("/api/openrouter")
async def proxy_openrouter(req:Request):
b=await req.json(); msgs=b.get("messages",[]); tokens=b.get("max_tokens",3500)
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); d=r.json(); d["_fallback"]="groq"; return JSONResponse(d)
return JSONResponse({"error":"No API keys"},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",3500)
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.1); return JSONResponse(r.json())
return JSONResponse({"error":"No API keys"},status_code=500)
@app.post("/api/image-gen")
async def image_gen(req:Request):
"""Generate AI image via Pollinations.ai β€” no API key needed"""
b=await req.json(); prompt=b.get("prompt","abstract art"); w=b.get("w",512); h=b.get("h",512)
import urllib.parse
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):
"""Generate seamless texture via Pollinations and apply to selected mesh"""
b=await req.json(); desc=b.get("description","metal plate"); obj_name=b.get("object","object")
import urllib.parse
prompt=f"seamless tileable texture, {desc}, top-down flat surface, no shadows, game texture"
url=f"https://image.pollinations.ai/prompt/{urllib.parse.quote(prompt)}?width=512&height=512&nologo=true"
code=f"""const _obj=OBJS.find(o=>o.mesh===SEL?.mesh)||OBJS[OBJS.length-1];
if(_obj){{const _l=new THREE.TextureLoader();
_l.load('{url}',t=>{{t.wrapS=t.wrapT=THREE.RepeatWrapping;t.repeat.set(4,4);
_obj.mesh.material.map=t;_obj.mesh.material.needsUpdate=true;}});
toast('AI texture applied: {desc}');}};"""
return JSONResponse({"code":code,"url":url})
@app.post("/api/texture-search")
async def texture_search(req:Request):
b=await req.json(); q=b.get("query","metal"); obj=b.get("object","object")
prompt=f"""Apply texture to "{obj}" for: "{q}". Output ONLY raw JS.
const _o=OBJS.find(o=>o.mesh===SEL?.mesh)||OBJS[OBJS.length-1];
const _l=new THREE.TextureLoader();
_l.load('URL',t=>{{t.wrapS=t.wrapT=THREE.RepeatWrapping;t.repeat.set(3,3);_o.mesh.material.map=t;_o.mesh.material.needsUpdate=true;}});
toast('Texture applied');
PolyHaven options: metal_plate worn_metal oak_veneer concrete_wall_003 rocks_ground_01 mossy_cobblestone"""
if GROQ_KEY:
r=await groq_call([{"role":"user","content":prompt}],BASE,500)
return JSONResponse(r.json())
return JSONResponse({"error":"No key"},status_code=500)
@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=12) as c:
r=await c.post("https://integrate.api.nvidia.com/v1/chat/completions",
headers={"Authorization":f"Bearer {NEMOTRON_KEY}","Content-Type":"application/json"},
json={"model":"nvidia/llama-3.1-nemotron-ultra-253b-v1",
"messages":[{"role":"system","content":"detailed thinking off"},{"role":"user","content":"hi"}],"max_tokens":3})
res["nemotron"]="βœ… Nemotron Ultra Ready" if r.status_code==200 else f"⚠️ NVIDIA {r.status_code}: {r.text[:80]}"
except Exception as e: res["nemotron"]=f"⚠️ NVIDIA err: {str(e)[:40]}"
else: res["nemotron"]="❌ Missing NEMOTRON_API_KEY"
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":"deepseek/deepseek-chat-v3-0324:free","messages":[{"role":"user","content":"hi"}],"max_tokens":3})
res["openrouter"]="βœ… OpenRouter Ready (DeepSeek+Llama+Nemotron)" if r.status_code==200 else f"❌ OR {r.status_code}: {r.text[:80]}"
except Exception as e: res["openrouter"]=f"❌ OR: {str(e)[:40]}"
else: res["openrouter"]="❌ Missing OPENROUTER_API_KEY"
res["pollinations"]="βœ… Pollinations Ready (free AI images, no key)"
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")