floor_designer / app.py
gabar92's picture
Upload 15 files
a67e105 verified
import os
import glob
import uuid
from typing import Tuple, Optional
from io import BytesIO
import gradio as gr
from PIL import Image, ImageFile
from google import genai
from google.genai import types
import logging
logger = logging.getLogger(__name__)
# -----------------------------------------
# Configuration
# -----------------------------------------
TEXTURE_FOLDERS = {
"Laminált padló": "laminate",
"Járólap": "tile"
}
ImageFile.LOAD_TRUNCATED_IMAGES = True
IMAGE_MODEL_ID = os.getenv("AI_IMAGE_MODEL_ID", "image-model-default")
print("IMAGE_MODEL_ID:", IMAGE_MODEL_ID)
# -----------------------------------------
# Structured Prompt for A | B Full Room Duplication
# -----------------------------------------
# A prompt most már biztonságosan, környezeti változóból töltődik be.
PROMPT_TEMPLATE = os.getenv("FLOOR_PROMPT_TEMPLATE")
if not PROMPT_TEMPLATE:
logger.warning("⚠️ FLOOR_PROMPT_TEMPLATE secret is missing! Please set it in Hugging Face Space Settings.")
# Fallback to prevent immediate crash, though API will likely reject an empty prompt.
PROMPT_TEMPLATE = "{{ \"error\": \"Secret prompt missing.\" }}"
# Gallery layout
GALLERY_COLUMNS = 5
MAX_IMAGE_DIMENSION = 1280
# -----------------------------------------
# Custom CSS
# -----------------------------------------
CUSTOM_CSS = """
<style>
.gradio-gallery .selected,
.gradio-gallery .gallery-item.selected,
.gradio-gallery :focus-within {
border-color: #FF8C00 !important;
border-width: 4px !important;
transform: scale(0.96);
transition: all 0.2s ease-in-out;
outline: 4px solid #FF8C00 !important;
}
</style>
"""
# -----------------------------------------
# Texture loading
# -----------------------------------------
def load_textures_from_folder(folder_path: str, max_size: Tuple[int, int] = (256, 256)):
valid_names = []
gallery_items = []
path_map = {}
if not os.path.isdir(folder_path):
os.makedirs(folder_path, exist_ok=True)
return valid_names, gallery_items, path_map
exts = ("*.png", "*.jpg", "*.jpeg", "*.webp")
found_files = []
for ext in exts:
found_files.extend(glob.glob(os.path.join(folder_path, ext)))
found_files = sorted(found_files)
for path in found_files:
if not os.path.isfile(path):
continue
name = os.path.basename(path)
display_name = os.path.splitext(name)[0]
try:
img = Image.open(path).convert("RGB")
img.thumbnail(max_size, Image.LANCZOS)
valid_names.append(display_name)
gallery_items.append((img, display_name))
path_map[display_name] = os.path.abspath(path)
except Exception as e:
continue
return valid_names, gallery_items, path_map
CATEGORY_DATA = {}
ALL_TEXTURE_PATHS = {}
for category, folder in TEXTURE_FOLDERS.items():
names, items, paths = load_textures_from_folder(folder)
CATEGORY_DATA[category] = {
"names": names,
"items": items
}
ALL_TEXTURE_PATHS.update(paths)
# -----------------------------------------
# Image client
# -----------------------------------------
def get_image_client() -> genai.Client:
return genai.Client()
try:
image_client = get_image_client()
IMAGE_CLIENT_INIT_ERROR = None
except Exception as e:
image_client = None
IMAGE_CLIENT_INIT_ERROR = str(e)
# -----------------------------------------
# Helper Functions
# -----------------------------------------
def smart_resize(img: Image.Image, max_dim: int) -> Image.Image:
width, height = img.size
if width <= max_dim and height <= max_dim:
return img
if width > height:
new_width = max_dim
new_height = int(height * (max_dim / width))
else:
new_height = max_dim
new_width = int(width * (max_dim / height))
return img.resize((new_width, new_height), Image.LANCZOS)
def build_floor_prompt(
user_extra_prompt: str,
flooring_type: str,
width: int,
height: int
) -> str:
flooring_type_en = "Laminate" if flooring_type == "Laminált padló" else "Tile"
material_desc = "laminate floor planks" if flooring_type == "Laminált padló" else "floor tiles"
safe_user_prompt = user_extra_prompt if user_extra_prompt else "None"
return PROMPT_TEMPLATE.format(
width=width,
height=height,
flooring_type=flooring_type_en,
material_desc=material_desc,
user_extra_prompt=safe_user_prompt,
).strip()
# -----------------------------------------
# Main Logic
# -----------------------------------------
def generate_floor_preview(
room_image: Optional[Image.Image],
flooring_type: str,
selected_texture_name: Optional[str],
user_extra_prompt: str,
) -> Tuple[Optional[Image.Image], str]:
if IMAGE_CLIENT_INIT_ERROR is not None or image_client is None:
return None, "❌ A képgeneráló szolgáltatás inicializálása sikertelen."
if room_image is None:
return None, "⚠️ Kérlek, először tölts fel egy fotót a szobáról."
# 1. Bemeneti kép méretezése
room_image = smart_resize(room_image.convert("RGB"), MAX_IMAGE_DIMENSION)
target_w, target_h = room_image.size
# 2. KIMENETI MÉRET KISZÁMÍTÁSA (Dupla szélesség!)
double_w = target_w * 2
if not selected_texture_name:
return None, "⚠️ Kérlek, válassz egy textúrát."
if selected_texture_name not in ALL_TEXTURE_PATHS:
return None, f"⚠️ A '{selected_texture_name}' textúra nem található. Kérlek, frissítsd az oldalt."
texture_path = ALL_TEXTURE_PATHS[selected_texture_name]
try:
texture_image = Image.open(texture_path).convert("RGB")
except Exception as e:
logger.exception("Failed to load texture image")
return None, "❌ Nem sikerült betölteni a textúra képet."
# Cache buster a prompt végére
safe_user_prompt = user_extra_prompt if user_extra_prompt else ""
prompt_with_cache_buster = f"{safe_user_prompt} [System ID: {uuid.uuid4()}]".strip()
# EGYETLEN prompt generálása, a DUPLA SZÉLESSÉGET (double_w) adjuk át neki!
prompt = build_floor_prompt(prompt_with_cache_buster, flooring_type, double_w, target_h)
try:
# EGYETLEN API hívás
response = image_client.models.generate_content(
model=IMAGE_MODEL_ID,
contents=[prompt, room_image, texture_image],
config=types.GenerateContentConfig(
response_modalities=["IMAGE"],
),
)
edited_image: Optional[Image.Image] = None
parts = getattr(response, "parts", None)
if parts is None and getattr(response, "candidates", None):
candidate0 = response.candidates[0]
parts = getattr(candidate0, "content", candidate0).parts
if not parts:
return None, "⚠️ A szolgáltatás válaszolt, de nem küldött vissza képet."
for part in parts:
img_method = getattr(part, "as_image", None)
if callable(img_method):
pil_img = img_method()
if isinstance(pil_img, Image.Image):
edited_image = pil_img.convert("RGB")
break
inline = getattr(part, "inline_data", None)
data = getattr(inline, "data", None) if inline is not None else None
if data:
try:
edited_image = Image.open(BytesIO(data)).convert("RGB")
break
except Exception:
continue
if edited_image is None:
return None, "⚠️ A kép dekódolása sikertelen."
# KULCSFONTOSSÁGÚ: Itt most a DUPLA szélességet ellenőrizzük,
# nehogy a Python visszanyomja a képet szimpla méretűre!
if edited_image.size != (double_w, target_h):
edited_image = edited_image.resize((double_w, target_h), Image.LANCZOS)
status = (
"✅ Dupla padló előnézet elkészült!\n"
f"- Típus: **{flooring_type}**\n"
f"- Textúra: **{selected_texture_name}**\n"
f"Eredmény: **A teljes szoba kétszer renderelve (Balra: Párhuzamos | Jobbra: Merőleges)**"
)
return edited_image, status
except Exception:
logger.exception("Image service API error")
return None, "❌ Valami hiba történt a képszolgáltatással. Kérlek, próbáld újra."
# -----------------------------------------
# Ergonomic UI Layout
# -----------------------------------------
with gr.Blocks(title="Padló Előnézet Tervező") as demo:
gr.HTML(CUSTOM_CSS)
selected_texture_state = gr.State(value=None)
initial_category = "Laminált padló"
initial_gallery_items = CATEGORY_DATA[initial_category]["items"]
if initial_gallery_items:
selected_texture_state.value = CATEGORY_DATA[initial_category]["names"][0]
gr.Markdown(
"""
# 🏠 Padló látványtervező
**Tölts fel egy fotót a szobáról, válassz padlótípust, és nézd meg a látványterveket azonnal.**
"""
)
with gr.Row():
with gr.Column(scale=1):
room_image_input = gr.Image(
label="1. Szoba fotójának feltöltése",
type="pil",
height=450,
sources=["upload", "clipboard"],
)
gr.Markdown("### 2. Padló beállítása")
flooring_type_radio = gr.Radio(
choices=["Laminált padló", "Járólap"],
value=initial_category,
label="Válassz padlótípust",
interactive=True
)
texture_gallery = gr.Gallery(
value=initial_gallery_items,
label="Válassz textúrát",
columns=GALLERY_COLUMNS,
height=320,
allow_preview=False,
interactive=True,
object_fit="cover",
type="pil",
show_label=True
)
selection_feedback = gr.Markdown(
f"Kiválasztva: **{selected_texture_state.value if selected_texture_state.value else 'Semmi'}**"
)
with gr.Accordion("Haladó beállítások", open=False):
extra_prompt_input = gr.Textbox(
label="Egyedi módosítások",
lines=2,
placeholder="pl. Legyen fényesebb, távolítsd el a szőnyeget...",
)
generate_button = gr.Button("✨ Látványtervek Generálása", variant="primary", size="lg")
with gr.Column(scale=2):
edited_output_image = gr.Image(
label="MI Eredmény (A: Párhuzamos | B: Merőleges)",
interactive=False,
height=750
)
status_text = gr.Markdown()
# -----------------------------------------
# Event Wiring
# -----------------------------------------
def on_flooring_type_change(new_type):
items = CATEGORY_DATA.get(new_type, {}).get("items", [])
names = CATEGORY_DATA.get(new_type, {}).get("names", [])
new_selection = names[0] if names else None
feedback_str = f"Kiválasztva: **{new_selection}**" if new_selection else "Kiválasztva: **Semmi**"
return items, new_selection, feedback_str
flooring_type_radio.change(
fn=on_flooring_type_change,
inputs=[flooring_type_radio],
outputs=[texture_gallery, selected_texture_state, selection_feedback]
)
def update_selection(evt: gr.SelectData, current_type):
names = CATEGORY_DATA.get(current_type, {}).get("names", [])
if evt.index < len(names):
name = names[evt.index]
return name, f"Kiválasztva: **{name}**"
return None, "Kiválasztva: **Semmi**"
texture_gallery.select(
fn=update_selection,
inputs=[flooring_type_radio],
outputs=[selected_texture_state, selection_feedback]
)
def clear_output():
return None, "⏳ Generálás folyamatban... (Dupla széles A|B nézet készül)"
generate_button.click(
fn=clear_output,
inputs=[],
outputs=[edited_output_image, status_text],
queue=False
).then(
fn=generate_floor_preview,
inputs=[room_image_input, flooring_type_radio, selected_texture_state, extra_prompt_input],
outputs=[edited_output_image, status_text],
)
if __name__ == "__main__":
DARK_MODE_JS = """
() => {
document.body.classList.toggle('dark', true);
}
"""
demo.launch(js=DARK_MODE_JS)