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")