Spaces:
Sleeping
Sleeping
| 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 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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) | |
| 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) | |
| 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) | |
| 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) | |
| 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}) | |
| 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}) | |
| 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) | |
| 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") | |
| async def root(): return FileResponse("static/homepage.html") | |
| async def editor(): return FileResponse("static/index.html") | |