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