Isometric-City / app.py
victor's picture
victor HF Staff
Isometric City: infinite grid city builder with FLUX Klein 9B
dfb0c26
import os
import json
import shutil
import time
import threading
from gradio import Server
from gradio_client import Client
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
# ---------------------------------------------------------------------------
# Persistent storage
# ---------------------------------------------------------------------------
DATA_DIR = os.environ.get("DATA_DIR", "/data")
GRID_FILE = os.path.join(DATA_DIR, "grid.json")
IMAGES_DIR = os.path.join(DATA_DIR, "images")
os.makedirs(IMAGES_DIR, exist_ok=True)
_grid_lock = threading.Lock()
def load_grid():
with _grid_lock:
if os.path.exists(GRID_FILE):
with open(GRID_FILE) as f:
return json.load(f)
return {}
def save_grid(grid):
with _grid_lock:
with open(GRID_FILE, "w") as f:
json.dump(grid, f)
# ---------------------------------------------------------------------------
# FLUX Klein client (lazy init — Space may be sleeping)
# ---------------------------------------------------------------------------
_flux = None
def get_flux():
global _flux
if _flux is None:
_flux = Client("black-forest-labs/flux-klein-9b-kv")
return _flux
# ---------------------------------------------------------------------------
# Server
# ---------------------------------------------------------------------------
app = Server()
# Serve generated images as static files
app.mount("/images", StaticFiles(directory=IMAGES_DIR), name="images")
@app.get("/", response_class=HTMLResponse)
async def homepage():
html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
with open(html_path, "r", encoding="utf-8") as f:
return f.read()
@app.api(name="get_grid")
def get_grid() -> str:
"""Return all placed buildings as JSON."""
return json.dumps(load_grid())
@app.api(name="place_building")
def place_building(x: int, y: int, prompt: str) -> str:
"""Generate an isometric building and place it on the grid."""
key = f"{x},{y}"
grid = load_grid()
if key in grid:
return json.dumps({"error": "Tile already occupied"})
full_prompt = (
f"isometric building, white background, game asset, "
f"single building centered, no ground, no shadow, clean edges, "
f"{prompt}"
)
try:
result, seed = get_flux().predict(
prompt=full_prompt,
input_images=[],
seed=0,
randomize_seed=True,
width=512,
height=512,
num_inference_steps=4,
prompt_upsampling=False,
api_name="/generate",
)
except Exception as e:
return json.dumps({"error": str(e)})
# Copy generated image to persistent storage
filename = f"{x}_{y}.webp"
dest = os.path.join(IMAGES_DIR, filename)
shutil.copy2(result, dest)
grid[key] = {
"prompt": prompt,
"seed": seed,
"ts": int(time.time()),
}
save_grid(grid)
return json.dumps({"ok": True, "image": f"/images/{filename}", "seed": seed})
@app.api(name="remove_building")
def remove_building(x: int, y: int) -> str:
"""Remove a building from the grid."""
key = f"{x},{y}"
grid = load_grid()
if key not in grid:
return json.dumps({"error": "No building at this tile"})
del grid[key]
save_grid(grid)
img_path = os.path.join(IMAGES_DIR, f"{x}_{y}.webp")
if os.path.exists(img_path):
os.remove(img_path)
return json.dumps({"ok": True})
demo = app
if __name__ == "__main__":
demo.launch(show_error=True, ssr_mode=False)