Mike0021's picture
fix: HF_HOME to /tmp/hf_cache for non-bucket space
17bdf9e verified
Raw
History Blame Contribute Delete
13.6 kB
import os
os.environ.setdefault("HF_HOME", "/tmp/hf_cache")
os.environ.setdefault("HF_MODULES_CACHE", "/tmp/hf_modules")
os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")
import spaces
import gradio as gr
import numpy as np
import torch
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from PIL import Image
import io
import time
from terrain_diffusion.inference.world_pipeline import WorldPipeline
from terrain_diffusion.inference.relief_map import get_relief_map
MODEL_ID = "xandergos/terrain-diffusion-30m"
# Global pipeline β€” loaded once at startup
_world = None
def _get_world():
global _world
if _world is not None:
return _world
print("Loading terrain diffusion model...")
t0 = time.time()
_world = WorldPipeline.from_pretrained(
MODEL_ID,
seed=42,
latents_batch_size=[1, 2, 4, 8, 16],
log_mode="info",
torch_compile=False,
dtype=None,
caching_strategy="direct",
cache_limit=100 * 1024 * 1024,
)
_world.to("cpu")
_world.bind(hdf5_file=None)
print(f"Model loaded in {time.time() - t0:.1f}s, seed={_world.seed}")
return _world
@spaces.GPU(duration=1)
def _zerogpu_probe():
return "ready"
# ─────────────────────────────────────────────────────────────────────────────
# Preset conditioning maps
# Each preset is a small (cells Γ— cells) elevation conditioning grid in meters.
# 1 cell = 256 pixels, so a 4Γ—4 grid generates a 1024Γ—1024 region.
# PADDING adds border cells so the model has context.
# ─────────────────────────────────────────────────────────────────────────────
PADDING = 4
GRID_SIZE = 4
TOTAL = GRID_SIZE + 2 * PADDING
def _empty_grid(default_val=-1000.0):
return np.full((TOTAL, TOTAL), default_val, dtype=np.float32)
def _mountains_grid():
g = _empty_grid()
cx = TOTAL // 2
for i in range(TOTAL):
for j in range(TOTAL):
di = (i - cx) / (GRID_SIZE / 2)
dj = (j - cx) / (GRID_SIZE / 2)
dist = np.sqrt(di**2 + dj**2)
if dist < 1.0:
g[i, j] = 2500.0 * (1.0 - dist)
elif dist < 1.5:
g[i, j] = 500.0 * (1.5 - dist) / 0.5
return g
def _plains_grid():
g = _empty_grid()
cx = TOTAL // 2
for i in range(PADDING, PADDING + GRID_SIZE):
for j in range(PADDING, PADDING + GRID_SIZE):
di = (i - cx) / (GRID_SIZE / 2)
dj = (j - cx) / (GRID_SIZE / 2)
g[i, j] = 200.0 + 150.0 * np.sin(di * 3) * np.cos(dj * 3)
return g
def _islands_grid():
g = _empty_grid(-2000.0)
cx = TOTAL // 2
for i in range(TOTAL):
for j in range(TOTAL):
di = (i - cx) / (GRID_SIZE / 2)
dj = (j - cx) / (GRID_SIZE / 2)
dist = np.sqrt(di**2 + dj**2)
if dist < 0.5:
g[i, j] = 800.0 * (1.0 - dist / 0.5)
elif dist < 0.8:
g[i, j] = 100.0 * (0.8 - dist) / 0.3
return g
def _canyon_grid():
g = _empty_grid()
cx = TOTAL // 2
for i in range(TOTAL):
for j in range(PADDING, PADDING + GRID_SIZE):
dist_from_center = abs(j - cx) / (GRID_SIZE / 2)
if dist_from_center < 0.3:
g[i, j] = -300.0
elif dist_from_center < 0.5:
g[i, j] = 1500.0
else:
g[i, j] = 800.0
return g
PRESETS = {
"Mountains": _mountains_grid,
"Plains": _plains_grid,
"Islands": _islands_grid,
"Canyon": _canyon_grid,
}
def grid_to_display(grid):
"""Convert conditioning grid to display image."""
display = np.kron(grid, np.ones((16, 16)))
vmin, vmax = float(display.min()), float(display.max())
if vmax == vmin:
vmax = vmin + 1
norm = matplotlib.colors.Normalize(vmin=vmin, vmax=vmax)
rgba = plt.get_cmap("terrain")(norm(display))
return (np.clip(rgba, 0, 1) * 255).astype(np.uint8)
def preset_to_display(preset_name):
"""Return a visualization of the preset conditioning map for display."""
grid = PRESETS[preset_name]()
return grid_to_display(grid)
def sketch_to_conditioning(sketch_img):
"""Convert a user-drawn sketch image to an elevation conditioning grid."""
if sketch_img is None:
return None
if isinstance(sketch_img, dict):
img_data = sketch_img.get("background") or sketch_img.get("composite")
if img_data is None:
for key in ("path", "url"):
if sketch_img.get(key):
img_data = sketch_img[key]
break
if isinstance(img_data, str):
arr = np.array(Image.open(img_data))
else:
arr = np.array(img_data, dtype=np.float32)
elif isinstance(sketch_img, str):
arr = np.array(Image.open(sketch_img))
else:
arr = np.array(sketch_img, dtype=np.float32)
if arr.ndim == 3:
if arr.shape[2] == 4:
arr = arr[:, :, :3]
arr = arr.mean(axis=2)
normalized = arr.astype(np.float32) / 255.0
elev = (normalized - 0.3) * 5000.0
pil_img = Image.fromarray(elev.astype(np.float32), mode="F")
pil_resized = pil_img.resize((TOTAL, TOTAL), Image.BILINEAR)
return np.array(pil_resized, dtype=np.float32)
# ─────────────────────────────────────────────────────────────────────────────
# Terrain generation
# ─────────────────────────────────────────────────────────────────────────────
@spaces.GPU(duration=120)
def generate_terrain(preset_choice, sketch_img, seed, input_mode, progress=gr.Progress(track_tqdm=True)):
"""Generate terrain from preset or user sketch."""
world = _get_world()
world.to("cuda")
try:
if seed is not None and int(seed) != world.seed:
world.change_seed(int(seed))
use_sketch = input_mode == "Custom Sketch"
if use_sketch and sketch_img is not None:
cond_elev = sketch_to_conditioning(sketch_img)
if cond_elev is not None:
world.set_custom_conditioning_import(0, cond_elev, 0, 0, default_value=-1000.0)
world.set_cond_snr([0.5, 0.5, 0.5, 0.5, 0.5])
cond_display = grid_to_display(cond_elev)
else:
grid = PRESETS[preset_choice]()
world.set_custom_conditioning_import(0, grid, 0, 0, default_value=-1000.0)
world.set_cond_snr([0.5, 0.5, 0.5, 0.5, 0.5])
cond_display = grid_to_display(grid)
else:
grid = PRESETS[preset_choice]()
world.set_custom_conditioning_import(0, grid, 0, 0, default_value=-1000.0)
world.set_cond_snr([0.5, 0.5, 0.5, 0.5, 0.5])
cond_display = grid_to_display(grid)
pi1 = PADDING * 256
pi2 = (PADDING + GRID_SIZE) * 256
with world:
result = world.get(pi1, pi1, pi2, pi2, with_climate=False)
elev = result["elev"].cpu().numpy()
# 1. Shaded relief map
relief = get_relief_map(elev, None, None, None, resolution=30)
relief_img = (np.clip(relief, 0, 1) * 255).astype(np.uint8)
# 2. Elevation colormap
vmin, vmax = float(elev.min()), float(elev.max())
if vmax == vmin:
vmax = vmin + 1
norm = matplotlib.colors.Normalize(vmin=vmin, vmax=vmax)
elev_rgba = plt.get_cmap("terrain")(norm(elev))
elev_img = (np.clip(elev_rgba, 0, 1) * 255).astype(np.uint8)
# 3. 3D preview
fig = plt.figure(figsize=(6, 5))
ax = fig.add_subplot(111, projection="3d")
h, w = elev.shape
downsample = 4
X, Y = np.meshgrid(
np.arange(0, w, downsample),
np.arange(0, h, downsample)
)
Z = elev[::downsample, ::downsample]
ax.plot_surface(X, Y, Z, cmap="terrain", linewidth=0, antialiased=True)
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_zlabel("Elevation (m)")
ax.set_title(f"3D Terrain Preview\nRange: {elev.min():.0f}m to {elev.max():.0f}m")
buf = io.BytesIO()
fig.savefig(buf, format="png", dpi=120, bbox_inches="tight")
plt.close(fig)
buf.seek(0)
preview_3d = np.array(Image.open(buf))
# 4. Heightmap PNG (16-bit)
elev_clipped = np.clip(elev, 0, 65535).astype(np.uint16)
heightmap_path = "/tmp/heightmap.png"
Image.fromarray(elev_clipped).save(heightmap_path)
stats = f"Elevation range: {elev.min():.1f}m to {elev.max():.1f}m | Mean: {elev.mean():.1f}m"
return (
relief_img,
elev_img,
preview_3d,
heightmap_path,
cond_display,
stats,
)
finally:
world.to("cpu")
# ─────────────────────────────────────────────────────────────────────────────
# Gradio UI
# ─────────────────────────────────────────────────────────────────────────────
CSS = """
.gradio-container {max-width: 1200px !important;}
"""
with gr.Blocks(title="Terrain Diffusion Demo") as demo:
gr.Markdown("# πŸ”οΈ Terrain Diffusion Demo")
gr.Markdown(
"Generate realistic terrain heightmaps using the "
"[terrain-diffusion-30m](https://huggingface.co/xandergos/terrain-diffusion-30m) model. "
"Based on the paper *Terrain Diffusion: A Diffusion-Based Successor to Perlin Noise "
"in Infinite, Real-Time Terrain Generation* ([arxiv:2512.08309](https://arxiv.org/abs/2512.08309))."
)
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### 1. Choose Terrain Type")
input_mode = gr.Radio(
["Presets", "Custom Sketch"],
value="Presets",
label="Input Mode",
)
with gr.Group(visible=True) as preset_group:
preset = gr.Radio(
list(PRESETS.keys()),
value="Mountains",
label="Preset Terrain",
)
preset_display = gr.Image(
label="Preset Preview",
value=preset_to_display("Mountains"),
interactive=False,
height=200,
)
with gr.Group(visible=False) as sketch_group:
sketch = gr.Sketchpad(
label="Draw Terrain Sketch",
brush=gr.Brush(default_size=40, colors=["#ffffff", "#000000"]),
layers=False,
height=200,
width=200,
)
gr.Markdown(
"πŸ’‘ Draw white for high elevation, black for low/ocean. "
"The sketch guides the AI-generated terrain."
)
seed = gr.Slider(
0, 2**31 - 1, value=42, step=1,
label="Random Seed",
)
generate_btn = gr.Button("πŸš€ Generate Terrain", variant="primary", size="lg")
gr.Markdown(
"⚠️ Generation takes ~30-60 seconds on GPU. "
"The model uses a 3-stage diffusion pipeline (coarse β†’ base β†’ decoder)."
)
with gr.Column(scale=2):
gr.Markdown("### 2. Results")
with gr.Tab("Shaded Relief"):
relief_output = gr.Image(label="Shaded Relief Map", height=400)
with gr.Tab("Elevation"):
elev_output = gr.Image(label="Elevation Colormap", height=400)
with gr.Tab("3D Preview"):
preview_3d_output = gr.Image(label="3D Terrain Preview", height=400)
with gr.Tab("Conditioning"):
cond_output = gr.Image(label="Conditioning Map", height=300)
stats_output = gr.Textbox(label="Statistics", interactive=False)
download_output = gr.File(label="Download Heightmap (16-bit PNG)")
def toggle_input(mode):
if mode == "Presets":
return gr.update(visible=True), gr.update(visible=False)
else:
return gr.update(visible=False), gr.update(visible=True)
def update_preset_display(preset_name):
return preset_to_display(preset_name)
input_mode.change(
toggle_input,
inputs=[input_mode],
outputs=[preset_group, sketch_group],
)
preset.change(
update_preset_display,
inputs=[preset],
outputs=[preset_display],
)
generate_btn.click(
generate_terrain,
inputs=[preset, sketch, seed, input_mode],
outputs=[relief_output, elev_output, preview_3d_output, download_output, cond_output, stats_output],
)
demo.launch(css=CSS)