feat: two-step image gen, Dockerfile builds floorplan whl at deploy time
Browse files- .gitignore +24 -0
- Dockerfile +28 -0
- app.py +224 -28
- floorplan/.gitignore +1 -1
- floorplan/README.md +255 -3
- floorplan/frontend/FloorPlan.svelte +101 -10
- floorplan/frontend/package-lock.json +1 -1
- floorplan/frontend/package.json +1 -1
- floorplan/pyproject.toml +1 -1
- requirements.txt +4 -0
.gitignore
CHANGED
|
@@ -1 +1,25 @@
|
|
| 1 |
.worktrees/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
.worktrees/
|
| 2 |
+
.env
|
| 3 |
+
|
| 4 |
+
# Python
|
| 5 |
+
__pycache__/
|
| 6 |
+
*.pyc
|
| 7 |
+
*.pyo
|
| 8 |
+
.venv/
|
| 9 |
+
*.egg-info/
|
| 10 |
+
|
| 11 |
+
# Node / frontend
|
| 12 |
+
node_modules/
|
| 13 |
+
|
| 14 |
+
# macOS
|
| 15 |
+
.DS_Store
|
| 16 |
+
|
| 17 |
+
# Built artifacts (built by Dockerfile at deploy time)
|
| 18 |
+
floorplan/dist/
|
| 19 |
+
|
| 20 |
+
# Dev/test files
|
| 21 |
+
test_gemini.py
|
| 22 |
+
|
| 23 |
+
# Docs artifacts
|
| 24 |
+
docs/.DS_Store
|
| 25 |
+
docs/superpowers/.DS_Store
|
Dockerfile
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
# Install Node.js for building the frontend
|
| 4 |
+
RUN apt-get update && apt-get install -y curl && \
|
| 5 |
+
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
| 6 |
+
apt-get install -y nodejs && \
|
| 7 |
+
apt-get clean && rm -rf /var/lib/apt/lists/*
|
| 8 |
+
|
| 9 |
+
WORKDIR /app
|
| 10 |
+
|
| 11 |
+
# Copy floorplan source first and build the whl
|
| 12 |
+
COPY floorplan/ ./floorplan/
|
| 13 |
+
RUN pip install gradio hatchling hatch-requirements-txt hatch-fancy-pypi-readme build && \
|
| 14 |
+
cd floorplan && \
|
| 15 |
+
pip install gradio-cli && \
|
| 16 |
+
gradio cc build --no-generate-docs && \
|
| 17 |
+
pip install dist/gradio_floorplan-0.0.1-py3-none-any.whl
|
| 18 |
+
|
| 19 |
+
# Install remaining app dependencies
|
| 20 |
+
COPY requirements.txt ./
|
| 21 |
+
RUN pip install google-genai python-dotenv pillow pydantic
|
| 22 |
+
|
| 23 |
+
# Copy app
|
| 24 |
+
COPY app.py ./
|
| 25 |
+
|
| 26 |
+
EXPOSE 7860
|
| 27 |
+
|
| 28 |
+
CMD ["python", "app.py"]
|
app.py
CHANGED
|
@@ -1,43 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
from gradio_floorplan import FloorPlan
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
DEFAULT_FLOOR_PLAN = {
|
| 5 |
-
"corners": [
|
| 6 |
-
"furnitures": [
|
| 7 |
-
{
|
| 8 |
-
"object": "Sofa",
|
| 9 |
-
"localisation": [150, 100, 250, 300],
|
| 10 |
-
"description": "3-seat sofa",
|
| 11 |
-
},
|
| 12 |
-
{
|
| 13 |
-
"object": "Table",
|
| 14 |
-
"localisation": [300, 200, 380, 400],
|
| 15 |
-
"description": "Coffee table",
|
| 16 |
-
},
|
| 17 |
-
],
|
| 18 |
}
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
| 22 |
-
return value
|
| 23 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
with gr.Blocks() as demo:
|
| 26 |
gr.Markdown("# LaMaison\nRearrange your spaces using visual planning.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
with gr.
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
image_input
|
|
|
|
|
|
|
| 41 |
|
| 42 |
if __name__ == "__main__":
|
| 43 |
-
demo.launch()
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
from io import BytesIO
|
| 4 |
+
from typing import List
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
|
| 7 |
import gradio as gr
|
| 8 |
from gradio_floorplan import FloorPlan
|
| 9 |
+
from PIL import Image
|
| 10 |
+
from pydantic import BaseModel, Field
|
| 11 |
+
from google import genai
|
| 12 |
+
|
| 13 |
+
from google.genai import types
|
| 14 |
+
|
| 15 |
+
load_dotenv()
|
| 16 |
+
client = genai.Client()
|
| 17 |
+
|
| 18 |
+
print("--- LA MAISON APP LOADED (VERSION 1.4 - OPTIMIZED IMAGE GEN & UI) ---")
|
| 19 |
|
| 20 |
DEFAULT_FLOOR_PLAN = {
|
| 21 |
+
"corners": [],
|
| 22 |
+
"furnitures": [],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
}
|
| 24 |
|
| 25 |
+
class FurnitureItem(BaseModel):
|
| 26 |
+
object: str
|
| 27 |
+
localisation: List[int] = Field(description="[ymin, xmin, ymax, xmax] coordinates")
|
| 28 |
+
description: str
|
| 29 |
+
|
| 30 |
+
class RoomLayout(BaseModel):
|
| 31 |
+
corners: List[List[int]] = Field(description="List of [x, y] coordinates for room corners")
|
| 32 |
+
furnitures: List[FurnitureItem]
|
| 33 |
+
|
| 34 |
+
def process_room_image(image_numpy):
|
| 35 |
+
if image_numpy is None:
|
| 36 |
+
return DEFAULT_FLOOR_PLAN, DEFAULT_FLOOR_PLAN
|
| 37 |
+
|
| 38 |
+
pil_image = Image.fromarray(image_numpy)
|
| 39 |
+
|
| 40 |
+
# Step 1: Generate Floor Plan image with Nano Banana 2 (Optimized)
|
| 41 |
+
generated_image = None
|
| 42 |
+
gr.Info("Step 1: Generating 2D floor plan image...")
|
| 43 |
+
print("\n--- Starting Gemini 3.1 Flash Image Preview (Nano Banana 2) - Optimized ---")
|
| 44 |
+
|
| 45 |
+
generate_content_config = types.GenerateContentConfig(
|
| 46 |
+
thinking_config=types.ThinkingConfig(
|
| 47 |
+
thinking_level="MINIMAL",
|
| 48 |
+
),
|
| 49 |
+
image_config = types.ImageConfig(
|
| 50 |
+
image_size="1K",
|
| 51 |
+
),
|
| 52 |
+
response_modalities=[
|
| 53 |
+
"IMAGE",
|
| 54 |
+
],
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
try:
|
| 58 |
+
response_stream = client.models.generate_content_stream(
|
| 59 |
+
model='models/gemini-3.1-flash-image-preview',
|
| 60 |
+
contents=[pil_image, "Generate a clean, 2D top-down floor plan image representing this exact room. OUTPUT ONLY THE IMAGE."],
|
| 61 |
+
config=generate_content_config
|
| 62 |
+
)
|
| 63 |
+
for chunk in response_stream:
|
| 64 |
+
if chunk.parts:
|
| 65 |
+
for part in chunk.parts:
|
| 66 |
+
if part.inline_data:
|
| 67 |
+
print("[Image Data] Received inline image data!")
|
| 68 |
+
generated_image = Image.open(BytesIO(part.inline_data.data))
|
| 69 |
+
break
|
| 70 |
+
|
| 71 |
+
print("--- Finished generating floor plan image ---\n")
|
| 72 |
+
gr.Info("Step 1 Complete: Floor plan image generated!")
|
| 73 |
+
except Exception as e:
|
| 74 |
+
gr.Warning(f"Error generating image: {e}")
|
| 75 |
+
print(f"Error generating image: {e}")
|
| 76 |
+
|
| 77 |
+
# Step 2: Extract coordinates
|
| 78 |
+
layout_json = DEFAULT_FLOOR_PLAN
|
| 79 |
+
image_to_parse = generated_image if generated_image else pil_image
|
| 80 |
+
|
| 81 |
+
gr.Info("Step 2: Analyzing top plan image to extract coordinates...")
|
| 82 |
+
|
| 83 |
+
prompted_model = "gemini-3-flash-preview"
|
| 84 |
+
|
| 85 |
+
try:
|
| 86 |
+
response_json = client.models.generate_content(
|
| 87 |
+
model=prompted_model,
|
| 88 |
+
contents=[
|
| 89 |
+
image_to_parse,
|
| 90 |
+
"OUTPUT FORMAT INSTRUCTION: Return ONLY a valid JSON string. DO NOT provide any reasoning, markdown formatting, or conversational text.\n"
|
| 91 |
+
"Analyze this floor plan (or room image) and output the exact coordinates "
|
| 92 |
+
"for the room corners formatted as [[x, y], ...] and a list of furnitures with "
|
| 93 |
+
"their object name, description, and bounding box [ymin, xmin, ymax, xmax]."
|
| 94 |
+
],
|
| 95 |
+
config={
|
| 96 |
+
'response_mime_type': 'application/json',
|
| 97 |
+
'response_schema': RoomLayout
|
| 98 |
+
}
|
| 99 |
+
)
|
| 100 |
+
if response_json.text:
|
| 101 |
+
layout_json = json.loads(response_json.text)
|
| 102 |
+
gr.Info("Step 2 Complete: Coordinates extracted successfully!")
|
| 103 |
+
print(layout_json)
|
| 104 |
+
except Exception as e:
|
| 105 |
+
gr.Warning(f"Error parsing layout JSON: {e}")
|
| 106 |
+
print(f"Error parsing layout JSON: {e}")
|
| 107 |
+
|
| 108 |
+
# Return: floor_plan value, initial_layout_state, topview_state, topview_before display
|
| 109 |
+
return layout_json, layout_json, generated_image, generated_image
|
| 110 |
+
|
| 111 |
+
def _generate_image(contents, label="image"):
|
| 112 |
+
"""Helper: call Gemini image generation and return a PIL Image or None."""
|
| 113 |
+
config = types.GenerateContentConfig(
|
| 114 |
+
thinking_config=types.ThinkingConfig(thinking_level="MINIMAL"),
|
| 115 |
+
image_config=types.ImageConfig(image_size="1K"),
|
| 116 |
+
response_modalities=["IMAGE"],
|
| 117 |
+
)
|
| 118 |
+
try:
|
| 119 |
+
response_stream = client.models.generate_content_stream(
|
| 120 |
+
model='models/gemini-3.1-flash-image-preview',
|
| 121 |
+
contents=contents,
|
| 122 |
+
config=config,
|
| 123 |
+
)
|
| 124 |
+
for chunk in response_stream:
|
| 125 |
+
if chunk.parts:
|
| 126 |
+
for part in chunk.parts:
|
| 127 |
+
if part.inline_data:
|
| 128 |
+
print(f"[Image Data] Received {label}")
|
| 129 |
+
return Image.open(BytesIO(part.inline_data.data))
|
| 130 |
+
except Exception as e:
|
| 131 |
+
gr.Warning(f"Error generating {label}: {e}")
|
| 132 |
+
print(f"Error generating {label}: {e}")
|
| 133 |
+
return None
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def generate_final_image(original_image_numpy, current_layout_json, initial_layout_json, original_topview):
|
| 137 |
+
if original_image_numpy is None:
|
| 138 |
+
gr.Warning("Please upload an image first.")
|
| 139 |
+
return None, None
|
| 140 |
|
| 141 |
+
pil_image = Image.fromarray(original_image_numpy)
|
|
|
|
| 142 |
|
| 143 |
+
# Compute changed furnitures
|
| 144 |
+
old_furnitures = {f["object"]: f for f in (initial_layout_json.get("furnitures") or [])}
|
| 145 |
+
new_furnitures = {f["object"]: f for f in (current_layout_json.get("furnitures") or [])}
|
| 146 |
+
moved = [
|
| 147 |
+
{"object": name, "from": old_furnitures[name]["localisation"], "to": new_furnitures[name]["localisation"]}
|
| 148 |
+
for name in new_furnitures
|
| 149 |
+
if name in old_furnitures and old_furnitures[name]["localisation"] != new_furnitures[name]["localisation"]
|
| 150 |
+
]
|
| 151 |
+
added = [f for name, f in new_furnitures.items() if name not in old_furnitures]
|
| 152 |
+
removed = [f for name, f in old_furnitures.items() if name not in new_furnitures]
|
| 153 |
+
changes = {"moved": moved, "added": added, "removed": removed}
|
| 154 |
+
changes_str = json.dumps(changes, indent=2)
|
| 155 |
+
new_layout_str = json.dumps(current_layout_json, indent=2)
|
| 156 |
+
print(f"Furniture changes: {changes_str}")
|
| 157 |
+
|
| 158 |
+
# Step A: original top-view + new layout → new top-view
|
| 159 |
+
gr.Info("Step A: Generating updated top-view floor plan image...")
|
| 160 |
+
print("\n--- Step A: new top-view from floor plan ---")
|
| 161 |
+
step_a_input = original_topview if original_topview is not None else pil_image
|
| 162 |
+
prompt_a = f"""This is a 2D top-down floor plan image of a room.
|
| 163 |
+
Redraw it as a clean 2D top-down floor plan image applying the following furniture changes.
|
| 164 |
+
Coordinates are bounding boxes [ymin, xmin, ymax, xmax].
|
| 165 |
+
|
| 166 |
+
Changes to apply:
|
| 167 |
+
{changes_str}
|
| 168 |
+
|
| 169 |
+
Full new target layout for reference:
|
| 170 |
+
{new_layout_str}
|
| 171 |
+
|
| 172 |
+
OUTPUT ONLY THE IMAGE. Keep the same room boundaries and style."""
|
| 173 |
+
|
| 174 |
+
new_topview = _generate_image([step_a_input, prompt_a], label="new top-view")
|
| 175 |
+
if new_topview is None:
|
| 176 |
+
gr.Warning("Step A failed: could not generate updated top-view.")
|
| 177 |
+
return None, None
|
| 178 |
+
|
| 179 |
+
gr.Info("Step A complete. Generating final photorealistic image...")
|
| 180 |
+
|
| 181 |
+
# Step B: new top-view + original photo → final photorealistic image
|
| 182 |
+
print("\n--- Step B: photorealistic synthesis ---")
|
| 183 |
+
prompt_b = """You are given two images:
|
| 184 |
+
1. A 2D top-down floor plan showing the NEW furniture layout.
|
| 185 |
+
2. The original room photo.
|
| 186 |
+
|
| 187 |
+
Generate a high-quality photorealistic image of the room from the EXACT SAME camera angle as the original photo, but with the furniture repositioned to match the new floor plan.
|
| 188 |
+
|
| 189 |
+
STRICT INSTRUCTIONS:
|
| 190 |
+
1. Maintain the exact camera point-of-view, lighting, colors, and architectural features of the original photo.
|
| 191 |
+
2. Place furniture according to the new floor plan layout.
|
| 192 |
+
3. The room must look realistic and clean."""
|
| 193 |
+
|
| 194 |
+
final_image = _generate_image([new_topview, pil_image, prompt_b], label="final photorealistic")
|
| 195 |
+
|
| 196 |
+
if final_image is None:
|
| 197 |
+
gr.Warning("Step B failed: could not generate final image.")
|
| 198 |
+
return new_topview, None
|
| 199 |
+
|
| 200 |
+
gr.Info("Final image generated successfully!")
|
| 201 |
+
return new_topview, final_image
|
| 202 |
|
| 203 |
with gr.Blocks() as demo:
|
| 204 |
gr.Markdown("# LaMaison\nRearrange your spaces using visual planning.")
|
| 205 |
+
|
| 206 |
+
# State to store the initial layout snapshot and the original top-view image
|
| 207 |
+
initial_layout_state = gr.State(DEFAULT_FLOOR_PLAN)
|
| 208 |
+
topview_state = gr.State(None)
|
| 209 |
|
| 210 |
+
with gr.Row():
|
| 211 |
+
with gr.Column(scale=1):
|
| 212 |
+
image_input = gr.Image(label="Upload a room image", type="numpy")
|
| 213 |
+
floor_plan = FloorPlan(
|
| 214 |
+
value=DEFAULT_FLOOR_PLAN,
|
| 215 |
+
label="Floor Plan",
|
| 216 |
+
interactive=True,
|
| 217 |
+
)
|
| 218 |
+
generate_button = gr.Button("Generate New Image based on Floor plan", variant="primary")
|
| 219 |
+
with gr.Column(scale=1):
|
| 220 |
+
topview_before = gr.Image(label="Top View — Original", interactive=False)
|
| 221 |
+
topview_after = gr.Image(label="Top View — New Layout (Step A)", interactive=False)
|
| 222 |
+
image_output = gr.Image(label="Final Photorealistic Output (Step B)", interactive=False)
|
| 223 |
+
|
| 224 |
+
# Wire it: input -> [floor_plan, initial_layout_state, topview_state, topview_before]
|
| 225 |
+
image_input.change(
|
| 226 |
+
process_room_image,
|
| 227 |
+
inputs=image_input,
|
| 228 |
+
outputs=[floor_plan, initial_layout_state, topview_state, topview_before]
|
| 229 |
+
)
|
| 230 |
|
| 231 |
+
# Phase 2: User clicks button -> generates new top-view then final photorealistic image
|
| 232 |
+
generate_button.click(
|
| 233 |
+
generate_final_image,
|
| 234 |
+
inputs=[image_input, floor_plan, initial_layout_state, topview_state],
|
| 235 |
+
outputs=[topview_after, image_output]
|
| 236 |
+
)
|
| 237 |
|
| 238 |
if __name__ == "__main__":
|
| 239 |
+
demo.launch(server_name="0.0.0.0", server_port=7860)
|
floorplan/.gitignore
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
.eggs/
|
| 2 |
-
dist/
|
| 3 |
*.pyc
|
| 4 |
__pycache__/
|
| 5 |
*.py[cod]
|
|
|
|
| 1 |
.eggs/
|
| 2 |
+
dist/*.tar.gz
|
| 3 |
*.pyc
|
| 4 |
__pycache__/
|
| 5 |
*.py[cod]
|
floorplan/README.md
CHANGED
|
@@ -1,10 +1,262 @@
|
|
| 1 |
|
| 2 |
-
# gradio_floorplan
|
| 3 |
-
|
| 4 |
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
```python
|
| 8 |
import gradio as gr
|
| 9 |
from gradio_floorplan import FloorPlan
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
+
# `gradio_floorplan`
|
| 3 |
+
<img alt="Static Badge" src="https://img.shields.io/badge/version%20-%200.0.1%20-%20orange">
|
| 4 |
|
| 5 |
+
A Gradio custom component for interactive SVG floor plan editing
|
| 6 |
+
|
| 7 |
+
## Installation
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
pip install gradio_floorplan
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
## Usage
|
| 14 |
|
| 15 |
```python
|
| 16 |
import gradio as gr
|
| 17 |
from gradio_floorplan import FloorPlan
|
| 18 |
+
|
| 19 |
+
DEFAULT_VALUE = {
|
| 20 |
+
"corners": [[50, 50], [550, 50], [550, 450], [50, 450]],
|
| 21 |
+
"furnitures": [
|
| 22 |
+
{
|
| 23 |
+
"object": "Sofa",
|
| 24 |
+
"localisation": [150, 100, 250, 300],
|
| 25 |
+
"description": "3-seat sofa",
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"object": "Table",
|
| 29 |
+
"localisation": [300, 200, 380, 400],
|
| 30 |
+
"description": "Coffee table",
|
| 31 |
+
},
|
| 32 |
+
],
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def on_furniture_moved(value):
|
| 37 |
+
"""Receives updated floor plan after user moves a furniture item."""
|
| 38 |
+
return value
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
with gr.Blocks() as demo:
|
| 42 |
+
gr.Markdown("## LaMaison — Floor Plan")
|
| 43 |
+
floor_plan = FloorPlan(value=DEFAULT_VALUE, label="Floor Plan", interactive=True)
|
| 44 |
+
output = gr.JSON(label="Updated positions")
|
| 45 |
+
floor_plan.change(on_furniture_moved, inputs=floor_plan, outputs=output)
|
| 46 |
+
|
| 47 |
+
if __name__ == "__main__":
|
| 48 |
+
demo.launch()
|
| 49 |
+
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## `FloorPlan`
|
| 53 |
+
|
| 54 |
+
### Initialization
|
| 55 |
+
|
| 56 |
+
<table>
|
| 57 |
+
<thead>
|
| 58 |
+
<tr>
|
| 59 |
+
<th align="left">name</th>
|
| 60 |
+
<th align="left" style="width: 25%;">type</th>
|
| 61 |
+
<th align="left">default</th>
|
| 62 |
+
<th align="left">description</th>
|
| 63 |
+
</tr>
|
| 64 |
+
</thead>
|
| 65 |
+
<tbody>
|
| 66 |
+
<tr>
|
| 67 |
+
<td align="left"><code>value</code></td>
|
| 68 |
+
<td align="left" style="width: 25%;">
|
| 69 |
+
|
| 70 |
+
```python
|
| 71 |
+
dict | None
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
</td>
|
| 75 |
+
<td align="left"><code>value = None</code></td>
|
| 76 |
+
<td align="left">None</td>
|
| 77 |
+
</tr>
|
| 78 |
+
|
| 79 |
+
<tr>
|
| 80 |
+
<td align="left"><code>label</code></td>
|
| 81 |
+
<td align="left" style="width: 25%;">
|
| 82 |
+
|
| 83 |
+
```python
|
| 84 |
+
str | None
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
</td>
|
| 88 |
+
<td align="left"><code>value = None</code></td>
|
| 89 |
+
<td align="left">None</td>
|
| 90 |
+
</tr>
|
| 91 |
+
|
| 92 |
+
<tr>
|
| 93 |
+
<td align="left"><code>info</code></td>
|
| 94 |
+
<td align="left" style="width: 25%;">
|
| 95 |
+
|
| 96 |
+
```python
|
| 97 |
+
str | None
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
</td>
|
| 101 |
+
<td align="left"><code>value = None</code></td>
|
| 102 |
+
<td align="left">None</td>
|
| 103 |
+
</tr>
|
| 104 |
+
|
| 105 |
+
<tr>
|
| 106 |
+
<td align="left"><code>every</code></td>
|
| 107 |
+
<td align="left" style="width: 25%;">
|
| 108 |
+
|
| 109 |
+
```python
|
| 110 |
+
'Timer | float | None'
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
</td>
|
| 114 |
+
<td align="left"><code>value = None</code></td>
|
| 115 |
+
<td align="left">None</td>
|
| 116 |
+
</tr>
|
| 117 |
+
|
| 118 |
+
<tr>
|
| 119 |
+
<td align="left"><code>show_label</code></td>
|
| 120 |
+
<td align="left" style="width: 25%;">
|
| 121 |
+
|
| 122 |
+
```python
|
| 123 |
+
bool | None
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
</td>
|
| 127 |
+
<td align="left"><code>value = None</code></td>
|
| 128 |
+
<td align="left">None</td>
|
| 129 |
+
</tr>
|
| 130 |
+
|
| 131 |
+
<tr>
|
| 132 |
+
<td align="left"><code>container</code></td>
|
| 133 |
+
<td align="left" style="width: 25%;">
|
| 134 |
+
|
| 135 |
+
```python
|
| 136 |
+
bool
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
</td>
|
| 140 |
+
<td align="left"><code>value = True</code></td>
|
| 141 |
+
<td align="left">None</td>
|
| 142 |
+
</tr>
|
| 143 |
+
|
| 144 |
+
<tr>
|
| 145 |
+
<td align="left"><code>scale</code></td>
|
| 146 |
+
<td align="left" style="width: 25%;">
|
| 147 |
+
|
| 148 |
+
```python
|
| 149 |
+
int | None
|
| 150 |
+
```
|
| 151 |
+
|
| 152 |
+
</td>
|
| 153 |
+
<td align="left"><code>value = None</code></td>
|
| 154 |
+
<td align="left">None</td>
|
| 155 |
+
</tr>
|
| 156 |
+
|
| 157 |
+
<tr>
|
| 158 |
+
<td align="left"><code>min_width</code></td>
|
| 159 |
+
<td align="left" style="width: 25%;">
|
| 160 |
+
|
| 161 |
+
```python
|
| 162 |
+
int
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
</td>
|
| 166 |
+
<td align="left"><code>value = 160</code></td>
|
| 167 |
+
<td align="left">None</td>
|
| 168 |
+
</tr>
|
| 169 |
+
|
| 170 |
+
<tr>
|
| 171 |
+
<td align="left"><code>interactive</code></td>
|
| 172 |
+
<td align="left" style="width: 25%;">
|
| 173 |
+
|
| 174 |
+
```python
|
| 175 |
+
bool | None
|
| 176 |
```
|
| 177 |
+
|
| 178 |
+
</td>
|
| 179 |
+
<td align="left"><code>value = None</code></td>
|
| 180 |
+
<td align="left">None</td>
|
| 181 |
+
</tr>
|
| 182 |
+
|
| 183 |
+
<tr>
|
| 184 |
+
<td align="left"><code>visible</code></td>
|
| 185 |
+
<td align="left" style="width: 25%;">
|
| 186 |
+
|
| 187 |
+
```python
|
| 188 |
+
bool
|
| 189 |
+
```
|
| 190 |
+
|
| 191 |
+
</td>
|
| 192 |
+
<td align="left"><code>value = True</code></td>
|
| 193 |
+
<td align="left">None</td>
|
| 194 |
+
</tr>
|
| 195 |
+
|
| 196 |
+
<tr>
|
| 197 |
+
<td align="left"><code>elem_id</code></td>
|
| 198 |
+
<td align="left" style="width: 25%;">
|
| 199 |
+
|
| 200 |
+
```python
|
| 201 |
+
str | None
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
</td>
|
| 205 |
+
<td align="left"><code>value = None</code></td>
|
| 206 |
+
<td align="left">None</td>
|
| 207 |
+
</tr>
|
| 208 |
+
|
| 209 |
+
<tr>
|
| 210 |
+
<td align="left"><code>elem_classes</code></td>
|
| 211 |
+
<td align="left" style="width: 25%;">
|
| 212 |
+
|
| 213 |
+
```python
|
| 214 |
+
list[str] | str | None
|
| 215 |
+
```
|
| 216 |
+
|
| 217 |
+
</td>
|
| 218 |
+
<td align="left"><code>value = None</code></td>
|
| 219 |
+
<td align="left">None</td>
|
| 220 |
+
</tr>
|
| 221 |
+
|
| 222 |
+
<tr>
|
| 223 |
+
<td align="left"><code>render</code></td>
|
| 224 |
+
<td align="left" style="width: 25%;">
|
| 225 |
+
|
| 226 |
+
```python
|
| 227 |
+
bool
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
</td>
|
| 231 |
+
<td align="left"><code>value = True</code></td>
|
| 232 |
+
<td align="left">None</td>
|
| 233 |
+
</tr>
|
| 234 |
+
</tbody></table>
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
### Events
|
| 238 |
+
|
| 239 |
+
| name | description |
|
| 240 |
+
|:-----|:------------|
|
| 241 |
+
| `change` | |
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
### User function
|
| 246 |
+
|
| 247 |
+
The impact on the users predict function varies depending on whether the component is used as an input or output for an event (or both).
|
| 248 |
+
|
| 249 |
+
- When used as an Input, the component only impacts the input signature of the user function.
|
| 250 |
+
- When used as an output, the component only impacts the return signature of the user function.
|
| 251 |
+
|
| 252 |
+
The code snippet below is accurate in cases where the component is used as both an input and an output.
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
```python
|
| 257 |
+
def predict(
|
| 258 |
+
value: dict| None
|
| 259 |
+
) -> dict| None:
|
| 260 |
+
return value
|
| 261 |
+
```
|
| 262 |
+
|
floorplan/frontend/FloorPlan.svelte
CHANGED
|
@@ -44,6 +44,16 @@
|
|
| 44 |
let startDy = 0;
|
| 45 |
let savedOffsets: [number, number][] = [];
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
function clampOffset(
|
| 48 |
dx: number,
|
| 49 |
dy: number,
|
|
@@ -58,15 +68,93 @@
|
|
| 58 |
}
|
| 59 |
|
| 60 |
function clientToSvg(clientX: number, clientY: number): [number, number] {
|
|
|
|
|
|
|
| 61 |
const rect = svgEl.getBoundingClientRect();
|
|
|
|
|
|
|
| 62 |
const scaleX = SVG_WIDTH / rect.width;
|
| 63 |
const scaleY = SVG_HEIGHT / rect.height;
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
}
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
function onPointerDown(e: PointerEvent, i: number) {
|
| 71 |
if (!interactive) return;
|
| 72 |
e.preventDefault();
|
|
@@ -125,12 +213,15 @@
|
|
| 125 |
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
| 126 |
<svg
|
| 127 |
bind:this={svgEl}
|
| 128 |
-
width=
|
| 129 |
-
height=
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
on:
|
| 133 |
-
on:
|
|
|
|
|
|
|
|
|
|
| 134 |
>
|
| 135 |
<!-- Room outline -->
|
| 136 |
<polygon points={polygonPoints} fill="none" stroke="#333" stroke-width="2" />
|
|
|
|
| 44 |
let startDy = 0;
|
| 45 |
let savedOffsets: [number, number][] = [];
|
| 46 |
|
| 47 |
+
// View state
|
| 48 |
+
let zoom = 1;
|
| 49 |
+
let viewBoxPanX = 0;
|
| 50 |
+
let viewBoxPanY = 0;
|
| 51 |
+
let isPanning = false;
|
| 52 |
+
let panStartX = 0;
|
| 53 |
+
let panStartY = 0;
|
| 54 |
+
let panStartViewBoxX = 0;
|
| 55 |
+
let panStartViewBoxY = 0;
|
| 56 |
+
|
| 57 |
function clampOffset(
|
| 58 |
dx: number,
|
| 59 |
dy: number,
|
|
|
|
| 68 |
}
|
| 69 |
|
| 70 |
function clientToSvg(clientX: number, clientY: number): [number, number] {
|
| 71 |
+
// Return coordinates mapped precisely taking zoom and pan into account
|
| 72 |
+
// (bounding box returns layout size in CSS pixels)
|
| 73 |
const rect = svgEl.getBoundingClientRect();
|
| 74 |
+
|
| 75 |
+
// Scale from CSS pixels to base SVG pixels
|
| 76 |
const scaleX = SVG_WIDTH / rect.width;
|
| 77 |
const scaleY = SVG_HEIGHT / rect.height;
|
| 78 |
+
|
| 79 |
+
// Apply viewport scale and translation (zoom & pan)
|
| 80 |
+
const baseSvgX = (clientX - rect.left) * scaleX;
|
| 81 |
+
const baseSvgY = (clientY - rect.top) * scaleY;
|
| 82 |
+
|
| 83 |
+
// Apply the inverse of the viewbox transform
|
| 84 |
+
const sx = viewBoxPanX + (baseSvgX / zoom);
|
| 85 |
+
const sy = viewBoxPanY + (baseSvgY / zoom);
|
| 86 |
+
|
| 87 |
+
return [sx, sy];
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
// --- Background Panning / Zooming ---
|
| 91 |
+
|
| 92 |
+
function onSvgPointerDown(e: PointerEvent) {
|
| 93 |
+
// If we clicked directly on the background (svgEl) or polygon
|
| 94 |
+
if (e.target === svgEl || (e.target as Element).tagName.toLowerCase() === 'polygon') {
|
| 95 |
+
isPanning = true;
|
| 96 |
+
panStartX = e.clientX;
|
| 97 |
+
panStartY = e.clientY;
|
| 98 |
+
panStartViewBoxX = viewBoxPanX;
|
| 99 |
+
panStartViewBoxY = viewBoxPanY;
|
| 100 |
+
svgEl.setPointerCapture(e.pointerId);
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
function onSvgPointerMove(e: PointerEvent) {
|
| 105 |
+
if (isPanning) {
|
| 106 |
+
const rect = svgEl.getBoundingClientRect();
|
| 107 |
+
const scaleX = SVG_WIDTH / rect.width;
|
| 108 |
+
const scaleY = SVG_HEIGHT / rect.height;
|
| 109 |
+
|
| 110 |
+
const dxStr = (e.clientX - panStartX) * scaleX;
|
| 111 |
+
const dyStr = (e.clientY - panStartY) * scaleY;
|
| 112 |
+
|
| 113 |
+
viewBoxPanX = panStartViewBoxX - (dxStr / zoom);
|
| 114 |
+
viewBoxPanY = panStartViewBoxY - (dyStr / zoom);
|
| 115 |
+
} else {
|
| 116 |
+
onPointerMove(e);
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
function onSvgPointerUp(e: PointerEvent) {
|
| 121 |
+
if (isPanning) {
|
| 122 |
+
isPanning = false;
|
| 123 |
+
svgEl.releasePointerCapture(e.pointerId);
|
| 124 |
+
} else {
|
| 125 |
+
if (e.type === 'pointercancel') {
|
| 126 |
+
onPointerCancel(e);
|
| 127 |
+
} else {
|
| 128 |
+
onPointerUp(e);
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
}
|
| 132 |
|
| 133 |
+
function onWheel(e: WheelEvent) {
|
| 134 |
+
// Zoom centered at pointer
|
| 135 |
+
const zoomFactor = 1.1;
|
| 136 |
+
const direction = e.deltaY > 0 ? -1 : 1;
|
| 137 |
+
|
| 138 |
+
const [pointerSvgX, pointerSvgY] = clientToSvg(e.clientX, e.clientY);
|
| 139 |
+
|
| 140 |
+
if (direction === 1) {
|
| 141 |
+
zoom *= zoomFactor;
|
| 142 |
+
} else {
|
| 143 |
+
zoom /= zoomFactor;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
// Constrain zoom to reasonable bounds
|
| 147 |
+
zoom = Math.max(0.2, Math.min(zoom, 10));
|
| 148 |
+
|
| 149 |
+
// Re-center around pointer position
|
| 150 |
+
const [newPointerSvgX, newPointerSvgY] = clientToSvg(e.clientX, e.clientY);
|
| 151 |
+
viewBoxPanX -= (newPointerSvgX - pointerSvgX);
|
| 152 |
+
viewBoxPanY -= (newPointerSvgY - pointerSvgY);
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
// --- Object Dragging ---
|
| 156 |
+
|
| 157 |
+
|
| 158 |
function onPointerDown(e: PointerEvent, i: number) {
|
| 159 |
if (!interactive) return;
|
| 160 |
e.preventDefault();
|
|
|
|
| 213 |
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
| 214 |
<svg
|
| 215 |
bind:this={svgEl}
|
| 216 |
+
width="100%"
|
| 217 |
+
height="100%"
|
| 218 |
+
viewBox="{viewBoxPanX} {viewBoxPanY} {SVG_WIDTH / zoom} {SVG_HEIGHT / zoom}"
|
| 219 |
+
style="border: 1px solid #ccc; background: #fafafa; display: block; min-height: 400px; cursor: {isPanning ? 'grabbing' : 'grab'}; touch-action: none;"
|
| 220 |
+
on:pointerdown={onSvgPointerDown}
|
| 221 |
+
on:pointermove={onSvgPointerMove}
|
| 222 |
+
on:pointerup={onSvgPointerUp}
|
| 223 |
+
on:pointercancel={onSvgPointerUp}
|
| 224 |
+
on:wheel|preventDefault={onWheel}
|
| 225 |
>
|
| 226 |
<!-- Room outline -->
|
| 227 |
<polygon points={polygonPoints} fill="none" stroke="#333" stroke-width="2" />
|
floorplan/frontend/package-lock.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
| 16 |
"svelte": "^5.48.0"
|
| 17 |
},
|
| 18 |
"devDependencies": {
|
| 19 |
-
"@gradio/preview": "0.16.0",
|
| 20 |
"vitest": "^4.1.0"
|
| 21 |
},
|
| 22 |
"peerDependencies": {
|
|
|
|
| 16 |
"svelte": "^5.48.0"
|
| 17 |
},
|
| 18 |
"devDependencies": {
|
| 19 |
+
"@gradio/preview": "^0.16.0",
|
| 20 |
"vitest": "^4.1.0"
|
| 21 |
},
|
| 22 |
"peerDependencies": {
|
floorplan/frontend/package.json
CHANGED
|
@@ -28,7 +28,7 @@
|
|
| 28 |
"svelte": "^5.48.0"
|
| 29 |
},
|
| 30 |
"devDependencies": {
|
| 31 |
-
"@gradio/preview": "0.16.0",
|
| 32 |
"vitest": "^4.1.0"
|
| 33 |
},
|
| 34 |
"peerDependencies": {
|
|
|
|
| 28 |
"svelte": "^5.48.0"
|
| 29 |
},
|
| 30 |
"devDependencies": {
|
| 31 |
+
"@gradio/preview": "^0.16.0",
|
| 32 |
"vitest": "^4.1.0"
|
| 33 |
},
|
| 34 |
"peerDependencies": {
|
floorplan/pyproject.toml
CHANGED
|
@@ -45,7 +45,7 @@ classifiers = [
|
|
| 45 |
dev = ["build", "twine"]
|
| 46 |
|
| 47 |
[tool.hatch.build]
|
| 48 |
-
artifacts = ["/backend/gradio_floorplan/templates", "*.pyi"]
|
| 49 |
|
| 50 |
[tool.hatch.build.targets.wheel]
|
| 51 |
packages = ["/backend/gradio_floorplan"]
|
|
|
|
| 45 |
dev = ["build", "twine"]
|
| 46 |
|
| 47 |
[tool.hatch.build]
|
| 48 |
+
artifacts = ["/backend/gradio_floorplan/templates", "*.pyi", "/opt/homebrew/lib/python3.14/site-packages/gradio_floorplan/templates"]
|
| 49 |
|
| 50 |
[tool.hatch.build.targets.wheel]
|
| 51 |
packages = ["/backend/gradio_floorplan"]
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
google-genai
|
| 2 |
+
python-dotenv
|
| 3 |
+
pillow
|
| 4 |
+
pydantic
|