Spaces:
Sleeping
Sleeping
| """ | |
| Buildify HouseGAN++ β HuggingFace Spaces API (FastAPI, no Gradio). | |
| API: | |
| POST /api/predict | |
| {"data": [hg_type_vector, binary_adj, house_w, house_h, num_samples]} | |
| Returns: {"data": [layouts]} | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from fastapi import FastAPI | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel | |
| from typing import List | |
| # ββ Constants βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| NUM_ROOM_TYPES = 18 | |
| NOISE_DIM = 128 | |
| GRAPH_DIM = 128 | |
| MASK_SIZE = 64 | |
| # ββ Model βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class GraphConvLayer(nn.Module): | |
| def __init__(self, in_dim, out_dim): | |
| super().__init__() | |
| self.self_fc = nn.Linear(in_dim, out_dim) | |
| self.neigh_fc = nn.Linear(in_dim, out_dim) | |
| self.norm = nn.LayerNorm(out_dim) | |
| def forward(self, x, adj): | |
| deg = adj.sum(dim=-1, keepdim=True).clamp(min=1) | |
| agg = torch.matmul(adj / deg, x) | |
| return F.relu(self.norm(self.self_fc(x) + self.neigh_fc(agg))) | |
| class GraphRelationNetwork(nn.Module): | |
| def __init__(self, in_dim, hidden=GRAPH_DIM, out_dim=GRAPH_DIM): | |
| super().__init__() | |
| self.gc1 = GraphConvLayer(in_dim, hidden) | |
| self.gc2 = GraphConvLayer(hidden, hidden) | |
| self.gc3 = GraphConvLayer(hidden, out_dim) | |
| def forward(self, x, adj): | |
| return self.gc3(self.gc2(self.gc1(x, adj), adj), adj) | |
| class MaskDecoder(nn.Module): | |
| def __init__(self, in_dim=GRAPH_DIM): | |
| super().__init__() | |
| self.fc = nn.Linear(in_dim, 256 * 4 * 4) | |
| self.deconv = nn.Sequential( | |
| nn.ConvTranspose2d(256, 128, 4, 2, 1), nn.BatchNorm2d(128), nn.ReLU(True), | |
| nn.ConvTranspose2d(128, 64, 4, 2, 1), nn.BatchNorm2d(64), nn.ReLU(True), | |
| nn.ConvTranspose2d(64, 32, 4, 2, 1), nn.BatchNorm2d(32), nn.ReLU(True), | |
| nn.ConvTranspose2d(32, 1, 4, 2, 1), nn.Sigmoid(), | |
| ) | |
| def forward(self, z): | |
| return self.deconv(F.relu(self.fc(z)).view(-1, 256, 4, 4)) | |
| class HouseGANGenerator(nn.Module): | |
| def __init__(self, num_types=NUM_ROOM_TYPES, noise_dim=NOISE_DIM, | |
| graph_dim=GRAPH_DIM, refinement_steps=3): | |
| super().__init__() | |
| self.noise_dim = noise_dim | |
| self.refinement_steps = refinement_steps | |
| self.type_embed = nn.Embedding(num_types + 1, 64, padding_idx=0) | |
| self.grn_init = GraphRelationNetwork(noise_dim + 64, graph_dim, graph_dim) | |
| self.dec_init = MaskDecoder(graph_dim) | |
| self.grn_refine = GraphRelationNetwork(noise_dim + 64 + 4, graph_dim, graph_dim) | |
| self.dec_refine = MaskDecoder(graph_dim) | |
| def _mask_stats(self, masks): | |
| N = masks.size(0) | |
| flat = masks.view(N, -1) | |
| gy, gx = torch.meshgrid( | |
| torch.linspace(0, 1, MASK_SIZE, device=masks.device), | |
| torch.linspace(0, 1, MASK_SIZE, device=masks.device), | |
| indexing='ij' | |
| ) | |
| gy, gx = gy.reshape(-1), gx.reshape(-1) | |
| total = flat.sum(-1, keepdim=True).clamp(min=1e-6) | |
| cx = (flat * gx).sum(-1, keepdim=True) / total | |
| cy = (flat * gy).sum(-1, keepdim=True) / total | |
| sx = ((flat * (gx - cx) ** 2).sum(-1, keepdim=True) / total).sqrt() | |
| sy = ((flat * (gy - cy) ** 2).sum(-1, keepdim=True) / total).sqrt() | |
| return torch.cat([cx, cy, sx, sy], dim=-1) | |
| def forward(self, room_types, adj, z=None): | |
| N = room_types.size(0) | |
| if z is None: | |
| z = torch.randn(N, self.noise_dim) | |
| te = self.type_embed(room_types) | |
| masks = self.dec_init(self.grn_init(torch.cat([z, te], -1), adj)) | |
| for _ in range(self.refinement_steps): | |
| masks = self.dec_refine(self.grn_refine( | |
| torch.cat([z, te, self._mask_stats(masks)], -1), adj)) | |
| return masks | |
| # ββ Load model ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _model = None | |
| def get_model(): | |
| global _model | |
| if _model is None: | |
| _model = HouseGANGenerator() | |
| weights = "housegan_pp.pt" | |
| if os.path.exists(weights): | |
| ckpt = torch.load(weights, map_location="cpu", weights_only=False) | |
| state = ckpt.get("generator", ckpt.get("model_state_dict", ckpt)) | |
| _model.load_state_dict(state, strict=False) | |
| _model.eval() | |
| return _model | |
| def masks_to_bboxes(masks_np, threshold=0.5): | |
| bboxes = [] | |
| for mask in masks_np: | |
| m = (mask[0] >= threshold).astype(np.uint8) | |
| ys, xs = np.where(m) | |
| if len(xs) == 0: | |
| bboxes.append([0.1, 0.1, 0.4, 0.4]) | |
| else: | |
| bboxes.append([ | |
| float(xs.min()) / MASK_SIZE, float(ys.min()) / MASK_SIZE, | |
| float(xs.max() + 1) / MASK_SIZE, float(ys.max() + 1) / MASK_SIZE, | |
| ]) | |
| return bboxes | |
| # ββ FastAPI app βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app = FastAPI(title="Buildify HouseGAN++") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| class PredictRequest(BaseModel): | |
| data: list | |
| def root(): | |
| return {"status": "ok", "service": "Buildify HouseGAN++"} | |
| def health(): | |
| return {"status": "ok"} | |
| def predict(req: PredictRequest): | |
| hg_type_vector, binary_adj, house_w, house_h, num_samples = req.data | |
| num_samples = max(1, min(int(num_samples), 5)) | |
| model = get_model() | |
| room_types = torch.tensor(hg_type_vector, dtype=torch.long) | |
| adj = torch.tensor(binary_adj, dtype=torch.float32) | |
| layouts = [] | |
| with torch.no_grad(): | |
| for _ in range(num_samples): | |
| masks = model(room_types, adj) | |
| bboxes = masks_to_bboxes(masks.numpy()) | |
| layouts.append(bboxes) | |
| return {"data": [layouts]} | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) | |