eduardo4547 commited on
Commit
d61c655
·
verified ·
1 Parent(s): db3337f

Upload 217 files

Browse files
backend/.env CHANGED
@@ -1,6 +1,6 @@
1
  MONGODB_URI=mongodb+srv://alaneduardodelacruz407_db_user:YGUWdpXqUiGH104Q@naufar-cluster.n9htwoa.mongodb.net/
2
  # Space GPU (principal) — ZeroGPU, más rápido
3
- GRADIO_SPACE_URL=https://eduardo4547-hyper-reality-sam2-gpu.hf.space
4
  # Space CPU (respaldo automático si el GPU falla o agota quota)
5
  GRADIO_CPU_FALLBACK_URL=https://eduardo4547-hyper-reality-sam2-cpu.hf.space
6
  # Para desarrollo local:
 
1
  MONGODB_URI=mongodb+srv://alaneduardodelacruz407_db_user:YGUWdpXqUiGH104Q@naufar-cluster.n9htwoa.mongodb.net/
2
  # Space GPU (principal) — ZeroGPU, más rápido
3
+ GRADIO_SPACE_URL=http://localhost:7860
4
  # Space CPU (respaldo automático si el GPU falla o agota quota)
5
  GRADIO_CPU_FALLBACK_URL=https://eduardo4547-hyper-reality-sam2-cpu.hf.space
6
  # Para desarrollo local:
backend/__pycache__/main.cpython-312.pyc CHANGED
Binary files a/backend/__pycache__/main.cpython-312.pyc and b/backend/__pycache__/main.cpython-312.pyc differ
 
backend/data/presets.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "ACM": {
3
+ "ancho_panel_m": 0.3,
4
+ "alto_panel_m": 0.3,
5
+ "intensidad_textura": 0.85,
6
+ "separacion_vertical_px": 0.3,
7
+ "separacion_horizontal_px": 0.3,
8
+ "orientacion": "vertical",
9
+ "perspectiva_horizontal": 0.5,
10
+ "perspectiva_vertical": 0.7,
11
+ "modo_fusion": "luz suave"
12
+ },
13
+ "WPC": {
14
+ "ancho_panel_m": 0.3,
15
+ "alto_panel_m": 1.0,
16
+ "intensidad_textura": 0.85,
17
+ "separacion_vertical_px": 0.3,
18
+ "separacion_horizontal_px": 0.3,
19
+ "orientacion": "vertical",
20
+ "perspectiva_horizontal": 0.5,
21
+ "perspectiva_vertical": 0.7,
22
+ "modo_fusion": "luz suave"
23
+ },
24
+ "WPC_DECK": {
25
+ "ancho_panel_m": 0.25,
26
+ "alto_panel_m": 0.05,
27
+ "intensidad_textura": 0.85,
28
+ "separacion_vertical_px": 0,
29
+ "separacion_horizontal_px": 5,
30
+ "orientacion": "horizontal",
31
+ "perspectiva_horizontal": 0.5,
32
+ "perspectiva_vertical": 0.5,
33
+ "modo_fusion": "luz suave"
34
+ }
35
+ }
backend/logs/app.log CHANGED
The diff for this file is too large to render. See raw diff
 
backend/main.py CHANGED
@@ -13,7 +13,7 @@ from fastapi.middleware.cors import CORSMiddleware
13
  from fastapi.staticfiles import StaticFiles
14
 
15
  from core.config import GRADIO_SPACE_URL, logger
16
- from routers import auth, catalog, media, pages, segmentation, sessions, share
17
  from routers.catalog import seed_catalog
18
  from services.sam2_service import lifespan
19
 
@@ -51,6 +51,7 @@ app.include_router(media.router)
51
  app.include_router(catalog.router)
52
  app.include_router(sessions.router)
53
  app.include_router(segmentation.router)
 
54
 
55
  # Static files
56
  BASE_DIR = Path(__file__).resolve().parent
 
13
  from fastapi.staticfiles import StaticFiles
14
 
15
  from core.config import GRADIO_SPACE_URL, logger
16
+ from routers import auth, catalog, media, pages, segmentation, sessions, share, presets
17
  from routers.catalog import seed_catalog
18
  from services.sam2_service import lifespan
19
 
 
51
  app.include_router(catalog.router)
52
  app.include_router(sessions.router)
53
  app.include_router(segmentation.router)
54
+ app.include_router(presets.router)
55
 
56
  # Static files
57
  BASE_DIR = Path(__file__).resolve().parent
backend/routers/__pycache__/catalog.cpython-312.pyc CHANGED
Binary files a/backend/routers/__pycache__/catalog.cpython-312.pyc and b/backend/routers/__pycache__/catalog.cpython-312.pyc differ
 
backend/routers/__pycache__/presets.cpython-312.pyc ADDED
Binary file (1.95 kB). View file
 
backend/routers/presets.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException
2
+ from pydantic import BaseModel
3
+ from typing import Any
4
+
5
+ from services.presets_service import save_preset, get_preset, list_presets, delete_preset
6
+
7
+ router = APIRouter(prefix="/api/presets")
8
+
9
+
10
+ class PresetPayload(BaseModel):
11
+ name: str
12
+ params: dict
13
+
14
+
15
+ @router.get("/")
16
+ async def api_list_presets() -> dict:
17
+ return list_presets()
18
+
19
+
20
+ @router.get("/{name}")
21
+ async def api_get_preset(name: str) -> dict:
22
+ return get_preset(name)
23
+
24
+
25
+ @router.post("/")
26
+ async def api_save_preset(payload: PresetPayload) -> dict:
27
+ saved = save_preset(payload.name, payload.params)
28
+ return {"message": "saved", "preset": saved}
29
+
30
+
31
+ @router.delete("/{name}")
32
+ async def api_delete_preset(name: str) -> dict:
33
+ delete_preset(name)
34
+ return {"message": "deleted", "name": name}
backend/services/__pycache__/gradio_client_service.cpython-312.pyc CHANGED
Binary files a/backend/services/__pycache__/gradio_client_service.cpython-312.pyc and b/backend/services/__pycache__/gradio_client_service.cpython-312.pyc differ
 
backend/services/__pycache__/presets_service.cpython-312.pyc ADDED
Binary file (2.79 kB). View file
 
backend/services/__pycache__/texture_service.cpython-312.pyc CHANGED
Binary files a/backend/services/__pycache__/texture_service.cpython-312.pyc and b/backend/services/__pycache__/texture_service.cpython-312.pyc differ
 
backend/services/gradio_client_service.py CHANGED
@@ -102,3 +102,75 @@ async def segment_via_gradio(image_path: Path) -> tuple[np.ndarray, int]:
102
  Async wrapper — offloads the blocking call (with GPU→CPU fallback) to a thread.
103
  """
104
  return await asyncio.to_thread(segment_via_gradio_sync, image_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  Async wrapper — offloads the blocking call (with GPU→CPU fallback) to a thread.
103
  """
104
  return await asyncio.to_thread(segment_via_gradio_sync, image_path)
105
+
106
+
107
+ def _call_gradio_render_sync(
108
+ image_path: Path,
109
+ label_map_b64: str,
110
+ mask_index: int,
111
+ texture_name: str | None,
112
+ texture_b64: str | None,
113
+ params_json: str,
114
+ space_url: str,
115
+ ) -> tuple[np.ndarray, dict]:
116
+ """
117
+ Synchronous call to the Gradio Space render endpoint.
118
+ Returns (rendered_image_np, combined_dict)
119
+ """
120
+ from gradio_client import Client, file # type: ignore
121
+
122
+ client = Client(space_url, httpx_kwargs={"timeout": 300.0})
123
+
124
+ # Prepare args: image file + other strings/values
125
+ inputs = [file(str(image_path)), label_map_b64 or "", int(mask_index or 1), texture_name or "", texture_b64 or "", params_json or "{}"]
126
+
127
+ rendered_img, combined_json_str = client.predict(*inputs, api_name="/render")
128
+
129
+ if not isinstance(combined_json_str, str):
130
+ raise ValueError(f"Unexpected response type from Gradio Space render: {type(combined_json_str)}")
131
+
132
+ combined = json.loads(combined_json_str)
133
+ if "error" in combined:
134
+ raise RuntimeError(f"Gradio Space render error: {combined['error'][:500]}")
135
+
136
+ return rendered_img, combined
137
+
138
+
139
+ def render_via_gradio_sync(
140
+ image_path: Path,
141
+ label_map_b64: str,
142
+ mask_index: int = 1,
143
+ texture_name: str | None = None,
144
+ texture_b64: str | None = None,
145
+ params_json: str = "{}",
146
+ ) -> tuple[np.ndarray, dict]:
147
+ if not is_gradio_enabled():
148
+ raise RuntimeError("GRADIO_SPACE_URL is not configured")
149
+
150
+ gpu_error: Exception | None = None
151
+ try:
152
+ return _call_gradio_render_sync(image_path, label_map_b64, mask_index, texture_name, texture_b64, params_json, GRADIO_SPACE_URL)
153
+ except Exception as e:
154
+ gpu_error = e
155
+ logger.warning("GPU Space render failed (%s), trying CPU fallback...", gpu_error)
156
+
157
+ if not GRADIO_CPU_FALLBACK_URL:
158
+ raise RuntimeError(f"GPU Gradio Space render failed and no CPU fallback configured. Error: {gpu_error}")
159
+
160
+ try:
161
+ return _call_gradio_render_sync(image_path, label_map_b64, mask_index, texture_name, texture_b64, params_json, GRADIO_CPU_FALLBACK_URL)
162
+ except Exception as exc_cpu:
163
+ raise RuntimeError(
164
+ f"Both Gradio Spaces render failed.\n GPU ({GRADIO_SPACE_URL}): {gpu_error}\n CPU ({GRADIO_CPU_FALLBACK_URL}): {exc_cpu}"
165
+ ) from exc_cpu
166
+
167
+
168
+ async def render_via_gradio(
169
+ image_path: Path,
170
+ label_map_b64: str,
171
+ mask_index: int = 1,
172
+ texture_name: str | None = None,
173
+ texture_b64: str | None = None,
174
+ params_json: str = "{}",
175
+ ) -> tuple[np.ndarray, dict]:
176
+ return await asyncio.to_thread(render_via_gradio_sync, image_path, label_map_b64, mask_index, texture_name, texture_b64, params_json)
backend/services/presets_service.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import json
3
+ from typing import Any, Dict
4
+
5
+ from fastapi import HTTPException
6
+
7
+ DATA_DIR = Path(__file__).resolve().parent.parent / "data"
8
+ DATA_DIR.mkdir(parents=True, exist_ok=True)
9
+ PRESETS_PATH = DATA_DIR / "presets.json"
10
+ if not PRESETS_PATH.exists():
11
+ PRESETS_PATH.write_text(json.dumps({}), encoding="utf-8")
12
+
13
+
14
+ def _read_all() -> Dict[str, Any]:
15
+ try:
16
+ text = PRESETS_PATH.read_text(encoding="utf-8")
17
+ return json.loads(text) if text.strip() else {}
18
+ except Exception:
19
+ return {}
20
+
21
+
22
+ def list_presets() -> Dict[str, Any]:
23
+ return _read_all()
24
+
25
+
26
+ def get_preset(name: str) -> Dict[str, Any]:
27
+ presets = _read_all()
28
+ if name not in presets:
29
+ raise HTTPException(status_code=404, detail=f"Preset not found: {name}")
30
+ return presets[name]
31
+
32
+
33
+ def save_preset(name: str, params: Dict[str, Any]) -> Dict[str, Any]:
34
+ if not name or not isinstance(params, dict):
35
+ raise HTTPException(status_code=400, detail="Invalid preset payload")
36
+ presets = _read_all()
37
+ presets[name] = params
38
+ PRESETS_PATH.write_text(json.dumps(presets, ensure_ascii=False, indent=2), encoding="utf-8")
39
+ return presets[name]
40
+
41
+
42
+ def delete_preset(name: str) -> None:
43
+ presets = _read_all()
44
+ if name in presets:
45
+ del presets[name]
46
+ PRESETS_PATH.write_text(json.dumps(presets, ensure_ascii=False, indent=2), encoding="utf-8")
47
+ else:
48
+ raise HTTPException(status_code=404, detail=f"Preset not found: {name}")
backend/services/texture_service.py CHANGED
@@ -655,6 +655,173 @@ def apply_local_texture_sync(payload: ApplyTextureRequest) -> dict[str, Any]:
655
  lighting_mode = str(getattr(payload, "lighting_mode", "scene") or "scene").strip().lower()
656
 
657
  material = classify_texture_material(payload.texture_name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
  surface_type, inferred_angle, blend_alpha, target_w = infer_surface_type_and_direction(
659
  binary_mask, width, height, payload.texture_name,
660
  )
 
655
  lighting_mode = str(getattr(payload, "lighting_mode", "scene") or "scene").strip().lower()
656
 
657
  material = classify_texture_material(payload.texture_name)
658
+
659
+ # Integración: si la textura es 'acm' y hay un Gradio Space configurado,
660
+ # delegar el render al Space usando el preset 'ACM' (si existe).
661
+ # Construimos un label_map reducido (1 = unión de máscaras seleccionadas)
662
+ # y enviamos la textura como base64 para evitar dependencias de archivos.
663
+ try:
664
+ import json as _json
665
+ import base64 as _base64
666
+ from services.gradio_client_service import is_gradio_enabled, render_via_gradio_sync
667
+ from services.presets_service import get_preset
668
+
669
+ try:
670
+ # Activar render remoto para ACM y también para materiales 'deck'
671
+ _use_remote = is_gradio_enabled() and material in {"acm", "deck"}
672
+ except Exception:
673
+ _use_remote = False
674
+
675
+ if _use_remote:
676
+ # Preparar label_map de unión
677
+ lm = cv2.imread(str(label_path), cv2.IMREAD_GRAYSCALE)
678
+ if lm is None:
679
+ raise HTTPException(status_code=500, detail="Could not read label map for remote render")
680
+
681
+ union = np.zeros_like(lm, dtype=np.uint8)
682
+ for idx in payload.mask_indices:
683
+ union[lm == int(idx)] = 1
684
+
685
+ # Encode label_map (PNG) -> base64
686
+ buf = io.BytesIO()
687
+ Image.fromarray(union, mode="L").save(buf, format="PNG")
688
+ label_map_b64 = _base64.b64encode(buf.getvalue()).decode("utf-8")
689
+
690
+ # Encodear textura como base64
691
+ tex_path = resolve_texture_path(payload.texture_name)
692
+ tex_pil = load_texture_pil_rgb(tex_path)
693
+ tbuf = io.BytesIO()
694
+ tex_pil.save(tbuf, format="PNG")
695
+ texture_b64 = _base64.b64encode(tbuf.getvalue()).decode("utf-8")
696
+
697
+ # Intentar obtener preset correspondiente:
698
+ # - Si el nombre contiene 'wpc' + 'deck' -> 'WPC_DECK'
699
+ # - Si contiene solo 'wpc' -> 'WPC'
700
+ # - En otro caso -> 'ACM'
701
+ preset = {}
702
+ try:
703
+ _tex_key = (payload.texture_name or "").lower()
704
+ if "wpc" in _tex_key:
705
+ if "deck" in _tex_key:
706
+ preset = get_preset("WPC_DECK")
707
+ else:
708
+ preset = get_preset("WPC")
709
+ else:
710
+ preset = get_preset("ACM")
711
+ except Exception:
712
+ preset = {}
713
+
714
+ # Detectar tipo de superficie para que el Space oriente correctamente
715
+ # la textura (piso=horizontal, pared=vertical)
716
+ try:
717
+ _surface_type, _, _, _ = infer_surface_type_and_direction(
718
+ binary_mask, width, height, payload.texture_name
719
+ )
720
+ except Exception:
721
+ _surface_type = "wall"
722
+
723
+ # Merge: preset base + surface_type (surface_type tiene prioridad
724
+ # sobre la orientacion del preset cuando es piso o deck)
725
+ params_dict = dict(preset or {})
726
+ params_dict["surface_type"] = _surface_type
727
+ params_json = _json.dumps(params_dict)
728
+
729
+ try:
730
+ rendered_img_np, meta = render_via_gradio_sync(
731
+ image_path, label_map_b64, 1, None, texture_b64, params_json
732
+ )
733
+ except Exception as _e:
734
+ logger.warning("Remote render via Gradio failed, falling back to local: %s", _e)
735
+ rendered_img_np = None
736
+
737
+ if rendered_img_np is not None:
738
+ # Guardar resultado remoto con mismo formato de salida
739
+ input_stem = Path(image_path).stem
740
+ edit_suffix = uuid.uuid4().hex[:8]
741
+ out_filename = f"{input_stem}_edit_{edit_suffix}.jpg"
742
+ out_path = UPLOAD_DIR / out_filename
743
+ # Aceptar tanto np.ndarray como PIL.Image devueltos por el Space
744
+ try:
745
+ if isinstance(rendered_img_np, np.ndarray):
746
+ img_to_save = Image.fromarray(rendered_img_np)
747
+ elif isinstance(rendered_img_np, Image.Image):
748
+ img_to_save = rendered_img_np
749
+ else:
750
+ # manejar strings: pueden ser rutas locales, URLs o base64
751
+ if isinstance(rendered_img_np, str):
752
+ s = rendered_img_np.strip()
753
+ # 1) ruta local descargada por gradio_client
754
+ try:
755
+ p = Path(s)
756
+ if p.exists():
757
+ img_to_save = Image.open(str(p))
758
+ else:
759
+ # 2) intentar descargar si es URL
760
+ if s.startswith("http://") or s.startswith("https://") or "/gradio_api/file=" in s:
761
+ try:
762
+ from urllib.request import urlopen as _urlopen
763
+
764
+ with _urlopen(s) as resp:
765
+ data = resp.read()
766
+ img_to_save = Image.open(io.BytesIO(data))
767
+ except Exception:
768
+ # 3) fallback: intentar decodificar base64
769
+ try:
770
+ b = _base64.b64decode(s)
771
+ img_to_save = Image.open(io.BytesIO(b))
772
+ except Exception:
773
+ raise RuntimeError("Unsupported rendered image string from Gradio Space")
774
+ else:
775
+ # 3) fallback: intentar decodificar base64
776
+ try:
777
+ b = _base64.b64decode(s)
778
+ img_to_save = Image.open(io.BytesIO(b))
779
+ except Exception:
780
+ raise RuntimeError("Unsupported rendered image string from Gradio Space")
781
+ except Exception:
782
+ raise RuntimeError("Unsupported rendered image string from Gradio Space")
783
+ else:
784
+ # intentar convertir bytes -> Image
785
+ try:
786
+ img_to_save = Image.open(io.BytesIO(rendered_img_np))
787
+ except Exception:
788
+ raise RuntimeError("Unsupported rendered image type from Gradio Space")
789
+
790
+ img_to_save.convert("RGB").save(str(out_path), format="JPEG", quality=UPLOAD_JPEG_QUALITY, optimize=True)
791
+ except Exception:
792
+ logger.exception("Failed to save rendered image from Gradio Space")
793
+ raise
794
+
795
+ try:
796
+ out_label_path = masks_dir / f"{Path(out_filename).stem}_labels.png"
797
+ if label_path.exists():
798
+ shutil.copyfile(str(label_path), str(out_label_path))
799
+ except Exception:
800
+ logger.exception("Failed to copy label map for remote output image")
801
+
802
+ return {
803
+ "message": "Texture applied successfully (remote)",
804
+ "original": safe_name,
805
+ "mask_indices": payload.mask_indices,
806
+ "texture_name": payload.texture_name,
807
+ "material": material,
808
+ "direction_mode": direction_mode,
809
+ "surface_type": None,
810
+ "replace_mode": replace_mode,
811
+ "replace_strength": None,
812
+ "lighting_mode": lighting_mode,
813
+ "light_angle_degrees": None,
814
+ "light_intensity": None,
815
+ "blend_alpha": None,
816
+ "applied_angle_degrees": None,
817
+ "output_filename": out_filename,
818
+ "output_url": f"/seg/image/{out_filename}",
819
+ }
820
+ except HTTPException:
821
+ raise
822
+ except Exception:
823
+ logger.exception("Error during remote render integration (continuing local flow)")
824
+
825
  surface_type, inferred_angle, blend_alpha, target_w = infer_surface_type_and_direction(
826
  binary_mask, width, height, payload.texture_name,
827
  )
frontend/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const appVersion = "0.1.0-dev.20260508T144158";
 
1
+ export const appVersion = "0.1.0-dev.20260511T131335";