gabar92 commited on
Commit
a67e105
·
verified ·
1 Parent(s): 393d2a6

Upload 15 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,16 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ laminate/laminate_01.png filter=lfs diff=lfs merge=lfs -text
37
+ laminate/laminate_02.png filter=lfs diff=lfs merge=lfs -text
38
+ laminate/laminate_03.png filter=lfs diff=lfs merge=lfs -text
39
+ laminate/laminate_04.png filter=lfs diff=lfs merge=lfs -text
40
+ laminate/laminate_05.png filter=lfs diff=lfs merge=lfs -text
41
+ laminate/laminate_06.png filter=lfs diff=lfs merge=lfs -text
42
+ tile/tile_01.png filter=lfs diff=lfs merge=lfs -text
43
+ tile/tile_02.png filter=lfs diff=lfs merge=lfs -text
44
+ tile/tile_03.png filter=lfs diff=lfs merge=lfs -text
45
+ tile/tile_04.png filter=lfs diff=lfs merge=lfs -text
46
+ tile/tile_05.png filter=lfs diff=lfs merge=lfs -text
47
+ tile/tile_06.png filter=lfs diff=lfs merge=lfs -text
48
+ tile/tile_07.png filter=lfs diff=lfs merge=lfs -text
app.py ADDED
@@ -0,0 +1,397 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import glob
3
+ import uuid
4
+ from typing import Tuple, Optional
5
+ from io import BytesIO
6
+
7
+ import gradio as gr
8
+ from PIL import Image, ImageFile
9
+
10
+ from google import genai
11
+ from google.genai import types
12
+
13
+ import logging
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # -----------------------------------------
18
+ # Configuration
19
+ # -----------------------------------------
20
+
21
+ TEXTURE_FOLDERS = {
22
+ "Laminált padló": "laminate",
23
+ "Járólap": "tile"
24
+ }
25
+
26
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
27
+
28
+ IMAGE_MODEL_ID = os.getenv("AI_IMAGE_MODEL_ID", "image-model-default")
29
+ print("IMAGE_MODEL_ID:", IMAGE_MODEL_ID)
30
+
31
+ # -----------------------------------------
32
+ # Structured Prompt for A | B Full Room Duplication
33
+ # -----------------------------------------
34
+ # A prompt most már biztonságosan, környezeti változóból töltődik be.
35
+ PROMPT_TEMPLATE = os.getenv("FLOOR_PROMPT_TEMPLATE")
36
+
37
+ if not PROMPT_TEMPLATE:
38
+ logger.warning("⚠️ FLOOR_PROMPT_TEMPLATE secret is missing! Please set it in Hugging Face Space Settings.")
39
+ # Fallback to prevent immediate crash, though API will likely reject an empty prompt.
40
+ PROMPT_TEMPLATE = "{{ \"error\": \"Secret prompt missing.\" }}"
41
+
42
+ # Gallery layout
43
+ GALLERY_COLUMNS = 5
44
+ MAX_IMAGE_DIMENSION = 1280
45
+
46
+ # -----------------------------------------
47
+ # Custom CSS
48
+ # -----------------------------------------
49
+ CUSTOM_CSS = """
50
+ <style>
51
+ .gradio-gallery .selected,
52
+ .gradio-gallery .gallery-item.selected,
53
+ .gradio-gallery :focus-within {
54
+ border-color: #FF8C00 !important;
55
+ border-width: 4px !important;
56
+ transform: scale(0.96);
57
+ transition: all 0.2s ease-in-out;
58
+ outline: 4px solid #FF8C00 !important;
59
+ }
60
+ </style>
61
+ """
62
+
63
+ # -----------------------------------------
64
+ # Texture loading
65
+ # -----------------------------------------
66
+
67
+ def load_textures_from_folder(folder_path: str, max_size: Tuple[int, int] = (256, 256)):
68
+ valid_names = []
69
+ gallery_items = []
70
+ path_map = {}
71
+
72
+ if not os.path.isdir(folder_path):
73
+ os.makedirs(folder_path, exist_ok=True)
74
+ return valid_names, gallery_items, path_map
75
+
76
+ exts = ("*.png", "*.jpg", "*.jpeg", "*.webp")
77
+ found_files = []
78
+ for ext in exts:
79
+ found_files.extend(glob.glob(os.path.join(folder_path, ext)))
80
+
81
+ found_files = sorted(found_files)
82
+
83
+ for path in found_files:
84
+ if not os.path.isfile(path):
85
+ continue
86
+
87
+ name = os.path.basename(path)
88
+ display_name = os.path.splitext(name)[0]
89
+
90
+ try:
91
+ img = Image.open(path).convert("RGB")
92
+ img.thumbnail(max_size, Image.LANCZOS)
93
+
94
+ valid_names.append(display_name)
95
+ gallery_items.append((img, display_name))
96
+ path_map[display_name] = os.path.abspath(path)
97
+
98
+ except Exception as e:
99
+ continue
100
+
101
+ return valid_names, gallery_items, path_map
102
+
103
+ CATEGORY_DATA = {}
104
+ ALL_TEXTURE_PATHS = {}
105
+
106
+ for category, folder in TEXTURE_FOLDERS.items():
107
+ names, items, paths = load_textures_from_folder(folder)
108
+ CATEGORY_DATA[category] = {
109
+ "names": names,
110
+ "items": items
111
+ }
112
+ ALL_TEXTURE_PATHS.update(paths)
113
+
114
+
115
+ # -----------------------------------------
116
+ # Image client
117
+ # -----------------------------------------
118
+
119
+ def get_image_client() -> genai.Client:
120
+ return genai.Client()
121
+
122
+ try:
123
+ image_client = get_image_client()
124
+ IMAGE_CLIENT_INIT_ERROR = None
125
+ except Exception as e:
126
+ image_client = None
127
+ IMAGE_CLIENT_INIT_ERROR = str(e)
128
+
129
+
130
+ # -----------------------------------------
131
+ # Helper Functions
132
+ # -----------------------------------------
133
+
134
+ def smart_resize(img: Image.Image, max_dim: int) -> Image.Image:
135
+ width, height = img.size
136
+ if width <= max_dim and height <= max_dim:
137
+ return img
138
+
139
+ if width > height:
140
+ new_width = max_dim
141
+ new_height = int(height * (max_dim / width))
142
+ else:
143
+ new_height = max_dim
144
+ new_width = int(width * (max_dim / height))
145
+
146
+ return img.resize((new_width, new_height), Image.LANCZOS)
147
+
148
+
149
+ def build_floor_prompt(
150
+ user_extra_prompt: str,
151
+ flooring_type: str,
152
+ width: int,
153
+ height: int
154
+ ) -> str:
155
+ flooring_type_en = "Laminate" if flooring_type == "Laminált padló" else "Tile"
156
+ material_desc = "laminate floor planks" if flooring_type == "Laminált padló" else "floor tiles"
157
+ safe_user_prompt = user_extra_prompt if user_extra_prompt else "None"
158
+
159
+ return PROMPT_TEMPLATE.format(
160
+ width=width,
161
+ height=height,
162
+ flooring_type=flooring_type_en,
163
+ material_desc=material_desc,
164
+ user_extra_prompt=safe_user_prompt,
165
+ ).strip()
166
+
167
+
168
+ # -----------------------------------------
169
+ # Main Logic
170
+ # -----------------------------------------
171
+
172
+ def generate_floor_preview(
173
+ room_image: Optional[Image.Image],
174
+ flooring_type: str,
175
+ selected_texture_name: Optional[str],
176
+ user_extra_prompt: str,
177
+ ) -> Tuple[Optional[Image.Image], str]:
178
+
179
+ if IMAGE_CLIENT_INIT_ERROR is not None or image_client is None:
180
+ return None, "❌ A képgeneráló szolgáltatás inicializálása sikertelen."
181
+
182
+ if room_image is None:
183
+ return None, "⚠️ Kérlek, először tölts fel egy fotót a szobáról."
184
+
185
+ # 1. Bemeneti kép méretezése
186
+ room_image = smart_resize(room_image.convert("RGB"), MAX_IMAGE_DIMENSION)
187
+ target_w, target_h = room_image.size
188
+
189
+ # 2. KIMENETI MÉRET KISZÁMÍTÁSA (Dupla szélesség!)
190
+ double_w = target_w * 2
191
+
192
+ if not selected_texture_name:
193
+ return None, "⚠️ Kérlek, válassz egy textúrát."
194
+
195
+ if selected_texture_name not in ALL_TEXTURE_PATHS:
196
+ return None, f"⚠️ A '{selected_texture_name}' textúra nem található. Kérlek, frissítsd az oldalt."
197
+
198
+ texture_path = ALL_TEXTURE_PATHS[selected_texture_name]
199
+ try:
200
+ texture_image = Image.open(texture_path).convert("RGB")
201
+ except Exception as e:
202
+ logger.exception("Failed to load texture image")
203
+ return None, "❌ Nem sikerült betölteni a textúra képet."
204
+
205
+ # Cache buster a prompt végére
206
+ safe_user_prompt = user_extra_prompt if user_extra_prompt else ""
207
+ prompt_with_cache_buster = f"{safe_user_prompt} [System ID: {uuid.uuid4()}]".strip()
208
+
209
+ # EGYETLEN prompt generálása, a DUPLA SZÉLESSÉGET (double_w) adjuk át neki!
210
+ prompt = build_floor_prompt(prompt_with_cache_buster, flooring_type, double_w, target_h)
211
+
212
+ try:
213
+ # EGYETLEN API hívás
214
+ response = image_client.models.generate_content(
215
+ model=IMAGE_MODEL_ID,
216
+ contents=[prompt, room_image, texture_image],
217
+ config=types.GenerateContentConfig(
218
+ response_modalities=["IMAGE"],
219
+ ),
220
+ )
221
+
222
+ edited_image: Optional[Image.Image] = None
223
+ parts = getattr(response, "parts", None)
224
+ if parts is None and getattr(response, "candidates", None):
225
+ candidate0 = response.candidates[0]
226
+ parts = getattr(candidate0, "content", candidate0).parts
227
+
228
+ if not parts:
229
+ return None, "⚠️ A szolgáltatás válaszolt, de nem küldött vissza képet."
230
+
231
+ for part in parts:
232
+ img_method = getattr(part, "as_image", None)
233
+ if callable(img_method):
234
+ pil_img = img_method()
235
+ if isinstance(pil_img, Image.Image):
236
+ edited_image = pil_img.convert("RGB")
237
+ break
238
+
239
+ inline = getattr(part, "inline_data", None)
240
+ data = getattr(inline, "data", None) if inline is not None else None
241
+ if data:
242
+ try:
243
+ edited_image = Image.open(BytesIO(data)).convert("RGB")
244
+ break
245
+ except Exception:
246
+ continue
247
+
248
+ if edited_image is None:
249
+ return None, "⚠️ A kép dekódolása sikertelen."
250
+
251
+ # KULCSFONTOSSÁGÚ: Itt most a DUPLA szélességet ellenőrizzük,
252
+ # nehogy a Python visszanyomja a képet szimpla méretűre!
253
+ if edited_image.size != (double_w, target_h):
254
+ edited_image = edited_image.resize((double_w, target_h), Image.LANCZOS)
255
+
256
+ status = (
257
+ "✅ Dupla padló előnézet elkészült!\n"
258
+ f"- Típus: **{flooring_type}**\n"
259
+ f"- Textúra: **{selected_texture_name}**\n"
260
+ f"Eredmény: **A teljes szoba kétszer renderelve (Balra: Párhuzamos | Jobbra: Merőleges)**"
261
+ )
262
+ return edited_image, status
263
+
264
+ except Exception:
265
+ logger.exception("Image service API error")
266
+ return None, "❌ Valami hiba történt a képszolgáltatással. Kérlek, próbáld újra."
267
+
268
+
269
+ # -----------------------------------------
270
+ # Ergonomic UI Layout
271
+ # -----------------------------------------
272
+
273
+ with gr.Blocks(title="Padló Előnézet Tervező") as demo:
274
+
275
+ gr.HTML(CUSTOM_CSS)
276
+
277
+ selected_texture_state = gr.State(value=None)
278
+
279
+ initial_category = "Laminált padló"
280
+ initial_gallery_items = CATEGORY_DATA[initial_category]["items"]
281
+
282
+ if initial_gallery_items:
283
+ selected_texture_state.value = CATEGORY_DATA[initial_category]["names"][0]
284
+
285
+ gr.Markdown(
286
+ """
287
+ # 🏠 Padló látványtervező
288
+ **Tölts fel egy fotót a szobáról, válassz padlótípust, és nézd meg a látványterveket azonnal.**
289
+ """
290
+ )
291
+
292
+ with gr.Row():
293
+ with gr.Column(scale=1):
294
+
295
+ room_image_input = gr.Image(
296
+ label="1. Szoba fotójának feltöltése",
297
+ type="pil",
298
+ height=450,
299
+ sources=["upload", "clipboard"],
300
+ )
301
+
302
+ gr.Markdown("### 2. Padló beállítása")
303
+
304
+ flooring_type_radio = gr.Radio(
305
+ choices=["Laminált padló", "Járólap"],
306
+ value=initial_category,
307
+ label="Válassz padlótípust",
308
+ interactive=True
309
+ )
310
+
311
+ texture_gallery = gr.Gallery(
312
+ value=initial_gallery_items,
313
+ label="Válassz textúrát",
314
+ columns=GALLERY_COLUMNS,
315
+ height=320,
316
+ allow_preview=False,
317
+ interactive=True,
318
+ object_fit="cover",
319
+ type="pil",
320
+ show_label=True
321
+ )
322
+
323
+ selection_feedback = gr.Markdown(
324
+ f"Kiválasztva: **{selected_texture_state.value if selected_texture_state.value else 'Semmi'}**"
325
+ )
326
+
327
+ with gr.Accordion("Haladó beállítások", open=False):
328
+ extra_prompt_input = gr.Textbox(
329
+ label="Egyedi módosítások",
330
+ lines=2,
331
+ placeholder="pl. Legyen fényesebb, távolítsd el a szőnyeget...",
332
+ )
333
+
334
+ generate_button = gr.Button("✨ Látványtervek Generálása", variant="primary", size="lg")
335
+
336
+ with gr.Column(scale=2):
337
+ edited_output_image = gr.Image(
338
+ label="MI Eredmény (A: Párhuzamos | B: Merőleges)",
339
+ interactive=False,
340
+ height=750
341
+ )
342
+
343
+ status_text = gr.Markdown()
344
+
345
+ # -----------------------------------------
346
+ # Event Wiring
347
+ # -----------------------------------------
348
+
349
+ def on_flooring_type_change(new_type):
350
+ items = CATEGORY_DATA.get(new_type, {}).get("items", [])
351
+ names = CATEGORY_DATA.get(new_type, {}).get("names", [])
352
+
353
+ new_selection = names[0] if names else None
354
+ feedback_str = f"Kiválasztva: **{new_selection}**" if new_selection else "Kiválasztva: **Semmi**"
355
+
356
+ return items, new_selection, feedback_str
357
+
358
+ flooring_type_radio.change(
359
+ fn=on_flooring_type_change,
360
+ inputs=[flooring_type_radio],
361
+ outputs=[texture_gallery, selected_texture_state, selection_feedback]
362
+ )
363
+
364
+ def update_selection(evt: gr.SelectData, current_type):
365
+ names = CATEGORY_DATA.get(current_type, {}).get("names", [])
366
+ if evt.index < len(names):
367
+ name = names[evt.index]
368
+ return name, f"Kiválasztva: **{name}**"
369
+ return None, "Kiválasztva: **Semmi**"
370
+
371
+ texture_gallery.select(
372
+ fn=update_selection,
373
+ inputs=[flooring_type_radio],
374
+ outputs=[selected_texture_state, selection_feedback]
375
+ )
376
+
377
+ def clear_output():
378
+ return None, "⏳ Generálás folyamatban... (Dupla széles A|B nézet készül)"
379
+
380
+ generate_button.click(
381
+ fn=clear_output,
382
+ inputs=[],
383
+ outputs=[edited_output_image, status_text],
384
+ queue=False
385
+ ).then(
386
+ fn=generate_floor_preview,
387
+ inputs=[room_image_input, flooring_type_radio, selected_texture_state, extra_prompt_input],
388
+ outputs=[edited_output_image, status_text],
389
+ )
390
+
391
+ if __name__ == "__main__":
392
+ DARK_MODE_JS = """
393
+ () => {
394
+ document.body.classList.toggle('dark', true);
395
+ }
396
+ """
397
+ demo.launch(js=DARK_MODE_JS)
laminate/laminate_01.png ADDED

Git LFS Details

  • SHA256: 1fce77f7984043bd6ff5b0a916efce5c51a679fb1b4677dd48f5dc6b9bfb9906
  • Pointer size: 131 Bytes
  • Size of remote file: 419 kB
laminate/laminate_02.png ADDED

Git LFS Details

  • SHA256: cd6d65b9a64e375d9947a00e911f2cf4913b15c1b4ca1e84d2561ade5eab8455
  • Pointer size: 131 Bytes
  • Size of remote file: 637 kB
laminate/laminate_03.png ADDED

Git LFS Details

  • SHA256: 80fdbf6c9e689e55fed150e31bda86aa87860985edd1ee91e894ecfb4bebb9b0
  • Pointer size: 131 Bytes
  • Size of remote file: 792 kB
laminate/laminate_04.png ADDED

Git LFS Details

  • SHA256: 46a3cb4e508bc39dbd4f500b67def78794e4a4d3ce2d058acc3cdfa389133b94
  • Pointer size: 131 Bytes
  • Size of remote file: 624 kB
laminate/laminate_05.png ADDED

Git LFS Details

  • SHA256: e715d2cbe61f43d63740b9f3663623ec94c067c876164c919b3b7535a7a151ad
  • Pointer size: 131 Bytes
  • Size of remote file: 833 kB
laminate/laminate_06.png ADDED

Git LFS Details

  • SHA256: fa6dad46438e4b23d07f61a652e2e6a86500c5e95b2e6074f1c4e275be20c071
  • Pointer size: 131 Bytes
  • Size of remote file: 684 kB
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ google-genai==1.53.0
2
+ Pillow==10.4.0
tile/tile_01.png ADDED

Git LFS Details

  • SHA256: 3a1ecd7396080a3230578a2d844ddfbdba7347debc176dce2115f128ce5d79bd
  • Pointer size: 131 Bytes
  • Size of remote file: 251 kB
tile/tile_02.png ADDED

Git LFS Details

  • SHA256: 9fe035ce7246e78c78125e2e054b0babea7a44a366eb88ea0d3f2719bdadff7c
  • Pointer size: 131 Bytes
  • Size of remote file: 137 kB
tile/tile_03.png ADDED

Git LFS Details

  • SHA256: 6535db3da814315f5973349cccef49d012d4cc84350ca3da0947c530b8a31f4f
  • Pointer size: 131 Bytes
  • Size of remote file: 226 kB
tile/tile_04.png ADDED

Git LFS Details

  • SHA256: 470af3c5fdb7d659400835c1ba7ad8465518cce4e3302bb462249dce3b33dbfb
  • Pointer size: 131 Bytes
  • Size of remote file: 142 kB
tile/tile_05.png ADDED

Git LFS Details

  • SHA256: 4816d569359fcfdd654af74fe4c75a4e2e1be9a794d3b8086f9f856b894ca588
  • Pointer size: 131 Bytes
  • Size of remote file: 124 kB
tile/tile_06.png ADDED

Git LFS Details

  • SHA256: a2372dc4d4b0109d409b26135f637385200313334f756e2c5ffabc28fbf186dc
  • Pointer size: 131 Bytes
  • Size of remote file: 261 kB
tile/tile_07.png ADDED

Git LFS Details

  • SHA256: 6ee853afacd60d038d65964ce1680b90232e54cb7b1dd1c12f9a02351bc4cd1a
  • Pointer size: 131 Bytes
  • Size of remote file: 311 kB