Spaces:
Sleeping
Sleeping
Download app.py from lucashudsn/FastRender: direct link, hf CLI and curl.
- Browser
- Download file 16 kB
-
https://huggingface.co/spaces/lucashudsn/FastRender/resolve/main/app.py
- Command line
-
hf download hf://spaces/lucashudsn/FastRender/app.py
-
curl -L -o app.py https://huggingface.co/spaces/lucashudsn/FastRender/resolve/main/app.py
16 kB
| """ | |
| ArchiCAD AI Renderer — Gradio app for Hugging Face ZeroGPU Spaces. | |
| Takes an ArchiCAD viewport export (shaded 3D view, white/clay model, elevation, | |
| or line drawing) and turns it into a photorealistic render while preserving the | |
| geometry. Under the hood: | |
| * Depth-Anything-V2 (depth map) ─┐ | |
| * Canny edges (linework) ├─> multi-ControlNet SDXL (RealVisXL V5) | |
| * Prompt builder (scene/time/etc) ─┘ │ | |
| └─> optional ControlNet img2img | |
| "detail refine" + 1.5-2x upscale | |
| """ | |
| import gc | |
| import random | |
| import cv2 | |
| import gradio as gr | |
| import numpy as np | |
| import spaces | |
| import torch | |
| from diffusers import ( | |
| AutoencoderKL, | |
| ControlNetModel, | |
| DPMSolverMultistepScheduler, | |
| StableDiffusionXLControlNetImg2ImgPipeline, | |
| StableDiffusionXLControlNetPipeline, | |
| ) | |
| from PIL import Image, ImageOps | |
| from transformers import pipeline as hf_pipeline | |
| # ---------------------------------------------------------------------------- | |
| # Model loading (done once at import; ZeroGPU moves to GPU on demand) | |
| # ---------------------------------------------------------------------------- | |
| DTYPE = torch.float16 | |
| BASE_MODEL = "SG161222/RealVisXL_V5.0" | |
| VAE_MODEL = "madebyollin/sdxl-vae-fp16-fix" | |
| CN_DEPTH = "diffusers/controlnet-depth-sdxl-1.0-small" | |
| CN_CANNY = "diffusers/controlnet-canny-sdxl-1.0-small" | |
| DEPTH_MODEL = "depth-anything/Depth-Anything-V2-Small-hf" | |
| print("Loading models…") | |
| vae = AutoencoderKL.from_pretrained(VAE_MODEL, torch_dtype=DTYPE) | |
| controlnets = [ | |
| ControlNetModel.from_pretrained(CN_DEPTH, torch_dtype=DTYPE, use_safetensors=True), | |
| ControlNetModel.from_pretrained(CN_CANNY, torch_dtype=DTYPE, use_safetensors=True), | |
| ] | |
| pipe = StableDiffusionXLControlNetPipeline.from_pretrained( | |
| BASE_MODEL, | |
| controlnet=controlnets, | |
| vae=vae, | |
| torch_dtype=DTYPE, | |
| variant="fp16", | |
| use_safetensors=True, | |
| ) | |
| pipe.scheduler = DPMSolverMultistepScheduler.from_config( | |
| pipe.scheduler.config, use_karras_sigmas=True, algorithm_type="dpmsolver++" | |
| ) | |
| pipe.vae.enable_tiling() | |
| pipe.vae.enable_slicing() | |
| pipe.to("cuda") | |
| # Refine pipeline shares all weights with the main one (no extra VRAM) | |
| refine_pipe = StableDiffusionXLControlNetImg2ImgPipeline(**pipe.components) | |
| refine_pipe.to("cuda") | |
| depth_estimator = hf_pipeline("depth-estimation", model=DEPTH_MODEL, device="cuda") | |
| print("Models ready.") | |
| # ---------------------------------------------------------------------------- | |
| # Prompt building | |
| # ---------------------------------------------------------------------------- | |
| QUALITY = ( | |
| "architectural photography, photorealistic, ultra detailed, sharp focus, " | |
| "natural lighting, realistic materials, professional DSLR, 8k" | |
| ) | |
| NEGATIVE = ( | |
| "cartoon, illustration, painting, sketch, lowres, blurry, deformed geometry, " | |
| "warped walls, distorted perspective, extra floors, extra windows, text, " | |
| "watermark, logo, oversaturated, cgi, plastic, noise, jpeg artifacts" | |
| ) | |
| SCENES = { | |
| "Exterior – residential": "modern residential house exterior, landscaped garden, driveway", | |
| "Exterior – commercial / office": "contemporary commercial building exterior, plaza, street trees", | |
| "Exterior – urban / mixed-use": "urban mixed-use building, city street, pedestrians, cars", | |
| "Interior – living space": "interior design photography, living room, furniture, decor", | |
| "Interior – kitchen / dining": "interior design photography, kitchen and dining area, styled", | |
| "Interior – bedroom": "interior design photography, bedroom, bedding, soft textiles", | |
| "Interior – office / commercial": "modern office interior, workstations, plants, glazing", | |
| "Detail / facade close-up": "architectural facade detail, material junctions, shadows", | |
| } | |
| TIMES = { | |
| "Midday – clear sky": "bright midday sun, clear blue sky, crisp shadows", | |
| "Golden hour": "golden hour, warm low sun, long soft shadows, glowing sky", | |
| "Overcast / soft light": "overcast sky, soft diffuse light, muted tones", | |
| "Blue hour / dusk": "blue hour, dusk, glowing interior lights, deep blue sky", | |
| "Night": "night, illuminated interior lighting, warm lights, dark sky", | |
| "Misty morning": "early morning mist, soft sunrise light, atmospheric haze", | |
| } | |
| SEASONS = { | |
| "Summer": "lush green summer vegetation", | |
| "Spring": "spring blossoms, fresh green foliage", | |
| "Autumn": "autumn foliage, orange and red leaves", | |
| "Winter": "winter, light snow, bare trees", | |
| "Not specified": "", | |
| } | |
| STYLES = { | |
| "Photorealistic": "photorealistic archviz render, V-Ray look", | |
| "Editorial magazine": "architectural digest editorial photo, moody, high-end", | |
| "Minimal / Scandinavian": "minimal Scandinavian aesthetic, light wood, white walls", | |
| "Brutalist concrete": "brutalist architecture, raw board-formed concrete", | |
| "Warm / Mediterranean": "Mediterranean style, warm plaster, terracotta, olive trees", | |
| } | |
| def build_prompt(description, scene, time_of_day, season, style, materials): | |
| parts = [ | |
| description.strip(), | |
| materials.strip(), | |
| SCENES.get(scene, ""), | |
| STYLES.get(style, ""), | |
| TIMES.get(time_of_day, ""), | |
| SEASONS.get(season, "") if scene.startswith("Exterior") else "", | |
| QUALITY, | |
| ] | |
| return ", ".join(p for p in parts if p) | |
| # ---------------------------------------------------------------------------- | |
| # Control-image preparation | |
| # ---------------------------------------------------------------------------- | |
| def fit_size(w, h, long_side): | |
| scale = long_side / max(w, h) | |
| nw, nh = int(round(w * scale / 8) * 8), int(round(h * scale / 8) * 8) | |
| return max(nw, 256), max(nh, 256) | |
| def make_canny(img: Image.Image, low: int, high: int, thicken: bool) -> Image.Image: | |
| gray = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2GRAY) | |
| gray = cv2.GaussianBlur(gray, (3, 3), 0) | |
| edges = cv2.Canny(gray, low, high) | |
| if thicken: | |
| edges = cv2.dilate(edges, np.ones((2, 2), np.uint8), iterations=1) | |
| return Image.fromarray(np.stack([edges] * 3, axis=-1)) | |
| def make_depth(img: Image.Image) -> Image.Image: | |
| d = depth_estimator(img)["depth"] | |
| d = np.array(d).astype(np.float32) | |
| d = (d - d.min()) / max(d.max() - d.min(), 1e-6) | |
| d = (d * 255).astype(np.uint8) | |
| return Image.fromarray(np.stack([d] * 3, axis=-1)).resize(img.size, Image.LANCZOS) | |
| def prepare(image, long_side, mode, low, high): | |
| img = ImageOps.exif_transpose(image).convert("RGB") | |
| w, h = fit_size(*img.size, long_side) | |
| img = img.resize((w, h), Image.LANCZOS) | |
| depth = make_depth(img) | |
| canny = make_canny(img, low, high, thicken=(mode == "Line drawing / elevation")) | |
| return img, depth, canny | |
| MODE_DEFAULTS = { | |
| # (depth_scale, canny_scale) | |
| "3D view (shaded / clay model)": (0.65, 0.55), | |
| "Line drawing / elevation": (0.30, 0.90), | |
| } | |
| def on_mode_change(mode): | |
| d, c = MODE_DEFAULTS[mode] | |
| return d, c | |
| # ---------------------------------------------------------------------------- | |
| # GPU functions | |
| # ---------------------------------------------------------------------------- | |
| def preview_controls(image, long_side, mode, canny_low, canny_high): | |
| if image is None: | |
| raise gr.Error("Upload an ArchiCAD export first.") | |
| _, depth, canny = prepare(image, long_side, mode, canny_low, canny_high) | |
| return [(depth, "Depth"), (canny, "Edges")] | |
| def render( | |
| image, | |
| description, | |
| scene, | |
| time_of_day, | |
| season, | |
| style, | |
| materials, | |
| negative_extra, | |
| mode, | |
| long_side, | |
| depth_scale, | |
| canny_scale, | |
| canny_low, | |
| canny_high, | |
| steps, | |
| guidance, | |
| num_variations, | |
| seed, | |
| do_refine, | |
| upscale, | |
| refine_strength, | |
| progress=gr.Progress(track_tqdm=True), | |
| ): | |
| if image is None: | |
| raise gr.Error("Upload an ArchiCAD export first.") | |
| seed = random.randint(0, 2**31 - 1) if seed is None or int(seed) < 0 else int(seed) | |
| prompt = build_prompt(description or "", scene, time_of_day, season, style, materials or "") | |
| negative = NEGATIVE + (", " + negative_extra.strip() if negative_extra else "") | |
| progress(0.05, desc="Extracting depth + edges") | |
| img, depth, canny = prepare(image, long_side, mode, canny_low, canny_high) | |
| w, h = img.size | |
| scales = [float(depth_scale), float(canny_scale)] | |
| progress(0.15, desc="Rendering") | |
| gen = torch.Generator("cuda").manual_seed(seed) | |
| out = pipe( | |
| prompt=prompt, | |
| negative_prompt=negative, | |
| image=[depth, canny], | |
| width=w, | |
| height=h, | |
| num_inference_steps=int(steps), | |
| guidance_scale=float(guidance), | |
| controlnet_conditioning_scale=scales, | |
| control_guidance_start=[0.0, 0.0], | |
| control_guidance_end=[0.8, 0.9], | |
| num_images_per_prompt=int(num_variations), | |
| generator=gen, | |
| ).images | |
| if do_refine: | |
| progress(0.7, desc="Refining details + upscaling") | |
| nw = int(round(w * upscale / 8) * 8) | |
| nh = int(round(h * upscale / 8) * 8) | |
| d_up = depth.resize((nw, nh), Image.LANCZOS) | |
| c_up = canny.resize((nw, nh), Image.LANCZOS) | |
| refined = [] | |
| for i, im in enumerate(out): | |
| up = im.resize((nw, nh), Image.LANCZOS) | |
| r = refine_pipe( | |
| prompt=prompt, | |
| negative_prompt=negative, | |
| image=up, | |
| control_image=[d_up, c_up], | |
| strength=float(refine_strength), | |
| num_inference_steps=max(int(steps), 24), | |
| guidance_scale=float(guidance), | |
| controlnet_conditioning_scale=scales, | |
| generator=torch.Generator("cuda").manual_seed(seed + i), | |
| ).images[0] | |
| refined.append(r) | |
| out = refined | |
| gc.collect() | |
| torch.cuda.empty_cache() | |
| captions = [(im, f"seed {seed}" + (f"+{i}" if i else "")) for i, im in enumerate(out)] | |
| return captions, seed, prompt | |
| # ---------------------------------------------------------------------------- | |
| # UI | |
| # ---------------------------------------------------------------------------- | |
| TIPS = """ | |
| ### Exporting from ArchiCAD for best results | |
| 1. **3D window → Perspective** (or Parallel). Set the *3D Window Settings → Model Display* to **Shading** or **Shading with Contours**. | |
| 2. Use **Sun/Shadow** on so volumes read clearly — depth estimation loves shadows. | |
| 3. **File → Save As → Image** (`.png`, ≥1500 px on the long side), or *Publisher → Image* for batches. | |
| 4. For **Elevations/Sections**, publish as PNG with a white background, black lines, and set *Line drawing* mode. | |
| 5. Hide furniture/labels/dimensions/zones you don't want interpreted. | |
| 6. For a "clay" look: set all surfaces to a single light grey material via *Model View Options → Override*. | |
| ### Tuning | |
| | Goal | What to change | | |
| |---|---| | |
| | Geometry drifting | Raise **Depth** and **Edge** strength (0.8+) | | |
| | Looks too CGI / stiff | Lower both strengths (0.4–0.5), raise guidance slightly | | |
| | Too many weird details | Lower guidance (5–6), shorter prompt | | |
| | Need a larger/sharper image | Enable **Refine + upscale** (1.5×) | | |
| | Windows/doors missing | Lower Canny low threshold (e.g. 40/120) | | |
| Material ideas for the prompt: `white lime render, charred timber cladding, black steel window frames, board-formed concrete, large glazing, standing seam zinc roof`. | |
| """ | |
| CSS = """ | |
| .gradio-container {max-width: 1280px !important} | |
| #title h1 {font-weight: 700; letter-spacing: -0.02em} | |
| """ | |
| with gr.Blocks(title="ArchiCAD AI Renderer", theme=gr.themes.Soft(), css=CSS) as demo: | |
| gr.Markdown( | |
| "# 🏛️ ArchiCAD AI Renderer\n" | |
| "Upload a viewport export from ArchiCAD and get a photorealistic render that " | |
| "**keeps your geometry**. SDXL + depth/edge ControlNet on ZeroGPU.", | |
| elem_id="title", | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=5): | |
| image = gr.Image(label="ArchiCAD export (3D view, clay model, elevation…)", type="pil", height=380) | |
| mode = gr.Radio( | |
| list(MODE_DEFAULTS.keys()), value="3D view (shaded / clay model)", label="Input type" | |
| ) | |
| description = gr.Textbox( | |
| label="Describe the project", | |
| placeholder="two-storey family home with flat roof, large corner glazing, timber screen", | |
| lines=2, | |
| ) | |
| materials = gr.Textbox( | |
| label="Materials", | |
| placeholder="white lime render, charred larch cladding, black aluminium windows, concrete paving", | |
| ) | |
| with gr.Row(): | |
| scene = gr.Dropdown(list(SCENES), value="Exterior – residential", label="Scene") | |
| style = gr.Dropdown(list(STYLES), value="Photorealistic", label="Style") | |
| with gr.Row(): | |
| time_of_day = gr.Dropdown(list(TIMES), value="Golden hour", label="Lighting / time") | |
| season = gr.Dropdown(list(SEASONS), value="Summer", label="Season (exteriors)") | |
| with gr.Accordion("Control & generation settings", open=False): | |
| negative_extra = gr.Textbox(label="Extra negative prompt", placeholder="people, cars") | |
| long_side = gr.Slider(768, 1280, value=1024, step=64, label="Base resolution (long side)") | |
| with gr.Row(): | |
| depth_scale = gr.Slider(0, 1.2, value=0.65, step=0.05, label="Depth strength") | |
| canny_scale = gr.Slider(0, 1.2, value=0.55, step=0.05, label="Edge strength") | |
| with gr.Row(): | |
| canny_low = gr.Slider(10, 200, value=70, step=5, label="Canny low") | |
| canny_high = gr.Slider(50, 300, value=160, step=5, label="Canny high") | |
| with gr.Row(): | |
| steps = gr.Slider(15, 50, value=30, step=1, label="Steps") | |
| guidance = gr.Slider(3, 12, value=6.5, step=0.5, label="Guidance (CFG)") | |
| with gr.Row(): | |
| num_variations = gr.Slider(1, 4, value=1, step=1, label="Variations") | |
| seed = gr.Number(value=-1, precision=0, label="Seed (-1 = random)") | |
| with gr.Accordion("Refine + upscale", open=True): | |
| do_refine = gr.Checkbox(value=True, label="Detail refine pass (img2img + ControlNet)") | |
| with gr.Row(): | |
| upscale = gr.Slider(1.0, 2.0, value=1.5, step=0.25, label="Upscale factor") | |
| refine_strength = gr.Slider(0.15, 0.55, value=0.35, step=0.05, label="Refine strength") | |
| with gr.Row(): | |
| preview_btn = gr.Button("👁️ Preview control maps", variant="secondary") | |
| run_btn = gr.Button("🎨 Render", variant="primary") | |
| with gr.Column(scale=6): | |
| gallery = gr.Gallery(label="Renders", columns=2, height=560, object_fit="contain", preview=True) | |
| with gr.Row(): | |
| used_seed = gr.Number(label="Seed used", interactive=False) | |
| used_prompt = gr.Textbox(label="Final prompt", interactive=False, lines=3) | |
| with gr.Accordion("Control maps", open=False): | |
| control_gallery = gr.Gallery(columns=2, height=260, object_fit="contain", show_label=False) | |
| with gr.Accordion("📘 ArchiCAD export guide & tuning tips", open=False): | |
| gr.Markdown(TIPS) | |
| mode.change(on_mode_change, mode, [depth_scale, canny_scale]) | |
| preview_btn.click( | |
| preview_controls, | |
| [image, long_side, mode, canny_low, canny_high], | |
| control_gallery, | |
| ) | |
| run_btn.click( | |
| render, | |
| [ | |
| image, description, scene, time_of_day, season, style, materials, negative_extra, | |
| mode, long_side, depth_scale, canny_scale, canny_low, canny_high, | |
| steps, guidance, num_variations, seed, do_refine, upscale, refine_strength, | |
| ], | |
| [gallery, used_seed, used_prompt], | |
| ) | |
| demo.queue(max_size=20).launch() |