Spaces:
Runtime error
Runtime error
| """Asterism Relay — a first-person explorable cosmos for the Build Small hackathon. | |
| - Custom Three.js / WebGL frontend served from gr.Server (Off-Brand). | |
| - Backend deterministically lays out a universe of real-phenomenon bodies. | |
| - As the player approaches each body, MiniCPM4.1-8B (OpenBMB) running on Modal | |
| AUTHORS that sector: names the body, selects which curated facts to surface, | |
| and writes a fragment of a vanished civilization's final transmissions. | |
| - The FACT layer is composed from a curated local knowledge base, so the science | |
| is always accurate; the FICTION layer is freely authored by the model. | |
| """ | |
| import json | |
| import os | |
| import random | |
| from functools import lru_cache | |
| from pathlib import Path | |
| from typing import Any | |
| import httpx | |
| from fastapi.responses import HTMLResponse, JSONResponse | |
| from fastapi.staticfiles import StaticFiles | |
| from gradio import Server | |
| ROOT = Path(__file__).parent | |
| DATA = ROOT / "data" | |
| STATIC = ROOT / "static" | |
| # Deployed Modal endpoint (scripts/minicpm_modal.py). Set in the Space secrets. | |
| MODAL_AUTHOR_URL = os.environ.get("MODAL_AUTHOR_URL", "").strip() | |
| # Universe layout parameters. | |
| NUM_BODIES = 26 | |
| UNIVERSE_SEED = 70707 | |
| SPAN_XZ = 7000 | |
| SPAN_Y = 1400 | |
| MIN_DISTANCE = 1500 | |
| app = Server() | |
| app.mount("/static", StaticFiles(directory=STATIC), name="static") | |
| app.mount("/data", StaticFiles(directory=DATA), name="data") | |
| def load_facts() -> list[dict[str, Any]]: | |
| return json.loads((DATA / "astro_facts.json").read_text(encoding="utf-8")) | |
| def build_universe() -> dict[str, Any]: | |
| """Deterministic body field. Each body is assigned a real phenomenon; appearance | |
| is drawn instantly in-browser from its visual cues, content is authored on approach.""" | |
| facts = load_facts() | |
| rng = random.Random(UNIVERSE_SEED) | |
| # A shuffled bag so every phenomenon appears, roughly evenly. | |
| bag: list[dict[str, Any]] = [] | |
| while len(bag) < NUM_BODIES: | |
| order = facts[:] | |
| rng.shuffle(order) | |
| bag.extend(order) | |
| bag = bag[:NUM_BODIES] | |
| bodies: list[dict[str, Any]] = [] | |
| placed: list[tuple[float, float, float]] = [] | |
| for i, concept in enumerate(bag): | |
| for _ in range(200): | |
| x = rng.uniform(-SPAN_XZ, SPAN_XZ) | |
| y = rng.uniform(-SPAN_Y, SPAN_Y) | |
| z = rng.uniform(-SPAN_XZ, SPAN_XZ) | |
| if all((x - px) ** 2 + (y - py) ** 2 + (z - pz) ** 2 > MIN_DISTANCE ** 2 for px, py, pz in placed): | |
| break | |
| placed.append((x, y, z)) | |
| vc = concept["visual_cues"] | |
| bodies.append({ | |
| "id": f"body-{i:02d}", | |
| "concept_id": concept["id"], | |
| "name": concept["name"], | |
| "body_type": concept["body_type"], | |
| "category": concept.get("category", "star"), | |
| "position": {"x": round(x, 1), "y": round(y, 1), "z": round(z, 1)}, | |
| "visual": vc, | |
| }) | |
| return { | |
| "seed": UNIVERSE_SEED, | |
| "intro": ( | |
| "Long before you, the Last Cartographers mapped this whole sky — then fell silent. " | |
| "Their final transmissions are scattered across real stars and ruins. Fly out and read them." | |
| ), | |
| "bodies": bodies, | |
| } | |
| def compose_fallback(body: dict[str, Any]) -> dict[str, Any]: | |
| """Deterministic authoring used when the model endpoint is cold or unavailable — | |
| the app never stalls. Fact layer from curated facts; fiction from curated fragments.""" | |
| facts = {f["id"]: f for f in load_facts()} | |
| concept = facts[body["concept_id"]] | |
| rng = random.Random(f"{body['id']}:{concept['id']}") | |
| ids = sorted(rng.sample(range(len(concept["facts"])), k=min(3, len(concept["facts"])))) | |
| explanation = " ".join([concept["one_line"]] + [concept["facts"][i] for i in ids]) | |
| vc = concept["visual_cues"] | |
| return { | |
| "sector_key": body["id"], | |
| "authored_by": "curated-fallback", | |
| "body": { | |
| "id": f"{concept['id']}-{rng.randrange(100, 999)}", | |
| "name": concept["name"], | |
| "type": concept["body_type"], | |
| "category": concept.get("category", "star"), | |
| "phenomenon": concept["name"], | |
| "position": body["position"], | |
| }, | |
| "fact_layer": { | |
| "source": "curated_local_knowledge_base", | |
| "concept_id": concept["id"], | |
| "title": concept["name"], | |
| "fact_ids": ids, | |
| "explanation": explanation, | |
| }, | |
| "fiction_layer": { | |
| "civilization": "the Last Cartographers", | |
| "fragment_id": f"transmission-{rng.randrange(1, 99):02d}", | |
| "transmission": rng.choice(concept["transmissions"]), | |
| }, | |
| "shader": { | |
| "seed": rng.randrange(100000, 9999999), | |
| "render": vc["render"], | |
| "primary": vc["primary_color"], | |
| "secondary": vc["secondary_color"], | |
| "emissive": vc["emissive"], | |
| "radius": vc["radius"], | |
| "beam": vc.get("beam", False), | |
| "corona": vc.get("corona", False), | |
| "rings": vc.get("rings", False), | |
| "jets": vc.get("jets", False), | |
| "particles": vc.get("particles", vc["secondary_color"]), | |
| "noise_scale": round(1.8 + rng.random() * 3.0, 3), | |
| "atmosphere": round(0.3 + rng.random() * 0.5, 3), | |
| }, | |
| } | |
| def author_sector(body_id: str) -> str: | |
| """Author one sector via the Modal MiniCPM endpoint, falling back to curated | |
| composition. Cached so each body is authored once per process.""" | |
| universe = build_universe() | |
| body = next((b for b in universe["bodies"] if b["id"] == body_id), None) | |
| if body is None: | |
| return json.dumps({"error": "unknown body"}) | |
| if MODAL_AUTHOR_URL: | |
| pos = body["position"] | |
| params = { | |
| "sector_key": body_id, | |
| "position": f"{pos['x']},{pos['y']},{pos['z']}", | |
| "concept_id": body["concept_id"], | |
| } | |
| try: | |
| resp = httpx.get(MODAL_AUTHOR_URL, params=params, timeout=25.0, follow_redirects=True) | |
| resp.raise_for_status() | |
| return json.dumps(resp.json()) | |
| except Exception: | |
| pass # fall through to deterministic composition | |
| return json.dumps(compose_fallback(body)) | |
| async def universe_manifest(): | |
| return JSONResponse(build_universe()) | |
| async def sector_endpoint(body_id: str): | |
| return JSONResponse(json.loads(author_sector(body_id))) | |
| async def homepage(): | |
| return HTMLResponse((STATIC / "index.html").read_text(encoding="utf-8")) | |
| # Hugging Face Spaces (gradio SDK) imports this module and launches a top-level | |
| # object named `demo`; it does not run the __main__ block below. | |
| demo = app | |
| if __name__ == "__main__": | |
| app.launch(server_name="0.0.0.0", server_port=7860) | |