diff --git a/.gitattributes b/.gitattributes index 6b693b67015daff0fad234f760fb1ec0014f913d..f6c5fcc97bca298c430be904abdaf627b48cd477 100644 --- a/.gitattributes +++ b/.gitattributes @@ -156,3 +156,9 @@ backend/texturas/Texture_WPC_EXTERIOR_INTERIOR/WPC_negro.png filter=lfs diff=lfs backend/texturas/Texture_WPC_DECK/DECK_gris.png filter=lfs diff=lfs merge=lfs -text backend/texturas/Texture_WPC_DECK/DECK_madera_oscuro.png filter=lfs diff=lfs merge=lfs -text backend/texturas/Texture_WPC_DECK/DECK_madera.png filter=lfs diff=lfs merge=lfs -text +backend/uploads/acm[[:space:]]azul_edit_edb46e2f.jpg filter=lfs diff=lfs merge=lfs -text +backend/uploads/acm[[:space:]]gris_edit_09f2e747.jpg filter=lfs diff=lfs merge=lfs -text +backend/uploads/acm[[:space:]]gris_edit_19d266a8.jpg filter=lfs diff=lfs merge=lfs -text +backend/uploads/acm[[:space:]]gris.jpg filter=lfs diff=lfs merge=lfs -text +backend/uploads/hyper-reality-1778254222964_edit_9c5de50a_edit_7e5043d5.jpg filter=lfs diff=lfs merge=lfs -text +backend/uploads/hyper-reality-1778254222964_edit_a92f4266_edit_161ee5c4.jpg filter=lfs diff=lfs merge=lfs -text diff --git a/backend/.env b/backend/.env index 6d49d25e350c91e0b77a177e6a9641341dc81713..e63e77fca3c6762d9c15610ddf2461a40b099c32 100644 --- a/backend/.env +++ b/backend/.env @@ -1,8 +1,11 @@ MONGODB_URI=mongodb+srv://alaneduardodelacruz407_db_user:YGUWdpXqUiGH104Q@naufar-cluster.n9htwoa.mongodb.net/ # Space GPU (principal) — ZeroGPU, más rápido -GRADIO_SPACE_URL=https://eduardo4547-hyper-reality-sam2-gpu.hf.space +GRADIO_SPACE_URL=http://localhost:7860 # Space CPU (respaldo automático si el GPU falla o agota quota) GRADIO_CPU_FALLBACK_URL=https://eduardo4547-hyper-reality-sam2-cpu.hf.space # Para desarrollo local: # GRADIO_SPACE_URL=http://localhost:7860 -# https://eduardo4547-hyper-reality-sam2-gpu.hf.space \ No newline at end of file +# https://eduardo4547-hyper-reality-sam2-gpu.hf.space +# URL del servicio local que procesa imágenes con OpenAI (ejemplo Flask/Gradio proxy) +OPENAI_PROCESS_URL=http://localhost:7861/api/process +OPENAI_API_KEY=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 \ No newline at end of file diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc index 9a7618a0747c35d1d44dad83368500ddb65c3cd3..f9c77d6310aa8c4ccae3246c0452eb9eb98b3eef 100644 Binary files a/backend/__pycache__/main.cpython-312.pyc and b/backend/__pycache__/main.cpython-312.pyc differ diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eac8985c07d1c7e66f5c8cee3b0d98bb13bb46f9 Binary files /dev/null and b/backend/__pycache__/main.cpython-313.pyc differ diff --git a/backend/core/__pycache__/__init__.cpython-313.pyc b/backend/core/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8958e2e1c539ba0a0f800a22dcc22f643efb41f8 Binary files /dev/null and b/backend/core/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/core/__pycache__/config.cpython-312.pyc b/backend/core/__pycache__/config.cpython-312.pyc index d6443406a9a4fd4f1d1d4c274a60ec94e56b3aa2..8517fc56ce38de0d0fc9e68f87cddba2ff42c75b 100644 Binary files a/backend/core/__pycache__/config.cpython-312.pyc and b/backend/core/__pycache__/config.cpython-312.pyc differ diff --git a/backend/core/__pycache__/config.cpython-313.pyc b/backend/core/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ee238adc5af8eb4f4505667e8f36d5643f6e929a Binary files /dev/null and b/backend/core/__pycache__/config.cpython-313.pyc differ diff --git a/backend/core/config.py b/backend/core/config.py index 042d330b6933c69fffee90bdb6c670dd01c457c0..b68f5980d84d7567c7af9d67583eef8d94fe5132 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -1,92 +1,92 @@ -import logging -import os -import time -from datetime import datetime, timezone -from pathlib import Path - -BASE_DIR = Path(__file__).resolve().parent.parent - -UPLOAD_DIR = BASE_DIR / "uploads" -UPLOAD_DIR.mkdir(parents=True, exist_ok=True) - -VIDEO_UPLOAD_DIR = UPLOAD_DIR / "videos" -VIDEO_UPLOAD_DIR.mkdir(parents=True, exist_ok=True) - -OUTPUT_DIR = BASE_DIR / "outputs" -OUTPUT_DIR.mkdir(parents=True, exist_ok=True) - -VIDEO_OUTPUT_DIR = OUTPUT_DIR / "videos" -VIDEO_OUTPUT_DIR.mkdir(parents=True, exist_ok=True) - -TEXTURE_DIR = BASE_DIR / "texturas" -TEXTURE_DIR.mkdir(parents=True, exist_ok=True) - -LOG_DIR = BASE_DIR / "logs" -LOG_DIR.mkdir(exist_ok=True) - -TEMPLATES_DIR = BASE_DIR / "templates" -TEMPLATES_DIR.mkdir(parents=True, exist_ok=True) - -CLASSIC_DASHBOARD_HTML_PATH = TEMPLATES_DIR / "classic_dashboard.html" - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s %(levelname)s %(name)s: %(message)s", - handlers=[ - logging.FileHandler(LOG_DIR / "app.log", encoding="utf-8"), - logging.StreamHandler(), - ], -) -logger = logging.getLogger("backend.segmentation") - -SAM2_CONFIG_PATH = os.getenv("SAM2_CONFIG_PATH", "configs/sam2.1/sam2.1_hiera_l.yaml") -SAM2_MODEL_PATH = os.getenv("SAM2_MODEL_PATH") -SAM2_DEFAULT_MODEL_NAMES = ( - "sam2.1_hiera_large_fresh.pt", - "sam2.1_hiera_large.pt", - "sam2.1_hiera_large.pth", - "sam2_hiera_large.pt", -) -SAM2_MODEL_DIR_CANDIDATES = ("models", "modelo") -SAM2_UNLOAD_AFTER_USE = str(os.getenv("SAM2_UNLOAD_AFTER_USE", "0")).strip().lower() in {"1", "true", "yes"} -FRONTEND_DEBUG = str(os.getenv("FRONTEND_DEBUG", "0")).strip().lower() in {"1", "true", "yes", "on"} - -# URL del Space de Gradio GPU (principal). Si falla, se usa el CPU fallback. -# Local: http://localhost:7860 -# Producción: https://.hf.space -GRADIO_SPACE_URL: str = os.getenv("GRADIO_SPACE_URL", "").rstrip("/") - -# URL del Space de Gradio CPU (respaldo automático si el GPU falla o agota quota). -GRADIO_CPU_FALLBACK_URL: str = os.getenv("GRADIO_CPU_FALLBACK_URL", "").rstrip("/") - -MAX_UPLOAD_WIDTH = 1024 -UPLOAD_JPEG_QUALITY = 82 -SD_JOB_STALE_SECONDS = 120 -UPLOAD_JOB_STALE_SECONDS = 900 -UPLOAD_BASE_SECONDS = 8.0 -UPLOAD_SECONDS_PER_MEGAPIXEL = 70.0 -SD_QUICK_TIMEOUT_SECONDS = 15.0 - -SEMANTIC_MODEL_ID = "nvidia/segformer-b5-finetuned-ade-640-640" -DEPTH_MODEL_ID = "Intel/dpt-hybrid-midas" - - -def utc_now_iso() -> str: - return datetime.now(timezone.utc).isoformat() - - -def log_timing_start(step_name: str) -> float: - started = time.perf_counter() - logger.info(f"[{step_name}] START at {utc_now_iso()}") - return started - - -def log_timing_end(step_name: str, started: float) -> None: - elapsed = time.perf_counter() - started - logger.info(f"[{step_name}] DONE {elapsed:.3f}s at {utc_now_iso()}") - - -def load_classic_dashboard_html() -> str: - if not CLASSIC_DASHBOARD_HTML_PATH.exists() or not CLASSIC_DASHBOARD_HTML_PATH.is_file(): - raise RuntimeError(f"Dashboard HTML template not found: {CLASSIC_DASHBOARD_HTML_PATH}") - return CLASSIC_DASHBOARD_HTML_PATH.read_text(encoding="utf-8") +import logging +import os +import time +from datetime import datetime, timezone +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent + +UPLOAD_DIR = BASE_DIR / "uploads" +UPLOAD_DIR.mkdir(parents=True, exist_ok=True) + +VIDEO_UPLOAD_DIR = UPLOAD_DIR / "videos" +VIDEO_UPLOAD_DIR.mkdir(parents=True, exist_ok=True) + +OUTPUT_DIR = BASE_DIR / "outputs" +OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + +VIDEO_OUTPUT_DIR = OUTPUT_DIR / "videos" +VIDEO_OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + +TEXTURE_DIR = BASE_DIR / "texturas" +TEXTURE_DIR.mkdir(parents=True, exist_ok=True) + +LOG_DIR = BASE_DIR / "logs" +LOG_DIR.mkdir(exist_ok=True) + +TEMPLATES_DIR = BASE_DIR / "templates" +TEMPLATES_DIR.mkdir(parents=True, exist_ok=True) + +CLASSIC_DASHBOARD_HTML_PATH = TEMPLATES_DIR / "classic_dashboard.html" + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s %(name)s: %(message)s", + handlers=[ + logging.FileHandler(LOG_DIR / "app.log", encoding="utf-8"), + logging.StreamHandler(), + ], +) +logger = logging.getLogger("backend.segmentation") + +SAM2_CONFIG_PATH = os.getenv("SAM2_CONFIG_PATH", "configs/sam2.1/sam2.1_hiera_l.yaml") +SAM2_MODEL_PATH = os.getenv("SAM2_MODEL_PATH") +SAM2_DEFAULT_MODEL_NAMES = ( + "sam2.1_hiera_large_fresh.pt", + "sam2.1_hiera_large.pt", + "sam2.1_hiera_large.pth", + "sam2_hiera_large.pt", +) +SAM2_MODEL_DIR_CANDIDATES = ("models", "modelo") +SAM2_UNLOAD_AFTER_USE = str(os.getenv("SAM2_UNLOAD_AFTER_USE", "0")).strip().lower() in {"1", "true", "yes"} +FRONTEND_DEBUG = str(os.getenv("FRONTEND_DEBUG", "0")).strip().lower() in {"1", "true", "yes", "on"} + +# URL del Space de Gradio GPU (principal). Si falla, se usa el CPU fallback. +# Local: http://localhost:7860 +# Producción: https://.hf.space +GRADIO_SPACE_URL: str = os.getenv("GRADIO_SPACE_URL", "").rstrip("/") + +# URL del Space de Gradio CPU (respaldo automático si el GPU falla o agota quota). +GRADIO_CPU_FALLBACK_URL: str = os.getenv("GRADIO_CPU_FALLBACK_URL", "").rstrip("/") + +MAX_UPLOAD_WIDTH = 1024 +UPLOAD_JPEG_QUALITY = 82 +SD_JOB_STALE_SECONDS = 120 +UPLOAD_JOB_STALE_SECONDS = 900 +UPLOAD_BASE_SECONDS = 8.0 +UPLOAD_SECONDS_PER_MEGAPIXEL = 70.0 +SD_QUICK_TIMEOUT_SECONDS = 15.0 + +SEMANTIC_MODEL_ID = "nvidia/segformer-b5-finetuned-ade-640-640" +DEPTH_MODEL_ID = "Intel/dpt-hybrid-midas" + + +def utc_now_iso() -> str: + return datetime.now(timezone.utc).isoformat() + + +def log_timing_start(step_name: str) -> float: + started = time.perf_counter() + logger.info(f"[{step_name}] START at {utc_now_iso()}") + return started + + +def log_timing_end(step_name: str, started: float) -> None: + elapsed = time.perf_counter() - started + logger.info(f"[{step_name}] DONE {elapsed:.3f}s at {utc_now_iso()}") + + +def load_classic_dashboard_html() -> str: + if not CLASSIC_DASHBOARD_HTML_PATH.exists() or not CLASSIC_DASHBOARD_HTML_PATH.is_file(): + raise RuntimeError(f"Dashboard HTML template not found: {CLASSIC_DASHBOARD_HTML_PATH}") + return CLASSIC_DASHBOARD_HTML_PATH.read_text(encoding="utf-8") diff --git a/backend/data/presets.json b/backend/data/presets.json index 96fae0db8fd9aece048a4bd1278bfda42ad93666..d8aa281fc0a48ffad33e7e02ffe020b4dce3fd79 100644 --- a/backend/data/presets.json +++ b/backend/data/presets.json @@ -3,8 +3,8 @@ "ancho_panel_m": 0.3, "alto_panel_m": 0.3, "intensidad_textura": 0.85, - "separacion_vertical_px": 0.3, - "separacion_horizontal_px": 0.3, + "separacion_vertical_px": 0.4, + "separacion_horizontal_px": 0.4, "orientacion": "vertical", "perspectiva_horizontal": 0.5, "perspectiva_vertical": 0.7, @@ -22,8 +22,8 @@ "modo_fusion": "luz suave" }, "WPC_DECK": { - "ancho_panel_m": 0.25, - "alto_panel_m": 0.05, + "ancho_panel_m": 0.14, + "alto_panel_m": 0.025, "intensidad_textura": 0.85, "separacion_vertical_px": 0, "separacion_horizontal_px": 5, diff --git a/backend/logs/app.log b/backend/logs/app.log index 8c52dd856ec23ebb07468ec1c34d9c575198e0d0..d79477bbe018c5ec7b9cdc48712b5bfd82ae23ef 100644 --- a/backend/logs/app.log +++ b/backend/logs/app.log @@ -4000,3 +4000,1029 @@ cv2.error: OpenCV(4.13.0) D:\a\opencv-python\opencv-python\opencv\modules\imgpro 2026-05-11 08:52:09,282 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\16c245147932b16e84272f485aa1e64a205303e1669fe3192f4040c5211bfe65\image.webp "HTTP/1.1 200 OK" 2026-05-11 08:52:09,330 INFO backend.segmentation: [APPLY_TEXTURE] DONE 19.889s at 2026-05-11T14:52:09.330236+00:00 2026-05-11 08:53:23,170 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a01ed63ac1ef105a6100284"}, "remainingTimeMS": 30} +2026-05-11 08:59:34,053 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=https://eduardo4547-hyper-reality-sam2-gpu.hf.space +2026-05-11 08:59:34,078 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 08:59:56,934 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a01eeec350249fbce1c5550"}, "remainingTimeMS": 30} +2026-05-11 09:00:03,790 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T15:00:03.790265+00:00 +2026-05-11 09:00:03,790 INFO backend.segmentation: [JOB 3f36b79482d04fdba69fa180e8d82b55] preparing_image progress=12 +2026-05-11 09:00:03,941 INFO backend.segmentation: [JOB 3f36b79482d04fdba69fa180e8d82b55] segmenting_with_sam2 progress=30 estimated_seconds=49.28768 +2026-05-11 09:00:03,941 INFO services.gradio_client_service: Calling GPU Gradio Space: https://eduardo4547-hyper-reality-sam2-gpu.hf.space +2026-05-11 09:00:06,608 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-gpu.hf.space/config "HTTP/1.1 200 OK" +2026-05-11 09:00:07,171 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-gpu.hf.space/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:00:07,919 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-gpu.hf.space/gradio_api/heartbeat/ac77bcfc-7232-4a63-8604-25d45c813fcc "HTTP/1.1 200 OK" +2026-05-11 09:00:08,152 INFO httpx: HTTP Request: POST https://eduardo4547-hyper-reality-sam2-gpu.hf.space/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:00:08,179 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:00:08,707 INFO httpx: HTTP Request: POST https://eduardo4547-hyper-reality-sam2-gpu.hf.space/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:00:09,377 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-gpu.hf.space/gradio_api/queue/data?session_hash=ac77bcfc-7232-4a63-8604-25d45c813fcc "HTTP/1.1 200 OK" +2026-05-11 09:00:28,367 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-gpu.hf.space/gradio_api/file=/tmp/gradio/b1c58c6503edaf5fcdc01526aef97fe3d82c533e6dd5a577bdcf26f56d685366/image.webp "HTTP/1.1 200 OK" +2026-05-11 09:00:28,505 INFO services.gradio_client_service: Gradio Space segmentation: entorno=gpu motor=SAM Auto (GPU - ZeroGPU) mask_count=61 +2026-05-11 09:00:28,507 INFO backend.segmentation: [JOB 3f36b79482d04fdba69fa180e8d82b55] saving_masks progress=92 +2026-05-11 09:00:28,643 INFO backend.segmentation: [JOB 3f36b79482d04fdba69fa180e8d82b55] segments_meta saved (61 segments) +2026-05-11 09:00:28,643 INFO backend.segmentation: [JOB 3f36b79482d04fdba69fa180e8d82b55] done mask_count=61 +2026-05-11 09:00:28,643 INFO backend.segmentation: [UPLOAD_BG] DONE 24.854s at 2026-05-11T15:00:28.643926+00:00 +2026-05-11 09:00:28,643 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 09:00:30,184 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a01ef0e350249fbce1c5551"}, "remainingTimeMS": 30} +2026-05-11 09:00:38,461 INFO backend.segmentation: [APPLY_TEXTURE] START at 2026-05-11T15:00:38.461550+00:00 +2026-05-11 09:00:41,507 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-gpu.hf.space/config "HTTP/1.1 200 OK" +2026-05-11 09:00:42,158 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-gpu.hf.space/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:00:42,658 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:00:42,805 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-gpu.hf.space/gradio_api/heartbeat/f0a07ce3-8ce5-479c-8a9b-581234198f5c "HTTP/1.1 200 OK" +2026-05-11 09:00:42,967 INFO httpx: HTTP Request: POST https://eduardo4547-hyper-reality-sam2-gpu.hf.space/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:01:03,284 INFO httpx: HTTP Request: POST https://eduardo4547-hyper-reality-sam2-gpu.hf.space/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:01:03,918 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-gpu.hf.space/gradio_api/queue/data?session_hash=f0a07ce3-8ce5-479c-8a9b-581234198f5c "HTTP/1.1 200 OK" +2026-05-11 09:01:04,561 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-gpu.hf.space/gradio_api/file=/tmp/gradio/23314345710bf7dd21e8b07d1fa654a32440adfbed83aaf2feef6dab22f1a1ca/image.webp "HTTP/1.1 200 OK" +2026-05-11 09:01:04,701 WARNING services.gradio_client_service: GPU Space render failed (Gradio Space render error: import_failed), trying CPU fallback... +2026-05-11 09:01:05,471 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-cpu.hf.space/config "HTTP/1.1 200 OK" +2026-05-11 09:01:06,197 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-cpu.hf.space/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:01:06,263 WARNING backend.segmentation: Remote render via Gradio failed, falling back to local: Both Gradio Spaces render failed. + GPU (https://eduardo4547-hyper-reality-sam2-gpu.hf.space): Gradio Space render error: import_failed + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Cannot find a function with `api_name`: /render. +2026-05-11 09:01:06,697 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:01:06,790 INFO backend.segmentation: [APPLY_TEXTURE] cleared mask from original source: C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\uploads\hyper-reality-1778254222964.jpg +2026-05-11 09:01:06,889 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-cpu.hf.space/gradio_api/heartbeat/7549c987-8bbe-4e41-8da5-e08804270246 "HTTP/1.1 200 OK" +2026-05-11 09:01:07,106 INFO backend.segmentation: [APPLY_TEXTURE] DONE 28.645s at 2026-05-11T15:01:07.106365+00:00 +2026-05-11 09:02:27,357 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 09:02:27,381 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 09:02:55,966 INFO backend.segmentation: [APPLY_TEXTURE] START at 2026-05-11T15:02:55.966358+00:00 +2026-05-11 09:02:58,857 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:03:01,243 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:03:02,224 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:03:03,744 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/65b7a704-e4c5-4883-a51e-20f4487db3f8 "HTTP/1.1 200 OK" +2026-05-11 09:03:03,778 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:03:06,155 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:03:08,583 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=65b7a704-e4c5-4883-a51e-20f4487db3f8 "HTTP/1.1 200 OK" +2026-05-11 09:03:18,771 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\dee0b06b7f3205e519abee75a0fd781fbc1d05fe893f21dc1d9562e0934ee509\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:03:18,848 INFO backend.segmentation: [APPLY_TEXTURE] DONE 22.883s at 2026-05-11T15:03:18.848958+00:00 +2026-05-11 09:04:16,293 INFO backend.segmentation: [APPLY_TEXTURE] START at 2026-05-11T15:04:16.293028+00:00 +2026-05-11 09:04:18,844 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:04:21,221 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:04:21,710 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:04:23,638 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:04:23,649 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/76dab94b-b9d8-44a8-b036-03e15f1fe2bd "HTTP/1.1 200 OK" +2026-05-11 09:04:25,990 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:04:28,339 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=76dab94b-b9d8-44a8-b036-03e15f1fe2bd "HTTP/1.1 200 OK" +2026-05-11 09:04:43,718 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\29aec395f88d98858911bde6acc6466dbf45da9fc592b83acb5a57d64ec466c4\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:04:43,757 INFO backend.segmentation: [APPLY_TEXTURE] DONE 27.465s at 2026-05-11T15:04:43.757815+00:00 +2026-05-11 09:05:26,410 INFO backend.segmentation: [APPLY_TEXTURE] START at 2026-05-11T15:05:26.410908+00:00 +2026-05-11 09:05:28,958 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:05:31,343 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:05:31,834 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:05:33,794 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:05:33,805 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/5d38c3f7-4809-4b8a-b019-e5e4282ccf57 "HTTP/1.1 200 OK" +2026-05-11 09:05:36,147 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:05:38,539 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=5d38c3f7-4809-4b8a-b019-e5e4282ccf57 "HTTP/1.1 200 OK" +2026-05-11 09:05:47,129 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\1eb839c12f77649f31745595b80bfae381ebbfe625276d80619558c27fc4100c\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:05:47,169 INFO backend.segmentation: [APPLY_TEXTURE] DONE 20.759s at 2026-05-11T15:05:47.169527+00:00 +2026-05-11 09:07:00,832 INFO backend.segmentation: [APPLY_TEXTURE] START at 2026-05-11T15:07:00.832585+00:00 +2026-05-11 09:07:03,220 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:07:05,487 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:07:05,958 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:07:07,823 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/d9226b2c-d5c9-4753-831e-11b179671135 "HTTP/1.1 200 OK" +2026-05-11 09:07:07,825 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:07:10,066 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:07:12,331 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=d9226b2c-d5c9-4753-831e-11b179671135 "HTTP/1.1 200 OK" +2026-05-11 09:07:21,277 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\0c7b3464bcf12e33aeec3d97d77559a8c2e926abb18ebf01ce372c31ed7c3bd1\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:07:21,316 INFO backend.segmentation: [APPLY_TEXTURE] DONE 20.485s at 2026-05-11T15:07:21.316788+00:00 +2026-05-11 09:07:39,324 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a01f0bbb29f424d7c4dde31"}, "remainingTimeMS": 30} +2026-05-11 09:07:56,182 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T15:07:56.182552+00:00 +2026-05-11 09:07:56,182 INFO backend.segmentation: [JOB b454dca58f474e8aaed0a05b80f7f04c] preparing_image progress=12 +2026-05-11 09:07:56,226 INFO backend.segmentation: [JOB b454dca58f474e8aaed0a05b80f7f04c] segmenting_with_sam2 progress=30 estimated_seconds=94.01599999999999 +2026-05-11 09:07:56,227 INFO services.gradio_client_service: Calling GPU Gradio Space: http://localhost:7860 +2026-05-11 09:07:58,573 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:08:00,842 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:08:01,274 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:08:03,206 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/7763e966-1bc8-4a49-87f8-9d780f488054 "HTTP/1.1 200 OK" +2026-05-11 09:08:03,210 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:08:05,435 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:08:07,659 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=7763e966-1bc8-4a49-87f8-9d780f488054 "HTTP/1.1 200 OK" +2026-05-11 09:10:17,025 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\56758f540e1c547de434b369fae158810fde033fe3650199c54eecf9fd68ac46\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:10:17,041 INFO services.gradio_client_service: Gradio Space segmentation: entorno=gpu motor=SAM Auto (GPU - ZeroGPU) mask_count=58 +2026-05-11 09:10:17,043 INFO backend.segmentation: [JOB b454dca58f474e8aaed0a05b80f7f04c] saving_masks progress=92 +2026-05-11 09:10:17,288 INFO backend.segmentation: [JOB b454dca58f474e8aaed0a05b80f7f04c] segments_meta saved (58 segments) +2026-05-11 09:10:17,288 INFO backend.segmentation: [JOB b454dca58f474e8aaed0a05b80f7f04c] done mask_count=58 +2026-05-11 09:10:17,288 INFO backend.segmentation: [UPLOAD_BG] DONE 141.106s at 2026-05-11T15:10:17.288427+00:00 +2026-05-11 09:10:17,288 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 09:10:20,270 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a01f15cb29f424d7c4dde32"}, "remainingTimeMS": 30} +2026-05-11 09:10:34,161 INFO backend.segmentation: [APPLY_TEXTURE] START at 2026-05-11T15:10:34.161901+00:00 +2026-05-11 09:10:38,153 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:10:40,400 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:10:40,914 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:10:42,826 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/5de7167f-1177-4093-ab8e-02c57efd6725 "HTTP/1.1 200 OK" +2026-05-11 09:10:42,829 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:10:45,122 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:10:47,370 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=5de7167f-1177-4093-ab8e-02c57efd6725 "HTTP/1.1 200 OK" +2026-05-11 09:10:55,340 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\08d0c8382b41a8cb1afd5ca2933402871534ace900c8c25dbc04dfb0c7fac838\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:10:55,382 INFO backend.segmentation: [APPLY_TEXTURE] DONE 21.221s at 2026-05-11T15:10:55.382977+00:00 +2026-05-11 09:11:14,068 INFO backend.segmentation: [APPLY_TEXTURE] START at 2026-05-11T15:11:14.068589+00:00 +2026-05-11 09:11:18,054 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:11:20,338 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:11:20,755 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:11:22,681 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/aa5ffe1e-cf6f-4a59-8b5d-9f8f844ccf35 "HTTP/1.1 200 OK" +2026-05-11 09:11:22,685 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:11:25,069 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:11:27,403 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=aa5ffe1e-cf6f-4a59-8b5d-9f8f844ccf35 "HTTP/1.1 200 OK" +2026-05-11 09:11:37,029 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\0d515bdb32e656b506e326f168d5fb2ca05cba9bc53cb511e42dd260d62301f7\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:11:37,090 INFO backend.segmentation: [APPLY_TEXTURE] DONE 23.022s at 2026-05-11T15:11:37.090970+00:00 +2026-05-11 09:11:49,760 INFO backend.segmentation: [APPLY_TEXTURE] START at 2026-05-11T15:11:49.760016+00:00 +2026-05-11 09:11:53,744 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:11:56,074 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:11:56,569 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:11:58,419 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/859f6de7-07f6-480e-9492-6d73356bbf4c "HTTP/1.1 200 OK" +2026-05-11 09:11:58,437 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:12:00,755 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:12:03,037 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=859f6de7-07f6-480e-9492-6d73356bbf4c "HTTP/1.1 200 OK" +2026-05-11 09:12:11,490 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\b849e3480a5febb83d4fb8ebb52ed500f40df99c6d5f8a20108eed00ab315153\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:12:11,536 INFO backend.segmentation: [APPLY_TEXTURE] DONE 21.777s at 2026-05-11T15:12:11.536831+00:00 +2026-05-11 09:12:37,043 INFO backend.segmentation: [APPLY_TEXTURE] START at 2026-05-11T15:12:37.042144+00:00 +2026-05-11 09:12:41,791 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:12:44,105 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:12:44,550 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:12:46,524 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/3deaf637-5ada-4a54-b02b-6b588dec87a5 "HTTP/1.1 200 OK" +2026-05-11 09:12:46,572 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:12:48,966 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:12:51,228 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=3deaf637-5ada-4a54-b02b-6b588dec87a5 "HTTP/1.1 200 OK" +2026-05-11 09:13:00,100 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\25d12f1c2a9c336c1b887ee7e0398b601b42b7565509d9dfaf16f582b2ca907a\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:13:00,146 INFO backend.segmentation: [APPLY_TEXTURE] DONE 23.104s at 2026-05-11T15:13:00.146803+00:00 +2026-05-11 09:13:10,087 INFO backend.segmentation: [APPLY_TEXTURE] START at 2026-05-11T15:13:10.087403+00:00 +2026-05-11 09:13:14,037 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:13:16,298 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:13:16,857 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:13:18,704 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/f345958a-a52f-4849-b880-ca96676d7386 "HTTP/1.1 200 OK" +2026-05-11 09:13:18,707 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:13:21,027 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:13:23,316 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=f345958a-a52f-4849-b880-ca96676d7386 "HTTP/1.1 200 OK" +2026-05-11 09:13:33,722 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\08d0c8382b41a8cb1afd5ca2933402871534ace900c8c25dbc04dfb0c7fac838\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:13:33,775 INFO backend.segmentation: [APPLY_TEXTURE] DONE 23.688s at 2026-05-11T15:13:33.775460+00:00 +2026-05-11 09:14:26,391 INFO backend.segmentation: [APPLY_TEXTURE] START at 2026-05-11T15:14:26.391654+00:00 +2026-05-11 09:14:30,328 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:14:32,580 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:14:33,093 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:14:34,893 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/71de3a6c-9c39-4178-83cd-abe0743e32ba "HTTP/1.1 200 OK" +2026-05-11 09:14:34,911 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:14:37,273 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:14:39,540 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=71de3a6c-9c39-4178-83cd-abe0743e32ba "HTTP/1.1 200 OK" +2026-05-11 09:14:47,492 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\29b96e9889e909ab544c7e84938cfd34a01f8b5169111b550e573aa6b0b57b67\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:14:47,538 INFO backend.segmentation: [APPLY_TEXTURE] DONE 21.146s at 2026-05-11T15:14:47.538602+00:00 +2026-05-11 09:15:04,802 INFO backend.segmentation: [APPLY_TEXTURE] START at 2026-05-11T15:15:04.802101+00:00 +2026-05-11 09:15:09,381 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:15:11,700 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:15:12,871 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:15:14,058 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:15:14,069 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/94003c7a-84a3-4b8f-a66a-5460381f8d8b "HTTP/1.1 200 OK" +2026-05-11 09:15:16,401 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:15:18,674 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=94003c7a-84a3-4b8f-a66a-5460381f8d8b "HTTP/1.1 200 OK" +2026-05-11 09:15:30,091 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\ccbd80baf8b8064609e97da4a8f97ee6947813f3ee88f8e9057e58426278046d\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:15:30,167 INFO backend.segmentation: [APPLY_TEXTURE] DONE 25.366s at 2026-05-11T15:15:30.167665+00:00 +2026-05-11 09:15:54,665 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T15:15:54.665708+00:00 +2026-05-11 09:15:54,666 INFO backend.segmentation: [JOB 14bc0a9c8c784733b22bee657e4a131f] preparing_image progress=12 +2026-05-11 09:15:55,008 INFO backend.segmentation: [JOB 14bc0a9c8c784733b22bee657e4a131f] segmenting_with_sam2 progress=30 estimated_seconds=105.8432 +2026-05-11 09:15:55,008 INFO services.gradio_client_service: Calling GPU Gradio Space: http://localhost:7860 +2026-05-11 09:15:57,374 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:15:59,723 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:16:00,285 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:16:02,177 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/9897633b-f106-45fb-b9a4-78e34f8ccf0f "HTTP/1.1 200 OK" +2026-05-11 09:16:02,181 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:16:04,558 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:16:07,298 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=9897633b-f106-45fb-b9a4-78e34f8ccf0f "HTTP/1.1 200 OK" +2026-05-11 09:18:04,401 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\fe7e296f49e3953b396fc8aeb7f4885b83ba4201a8d3f8d451ff0339d7391c52\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:18:04,415 INFO services.gradio_client_service: Gradio Space segmentation: entorno=gpu motor=SAM Auto (GPU - ZeroGPU) mask_count=74 +2026-05-11 09:18:04,415 INFO backend.segmentation: [JOB 14bc0a9c8c784733b22bee657e4a131f] saving_masks progress=92 +2026-05-11 09:18:04,694 INFO backend.segmentation: [JOB 14bc0a9c8c784733b22bee657e4a131f] segments_meta saved (74 segments) +2026-05-11 09:18:04,694 INFO backend.segmentation: [JOB 14bc0a9c8c784733b22bee657e4a131f] done mask_count=74 +2026-05-11 09:18:04,694 INFO backend.segmentation: [UPLOAD_BG] DONE 130.029s at 2026-05-11T15:18:04.694806+00:00 +2026-05-11 09:18:04,694 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 09:18:14,220 INFO backend.segmentation: [APPLY_TEXTURE] START at 2026-05-11T15:18:14.220091+00:00 +2026-05-11 09:18:18,813 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:18:21,048 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:18:21,523 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:18:23,424 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/772ed342-ed42-4191-8bab-4d187a9eedab "HTTP/1.1 200 OK" +2026-05-11 09:18:23,426 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:18:25,820 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:18:28,063 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=772ed342-ed42-4191-8bab-4d187a9eedab "HTTP/1.1 200 OK" +2026-05-11 09:18:36,513 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\0bf73bbc2dfb1dfdbce2bd6f148711a867920edab77a04c28005b7b51bec563e\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:18:36,580 INFO backend.segmentation: [APPLY_TEXTURE] DONE 22.361s at 2026-05-11T15:18:36.580363+00:00 +2026-05-11 09:18:51,861 INFO backend.segmentation: [APPLY_TEXTURE] START at 2026-05-11T15:18:51.861615+00:00 +2026-05-11 09:18:56,659 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:18:58,950 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:18:59,388 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:19:01,288 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:19:01,301 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/02aba9f9-2d22-411e-ada8-d1649e5bf879 "HTTP/1.1 200 OK" +2026-05-11 09:19:03,716 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:19:05,979 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=02aba9f9-2d22-411e-ada8-d1649e5bf879 "HTTP/1.1 200 OK" +2026-05-11 09:19:15,134 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\6b114b0f1869c839104ad71d04efe8b1c42c2ada6548a24dd01c0f1195554552\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:19:15,204 INFO backend.segmentation: [APPLY_TEXTURE] DONE 23.342s at 2026-05-11T15:19:15.204045+00:00 +2026-05-11 09:19:25,506 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T15:19:25.506664+00:00 +2026-05-11 09:19:25,506 INFO backend.segmentation: [JOB 306b039e65b146b69dd8b75ba05c88b9] preparing_image progress=12 +2026-05-11 09:19:25,535 INFO backend.segmentation: [JOB 306b039e65b146b69dd8b75ba05c88b9] segmenting_with_sam2 progress=30 estimated_seconds=49.28768 +2026-05-11 09:19:25,536 INFO services.gradio_client_service: Calling GPU Gradio Space: http://localhost:7860 +2026-05-11 09:19:27,881 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:19:30,133 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:19:30,690 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:19:32,418 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/8b3cfccf-fbce-49ba-9886-7e60d3a4f9e5 "HTTP/1.1 200 OK" +2026-05-11 09:19:32,420 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:19:34,631 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:19:36,913 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=8b3cfccf-fbce-49ba-9886-7e60d3a4f9e5 "HTTP/1.1 200 OK" +2026-05-11 09:21:11,357 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\fdba6c817445c957bf57f0f37d255298685625d2847381414fb08306405b69d3\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:21:11,374 INFO services.gradio_client_service: Gradio Space segmentation: entorno=gpu motor=SAM Auto (GPU - ZeroGPU) mask_count=61 +2026-05-11 09:21:11,374 INFO backend.segmentation: [JOB 306b039e65b146b69dd8b75ba05c88b9] saving_masks progress=92 +2026-05-11 09:21:11,543 INFO backend.segmentation: [JOB 306b039e65b146b69dd8b75ba05c88b9] segments_meta saved (61 segments) +2026-05-11 09:21:11,545 INFO backend.segmentation: [JOB 306b039e65b146b69dd8b75ba05c88b9] done mask_count=61 +2026-05-11 09:21:11,545 INFO backend.segmentation: [UPLOAD_BG] DONE 106.039s at 2026-05-11T15:21:11.545131+00:00 +2026-05-11 09:21:11,545 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 09:21:23,322 INFO backend.segmentation: [APPLY_TEXTURE] START at 2026-05-11T15:21:23.322567+00:00 +2026-05-11 09:21:29,196 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:21:31,524 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:21:32,195 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:21:33,987 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:21:34,012 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/39c2dd92-8c58-4707-8071-1e597e73530f "HTTP/1.1 200 OK" +2026-05-11 09:21:36,542 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:21:38,902 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=39c2dd92-8c58-4707-8071-1e597e73530f "HTTP/1.1 200 OK" +2026-05-11 09:21:55,622 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\0e52d54170d30b6d09308ddfb46b306d2afc5dfc6e4026ae5f907d07f6ed6436\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:21:55,666 INFO backend.segmentation: [APPLY_TEXTURE] DONE 32.343s at 2026-05-11T15:21:55.666246+00:00 +2026-05-11 09:22:14,587 INFO backend.segmentation: [APPLY_TEXTURE] START at 2026-05-11T15:22:14.587711+00:00 +2026-05-11 09:22:19,127 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:22:21,368 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:22:21,771 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:22:23,686 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/40e6db7c-cb8a-4621-8be5-28c640b19c75 "HTTP/1.1 200 OK" +2026-05-11 09:22:23,705 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:22:26,139 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:22:28,393 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=40e6db7c-cb8a-4621-8be5-28c640b19c75 "HTTP/1.1 200 OK" +2026-05-11 09:22:37,108 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\61825b19b734baf326a7a6a6d9a5f9ff601d6f3413738e4bb22c0a882ef83af7\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:22:37,145 INFO backend.segmentation: [APPLY_TEXTURE] DONE 22.557s at 2026-05-11T15:22:37.145209+00:00 +2026-05-11 09:22:45,279 INFO backend.segmentation: [APPLY_TEXTURE] START at 2026-05-11T15:22:45.279333+00:00 +2026-05-11 09:22:49,178 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:22:51,446 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:22:51,982 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:22:53,829 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/d7b9735a-6e78-4fe3-8a42-109575b316a7 "HTTP/1.1 200 OK" +2026-05-11 09:22:53,830 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:22:56,146 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:22:58,396 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=d7b9735a-6e78-4fe3-8a42-109575b316a7 "HTTP/1.1 200 OK" +2026-05-11 09:23:06,755 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\9d8eab6a52e56697cb95b20e61dcc73246f45f3e0c14549e980ea1d8ab5411db\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:23:06,791 INFO backend.segmentation: [APPLY_TEXTURE] DONE 21.513s at 2026-05-11T15:23:06.791913+00:00 +2026-05-11 09:23:26,777 INFO backend.segmentation: [APPLY_TEXTURE] START at 2026-05-11T15:23:26.777258+00:00 +2026-05-11 09:23:29,185 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 09:23:31,437 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 09:23:31,854 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 09:23:33,733 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/28c290d8-f558-42f2-9807-2eb9a8ee03aa "HTTP/1.1 200 OK" +2026-05-11 09:23:33,753 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 09:23:36,010 INFO httpx: HTTP Request: POST http://localhost:7860/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 09:23:38,255 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/queue/data?session_hash=28c290d8-f558-42f2-9807-2eb9a8ee03aa "HTTP/1.1 200 OK" +2026-05-11 09:23:46,257 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/file=C:\Users\alane\AppData\Local\Temp\gradio\dee0b06b7f3205e519abee75a0fd781fbc1d05fe893f21dc1d9562e0934ee509\image.webp "HTTP/1.1 200 OK" +2026-05-11 09:23:46,296 INFO backend.segmentation: [APPLY_TEXTURE] DONE 19.519s at 2026-05-11T15:23:46.296656+00:00 +2026-05-11 15:49:44,491 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 15:49:44,515 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:05:50,322 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:05:50,344 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:05:51,265 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:05:51,287 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:06:23,124 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a0252df14d88316415be4cb"}, "remainingTimeMS": 30} +2026-05-11 16:06:28,118 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T22:06:28.118464+00:00 +2026-05-11 16:06:28,119 INFO backend.segmentation: [JOB 4209423fe883434baa0fc2b4775b34d6] preparing_image progress=12 +2026-05-11 16:06:28,171 INFO backend.segmentation: [JOB 4209423fe883434baa0fc2b4775b34d6] segmenting_with_sam2 progress=30 estimated_seconds=49.28768 +2026-05-11 16:06:28,171 INFO services.gradio_client_service: Calling GPU Gradio Space: http://localhost:7860 +2026-05-11 16:06:30,746 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 16:06:32,996 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 16:06:33,002 WARNING services.gradio_client_service: GPU Space failed (Cannot find a function with `api_name`: /segment.), trying CPU fallback... +2026-05-11 16:06:33,003 INFO services.gradio_client_service: Calling CPU fallback Space: https://eduardo4547-hyper-reality-sam2-cpu.hf.space +2026-05-11 16:06:33,744 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-cpu.hf.space/config "HTTP/1.1 200 OK" +2026-05-11 16:06:33,787 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 16:06:34,375 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-cpu.hf.space/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 16:06:34,641 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 16:06:35,061 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-cpu.hf.space/gradio_api/heartbeat/4139b1e8-0885-4eda-aa82-fd71c176ecae "HTTP/1.1 200 OK" +2026-05-11 16:06:35,253 INFO httpx: HTTP Request: POST https://eduardo4547-hyper-reality-sam2-cpu.hf.space/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 16:06:35,340 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/cedee08b-ac8c-488b-8942-f0fa0569e68f "HTTP/1.1 200 OK" +2026-05-11 16:06:35,781 INFO httpx: HTTP Request: POST https://eduardo4547-hyper-reality-sam2-cpu.hf.space/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 16:06:36,314 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-cpu.hf.space/gradio_api/queue/data?session_hash=4139b1e8-0885-4eda-aa82-fd71c176ecae "HTTP/1.1 200 OK" +2026-05-11 16:09:43,045 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-cpu.hf.space/gradio_api/file=/tmp/gradio/6c61c4453525a0cc97a55213f8ab16e24157b02ad4d2cba459d14b95c79ac6ab/image.webp "HTTP/1.1 200 OK" +2026-05-11 16:09:43,192 INFO services.gradio_client_service: Gradio Space segmentation: entorno=cpu motor=SAM Auto (CPU) mask_count=61 +2026-05-11 16:09:43,192 INFO backend.segmentation: [JOB 4209423fe883434baa0fc2b4775b34d6] saving_masks progress=92 +2026-05-11 16:09:43,346 INFO backend.segmentation: [JOB 4209423fe883434baa0fc2b4775b34d6] segments_meta saved (61 segments) +2026-05-11 16:09:43,346 INFO backend.segmentation: [JOB 4209423fe883434baa0fc2b4775b34d6] done mask_count=61 +2026-05-11 16:09:43,347 INFO backend.segmentation: [UPLOAD_BG] DONE 195.229s at 2026-05-11T22:09:43.347393+00:00 +2026-05-11 16:09:43,347 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 16:10:57,618 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:10:57,643 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:11:03,882 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T22:11:03.882445+00:00 +2026-05-11 16:11:03,882 INFO backend.segmentation: [JOB 180176892d89444989453d181df30ddb] preparing_image progress=12 +2026-05-11 16:11:03,911 INFO backend.segmentation: [JOB 180176892d89444989453d181df30ddb] segmenting_with_sam2 progress=30 estimated_seconds=49.28768 +2026-05-11 16:11:03,912 INFO services.gradio_client_service: Calling GPU Gradio Space: http://localhost:7860 +2026-05-11 16:11:06,418 INFO httpx: HTTP Request: GET http://localhost:7860/config "HTTP/1.1 200 OK" +2026-05-11 16:11:08,689 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 16:11:08,691 WARNING services.gradio_client_service: GPU Space failed (Cannot find a function with `api_name`: /segment.), trying CPU fallback... +2026-05-11 16:11:08,691 INFO services.gradio_client_service: Calling CPU fallback Space: https://eduardo4547-hyper-reality-sam2-cpu.hf.space +2026-05-11 16:11:09,507 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 16:11:09,518 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-cpu.hf.space/config "HTTP/1.1 200 OK" +2026-05-11 16:11:10,059 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-cpu.hf.space/gradio_api/info?serialize=False "HTTP/1.1 200 OK" +2026-05-11 16:11:10,323 INFO httpx: HTTP Request: HEAD https://huggingface.co/api/telemetry/py_client/initiated "HTTP/1.1 200 OK" +2026-05-11 16:11:10,728 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-cpu.hf.space/gradio_api/heartbeat/e5da0f2d-1f57-48c5-bb1e-66e9f7fec1b0 "HTTP/1.1 200 OK" +2026-05-11 16:11:10,911 INFO httpx: HTTP Request: POST https://eduardo4547-hyper-reality-sam2-cpu.hf.space/gradio_api/upload "HTTP/1.1 200 OK" +2026-05-11 16:11:11,012 INFO httpx: HTTP Request: GET http://localhost:7860/gradio_api/heartbeat/e58b3da1-7be9-42cf-81f6-8c546e39f4be "HTTP/1.1 200 OK" +2026-05-11 16:11:11,463 INFO httpx: HTTP Request: POST https://eduardo4547-hyper-reality-sam2-cpu.hf.space/gradio_api/queue/join "HTTP/1.1 200 OK" +2026-05-11 16:11:12,023 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-cpu.hf.space/gradio_api/queue/data?session_hash=e5da0f2d-1f57-48c5-bb1e-66e9f7fec1b0 "HTTP/1.1 200 OK" +2026-05-11 16:11:16,110 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a025404a5cb3b148eec3621"}, "remainingTimeMS": 30} +2026-05-11 16:11:16,115 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a025404a5cb3b148eec3621"}, "remainingTimeMS": 30} +2026-05-11 16:14:13,810 INFO httpx: HTTP Request: GET https://eduardo4547-hyper-reality-sam2-cpu.hf.space/gradio_api/file=/tmp/gradio/6c61c4453525a0cc97a55213f8ab16e24157b02ad4d2cba459d14b95c79ac6ab/image.webp "HTTP/1.1 200 OK" +2026-05-11 16:14:14,003 INFO services.gradio_client_service: Gradio Space segmentation: entorno=cpu motor=SAM Auto (CPU) mask_count=61 +2026-05-11 16:14:14,004 INFO backend.segmentation: [JOB 180176892d89444989453d181df30ddb] saving_masks progress=92 +2026-05-11 16:14:14,147 INFO backend.segmentation: [JOB 180176892d89444989453d181df30ddb] segments_meta saved (61 segments) +2026-05-11 16:14:14,147 INFO backend.segmentation: [JOB 180176892d89444989453d181df30ddb] done mask_count=61 +2026-05-11 16:14:14,148 INFO backend.segmentation: [UPLOAD_BG] DONE 190.266s at 2026-05-11T22:14:14.148092+00:00 +2026-05-11 16:14:14,148 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 16:14:15,680 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:14:15,699 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:14:16,643 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:14:16,662 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:14:16,931 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a0254b8ea5401b51d0278a3"}, "remainingTimeMS": 30} +2026-05-11 16:14:16,932 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a0254b8ea5401b51d0278a3"}, "remainingTimeMS": 30} +2026-05-11 16:14:49,311 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:14:49,337 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:15:23,879 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a0254fbaa6db1d011b1e697"}, "remainingTimeMS": 30} +2026-05-11 16:15:24,106 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a0254fcaa6db1d011b1e698"}, "remainingTimeMS": 30} +2026-05-11 16:15:24,108 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a0254fcaa6db1d011b1e698"}, "remainingTimeMS": 30} +2026-05-11 16:27:59,753 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=(not set — using local SAM2) +2026-05-11 16:27:59,757 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=(not set — using local SAM2) +2026-05-11 16:27:59,758 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=(not set — using local SAM2) +2026-05-11 16:27:59,806 INFO backend.segmentation: [SAM2] Checking model paths: +2026-05-11 16:27:59,806 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2.1_hiera_large_fresh.pt | exists=False +2026-05-11 16:27:59,807 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2.1_hiera_large_fresh.pt | exists=False +2026-05-11 16:27:59,807 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2.1_hiera_large_fresh.pt | exists=False +2026-05-11 16:27:59,807 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2.1_hiera_large_fresh.pt | exists=False +2026-05-11 16:27:59,808 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2.1_hiera_large_fresh.pt | exists=False +2026-05-11 16:27:59,808 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2.1_hiera_large_fresh.pt | exists=False +2026-05-11 16:27:59,809 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2.1_hiera_large.pt | exists=False +2026-05-11 16:27:59,809 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2.1_hiera_large.pt | exists=False +2026-05-11 16:27:59,809 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2.1_hiera_large.pt | exists=False +2026-05-11 16:27:59,809 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2.1_hiera_large.pt | exists=False +2026-05-11 16:27:59,810 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2.1_hiera_large.pt | exists=False +2026-05-11 16:27:59,810 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2.1_hiera_large.pt | exists=False +2026-05-11 16:27:59,810 INFO backend.segmentation: [SAM2] Checking model paths: +2026-05-11 16:27:59,810 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2.1_hiera_large.pth | exists=False +2026-05-11 16:27:59,811 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2.1_hiera_large_fresh.pt | exists=False +2026-05-11 16:27:59,811 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2.1_hiera_large.pth | exists=False +2026-05-11 16:27:59,811 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2.1_hiera_large_fresh.pt | exists=False +2026-05-11 16:27:59,811 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2.1_hiera_large.pth | exists=False +2026-05-11 16:27:59,811 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2.1_hiera_large_fresh.pt | exists=False +2026-05-11 16:27:59,811 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2.1_hiera_large.pth | exists=False +2026-05-11 16:27:59,811 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2.1_hiera_large_fresh.pt | exists=False +2026-05-11 16:27:59,811 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2.1_hiera_large.pth | exists=False +2026-05-11 16:27:59,812 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2.1_hiera_large.pth | exists=False +2026-05-11 16:27:59,812 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2.1_hiera_large_fresh.pt | exists=False +2026-05-11 16:27:59,812 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2_hiera_large.pt | exists=False +2026-05-11 16:27:59,812 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2.1_hiera_large_fresh.pt | exists=False +2026-05-11 16:27:59,812 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2_hiera_large.pt | exists=False +2026-05-11 16:27:59,812 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2.1_hiera_large.pt | exists=False +2026-05-11 16:27:59,812 INFO backend.segmentation: [SAM2] Checking model paths: +ve\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2_hiera_large.pt | exists=False +2026-05-11 16:27:59,813 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2.1_hiera_large.pt | exists=False +2026-05-11 16:27:59,813 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2_hiera_large.pt | exists=False +2026-05-11 16:27:59,813 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2.1_hiera_large.pt | exists=False +2026-05-11 16:27:59,813 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2.1_hiera_large_fresh.pt | exists=False +2026-05-11 16:27:59,813 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2_hiera_large.pt | exists=False +2026-05-11 16:27:59,813 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2.1_hiera_large.pt | exists=False +2026-05-11 16:27:59,813 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2_hiera_large.pt | exists=False + +2026-05-11 16:27:59,813 ERROR backend.segmentation: [SAM2] ERROR: SAM 2 model file not found. Tried the following paths: +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2.1_hiera_large_fresh.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2.1_hiera_large_fresh.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2.1_hiera_large_fresh.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2.1_hiera_large_fresh.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2.1_hiera_large_fresh.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2.1_hiera_large_fresh.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2.1_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2.1_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2.1_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2.1_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2.1_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2.1_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2.1_hiera_large.pth +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2.1_hiera_large.pth +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2.1_hiera_large.pth +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2.1_hiera_large.pth +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2.1_hiera_large.pth +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2.1_hiera_large.pth +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2_hiera_large.pt +Set SAM2_MODEL_PATH to the checkpoint location. +2026-05-11 16:27:59,813 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2.1_hiera_large.pt | exists=False +2026-05-11 16:27:59,814 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2.1_hiera_large_fresh.pt | exists=False +2026-05-11 16:27:59,814 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2.1_hiera_large.pt | exists=False +2026-05-11 16:27:59,814 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2.1_hiera_large.pth | exists=False +2026-05-11 16:27:59,814 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2.1_hiera_large_fresh.pt | exists=False +2026-05-11 16:27:59,814 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2.1_hiera_large.pth | exists=False +2026-05-11 16:27:59,814 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2.1_hiera_large_fresh.pt | exists=False +2026-05-11 16:27:59,814 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2.1_hiera_large.pth | exists=False +2026-05-11 16:27:59,814 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2.1_hiera_large.pt | exists=False +2026-05-11 16:27:59,815 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2.1_hiera_large.pth | exists=False +2026-05-11 16:27:59,815 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2.1_hiera_large.pt | exists=False +2026-05-11 16:27:59,815 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2.1_hiera_large.pth | exists=False +2026-05-11 16:27:59,815 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2.1_hiera_large.pt | exists=False +2026-05-11 16:27:59,815 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2.1_hiera_large.pth | exists=False +2026-05-11 16:27:59,815 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2.1_hiera_large.pt | exists=False +2026-05-11 16:27:59,815 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2_hiera_large.pt | exists=False +2026-05-11 16:27:59,815 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2.1_hiera_large.pt | exists=False +2026-05-11 16:27:59,816 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2_hiera_large.pt | exists=False +2026-05-11 16:27:59,816 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2.1_hiera_large.pt | exists=False +2026-05-11 16:27:59,816 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2_hiera_large.pt | exists=False +2026-05-11 16:27:59,816 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2_hiera_large.pt | exists=False +2026-05-11 16:27:59,816 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2.1_hiera_large.pth | exists=False +2026-05-11 16:27:59,816 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2_hiera_large.pt | exists=False +2026-05-11 16:27:59,816 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2.1_hiera_large.pth | exists=False +2026-05-11 16:27:59,816 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2_hiera_large.pt | exists=False +2026-05-11 16:27:59,816 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2.1_hiera_large.pth | exists=False +2026-05-11 16:27:59,817 ERROR backend.segmentation: [SAM2] ERROR: SAM 2 model file not found. Tried the following paths: +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2.1_hiera_large_fresh.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2.1_hiera_large_fresh.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2.1_hiera_large_fresh.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2.1_hiera_large_fresh.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2.1_hiera_large_fresh.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2.1_hiera_large_fresh.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2.1_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2.1_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2.1_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2.1_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2.1_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2.1_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2.1_hiera_large.pth +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2.1_hiera_large.pth +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2.1_hiera_large.pth +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2.1_hiera_large.pth +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2.1_hiera_large.pth +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2.1_hiera_large.pth +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2_hiera_large.pt +Set SAM2_MODEL_PATH to the checkpoint location. +2026-05-11 16:27:59,817 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2.1_hiera_large.pth | exists=False +2026-05-11 16:27:59,817 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2.1_hiera_large.pth | exists=False +2026-05-11 16:27:59,817 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2.1_hiera_large.pth | exists=False +2026-05-11 16:27:59,817 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2_hiera_large.pt | exists=False +2026-05-11 16:27:59,818 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2_hiera_large.pt | exists=False +2026-05-11 16:27:59,818 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2_hiera_large.pt | exists=False +2026-05-11 16:27:59,818 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2_hiera_large.pt | exists=False +2026-05-11 16:27:59,818 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2_hiera_large.pt | exists=False +2026-05-11 16:27:59,818 INFO backend.segmentation: [SAM2] - C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2_hiera_large.pt | exists=False +2026-05-11 16:27:59,818 ERROR backend.segmentation: [SAM2] ERROR: SAM 2 model file not found. Tried the following paths: +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2.1_hiera_large_fresh.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2.1_hiera_large_fresh.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2.1_hiera_large_fresh.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2.1_hiera_large_fresh.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2.1_hiera_large_fresh.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2.1_hiera_large_fresh.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2.1_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2.1_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2.1_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2.1_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2.1_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2.1_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2.1_hiera_large.pth +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2.1_hiera_large.pth +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2.1_hiera_large.pth +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2.1_hiera_large.pth +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2.1_hiera_large.pth +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2.1_hiera_large.pth +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\sam2_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\sam2_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\models\sam2_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\models\sam2_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\modelo\sam2_hiera_large.pt +- C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\modelo\sam2_hiera_large.pt +Set SAM2_MODEL_PATH to the checkpoint location. +2026-05-11 16:27:59,917 INFO backend.segmentation: Lifespan exiting; releasing resources. +2026-05-11 16:27:59,917 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 16:27:59,932 INFO backend.segmentation: Lifespan exiting; releasing resources. +2026-05-11 16:27:59,933 INFO backend.segmentation: Lifespan exiting; releasing resources. +2026-05-11 16:27:59,933 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 16:27:59,933 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 16:28:07,147 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:28:07,147 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:28:07,148 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:28:07,164 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:28:07,164 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:28:07,165 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:31:47,348 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:31:47,360 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:31:48,139 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:31:48,153 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:32:34,878 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:32:34,898 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:32:35,134 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:32:35,142 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:32:35,155 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:32:35,160 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:33:03,614 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:33:03,631 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:33:03,838 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:33:03,857 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:33:03,874 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:33:03,890 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:33:27,698 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:33:27,711 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:33:28,467 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:33:28,479 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:33:35,174 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:33:35,185 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:35:52,942 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:35:52,944 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:35:52,951 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:35:52,968 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:35:52,970 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:35:52,974 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:36:02,795 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:36:02,806 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:36:02,822 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:36:02,837 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:36:02,851 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:36:02,902 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:36:18,763 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:36:18,791 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:38:44,845 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:38:44,846 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:38:44,864 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:38:44,865 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:38:44,946 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:38:44,964 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:44:31,497 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:44:31,522 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:45:00,726 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T22:45:00.726193+00:00 +2026-05-11 16:45:00,726 INFO backend.segmentation: [JOB 9df754b89e794ddf81b412c1c417a64b] preparing_image progress=12 +2026-05-11 16:45:00,795 INFO backend.segmentation: [JOB 9df754b89e794ddf81b412c1c417a64b] segmenting_with_sam2 progress=30 estimated_seconds=49.28768 +2026-05-11 16:45:00,795 INFO services.gradio_client_service: Calling GPU Gradio Space: http://localhost:7860 +2026-05-11 16:45:01,032 WARNING services.gradio_client_service: GPU Space failed (Client.__init__() got an unexpected keyword argument 'httpx_kwargs'), trying CPU fallback... +2026-05-11 16:45:01,032 INFO services.gradio_client_service: Calling CPU fallback Space: https://eduardo4547-hyper-reality-sam2-cpu.hf.space +2026-05-11 16:45:01,033 ERROR backend.segmentation: [JOB 9df754b89e794ddf81b412c1c417a64b] failed: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 91, in segment_via_gradio_sync + return _call_gradio_sync(image_path, GRADIO_CPU_FALLBACK_URL) + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 33, in _call_gradio_sync + client = Client(space_url, httpx_kwargs={"timeout": 300.0}) +TypeError: Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\image_service.py", line 138, in run_upload_job + label_map, mask_count = segment_via_gradio_sync(image_path) + ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^ + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 93, in segment_via_gradio_sync + raise RuntimeError( + ...<3 lines>... + ) from exc_cpu +RuntimeError: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +2026-05-11 16:45:01,034 INFO backend.segmentation: [UPLOAD_BG] DONE 0.308s at 2026-05-11T22:45:01.034236+00:00 +2026-05-11 16:45:01,034 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 16:47:49,523 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T22:47:49.523811+00:00 +2026-05-11 16:47:49,524 INFO backend.segmentation: [JOB 0742c2f8d0c2446d8c7d8de8d4412464] preparing_image progress=12 +2026-05-11 16:47:49,543 INFO backend.segmentation: [JOB 0742c2f8d0c2446d8c7d8de8d4412464] segmenting_with_sam2 progress=30 estimated_seconds=49.28768 +2026-05-11 16:47:49,543 INFO services.gradio_client_service: Calling GPU Gradio Space: http://localhost:7860 +2026-05-11 16:47:49,543 WARNING services.gradio_client_service: GPU Space failed (Client.__init__() got an unexpected keyword argument 'httpx_kwargs'), trying CPU fallback... +2026-05-11 16:47:49,543 INFO services.gradio_client_service: Calling CPU fallback Space: https://eduardo4547-hyper-reality-sam2-cpu.hf.space +2026-05-11 16:47:49,544 ERROR backend.segmentation: [JOB 0742c2f8d0c2446d8c7d8de8d4412464] failed: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 91, in segment_via_gradio_sync + return _call_gradio_sync(image_path, GRADIO_CPU_FALLBACK_URL) + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 33, in _call_gradio_sync + client = Client(space_url, httpx_kwargs={"timeout": 300.0}) +TypeError: Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\image_service.py", line 138, in run_upload_job + label_map, mask_count = segment_via_gradio_sync(image_path) + ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^ + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 93, in segment_via_gradio_sync + raise RuntimeError( + ...<3 lines>... + ) from exc_cpu +RuntimeError: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +2026-05-11 16:47:49,545 INFO backend.segmentation: [UPLOAD_BG] DONE 0.022s at 2026-05-11T22:47:49.545319+00:00 +2026-05-11 16:47:49,545 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 16:47:55,620 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T22:47:55.620933+00:00 +2026-05-11 16:47:55,621 INFO backend.segmentation: [JOB 6441970dde1b47ca8bb3e489f5839c19] preparing_image progress=12 +2026-05-11 16:47:55,640 INFO backend.segmentation: [JOB 6441970dde1b47ca8bb3e489f5839c19] segmenting_with_sam2 progress=30 estimated_seconds=49.28768 +2026-05-11 16:47:55,640 INFO services.gradio_client_service: Calling GPU Gradio Space: http://localhost:7860 +2026-05-11 16:47:55,641 WARNING services.gradio_client_service: GPU Space failed (Client.__init__() got an unexpected keyword argument 'httpx_kwargs'), trying CPU fallback... +2026-05-11 16:47:55,641 INFO services.gradio_client_service: Calling CPU fallback Space: https://eduardo4547-hyper-reality-sam2-cpu.hf.space +2026-05-11 16:47:55,641 ERROR backend.segmentation: [JOB 6441970dde1b47ca8bb3e489f5839c19] failed: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 91, in segment_via_gradio_sync + return _call_gradio_sync(image_path, GRADIO_CPU_FALLBACK_URL) + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 33, in _call_gradio_sync + client = Client(space_url, httpx_kwargs={"timeout": 300.0}) +TypeError: Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\image_service.py", line 138, in run_upload_job + label_map, mask_count = segment_via_gradio_sync(image_path) + ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^ + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 93, in segment_via_gradio_sync + raise RuntimeError( + ...<3 lines>... + ) from exc_cpu +RuntimeError: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +2026-05-11 16:47:55,643 INFO backend.segmentation: [UPLOAD_BG] DONE 0.022s at 2026-05-11T22:47:55.643392+00:00 +2026-05-11 16:47:55,643 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 16:48:04,986 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:48:05,017 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:48:20,560 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T22:48:20.560278+00:00 +2026-05-11 16:48:20,560 INFO backend.segmentation: [JOB 926e56e3fb754b469dea9cbaf622e6d8] preparing_image progress=12 +2026-05-11 16:48:20,581 INFO backend.segmentation: [JOB 926e56e3fb754b469dea9cbaf622e6d8] segmenting_with_sam2 progress=30 estimated_seconds=49.28768 +2026-05-11 16:48:20,581 INFO services.gradio_client_service: Calling GPU Gradio Space: http://localhost:7860 +2026-05-11 16:48:20,581 WARNING services.gradio_client_service: GPU Space failed (Client.__init__() got an unexpected keyword argument 'httpx_kwargs'), trying CPU fallback... +2026-05-11 16:48:20,581 INFO services.gradio_client_service: Calling CPU fallback Space: https://eduardo4547-hyper-reality-sam2-cpu.hf.space +2026-05-11 16:48:20,582 ERROR backend.segmentation: [JOB 926e56e3fb754b469dea9cbaf622e6d8] failed: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 91, in segment_via_gradio_sync + return _call_gradio_sync(image_path, GRADIO_CPU_FALLBACK_URL) + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 33, in _call_gradio_sync + client = Client(space_url, httpx_kwargs={"timeout": 300.0}) +TypeError: Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\image_service.py", line 138, in run_upload_job + label_map, mask_count = segment_via_gradio_sync(image_path) + ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^ + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 93, in segment_via_gradio_sync + raise RuntimeError( + ...<3 lines>... + ) from exc_cpu +RuntimeError: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +2026-05-11 16:48:20,583 INFO backend.segmentation: [UPLOAD_BG] DONE 0.023s at 2026-05-11T22:48:20.583708+00:00 +2026-05-11 16:48:20,583 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 16:58:04,997 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:58:05,016 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 16:58:05,950 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 16:58:05,971 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 17:00:40,362 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T23:00:40.362157+00:00 +2026-05-11 17:00:40,362 INFO backend.segmentation: [JOB e08dc302227b460db3db112597ff63e2] preparing_image progress=12 +2026-05-11 17:00:40,388 INFO backend.segmentation: [JOB e08dc302227b460db3db112597ff63e2] segmenting_with_sam2 progress=30 estimated_seconds=49.28768 +2026-05-11 17:00:40,389 INFO services.gradio_client_service: Calling GPU Gradio Space: http://localhost:7860 +2026-05-11 17:00:40,389 WARNING services.gradio_client_service: GPU Space failed (Client.__init__() got an unexpected keyword argument 'httpx_kwargs'), trying CPU fallback... +2026-05-11 17:00:40,389 INFO services.gradio_client_service: Calling CPU fallback Space: https://eduardo4547-hyper-reality-sam2-cpu.hf.space +2026-05-11 17:00:40,389 ERROR backend.segmentation: [JOB e08dc302227b460db3db112597ff63e2] failed: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 91, in segment_via_gradio_sync + return _call_gradio_sync(image_path, GRADIO_CPU_FALLBACK_URL) + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 33, in _call_gradio_sync + client = Client(space_url, httpx_kwargs={"timeout": 300.0}) +TypeError: Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\image_service.py", line 138, in run_upload_job + # We are simplifying the flow: skipping SAM 2 segmentation. + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 93, in segment_via_gradio_sync + raise RuntimeError( + ...<3 lines>... + ) from exc_cpu +RuntimeError: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +2026-05-11 17:00:40,393 INFO backend.segmentation: [UPLOAD_BG] DONE 0.031s at 2026-05-11T23:00:40.393043+00:00 +2026-05-11 17:00:40,393 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:01:51,772 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 17:01:51,792 INFO backend.segmentation: [LIFESPAN] GRADIO_SPACE_URL set — skipping local SAM2 load. +2026-05-11 17:01:52,577 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T23:01:52.577203+00:00 +2026-05-11 17:01:52,577 INFO backend.segmentation: [JOB b248488a436441c1af4769102b3e76a0] preparing_image progress=12 +2026-05-11 17:01:52,605 INFO backend.segmentation: [JOB b248488a436441c1af4769102b3e76a0] segmenting_with_sam2 progress=30 estimated_seconds=49.28768 +2026-05-11 17:01:52,605 INFO services.gradio_client_service: Calling GPU Gradio Space: http://localhost:7860 +2026-05-11 17:01:52,606 WARNING services.gradio_client_service: GPU Space failed (Client.__init__() got an unexpected keyword argument 'httpx_kwargs'), trying CPU fallback... +2026-05-11 17:01:52,606 INFO services.gradio_client_service: Calling CPU fallback Space: https://eduardo4547-hyper-reality-sam2-cpu.hf.space +2026-05-11 17:01:52,606 ERROR backend.segmentation: [JOB b248488a436441c1af4769102b3e76a0] failed: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 91, in segment_via_gradio_sync + return _call_gradio_sync(image_path, GRADIO_CPU_FALLBACK_URL) + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 33, in _call_gradio_sync + client = Client(space_url, httpx_kwargs={"timeout": 300.0}) +TypeError: Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\image_service.py", line 138, in run_upload_job + # We are simplifying the flow: skipping SAM 2 segmentation. + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 93, in segment_via_gradio_sync + raise RuntimeError( + ...<3 lines>... + ) from exc_cpu +RuntimeError: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +2026-05-11 17:01:52,608 INFO backend.segmentation: [UPLOAD_BG] DONE 0.032s at 2026-05-11T23:01:52.608875+00:00 +2026-05-11 17:01:52,609 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:05:12,352 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 17:05:12,378 INFO backend.segmentation: [SAM2] SAM2 disabled (using Simplified OpenAI flow) +2026-05-11 17:05:12,379 WARNING backend.segmentation: Lifespan startup cancelled. Releasing resources. +2026-05-11 17:05:12,380 INFO backend.segmentation: Releasing resources (full_unload=True) +2026-05-11 17:05:12,418 INFO backend.segmentation: Lifespan exiting; releasing resources. +2026-05-11 17:05:12,419 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:05:15,614 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 17:05:15,643 INFO backend.segmentation: [SAM2] SAM2 disabled (using Simplified OpenAI flow) +2026-05-11 17:05:22,954 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T23:05:22.954684+00:00 +2026-05-11 17:05:22,955 INFO backend.segmentation: [JOB 8c340c544ab84952bec7959289cecf9c] preparing_image progress=12 +2026-05-11 17:05:22,976 INFO backend.segmentation: [JOB 8c340c544ab84952bec7959289cecf9c] segmenting_with_sam2 progress=30 estimated_seconds=49.28768 +2026-05-11 17:05:22,976 INFO services.gradio_client_service: Calling GPU Gradio Space: http://localhost:7860 +2026-05-11 17:05:22,976 WARNING services.gradio_client_service: GPU Space failed (Client.__init__() got an unexpected keyword argument 'httpx_kwargs'), trying CPU fallback... +2026-05-11 17:05:22,977 INFO services.gradio_client_service: Calling CPU fallback Space: https://eduardo4547-hyper-reality-sam2-cpu.hf.space +2026-05-11 17:05:22,977 ERROR backend.segmentation: [JOB 8c340c544ab84952bec7959289cecf9c] failed: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 91, in segment_via_gradio_sync + raise RuntimeError(f"GPU Gradio Space failed and no CPU fallback configured. Error: {gpu_error}") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 33, in _call_gradio_sync +TypeError: Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\image_service.py", line 138, in run_upload_job + # We are simplifying the flow: skipping SAM 2 segmentation. + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 93, in segment_via_gradio_sync + try: + + ...<3 lines>... + raise RuntimeError( + ^^^^^^^^^^^^^^ +RuntimeError: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +2026-05-11 17:05:22,979 INFO backend.segmentation: [UPLOAD_BG] DONE 0.025s at 2026-05-11T23:05:22.979381+00:00 +2026-05-11 17:05:22,979 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:07:10,779 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T23:07:10.779085+00:00 +2026-05-11 17:07:10,779 INFO backend.segmentation: [JOB 293b338a28b84ed4b5950719a8beecff] preparing_image progress=12 +2026-05-11 17:07:10,800 INFO backend.segmentation: [JOB 293b338a28b84ed4b5950719a8beecff] segmenting_with_sam2 progress=30 estimated_seconds=49.28768 +2026-05-11 17:07:10,800 INFO services.gradio_client_service: Calling GPU Gradio Space: http://localhost:7860 +2026-05-11 17:07:10,800 WARNING services.gradio_client_service: GPU Space failed (Client.__init__() got an unexpected keyword argument 'httpx_kwargs'), trying CPU fallback... +2026-05-11 17:07:10,800 INFO services.gradio_client_service: Calling CPU fallback Space: https://eduardo4547-hyper-reality-sam2-cpu.hf.space +2026-05-11 17:07:10,800 ERROR backend.segmentation: [JOB 293b338a28b84ed4b5950719a8beecff] failed: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 91, in segment_via_gradio_sync + raise RuntimeError(f"GPU Gradio Space failed and no CPU fallback configured. Error: {gpu_error}") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 33, in _call_gradio_sync +TypeError: Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\image_service.py", line 138, in run_upload_job + # We are simplifying the flow: skipping SAM 2 segmentation. + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 93, in segment_via_gradio_sync + try: + + ...<3 lines>... + raise RuntimeError( + ^^^^^^^^^^^^^^ +RuntimeError: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +2026-05-11 17:07:10,802 INFO backend.segmentation: [UPLOAD_BG] DONE 0.023s at 2026-05-11T23:07:10.802362+00:00 +2026-05-11 17:07:10,802 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:07:52,210 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T23:07:52.210727+00:00 +2026-05-11 17:07:52,211 INFO backend.segmentation: [JOB a5fedf0501a94001b42fc9a71c7b75ca] preparing_image progress=12 +2026-05-11 17:07:52,230 INFO backend.segmentation: [JOB a5fedf0501a94001b42fc9a71c7b75ca] segmenting_with_sam2 progress=30 estimated_seconds=49.28768 +2026-05-11 17:07:52,231 INFO services.gradio_client_service: Calling GPU Gradio Space: http://localhost:7860 +2026-05-11 17:07:52,231 WARNING services.gradio_client_service: GPU Space failed (Client.__init__() got an unexpected keyword argument 'httpx_kwargs'), trying CPU fallback... +2026-05-11 17:07:52,231 INFO services.gradio_client_service: Calling CPU fallback Space: https://eduardo4547-hyper-reality-sam2-cpu.hf.space +2026-05-11 17:07:52,231 ERROR backend.segmentation: [JOB a5fedf0501a94001b42fc9a71c7b75ca] failed: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 91, in segment_via_gradio_sync + raise RuntimeError(f"GPU Gradio Space failed and no CPU fallback configured. Error: {gpu_error}") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 33, in _call_gradio_sync +TypeError: Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\image_service.py", line 138, in run_upload_job + # We are simplifying the flow: skipping SAM 2 segmentation. + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 93, in segment_via_gradio_sync + try: + + ...<3 lines>... + raise RuntimeError( + ^^^^^^^^^^^^^^ +RuntimeError: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +2026-05-11 17:07:52,233 INFO backend.segmentation: [UPLOAD_BG] DONE 0.023s at 2026-05-11T23:07:52.233475+00:00 +2026-05-11 17:07:52,233 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:09:10,111 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T23:09:10.111062+00:00 +2026-05-11 17:09:10,111 INFO backend.segmentation: [JOB 0a7f381bf0964d229708409bc0c5bd3b] preparing_image progress=12 +2026-05-11 17:09:10,128 INFO backend.segmentation: [JOB 0a7f381bf0964d229708409bc0c5bd3b] segmenting_with_sam2 progress=30 estimated_seconds=49.28768 +2026-05-11 17:09:10,129 INFO services.gradio_client_service: Calling GPU Gradio Space: http://localhost:7860 +2026-05-11 17:09:10,129 WARNING services.gradio_client_service: GPU Space failed (Client.__init__() got an unexpected keyword argument 'httpx_kwargs'), trying CPU fallback... +2026-05-11 17:09:10,129 INFO services.gradio_client_service: Calling CPU fallback Space: https://eduardo4547-hyper-reality-sam2-cpu.hf.space +2026-05-11 17:09:10,129 ERROR backend.segmentation: [JOB 0a7f381bf0964d229708409bc0c5bd3b] failed: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 91, in segment_via_gradio_sync + raise RuntimeError(f"GPU Gradio Space failed and no CPU fallback configured. Error: {gpu_error}") + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 33, in _call_gradio_sync +TypeError: Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + +The above exception was the direct cause of the following exception: + +Traceback (most recent call last): + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\image_service.py", line 138, in run_upload_job + # We are simplifying the flow: skipping SAM 2 segmentation. + File "C:\Users\alane\OneDrive\Escritorio\Trabajo\Prueba-PoC\backend\services\gradio_client_service.py", line 93, in segment_via_gradio_sync + try: + + ...<3 lines>... + raise RuntimeError( + ^^^^^^^^^^^^^^ +RuntimeError: Both Gradio Spaces failed. + GPU (http://localhost:7860): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' + CPU (https://eduardo4547-hyper-reality-sam2-cpu.hf.space): Client.__init__() got an unexpected keyword argument 'httpx_kwargs' +2026-05-11 17:09:10,130 INFO backend.segmentation: [UPLOAD_BG] DONE 0.019s at 2026-05-11T23:09:10.130195+00:00 +2026-05-11 17:09:10,130 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:10:41,947 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 17:10:41,965 INFO backend.segmentation: [SAM2] SAM2 disabled (using Simplified OpenAI flow) +2026-05-11 17:10:44,865 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a0261f4b9a9473ac9483d03"}, "remainingTimeMS": 30} +2026-05-11 17:10:53,790 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T23:10:53.790345+00:00 +2026-05-11 17:10:53,790 INFO backend.segmentation: [JOB e7931cb747a044c0b5798ba3fd043246] preparing_image progress=12 +2026-05-11 17:10:53,918 INFO backend.segmentation: [JOB e7931cb747a044c0b5798ba3fd043246] segmenting_with_sam2 progress=30 estimated_seconds=49.28768 +2026-05-11 17:10:53,919 INFO backend.segmentation: [JOB e7931cb747a044c0b5798ba3fd043246] done (simplified, 0 masks) +2026-05-11 17:10:53,919 INFO backend.segmentation: [UPLOAD_BG] DONE 0.129s at 2026-05-11T23:10:53.919102+00:00 +2026-05-11 17:10:53,919 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:10:56,143 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a026200b9a9473ac9483d04"}, "remainingTimeMS": 30} +2026-05-11 17:10:58,808 INFO backend.segmentation: [APPLY_TEXTURE_AI] START at 2026-05-11T23:10:58.808470+00:00 +2026-05-11 17:10:58,809 INFO backend.segmentation: [APPLY_TEXTURE_AI] DONE 0.001s at 2026-05-11T23:10:58.809468+00:00 +2026-05-11 17:10:58,809 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:10:59,967 INFO backend.segmentation: [APPLY_TEXTURE_AI] START at 2026-05-11T23:10:59.967490+00:00 +2026-05-11 17:10:59,967 INFO backend.segmentation: [APPLY_TEXTURE_AI] DONE 0.001s at 2026-05-11T23:10:59.967490+00:00 +2026-05-11 17:10:59,968 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:11:13,886 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T23:11:13.886474+00:00 +2026-05-11 17:11:13,887 INFO backend.segmentation: [JOB 650975a38c7749568b7341423ad4d7d9] preparing_image progress=12 +2026-05-11 17:11:13,906 INFO backend.segmentation: [JOB 650975a38c7749568b7341423ad4d7d9] segmenting_with_sam2 progress=30 estimated_seconds=49.28768 +2026-05-11 17:11:13,907 INFO backend.segmentation: [JOB 650975a38c7749568b7341423ad4d7d9] done (simplified, 0 masks) +2026-05-11 17:11:13,907 INFO backend.segmentation: [UPLOAD_BG] DONE 0.021s at 2026-05-11T23:11:13.907778+00:00 +2026-05-11 17:11:13,907 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:11:28,374 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T23:11:28.374195+00:00 +2026-05-11 17:11:28,374 INFO backend.segmentation: [JOB 3f1f91ed35624099bffaf23f5c5badc3] preparing_image progress=12 +2026-05-11 17:11:28,400 INFO backend.segmentation: [JOB 3f1f91ed35624099bffaf23f5c5badc3] segmenting_with_sam2 progress=30 estimated_seconds=49.28768 +2026-05-11 17:11:28,401 INFO backend.segmentation: [JOB 3f1f91ed35624099bffaf23f5c5badc3] done (simplified, 0 masks) +2026-05-11 17:11:28,401 INFO backend.segmentation: [UPLOAD_BG] DONE 0.027s at 2026-05-11T23:11:28.401201+00:00 +2026-05-11 17:11:28,401 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:19:23,122 INFO backend.segmentation: Lifespan exiting; releasing resources. +2026-05-11 17:19:23,122 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:19:25,153 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 17:19:25,157 INFO backend.segmentation: [SAM2] SAM2 disabled (using Simplified OpenAI flow) +2026-05-11 17:19:25,157 WARNING backend.segmentation: Lifespan startup cancelled. Releasing resources. +2026-05-11 17:19:25,157 INFO backend.segmentation: Releasing resources (full_unload=True) +2026-05-11 17:19:25,192 INFO backend.segmentation: Lifespan exiting; releasing resources. +2026-05-11 17:19:25,193 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:19:26,642 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 17:19:26,646 INFO backend.segmentation: [SAM2] SAM2 disabled (using Simplified OpenAI flow) +2026-05-11 17:19:57,031 INFO backend.segmentation: Lifespan exiting; releasing resources. +2026-05-11 17:19:57,031 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:19:58,900 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 17:19:58,903 INFO backend.segmentation: [SAM2] SAM2 disabled (using Simplified OpenAI flow) +2026-05-11 17:19:58,904 WARNING backend.segmentation: Lifespan startup cancelled. Releasing resources. +2026-05-11 17:19:58,904 INFO backend.segmentation: Releasing resources (full_unload=True) +2026-05-11 17:19:58,935 INFO backend.segmentation: Lifespan exiting; releasing resources. +2026-05-11 17:19:58,935 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:20:00,569 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 17:20:00,574 INFO backend.segmentation: [SAM2] SAM2 disabled (using Simplified OpenAI flow) +2026-05-11 17:23:04,040 INFO backend.segmentation: Lifespan exiting; releasing resources. +2026-05-11 17:23:04,040 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:23:05,765 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 17:23:05,778 INFO backend.segmentation: [SAM2] SAM2 disabled (using Simplified OpenAI flow) +2026-05-11 17:23:05,778 WARNING backend.segmentation: Lifespan startup cancelled. Releasing resources. +2026-05-11 17:23:05,778 INFO backend.segmentation: Releasing resources (full_unload=True) +2026-05-11 17:23:05,805 INFO backend.segmentation: Lifespan exiting; releasing resources. +2026-05-11 17:23:05,805 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:23:07,223 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 17:23:07,237 INFO backend.segmentation: [SAM2] SAM2 disabled (using Simplified OpenAI flow) +2026-05-11 17:23:38,986 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a0264fa2cee8192599ed7d0"}, "remainingTimeMS": 30} +2026-05-11 17:23:42,830 INFO backend.segmentation: [UPLOAD_BG] START at 2026-05-11T23:23:42.830320+00:00 +2026-05-11 17:23:42,831 INFO backend.segmentation: [JOB f67851b861634797a6d951fb8eb77d1c] preparing_image progress=12 +2026-05-11 17:23:42,987 INFO backend.segmentation: [JOB f67851b861634797a6d951fb8eb77d1c] segmenting_with_sam2 progress=30 estimated_seconds=49.28768 +2026-05-11 17:23:42,988 INFO backend.segmentation: [JOB f67851b861634797a6d951fb8eb77d1c] done (simplified, 0 masks) +2026-05-11 17:23:42,989 INFO backend.segmentation: [UPLOAD_BG] DONE 0.158s at 2026-05-11T23:23:42.989362+00:00 +2026-05-11 17:23:42,989 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:23:50,263 INFO backend.segmentation: Lifespan exiting; releasing resources. +2026-05-11 17:23:50,263 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:23:52,250 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 17:23:52,264 INFO backend.segmentation: [SAM2] SAM2 disabled (using Simplified OpenAI flow) +2026-05-11 17:23:52,452 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a02650807bfc227790002e0"}, "remainingTimeMS": 30} +2026-05-11 17:35:48,564 INFO backend.segmentation: Lifespan exiting; releasing resources. +2026-05-11 17:35:48,564 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:35:51,035 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 17:35:51,056 INFO backend.segmentation: [SAM2] SAM2 disabled (using Simplified OpenAI flow) +2026-05-11 17:35:51,056 WARNING backend.segmentation: Lifespan startup cancelled. Releasing resources. +2026-05-11 17:35:51,056 INFO backend.segmentation: Releasing resources (full_unload=True) +2026-05-11 17:35:51,100 INFO backend.segmentation: Lifespan exiting; releasing resources. +2026-05-11 17:35:51,100 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:35:52,795 INFO backend.segmentation: [STARTUP] GRADIO_SPACE_URL=http://localhost:7860 +2026-05-11 17:35:52,809 INFO backend.segmentation: [SAM2] SAM2 disabled (using Simplified OpenAI flow) +2026-05-11 17:39:57,426 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "count", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a0268cd69c7f8481e63f142"}, "remainingTimeMS": 30} +2026-05-11 17:40:06,587 INFO backend.segmentation: [APPLY_TEXTURE_AI] START at 2026-05-11T23:40:06.587125+00:00 +2026-05-11 17:40:06,587 INFO backend.segmentation: [APPLY_TEXTURE_AI] DONE 0.000s at 2026-05-11T23:40:06.587125+00:00 +2026-05-11 17:40:06,587 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:42:20,430 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "count", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a02695c8c7c9cbbe3d73a93"}, "remainingTimeMS": 30} +2026-05-11 17:42:23,168 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "count", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a02695fd14dfcab99b31bb0"}, "remainingTimeMS": 30} +2026-05-11 17:42:24,195 INFO backend.segmentation: [APPLY_TEXTURE_AI] START at 2026-05-11T23:42:24.195000+00:00 +2026-05-11 17:42:24,207 INFO backend.segmentation: [APPLY_TEXTURE_AI] DONE 0.012s at 2026-05-11T23:42:24.207474+00:00 +2026-05-11 17:42:24,208 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:42:49,195 INFO backend.segmentation: [APPLY_TEXTURE_AI] START at 2026-05-11T23:42:49.195264+00:00 +2026-05-11 17:42:49,202 INFO backend.segmentation: [APPLY_TEXTURE_AI] DONE 0.007s at 2026-05-11T23:42:49.202264+00:00 +2026-05-11 17:42:49,202 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:42:50,897 INFO backend.segmentation: [APPLY_TEXTURE_AI] START at 2026-05-11T23:42:50.897139+00:00 +2026-05-11 17:42:50,905 INFO backend.segmentation: [APPLY_TEXTURE_AI] DONE 0.008s at 2026-05-11T23:42:50.905145+00:00 +2026-05-11 17:42:50,905 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:42:54,729 INFO backend.segmentation: [APPLY_TEXTURE_AI] START at 2026-05-11T23:42:54.729010+00:00 +2026-05-11 17:42:54,737 INFO backend.segmentation: [APPLY_TEXTURE_AI] DONE 0.008s at 2026-05-11T23:42:54.737010+00:00 +2026-05-11 17:42:54,737 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:44:02,023 INFO backend.segmentation: [APPLY_TEXTURE_AI] START at 2026-05-11T23:44:02.023505+00:00 +2026-05-11 17:44:02,031 INFO backend.segmentation: [APPLY_TEXTURE_AI] DONE 0.007s at 2026-05-11T23:44:02.031502+00:00 +2026-05-11 17:44:02,031 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:44:06,912 INFO backend.segmentation: [APPLY_TEXTURE_AI] START at 2026-05-11T23:44:06.912421+00:00 +2026-05-11 17:44:06,919 INFO backend.segmentation: [APPLY_TEXTURE_AI] DONE 0.007s at 2026-05-11T23:44:06.919421+00:00 +2026-05-11 17:44:06,919 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:44:08,458 INFO backend.segmentation: [APPLY_TEXTURE_AI] START at 2026-05-11T23:44:08.458048+00:00 +2026-05-11 17:44:08,465 INFO backend.segmentation: [APPLY_TEXTURE_AI] DONE 0.007s at 2026-05-11T23:44:08.465047+00:00 +2026-05-11 17:44:08,466 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:44:15,785 INFO backend.segmentation: [APPLY_TEXTURE_AI] START at 2026-05-11T23:44:15.785067+00:00 +2026-05-11 17:44:15,792 INFO backend.segmentation: [APPLY_TEXTURE_AI] DONE 0.008s at 2026-05-11T23:44:15.792067+00:00 +2026-05-11 17:44:15,793 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:44:45,666 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "count", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a0269eda4c4e67897e4de91"}, "remainingTimeMS": 30} +2026-05-11 17:44:50,557 INFO backend.segmentation: [APPLY_TEXTURE_AI] START at 2026-05-11T23:44:50.557167+00:00 +2026-05-11 17:44:50,565 INFO backend.segmentation: [APPLY_TEXTURE_AI] DONE 0.007s at 2026-05-11T23:44:50.565142+00:00 +2026-05-11 17:44:50,565 INFO backend.segmentation: Releasing resources (full_unload=False) +2026-05-11 17:44:52,481 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a0269f4a4c4e67897e4de92"}, "remainingTimeMS": 30} +2026-05-11 17:44:52,482 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a0269f4a4c4e67897e4de92"}, "remainingTimeMS": 30} +2026-05-11 17:46:59,700 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "count", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a026a73d56a3146484b59dd"}, "remainingTimeMS": 30} +2026-05-11 17:47:05,233 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a026a79d56a3146484b59de"}, "remainingTimeMS": 30} +2026-05-11 17:47:05,234 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a026a79d56a3146484b59de"}, "remainingTimeMS": 30} +2026-05-11 17:47:30,427 INFO httpx: HTTP Request: POST https://api.openai.com/v1/images/edits "HTTP/1.1 200 OK" +2026-05-11 17:47:56,420 INFO httpx: HTTP Request: POST https://api.openai.com/v1/images/edits "HTTP/1.1 200 OK" +2026-05-11 17:50:02,713 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "count", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a026b2ac773958e310a1326"}, "remainingTimeMS": 30} +2026-05-11 17:50:04,167 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a026b2cc773958e310a1327"}, "remainingTimeMS": 30} +2026-05-11 17:50:04,207 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a026b2cc773958e310a1327"}, "remainingTimeMS": 30} +2026-05-11 17:50:34,550 INFO httpx: HTTP Request: POST https://api.openai.com/v1/images/edits "HTTP/1.1 200 OK" +2026-05-11 17:51:14,601 INFO httpx: HTTP Request: POST https://api.openai.com/v1/images/edits "HTTP/1.1 200 OK" +2026-05-11 17:57:39,513 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "count", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a026cf3162482e62cf7ea3e"}, "remainingTimeMS": 30} +2026-05-11 17:57:42,313 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "count", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a026cf6472643c8c1e786d8"}, "remainingTimeMS": 30} +2026-05-11 17:58:01,618 INFO httpx: HTTP Request: POST https://api.openai.com/v1/images/edits "HTTP/1.1 200 OK" +2026-05-11 17:58:27,575 INFO httpx: HTTP Request: POST https://api.openai.com/v1/images/edits "HTTP/1.1 200 OK" +2026-05-11 17:59:21,451 INFO httpx: HTTP Request: POST https://api.openai.com/v1/images/edits "HTTP/1.1 200 OK" +2026-05-11 17:59:29,572 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "count", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a026d61676e4e120285cb22"}, "remainingTimeMS": 30} +2026-05-11 17:59:32,127 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "count", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a026d649325671c76e0d3cc"}, "remainingTimeMS": 30} +2026-05-11 18:00:00,476 INFO httpx: HTTP Request: POST https://api.openai.com/v1/images/edits "HTTP/1.1 200 OK" +2026-05-11 18:00:20,146 INFO httpx: HTTP Request: POST https://api.openai.com/v1/images/edits "HTTP/1.1 200 OK" +2026-05-11 18:00:36,292 INFO httpx: HTTP Request: POST https://api.openai.com/v1/images/edits "HTTP/1.1 200 OK" +2026-05-11 18:13:24,578 INFO httpx: HTTP Request: POST https://api.openai.com/v1/images/edits "HTTP/1.1 200 OK" +2026-05-11 18:22:57,043 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a0272e19325671c76e0d3cd"}, "remainingTimeMS": 30} +2026-05-11 18:22:57,044 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a0272e19325671c76e0d3cd"}, "remainingTimeMS": 30} +2026-05-11 18:23:24,738 INFO httpx: HTTP Request: POST https://api.openai.com/v1/images/edits "HTTP/1.1 200 OK" +2026-05-11 18:43:21,019 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "count", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a0277a9666e7bcf74e6e618"}, "remainingTimeMS": 30} +2026-05-11 18:47:06,723 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a02788a666e7bcf74e6e619"}, "remainingTimeMS": 30} +2026-05-11 18:47:06,724 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a02788a666e7bcf74e6e619"}, "remainingTimeMS": 30} +2026-05-11 18:47:14,864 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "count", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a0278924b3f5d3d0d27e63c"}, "remainingTimeMS": 30} +2026-05-11 18:47:17,556 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "count", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a0278954a12dd2c11934073"}, "remainingTimeMS": 30} +2026-05-11 18:47:40,240 INFO httpx: HTTP Request: POST https://api.openai.com/v1/images/edits "HTTP/1.1 200 OK" +2026-05-11 18:47:53,426 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a0278b94a12dd2c11934074"}, "remainingTimeMS": 30} +2026-05-11 18:47:53,426 INFO pymongo.serverSelection: {"message": "Waiting for suitable server to become available", "selector": "Primary()", "operation": "find", "topologyDescription": ", , ]>", "clientId": {"$oid": "6a0278b94a12dd2c11934074"}, "remainingTimeMS": 30} diff --git a/backend/main.py b/backend/main.py index 83418e07ce38a7f5a6586e8861ecb406899b89a7..be717ceec677383b00e4a11a65a28806dc8c6930 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,129 +1,127 @@ -import mimetypes -import os -import subprocess -import threading -import time -from pathlib import Path - -from dotenv import load_dotenv -load_dotenv(Path(__file__).resolve().parent / ".env") - -from fastapi import FastAPI, Request -from fastapi.middleware.cors import CORSMiddleware -from fastapi.staticfiles import StaticFiles - -from core.config import GRADIO_SPACE_URL, logger -from routers import auth, catalog, media, pages, segmentation, sessions, share, presets -from routers.catalog import seed_catalog -from services.sam2_service import lifespan - -mimetypes.add_type("application/javascript", ".js", strict=True) -mimetypes.add_type("text/css", ".css", strict=True) -mimetypes.add_type("image/svg+xml", ".svg", strict=True) - -logger.info("[STARTUP] GRADIO_SPACE_URL=%s", GRADIO_SPACE_URL or "(not set — using local SAM2)") - -app = FastAPI(title="Hyper Reality Backend", lifespan=lifespan) - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], - allow_headers=["*"], -) - - -@app.middleware("http") -async def remove_x_frame_options(request: Request, call_next): - response = await call_next(request) - if "x-frame-options" in response.headers: - del response.headers["x-frame-options"] - response.headers["Content-Security-Policy"] = "frame-ancestors *" - return response - - -# Routers -app.include_router(pages.router) -app.include_router(auth.router) -app.include_router(share.router) -app.include_router(media.router) -app.include_router(catalog.router) -app.include_router(sessions.router) -app.include_router(segmentation.router) -app.include_router(presets.router) - -# Static files -BASE_DIR = Path(__file__).resolve().parent -UPLOADS_DIR = BASE_DIR / "uploads" -FRONTEND_DIST = BASE_DIR.parent / "frontend" / "dist" - -UPLOADS_DIR.mkdir(parents=True, exist_ok=True) -app.mount("/uploads", StaticFiles(directory=UPLOADS_DIR), name="uploads") - -if (FRONTEND_DIST / "index.html").exists(): - # Montado en "/" como catch-all para SPA — los routers de API tienen prioridad - app.mount("/", StaticFiles(directory=FRONTEND_DIST, html=True), name="frontend") - - -# Frontend watcher (development helper) -FRONTEND_DIR = BASE_DIR.parent / "frontend" -FRONTEND_SRC = FRONTEND_DIR / "src" - - -def scan_frontend_sources() -> dict: - if not FRONTEND_SRC.exists(): - return {} - files = {} - for path in FRONTEND_SRC.rglob("*"): - if path.is_file() and path.suffix in {".ts", ".tsx", ".js", ".jsx", ".css", ".json", ".html"}: - files[path] = path.stat().st_mtime - for extra in [FRONTEND_DIR / "vite.config.ts", FRONTEND_DIR / "package.json", FRONTEND_DIR / "tsconfig.json"]: - if extra.exists(): - files[extra] = extra.stat().st_mtime - return files - - -def run_frontend_build() -> None: - if not FRONTEND_DIR.exists(): - return - print("[backend] Ejecutando build del frontend...") - result = subprocess.run(["npm", "run", "build"], cwd=str(FRONTEND_DIR), capture_output=True, text=True) - if result.returncode != 0: - print("[backend] Build falló:") - print(result.stdout) - print(result.stderr) - else: - print("[backend] Build completado correctamente.") - - -def watch_frontend_changes(interval: float = 2.0) -> None: - last_state = scan_frontend_sources() - while True: - time.sleep(interval) - current_state = scan_frontend_sources() - if current_state != last_state: - if last_state: - print("[backend] Cambio detectado en frontend. Reconstruyendo...") - run_frontend_build() - last_state = current_state - - -@app.on_event("startup") -async def startup_seed_catalog(): - if MONGODB_URI := os.getenv("MONGODB_URI", ""): - try: - await seed_catalog() - except Exception as exc: - logger.warning("[STARTUP] seed_catalog falló: %s", exc) - - -@app.on_event("startup") -async def startup_watch_frontend(): - thread = threading.Thread(target=watch_frontend_changes, daemon=True) - thread.start() - - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info") +import mimetypes +import os +import subprocess +import threading +import time +from pathlib import Path + +from dotenv import load_dotenv +load_dotenv(Path(__file__).resolve().parent / ".env") + +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles + +from core.config import logger +from routers import auth, catalog, media, pages, segmentation, sessions, share, openai_image, active_sessions +from routers.catalog import seed_catalog + +mimetypes.add_type("application/javascript", ".js", strict=True) +mimetypes.add_type("text/css", ".css", strict=True) +mimetypes.add_type("image/svg+xml", ".svg", strict=True) + +app = FastAPI(title="Hyper Reality Backend") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allow_headers=["*"], +) + + +@app.middleware("http") +async def remove_x_frame_options(request: Request, call_next): + response = await call_next(request) + if "x-frame-options" in response.headers: + del response.headers["x-frame-options"] + response.headers["Content-Security-Policy"] = "frame-ancestors *" + return response + + +# Routers +# Mantener sólo la ruta de subida de imágenes (/api/upload-image). +# Comentamos las demás inclusiones para deshabilitar funcionalidades +# posteriores (segmentación, inpainting, sesiones, catálogo, etc.). +# Re-activar routers necesarios para el frontend +app.include_router(sessions.router) +app.include_router(segmentation.router) +app.include_router(openai_image.router) +app.include_router(active_sessions.router) +app.include_router(catalog.router) + +# Static files +BASE_DIR = Path(__file__).resolve().parent +UPLOADS_DIR = BASE_DIR / "uploads" +FRONTEND_DIST = BASE_DIR.parent / "frontend" / "dist" + +UPLOADS_DIR.mkdir(parents=True, exist_ok=True) +app.mount("/uploads", StaticFiles(directory=UPLOADS_DIR), name="uploads") + +if (FRONTEND_DIST / "index.html").exists(): + # Montado en "/" como catch-all para SPA — los routers de API tienen prioridad + app.mount("/", StaticFiles(directory=FRONTEND_DIST, html=True), name="frontend") + + +# Frontend watcher (development helper) +FRONTEND_DIR = BASE_DIR.parent / "frontend" +FRONTEND_SRC = FRONTEND_DIR / "src" + + +def scan_frontend_sources() -> dict: + if not FRONTEND_SRC.exists(): + return {} + files = {} + for path in FRONTEND_SRC.rglob("*"): + if path.is_file() and path.suffix in {".ts", ".tsx", ".js", ".jsx", ".css", ".json", ".html"}: + files[path] = path.stat().st_mtime + for extra in [FRONTEND_DIR / "vite.config.ts", FRONTEND_DIR / "package.json", FRONTEND_DIR / "tsconfig.json"]: + if extra.exists(): + files[extra] = extra.stat().st_mtime + return files + + +def run_frontend_build() -> None: + if not FRONTEND_DIR.exists(): + return + print("[backend] Ejecutando build del frontend...") + result = subprocess.run(["npm", "run", "build"], cwd=str(FRONTEND_DIR), capture_output=True, text=True) + if result.returncode != 0: + print("[backend] Build falló:") + print(result.stdout) + print(result.stderr) + else: + print("[backend] Build completado correctamente.") + + +def watch_frontend_changes(interval: float = 2.0) -> None: + last_state = scan_frontend_sources() + while True: + time.sleep(interval) + current_state = scan_frontend_sources() + if current_state != last_state: + if last_state: + print("[backend] Cambio detectado en frontend. Reconstruyendo...") + run_frontend_build() + last_state = current_state + + +@app.on_event("startup") +async def startup_seed_catalog(): + if MONGODB_URI := os.getenv("MONGODB_URI", ""): + try: + await seed_catalog() + except Exception as exc: + logger.warning("[STARTUP] seed_catalog falló: %s", exc) + + +@app.on_event("startup") +async def startup_watch_frontend(): + thread = threading.Thread(target=watch_frontend_changes, daemon=True) + thread.start() + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info") diff --git a/backend/models/__pycache__/__init__.cpython-313.pyc b/backend/models/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f26df08ef10d2fcb7cf86d1168420354b710007c Binary files /dev/null and b/backend/models/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/models/__pycache__/schemas.cpython-312.pyc b/backend/models/__pycache__/schemas.cpython-312.pyc index 3cd24a237eda094ecd7d81aa29e6f656b95d242c..f2a37ea20e093295e34baa03a7c0f5bb99b70351 100644 Binary files a/backend/models/__pycache__/schemas.cpython-312.pyc and b/backend/models/__pycache__/schemas.cpython-312.pyc differ diff --git a/backend/models/__pycache__/schemas.cpython-313.pyc b/backend/models/__pycache__/schemas.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4725c8ff1702d8a9f8c3057fbe7f6497eac58e22 Binary files /dev/null and b/backend/models/__pycache__/schemas.cpython-313.pyc differ diff --git a/backend/models/schemas.py b/backend/models/schemas.py index 8dafd515c5368ebdb39978c249fa96179c2ebc7a..e11337e242da2a879a7ca3bdbb8703994fc01a18 100644 --- a/backend/models/schemas.py +++ b/backend/models/schemas.py @@ -112,5 +112,7 @@ class ApplyColorRequest(BaseModel): class ApplyTextureAIRequest(BaseModel): filename: str original_filename: str = "" - mask_filename: str + mask_filename: str = "" prompt: str = "" + texture_name: str = "" + diff --git a/backend/requirements.txt b/backend/requirements.txt index 3175b21c48e925ab26cc56aff8037f1b7054dea8..9cdd6f80918e68712eb7a2223f6323bb95d490bc 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -9,3 +9,4 @@ pydantic python-multipart gradio_client opencv-python-headless +openai diff --git a/backend/routers/__pycache__/__init__.cpython-313.pyc b/backend/routers/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..568dc4ff8e14357f6922d570fe12e593bce7fd4b Binary files /dev/null and b/backend/routers/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/routers/__pycache__/active_sessions.cpython-312.pyc b/backend/routers/__pycache__/active_sessions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fa8757c06ff833c2488f1d8786943e43430b0d30 Binary files /dev/null and b/backend/routers/__pycache__/active_sessions.cpython-312.pyc differ diff --git a/backend/routers/__pycache__/auth.cpython-313.pyc b/backend/routers/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67ab50f91fa3493e13379f48a6e0decb87150899 Binary files /dev/null and b/backend/routers/__pycache__/auth.cpython-313.pyc differ diff --git a/backend/routers/__pycache__/catalog.cpython-312.pyc b/backend/routers/__pycache__/catalog.cpython-312.pyc index 1570f4b6908ad62164d9038ec2746dafe1ec1490..5416078ebf8fec2b8a893c3e58307d87ef79ef23 100644 Binary files a/backend/routers/__pycache__/catalog.cpython-312.pyc and b/backend/routers/__pycache__/catalog.cpython-312.pyc differ diff --git a/backend/routers/__pycache__/catalog.cpython-313.pyc b/backend/routers/__pycache__/catalog.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..545c254a54d65fda0d5eeb8c25ba828cedea9d26 Binary files /dev/null and b/backend/routers/__pycache__/catalog.cpython-313.pyc differ diff --git a/backend/routers/__pycache__/openai_image.cpython-312.pyc b/backend/routers/__pycache__/openai_image.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c6ff539d3bd3d5a7df95acef9aad0a8545689521 Binary files /dev/null and b/backend/routers/__pycache__/openai_image.cpython-312.pyc differ diff --git a/backend/routers/__pycache__/segmentation.cpython-312.pyc b/backend/routers/__pycache__/segmentation.cpython-312.pyc index 97441b0d76ec6565ba136e4c58dcfb318a07615f..093124dd15ceb9cebe86e8d54f7a7085f19432ab 100644 Binary files a/backend/routers/__pycache__/segmentation.cpython-312.pyc and b/backend/routers/__pycache__/segmentation.cpython-312.pyc differ diff --git a/backend/routers/__pycache__/segmentation.cpython-313.pyc b/backend/routers/__pycache__/segmentation.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f939e22e2772a22d1497709655e33d3b202cdf75 Binary files /dev/null and b/backend/routers/__pycache__/segmentation.cpython-313.pyc differ diff --git a/backend/routers/active_sessions.py b/backend/routers/active_sessions.py new file mode 100644 index 0000000000000000000000000000000000000000..9675125ac316b1b506791bbbdfc8d4e2aa1630a0 --- /dev/null +++ b/backend/routers/active_sessions.py @@ -0,0 +1,11 @@ +from fastapi import APIRouter +from fastapi.responses import JSONResponse + +router = APIRouter() + + +@router.get("/api/active-sessions") +async def get_active_sessions(): + # Endpoint ligero que devuelve sesiones activas. + # Actualmente no consultamos BD aquí; devolvemos un objeto vacío para evitar errores en frontend. + return JSONResponse(content={"count": 0, "active_sessions": []}) diff --git a/backend/routers/catalog.py b/backend/routers/catalog.py index e198f032eb3d2ba51c3412fdee0bf5caae70d4bf..b164f0e5c848336742d2f8d7b691fa1ef4560c78 100644 --- a/backend/routers/catalog.py +++ b/backend/routers/catalog.py @@ -1,241 +1,240 @@ -import os -from datetime import datetime - -from fastapi import APIRouter -from fastapi.responses import JSONResponse -from motor.motor_asyncio import AsyncIOMotorClient -from pydantic import BaseModel - -router = APIRouter(prefix="/api/catalog") - -MONGODB_URI = os.getenv("MONGODB_URI", "") -_client: AsyncIOMotorClient | None = None -_db = None -_col = None - - -def _get_col(): - global _client, _db, _col - if _col is None: - if not MONGODB_URI: - raise RuntimeError("MONGODB_URI no configurado") - _client = AsyncIOMotorClient(MONGODB_URI) - _db = _client["hyper_reality"] - _col = _db["catalog"] - return _col - - -# ── Datos iniciales (se insertan solo si la colección está vacía) ───────────── -_SEED = [ - { - "_id": "acm", - "nombre": "ACM (Aluminio Compuesto)", - "tipo": "paredes", - "descripcion": "Paneles de aluminio compuesto para fachadas y exteriores", - "especificaciones": [ - "Espesor de ACM 4mm.", - "Medida 1.22m x 2.44m", - "Facil Mantenimiento.", - "Espesor de Aluminio 0.40mm.", - "Se puede doblar o biselar", - ], - "url_detalle": "https://heyzine.com/flip-book/447fe3eb8e.html#page/16", - "productos": [ - {"id": "acm_white", "nombre": "Glossy White", "textura": "Texture_ACM/ACM_White.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_White.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, - {"id": "acm_amarillo", "nombre": "Amarillo", "textura": "Texture_ACM/ACM_Amarillo.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Amarillo.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, - {"id": "acm_orange", "nombre": "Glossy Orange", "textura": "Texture_ACM/ACM_Orange.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Orange.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, - {"id": "acm_red", "nombre": "Glossy Red", "textura": "Texture_ACM/ACM_Glossy_Red.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Glossy_Red.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, - {"id": "acm_light_blue", "nombre": "Light Blue", "textura": "Texture_ACM/ACM_Light_Blue.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Light_Blue.png", "dimensiones": ["1.22x2.44"]}, - {"id": "acm_azul", "nombre": "Azul", "textura": "Texture_ACM/ACM_Azul.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Azul.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, - {"id": "acm_verde_hn", "nombre": "Verde HN", "textura": "Texture_ACM/ACM_Verde_HN.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Verde_HN.png", "dimensiones": ["1.22x2.44"]}, - {"id": "acm_verde_lima", "nombre": "Verde Lima", "textura": "Texture_ACM/ACM_Verde_Lima.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Verde_Lima.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, - {"id": "acm_verde", "nombre": "Verde", "textura": "Texture_ACM/ACM_Verde.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Verde.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, - {"id": "acm_madera_clara","nombre": "Madera Clara", "textura": "Texture_ACM/ACM_Madera_Clara.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Madera_Clara.png", "dimensiones": ["1.22x2.44"]}, - {"id": "acm_roble", "nombre": "Roble (Oak)", "textura": "Texture_ACM/ACM_ROBLE(OAK).png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_ROBLE(OAK).png", "dimensiones": ["1.22x2.44"]}, - {"id": "acm_grafito", "nombre": "Grafito", "textura": "Texture_ACM/ACM_Grafito.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Grafito.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, - {"id": "acm_metalic", "nombre": "Silver Metallic", "textura": "Texture_ACM/ACM_Metalic.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Metalic.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, - {"id": "acm_mouse_grey", "nombre": "Mouse Grey", "textura": "Texture_ACM/ACM_MouseGrey.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_MouseGrey.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, - {"id": "acm_matte_black", "nombre": "Matte Black", "textura": "Texture_ACM/ACM_Matteblack.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Matteblack.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, - {"id": "acm_glossy_black","nombre": "Glossy Black", "textura": "Texture_ACM/ACM_Glossy_Black.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Glossy_Black.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, - ], - "created_at": "2026-04-20T00:00:00Z", - }, - { - "_id": "wpc", - "nombre": "WPC (Exterior e Interior)", - "tipo": "paredes", - "descripcion": "Los paneles de WPC se utilizan como revestimiento decorativo para paredes. No se deforma, no requiere mantenimiento constante y tiene mayor durabilidad. Crea mayor estetica e instalacion rapida.", - "especificaciones": [ - "Revestimiento decorativo para paredes.", - "No se deforma ni requiere mantenimiento constante.", - "Mayor durabilidad y estetica.", - "Instalacion rapida.", - "Ideal para: sala principal (pared protagonista), comedor, interior de oficina, pasillo o entradas.", - ], - "url_detalle": "https://heyzine.com/flip-book/447fe3eb8e.html#page/39", - "productos": [ - {"id": "WPC_madera_oscuro", "nombre": "WPC Madera Oscuro", "textura": "Texture_WPC_EXTERIOR_INTERIOR/WPC_madera_oscuro.png", "url_preview": "/seg/texture-preview/Texture_WPC_EXTERIOR_INTERIOR/WPC_madera_oscuro.png", "dimensiones": ["2.90x0.25"]}, - {"id": "WPC_madera_claro", "nombre": "WPC Madera Claro", "textura": "Texture_WPC_EXTERIOR_INTERIOR/WPC_madera_claro.png", "url_preview": "/seg/texture-preview/Texture_WPC_EXTERIOR_INTERIOR/WPC_madera_claro.png", "dimensiones": ["2.90x0.25"]}, - {"id": "WPC_madera_gris", "nombre": "WPC Madera Gris", "textura": "Texture_WPC_EXTERIOR_INTERIOR/WPC_madera_gris.png", "url_preview": "/seg/texture-preview/Texture_WPC_EXTERIOR_INTERIOR/WPC_madera_gris.png", "dimensiones": ["2.90x0.25"]}, - {"id": "WPC_negro", "nombre": "WPC Negro", "textura": "Texture_WPC_EXTERIOR_INTERIOR/WPC_negro.png", "url_preview": "/seg/texture-preview/Texture_WPC_EXTERIOR_INTERIOR/WPC_negro.png", "dimensiones": ["2.90x0.25"]}, - ], - "created_at": "2026-05-07T00:00:00Z", - }, - { - "_id": "wpc_deck", - "nombre": "WPC Deck", - "tipo": "suelos", - "descripcion": "Deck de WPC para exteriores e interiores. Ideal para terrazas, jardines, bordes de piscina y espacios al aire libre.", - "especificaciones": [ - "Resistencia a la intemperie.", - "Bajo mantenimiento.", - "Respetuosa con el medio ambiente.", - "Esteticamente agradable.", - ], - "url_detalle": "https://heyzine.com/flip-book/447fe3eb8e.html#page/38", - "productos": [ - {"id": "DECK_madera", "nombre": "Deck Madera", "textura": "Texture_WPC_DECK/DECK_madera.png", "url_preview": "/seg/texture-preview/Texture_WPC_DECK/DECK_madera.png", "dimensiones": ["2.90x0.14"]}, - {"id": "DECK_madera_oscuro", "nombre": "Deck Madera Oscuro", "textura": "Texture_WPC_DECK/DECK_madera_oscuro.png", "url_preview": "/seg/texture-preview/Texture_WPC_DECK/DECK_madera_oscuro.png", "dimensiones": ["2.90x0.14"]}, - {"id": "DECK_gris", "nombre": "Deck Gris", "textura": "Texture_WPC_DECK/DECK_gris.png", "url_preview": "/seg/texture-preview/Texture_WPC_DECK/DECK_gris.png", "dimensiones": ["2.90x0.14"]}, - ], - "created_at": "2026-05-08T00:00:00Z", - }, -] - - -async def seed_catalog() -> None: - col = _get_col() - count = await col.count_documents({}) - if count == 0: - await col.insert_many(_SEED) - return - - # Insertar entradas nuevas del seed y parchar campos faltantes en las existentes - for seed_item in _SEED: - doc = await col.find_one({"_id": seed_item["_id"]}) - if not doc: - await col.insert_one(dict(seed_item)) - continue - patch = {k: v for k, v in seed_item.items() if k not in doc and k != "_id"} - if patch: - await col.update_one({"_id": seed_item["_id"]}, {"$set": patch}) - - -def _serialize(doc: dict) -> dict: - out = dict(doc) - out["id"] = str(out.pop("_id")) - return out - - -def _seed_as_response() -> list[dict]: - return [{**{k: v for k, v in item.items() if k != "_id"}, "id": item["_id"]} for item in _SEED] - - -# ── Endpoints de lectura ────────────────────────────────────────────────────── - -@router.get("/textures") -async def get_texture_catalog() -> JSONResponse: - try: - col = _get_col() - docs = await col.find({}).to_list(length=200) - if docs: - return JSONResponse(content={"categories": [_serialize(d) for d in docs]}) - except Exception: - pass - # Fallback a datos estáticos si MongoDB no está disponible o la colección está vacía - return JSONResponse(content={"categories": _seed_as_response()}) - - -@router.get("/textures/{category_id}") -async def get_texture_category(category_id: str) -> JSONResponse: - try: - col = _get_col() - doc = await col.find_one({"_id": category_id}) - if doc: - return JSONResponse(content=_serialize(doc)) - except Exception: - pass - fallback = next((item for item in _SEED if item["_id"] == category_id), None) - if fallback: - return JSONResponse(content=_seed_as_response()[_SEED.index(fallback)]) - return JSONResponse(content={"detail": f"Categoria '{category_id}' no encontrada"}, status_code=404) - - -# ── Modelos ─────────────────────────────────────────────────────────────────── - -class ProductoItem(BaseModel): - id: str - nombre: str - textura: str - url_preview: str - dimensiones: list[str] = [] - - -class CategoriaBody(BaseModel): - id: str - nombre: str - tipo: str = "paredes" - descripcion: str = "" - especificaciones: list[str] = [] - url_detalle: str = "" - productos: list[ProductoItem] = [] - - -# ── Endpoints de escritura ──────────────────────────────────────────────────── - -@router.post("/category") -async def add_category(body: CategoriaBody) -> JSONResponse: - col = _get_col() - existing = await col.find_one({"_id": body.id}) - if existing: - return JSONResponse(content={"error": f"Categoria '{body.id}' ya existe"}, status_code=409) - doc = body.model_dump() - doc["_id"] = doc.pop("id") - doc["created_at"] = datetime.utcnow().isoformat() + "Z" - await col.insert_one(doc) - return JSONResponse(content={"ok": True, "id": body.id}, status_code=201) - - -@router.put("/category/{category_id}") -async def update_category(category_id: str, body: CategoriaBody) -> JSONResponse: - col = _get_col() - doc = body.model_dump() - doc.pop("id", None) - doc["updated_at"] = datetime.utcnow().isoformat() + "Z" - result = await col.update_one({"_id": category_id}, {"$set": doc}) - if result.matched_count == 0: - return JSONResponse(content={"error": "Categoria no encontrada"}, status_code=404) - return JSONResponse(content={"ok": True}) - - -@router.delete("/category/{category_id}") -async def delete_category(category_id: str) -> JSONResponse: - col = _get_col() - result = await col.delete_one({"_id": category_id}) - if result.deleted_count == 0: - return JSONResponse(content={"error": "Categoria no encontrada"}, status_code=404) - return JSONResponse(content={"ok": True, "deleted": category_id}) - - -@router.post("/category/{category_id}/product") -async def add_product(category_id: str, product: ProductoItem) -> JSONResponse: - col = _get_col() - result = await col.update_one( - {"_id": category_id, "productos.id": {"$ne": product.id}}, - {"$push": {"productos": product.model_dump()}, "$set": {"updated_at": datetime.utcnow().isoformat() + "Z"}}, - ) - if result.matched_count == 0: - return JSONResponse(content={"error": "Categoria no encontrada o producto duplicado"}, status_code=409) - return JSONResponse(content={"ok": True}, status_code=201) - - -@router.delete("/category/{category_id}/product/{product_id}") -async def delete_product(category_id: str, product_id: str) -> JSONResponse: - col = _get_col() - result = await col.update_one( - {"_id": category_id}, - {"$pull": {"productos": {"id": product_id}}, "$set": {"updated_at": datetime.utcnow().isoformat() + "Z"}}, - ) - if result.matched_count == 0: - return JSONResponse(content={"error": "Categoria no encontrada"}, status_code=404) - return JSONResponse(content={"ok": True}) +import os +from datetime import datetime + +from fastapi import APIRouter +from fastapi.responses import JSONResponse +from motor.motor_asyncio import AsyncIOMotorClient +from pydantic import BaseModel + +router = APIRouter(prefix="/api/catalog") + +MONGODB_URI = os.getenv("MONGODB_URI", "") +_client: AsyncIOMotorClient | None = None +_db = None +_col = None + + +def _get_col(): + global _client, _db, _col + if _col is None: + if not MONGODB_URI: + raise RuntimeError("MONGODB_URI no configurado") + _client = AsyncIOMotorClient(MONGODB_URI) + _db = _client["hyper_reality"] + _col = _db["catalog"] + return _col + + +# ── Datos iniciales (se insertan solo si la colección está vacía) ───────────── +_SEED = [ + { + "_id": "acm", + "nombre": "ACM (Aluminio Compuesto)", + "tipo": "paredes", + "descripcion": "Paneles de aluminio compuesto para fachadas y exteriores", + "especificaciones": [ + "Espesor de ACM 4mm.", + "Medida 1.22m x 2.44m", + "Facil Mantenimiento.", + "Espesor de Aluminio 0.40mm.", + "Se puede doblar o biselar", + ], + "url_detalle": "https://heyzine.com/flip-book/447fe3eb8e.html#page/16", + "productos": [ + {"id": "acm_white", "nombre": "Glossy White", "textura": "Texture_ACM/ACM_White.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_White.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, + {"id": "acm_amarillo", "nombre": "Amarillo", "textura": "Texture_ACM/ACM_Amarillo.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Amarillo.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, + {"id": "acm_orange", "nombre": "Glossy Orange", "textura": "Texture_ACM/ACM_Orange.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Orange.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, + {"id": "acm_red", "nombre": "Glossy Red", "textura": "Texture_ACM/ACM_Glossy_Red.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Glossy_Red.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, + {"id": "acm_light_blue", "nombre": "Light Blue", "textura": "Texture_ACM/ACM_Light_Blue.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Light_Blue.png", "dimensiones": ["1.22x2.44"]}, + {"id": "acm_azul", "nombre": "Azul", "textura": "Texture_ACM/ACM_Azul.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Azul.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, + {"id": "acm_verde_hn", "nombre": "Verde HN", "textura": "Texture_ACM/ACM_Verde_HN.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Verde_HN.png", "dimensiones": ["1.22x2.44"]}, + {"id": "acm_verde_lima", "nombre": "Verde Lima", "textura": "Texture_ACM/ACM_Verde_Lima.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Verde_Lima.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, + {"id": "acm_verde", "nombre": "Verde", "textura": "Texture_ACM/ACM_Verde.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Verde.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, + {"id": "acm_madera_clara","nombre": "Madera Clara", "textura": "Texture_ACM/ACM_Madera_Clara.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Madera_Clara.png", "dimensiones": ["1.22x2.44"]}, + {"id": "acm_roble", "nombre": "Roble (Oak)", "textura": "Texture_ACM/ACM_ROBLE(OAK).png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_ROBLE(OAK).png", "dimensiones": ["1.22x2.44"]}, + {"id": "acm_grafito", "nombre": "Grafito", "textura": "Texture_ACM/ACM_Grafito.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Grafito.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, + {"id": "acm_metalic", "nombre": "Silver Metallic", "textura": "Texture_ACM/ACM_Metalic.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Metalic.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, + {"id": "acm_mouse_grey", "nombre": "Mouse Grey", "textura": "Texture_ACM/ACM_MouseGrey.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_MouseGrey.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, + {"id": "acm_matte_black", "nombre": "Matte Black", "textura": "Texture_ACM/ACM_Matteblack.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Matteblack.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, + {"id": "acm_glossy_black","nombre": "Glossy Black", "textura": "Texture_ACM/ACM_Glossy_Black.png", "url_preview": "/seg/texture-preview/Texture_ACM/ACM_Glossy_Black.png", "dimensiones": ["1.22x2.44", "1.50x4.98"]}, + ], + "created_at": "2026-04-20T00:00:00Z", + }, + { + "_id": "wpc", + "nombre": "WPC (Exterior e Interior)", + "tipo": "paredes", + "descripcion": "Los paneles de WPC se utilizan como revestimiento decorativo para paredes. No se deforma, no requiere mantenimiento constante y tiene mayor durabilidad. Crea mayor estetica e instalacion rapida.", + "especificaciones": [ + "Revestimiento decorativo para paredes.", + "No se deforma ni requiere mantenimiento constante.", + "Mayor durabilidad y estetica.", + "Instalacion rapida.", + "Ideal para: sala principal (pared protagonista), comedor, interior de oficina, pasillo o entradas.", + ], + "url_detalle": "https://heyzine.com/flip-book/447fe3eb8e.html#page/39", + "productos": [ + {"id": "WPC_madera_oscuro", "nombre": "WPC Madera Oscuro", "textura": "Texture_WPC_EXTERIOR_INTERIOR/WPC_madera_oscuro.png", "url_preview": "/seg/texture-preview/Texture_WPC_EXTERIOR_INTERIOR/WPC_madera_oscuro.png", "dimensiones": ["2.90x0.25"]}, + {"id": "WPC_madera_claro", "nombre": "WPC Madera Claro", "textura": "Texture_WPC_EXTERIOR_INTERIOR/WPC_madera_claro.png", "url_preview": "/seg/texture-preview/Texture_WPC_EXTERIOR_INTERIOR/WPC_madera_claro.png", "dimensiones": ["2.90x0.25"]}, + {"id": "WPC_madera_gris", "nombre": "WPC Madera Gris", "textura": "Texture_WPC_EXTERIOR_INTERIOR/WPC_madera_gris.png", "url_preview": "/seg/texture-preview/Texture_WPC_EXTERIOR_INTERIOR/WPC_madera_gris.png", "dimensiones": ["2.90x0.25"]}, + {"id": "WPC_negro", "nombre": "WPC Negro", "textura": "Texture_WPC_EXTERIOR_INTERIOR/WPC_negro.png", "url_preview": "/seg/texture-preview/Texture_WPC_EXTERIOR_INTERIOR/WPC_negro.png", "dimensiones": ["2.90x0.25"]}, + ], + "created_at": "2026-05-07T00:00:00Z", + }, + { + "_id": "wpc_deck", + "nombre": "WPC Deck", + "tipo": "suelos", + "descripcion": "Deck de WPC para exteriores e interiores. Ideal para terrazas, jardines, bordes de piscina y espacios al aire libre.", + "especificaciones": [ + "Resistencia a la intemperie.", + "Bajo mantenimiento.", + "Respetuosa con el medio ambiente.", + "Esteticamente agradable.", + ], + "url_detalle": "https://heyzine.com/flip-book/447fe3eb8e.html#page/38", + "productos": [ + {"id": "DECK_madera", "nombre": "Deck Madera", "textura": "Texture_wpc_deck/DECK_madera.png", "url_preview": "/seg/texture-preview/Texture_wpc_deck/DECK_madera.png", "dimensiones": ["2.90x0.14"]}, + {"id": "DECK_madera_oscuro", "nombre": "Deck Madera Oscuro", "textura": "Texture_wpc_deck/DECK_madera_oscuro.png", "url_preview": "/seg/texture-preview/Texture_wpc_deck/DECK_madera_oscuro.png", "dimensiones": ["2.90x0.14"]}, + {"id": "DECK_gris", "nombre": "Deck Gris", "textura": "Texture_wpc_deck/DECK_gris.png", "url_preview": "/seg/texture-preview/Texture_wpc_deck/DECK_gris.png", "dimensiones": ["2.90x0.14"]}, + ], + "created_at": "2026-05-08T00:00:00Z", + }, +] + + +async def seed_catalog() -> None: + col = _get_col() + count = await col.count_documents({}) + if count == 0: + await col.insert_many(_SEED) + return + + # Migrar documentos existentes: añadir campos que falten según _SEED + for seed_item in _SEED: + doc = await col.find_one({"_id": seed_item["_id"]}) + if not doc: + continue + patch = {k: v for k, v in seed_item.items() if k not in doc and k != "_id"} + if patch: + await col.update_one({"_id": seed_item["_id"]}, {"$set": patch}) + + +def _serialize(doc: dict) -> dict: + out = dict(doc) + out["id"] = str(out.pop("_id")) + return out + + +def _seed_as_response() -> list[dict]: + return [{**{k: v for k, v in item.items() if k != "_id"}, "id": item["_id"]} for item in _SEED] + + +# ── Endpoints de lectura ────────────────────────────────────────────────────── + +@router.get("/textures") +async def get_texture_catalog() -> JSONResponse: + try: + col = _get_col() + docs = await col.find({}).to_list(length=200) + if docs: + return JSONResponse(content={"categories": [_serialize(d) for d in docs]}) + except Exception: + pass + # Fallback a datos estáticos si MongoDB no está disponible o la colección está vacía + return JSONResponse(content={"categories": _seed_as_response()}) + + +@router.get("/textures/{category_id}") +async def get_texture_category(category_id: str) -> JSONResponse: + try: + col = _get_col() + doc = await col.find_one({"_id": category_id}) + if doc: + return JSONResponse(content=_serialize(doc)) + except Exception: + pass + fallback = next((item for item in _SEED if item["_id"] == category_id), None) + if fallback: + return JSONResponse(content=_seed_as_response()[_SEED.index(fallback)]) + return JSONResponse(content={"detail": f"Categoria '{category_id}' no encontrada"}, status_code=404) + + +# ── Modelos ─────────────────────────────────────────────────────────────────── + +class ProductoItem(BaseModel): + id: str + nombre: str + textura: str + url_preview: str + dimensiones: list[str] = [] + + +class CategoriaBody(BaseModel): + id: str + nombre: str + tipo: str = "paredes" + descripcion: str = "" + especificaciones: list[str] = [] + url_detalle: str = "" + productos: list[ProductoItem] = [] + + +# ── Endpoints de escritura ──────────────────────────────────────────────────── + +@router.post("/category") +async def add_category(body: CategoriaBody) -> JSONResponse: + col = _get_col() + existing = await col.find_one({"_id": body.id}) + if existing: + return JSONResponse(content={"error": f"Categoria '{body.id}' ya existe"}, status_code=409) + doc = body.model_dump() + doc["_id"] = doc.pop("id") + doc["created_at"] = datetime.utcnow().isoformat() + "Z" + await col.insert_one(doc) + return JSONResponse(content={"ok": True, "id": body.id}, status_code=201) + + +@router.put("/category/{category_id}") +async def update_category(category_id: str, body: CategoriaBody) -> JSONResponse: + col = _get_col() + doc = body.model_dump() + doc.pop("id", None) + doc["updated_at"] = datetime.utcnow().isoformat() + "Z" + result = await col.update_one({"_id": category_id}, {"$set": doc}) + if result.matched_count == 0: + return JSONResponse(content={"error": "Categoria no encontrada"}, status_code=404) + return JSONResponse(content={"ok": True}) + + +@router.delete("/category/{category_id}") +async def delete_category(category_id: str) -> JSONResponse: + col = _get_col() + result = await col.delete_one({"_id": category_id}) + if result.deleted_count == 0: + return JSONResponse(content={"error": "Categoria no encontrada"}, status_code=404) + return JSONResponse(content={"ok": True, "deleted": category_id}) + + +@router.post("/category/{category_id}/product") +async def add_product(category_id: str, product: ProductoItem) -> JSONResponse: + col = _get_col() + result = await col.update_one( + {"_id": category_id, "productos.id": {"$ne": product.id}}, + {"$push": {"productos": product.model_dump()}, "$set": {"updated_at": datetime.utcnow().isoformat() + "Z"}}, + ) + if result.matched_count == 0: + return JSONResponse(content={"error": "Categoria no encontrada o producto duplicado"}, status_code=409) + return JSONResponse(content={"ok": True}, status_code=201) + + +@router.delete("/category/{category_id}/product/{product_id}") +async def delete_product(category_id: str, product_id: str) -> JSONResponse: + col = _get_col() + result = await col.update_one( + {"_id": category_id}, + {"$pull": {"productos": {"id": product_id}}, "$set": {"updated_at": datetime.utcnow().isoformat() + "Z"}}, + ) + if result.matched_count == 0: + return JSONResponse(content={"error": "Categoria no encontrada"}, status_code=404) + return JSONResponse(content={"ok": True}) diff --git a/backend/routers/openai_image.py b/backend/routers/openai_image.py new file mode 100644 index 0000000000000000000000000000000000000000..1dc34c39ec282c650036cbfca92ad49ba954c95c --- /dev/null +++ b/backend/routers/openai_image.py @@ -0,0 +1,32 @@ +from fastapi import APIRouter, File, Form, HTTPException, UploadFile +from fastapi.responses import JSONResponse +from PIL import Image +import io + +from services.openai_service import generate_image_with_openai + +router = APIRouter() + + +@router.post("/api/generate-image") +async def generate_image_endpoint( + file: UploadFile = File(...), + texture: str = Form(...), + api_key: str | None = Form(None), + preserve: int | None = Form(0), +): + if not file.content_type or not file.content_type.startswith("image/"): + raise HTTPException(status_code=400, detail="El archivo debe ser una imagen") + + try: + contents = await file.read() + pil = Image.open(io.BytesIO(contents)).convert("RGB") + except Exception: + raise HTTPException(status_code=400, detail="No se pudo leer la imagen subida") + + png_bytes, msg = generate_image_with_openai(api_key, pil, texture, preserve) + if png_bytes is None: + raise HTTPException(status_code=500, detail=msg) + + b64 = __import__("base64").b64encode(png_bytes).decode("ascii") + return JSONResponse(content={"result_b64": b64, "message": msg}) diff --git a/backend/routers/segmentation.py b/backend/routers/segmentation.py index b36dbba70bc7ac99405b8d4b9e485a4e63aa9f4d..11b5612b2d7355abf7dcd9348b6677f3a227fe98 100644 --- a/backend/routers/segmentation.py +++ b/backend/routers/segmentation.py @@ -1,723 +1,760 @@ -""" -Segmentation router - todos los endpoints del editor de texturas con SAM2. -Prefijo: /seg -""" -import asyncio -import uuid -from datetime import datetime, timezone -from pathlib import Path -from typing import Any, cast - -from fastapi import APIRouter, BackgroundTasks, File, HTTPException, UploadFile -from fastapi.responses import FileResponse, HTMLResponse, Response - -from core.config import ( - FRONTEND_DEBUG, - OUTPUT_DIR, - SD_JOB_STALE_SECONDS, - SD_QUICK_TIMEOUT_SECONDS, - UPLOAD_DIR, - UPLOAD_JOB_STALE_SECONDS, - VIDEO_OUTPUT_DIR, - VIDEO_UPLOAD_DIR, - load_classic_dashboard_html, - log_timing_end, - log_timing_start, - logger, - utc_now_iso, -) -from pydantic import BaseModel - -from models.schemas import ( - ApplyColorRequest, - ApplyTextureAIRequest, - ApplyTextureRequest, - ExteriorBrickRequest, - ExteriorDepthRequest, - ExteriorGrabCutRequest, - ExteriorHybridRequest, - ExteriorSuggestRequest, - GuidedSegmentRequest, - SceneAnalyzeRequest, - SegmentAdaptiveRequest, - SegmentVideoRequest, -) -from services.image_service import ( - prepare_and_store_upload, - run_upload_job, - save_label_map_for_owner, -) -from services.inpainting_service import run_inpainting_job, run_inpainting_sync -from services.sam2_service import jobs, jobs_lock, release_resources -from services.scene_service import ( - build_adaptive_plan, - generate_label_map, - infer_scene_type, - normalize_priority, - normalize_scene_hint, - rank_exterior_candidates, - rank_interior_candidates, -) -from services.segmentation_service import ( - generate_guided_label_map, - parse_mask_index, - parse_rgb_color, - segment_exterior_brick_sync, - segment_exterior_depth_sync, - segment_exterior_grabcut_sync, - segment_exterior_hybrid_sync, - segment_video_sync, -) -from services.texture_service import ( - apply_local_texture_sync, - build_texture_preview_jpeg, - generate_texture_variations, - list_available_textures, - resolve_texture_path, -) - -import cv2 - -router = APIRouter(prefix="/seg") - - -@router.get("/", response_class=HTMLResponse) -async def home() -> HTMLResponse: - dashboard_html = load_classic_dashboard_html().replace( - "__FRONTEND_DEBUG_ENABLED__", - "true" if FRONTEND_DEBUG else "false", - ) - return HTMLResponse(content=dashboard_html) - - -@router.post("/upload_video") -async def upload_video(file: UploadFile = File(...)) -> dict[str, Any]: - if not file.content_type or not file.content_type.startswith("video/"): - raise HTTPException(status_code=400, detail="Only video files are allowed") - - safe_name = Path(file.filename or "uploaded_video").name - if not safe_name: - raise HTTPException(status_code=400, detail="Invalid filename") - - destination = VIDEO_UPLOAD_DIR / safe_name - content = await file.read() - if not content: - raise HTTPException(status_code=400, detail="Uploaded video is empty") - - destination.write_bytes(content) - return { - "message": "Video uploaded successfully", - "filename": safe_name, - "url": f"/seg/video/{safe_name}", - } - - -@router.post("/upload_async") -async def upload_image_async( - background_tasks: BackgroundTasks, - file: UploadFile = File(...), -) -> dict[str, Any]: - if not file.content_type or not file.content_type.startswith("image/"): - raise HTTPException(status_code=400, detail="Only image files are allowed") - - content = await file.read() - job_id = uuid.uuid4().hex - with jobs_lock: - jobs[job_id] = { - "kind": "upload", - "status": "processing", - "stage": "queued", - "progress": 2, - "message": "Queued for segmentation", - "created_at": utc_now_iso(), - "updated_at": utc_now_iso(), - } - - background_tasks.add_task(run_upload_job, job_id, content, file.filename or "uploaded_image") - return { - "processing": True, - "job_id": job_id, - "status": "processing", - "stage": "queued", - "progress": 2, - "message": "Upload accepted. Segmentation started in background.", - "status_url": f"/seg/jobs/{job_id}", - } - - -@router.post("/segment_guided") -async def segment_guided(payload: GuidedSegmentRequest) -> dict[str, Any]: - started = log_timing_start("SEGMENT_GUIDED") - try: - from services.image_service import load_image_rgb_for_edit - safe_name, image_rgb = await asyncio.to_thread(load_image_rgb_for_edit, payload.filename) - label_map, ranked_scores = await asyncio.to_thread( - generate_guided_label_map, - image_rgb, - [list(point) for point in payload.point_coords], - list(payload.point_labels), - list(payload.box_xyxy) if payload.box_xyxy is not None else [], - payload.multimask_output, - ) - - guided_owner = f"{Path(safe_name).stem}_guided.jpg" - label_owner = await asyncio.to_thread(save_label_map_for_owner, guided_owner, label_map) - available_indices = list(range(1, len(ranked_scores) + 1)) - - return { - "message": "Guided segmentation completed", - "filename": safe_name, - "original_filename_for_apply": label_owner, - "mask_count": len(ranked_scores), - "available_mask_indices": available_indices, - "recommended_mask_index": 1, - "scores": [round(score, 6) for score in ranked_scores], - } - finally: - log_timing_end("SEGMENT_GUIDED", started) - try: - release_resources() - except Exception: - logger.exception("Error releasing resources after SEGMENT_GUIDED") - - -@router.post("/suggest_exterior_masks") -async def suggest_exterior_masks(payload: ExteriorSuggestRequest) -> dict[str, Any]: - started = log_timing_start("EXTERIOR_SUGGEST") - try: - from services.image_service import load_image_rgb_for_edit - safe_name, image_rgb = await asyncio.to_thread(load_image_rgb_for_edit, payload.filename) - - label_owner_name = Path(payload.original_filename).name if payload.original_filename else safe_name - - masks_dir = UPLOAD_DIR / "masks" - label_path = masks_dir / f"{label_owner_name}_labels.png" - if not label_path.exists(): - label_map, _ = await asyncio.to_thread(generate_label_map, image_rgb) - label_owner_name = await asyncio.to_thread(save_label_map_for_owner, label_owner_name, label_map) - label_path = masks_dir / f"{label_owner_name}_labels.png" - - label_map_arr = cv2.imread(str(label_path), cv2.IMREAD_GRAYSCALE) - if label_map_arr is None: - raise HTTPException(status_code=404, detail="Label map not found") - - candidates = rank_exterior_candidates( - label_map_arr, - payload.top_k, - target=payload.target, - min_area_ratio=payload.min_area_ratio, - max_area_ratio=payload.max_area_ratio, - ) - - return { - "message": "Exterior mask suggestions generated", - "filename": safe_name, - "original_filename_for_apply": label_owner_name, - "suggestions": candidates, - "target": payload.target, - } - finally: - log_timing_end("EXTERIOR_SUGGEST", started) - try: - release_resources() - except Exception: - logger.exception("Error releasing resources after EXTERIOR_SUGGEST") - - -@router.post("/analyze_scene") -async def analyze_scene(payload: SceneAnalyzeRequest) -> dict[str, Any]: - started = log_timing_start("ANALYZE_SCENE") - try: - from services.image_service import load_image_rgb_for_edit - safe_name, image_rgb = await asyncio.to_thread(load_image_rgb_for_edit, payload.filename) - - label_owner_name = Path(payload.original_filename).name if payload.original_filename else safe_name - masks_dir = UPLOAD_DIR / "masks" - label_path = masks_dir / f"{label_owner_name}_labels.png" - - if not label_path.exists(): - label_map, _ = await asyncio.to_thread(generate_label_map, image_rgb) - label_owner_name = await asyncio.to_thread(save_label_map_for_owner, label_owner_name, label_map) - - scene_info = await asyncio.to_thread( - infer_scene_type, - image_rgb, - payload.semantic_keywords, - payload.exterior_target, - payload.min_area_ratio, - payload.max_area_ratio, - ) - scene_type = scene_info["scene_type"] - scene_hint = normalize_scene_hint(payload.scene_hint) - effective_scene = scene_hint if scene_hint != "auto" else scene_type - - adaptive_plan = build_adaptive_plan(effective_scene, payload.priority, payload.exterior_target) - - label_map_arr = cv2.imread(str(masks_dir / f"{label_owner_name}_labels.png"), cv2.IMREAD_GRAYSCALE) - suggestions: list[dict[str, Any]] = [] - if label_map_arr is not None: - if effective_scene == "exterior": - suggestions = rank_exterior_candidates( - label_map_arr, payload.top_k, - target=payload.exterior_target, - min_area_ratio=payload.min_area_ratio, - max_area_ratio=payload.max_area_ratio, - ) - else: - suggestions = rank_interior_candidates(label_map_arr, payload.top_k) - - return { - "message": "Scene analysis completed", - "filename": safe_name, - "original_filename_for_apply": label_owner_name, - "scene_type": scene_type, - "effective_scene": effective_scene, - "confidence": scene_info["confidence"], - "signals": scene_info["signals"], - "adaptive_plan": adaptive_plan, - "suggestions": suggestions, - "priority": normalize_priority(payload.priority), - } - finally: - log_timing_end("ANALYZE_SCENE", started) - try: - release_resources() - except Exception: - logger.exception("Error releasing resources after ANALYZE_SCENE") - - -@router.post("/segment_adaptive") -async def segment_adaptive(payload: SegmentAdaptiveRequest) -> dict[str, Any]: - started = log_timing_start("SEGMENT_ADAPTIVE") - try: - from services.image_service import load_image_rgb_for_edit - safe_name, image_rgb = await asyncio.to_thread(load_image_rgb_for_edit, payload.filename) - - scene_info = await asyncio.to_thread( - infer_scene_type, - image_rgb, - payload.semantic_keywords, - payload.exterior_target, - ) - scene_hint = normalize_scene_hint(payload.scene_hint) - effective_scene = scene_hint if scene_hint != "auto" else scene_info["scene_type"] - priority = normalize_priority(payload.priority) - adaptive_plan = build_adaptive_plan(effective_scene, priority, payload.exterior_target) - - label_owner_name = Path(payload.original_filename).name if payload.original_filename else safe_name - - if effective_scene == "exterior": - from services.segmentation_service import segment_exterior_depth_sync as seg_depth - from models.schemas import ExteriorDepthRequest as DepthReq - - depth_payload = DepthReq( - filename=payload.filename, - exterior_target=payload.exterior_target, - rect_xywh=payload.rect_xywh, - smooth_strength=1, - sam2_merge_top_k=12, - iterations=6, - use_semantic_hint=True, - use_depth_hint=True, - semantic_keywords=payload.semantic_keywords, - ) - result = await asyncio.to_thread(seg_depth, depth_payload) - else: - label_map, _ = await asyncio.to_thread(generate_label_map, image_rgb) - label_owner_name = await asyncio.to_thread(save_label_map_for_owner, label_owner_name, label_map) - top_k = 4 if priority == "speed" else (10 if priority == "quality" else 6) - candidates = rank_interior_candidates(label_map, top_k) - result = { - "message": "Interior adaptive segmentation completed", - "filename": safe_name, - "original_filename_for_apply": label_owner_name, - "scene_type": effective_scene, - "suggestions": candidates, - } - - result["adaptive_plan"] = adaptive_plan - result["detected_scene_type"] = scene_info["scene_type"] - result["effective_scene"] = effective_scene - result["scene_confidence"] = scene_info["confidence"] - return result - finally: - log_timing_end("SEGMENT_ADAPTIVE", started) - try: - release_resources() - except Exception: - logger.exception("Error releasing resources after SEGMENT_ADAPTIVE") - - -@router.post("/segment_video") -async def segment_video(payload: SegmentVideoRequest) -> dict[str, Any]: - try: - return await asyncio.to_thread(segment_video_sync, payload) - except HTTPException: - raise - except Exception as exc: - raise HTTPException(status_code=500, detail=f"Video segmentation failed: {exc}") from exc - - -@router.post("/segment_exterior_grabcut") -async def segment_exterior_grabcut(payload: ExteriorGrabCutRequest) -> dict[str, Any]: - try: - return await asyncio.to_thread(segment_exterior_grabcut_sync, payload) - except HTTPException: - raise - except Exception as exc: - raise HTTPException(status_code=500, detail=f"GrabCut segmentation failed: {exc}") from exc - - -@router.post("/segment_exterior_hybrid") -async def segment_exterior_hybrid(payload: ExteriorHybridRequest) -> dict[str, Any]: - try: - return await asyncio.to_thread(segment_exterior_hybrid_sync, payload) - except HTTPException: - raise - except Exception as exc: - raise HTTPException(status_code=500, detail=f"Hybrid exterior segmentation failed: {exc}") from exc - - -@router.post("/segment_exterior_brick") -async def segment_exterior_brick(payload: ExteriorBrickRequest) -> dict[str, Any]: - try: - return await asyncio.to_thread(segment_exterior_brick_sync, payload) - except HTTPException: - raise - except Exception as exc: - raise HTTPException(status_code=500, detail=f"Brick segmentation failed: {exc}") from exc - - -@router.post("/segment_exterior_depth") -async def segment_exterior_depth(payload: ExteriorDepthRequest) -> dict[str, Any]: - try: - return await asyncio.to_thread(segment_exterior_depth_sync, payload) - except HTTPException: - raise - except Exception as exc: - raise HTTPException(status_code=500, detail=f"Depth exterior segmentation failed: {exc}") from exc - - -@router.post("/apply_texture_ai") -async def apply_texture_ai( - payload: ApplyTextureAIRequest, - background_tasks: BackgroundTasks, -) -> dict[str, Any]: - started = log_timing_start("APPLY_TEXTURE_AI") - try: - result = await asyncio.wait_for( - asyncio.to_thread(run_inpainting_sync, payload), - timeout=SD_QUICK_TIMEOUT_SECONDS, - ) - log_timing_end("APPLY_TEXTURE_AI", started) - try: - release_resources() - except Exception: - logger.exception("Error releasing resources after APPLY_TEXTURE_AI") - result["processing"] = False - return result - except asyncio.TimeoutError: - job_id = uuid.uuid4().hex - with jobs_lock: - jobs[job_id] = {"status": "processing", "created_at": utc_now_iso(), "updated_at": utc_now_iso()} - background_tasks.add_task(run_inpainting_job, job_id, payload) - log_timing_end("APPLY_TEXTURE_AI", started) - try: - release_resources() - except Exception: - pass - return { - "processing": True, - "job_id": job_id, - "message": "Inpainting is taking longer than expected and continues in background.", - "status_url": f"/seg/jobs/{job_id}", - } - except HTTPException: - log_timing_end("APPLY_TEXTURE_AI", started) - try: - release_resources() - except Exception: - pass - raise - except Exception as exc: - log_timing_end("APPLY_TEXTURE_AI", started) - try: - release_resources() - except Exception: - pass - raise HTTPException(status_code=500, detail=f"Inpainting failed: {exc}") from exc - - -@router.get("/jobs/{job_id}") -async def get_job_status(job_id: str) -> dict[str, Any]: - with jobs_lock: - job = jobs.get(job_id) - - if job is None: - raise HTTPException(status_code=404, detail="Job not found") - - if job.get("status") == "processing": - kind = str(job.get("kind", "generic")) - stage = str(job.get("stage", "processing")) - progress = int(job.get("progress", 0) or 0) - eta_seconds: int | None = None - - if kind == "upload" and stage == "segmenting_with_sam2": - stage_started_at_text = job.get("stage_started_at") - estimated_seconds = float(job.get("estimated_seconds", 0.0) or 0.0) - if stage_started_at_text and estimated_seconds > 0: - try: - stage_started_at = datetime.fromisoformat(str(stage_started_at_text)) - elapsed = (datetime.now(timezone.utc) - stage_started_at).total_seconds() - eta_seconds = max(0, int(estimated_seconds - elapsed)) - estimated_progress = int(min(95, 30 + (max(0.0, elapsed) / estimated_seconds) * 60)) - progress = max(progress, estimated_progress) - except ValueError: - pass - - stale_limit_seconds = UPLOAD_JOB_STALE_SECONDS if kind == "upload" else SD_JOB_STALE_SECONDS - created_at_text = job.get("created_at") - if created_at_text: - try: - created_at = datetime.fromisoformat(str(created_at_text)) - age_seconds = (datetime.now(timezone.utc) - created_at).total_seconds() - if age_seconds > stale_limit_seconds: - return { - "processing": False, - "status": "timeout", - "message": "The process is taking too long. Please retry.", - "job_id": job_id, - } - except ValueError: - pass - - response: dict[str, Any] = { - "processing": True, - "status": "processing", - "job_id": job_id, - "kind": kind, - "stage": stage, - "progress": progress, - "message": str(job.get("message", "Still processing.")), - } - if eta_seconds is not None: - response["eta_seconds"] = eta_seconds - return response - - if job.get("status") == "done": - result = cast(dict[str, Any], job.get("result", {})) - result["processing"] = False - result["job_id"] = job_id - result["status"] = "done" - return result - - if job.get("status") == "failed": - return { - "processing": False, - "status": "failed", - "job_id": job_id, - "message": job.get("error", "Background task failed"), - } - - return {"processing": True, "status": "processing", "job_id": job_id, "message": "Still processing."} - - -@router.post("/apply_color") -async def apply_color(payload: ApplyColorRequest) -> dict[str, Any]: - started = log_timing_start("APPLY_COLOR") - try: - safe_name = Path(payload.filename).name - if not safe_name: - raise HTTPException(status_code=400, detail="Invalid filename") - - label_safe_name = Path(payload.original_filename).name if payload.original_filename else safe_name - - image_path = UPLOAD_DIR / safe_name - if not image_path.exists(): - image_path = OUTPUT_DIR / safe_name - if not image_path.exists() or not image_path.is_file(): - raise HTTPException(status_code=404, detail=f"Image not found: {safe_name}") - - image_bgr = cv2.imread(str(image_path)) - if image_bgr is None: - raise HTTPException(status_code=400, detail="Image could not be read") - - mask_index = parse_mask_index(payload.mask_filename) - red, green, blue = parse_rgb_color(payload.color) - - label_path = UPLOAD_DIR / "masks" / f"{label_safe_name}_labels.png" - label_map = cv2.imread(str(label_path), cv2.IMREAD_GRAYSCALE) - if label_map is None: - raise HTTPException( - status_code=404, - detail="Label map not found. Upload the image first to generate segments.", - ) - - segmentation = label_map == mask_index - if not segmentation.any(): - raise HTTPException(status_code=400, detail=f"Segment index {mask_index} not found in label map.") - - edited_image = image_bgr.copy() - edited_image[segmentation] = (blue, green, red) - - original_stem = Path(label_safe_name).stem - out_filename = f"{original_stem}_edit.jpg" - out_path = UPLOAD_DIR / out_filename - if not cv2.imwrite(str(out_path), edited_image): - raise HTTPException(status_code=500, detail="Failed to save edited image") - - return { - "message": "Color applied successfully", - "output_filename": out_filename, - "output_url": f"/seg/image/{out_filename}", - } - finally: - log_timing_end("APPLY_COLOR", started) - try: - release_resources() - except Exception: - logger.exception("Error releasing resources after APPLY_COLOR") - - -@router.post("/apply_texture") -async def apply_texture(payload: ApplyTextureRequest) -> dict[str, Any]: - try: - result = await asyncio.to_thread(apply_local_texture_sync, payload) - result["processing"] = False - return result - except HTTPException: - raise - except Exception as exc: - raise HTTPException(status_code=500, detail=f"Texture apply failed: {exc}") from exc - - -@router.get("/textures") -async def get_textures() -> dict[str, Any]: - return {"textures": list_available_textures()} - - -class _GenerateVariationsRequest(BaseModel): - texture_name: str - - class Config: - extra = "ignore" - - -@router.post("/textures/generate") -async def generate_variations(payload: _GenerateVariationsRequest) -> dict[str, Any]: - if not payload.texture_name: - raise HTTPException(status_code=400, detail="texture_name is required") - try: - variations = await asyncio.to_thread(generate_texture_variations, payload.texture_name) - return {"variations": variations} - except HTTPException: - raise - except Exception as exc: - raise HTTPException(status_code=500, detail=f"Variation generation failed: {exc}") from exc - - -@router.get("/texture-preview/{filename:path}") -async def get_texture_preview(filename: str) -> Response: - texture_path = resolve_texture_path(filename) - jpeg = await asyncio.to_thread(build_texture_preview_jpeg, texture_path) - return Response(content=jpeg, media_type="image/jpeg", headers={"Cache-Control": "public, max-age=3600"}) - - -@router.get("/video/{filename}") -async def get_video(filename: str) -> FileResponse: - if Path(filename).name != filename: - raise HTTPException(status_code=400, detail="Invalid file name") - video_path = VIDEO_UPLOAD_DIR / filename - if not video_path.exists() or not video_path.is_file(): - raise HTTPException(status_code=404, detail="Video not found") - return FileResponse(video_path) - - -@router.get("/output-video/{filename}") -async def get_output_video(filename: str) -> FileResponse: - if Path(filename).name != filename: - raise HTTPException(status_code=400, detail="Invalid file name") - video_path = VIDEO_OUTPUT_DIR / filename - if not video_path.exists() or not video_path.is_file(): - raise HTTPException(status_code=404, detail="Output video not found") - return FileResponse(video_path) - - -@router.get("/image/{filename}") -async def get_image(filename: str) -> FileResponse: - if Path(filename).name != filename: - raise HTTPException(status_code=400, detail="Invalid file name") - image_path = UPLOAD_DIR / filename - if not image_path.exists() or not image_path.is_file(): - raise HTTPException(status_code=404, detail="Image not found") - return FileResponse(image_path) - - -@router.post("/masks/reclassify/{filename}") -async def reclassify_mask_metadata(filename: str) -> dict[str, Any]: - """Re-run semantic classification on an already-segmented image and overwrite its metadata JSON.""" - import json as _json - safe = Path(filename).name - if not safe: - raise HTTPException(status_code=400, detail="Invalid filename") - - masks_dir = UPLOAD_DIR / "masks" - label_path = masks_dir / f"{safe}_labels.png" - if not label_path.exists(): - raise HTTPException(status_code=404, detail="Label map not found — upload the image first") - - label_map = cv2.imread(str(label_path), cv2.IMREAD_GRAYSCALE) - if label_map is None: - raise HTTPException(status_code=500, detail="Could not read label map") - - image_path = UPLOAD_DIR / safe - image_rgb: Any = None - if image_path.exists(): - img_bgr = cv2.imread(str(image_path)) - if img_bgr is not None: - image_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) - - from services.scene_service import classify_all_label_map_segments - h, w = label_map.shape[:2] - segments_meta = await asyncio.to_thread( - classify_all_label_map_segments, label_map, w, h, image_rgb - ) - meta_path = masks_dir / f"{safe}_labels_meta.json" - meta_path.write_text(_json.dumps({"segments": segments_meta}, ensure_ascii=False), encoding="utf-8") - - return {"segments": segments_meta, "count": len(segments_meta)} - - -@router.get("/masks/meta/{filename}") -async def get_mask_metadata(filename: str) -> dict: - import json as _json - safe = Path(filename).name - if not safe: - raise HTTPException(status_code=400, detail="Invalid filename") - meta_path = UPLOAD_DIR / "masks" / f"{safe}_labels_meta.json" - if not meta_path.exists() or not meta_path.is_file(): - raise HTTPException(status_code=404, detail="Segment metadata not found") - try: - return _json.loads(meta_path.read_text(encoding="utf-8")) - except Exception as exc: - raise HTTPException(status_code=500, detail=f"Failed to read metadata: {exc}") from exc - - -@router.get("/masks/{filename}") -async def get_mask_labels(filename: str) -> FileResponse: - if Path(filename).name != filename: - raise HTTPException(status_code=400, detail="Invalid file name") - label_path = UPLOAD_DIR / "masks" / f"{filename}_labels.png" - if not label_path.exists() or not label_path.is_file(): - raise HTTPException(status_code=404, detail="Label map not found") - return FileResponse(label_path) - - -@router.get("/ai/{filename}") -async def get_ai_image(filename: str) -> FileResponse: - if Path(filename).name != filename: - raise HTTPException(status_code=400, detail="Invalid file name") - out_path = OUTPUT_DIR / filename - if not out_path.exists() or not out_path.is_file(): - raise HTTPException(status_code=404, detail="AI output image not found") - return FileResponse(out_path) +""" +Segmentation router - todos los endpoints del editor de texturas con SAM2. +Prefijo: /seg +""" +import asyncio +import uuid +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, cast + +from fastapi import APIRouter, BackgroundTasks, File, HTTPException, UploadFile +from fastapi.responses import FileResponse, HTMLResponse, Response + +from core.config import ( + FRONTEND_DEBUG, + OUTPUT_DIR, + SD_JOB_STALE_SECONDS, + SD_QUICK_TIMEOUT_SECONDS, + UPLOAD_DIR, + UPLOAD_JOB_STALE_SECONDS, + VIDEO_OUTPUT_DIR, + VIDEO_UPLOAD_DIR, + load_classic_dashboard_html, + log_timing_end, + log_timing_start, + logger, + utc_now_iso, +) +from pydantic import BaseModel + +from models.schemas import ( + ApplyColorRequest, + ApplyTextureAIRequest, + ApplyTextureRequest, + ExteriorBrickRequest, + ExteriorDepthRequest, + ExteriorGrabCutRequest, + ExteriorHybridRequest, + ExteriorSuggestRequest, + GuidedSegmentRequest, + SceneAnalyzeRequest, + SegmentAdaptiveRequest, + SegmentVideoRequest, +) +from services.image_service import ( + prepare_and_store_upload, + run_upload_job, + save_label_map_for_owner, +) +from services.inpainting_service import run_inpainting_job, run_inpainting_sync +from services.sam2_service import jobs, jobs_lock, release_resources +from services.openai_service import generate_image_with_openai +from PIL import Image +from services.scene_service import ( + build_adaptive_plan, + generate_label_map, + infer_scene_type, + normalize_priority, + normalize_scene_hint, + rank_exterior_candidates, + rank_interior_candidates, +) +from services.segmentation_service import ( + generate_guided_label_map, + parse_mask_index, + parse_rgb_color, + segment_exterior_brick_sync, + segment_exterior_depth_sync, + segment_exterior_grabcut_sync, + segment_exterior_hybrid_sync, + segment_video_sync, +) +from services.texture_service import ( + apply_local_texture_sync, + build_texture_preview_jpeg, + generate_texture_variations, + list_available_textures, + resolve_texture_path, +) + +import cv2 + +router = APIRouter(prefix="/seg") + + +@router.get("/", response_class=HTMLResponse) +async def home() -> HTMLResponse: + dashboard_html = load_classic_dashboard_html().replace( + "__FRONTEND_DEBUG_ENABLED__", + "true" if FRONTEND_DEBUG else "false", + ) + return HTMLResponse(content=dashboard_html) + + +@router.post("/upload_video") +async def upload_video(file: UploadFile = File(...)) -> dict[str, Any]: + if not file.content_type or not file.content_type.startswith("video/"): + raise HTTPException(status_code=400, detail="Only video files are allowed") + + safe_name = Path(file.filename or "uploaded_video").name + if not safe_name: + raise HTTPException(status_code=400, detail="Invalid filename") + + destination = VIDEO_UPLOAD_DIR / safe_name + content = await file.read() + if not content: + raise HTTPException(status_code=400, detail="Uploaded video is empty") + + destination.write_bytes(content) + return { + "message": "Video uploaded successfully", + "filename": safe_name, + "url": f"/seg/video/{safe_name}", + } + + +@router.post("/upload_async") +async def upload_image_async( + background_tasks: BackgroundTasks, + file: UploadFile = File(...), +) -> dict[str, Any]: + if not file.content_type or not file.content_type.startswith("image/"): + raise HTTPException(status_code=400, detail="Only image files are allowed") + + content = await file.read() + job_id = uuid.uuid4().hex + with jobs_lock: + jobs[job_id] = { + "kind": "upload", + "status": "processing", + "stage": "queued", + "progress": 2, + "message": "Queued for segmentation", + "created_at": utc_now_iso(), + "updated_at": utc_now_iso(), + } + + background_tasks.add_task(run_upload_job, job_id, content, file.filename or "uploaded_image") + return { + "processing": True, + "job_id": job_id, + "status": "processing", + "stage": "queued", + "progress": 2, + "message": "Upload accepted. Segmentation started in background.", + "status_url": f"/seg/jobs/{job_id}", + } + + +@router.post("/segment_guided") +async def segment_guided(payload: GuidedSegmentRequest) -> dict[str, Any]: + started = log_timing_start("SEGMENT_GUIDED") + try: + from services.image_service import load_image_rgb_for_edit + safe_name, image_rgb = await asyncio.to_thread(load_image_rgb_for_edit, payload.filename) + label_map, ranked_scores = await asyncio.to_thread( + generate_guided_label_map, + image_rgb, + [list(point) for point in payload.point_coords], + list(payload.point_labels), + list(payload.box_xyxy) if payload.box_xyxy is not None else [], + payload.multimask_output, + ) + + guided_owner = f"{Path(safe_name).stem}_guided.jpg" + label_owner = await asyncio.to_thread(save_label_map_for_owner, guided_owner, label_map) + available_indices = list(range(1, len(ranked_scores) + 1)) + + return { + "message": "Guided segmentation completed", + "filename": safe_name, + "original_filename_for_apply": label_owner, + "mask_count": len(ranked_scores), + "available_mask_indices": available_indices, + "recommended_mask_index": 1, + "scores": [round(score, 6) for score in ranked_scores], + } + finally: + log_timing_end("SEGMENT_GUIDED", started) + try: + release_resources() + except Exception: + logger.exception("Error releasing resources after SEGMENT_GUIDED") + + +@router.post("/suggest_exterior_masks") +async def suggest_exterior_masks(payload: ExteriorSuggestRequest) -> dict[str, Any]: + started = log_timing_start("EXTERIOR_SUGGEST") + try: + from services.image_service import load_image_rgb_for_edit + safe_name, image_rgb = await asyncio.to_thread(load_image_rgb_for_edit, payload.filename) + + label_owner_name = Path(payload.original_filename).name if payload.original_filename else safe_name + + masks_dir = UPLOAD_DIR / "masks" + label_path = masks_dir / f"{label_owner_name}_labels.png" + if not label_path.exists(): + label_map, _ = await asyncio.to_thread(generate_label_map, image_rgb) + label_owner_name = await asyncio.to_thread(save_label_map_for_owner, label_owner_name, label_map) + label_path = masks_dir / f"{label_owner_name}_labels.png" + + label_map_arr = cv2.imread(str(label_path), cv2.IMREAD_GRAYSCALE) + if label_map_arr is None: + raise HTTPException(status_code=404, detail="Label map not found") + + candidates = rank_exterior_candidates( + label_map_arr, + payload.top_k, + target=payload.target, + min_area_ratio=payload.min_area_ratio, + max_area_ratio=payload.max_area_ratio, + ) + + return { + "message": "Exterior mask suggestions generated", + "filename": safe_name, + "original_filename_for_apply": label_owner_name, + "suggestions": candidates, + "target": payload.target, + } + finally: + log_timing_end("EXTERIOR_SUGGEST", started) + try: + release_resources() + except Exception: + logger.exception("Error releasing resources after EXTERIOR_SUGGEST") + + +@router.post("/analyze_scene") +async def analyze_scene(payload: SceneAnalyzeRequest) -> dict[str, Any]: + started = log_timing_start("ANALYZE_SCENE") + try: + from services.image_service import load_image_rgb_for_edit + safe_name, image_rgb = await asyncio.to_thread(load_image_rgb_for_edit, payload.filename) + + label_owner_name = Path(payload.original_filename).name if payload.original_filename else safe_name + masks_dir = UPLOAD_DIR / "masks" + label_path = masks_dir / f"{label_owner_name}_labels.png" + + if not label_path.exists(): + label_map, _ = await asyncio.to_thread(generate_label_map, image_rgb) + label_owner_name = await asyncio.to_thread(save_label_map_for_owner, label_owner_name, label_map) + + scene_info = await asyncio.to_thread( + infer_scene_type, + image_rgb, + payload.semantic_keywords, + payload.exterior_target, + payload.min_area_ratio, + payload.max_area_ratio, + ) + scene_type = scene_info["scene_type"] + scene_hint = normalize_scene_hint(payload.scene_hint) + effective_scene = scene_hint if scene_hint != "auto" else scene_type + + adaptive_plan = build_adaptive_plan(effective_scene, payload.priority, payload.exterior_target) + + label_map_arr = cv2.imread(str(masks_dir / f"{label_owner_name}_labels.png"), cv2.IMREAD_GRAYSCALE) + suggestions: list[dict[str, Any]] = [] + if label_map_arr is not None: + if effective_scene == "exterior": + suggestions = rank_exterior_candidates( + label_map_arr, payload.top_k, + target=payload.exterior_target, + min_area_ratio=payload.min_area_ratio, + max_area_ratio=payload.max_area_ratio, + ) + else: + suggestions = rank_interior_candidates(label_map_arr, payload.top_k) + + return { + "message": "Scene analysis completed", + "filename": safe_name, + "original_filename_for_apply": label_owner_name, + "scene_type": scene_type, + "effective_scene": effective_scene, + "confidence": scene_info["confidence"], + "signals": scene_info["signals"], + "adaptive_plan": adaptive_plan, + "suggestions": suggestions, + "priority": normalize_priority(payload.priority), + } + finally: + log_timing_end("ANALYZE_SCENE", started) + try: + release_resources() + except Exception: + logger.exception("Error releasing resources after ANALYZE_SCENE") + + +@router.post("/segment_adaptive") +async def segment_adaptive(payload: SegmentAdaptiveRequest) -> dict[str, Any]: + started = log_timing_start("SEGMENT_ADAPTIVE") + try: + from services.image_service import load_image_rgb_for_edit + safe_name, image_rgb = await asyncio.to_thread(load_image_rgb_for_edit, payload.filename) + + scene_info = await asyncio.to_thread( + infer_scene_type, + image_rgb, + payload.semantic_keywords, + payload.exterior_target, + ) + scene_hint = normalize_scene_hint(payload.scene_hint) + effective_scene = scene_hint if scene_hint != "auto" else scene_info["scene_type"] + priority = normalize_priority(payload.priority) + adaptive_plan = build_adaptive_plan(effective_scene, priority, payload.exterior_target) + + label_owner_name = Path(payload.original_filename).name if payload.original_filename else safe_name + + if effective_scene == "exterior": + from services.segmentation_service import segment_exterior_depth_sync as seg_depth + from models.schemas import ExteriorDepthRequest as DepthReq + + depth_payload = DepthReq( + filename=payload.filename, + exterior_target=payload.exterior_target, + rect_xywh=payload.rect_xywh, + smooth_strength=1, + sam2_merge_top_k=12, + iterations=6, + use_semantic_hint=True, + use_depth_hint=True, + semantic_keywords=payload.semantic_keywords, + ) + result = await asyncio.to_thread(seg_depth, depth_payload) + else: + label_map, _ = await asyncio.to_thread(generate_label_map, image_rgb) + label_owner_name = await asyncio.to_thread(save_label_map_for_owner, label_owner_name, label_map) + top_k = 4 if priority == "speed" else (10 if priority == "quality" else 6) + candidates = rank_interior_candidates(label_map, top_k) + result = { + "message": "Interior adaptive segmentation completed", + "filename": safe_name, + "original_filename_for_apply": label_owner_name, + "scene_type": effective_scene, + "suggestions": candidates, + } + + result["adaptive_plan"] = adaptive_plan + result["detected_scene_type"] = scene_info["scene_type"] + result["effective_scene"] = effective_scene + result["scene_confidence"] = scene_info["confidence"] + return result + finally: + log_timing_end("SEGMENT_ADAPTIVE", started) + try: + release_resources() + except Exception: + logger.exception("Error releasing resources after SEGMENT_ADAPTIVE") + + +@router.post("/segment_video") +async def segment_video(payload: SegmentVideoRequest) -> dict[str, Any]: + try: + return await asyncio.to_thread(segment_video_sync, payload) + except HTTPException: + raise + except Exception as exc: + raise HTTPException(status_code=500, detail=f"Video segmentation failed: {exc}") from exc + + +@router.post("/segment_exterior_grabcut") +async def segment_exterior_grabcut(payload: ExteriorGrabCutRequest) -> dict[str, Any]: + try: + return await asyncio.to_thread(segment_exterior_grabcut_sync, payload) + except HTTPException: + raise + except Exception as exc: + raise HTTPException(status_code=500, detail=f"GrabCut segmentation failed: {exc}") from exc + + +@router.post("/segment_exterior_hybrid") +async def segment_exterior_hybrid(payload: ExteriorHybridRequest) -> dict[str, Any]: + try: + return await asyncio.to_thread(segment_exterior_hybrid_sync, payload) + except HTTPException: + raise + except Exception as exc: + raise HTTPException(status_code=500, detail=f"Hybrid exterior segmentation failed: {exc}") from exc + + +@router.post("/segment_exterior_brick") +async def segment_exterior_brick(payload: ExteriorBrickRequest) -> dict[str, Any]: + try: + return await asyncio.to_thread(segment_exterior_brick_sync, payload) + except HTTPException: + raise + except Exception as exc: + raise HTTPException(status_code=500, detail=f"Brick segmentation failed: {exc}") from exc + + +@router.post("/segment_exterior_depth") +async def segment_exterior_depth(payload: ExteriorDepthRequest) -> dict[str, Any]: + try: + return await asyncio.to_thread(segment_exterior_depth_sync, payload) + except HTTPException: + raise + except Exception as exc: + raise HTTPException(status_code=500, detail=f"Depth exterior segmentation failed: {exc}") from exc + + +@router.post("/apply_texture_ai") +async def apply_texture_ai( + payload: ApplyTextureAIRequest, + background_tasks: BackgroundTasks, +) -> dict[str, Any]: + started = log_timing_start("APPLY_TEXTURE_AI") + try: + # Try to run OpenAI generation synchronously within the quick timeout + def _run_openai(): + safe_name = Path(payload.filename).name + image_path = UPLOAD_DIR / safe_name + if not image_path.exists(): + image_path = OUTPUT_DIR / safe_name + + if not image_path.exists(): + return {"error": f"Image not found: {payload.filename}"} + + try: + pil = Image.open(str(image_path)).convert("RGBA") + except Exception as e: + return {"error": f"Cannot open image: {e}"} + + texture = payload.texture_name or payload.prompt or "" + png_bytes, msg = generate_image_with_openai(None, pil, texture) + if png_bytes is None: + return {"error": msg} + + out_name = f"{Path(safe_name).stem}_ai_{uuid.uuid4().hex}.png" + out_path = OUTPUT_DIR / out_name + out_path.write_bytes(png_bytes) + return {"message": msg, "filename": out_name, "url": f"/seg/ai/{out_name}", "processing": False} + + result = await asyncio.wait_for(asyncio.to_thread(_run_openai), timeout=SD_QUICK_TIMEOUT_SECONDS) + log_timing_end("APPLY_TEXTURE_AI", started) + try: + release_resources() + except Exception: + logger.exception("Error releasing resources after APPLY_TEXTURE_AI") + result["processing"] = False + return result + except asyncio.TimeoutError: + job_id = uuid.uuid4().hex + with jobs_lock: + jobs[job_id] = {"status": "processing", "created_at": utc_now_iso(), "updated_at": utc_now_iso()} + # enqueue background job that runs the OpenAI generation + def _run_openai_job(job_id_inner: str, payload_inner: ApplyTextureAIRequest) -> None: + try: + res = _run_openai() + with jobs_lock: + if "error" in res: + jobs[job_id_inner] = {"status": "failed", "error": res.get("error"), "updated_at": utc_now_iso()} + else: + jobs[job_id_inner] = {"status": "done", "result": res, "updated_at": utc_now_iso()} + except Exception as exc: + with jobs_lock: + jobs[job_id_inner] = {"status": "failed", "error": str(exc), "updated_at": utc_now_iso()} + + background_tasks.add_task(_run_openai_job, job_id, payload) + log_timing_end("APPLY_TEXTURE_AI", started) + try: + release_resources() + except Exception: + pass + return { + "processing": True, + "job_id": job_id, + "message": "Inpainting is taking longer than expected and continues in background.", + "status_url": f"/seg/jobs/{job_id}", + } + except HTTPException: + log_timing_end("APPLY_TEXTURE_AI", started) + try: + release_resources() + except Exception: + pass + raise + except Exception as exc: + log_timing_end("APPLY_TEXTURE_AI", started) + try: + release_resources() + except Exception: + pass + raise HTTPException(status_code=500, detail=f"Inpainting failed: {exc}") from exc + + +@router.get("/jobs/{job_id}") +async def get_job_status(job_id: str) -> dict[str, Any]: + with jobs_lock: + job = jobs.get(job_id) + + if job is None: + raise HTTPException(status_code=404, detail="Job not found") + + if job.get("status") == "processing": + kind = str(job.get("kind", "generic")) + stage = str(job.get("stage", "processing")) + progress = int(job.get("progress", 0) or 0) + eta_seconds: int | None = None + + if kind == "upload" and stage == "segmenting_with_sam2": + stage_started_at_text = job.get("stage_started_at") + estimated_seconds = float(job.get("estimated_seconds", 0.0) or 0.0) + if stage_started_at_text and estimated_seconds > 0: + try: + stage_started_at = datetime.fromisoformat(str(stage_started_at_text)) + elapsed = (datetime.now(timezone.utc) - stage_started_at).total_seconds() + eta_seconds = max(0, int(estimated_seconds - elapsed)) + estimated_progress = int(min(95, 30 + (max(0.0, elapsed) / estimated_seconds) * 60)) + progress = max(progress, estimated_progress) + except ValueError: + pass + + stale_limit_seconds = UPLOAD_JOB_STALE_SECONDS if kind == "upload" else SD_JOB_STALE_SECONDS + created_at_text = job.get("created_at") + if created_at_text: + try: + created_at = datetime.fromisoformat(str(created_at_text)) + age_seconds = (datetime.now(timezone.utc) - created_at).total_seconds() + if age_seconds > stale_limit_seconds: + return { + "processing": False, + "status": "timeout", + "message": "The process is taking too long. Please retry.", + "job_id": job_id, + } + except ValueError: + pass + + response: dict[str, Any] = { + "processing": True, + "status": "processing", + "job_id": job_id, + "kind": kind, + "stage": stage, + "progress": progress, + "message": str(job.get("message", "Still processing.")), + } + if eta_seconds is not None: + response["eta_seconds"] = eta_seconds + return response + + if job.get("status") == "done": + result = cast(dict[str, Any], job.get("result", {})) + result["processing"] = False + result["job_id"] = job_id + result["status"] = "done" + return result + + if job.get("status") == "failed": + return { + "processing": False, + "status": "failed", + "job_id": job_id, + "message": job.get("error", "Background task failed"), + } + + return {"processing": True, "status": "processing", "job_id": job_id, "message": "Still processing."} + + +@router.post("/apply_color") +async def apply_color(payload: ApplyColorRequest) -> dict[str, Any]: + started = log_timing_start("APPLY_COLOR") + try: + safe_name = Path(payload.filename).name + if not safe_name: + raise HTTPException(status_code=400, detail="Invalid filename") + + label_safe_name = Path(payload.original_filename).name if payload.original_filename else safe_name + + image_path = UPLOAD_DIR / safe_name + if not image_path.exists(): + image_path = OUTPUT_DIR / safe_name + if not image_path.exists() or not image_path.is_file(): + raise HTTPException(status_code=404, detail=f"Image not found: {safe_name}") + + image_bgr = cv2.imread(str(image_path)) + if image_bgr is None: + raise HTTPException(status_code=400, detail="Image could not be read") + + mask_index = parse_mask_index(payload.mask_filename) + red, green, blue = parse_rgb_color(payload.color) + + label_path = UPLOAD_DIR / "masks" / f"{label_safe_name}_labels.png" + label_map = cv2.imread(str(label_path), cv2.IMREAD_GRAYSCALE) + if label_map is None: + raise HTTPException( + status_code=404, + detail="Label map not found. Upload the image first to generate segments.", + ) + + segmentation = label_map == mask_index + if not segmentation.any(): + raise HTTPException(status_code=400, detail=f"Segment index {mask_index} not found in label map.") + + edited_image = image_bgr.copy() + edited_image[segmentation] = (blue, green, red) + + original_stem = Path(label_safe_name).stem + out_filename = f"{original_stem}_edit.jpg" + out_path = UPLOAD_DIR / out_filename + if not cv2.imwrite(str(out_path), edited_image): + raise HTTPException(status_code=500, detail="Failed to save edited image") + + return { + "message": "Color applied successfully", + "output_filename": out_filename, + "output_url": f"/seg/image/{out_filename}", + } + finally: + log_timing_end("APPLY_COLOR", started) + try: + release_resources() + except Exception: + logger.exception("Error releasing resources after APPLY_COLOR") + + +@router.post("/apply_texture") +async def apply_texture(payload: ApplyTextureRequest) -> dict[str, Any]: + try: + result = await asyncio.to_thread(apply_local_texture_sync, payload) + result["processing"] = False + return result + except HTTPException: + raise + except Exception as exc: + raise HTTPException(status_code=500, detail=f"Texture apply failed: {exc}") from exc + + +@router.get("/textures") +async def get_textures() -> dict[str, Any]: + return {"textures": list_available_textures()} + + +class _GenerateVariationsRequest(BaseModel): + texture_name: str + + class Config: + extra = "ignore" + + +@router.post("/textures/generate") +async def generate_variations(payload: _GenerateVariationsRequest) -> dict[str, Any]: + if not payload.texture_name: + raise HTTPException(status_code=400, detail="texture_name is required") + try: + variations = await asyncio.to_thread(generate_texture_variations, payload.texture_name) + return {"variations": variations} + except HTTPException: + raise + except Exception as exc: + raise HTTPException(status_code=500, detail=f"Variation generation failed: {exc}") from exc + + +@router.get("/texture-preview/{filename:path}") +async def get_texture_preview(filename: str) -> Response: + texture_path = resolve_texture_path(filename) + jpeg = await asyncio.to_thread(build_texture_preview_jpeg, texture_path) + return Response(content=jpeg, media_type="image/jpeg", headers={"Cache-Control": "public, max-age=3600"}) + + +@router.get("/video/{filename}") +async def get_video(filename: str) -> FileResponse: + if Path(filename).name != filename: + raise HTTPException(status_code=400, detail="Invalid file name") + video_path = VIDEO_UPLOAD_DIR / filename + if not video_path.exists() or not video_path.is_file(): + raise HTTPException(status_code=404, detail="Video not found") + return FileResponse(video_path) + + +@router.get("/output-video/{filename}") +async def get_output_video(filename: str) -> FileResponse: + if Path(filename).name != filename: + raise HTTPException(status_code=400, detail="Invalid file name") + video_path = VIDEO_OUTPUT_DIR / filename + if not video_path.exists() or not video_path.is_file(): + raise HTTPException(status_code=404, detail="Output video not found") + return FileResponse(video_path) + + +@router.get("/image/{filename}") +async def get_image(filename: str) -> FileResponse: + if Path(filename).name != filename: + raise HTTPException(status_code=400, detail="Invalid file name") + image_path = UPLOAD_DIR / filename + if not image_path.exists() or not image_path.is_file(): + raise HTTPException(status_code=404, detail="Image not found") + return FileResponse(image_path) + + +@router.post("/masks/reclassify/{filename}") +async def reclassify_mask_metadata(filename: str) -> dict[str, Any]: + """Re-run semantic classification on an already-segmented image and overwrite its metadata JSON.""" + import json as _json + safe = Path(filename).name + if not safe: + raise HTTPException(status_code=400, detail="Invalid filename") + + masks_dir = UPLOAD_DIR / "masks" + label_path = masks_dir / f"{safe}_labels.png" + if not label_path.exists(): + raise HTTPException(status_code=404, detail="Label map not found — upload the image first") + + label_map = cv2.imread(str(label_path), cv2.IMREAD_GRAYSCALE) + if label_map is None: + raise HTTPException(status_code=500, detail="Could not read label map") + + image_path = UPLOAD_DIR / safe + image_rgb: Any = None + if image_path.exists(): + img_bgr = cv2.imread(str(image_path)) + if img_bgr is not None: + image_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB) + + from services.scene_service import classify_all_label_map_segments + h, w = label_map.shape[:2] + segments_meta = await asyncio.to_thread( + classify_all_label_map_segments, label_map, w, h, image_rgb + ) + meta_path = masks_dir / f"{safe}_labels_meta.json" + meta_path.write_text(_json.dumps({"segments": segments_meta}, ensure_ascii=False), encoding="utf-8") + + return {"segments": segments_meta, "count": len(segments_meta)} + + +@router.get("/masks/meta/{filename}") +async def get_mask_metadata(filename: str) -> dict: + import json as _json + safe = Path(filename).name + if not safe: + raise HTTPException(status_code=400, detail="Invalid filename") + meta_path = UPLOAD_DIR / "masks" / f"{safe}_labels_meta.json" + if not meta_path.exists() or not meta_path.is_file(): + raise HTTPException(status_code=404, detail="Segment metadata not found") + try: + return _json.loads(meta_path.read_text(encoding="utf-8")) + except Exception as exc: + raise HTTPException(status_code=500, detail=f"Failed to read metadata: {exc}") from exc + + +@router.get("/masks/{filename}") +async def get_mask_labels(filename: str) -> FileResponse: + if Path(filename).name != filename: + raise HTTPException(status_code=400, detail="Invalid file name") + label_path = UPLOAD_DIR / "masks" / f"{filename}_labels.png" + if not label_path.exists() or not label_path.is_file(): + raise HTTPException(status_code=404, detail="Label map not found") + return FileResponse(label_path) + + +@router.get("/ai/{filename}") +async def get_ai_image(filename: str) -> FileResponse: + if Path(filename).name != filename: + raise HTTPException(status_code=400, detail="Invalid file name") + out_path = OUTPUT_DIR / filename + if not out_path.exists() or not out_path.is_file(): + raise HTTPException(status_code=404, detail="AI output image not found") + return FileResponse(out_path) diff --git a/backend/services/__pycache__/__init__.cpython-313.pyc b/backend/services/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2234f768f0f64f7dae0975c76913146423d61e2c Binary files /dev/null and b/backend/services/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/services/__pycache__/gradio_client_service.cpython-312.pyc b/backend/services/__pycache__/gradio_client_service.cpython-312.pyc index 95bc1c42a782e47b8370fe86d9859825fce920ba..17e14d7b86464b1b0f3195bd9220cdc1af63fbe5 100644 Binary files a/backend/services/__pycache__/gradio_client_service.cpython-312.pyc and b/backend/services/__pycache__/gradio_client_service.cpython-312.pyc differ diff --git a/backend/services/__pycache__/gradio_client_service.cpython-313.pyc b/backend/services/__pycache__/gradio_client_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..425c80ac685c206a823a5735efa0eff47f03d2a0 Binary files /dev/null and b/backend/services/__pycache__/gradio_client_service.cpython-313.pyc differ diff --git a/backend/services/__pycache__/image_service.cpython-312.pyc b/backend/services/__pycache__/image_service.cpython-312.pyc index eb8b465e3447900c49b57290cbd51602a5027c0f..bc968580f9499ddd48aea78c9f853de4328e6b91 100644 Binary files a/backend/services/__pycache__/image_service.cpython-312.pyc and b/backend/services/__pycache__/image_service.cpython-312.pyc differ diff --git a/backend/services/__pycache__/image_service.cpython-313.pyc b/backend/services/__pycache__/image_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bf454ba15ca8514e5db1204fc3e97c36e3ed0a4a Binary files /dev/null and b/backend/services/__pycache__/image_service.cpython-313.pyc differ diff --git a/backend/services/__pycache__/inpainting_service.cpython-312.pyc b/backend/services/__pycache__/inpainting_service.cpython-312.pyc index d0e5ab77fa059656e6943e343ed57a8c6c443a2f..94b90002ccb0b19f3e38816e33fae704e15c5810 100644 Binary files a/backend/services/__pycache__/inpainting_service.cpython-312.pyc and b/backend/services/__pycache__/inpainting_service.cpython-312.pyc differ diff --git a/backend/services/__pycache__/inpainting_service.cpython-313.pyc b/backend/services/__pycache__/inpainting_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..18f822e4fff12998ab516d352d2c5334e7c4c63e Binary files /dev/null and b/backend/services/__pycache__/inpainting_service.cpython-313.pyc differ diff --git a/backend/services/__pycache__/openai_service.cpython-312.pyc b/backend/services/__pycache__/openai_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..061fd3e01d812b9255bd626340654fd4ef7d162e Binary files /dev/null and b/backend/services/__pycache__/openai_service.cpython-312.pyc differ diff --git a/backend/services/__pycache__/sam2_service.cpython-312.pyc b/backend/services/__pycache__/sam2_service.cpython-312.pyc index 1ce4a5103462f6b5b195cb187b857e7c6759341e..c113ff843e81df2bfc149b979cc99d202cf11752 100644 Binary files a/backend/services/__pycache__/sam2_service.cpython-312.pyc and b/backend/services/__pycache__/sam2_service.cpython-312.pyc differ diff --git a/backend/services/__pycache__/sam2_service.cpython-313.pyc b/backend/services/__pycache__/sam2_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2bba806c3e3dc0ec0451e61107622bb801ead8cb Binary files /dev/null and b/backend/services/__pycache__/sam2_service.cpython-313.pyc differ diff --git a/backend/services/__pycache__/scene_service.cpython-313.pyc b/backend/services/__pycache__/scene_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67272048cb598593fab73a50ed812f21cd80f7fa Binary files /dev/null and b/backend/services/__pycache__/scene_service.cpython-313.pyc differ diff --git a/backend/services/__pycache__/segmentation_service.cpython-313.pyc b/backend/services/__pycache__/segmentation_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4d5e62b2ca9fc32d420b07ad8316490d57c7d437 Binary files /dev/null and b/backend/services/__pycache__/segmentation_service.cpython-313.pyc differ diff --git a/backend/services/__pycache__/texture_service.cpython-312.pyc b/backend/services/__pycache__/texture_service.cpython-312.pyc index 96302d99f155ba7d7954c05eb754f1c77d87742f..ef7041b49fabf9c9db7b447f0a7b74d0eb1f8069 100644 Binary files a/backend/services/__pycache__/texture_service.cpython-312.pyc and b/backend/services/__pycache__/texture_service.cpython-312.pyc differ diff --git a/backend/services/__pycache__/texture_service.cpython-313.pyc b/backend/services/__pycache__/texture_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..78f4527d3a4a5dce8e7fca1e3b5eb0cac069e10c Binary files /dev/null and b/backend/services/__pycache__/texture_service.cpython-313.pyc differ diff --git a/backend/services/gradio_client_service.py b/backend/services/gradio_client_service.py index d2088a7800854adaf257bf9c73351cc0ce0cbd52..21592fb3a212ae553affec2c3a943e8d7c778b7e 100644 --- a/backend/services/gradio_client_service.py +++ b/backend/services/gradio_client_service.py @@ -1,176 +1,108 @@ -""" -Cliente para llamar al Gradio Space de ZeroGPU (SAM2 + SegFormer + DINO). -Se activa solo si GRADIO_SPACE_URL está definido en el entorno. -""" -import asyncio -import base64 -import io -import json -import logging -from pathlib import Path - -import numpy as np -from PIL import Image - -from core.config import GRADIO_CPU_FALLBACK_URL, GRADIO_SPACE_URL - -logger = logging.getLogger(__name__) - - -def is_gradio_enabled() -> bool: - return bool(GRADIO_SPACE_URL) - - -def _call_gradio_sync(image_path: Path, space_url: str) -> tuple[np.ndarray, int]: - """ - Synchronous Gradio call — safe to invoke from a background thread. - Returns (label_map, mask_count). - Raises on any error so the caller can handle fallback. - """ - from gradio_client import Client, file # type: ignore - - # 300s timeout: ZeroGPU cold start + SAM2+DINO inference can take 60-120s - client = Client(space_url, httpx_kwargs={"timeout": 300.0}) - - # segment_for_backend returns (overlay_image, combined_json_str) - _overlay_file, combined_json_str = client.predict( - file(str(image_path)), - api_name="/segment", - ) - - if not isinstance(combined_json_str, str): - raise ValueError(f"Unexpected response type from Gradio Space: {type(combined_json_str)}") - - combined: dict = json.loads(combined_json_str) - - if "error" in combined: - raise RuntimeError(f"Gradio Space error: {combined['error'][:500]}") - - label_map_b64: str = combined.get("label_map_b64", "") - if not label_map_b64: - return np.zeros((1, 1), dtype=np.uint8), 0 - - # Decode PNG-encoded label map (lossless uint8 grayscale) - label_map_bytes = base64.b64decode(label_map_b64) - pil_label = Image.open(io.BytesIO(label_map_bytes)) - label_map = np.array(pil_label, dtype=np.uint8) - mask_count = int(label_map.max()) - - entorno = combined.get("entorno", "?") - motor = combined.get("motor", "?") - logger.info( - "Gradio Space segmentation: entorno=%s motor=%s mask_count=%d", - entorno, motor, mask_count, - ) - - return label_map, mask_count - - -def segment_via_gradio_sync(image_path: Path) -> tuple[np.ndarray, int]: - """ - Blocking call to the Gradio Space from a sync context (background task thread). - Tries the GPU Space first; if it fails, falls back to the CPU Space. - Raises RuntimeError if both fail or neither is configured. - """ - if not is_gradio_enabled(): - raise RuntimeError("GRADIO_SPACE_URL is not configured") - - gpu_error: Exception | None = None - try: - logger.info("Calling GPU Gradio Space: %s", GRADIO_SPACE_URL) - return _call_gradio_sync(image_path, GRADIO_SPACE_URL) - except Exception as e: - gpu_error = e - logger.warning("GPU Space failed (%s), trying CPU fallback...", gpu_error) - - if not GRADIO_CPU_FALLBACK_URL: - raise RuntimeError(f"GPU Gradio Space failed and no CPU fallback configured. Error: {gpu_error}") - - try: - logger.info("Calling CPU fallback Space: %s", GRADIO_CPU_FALLBACK_URL) - return _call_gradio_sync(image_path, GRADIO_CPU_FALLBACK_URL) - except Exception as exc_cpu: - raise RuntimeError( - f"Both Gradio Spaces failed.\n" - f" GPU ({GRADIO_SPACE_URL}): {gpu_error}\n" - f" CPU ({GRADIO_CPU_FALLBACK_URL}): {exc_cpu}" - ) from exc_cpu - - -async def segment_via_gradio(image_path: Path) -> tuple[np.ndarray, int]: - """ - Async wrapper — offloads the blocking call (with GPU→CPU fallback) to a thread. - """ - return await asyncio.to_thread(segment_via_gradio_sync, image_path) - - -def _call_gradio_render_sync( - image_path: Path, - label_map_b64: str, - mask_index: int, - texture_name: str | None, - texture_b64: str | None, - params_json: str, - space_url: str, -) -> tuple[np.ndarray, dict]: - """ - Synchronous call to the Gradio Space render endpoint. - Returns (rendered_image_np, combined_dict) - """ - from gradio_client import Client, file # type: ignore - - client = Client(space_url, httpx_kwargs={"timeout": 300.0}) - - # Prepare args: image file + other strings/values - inputs = [file(str(image_path)), label_map_b64 or "", int(mask_index or 1), texture_name or "", texture_b64 or "", params_json or "{}"] - - rendered_img, combined_json_str = client.predict(*inputs, api_name="/render") - - if not isinstance(combined_json_str, str): - raise ValueError(f"Unexpected response type from Gradio Space render: {type(combined_json_str)}") - - combined = json.loads(combined_json_str) - if "error" in combined: - raise RuntimeError(f"Gradio Space render error: {combined['error'][:500]}") - - return rendered_img, combined - - -def render_via_gradio_sync( - image_path: Path, - label_map_b64: str, - mask_index: int = 1, - texture_name: str | None = None, - texture_b64: str | None = None, - params_json: str = "{}", -) -> tuple[np.ndarray, dict]: - if not is_gradio_enabled(): - raise RuntimeError("GRADIO_SPACE_URL is not configured") - - gpu_error: Exception | None = None - try: - return _call_gradio_render_sync(image_path, label_map_b64, mask_index, texture_name, texture_b64, params_json, GRADIO_SPACE_URL) - except Exception as e: - gpu_error = e - logger.warning("GPU Space render failed (%s), trying CPU fallback...", gpu_error) - - if not GRADIO_CPU_FALLBACK_URL: - raise RuntimeError(f"GPU Gradio Space render failed and no CPU fallback configured. Error: {gpu_error}") - - try: - return _call_gradio_render_sync(image_path, label_map_b64, mask_index, texture_name, texture_b64, params_json, GRADIO_CPU_FALLBACK_URL) - except Exception as exc_cpu: - raise RuntimeError( - f"Both Gradio Spaces render failed.\n GPU ({GRADIO_SPACE_URL}): {gpu_error}\n CPU ({GRADIO_CPU_FALLBACK_URL}): {exc_cpu}" - ) from exc_cpu - - -async def render_via_gradio( - image_path: Path, - label_map_b64: str, - mask_index: int = 1, - texture_name: str | None = None, - texture_b64: str | None = None, - params_json: str = "{}", -) -> tuple[np.ndarray, dict]: - return await asyncio.to_thread(render_via_gradio_sync, image_path, label_map_b64, mask_index, texture_name, texture_b64, params_json) +""" +Cliente para llamar al Gradio Space de ZeroGPU (SAM2 + SegFormer + DINO). +Se activa solo si GRADIO_SPACE_URL está definido en el entorno. +""" +import asyncio +import base64 +import io +import json +import logging +from pathlib import Path + +import numpy as np +from PIL import Image + +from core.config import GRADIO_CPU_FALLBACK_URL, GRADIO_SPACE_URL + +logger = logging.getLogger(__name__) + + +def is_gradio_enabled() -> bool: + # Desactivado por petición del usuario para usar el nuevo flujo simplificado con OpenAI + return False + + + +def _call_gradio_sync(image_path: Path, space_url: str) -> tuple[np.ndarray, int]: + """ + Synchronous Gradio call — safe to invoke from a background thread. + Returns (label_map, mask_count). + Raises on any error so the caller can handle fallback. + """ + from gradio_client import Client, file # type: ignore + + # 300s timeout: ZeroGPU cold start + SAM2+DINO inference can take 60-120s + # client = Client(space_url, httpx_kwargs={"timeout": 300.0}) + client = Client(space_url) # httpx_kwargs no es compatible con todas las versiones + + + # segment_for_backend returns (overlay_image, combined_json_str) + _overlay_file, combined_json_str = client.predict( + file(str(image_path)), + api_name="/segment", + ) + + if not isinstance(combined_json_str, str): + raise ValueError(f"Unexpected response type from Gradio Space: {type(combined_json_str)}") + + combined: dict = json.loads(combined_json_str) + + if "error" in combined: + raise RuntimeError(f"Gradio Space error: {combined['error'][:500]}") + + label_map_b64: str = combined.get("label_map_b64", "") + if not label_map_b64: + return np.zeros((1, 1), dtype=np.uint8), 0 + + # Decode PNG-encoded label map (lossless uint8 grayscale) + label_map_bytes = base64.b64decode(label_map_b64) + pil_label = Image.open(io.BytesIO(label_map_bytes)) + label_map = np.array(pil_label, dtype=np.uint8) + mask_count = int(label_map.max()) + + entorno = combined.get("entorno", "?") + motor = combined.get("motor", "?") + logger.info( + "Gradio Space segmentation: entorno=%s motor=%s mask_count=%d", + entorno, motor, mask_count, + ) + + return label_map, mask_count + + +def segment_via_gradio_sync(image_path: Path) -> tuple[np.ndarray, int]: + """ + Blocking call to the Gradio Space from a sync context (background task thread). + Tries the GPU Space first; if it fails, falls back to the CPU Space. + Raises RuntimeError if both fail or neither is configured. + """ + if not is_gradio_enabled(): + raise RuntimeError("GRADIO_SPACE_URL is not configured") + + gpu_error: Exception | None = None + try: + logger.info("Calling GPU Gradio Space: %s", GRADIO_SPACE_URL) + return _call_gradio_sync(image_path, GRADIO_SPACE_URL) + except Exception as e: + gpu_error = e + logger.warning("GPU Space failed (%s), trying CPU fallback...", gpu_error) + + if not GRADIO_CPU_FALLBACK_URL: + raise RuntimeError(f"GPU Gradio Space failed and no CPU fallback configured. Error: {gpu_error}") + + try: + logger.info("Calling CPU fallback Space: %s", GRADIO_CPU_FALLBACK_URL) + return _call_gradio_sync(image_path, GRADIO_CPU_FALLBACK_URL) + except Exception as exc_cpu: + raise RuntimeError( + f"Both Gradio Spaces failed.\n" + f" GPU ({GRADIO_SPACE_URL}): {gpu_error}\n" + f" CPU ({GRADIO_CPU_FALLBACK_URL}): {exc_cpu}" + ) from exc_cpu + + +async def segment_via_gradio(image_path: Path) -> tuple[np.ndarray, int]: + """ + Async wrapper — offloads the blocking call (with GPU→CPU fallback) to a thread. + """ + return await asyncio.to_thread(segment_via_gradio_sync, image_path) diff --git a/backend/services/image_service.py b/backend/services/image_service.py index 08265d2eb68bb2ba6388f1207b71e0da92cbd3b3..5e6621f71d4e9f681be29e58924fd5d9fe8b90d3 100644 --- a/backend/services/image_service.py +++ b/backend/services/image_service.py @@ -18,13 +18,13 @@ from core.config import ( logger, utc_now_iso, ) -from services.gradio_client_service import is_gradio_enabled, segment_via_gradio_sync from services.sam2_service import ( jobs, jobs_lock, release_resources, ) + # Imported lazily to avoid circular imports def _get_generate_label_map(): from services.scene_service import generate_label_map @@ -134,47 +134,15 @@ def run_upload_job(job_id: str, content: bytes, original_name: str) -> None: logger.info(f"[JOB {job_id}] segmenting_with_sam2 progress=30 estimated_seconds={estimated_seconds}") image_path = UPLOAD_DIR / safe_name - if is_gradio_enabled(): - label_map, mask_count = segment_via_gradio_sync(image_path) - else: - generate_label_map = _get_generate_label_map() - label_map, mask_count = generate_label_map(image_rgb) - - with jobs_lock: - job = jobs.setdefault(job_id, {}) - job.update({ - "stage": "saving_masks", - "progress": 92, - "message": "Saving mask map", - "updated_at": utc_now_iso(), - }) - logger.info(f"[JOB {job_id}] saving_masks progress=92") - - masks_dir = UPLOAD_DIR / "masks" - masks_dir.mkdir(exist_ok=True) - label_path = masks_dir / f"{safe_name}_labels.png" - if not cv2.imwrite(str(label_path), label_map): - raise HTTPException(status_code=500, detail="Failed to save label map") - - # Classify each segment and save metadata - try: - from services.scene_service import classify_all_label_map_segments - h, w = image_rgb.shape[:2] - segments_meta = classify_all_label_map_segments(label_map, w, h, image_rgb) - meta_path = masks_dir / f"{safe_name}_labels_meta.json" - meta_path.write_text( - json.dumps({"segments": segments_meta}, ensure_ascii=False), - encoding="utf-8", - ) - logger.info(f"[JOB {job_id}] segments_meta saved ({len(segments_meta)} segments)") - except Exception: - logger.exception(f"[JOB {job_id}] Failed to save segment metadata") - + + # We are simplifying the flow: skipping SAM 2 segmentation. + # The result will have 0 masks. + result: dict[str, Any] = { "message": "Image uploaded successfully", "filename": safe_name, "url": f"/seg/image/{safe_name}", - "mask_count": mask_count, + "mask_count": 0, } with jobs_lock: @@ -183,11 +151,15 @@ def run_upload_job(job_id: str, content: bytes, original_name: str) -> None: "status": "done", "stage": "done", "progress": 100, - "message": "Segmentation complete", + "message": "Upload complete (Simplified flow)", "result": result, "updated_at": utc_now_iso(), } - logger.info(f"[JOB {job_id}] done mask_count={mask_count}") + logger.info(f"[JOB {job_id}] done (simplified, 0 masks)") + + # Segmentation-related files (masks, metadata) are skipped in the simplified flow. + pass + except Exception as exc: logger.exception(f"[JOB {job_id}] failed: {exc}") diff --git a/backend/services/inpainting_service.py b/backend/services/inpainting_service.py index 476c0190851ae1ef169939a88dc442adddbbf5cd..6409dbf0b5d4cd73103fddeef5310887a172eced 100644 --- a/backend/services/inpainting_service.py +++ b/backend/services/inpainting_service.py @@ -1,28 +1,220 @@ +import io +import os +import base64 +import openai +from pathlib import Path +from PIL import Image, ImageOps from typing import Any +import numpy as np -from core.config import utc_now_iso +from core.config import UPLOAD_DIR, OUTPUT_DIR, logger, utc_now_iso +from services.openai_service import _get_texture_hex from models.schemas import ApplyTextureAIRequest from services.sam2_service import jobs, jobs_lock +# ─── Descripciones en inglés para el prompt de DALL-E ───────────────────────── +TEXTURE_DESCRIPTIONS = { + "ACM_Amarillo": "bright yellow smooth aluminum composite panel exterior cladding", + "ACM_Azul": "blue aluminum composite panel exterior cladding", + "ACM_Glossy_Black": "glossy black aluminum composite panel exterior cladding", + "ACM_Glossy_Red": "glossy red aluminum composite panel exterior cladding", + "ACM_Grafito": "graphite dark grey aluminum composite panel exterior cladding", + "ACM_Light_Blue": "light sky blue aluminum composite panel exterior cladding", + "ACM_Madera_Clara": "light wood grain aluminum composite panel exterior cladding", + "ACM_Matteblack": "matte black aluminum composite panel exterior cladding", + "ACM_Metalic": "metallic silver brushed aluminum composite panel exterior cladding", + "ACM_MouseGrey": "mouse grey aluminum composite panel exterior cladding", + "ACM_Orange": "orange aluminum composite panel exterior cladding", + "ACM_ROBLE(OAK)": "oak wood grain aluminum composite panel exterior cladding", + "ACM_Verde": "green aluminum composite panel exterior cladding", + "ACM_Verde_HN": "dark forest green aluminum composite panel exterior cladding", + "ACM_Verde_Lima": "lime yellow-green aluminum composite panel exterior cladding", + "ACM_White": "white aluminum composite panel exterior cladding", + "DECK_gris": "grey WPC wood-plastic composite deck boards", + "DECK_madera": "natural wood-look WPC composite deck boards", + "DECK_madera_oscuro": "dark brown WPC composite deck boards", + "WPC_madera_claro": "light beige wood-grain WPC exterior wall cladding", + "WPC_madera_gris": "grey weathered wood-look WPC exterior wall cladding", + "WPC_madera_oscuro": "dark espresso wood-grain WPC exterior wall cladding", + "WPC_negro": "black WPC exterior wall cladding", +} + +def prepare_image_square(pil_img, size=1024): + """ + Ajusta la imagen a un cuadrado de 1024x1024 añadiendo padding + para no deformar la imagen original. + """ + orig_w, orig_h = pil_img.size + ratio = orig_w / orig_h + + if ratio > 1: # Es más ancha que alta (Horizontal) + new_w = size + new_h = int(size / ratio) + padding = (0, (size - new_h) // 2) + else: # Es más alta que ancha (Vertical) + new_h = size + new_w = int(size * ratio) + padding = ((size - new_w) // 2, 0) + + # Redimensionar manteniendo proporción + resized_img = pil_img.resize((new_w, new_h), Image.LANCZOS) + + # Crear fondo cuadrado con transparencia + new_img = Image.new("RGBA", (size, size), (255, 255, 255, 0)) + new_img.paste(resized_img, padding) + + return new_img, padding, (new_w, new_h) def run_inpainting_sync(payload: ApplyTextureAIRequest) -> dict[str, Any]: - return { - "message": "Inpainting not configured", - "filename": payload.filename, - "prompt": payload.prompt, - "processing": False, - } + """ + Usa DALL-E 2 para editar la imagen de la casa reemplazando las texturas. + Inspirado en la lógica de 'imagneConaI/app.py'. + """ + api_key = os.getenv("OPENAI_API_KEY") + if not api_key: + return {"error": "OpenAI API Key not found in environment"} + + safe_name = Path(payload.filename).name + image_path = UPLOAD_DIR / safe_name + if not image_path.exists(): + image_path = OUTPUT_DIR / safe_name + + if not image_path.exists(): + return {"error": f"Image not found: {payload.filename}"} + + # Determinar descripción de textura + texture_name = payload.texture_name or "ACM_White" + texture_stem = Path(texture_name).stem + texture_desc = TEXTURE_DESCRIPTIONS.get(texture_stem, texture_name) + + # Specs técnicas para el prompt + acm_specs = "" + if texture_stem.startswith("ACM"): + acm_specs = ( + "ACM aluminum composite panel 4mm thick, 0.40mm aluminum layers, " + "panels sized 1.22m x 2.44m, clean precision-cut joints between panels, " + ) + elif "WPC" in texture_stem or "DECK" in texture_stem: + acm_specs = "WPC wood-plastic composite profile boards, " + + try: + client = openai.OpenAI(api_key=api_key) + + pil_img_orig = Image.open(str(image_path)).convert("RGBA") + orig_size = pil_img_orig.size + + square_img, padding, resized_dims = prepare_image_square(pil_img_orig) + + buf = io.BytesIO() + square_img.save(buf, format="PNG") + buf.seek(0) + + # Usar el prompt enviado o generar uno por defecto + prompt = payload.prompt or ( + f"Edit ONLY the exterior wall cladding material of this house. " + f"Replace all facade wall surfaces with {acm_specs}{texture_desc}. " + f"Keep EVERYTHING ELSE exactly the same: the house structure, shape, proportions, " + f"roof, windows, doors, garage, balcony railings, garden, plants, sky and lighting. " + f"Only change the wall surface material. " + f"Result must look like a photorealistic architectural photo. " + f"CRITICAL: Do NOT stylize, do NOT apply artistic filters, do NOT add painterly or CGI effects. " + f"Preserve photographic grain, camera exposure, highlights and shadows, and match original lighting and perspective. " + f"Avoid oversmoothing — keep realistic texture detail and crisp panel edges." + ) + + # If texture file exists, compute representative hex and request exact color match + tex_hex = _get_texture_hex(texture_name or "") if texture_name else None + if tex_hex: + prompt += f" Use the exact color sample {tex_hex} from the reference texture and match its hue, saturation and brightness precisely. Do not alter the color tone." + # gpt-image-1 solo acepta 1024x1024 + # If a texture file exists, attach it as a reference image when possible + tex_path = None + if texture_name: + try: + from services.texture_service import resolve_texture_path as _resolve + tex_path = _resolve(texture_name) + except Exception: + tex_path = None + + if tex_path: + try: + with open(tex_path, "rb") as tf: + tex_buf = io.BytesIO(tf.read()) + tex_buf.seek(0) + response = client.images.edit( + model="gpt-image-1", + image=("house.png", buf, "image/png"), + reference_image=(Path(tex_path).name, tex_buf, "image/png"), + prompt=prompt, + n=1, + size="1024x1024", + response_format="b64_json", + ) + except Exception: + response = client.images.edit( + model="gpt-image-1", + image=("house.png", buf, "image/png"), + prompt=prompt, + n=1, + size="1024x1024", + response_format="b64_json", + ) + else: + response = client.images.edit( + model="gpt-image-1", + image=("house.png", buf, "image/png"), + prompt=prompt, + n=1, + size="1024x1024", + response_format="b64_json", + ) + + img_data = base64.b64decode(response.data[0].b64_json) + result_square = Image.open(io.BytesIO(img_data)).convert("RGBA") + + # 2. Recortar el resultado para volver al aspecto original (quitar padding) + left, top = padding + right, bottom = left + resized_dims[0], top + resized_dims[1] + result_cropped = result_square.crop((left, top, right, bottom)) + + # 3. Redimensionar al tamaño original exacto + result = result_cropped.resize(orig_size, Image.LANCZOS).convert("RGB") + + # Return generated result without blending with original image + final = result + + out_filename = f"{Path(safe_name).stem}_ai_{texture_stem}.jpg" + out_path = OUTPUT_DIR / out_filename + final.save(str(out_path), "JPEG", quality=90) + + return { + "message": f"AI Texture applied: {texture_name}", + "filename": out_filename, + "url": f"/seg/ai/{out_filename}", + "processing": False + } + + except Exception as e: + logger.error(f"DALL-E Error: {e}") + return {"error": str(e)} def run_inpainting_job(job_id: str, payload: ApplyTextureAIRequest) -> None: try: result = run_inpainting_sync(payload) with jobs_lock: - jobs[job_id] = { - "status": "done", - "result": result, - "updated_at": utc_now_iso(), - } + if "error" in result: + jobs[job_id] = { + "status": "failed", + "error": result["error"], + "updated_at": utc_now_iso(), + } + else: + jobs[job_id] = { + "status": "done", + "result": result, + "updated_at": utc_now_iso(), + } except Exception as exc: with jobs_lock: jobs[job_id] = { diff --git a/backend/services/openai_service.py b/backend/services/openai_service.py new file mode 100644 index 0000000000000000000000000000000000000000..db6652b498778694467c26f5fc49b9550754b34f --- /dev/null +++ b/backend/services/openai_service.py @@ -0,0 +1,251 @@ +import io +import os +import base64 +from pathlib import Path +from typing import Any, Tuple + +import openai +from PIL import Image + +from core.config import TEXTURE_DIR + + +TEXTURE_DESCRIPTIONS = { + # Copiado desde imagneConaI/app.py (reducido a los usados más adelante) + "ACM_Amarillo": "bright yellow smooth aluminum composite panel exterior cladding", + "ACM_Azul": "blue aluminum composite panel exterior cladding", + "ACM_White": "white aluminum composite panel exterior cladding", +} + + +def _resolve_texture_path(name_or_path: str) -> Tuple[str | None, str]: + if not name_or_path: + return None, "" + p = Path(name_or_path) + if p.exists(): + return str(p), p.stem + # buscar en TEXTURE_DIR + for folder in TEXTURE_DIR.iterdir(): + if not folder.is_dir(): + continue + for it in folder.glob("*.png"): + if it.stem.lower() == name_or_path.lower() or it.name.lower() == name_or_path.lower(): + return str(it), it.stem.replace("_", " ") + return None, name_or_path + + +def _get_texture_hex(name_or_path: str) -> str | None: + """Return average color hex for a texture file if available.""" + path, _ = _resolve_texture_path(name_or_path) + if not path: + return None + try: + img = Image.open(path).convert("RGB") + # downsize for speed + img = img.resize((64, 64)) + pixels = list(img.getdata()) + r = sum(p[0] for p in pixels) // len(pixels) + g = sum(p[1] for p in pixels) // len(pixels) + b = sum(p[2] for p in pixels) // len(pixels) + return f"#{r:02X}{g:02X}{b:02X}" + except Exception: + return None + + +def prepare_image_square(pil_img: Image.Image, size: int = 1024): + orig_w, orig_h = pil_img.size + ratio = orig_w / orig_h + if ratio > 1: + new_w = size + new_h = int(size / ratio) + padding = (0, (size - new_h) // 2) + else: + new_h = size + new_w = int(size * ratio) + padding = ((size - new_w) // 2, 0) + resized_img = pil_img.resize((new_w, new_h), Image.LANCZOS) + new_img = Image.new("RGBA", (size, size), (255, 255, 255, 0)) + new_img.paste(resized_img, padding) + return new_img, padding, (new_w, new_h) + + +def generate_image_with_openai( + api_key: str | None, + pil_img: Image.Image, + texture: str, + preserve_percent: int = 0, + use_vision: bool = False, +) -> Tuple[bytes | None, str]: + """ + Genera/edita una imagen usando la API de OpenAI (gpt-image-1 edit). + Retorna (png_bytes, message) o (None, error_message). + """ + key = api_key.strip() if api_key and isinstance(api_key, str) and api_key.strip() else os.getenv("OPENAI_API_KEY") + if not key: + return None, "OpenAI API key not provided" + + texture_path, texture_display = _resolve_texture_path(texture) + if not texture_path: + # intentar usar descripción simple + texture_desc = TEXTURE_DESCRIPTIONS.get(texture, texture) + else: + texture_desc = Path(texture_path).stem + + # Build specialized specs for ACM / WPC / DECK + acm_specs = "" + if texture_display and texture_display.upper().startswith("ACM"): + acm_specs = ( + "ACM aluminum composite panel 4mm thick, 0.40mm aluminum layers, " + "panels sized 1.22m x 2.44m, clean precision-cut joints between panels, " + ) + + deck_specs = "" + if Path(texture).stem.startswith("DECK") or ("DECK" in Path(texture).stem): + deck_specs = ( + "WPC deck boards (ash / \"ceniza\" color), plank dimensions 2.5 cm thick x 14 cm wide x 220 cm long, " + "installed as horizontal planks with 3-5 mm spacing and hidden fasteners, realistic wood grain, slight bevel on plank edges." + ) + + wpc_panel_specs = "" + if ("WPC" in Path(texture).stem) and ("DECK" not in Path(texture).stem): + wpc_panel_specs = ( + "WPC wall panels, each panel sized 0.16 m x 2.90 m (16 cm x 290 cm), realistic ash tone when appropriate, " + "installed with narrow vertical joints matching panel width and clean aligned seams." + ) + + # If we have a texture file, attempt to compute its representative color and ask model to match it + tex_hex = _get_texture_hex(texture) + + # Build prompt depending on type + if deck_specs: + prompt = ( + f"Edit ONLY the HORIZONTAL floor/deck surfaces (decks, terraces, balcony floors, wooden walkways). " + f"Replace all decking/floor surfaces with {deck_specs} {texture_desc}. " + f"Do NOT apply this material to vertical wall cladding or facades. " + f"Keep EVERYTHING ELSE exactly the same: the house structure, shape, proportions, roof, windows, doors, garage, balcony railings, garden, plants, sky and lighting. " + f"Result must look like a photorealistic architectural photo." + ) + elif wpc_panel_specs: + prompt = ( + f"Edit ONLY the VERTICAL wall cladding surfaces (exterior and interior wall panels where visible). " + f"Replace all vertical wall panels with {wpc_panel_specs} {texture_desc}. " + f"Do NOT apply this material to horizontal floor or deck surfaces. " + f"Keep EVERYTHING ELSE exactly the same: the house structure, shape, proportions, roof, windows, doors, garage, balcony railings, garden, plants, sky and lighting. " + f"Result must look like a photorealistic architectural photo." + ) + else: + prompt = ( + f"Edit ONLY the exterior wall cladding material of this house. " + f"Replace all facade wall surfaces with {acm_specs}{texture_desc}. " + f"Keep EVERYTHING ELSE exactly the same: the house structure, shape, proportions, " + f"roof, windows, doors, garage, balcony railings, garden, plants, sky and lighting. " + f"Only change the wall surface material. Result must look like a photorealistic architectural photo." + ) + + if tex_hex: + prompt += f" Use the exact color sample {tex_hex} from the reference texture and match its hue, saturation and brightness precisely. Do not alter the color tone." + + try: + client = openai.OpenAI(api_key=key) + + pil_img_orig = pil_img.convert("RGBA") + orig_size = pil_img_orig.size + square_img, padding, resized_dims = prepare_image_square(pil_img_orig) + + buf = io.BytesIO() + square_img.save(buf, format="PNG") + buf.seek(0) + + # Optionally run vision analysis to enrich prompt + house_desc = "" + if use_vision: + try: + bb = io.BytesIO() + pil_img_orig.convert("RGB").save(bb, format="JPEG", quality=85) + bb.seek(0) + b64 = base64.b64encode(bb.getvalue()).decode("ascii") + + vision_prompt = ( + "You are an architectural photographer. Describe exactly what is visible in this house photo: " + "number of floors, roof shape, main materials, prominent features (garage, balconies, large windows), " + "lighting conditions, and anything that affects how to apply a facade material. " + "Return a concise English description (max 200 words)." + ) + + vision_resp = client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": f"data:image/jpeg;base64,{b64}\n\n{vision_prompt}"}], + max_tokens=300, + ) + + try: + house_desc = vision_resp.choices[0].message.content.strip() + except Exception: + house_desc = str(vision_resp) + except Exception: + house_desc = "" + + if house_desc: + prompt = f"House description: {house_desc}. " + prompt + + # If a texture file exists, try to attach it as a reference image to the API call. + tex_path, _ = _resolve_texture_path(texture) + try: + if tex_path: + with open(tex_path, "rb") as tf: + tex_buf = io.BytesIO(tf.read()) + tex_buf.seek(0) + response = client.images.edit( + model="gpt-image-1", + image=("image.png", buf, "image/png"), + reference_image=(Path(tex_path).name, tex_buf, "image/png"), + prompt=prompt, + n=1, + size="1024x1024", + quality="medium", + ) + else: + response = client.images.edit( + model="gpt-image-1", + image=("image.png", buf, "image/png"), + prompt=prompt, + n=1, + size="1024x1024", + quality="medium", + ) + except Exception: + # Fallback: try without reference_image if SDK rejected the param + response = client.images.edit( + model="gpt-image-1", + image=("image.png", buf, "image/png"), + prompt=prompt, + n=1, + size="1024x1024", + quality="medium", + ) + + img_data = base64.b64decode(response.data[0].b64_json) + result_square = Image.open(io.BytesIO(img_data)).convert("RGBA") + left, top = padding + right, bottom = left + resized_dims[0], top + resized_dims[1] + result_cropped = result_square.crop((left, top, right, bottom)) + result = result_cropped.resize(orig_size, Image.LANCZOS).convert("RGB") + + # Apply optional blending/preserve percent (0-100) + try: + preserve = max(0.0, min(1.0, float(preserve_percent) / 100.0)) + except Exception: + preserve = 0.0 + + if preserve > 0: + pil_img_rgb = pil_img_orig.convert("RGB") + final = Image.blend(result, pil_img_rgb, preserve) + else: + final = result + + out_buf = io.BytesIO() + final.save(out_buf, format="PNG") + return out_buf.getvalue(), f"Generated with texture {texture_display if texture_display else texture_desc}" + + except Exception as exc: + return None, f"Error generating image: {exc}" diff --git a/backend/services/sam2_service.py b/backend/services/sam2_service.py index dc729c9e48607fa82e02bb2f52be8542ca98d44a..a455a74fde940f78ed21527a7e7caa6c6c6b6f1f 100644 --- a/backend/services/sam2_service.py +++ b/backend/services/sam2_service.py @@ -112,6 +112,9 @@ def find_sam2_model_path() -> Path: def load_sam2_model() -> None: global sam2_mask_generator, sam2_image_predictor, sam2_load_error + sam2_load_error = "SAM2 disabled (using Simplified OpenAI flow)" + logger.info(f"[SAM2] {sam2_load_error}") + return if not _TORCH_AVAILABLE: sam2_load_error = "torch not installed — SAM2 unavailable (using Gradio Space)" diff --git a/backend/services/texture_service.py b/backend/services/texture_service.py index 8c3af95ce1f764618623157acabea379e8000b04..61914b709052fd339c9205b052b2da7f2a7e4d5f 100644 --- a/backend/services/texture_service.py +++ b/backend/services/texture_service.py @@ -1,981 +1,851 @@ -import io -import shutil -import uuid -from pathlib import Path -from typing import Any - -import cv2 -import numpy as np -from fastapi import HTTPException -from PIL import Image - -from core.config import ( - OUTPUT_DIR, - TEXTURE_DIR, - UPLOAD_DIR, - UPLOAD_JPEG_QUALITY, - log_timing_end, - log_timing_start, - logger, -) -from models.schemas import ApplyTextureRequest - - -def generate_texture_variations(texture_name: str) -> list[dict[str, str]]: - """ - Genera variaciones de color/brillo/saturación de una textura de referencia usando HSV. - Los archivos se cachean en TEXTURE_DIR/generated/ — si ya existen no se regeneran. - Devuelve lista de {ref, label, preview_url}. - """ - texture_path = resolve_texture_path(texture_name) - generated_dir = TEXTURE_DIR / "generated" - generated_dir.mkdir(parents=True, exist_ok=True) - - tex_pil = load_texture_pil_rgb(texture_path) - tex_bgr = cv2.cvtColor(np.array(tex_pil, dtype=np.uint8), cv2.COLOR_RGB2BGR) - tex_hsv = cv2.cvtColor(tex_bgr, cv2.COLOR_BGR2HSV).astype(np.int32) - - base_stem = Path(texture_name).stem - results: list[dict[str, str]] = [] - - def _save(hsv: np.ndarray, suffix: str, label: str) -> None: - fname = f"{base_stem}__{suffix}.jpg" - out_path = generated_dir / fname - if not out_path.exists(): - bgr = cv2.cvtColor(np.clip(hsv, 0, 255).astype(np.uint8), cv2.COLOR_HSV2BGR) - rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) - Image.fromarray(rgb).save(str(out_path), format="JPEG", quality=92, optimize=True) - ref = f"generated/{fname}" - results.append({"ref": ref, "label": label, "preview_url": f"/seg/texture-preview/{ref}"}) - - # Rotaciones de tono: 12 pasos de 30° recorriendo el círculo cromático - # En OpenCV HSV, H ∈ [0,179] → shift en grados / 2 - for deg, label in [ - (30, "Naranja"), - (60, "Amarillo"), - (90, "Verde lima"), - (120, "Verde"), - (150, "Verde agua"), - (165, "Cyan"), - (180, "Azul cielo"), - (210, "Azul"), - (240, "Índigo"), - (270, "Violeta"), - (300, "Magenta"), - (330, "Rosa"), - ]: - v = tex_hsv.copy() - v[:, :, 0] = (v[:, :, 0] + deg // 2) % 180 - _save(v, f"hue{deg}", label) - - # Variaciones de brillo - for factor, label, suffix in [ - (0.45, "Oscuro", "dark"), - (1.55, "Claro", "light"), - ]: - v = tex_hsv.copy() - v[:, :, 2] = np.clip(v[:, :, 2] * factor, 0, 255) - _save(v, suffix, label) - - # Variaciones de saturación - for factor, label, suffix in [ - (0.0, "Gris", "gray"), - (0.45, "Apagado", "muted"), - (1.75, "Vívido", "vivid"), - ]: - v = tex_hsv.copy() - v[:, :, 1] = np.clip(v[:, :, 1] * factor, 0, 255) - _save(v, suffix, label) - - return results - - -def list_available_textures() -> list[str]: - allowed = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff", ".exr"} - return [ - str(path.relative_to(TEXTURE_DIR)).replace("\\", "/") - for path in sorted(TEXTURE_DIR.rglob("*")) - if path.is_file() and path.suffix.lower() in allowed - ] - - -def resolve_texture_path(texture_name: str) -> Path: - if not texture_name: - raise HTTPException(status_code=400, detail="Invalid texture_name") - - normalized = texture_name.replace("\\", "/").strip("/") - candidate = (TEXTURE_DIR / normalized).resolve() - base = TEXTURE_DIR.resolve() - - try: - candidate.relative_to(base) - except ValueError as exc: - raise HTTPException(status_code=400, detail="Invalid texture_name") from exc - - if not candidate.exists() or not candidate.is_file(): - raise HTTPException(status_code=404, detail=f"Texture not found: {normalized}") - - return candidate - - -def build_texture_preview_jpeg(texture_path: Path, max_size: int = 320) -> bytes: - pil_img = load_texture_pil_rgb(texture_path) - pil_img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS) - out = io.BytesIO() - pil_img.save(out, format="JPEG", quality=88, optimize=True) - return out.getvalue() - - -def load_texture_pil_rgb(texture_path: Path) -> Image.Image: - suffix = texture_path.suffix.lower() - - if suffix != ".exr": - try: - return Image.open(str(texture_path)).convert("RGB") - except Exception as exc: - raise HTTPException(status_code=500, detail=f"Could not read texture file: {exc}") from exc - - exr = cv2.imread(str(texture_path), cv2.IMREAD_UNCHANGED) - if exr is None: - raise HTTPException(status_code=500, detail="Could not decode EXR texture") - - if exr.ndim == 2: - exr = np.stack([exr, exr, exr], axis=-1) - if exr.ndim != 3: - raise HTTPException(status_code=500, detail="EXR texture has unsupported shape") - if exr.shape[2] > 3: - exr = exr[:, :, :3] - - exr = np.nan_to_num(exr, nan=0.0, posinf=0.0, neginf=0.0) - exr = np.maximum(exr, 0) - - if np.issubdtype(exr.dtype, np.floating): - scale = float(np.percentile(exr, 99.0)) - if scale <= 1e-8: - scale = float(np.max(exr)) - if scale <= 1e-8: - scale = 1.0 - img = np.clip(exr / scale, 0.0, 1.0) - img = np.power(img, 1.0 / 2.2) - img_u8 = (img * 255.0).astype(np.uint8) - elif exr.dtype == np.uint16: - img_u8 = (exr / 257.0).astype(np.uint8) - else: - img_u8 = np.clip(exr, 0, 255).astype(np.uint8) - - img_rgb = cv2.cvtColor(img_u8, cv2.COLOR_BGR2RGB) - return Image.fromarray(img_rgb).convert("RGB") - - -def estimate_mask_orientation_degrees(binary_mask: np.ndarray) -> float: - mask_u8 = (binary_mask > 0).astype(np.uint8) - contours, _ = cv2.findContours(mask_u8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - if not contours: - return 0.0 - - largest = max(contours, key=cv2.contourArea) - if cv2.contourArea(largest) < 25.0: - return 0.0 - - rect = cv2.minAreaRect(largest) - (_, _), (width, height), angle = rect - - dominant_angle = float(angle) - if width < height: - dominant_angle += 90.0 - - dominant_angle %= 180.0 - return dominant_angle - - -def _compute_trapezoid_score_from_mask( - binary_mask: np.ndarray, - ys: np.ndarray, - xs: np.ndarray, - min_y: int, - max_y: int, - bbox_h: int, -) -> float: - """Return 0..1 indicating how floor-like (wider at bottom) the mask shape is.""" - quarter = max(1, bbox_h // 4) - top_xs = xs[ys <= (min_y + quarter)] - bot_xs = xs[ys >= (max_y - quarter)] - if len(top_xs) < 3 or len(bot_xs) < 3: - return 0.0 - top_w = float(top_xs.max() - top_xs.min()) - bot_w = float(bot_xs.max() - bot_xs.min()) - if top_w < 5.0: - return 1.0 if bot_w > 20.0 else 0.0 - ratio = bot_w / top_w - return float(np.clip((ratio - 1.0) / 1.8, 0.0, 1.0)) - - -def _sort_quad_corners(pts: np.ndarray) -> np.ndarray: - """Sort 4 points into [TL, TR, BR, BL] order.""" - result = np.zeros((4, 2), dtype=np.float32) - s = pts[:, 0] + pts[:, 1] - d = pts[:, 0] - pts[:, 1] - result[0] = pts[np.argmin(s)] - result[1] = pts[np.argmax(d)] - result[2] = pts[np.argmax(s)] - result[3] = pts[np.argmin(d)] - return result - - -def _extract_mask_quad(binary_mask: np.ndarray) -> np.ndarray | None: - """Approximate mask as 4-corner polygon sorted [TL, TR, BR, BL], or None.""" - mask_u8 = (binary_mask > 0).astype(np.uint8) - contours, _ = cv2.findContours(mask_u8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - if not contours: - return None - largest = max(contours, key=cv2.contourArea) - if cv2.contourArea(largest) < 400.0: - return None - hull = cv2.convexHull(largest) - peri = cv2.arcLength(hull, True) - for eps_frac in (0.03, 0.05, 0.08, 0.10, 0.13): - approx = cv2.approxPolyDP(hull, eps_frac * peri, True) - if len(approx) == 4: - return _sort_quad_corners(approx.reshape(4, 2).astype(np.float32)) - return None - - -def _tile_texture_perspective( - tex_pil: Image.Image, - quad: np.ndarray, - image_width: int, - image_height: int, -) -> Image.Image | None: - """ - Tile texture with perspective correction for a floor surface. - quad: [TL, TR, BR, BL] in image coordinates. - Returns full-image-size PIL image with warped tiled texture, or None on failure. - Pixels outside the perspective quad are filled with regular tiling to avoid black gaps. - """ - tl, tr, br, bl = quad - bot_w = float(np.linalg.norm(br.astype(float) - bl.astype(float))) - top_w = float(np.linalg.norm(tr.astype(float) - tl.astype(float))) - left_h = float(np.linalg.norm(bl.astype(float) - tl.astype(float))) - right_h = float(np.linalg.norm(br.astype(float) - tr.astype(float))) - rect_w = min(int(max(bot_w, top_w)) + 1, image_width * 2) - rect_h = min(int(max(left_h, right_h)) + 1, image_height * 2) - if rect_w < 8 or rect_h < 8: - return None - tex_arr = np.array(tex_pil.convert("RGB"), dtype=np.uint8) - th, tw = tex_arr.shape[:2] - if tw < 1 or th < 1: - return None - rect_tiled = np.zeros((rect_h, rect_w, 3), dtype=np.uint8) - for ry in range(0, rect_h, th): - for rx in range(0, rect_w, tw): - py = min(th, rect_h - ry) - px = min(tw, rect_w - rx) - rect_tiled[ry : ry + py, rx : rx + px] = tex_arr[:py, :px] - src_pts = np.array( - [[0.0, 0.0], [float(rect_w - 1), 0.0], [float(rect_w - 1), float(rect_h - 1)], [0.0, float(rect_h - 1)]], - dtype=np.float32, - ) - dst_pts = quad.astype(np.float32) - try: - H = cv2.getPerspectiveTransform(src_pts, dst_pts) - warped = cv2.warpPerspective(rect_tiled, H, (image_width, image_height)) - # Mapa de cobertura: píxeles realmente cubiertos por el warp - cov_src = np.ones((rect_h, rect_w), dtype=np.uint8) * 255 - coverage = cv2.warpPerspective(cov_src, H, (image_width, image_height)) - except cv2.error: - return None - - # Rellenar píxeles sin cobertura (fuera del quad) con tiling regular - # para evitar espacios negros donde la máscara supera el quad aproximado - regular = np.zeros((image_height, image_width, 3), dtype=np.uint8) - for ry in range(0, image_height, th): - for rx in range(0, image_width, tw): - py = min(th, image_height - ry) - px = min(tw, image_width - rx) - regular[ry : ry + py, rx : rx + px] = tex_arr[:py, :px] - - uncovered = coverage < 128 - warped[uncovered] = regular[uncovered] - - return Image.fromarray(warped) - - -def classify_texture_material(texture_name: str) -> str: - texture_key = texture_name.lower() - if "deck" in texture_key: - return "deck" - if "acm" in texture_key or "wpc" in texture_key: - return "acm" - if any(hint in texture_key for hint in ("wood", "plank", "laminate", "floor")): - return "wood" - if any(hint in texture_key for hint in ("marble", "granite", "tile", "brick", "cobblestone", "stone", "cartago", "riverbed")): - return "stone" - if any(hint in texture_key for hint in ("metal", "rust", "iron", "steel")): - return "metal" - return "generic" - - -def infer_surface_type_and_direction( - binary_mask: np.ndarray, - image_width: int, - image_height: int, - texture_name: str, -) -> tuple[str, float, float, int]: - mask_u8 = (binary_mask > 0).astype(np.uint8) - ys, xs = np.where(mask_u8 > 0) - if ys.size == 0 or xs.size == 0: - return ("wall", 0.0, 0.78, max(180, image_width // 4)) - - min_x, max_x = int(xs.min()), int(xs.max()) - min_y, max_y = int(ys.min()), int(ys.max()) - bbox_w = max(1, max_x - min_x + 1) - bbox_h = max(1, max_y - min_y + 1) - aspect = bbox_w / max(1.0, float(bbox_h)) - center_y = float(ys.mean()) / max(1.0, float(image_height)) - dominant_angle = estimate_mask_orientation_degrees(binary_mask) - material = classify_texture_material(texture_name) - - # Trapezoid score: floor in perspective is wider at the bottom than the top - trapezoid_score = _compute_trapezoid_score_from_mask(binary_mask, ys, xs, min_y, max_y, bbox_h) - - is_ceiling = center_y < 0.26 and aspect > 1.35 - # Floor: low center + trapezoidal shape, OR clearly low + wide - is_floor = ( - (center_y > 0.55 and aspect >= 0.9 and trapezoid_score > 0.30) - or (center_y > 0.68 and aspect > 1.15) - ) - - if is_ceiling: - surface_type = "ceiling" - angle = 0.0 - blend_alpha = 0.58 - tile_width = max(128, image_width // 5) - elif is_floor: - if material in ("deck", "wood"): - surface_type = "deck" - angle = 0.0 # Always straight, no perspective distortion for deck - blend_alpha = 0.82 - tile_width = max(240, int(bbox_w * 0.55), image_width // 3) - else: - surface_type = "floor" - angle = 0.0 - blend_alpha = 0.80 - # ACM floor: ~3 large-format panels visible on the near edge - tile_width = max(200, int(bbox_w * 0.35)) if material == "acm" else max(144, image_width // 3) - else: - surface_type = "wall" - angle = 0.0 - if material == "acm": - blend_alpha = 0.78 - # ACM wall panels: ~3 panels across the surface width - tile_width = max(180, int(bbox_w * 0.33)) - elif material == "wood": - blend_alpha = 0.70 - tile_width = max(220, int(bbox_w * 0.55), image_width // 4) - elif material == "stone": - blend_alpha = 0.84 - tile_width = max(128, image_width // 4) - else: - blend_alpha = 0.66 - tile_width = max(128, image_width // 4) - - return (surface_type, float(angle % 180.0), float(blend_alpha), int(tile_width)) - - -def choose_auto_texture_settings(material: str, surface_type: str) -> tuple[float, float, float]: - strength_map = {"acm": 0.98, "stone": 0.96, "wood": 0.88, "deck": 0.88, "metal": 0.91, "generic": 0.9} - intensity_map = {"acm": 0.08, "stone": 0.36, "wood": 0.3, "deck": 0.3, "metal": 0.34, "generic": 0.32} - - strength = float(strength_map.get(material, 0.9)) - intensity = float(intensity_map.get(material, 0.32)) - - if surface_type in {"wall", "facade"}: - strength += 0.02 - angle = 28.0 - elif surface_type in {"roof"}: - angle = 42.0 - intensity += 0.03 - elif surface_type in {"floor", "deck"}: - angle = 24.0 - intensity += 0.02 - else: - angle = 35.0 - - return ( - float(np.clip(strength, 0.55, 0.99)), - float(angle % 360.0), - float(np.clip(intensity, 0.0, 1.0)), - ) - - -def build_feather_mask(binary_mask: np.ndarray, sigma: float = 2.2) -> np.ndarray: - mask = (binary_mask > 0).astype(np.float32) - if mask.max() <= 0: - return mask - feather = cv2.GaussianBlur(mask, (0, 0), sigmaX=sigma, sigmaY=sigma) - return np.clip(feather, 0.0, 1.0) - - -def build_scene_luminance_map(orig_rgb: np.ndarray) -> np.ndarray: - orig_u8 = orig_rgb.astype(np.uint8) - orig_lab = cv2.cvtColor(orig_u8, cv2.COLOR_RGB2LAB).astype(np.float32) - l_channel = orig_lab[:, :, 0] / 255.0 - broad_light = cv2.GaussianBlur(l_channel, (0, 0), sigmaX=18.0, sigmaY=18.0) - local_detail = l_channel - cv2.GaussianBlur(l_channel, (0, 0), sigmaX=4.0, sigmaY=4.0) - light_map = 0.82 + (broad_light * 0.36) + (local_detail * 0.18) - return np.clip(light_map, 0.72, 1.22) - - -def build_texture_relief_map(tex_rgb: np.ndarray, material: str = "generic") -> np.ndarray: - tex_u8 = tex_rgb.astype(np.uint8) - tex_gray = cv2.cvtColor(tex_u8, cv2.COLOR_RGB2GRAY).astype(np.float32) / 255.0 - micro_relief = tex_gray - cv2.GaussianBlur(tex_gray, (0, 0), sigmaX=3.0, sigmaY=3.0) - - relief_scale = {"acm": 0.35, "stone": 2.8, "wood": 2.2, "deck": 2.2, "metal": 1.8}.get(material, 2.0) - return np.clip(micro_relief * relief_scale, -1.0, 1.0) - - -def build_directional_light_map( - tex_rgb: np.ndarray, - material: str, - light_angle_degrees: float, - light_intensity: float, -) -> np.ndarray: - tex_u8 = tex_rgb.astype(np.uint8) - tex_gray = cv2.cvtColor(tex_u8, cv2.COLOR_RGB2GRAY).astype(np.float32) / 255.0 - height_map = cv2.GaussianBlur(tex_gray, (0, 0), sigmaX=1.4, sigmaY=1.4) - grad_x = cv2.Sobel(height_map, cv2.CV_32F, 1, 0, ksize=3) - grad_y = cv2.Sobel(height_map, cv2.CV_32F, 0, 1, ksize=3) - - relief_scale = {"acm": 0.45, "stone": 3.0, "wood": 2.4, "deck": 2.4, "metal": 1.6}.get(material, 2.0) - - nx = -grad_x * relief_scale - ny = -grad_y * relief_scale - nz = np.ones_like(nx, dtype=np.float32) - norm = np.sqrt((nx * nx) + (ny * ny) + (nz * nz)) + 1e-6 - nx = nx / norm - ny = ny / norm - nz = nz / norm - - theta = np.deg2rad(float(light_angle_degrees)) - lx = float(np.cos(theta)) - ly = float(-np.sin(theta)) - lz = 0.82 - light_norm = max(1e-6, float(np.sqrt((lx * lx) + (ly * ly) + (lz * lz)))) - lx /= light_norm - ly /= light_norm - lz /= light_norm - - diffuse = np.clip((nx * lx) + (ny * ly) + (nz * lz), 0.0, 1.0) - strength = float(np.clip(light_intensity, 0.0, 1.0)) - if material == "acm": - return np.clip(0.97 + (diffuse * (0.03 + (0.12 * strength))), 0.95, 1.12) - if material == "deck": - return np.clip(0.88 + (diffuse * (0.12 + (0.50 * strength))), 0.76, 1.28) - return np.clip(0.86 + (diffuse * (0.14 + (0.60 * strength))), 0.72, 1.35) - - -def build_mask_edge_occlusion(binary_mask: np.ndarray, light_intensity: float) -> np.ndarray: - mask_u8 = (binary_mask > 0).astype(np.uint8) - if mask_u8.max() == 0: - return np.ones(mask_u8.shape, dtype=np.float32) - - distance = cv2.distanceTransform(mask_u8, cv2.DIST_L2, 5).astype(np.float32) - inner_values = distance[mask_u8 > 0] - if inner_values.size == 0: - return np.ones(mask_u8.shape, dtype=np.float32) - - max_distance = max(1.0, float(np.percentile(inner_values, 95))) - normalized = np.clip(distance / (max_distance * 0.16), 0.0, 1.0) - edge_strength = 1.0 - normalized - occlusion = 1.0 - (edge_strength * (0.04 + (0.08 * float(np.clip(light_intensity, 0.0, 1.0))))) - occlusion[mask_u8 == 0] = 1.0 - return np.clip(occlusion, 0.88, 1.0) - - -def apply_surface_lighting( - tex_rgb: np.ndarray, - orig_rgb: np.ndarray, - binary_mask: np.ndarray, - material: str, - lighting_mode: str, - light_angle_degrees: float, - light_intensity: float, -) -> np.ndarray: - scene_light = build_scene_luminance_map(orig_rgb) - directional_light = build_directional_light_map(tex_rgb, material, light_angle_degrees, light_intensity) - relief_map = build_texture_relief_map(tex_rgb, material) - edge_occlusion = build_mask_edge_occlusion(binary_mask, light_intensity) - - if material == "acm": - if lighting_mode == "directional": - light_map = directional_light - elif lighting_mode == "flat": - light_map = np.ones(scene_light.shape, dtype=np.float32) - else: - # 45 % scene luminance so ACM panels inherit shadows/gradients from photo - light_map = (scene_light * 0.45) + (directional_light * 0.55) - elif lighting_mode == "directional": - light_map = directional_light - elif lighting_mode == "flat": - light_map = np.ones(scene_light.shape, dtype=np.float32) - else: - light_map = (scene_light * 0.78) + (directional_light * 0.22) - - detail_scale = 0.02 + (0.05 * float(np.clip(light_intensity, 0.0, 1.0))) if material == "acm" else 0.08 + (0.18 * float(np.clip(light_intensity, 0.0, 1.0))) - detail_boost = 1.0 + (relief_map * detail_scale) - enhanced = tex_rgb.astype(np.float32) - enhanced *= light_map[:, :, None] - enhanced *= detail_boost[:, :, None] - enhanced *= edge_occlusion[:, :, None] - return np.clip(enhanced, 0, 255).astype(np.uint8) - - -def blend_texture_preserve_shading( - orig_rgb: np.ndarray, - tex_rgb: np.ndarray, - alpha_mask: np.ndarray, - blend_alpha: float, - material: str = "generic", -) -> np.ndarray: - orig_u8 = orig_rgb.astype(np.uint8) - tex_u8 = tex_rgb.astype(np.uint8) - - orig_lab = cv2.cvtColor(orig_u8, cv2.COLOR_RGB2LAB).astype(np.float32) - tex_lab = cv2.cvtColor(tex_u8, cv2.COLOR_RGB2LAB).astype(np.float32) - - mixed_lab = tex_lab.copy() - if material == "acm": - # 30 % original luminance → panels inherit scene shadows while keeping panel colour - mixed_lab[:, :, 0] = (0.30 * orig_lab[:, :, 0]) + (0.70 * tex_lab[:, :, 0]) - mixed_lab[:, :, 1] = (0.97 * tex_lab[:, :, 1]) + (0.03 * orig_lab[:, :, 1]) - mixed_lab[:, :, 2] = (0.97 * tex_lab[:, :, 2]) + (0.03 * orig_lab[:, :, 2]) - elif material in ("wood", "deck"): - mixed_lab[:, :, 0] = (0.78 * orig_lab[:, :, 0]) + (0.22 * tex_lab[:, :, 0]) - mixed_lab[:, :, 1] = (0.9 * tex_lab[:, :, 1]) + (0.1 * orig_lab[:, :, 1]) - mixed_lab[:, :, 2] = (0.9 * tex_lab[:, :, 2]) + (0.1 * orig_lab[:, :, 2]) - elif material == "stone": - orig_l_base = cv2.GaussianBlur(orig_lab[:, :, 0], (0, 0), sigmaX=11.0, sigmaY=11.0) - mixed_lab[:, :, 0] = (0.18 * orig_l_base) + (0.82 * tex_lab[:, :, 0]) - mixed_lab[:, :, 1] = (0.95 * tex_lab[:, :, 1]) + (0.05 * orig_lab[:, :, 1]) - mixed_lab[:, :, 2] = (0.95 * tex_lab[:, :, 2]) + (0.05 * orig_lab[:, :, 2]) - else: - mixed_lab[:, :, 0] = orig_lab[:, :, 0] - mixed_lab[:, :, 1] = (0.8 * tex_lab[:, :, 1]) + (0.2 * orig_lab[:, :, 1]) - mixed_lab[:, :, 2] = (0.8 * tex_lab[:, :, 2]) + (0.2 * orig_lab[:, :, 2]) - - shaded_tex = cv2.cvtColor(np.clip(mixed_lab, 0, 255).astype(np.uint8), cv2.COLOR_LAB2RGB).astype(np.float32) - if material in ("wood", "deck"): - tex_gray = cv2.cvtColor(tex_u8, cv2.COLOR_RGB2GRAY).astype(np.float32) - tex_base = cv2.GaussianBlur(tex_gray, (0, 0), sigmaX=9.0, sigmaY=9.0) - tex_detail = np.clip((tex_gray - tex_base) / 255.0, -0.35, 0.35) - shaded_tex *= (1.0 + (tex_detail[:, :, None] * 0.28)) - - alpha = np.clip(alpha_mask[:, :, None] * float(blend_alpha), 0.0, 1.0) - composite = (orig_rgb * (1.0 - alpha)) + (shaded_tex * alpha) - return np.clip(composite, 0, 255).astype(np.uint8) - - -def blend_texture_direct( - orig_rgb: np.ndarray, - tex_rgb: np.ndarray, - alpha_mask: np.ndarray, - blend_alpha: float, -) -> np.ndarray: - alpha = np.clip(alpha_mask[:, :, None] * float(blend_alpha), 0.0, 1.0) - composite = (orig_rgb * (1.0 - alpha)) + (tex_rgb * alpha) - return np.clip(composite, 0, 255).astype(np.uint8) - - -def apply_local_texture_sync(payload: ApplyTextureRequest) -> dict[str, Any]: - step = "APPLY_TEXTURE" - started = log_timing_start(step) - try: - safe_name = Path(payload.filename).name - if not safe_name: - raise HTTPException(status_code=400, detail="Invalid filename") - - label_safe_name = Path(payload.original_filename).name if payload.original_filename else safe_name - - image_path = UPLOAD_DIR / safe_name - if not image_path.exists() or not image_path.is_file(): - image_path = OUTPUT_DIR / safe_name - - if (not image_path.exists() or not image_path.is_file()) and payload.original_filename: - orig_name = Path(payload.original_filename).name - image_path = UPLOAD_DIR / orig_name - if not image_path.exists() or not image_path.is_file(): - image_path = OUTPUT_DIR / orig_name - - if not image_path.exists() or not image_path.is_file(): - raise HTTPException( - status_code=404, - detail=f"Image not found: {safe_name} (also tried original: {payload.original_filename or 'n/a'})", - ) - - masks_dir = UPLOAD_DIR / "masks" - masks_dir.mkdir(exist_ok=True) - label_owner = Path(image_path).stem - label_path = masks_dir / f"{label_owner}_labels.png" - if not label_path.exists() and payload.original_filename: - alt_owner = Path(payload.original_filename).name - alt_label = masks_dir / f"{alt_owner}_labels.png" - if alt_label.exists(): - label_path = alt_label - - if not label_path.exists(): - raise HTTPException( - status_code=404, - detail=f"Label map not found for {label_owner}. Upload/segment the image first.", - ) - - if not payload.mask_indices: - raise HTTPException(status_code=400, detail="No mask indices provided") - - texture_path = resolve_texture_path(payload.texture_name) - orig_pil = Image.open(str(image_path)).convert("RGB") - width, height = orig_pil.size - - label_map = cv2.imread(str(label_path), cv2.IMREAD_GRAYSCALE) - if label_map is None: - raise HTTPException(status_code=500, detail="Could not read label map") - - binary_mask = np.zeros((label_map.shape[0], label_map.shape[1]), dtype=np.uint8) - for idx in payload.mask_indices: - binary_mask |= (label_map == idx).astype(np.uint8) - - if binary_mask.max() == 0: - raise HTTPException(status_code=400, detail="None of the selected segments were found in the label map.") - - direction_mode = str(payload.direction_mode or "auto").strip().lower() - if direction_mode not in {"auto", "manual", "none"}: - raise HTTPException(status_code=400, detail="Invalid direction_mode. Use auto, manual, or none.") - - replace_mode = str(getattr(payload, "replace_mode", "realistic") or "realistic").strip().lower() - lighting_mode = str(getattr(payload, "lighting_mode", "scene") or "scene").strip().lower() - - material = classify_texture_material(payload.texture_name) - - # Integración: si la textura es 'acm' y hay un Gradio Space configurado, - # delegar el render al Space usando el preset 'ACM' (si existe). - # Construimos un label_map reducido (1 = unión de máscaras seleccionadas) - # y enviamos la textura como base64 para evitar dependencias de archivos. - try: - import json as _json - import base64 as _base64 - from services.gradio_client_service import is_gradio_enabled, render_via_gradio_sync - from services.presets_service import get_preset - - try: - # Activar render remoto para ACM y también para materiales 'deck' - _use_remote = is_gradio_enabled() and material in {"acm", "deck"} - except Exception: - _use_remote = False - - if _use_remote: - # Preparar label_map de unión - lm = cv2.imread(str(label_path), cv2.IMREAD_GRAYSCALE) - if lm is None: - raise HTTPException(status_code=500, detail="Could not read label map for remote render") - - union = np.zeros_like(lm, dtype=np.uint8) - for idx in payload.mask_indices: - union[lm == int(idx)] = 1 - - # Encode label_map (PNG) -> base64 - buf = io.BytesIO() - Image.fromarray(union, mode="L").save(buf, format="PNG") - label_map_b64 = _base64.b64encode(buf.getvalue()).decode("utf-8") - - # Encodear textura como base64 - tex_path = resolve_texture_path(payload.texture_name) - tex_pil = load_texture_pil_rgb(tex_path) - tbuf = io.BytesIO() - tex_pil.save(tbuf, format="PNG") - texture_b64 = _base64.b64encode(tbuf.getvalue()).decode("utf-8") - - # Intentar obtener preset correspondiente: - # - Si el nombre contiene 'wpc' + 'deck' -> 'WPC_DECK' - # - Si contiene solo 'wpc' -> 'WPC' - # - En otro caso -> 'ACM' - preset = {} - try: - _tex_key = (payload.texture_name or "").lower() - if "wpc" in _tex_key: - if "deck" in _tex_key: - preset = get_preset("WPC_DECK") - else: - preset = get_preset("WPC") - else: - preset = get_preset("ACM") - except Exception: - preset = {} - - # Detectar tipo de superficie para que el Space oriente correctamente - # la textura (piso=horizontal, pared=vertical) - try: - _surface_type, _, _, _ = infer_surface_type_and_direction( - binary_mask, width, height, payload.texture_name - ) - except Exception: - _surface_type = "wall" - - # Merge: preset base + surface_type (surface_type tiene prioridad - # sobre la orientacion del preset cuando es piso o deck) - params_dict = dict(preset or {}) - params_dict["surface_type"] = _surface_type - params_json = _json.dumps(params_dict) - - try: - rendered_img_np, meta = render_via_gradio_sync( - image_path, label_map_b64, 1, None, texture_b64, params_json - ) - except Exception as _e: - logger.warning("Remote render via Gradio failed, falling back to local: %s", _e) - rendered_img_np = None - - if rendered_img_np is not None: - # Guardar resultado remoto con mismo formato de salida - input_stem = Path(image_path).stem - edit_suffix = uuid.uuid4().hex[:8] - out_filename = f"{input_stem}_edit_{edit_suffix}.jpg" - out_path = UPLOAD_DIR / out_filename - # Aceptar tanto np.ndarray como PIL.Image devueltos por el Space - try: - if isinstance(rendered_img_np, np.ndarray): - img_to_save = Image.fromarray(rendered_img_np) - elif isinstance(rendered_img_np, Image.Image): - img_to_save = rendered_img_np - else: - # manejar strings: pueden ser rutas locales, URLs o base64 - if isinstance(rendered_img_np, str): - s = rendered_img_np.strip() - # 1) ruta local descargada por gradio_client - try: - p = Path(s) - if p.exists(): - img_to_save = Image.open(str(p)) - else: - # 2) intentar descargar si es URL - if s.startswith("http://") or s.startswith("https://") or "/gradio_api/file=" in s: - try: - from urllib.request import urlopen as _urlopen - - with _urlopen(s) as resp: - data = resp.read() - img_to_save = Image.open(io.BytesIO(data)) - except Exception: - # 3) fallback: intentar decodificar base64 - try: - b = _base64.b64decode(s) - img_to_save = Image.open(io.BytesIO(b)) - except Exception: - raise RuntimeError("Unsupported rendered image string from Gradio Space") - else: - # 3) fallback: intentar decodificar base64 - try: - b = _base64.b64decode(s) - img_to_save = Image.open(io.BytesIO(b)) - except Exception: - raise RuntimeError("Unsupported rendered image string from Gradio Space") - except Exception: - raise RuntimeError("Unsupported rendered image string from Gradio Space") - else: - # intentar convertir bytes -> Image - try: - img_to_save = Image.open(io.BytesIO(rendered_img_np)) - except Exception: - raise RuntimeError("Unsupported rendered image type from Gradio Space") - - img_to_save.convert("RGB").save(str(out_path), format="JPEG", quality=UPLOAD_JPEG_QUALITY, optimize=True) - except Exception: - logger.exception("Failed to save rendered image from Gradio Space") - raise - - try: - out_label_path = masks_dir / f"{Path(out_filename).stem}_labels.png" - if label_path.exists(): - shutil.copyfile(str(label_path), str(out_label_path)) - except Exception: - logger.exception("Failed to copy label map for remote output image") - - return { - "message": "Texture applied successfully (remote)", - "original": safe_name, - "mask_indices": payload.mask_indices, - "texture_name": payload.texture_name, - "material": material, - "direction_mode": direction_mode, - "surface_type": None, - "replace_mode": replace_mode, - "replace_strength": None, - "lighting_mode": lighting_mode, - "light_angle_degrees": None, - "light_intensity": None, - "blend_alpha": None, - "applied_angle_degrees": None, - "output_filename": out_filename, - "output_url": f"/seg/image/{out_filename}", - } - except HTTPException: - raise - except Exception: - logger.exception("Error during remote render integration (continuing local flow)") - - surface_type, inferred_angle, blend_alpha, target_w = infer_surface_type_and_direction( - binary_mask, width, height, payload.texture_name, - ) - replace_strength, light_angle_degrees, light_intensity = choose_auto_texture_settings(material, surface_type) - effective_alpha = float(np.clip(blend_alpha * (0.55 + (0.75 * replace_strength)), 0.0, 0.98)) - if material == "acm": - effective_alpha = float(max(effective_alpha, 0.92)) - - applied_angle = 0.0 - if direction_mode == "auto": - applied_angle = inferred_angle - elif direction_mode == "manual": - applied_angle = float(payload.angle_degrees) - - tex_pil = load_texture_pil_rgb(texture_path) - - # Escalar al tamaño de tile deseado ANTES de rotar - tex_w, tex_h = tex_pil.size - scale = target_w / max(1, tex_w) - if abs(scale - 1.0) > 0.05: - tex_pil = tex_pil.resize( - (max(1, int(tex_w * scale)), max(1, int(tex_h * scale))), - Image.Resampling.LANCZOS, - ) - tex_w, tex_h = tex_pil.size - - tiled: Image.Image | None = None - - if tiled is None: - if abs(applied_angle) > 0.01: - # Tile on a large canvas first, then rotate the full canvas to avoid black corners - diag = int(np.ceil(np.sqrt(width ** 2 + height ** 2))) + max(tex_w, tex_h) - large_w = width + diag - large_h = height + diag - large = Image.new("RGB", (large_w, large_h)) - for y in range(0, large_h, tex_h): - for x in range(0, large_w, tex_w): - large.paste(tex_pil, (x, y)) - large = large.rotate(-applied_angle, resample=Image.Resampling.BICUBIC, expand=False) - cx = (large_w - width) // 2 - cy = (large_h - height) // 2 - tiled = large.crop((cx, cy, cx + width, cy + height)) - else: - tiled = Image.new("RGB", (width, height)) - for y in range(0, height, tex_h): - for x in range(0, width, tex_w): - tiled.paste(tex_pil, (x, y)) - - orig_u8 = np.array(orig_pil, dtype=np.uint8) - - try: - if bool(getattr(payload, "clear_mask_before_apply", False)): - orig_candidate = None - if payload.original_filename: - cand = UPLOAD_DIR / Path(payload.original_filename).name - if cand.exists() and cand.is_file(): - orig_candidate = cand - else: - cand2 = OUTPUT_DIR / Path(payload.original_filename).name - if cand2.exists() and cand2.is_file(): - orig_candidate = cand2 - if orig_candidate is None: - stem = Path(image_path).stem - if "_edit_" in stem: - base_name = stem.split("_edit_")[0] + ".jpg" - cand = UPLOAD_DIR / base_name - if cand.exists() and cand.is_file(): - orig_candidate = cand - else: - cand2 = OUTPUT_DIR / base_name - if cand2.exists() and cand2.is_file(): - orig_candidate = cand2 - - if orig_candidate is not None: - try: - base_pil = Image.open(str(orig_candidate)).convert("RGB") - if base_pil.size != (width, height): - base_pil = base_pil.resize((width, height), Image.Resampling.LANCZOS) - base_arr = np.array(base_pil, dtype=np.uint8) - mask_bool = (binary_mask > 0) - orig_u8[mask_bool] = base_arr[mask_bool] - logger.info(f"[APPLY_TEXTURE] cleared mask from original source: {orig_candidate}") - except Exception: - logger.exception("Failed to restore original pixels for clear_mask_before_apply") - except Exception: - logger.exception("Error handling clear_mask_before_apply") - - tiled_arr = np.array(tiled, dtype=np.uint8) - - # Parchar píxeles muy oscuros (suma R+G+B < 20) dentro de la máscara. - # Cubren tanto negro exacto (0,0,0) como píxeles casi negros que el warp - # de perspectiva puede generar en bordes del quad o zonas sin cobertura. - mask_bool = binary_mask > 0 - dark_in_mask = mask_bool & (tiled_arr.sum(axis=2) < 20) - if dark_in_mask.any(): - th_f, tw_f = tex_pil.size[1], tex_pil.size[0] - tex_np = np.array(tex_pil, dtype=np.uint8) - ys_b, xs_b = np.where(dark_in_mask) - tiled_arr[ys_b, xs_b] = tex_np[ys_b % th_f, xs_b % tw_f] - - lit_tex = apply_surface_lighting( - tiled_arr, - orig_u8, - binary_mask, - material, - lighting_mode, - light_angle_degrees, - light_intensity, - ) - - orig_arr = orig_u8.astype(np.float32) - tex_arr = lit_tex.astype(np.float32) - - if replace_mode in {"hard", "absolute", "force", "replace"}: - mask_bool = (binary_mask > 0).astype(bool) - composite_arr = orig_arr.copy() - composite_arr[mask_bool] = tex_arr[mask_bool] - composite = np.clip(composite_arr, 0, 255).astype(np.uint8) - else: - feather_mask = build_feather_mask(binary_mask) - # All materials use shading-preservation so scene luminance (shadows/highlights) - # from the original photo is transferred onto the texture. - composite = blend_texture_preserve_shading(orig_arr, tex_arr, feather_mask, effective_alpha, material) - - input_stem = Path(image_path).stem - edit_suffix = uuid.uuid4().hex[:8] - out_filename = f"{input_stem}_edit_{edit_suffix}.jpg" - out_path = UPLOAD_DIR / out_filename - Image.fromarray(composite).save(str(out_path), format="JPEG", quality=UPLOAD_JPEG_QUALITY, optimize=True) - - try: - out_label_path = masks_dir / f"{Path(out_filename).stem}_labels.png" - if label_path.exists(): - shutil.copyfile(str(label_path), str(out_label_path)) - except Exception: - logger.exception("Failed to copy label map for output image") - - return { - "message": "Texture applied successfully", - "original": safe_name, - "mask_indices": payload.mask_indices, - "texture_name": payload.texture_name, - "material": material, - "direction_mode": direction_mode, - "surface_type": surface_type, - "replace_mode": replace_mode, - "replace_strength": round(replace_strength, 3), - "lighting_mode": lighting_mode, - "light_angle_degrees": round(light_angle_degrees, 2), - "light_intensity": round(light_intensity, 3), - "blend_alpha": round(effective_alpha, 3), - "applied_angle_degrees": round(applied_angle, 2), - "output_filename": out_filename, - "output_url": f"/seg/image/{out_filename}", - } - finally: - log_timing_end(step, started) +import io +import shutil +import uuid +from pathlib import Path +from typing import Any + +import cv2 +import numpy as np +from fastapi import HTTPException +from PIL import Image + +from core.config import ( + OUTPUT_DIR, + TEXTURE_DIR, + UPLOAD_DIR, + UPLOAD_JPEG_QUALITY, + log_timing_end, + log_timing_start, + logger, +) +from models.schemas import ApplyTextureRequest + + +def generate_texture_variations(texture_name: str) -> list[dict[str, str]]: + """ + Genera variaciones de color/brillo/saturación de una textura de referencia usando HSV. + Los archivos se cachean en TEXTURE_DIR/generated/ — si ya existen no se regeneran. + Devuelve lista de {ref, label, preview_url}. + """ + texture_path = resolve_texture_path(texture_name) + generated_dir = TEXTURE_DIR / "generated" + generated_dir.mkdir(parents=True, exist_ok=True) + + tex_pil = load_texture_pil_rgb(texture_path) + tex_bgr = cv2.cvtColor(np.array(tex_pil, dtype=np.uint8), cv2.COLOR_RGB2BGR) + tex_hsv = cv2.cvtColor(tex_bgr, cv2.COLOR_BGR2HSV).astype(np.int32) + + base_stem = Path(texture_name).stem + results: list[dict[str, str]] = [] + + def _save(hsv: np.ndarray, suffix: str, label: str) -> None: + fname = f"{base_stem}__{suffix}.jpg" + out_path = generated_dir / fname + if not out_path.exists(): + bgr = cv2.cvtColor(np.clip(hsv, 0, 255).astype(np.uint8), cv2.COLOR_HSV2BGR) + rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) + Image.fromarray(rgb).save(str(out_path), format="JPEG", quality=92, optimize=True) + ref = f"generated/{fname}" + results.append({"ref": ref, "label": label, "preview_url": f"/seg/texture-preview/{ref}"}) + + # Rotaciones de tono: 12 pasos de 30° recorriendo el círculo cromático + # En OpenCV HSV, H ∈ [0,179] → shift en grados / 2 + for deg, label in [ + (30, "Naranja"), + (60, "Amarillo"), + (90, "Verde lima"), + (120, "Verde"), + (150, "Verde agua"), + (165, "Cyan"), + (180, "Azul cielo"), + (210, "Azul"), + (240, "Índigo"), + (270, "Violeta"), + (300, "Magenta"), + (330, "Rosa"), + ]: + v = tex_hsv.copy() + v[:, :, 0] = (v[:, :, 0] + deg // 2) % 180 + _save(v, f"hue{deg}", label) + + # Variaciones de brillo + for factor, label, suffix in [ + (0.45, "Oscuro", "dark"), + (1.55, "Claro", "light"), + ]: + v = tex_hsv.copy() + v[:, :, 2] = np.clip(v[:, :, 2] * factor, 0, 255) + _save(v, suffix, label) + + # Variaciones de saturación + for factor, label, suffix in [ + (0.0, "Gris", "gray"), + (0.45, "Apagado", "muted"), + (1.75, "Vívido", "vivid"), + ]: + v = tex_hsv.copy() + v[:, :, 1] = np.clip(v[:, :, 1] * factor, 0, 255) + _save(v, suffix, label) + + return results + + +def list_available_textures() -> list[str]: + allowed = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff", ".exr"} + return [ + str(path.relative_to(TEXTURE_DIR)).replace("\\", "/") + for path in sorted(TEXTURE_DIR.rglob("*")) + if path.is_file() and path.suffix.lower() in allowed + ] + + +def resolve_texture_path(texture_name: str) -> Path: + if not texture_name: + raise HTTPException(status_code=400, detail="Invalid texture_name") + + normalized = texture_name.replace("\\", "/").strip("/") + candidate = (TEXTURE_DIR / normalized).resolve() + base = TEXTURE_DIR.resolve() + + try: + candidate.relative_to(base) + except ValueError as exc: + raise HTTPException(status_code=400, detail="Invalid texture_name") from exc + + if not candidate.exists() or not candidate.is_file(): + raise HTTPException(status_code=404, detail=f"Texture not found: {normalized}") + + return candidate + + +def build_texture_preview_jpeg(texture_path: Path, max_size: int = 320) -> bytes: + pil_img = load_texture_pil_rgb(texture_path) + pil_img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS) + out = io.BytesIO() + pil_img.save(out, format="JPEG", quality=88, optimize=True) + return out.getvalue() + + +def load_texture_pil_rgb(texture_path: Path) -> Image.Image: + suffix = texture_path.suffix.lower() + + if suffix != ".exr": + try: + return Image.open(str(texture_path)).convert("RGB") + except Exception as exc: + raise HTTPException(status_code=500, detail=f"Could not read texture file: {exc}") from exc + + exr = cv2.imread(str(texture_path), cv2.IMREAD_UNCHANGED) + if exr is None: + raise HTTPException(status_code=500, detail="Could not decode EXR texture") + + if exr.ndim == 2: + exr = np.stack([exr, exr, exr], axis=-1) + if exr.ndim != 3: + raise HTTPException(status_code=500, detail="EXR texture has unsupported shape") + if exr.shape[2] > 3: + exr = exr[:, :, :3] + + exr = np.nan_to_num(exr, nan=0.0, posinf=0.0, neginf=0.0) + exr = np.maximum(exr, 0) + + if np.issubdtype(exr.dtype, np.floating): + scale = float(np.percentile(exr, 99.0)) + if scale <= 1e-8: + scale = float(np.max(exr)) + if scale <= 1e-8: + scale = 1.0 + img = np.clip(exr / scale, 0.0, 1.0) + img = np.power(img, 1.0 / 2.2) + img_u8 = (img * 255.0).astype(np.uint8) + elif exr.dtype == np.uint16: + img_u8 = (exr / 257.0).astype(np.uint8) + else: + img_u8 = np.clip(exr, 0, 255).astype(np.uint8) + + img_rgb = cv2.cvtColor(img_u8, cv2.COLOR_BGR2RGB) + return Image.fromarray(img_rgb).convert("RGB") + + +def estimate_mask_orientation_degrees(binary_mask: np.ndarray) -> float: + mask_u8 = (binary_mask > 0).astype(np.uint8) + contours, _ = cv2.findContours(mask_u8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + if not contours: + return 0.0 + + largest = max(contours, key=cv2.contourArea) + if cv2.contourArea(largest) < 25.0: + return 0.0 + + rect = cv2.minAreaRect(largest) + (_, _), (width, height), angle = rect + + dominant_angle = float(angle) + if width < height: + dominant_angle += 90.0 + + dominant_angle %= 180.0 + return dominant_angle + + +def _compute_trapezoid_score_from_mask( + binary_mask: np.ndarray, + ys: np.ndarray, + xs: np.ndarray, + min_y: int, + max_y: int, + bbox_h: int, +) -> float: + """Return 0..1 indicating how floor-like (wider at bottom) the mask shape is.""" + quarter = max(1, bbox_h // 4) + top_xs = xs[ys <= (min_y + quarter)] + bot_xs = xs[ys >= (max_y - quarter)] + if len(top_xs) < 3 or len(bot_xs) < 3: + return 0.0 + top_w = float(top_xs.max() - top_xs.min()) + bot_w = float(bot_xs.max() - bot_xs.min()) + if top_w < 5.0: + return 1.0 if bot_w > 20.0 else 0.0 + ratio = bot_w / top_w + return float(np.clip((ratio - 1.0) / 1.8, 0.0, 1.0)) + + +def _sort_quad_corners(pts: np.ndarray) -> np.ndarray: + """Sort 4 points into [TL, TR, BR, BL] order.""" + result = np.zeros((4, 2), dtype=np.float32) + s = pts[:, 0] + pts[:, 1] + d = pts[:, 0] - pts[:, 1] + result[0] = pts[np.argmin(s)] + result[1] = pts[np.argmax(d)] + result[2] = pts[np.argmax(s)] + result[3] = pts[np.argmin(d)] + return result + + +def _extract_mask_quad(binary_mask: np.ndarray) -> np.ndarray | None: + """Approximate mask as 4-corner polygon sorted [TL, TR, BR, BL], or None.""" + mask_u8 = (binary_mask > 0).astype(np.uint8) + contours, _ = cv2.findContours(mask_u8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + if not contours: + return None + largest = max(contours, key=cv2.contourArea) + if cv2.contourArea(largest) < 400.0: + return None + hull = cv2.convexHull(largest) + peri = cv2.arcLength(hull, True) + for eps_frac in (0.03, 0.05, 0.08, 0.10, 0.13): + approx = cv2.approxPolyDP(hull, eps_frac * peri, True) + if len(approx) == 4: + return _sort_quad_corners(approx.reshape(4, 2).astype(np.float32)) + return None + + +def _tile_texture_perspective( + tex_pil: Image.Image, + quad: np.ndarray, + image_width: int, + image_height: int, +) -> Image.Image | None: + """ + Tile texture with perspective correction for a floor surface. + quad: [TL, TR, BR, BL] in image coordinates. + Returns full-image-size PIL image with warped tiled texture, or None on failure. + Pixels outside the perspective quad are filled with regular tiling to avoid black gaps. + """ + tl, tr, br, bl = quad + bot_w = float(np.linalg.norm(br.astype(float) - bl.astype(float))) + top_w = float(np.linalg.norm(tr.astype(float) - tl.astype(float))) + left_h = float(np.linalg.norm(bl.astype(float) - tl.astype(float))) + right_h = float(np.linalg.norm(br.astype(float) - tr.astype(float))) + rect_w = min(int(max(bot_w, top_w)) + 1, image_width * 2) + rect_h = min(int(max(left_h, right_h)) + 1, image_height * 2) + if rect_w < 8 or rect_h < 8: + return None + tex_arr = np.array(tex_pil.convert("RGB"), dtype=np.uint8) + th, tw = tex_arr.shape[:2] + if tw < 1 or th < 1: + return None + rect_tiled = np.zeros((rect_h, rect_w, 3), dtype=np.uint8) + for ry in range(0, rect_h, th): + for rx in range(0, rect_w, tw): + py = min(th, rect_h - ry) + px = min(tw, rect_w - rx) + rect_tiled[ry : ry + py, rx : rx + px] = tex_arr[:py, :px] + src_pts = np.array( + [[0.0, 0.0], [float(rect_w - 1), 0.0], [float(rect_w - 1), float(rect_h - 1)], [0.0, float(rect_h - 1)]], + dtype=np.float32, + ) + dst_pts = quad.astype(np.float32) + try: + H = cv2.getPerspectiveTransform(src_pts, dst_pts) + warped = cv2.warpPerspective(rect_tiled, H, (image_width, image_height)) + # Mapa de cobertura: píxeles realmente cubiertos por el warp + cov_src = np.ones((rect_h, rect_w), dtype=np.uint8) * 255 + coverage = cv2.warpPerspective(cov_src, H, (image_width, image_height)) + except cv2.error: + return None + + # Rellenar píxeles sin cobertura (fuera del quad) con tiling regular + # para evitar espacios negros donde la máscara supera el quad aproximado + regular = np.zeros((image_height, image_width, 3), dtype=np.uint8) + for ry in range(0, image_height, th): + for rx in range(0, image_width, tw): + py = min(th, image_height - ry) + px = min(tw, image_width - rx) + regular[ry : ry + py, rx : rx + px] = tex_arr[:py, :px] + + uncovered = coverage < 128 + warped[uncovered] = regular[uncovered] + + return Image.fromarray(warped) + + +def classify_texture_material(texture_name: str) -> str: + texture_key = texture_name.lower() + if "acm" in texture_key or "wpc" in texture_key: + return "acm" + if any(hint in texture_key for hint in ("deck", "wood", "plank", "laminate", "floor")): + return "wood" + if any(hint in texture_key for hint in ("marble", "granite", "tile", "brick", "cobblestone", "stone", "cartago", "riverbed")): + return "stone" + if any(hint in texture_key for hint in ("metal", "rust", "iron", "steel")): + return "metal" + return "generic" + + +def infer_surface_type_and_direction( + binary_mask: np.ndarray, + image_width: int, + image_height: int, + texture_name: str, +) -> tuple[str, float, float, int]: + mask_u8 = (binary_mask > 0).astype(np.uint8) + ys, xs = np.where(mask_u8 > 0) + if ys.size == 0 or xs.size == 0: + return ("wall", 0.0, 0.78, max(180, image_width // 4)) + + min_x, max_x = int(xs.min()), int(xs.max()) + min_y, max_y = int(ys.min()), int(ys.max()) + bbox_w = max(1, max_x - min_x + 1) + bbox_h = max(1, max_y - min_y + 1) + aspect = bbox_w / max(1.0, float(bbox_h)) + center_y = float(ys.mean()) / max(1.0, float(image_height)) + dominant_angle = estimate_mask_orientation_degrees(binary_mask) + material = classify_texture_material(texture_name) + + # Trapezoid score: floor in perspective is wider at the bottom than the top + trapezoid_score = _compute_trapezoid_score_from_mask(binary_mask, ys, xs, min_y, max_y, bbox_h) + + is_ceiling = center_y < 0.26 and aspect > 1.35 + # Floor: low center + trapezoidal shape, OR clearly low + wide + is_floor = ( + (center_y > 0.55 and aspect >= 0.9 and trapezoid_score > 0.30) + or (center_y > 0.68 and aspect > 1.15) + ) + + if is_ceiling: + surface_type = "ceiling" + angle = 0.0 + blend_alpha = 0.58 + tile_width = max(128, image_width // 5) + elif is_floor: + if material == "wood": + surface_type = "deck" + angle = dominant_angle if 8.0 <= dominant_angle <= 172.0 else 0.0 + blend_alpha = 0.82 + tile_width = max(320, int(bbox_w * 0.95), image_width // 2) + else: + surface_type = "floor" + angle = 0.0 + blend_alpha = 0.80 + # ACM floor: ~3 large-format panels visible on the near edge + tile_width = max(200, int(bbox_w * 0.35)) if material == "acm" else max(144, image_width // 3) + else: + surface_type = "wall" + angle = 0.0 + if material == "acm": + blend_alpha = 0.78 + # ACM wall panels: ~3 panels across the surface width + tile_width = max(180, int(bbox_w * 0.33)) + elif material == "wood": + blend_alpha = 0.70 + tile_width = max(220, int(bbox_w * 0.55), image_width // 4) + elif material == "stone": + blend_alpha = 0.84 + tile_width = max(128, image_width // 4) + else: + blend_alpha = 0.66 + tile_width = max(128, image_width // 4) + + return (surface_type, float(angle % 180.0), float(blend_alpha), int(tile_width)) + + +def choose_auto_texture_settings(material: str, surface_type: str) -> tuple[float, float, float]: + strength_map = {"acm": 0.98, "stone": 0.96, "wood": 0.88, "metal": 0.91, "generic": 0.9} + intensity_map = {"acm": 0.08, "stone": 0.36, "wood": 0.3, "metal": 0.34, "generic": 0.32} + + strength = float(strength_map.get(material, 0.9)) + intensity = float(intensity_map.get(material, 0.32)) + + if surface_type in {"wall", "facade"}: + strength += 0.02 + angle = 28.0 + elif surface_type in {"roof"}: + angle = 42.0 + intensity += 0.03 + elif surface_type in {"floor", "deck"}: + angle = 24.0 + intensity += 0.02 + else: + angle = 35.0 + + return ( + float(np.clip(strength, 0.55, 0.99)), + float(angle % 360.0), + float(np.clip(intensity, 0.0, 1.0)), + ) + + +def build_feather_mask(binary_mask: np.ndarray, sigma: float = 2.2) -> np.ndarray: + mask = (binary_mask > 0).astype(np.float32) + if mask.max() <= 0: + return mask + feather = cv2.GaussianBlur(mask, (0, 0), sigmaX=sigma, sigmaY=sigma) + return np.clip(feather, 0.0, 1.0) + + +def build_scene_luminance_map(orig_rgb: np.ndarray) -> np.ndarray: + orig_u8 = orig_rgb.astype(np.uint8) + orig_lab = cv2.cvtColor(orig_u8, cv2.COLOR_RGB2LAB).astype(np.float32) + l_channel = orig_lab[:, :, 0] / 255.0 + broad_light = cv2.GaussianBlur(l_channel, (0, 0), sigmaX=18.0, sigmaY=18.0) + local_detail = l_channel - cv2.GaussianBlur(l_channel, (0, 0), sigmaX=4.0, sigmaY=4.0) + light_map = 0.82 + (broad_light * 0.36) + (local_detail * 0.18) + return np.clip(light_map, 0.72, 1.22) + + +def build_texture_relief_map(tex_rgb: np.ndarray, material: str = "generic") -> np.ndarray: + tex_u8 = tex_rgb.astype(np.uint8) + tex_gray = cv2.cvtColor(tex_u8, cv2.COLOR_RGB2GRAY).astype(np.float32) / 255.0 + micro_relief = tex_gray - cv2.GaussianBlur(tex_gray, (0, 0), sigmaX=3.0, sigmaY=3.0) + + relief_scale = {"acm": 0.35, "stone": 2.8, "wood": 2.2, "metal": 1.8}.get(material, 2.0) + return np.clip(micro_relief * relief_scale, -1.0, 1.0) + + +def build_directional_light_map( + tex_rgb: np.ndarray, + material: str, + light_angle_degrees: float, + light_intensity: float, +) -> np.ndarray: + tex_u8 = tex_rgb.astype(np.uint8) + tex_gray = cv2.cvtColor(tex_u8, cv2.COLOR_RGB2GRAY).astype(np.float32) / 255.0 + height_map = cv2.GaussianBlur(tex_gray, (0, 0), sigmaX=1.4, sigmaY=1.4) + grad_x = cv2.Sobel(height_map, cv2.CV_32F, 1, 0, ksize=3) + grad_y = cv2.Sobel(height_map, cv2.CV_32F, 0, 1, ksize=3) + + relief_scale = {"acm": 0.45, "stone": 3.0, "wood": 2.4, "metal": 1.6}.get(material, 2.0) + + nx = -grad_x * relief_scale + ny = -grad_y * relief_scale + nz = np.ones_like(nx, dtype=np.float32) + norm = np.sqrt((nx * nx) + (ny * ny) + (nz * nz)) + 1e-6 + nx = nx / norm + ny = ny / norm + nz = nz / norm + + theta = np.deg2rad(float(light_angle_degrees)) + lx = float(np.cos(theta)) + ly = float(-np.sin(theta)) + lz = 0.82 + light_norm = max(1e-6, float(np.sqrt((lx * lx) + (ly * ly) + (lz * lz)))) + lx /= light_norm + ly /= light_norm + lz /= light_norm + + diffuse = np.clip((nx * lx) + (ny * ly) + (nz * lz), 0.0, 1.0) + strength = float(np.clip(light_intensity, 0.0, 1.0)) + if material == "acm": + return np.clip(0.97 + (diffuse * (0.03 + (0.12 * strength))), 0.95, 1.12) + return np.clip(0.86 + (diffuse * (0.14 + (0.60 * strength))), 0.72, 1.35) + + +def build_mask_edge_occlusion(binary_mask: np.ndarray, light_intensity: float) -> np.ndarray: + mask_u8 = (binary_mask > 0).astype(np.uint8) + if mask_u8.max() == 0: + return np.ones(mask_u8.shape, dtype=np.float32) + + distance = cv2.distanceTransform(mask_u8, cv2.DIST_L2, 5).astype(np.float32) + inner_values = distance[mask_u8 > 0] + if inner_values.size == 0: + return np.ones(mask_u8.shape, dtype=np.float32) + + max_distance = max(1.0, float(np.percentile(inner_values, 95))) + normalized = np.clip(distance / (max_distance * 0.16), 0.0, 1.0) + edge_strength = 1.0 - normalized + occlusion = 1.0 - (edge_strength * (0.04 + (0.08 * float(np.clip(light_intensity, 0.0, 1.0))))) + occlusion[mask_u8 == 0] = 1.0 + return np.clip(occlusion, 0.88, 1.0) + + +def apply_surface_lighting( + tex_rgb: np.ndarray, + orig_rgb: np.ndarray, + binary_mask: np.ndarray, + material: str, + lighting_mode: str, + light_angle_degrees: float, + light_intensity: float, +) -> np.ndarray: + scene_light = build_scene_luminance_map(orig_rgb) + directional_light = build_directional_light_map(tex_rgb, material, light_angle_degrees, light_intensity) + relief_map = build_texture_relief_map(tex_rgb, material) + edge_occlusion = build_mask_edge_occlusion(binary_mask, light_intensity) + + if material == "acm": + if lighting_mode == "directional": + light_map = directional_light + elif lighting_mode == "flat": + light_map = np.ones(scene_light.shape, dtype=np.float32) + else: + # 45 % scene luminance so ACM panels inherit shadows/gradients from photo + light_map = (scene_light * 0.45) + (directional_light * 0.55) + elif lighting_mode == "directional": + light_map = directional_light + elif lighting_mode == "flat": + light_map = np.ones(scene_light.shape, dtype=np.float32) + else: + light_map = (scene_light * 0.78) + (directional_light * 0.22) + + detail_scale = 0.02 + (0.05 * float(np.clip(light_intensity, 0.0, 1.0))) if material == "acm" else 0.08 + (0.18 * float(np.clip(light_intensity, 0.0, 1.0))) + detail_boost = 1.0 + (relief_map * detail_scale) + enhanced = tex_rgb.astype(np.float32) + enhanced *= light_map[:, :, None] + enhanced *= detail_boost[:, :, None] + enhanced *= edge_occlusion[:, :, None] + return np.clip(enhanced, 0, 255).astype(np.uint8) + + +def blend_texture_preserve_shading( + orig_rgb: np.ndarray, + tex_rgb: np.ndarray, + alpha_mask: np.ndarray, + blend_alpha: float, + material: str = "generic", +) -> np.ndarray: + orig_u8 = orig_rgb.astype(np.uint8) + tex_u8 = tex_rgb.astype(np.uint8) + + orig_lab = cv2.cvtColor(orig_u8, cv2.COLOR_RGB2LAB).astype(np.float32) + tex_lab = cv2.cvtColor(tex_u8, cv2.COLOR_RGB2LAB).astype(np.float32) + + mixed_lab = tex_lab.copy() + if material == "acm": + # 30 % original luminance → panels inherit scene shadows while keeping panel colour + mixed_lab[:, :, 0] = (0.30 * orig_lab[:, :, 0]) + (0.70 * tex_lab[:, :, 0]) + mixed_lab[:, :, 1] = (0.97 * tex_lab[:, :, 1]) + (0.03 * orig_lab[:, :, 1]) + mixed_lab[:, :, 2] = (0.97 * tex_lab[:, :, 2]) + (0.03 * orig_lab[:, :, 2]) + elif material == "wood": + mixed_lab[:, :, 0] = (0.78 * orig_lab[:, :, 0]) + (0.22 * tex_lab[:, :, 0]) + mixed_lab[:, :, 1] = (0.9 * tex_lab[:, :, 1]) + (0.1 * orig_lab[:, :, 1]) + mixed_lab[:, :, 2] = (0.9 * tex_lab[:, :, 2]) + (0.1 * orig_lab[:, :, 2]) + elif material == "stone": + orig_l_base = cv2.GaussianBlur(orig_lab[:, :, 0], (0, 0), sigmaX=11.0, sigmaY=11.0) + mixed_lab[:, :, 0] = (0.18 * orig_l_base) + (0.82 * tex_lab[:, :, 0]) + mixed_lab[:, :, 1] = (0.95 * tex_lab[:, :, 1]) + (0.05 * orig_lab[:, :, 1]) + mixed_lab[:, :, 2] = (0.95 * tex_lab[:, :, 2]) + (0.05 * orig_lab[:, :, 2]) + else: + mixed_lab[:, :, 0] = orig_lab[:, :, 0] + mixed_lab[:, :, 1] = (0.8 * tex_lab[:, :, 1]) + (0.2 * orig_lab[:, :, 1]) + mixed_lab[:, :, 2] = (0.8 * tex_lab[:, :, 2]) + (0.2 * orig_lab[:, :, 2]) + + shaded_tex = cv2.cvtColor(np.clip(mixed_lab, 0, 255).astype(np.uint8), cv2.COLOR_LAB2RGB).astype(np.float32) + if material == "wood": + tex_gray = cv2.cvtColor(tex_u8, cv2.COLOR_RGB2GRAY).astype(np.float32) + tex_base = cv2.GaussianBlur(tex_gray, (0, 0), sigmaX=9.0, sigmaY=9.0) + tex_detail = np.clip((tex_gray - tex_base) / 255.0, -0.35, 0.35) + shaded_tex *= (1.0 + (tex_detail[:, :, None] * 0.28)) + + alpha = np.clip(alpha_mask[:, :, None] * float(blend_alpha), 0.0, 1.0) + composite = (orig_rgb * (1.0 - alpha)) + (shaded_tex * alpha) + return np.clip(composite, 0, 255).astype(np.uint8) + + +def blend_texture_direct( + orig_rgb: np.ndarray, + tex_rgb: np.ndarray, + alpha_mask: np.ndarray, + blend_alpha: float, +) -> np.ndarray: + alpha = np.clip(alpha_mask[:, :, None] * float(blend_alpha), 0.0, 1.0) + composite = (orig_rgb * (1.0 - alpha)) + (tex_rgb * alpha) + return np.clip(composite, 0, 255).astype(np.uint8) + + +def apply_local_texture_sync(payload: ApplyTextureRequest) -> dict[str, Any]: + step = "APPLY_TEXTURE" + started = log_timing_start(step) + try: + safe_name = Path(payload.filename).name + if not safe_name: + raise HTTPException(status_code=400, detail="Invalid filename") + + label_safe_name = Path(payload.original_filename).name if payload.original_filename else safe_name + + image_path = UPLOAD_DIR / safe_name + if not image_path.exists() or not image_path.is_file(): + image_path = OUTPUT_DIR / safe_name + + if (not image_path.exists() or not image_path.is_file()) and payload.original_filename: + orig_name = Path(payload.original_filename).name + image_path = UPLOAD_DIR / orig_name + if not image_path.exists() or not image_path.is_file(): + image_path = OUTPUT_DIR / orig_name + + if not image_path.exists() or not image_path.is_file(): + raise HTTPException( + status_code=404, + detail=f"Image not found: {safe_name} (also tried original: {payload.original_filename or 'n/a'})", + ) + + masks_dir = UPLOAD_DIR / "masks" + masks_dir.mkdir(exist_ok=True) + label_owner = Path(image_path).stem + label_path = masks_dir / f"{label_owner}_labels.png" + if not label_path.exists() and payload.original_filename: + alt_owner = Path(payload.original_filename).name + alt_label = masks_dir / f"{alt_owner}_labels.png" + if alt_label.exists(): + label_path = alt_label + + if not label_path.exists(): + raise HTTPException( + status_code=404, + detail=f"Label map not found for {label_owner}. Upload/segment the image first.", + ) + + if not payload.mask_indices: + raise HTTPException(status_code=400, detail="No mask indices provided") + + texture_path = resolve_texture_path(payload.texture_name) + orig_pil = Image.open(str(image_path)).convert("RGB") + width, height = orig_pil.size + + label_map = cv2.imread(str(label_path), cv2.IMREAD_GRAYSCALE) + if label_map is None: + raise HTTPException(status_code=500, detail="Could not read label map") + + binary_mask = np.zeros((label_map.shape[0], label_map.shape[1]), dtype=np.uint8) + for idx in payload.mask_indices: + binary_mask |= (label_map == idx).astype(np.uint8) + + if binary_mask.max() == 0: + raise HTTPException(status_code=400, detail="None of the selected segments were found in the label map.") + + direction_mode = str(payload.direction_mode or "auto").strip().lower() + if direction_mode not in {"auto", "manual", "none"}: + raise HTTPException(status_code=400, detail="Invalid direction_mode. Use auto, manual, or none.") + + replace_mode = str(getattr(payload, "replace_mode", "realistic") or "realistic").strip().lower() + lighting_mode = str(getattr(payload, "lighting_mode", "scene") or "scene").strip().lower() + + material = classify_texture_material(payload.texture_name) + surface_type, inferred_angle, blend_alpha, target_w = infer_surface_type_and_direction( + binary_mask, width, height, payload.texture_name, + ) + replace_strength, light_angle_degrees, light_intensity = choose_auto_texture_settings(material, surface_type) + effective_alpha = float(np.clip(blend_alpha * (0.55 + (0.75 * replace_strength)), 0.0, 0.98)) + if material == "acm": + effective_alpha = float(max(effective_alpha, 0.92)) + + applied_angle = 0.0 + if direction_mode == "auto": + applied_angle = inferred_angle + elif direction_mode == "manual": + applied_angle = float(payload.angle_degrees) + + tex_pil = load_texture_pil_rgb(texture_path) + + # Escalar al tamaño de tile deseado ANTES de rotar + tex_w, tex_h = tex_pil.size + scale = target_w / max(1, tex_w) + if abs(scale - 1.0) > 0.05: + tex_pil = tex_pil.resize( + (max(1, int(tex_w * scale)), max(1, int(tex_h * scale))), + Image.Resampling.LANCZOS, + ) + tex_w, tex_h = tex_pil.size + + tiled: Image.Image | None = None + + # Floor surfaces: perspective tiling solo cuando la perspectiva es fuerte. + # Para pisos de dormitorio/habitación (perspectiva suave) el quad aproximado + # no cubre bien el mask irregular → produce negro. Se usa tiling regular en esos casos. + if surface_type == "floor" and direction_mode in {"auto", "none"}: + ys_m, xs_m = np.where(binary_mask > 0) + if ys_m.size > 0: + min_y_m, max_y_m = int(ys_m.min()), int(ys_m.max()) + bbox_h_m = max(1, max_y_m - min_y_m + 1) + trap_score = _compute_trapezoid_score_from_mask( + binary_mask, ys_m, xs_m, min_y_m, max_y_m, bbox_h_m + ) + if trap_score > 0.35: + quad = _extract_mask_quad(binary_mask) + if quad is not None: + tiled = _tile_texture_perspective(tex_pil, quad, width, height) + if tiled is not None: + logger.info(f"[APPLY_TEXTURE] perspective floor tiling applied (trap={trap_score:.2f})") + else: + logger.info(f"[APPLY_TEXTURE] flat floor tiling (trap={trap_score:.2f} < 0.35, skip perspective)") + + # Wall / ceiling surfaces: perspective tiling cuando el quad es significativamente + # no-rectangular (pared fotografiada en ángulo). Un ratio > 1.20 entre lado mayor + # y lado menor indica distorsión perspectiva visible. + if surface_type in {"wall", "ceiling"} and tiled is None and direction_mode in {"auto", "none"}: + quad = _extract_mask_quad(binary_mask) + if quad is not None: + tl, tr, br, bl = quad + top_w = float(np.linalg.norm(tr.astype(float) - tl.astype(float))) + bot_w = float(np.linalg.norm(br.astype(float) - bl.astype(float))) + left_h = float(np.linalg.norm(bl.astype(float) - tl.astype(float))) + right_h = float(np.linalg.norm(br.astype(float) - tr.astype(float))) + w_ratio = max(top_w, bot_w) / max(1.0, min(top_w, bot_w)) + h_ratio = max(left_h, right_h) / max(1.0, min(left_h, right_h)) + if w_ratio > 1.20 or h_ratio > 1.20: + tiled = _tile_texture_perspective(tex_pil, quad, width, height) + if tiled is not None: + logger.info( + f"[APPLY_TEXTURE] perspective wall tiling applied " + f"(w_ratio={w_ratio:.2f}, h_ratio={h_ratio:.2f})" + ) + + if tiled is None: + if abs(applied_angle) > 0.01: + # Tile on a large canvas first, then rotate the full canvas to avoid black corners + diag = int(np.ceil(np.sqrt(width ** 2 + height ** 2))) + max(tex_w, tex_h) + large_w = width + diag + large_h = height + diag + large = Image.new("RGB", (large_w, large_h)) + for y in range(0, large_h, tex_h): + for x in range(0, large_w, tex_w): + large.paste(tex_pil, (x, y)) + large = large.rotate(-applied_angle, resample=Image.Resampling.BICUBIC, expand=False) + cx = (large_w - width) // 2 + cy = (large_h - height) // 2 + tiled = large.crop((cx, cy, cx + width, cy + height)) + else: + tiled = Image.new("RGB", (width, height)) + for y in range(0, height, tex_h): + for x in range(0, width, tex_w): + tiled.paste(tex_pil, (x, y)) + + orig_u8 = np.array(orig_pil, dtype=np.uint8) + + try: + if bool(getattr(payload, "clear_mask_before_apply", False)): + orig_candidate = None + if payload.original_filename: + cand = UPLOAD_DIR / Path(payload.original_filename).name + if cand.exists() and cand.is_file(): + orig_candidate = cand + else: + cand2 = OUTPUT_DIR / Path(payload.original_filename).name + if cand2.exists() and cand2.is_file(): + orig_candidate = cand2 + if orig_candidate is None: + stem = Path(image_path).stem + if "_edit_" in stem: + base_name = stem.split("_edit_")[0] + ".jpg" + cand = UPLOAD_DIR / base_name + if cand.exists() and cand.is_file(): + orig_candidate = cand + else: + cand2 = OUTPUT_DIR / base_name + if cand2.exists() and cand2.is_file(): + orig_candidate = cand2 + + if orig_candidate is not None: + try: + base_pil = Image.open(str(orig_candidate)).convert("RGB") + if base_pil.size != (width, height): + base_pil = base_pil.resize((width, height), Image.Resampling.LANCZOS) + base_arr = np.array(base_pil, dtype=np.uint8) + mask_bool = (binary_mask > 0) + orig_u8[mask_bool] = base_arr[mask_bool] + logger.info(f"[APPLY_TEXTURE] cleared mask from original source: {orig_candidate}") + except Exception: + logger.exception("Failed to restore original pixels for clear_mask_before_apply") + except Exception: + logger.exception("Error handling clear_mask_before_apply") + + tiled_arr = np.array(tiled, dtype=np.uint8) + + # Parchar píxeles muy oscuros (suma R+G+B < 20) dentro de la máscara. + # Cubren tanto negro exacto (0,0,0) como píxeles casi negros que el warp + # de perspectiva puede generar en bordes del quad o zonas sin cobertura. + mask_bool = binary_mask > 0 + dark_in_mask = mask_bool & (tiled_arr.sum(axis=2) < 20) + if dark_in_mask.any(): + th_f, tw_f = tex_pil.size[1], tex_pil.size[0] + tex_np = np.array(tex_pil, dtype=np.uint8) + ys_b, xs_b = np.where(dark_in_mask) + tiled_arr[ys_b, xs_b] = tex_np[ys_b % th_f, xs_b % tw_f] + + lit_tex = apply_surface_lighting( + tiled_arr, + orig_u8, + binary_mask, + material, + lighting_mode, + light_angle_degrees, + light_intensity, + ) + + orig_arr = orig_u8.astype(np.float32) + tex_arr = lit_tex.astype(np.float32) + + if replace_mode in {"hard", "absolute", "force", "replace"}: + mask_bool = (binary_mask > 0).astype(bool) + composite_arr = orig_arr.copy() + composite_arr[mask_bool] = tex_arr[mask_bool] + composite = np.clip(composite_arr, 0, 255).astype(np.uint8) + else: + feather_mask = build_feather_mask(binary_mask) + # All materials use shading-preservation so scene luminance (shadows/highlights) + # from the original photo is transferred onto the texture. + composite = blend_texture_preserve_shading(orig_arr, tex_arr, feather_mask, effective_alpha, material) + + input_stem = Path(image_path).stem + edit_suffix = uuid.uuid4().hex[:8] + out_filename = f"{input_stem}_edit_{edit_suffix}.jpg" + out_path = UPLOAD_DIR / out_filename + Image.fromarray(composite).save(str(out_path), format="JPEG", quality=UPLOAD_JPEG_QUALITY, optimize=True) + + try: + out_label_path = masks_dir / f"{Path(out_filename).stem}_labels.png" + if label_path.exists(): + shutil.copyfile(str(label_path), str(out_label_path)) + except Exception: + logger.exception("Failed to copy label map for output image") + + return { + "message": "Texture applied successfully", + "original": safe_name, + "mask_indices": payload.mask_indices, + "texture_name": payload.texture_name, + "material": material, + "direction_mode": direction_mode, + "surface_type": surface_type, + "replace_mode": replace_mode, + "replace_strength": round(replace_strength, 3), + "lighting_mode": lighting_mode, + "light_angle_degrees": round(light_angle_degrees, 2), + "light_intensity": round(light_intensity, 3), + "blend_alpha": round(effective_alpha, 3), + "applied_angle_degrees": round(applied_angle, 2), + "output_filename": out_filename, + "output_url": f"/seg/image/{out_filename}", + } + finally: + log_timing_end(step, started) diff --git a/backend/uploads/acm azul.jpg b/backend/uploads/acm azul.jpg new file mode 100644 index 0000000000000000000000000000000000000000..43932714951b7dfd2b10da91dcb685a2408440dc Binary files /dev/null and b/backend/uploads/acm azul.jpg differ diff --git a/backend/uploads/acm azul_edit_2e8f9183.jpg b/backend/uploads/acm azul_edit_2e8f9183.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ae1e7e055fad325841655d7a2b849b202949c891 Binary files /dev/null and b/backend/uploads/acm azul_edit_2e8f9183.jpg differ diff --git a/backend/uploads/acm azul_edit_3d3e91af.jpg b/backend/uploads/acm azul_edit_3d3e91af.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2a34c5a477656924ddf1e05cd37e069e90e8b183 Binary files /dev/null and b/backend/uploads/acm azul_edit_3d3e91af.jpg differ diff --git a/backend/uploads/acm azul_edit_55fe992d.jpg b/backend/uploads/acm azul_edit_55fe992d.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e0e0f9ce20343b2423782f4c68b88b5534b36a71 Binary files /dev/null and b/backend/uploads/acm azul_edit_55fe992d.jpg differ diff --git a/backend/uploads/acm azul_edit_83b77dfe.jpg b/backend/uploads/acm azul_edit_83b77dfe.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e0e0f9ce20343b2423782f4c68b88b5534b36a71 Binary files /dev/null and b/backend/uploads/acm azul_edit_83b77dfe.jpg differ diff --git a/backend/uploads/acm azul_edit_83b77dfe_edit_ec003a3a.jpg b/backend/uploads/acm azul_edit_83b77dfe_edit_ec003a3a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2a25eb135907f9d81e0d83b692858dcd13a6f9ef Binary files /dev/null and b/backend/uploads/acm azul_edit_83b77dfe_edit_ec003a3a.jpg differ diff --git a/backend/uploads/acm azul_edit_9456ec3a.jpg b/backend/uploads/acm azul_edit_9456ec3a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2b5de75b405ba978ddd7acafa40c53f919f3bf8d Binary files /dev/null and b/backend/uploads/acm azul_edit_9456ec3a.jpg differ diff --git a/backend/uploads/acm azul_edit_edb46e2f.jpg b/backend/uploads/acm azul_edit_edb46e2f.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3c74564eafcf1123df944c7041bcd373660bde66 --- /dev/null +++ b/backend/uploads/acm azul_edit_edb46e2f.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dede36f1fa337a563b6da2ecd7f2e3056727357efac692407c641ca329628f19 +size 113328 diff --git a/backend/uploads/acm gris.jpg b/backend/uploads/acm gris.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d7eb057de644d504cc24f4dcaa58af01cb267c78 --- /dev/null +++ b/backend/uploads/acm gris.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc667f4d70a6b2f394dfad09b052f7e2262e18d7a2d94272702c0c0f6c5286ff +size 155417 diff --git a/backend/uploads/acm gris_edit_09f2e747.jpg b/backend/uploads/acm gris_edit_09f2e747.jpg new file mode 100644 index 0000000000000000000000000000000000000000..173c0db00fa74ddbd60c619504bfddb5582c89f8 --- /dev/null +++ b/backend/uploads/acm gris_edit_09f2e747.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a7e5c6cb195cf4d661b314d77614707aa93c6c0e1afb011d642801f90706ffad +size 126313 diff --git a/backend/uploads/acm gris_edit_19d266a8.jpg b/backend/uploads/acm gris_edit_19d266a8.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4ce3460169d5164c4d1fdd9ac53be035d18afa85 --- /dev/null +++ b/backend/uploads/acm gris_edit_19d266a8.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:921fbe96309249c906c867346e476338549a3bc7b7be494e6729205956141178 +size 144256 diff --git a/backend/uploads/hyper-reality-1778254222964.jpg b/backend/uploads/hyper-reality-1778254222964.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a8e4af25c20f5b8ae0b85122a460478d3b2bb36c Binary files /dev/null and b/backend/uploads/hyper-reality-1778254222964.jpg differ diff --git a/backend/uploads/hyper-reality-1778254222964_edit_227e7927.jpg b/backend/uploads/hyper-reality-1778254222964_edit_227e7927.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b56c123397c0e0a494e66486a918ade1e88fe18e Binary files /dev/null and b/backend/uploads/hyper-reality-1778254222964_edit_227e7927.jpg differ diff --git a/backend/uploads/hyper-reality-1778254222964_edit_2ea5bcda.jpg b/backend/uploads/hyper-reality-1778254222964_edit_2ea5bcda.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a4013b9d190183f1c4fa5ef1fb28803104e5a373 Binary files /dev/null and b/backend/uploads/hyper-reality-1778254222964_edit_2ea5bcda.jpg differ diff --git a/backend/uploads/hyper-reality-1778254222964_edit_7d919375.jpg b/backend/uploads/hyper-reality-1778254222964_edit_7d919375.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3c527b0ab622141e7af44a26fb35fcbe69083a37 Binary files /dev/null and b/backend/uploads/hyper-reality-1778254222964_edit_7d919375.jpg differ diff --git a/backend/uploads/hyper-reality-1778254222964_edit_9c5de50a.jpg b/backend/uploads/hyper-reality-1778254222964_edit_9c5de50a.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b56c123397c0e0a494e66486a918ade1e88fe18e Binary files /dev/null and b/backend/uploads/hyper-reality-1778254222964_edit_9c5de50a.jpg differ diff --git a/backend/uploads/hyper-reality-1778254222964_edit_9c5de50a_edit_7e5043d5.jpg b/backend/uploads/hyper-reality-1778254222964_edit_9c5de50a_edit_7e5043d5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..55140796c4dc2a26a20d9ea1e5ccf330f2e386a2 --- /dev/null +++ b/backend/uploads/hyper-reality-1778254222964_edit_9c5de50a_edit_7e5043d5.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3dd215582d5c46992111d6681c9fa17b70ebd5973dcc8973c9f96fe54fac5eb3 +size 102091 diff --git a/backend/uploads/hyper-reality-1778254222964_edit_a25fb848.jpg b/backend/uploads/hyper-reality-1778254222964_edit_a25fb848.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8bdf62cfdfbf93cfb91f89f94c5cfc58a7cae707 Binary files /dev/null and b/backend/uploads/hyper-reality-1778254222964_edit_a25fb848.jpg differ diff --git a/backend/uploads/hyper-reality-1778254222964_edit_a92f4266.jpg b/backend/uploads/hyper-reality-1778254222964_edit_a92f4266.jpg new file mode 100644 index 0000000000000000000000000000000000000000..db51590d963c83d4e09da74d8bc758f9c7af73ba Binary files /dev/null and b/backend/uploads/hyper-reality-1778254222964_edit_a92f4266.jpg differ diff --git a/backend/uploads/hyper-reality-1778254222964_edit_a92f4266_edit_161ee5c4.jpg b/backend/uploads/hyper-reality-1778254222964_edit_a92f4266_edit_161ee5c4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8ab1427611952be15681e373fd4870ba06209a19 --- /dev/null +++ b/backend/uploads/hyper-reality-1778254222964_edit_a92f4266_edit_161ee5c4.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c73573d19a9e5ae7bf01e2f34b0e7228ffb236cba18929c879d26485c594554 +size 104884 diff --git a/backend/uploads/hyper-reality-1778254222964_edit_eb8100f4.jpg b/backend/uploads/hyper-reality-1778254222964_edit_eb8100f4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..896d65a27444f294b3d19d5d2c500abfed32e577 Binary files /dev/null and b/backend/uploads/hyper-reality-1778254222964_edit_eb8100f4.jpg differ diff --git a/backend/uploads/hyper-reality-DrcCx8G7EkfZn3VV.jpg b/backend/uploads/hyper-reality-DrcCx8G7EkfZn3VV.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1e784a6a9b37d497b456f88267599587c71ebf59 Binary files /dev/null and b/backend/uploads/hyper-reality-DrcCx8G7EkfZn3VV.jpg differ diff --git a/backend/uploads/masks/acm azul.jpg_labels.png b/backend/uploads/masks/acm azul.jpg_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..4bc70e25068afb81eec9b1dcbfd6da7c27f60120 Binary files /dev/null and b/backend/uploads/masks/acm azul.jpg_labels.png differ diff --git a/backend/uploads/masks/acm azul.jpg_labels_meta.json b/backend/uploads/masks/acm azul.jpg_labels_meta.json new file mode 100644 index 0000000000000000000000000000000000000000..23fa08867a0e158755503d096c3595698fff6cf3 --- /dev/null +++ b/backend/uploads/masks/acm azul.jpg_labels_meta.json @@ -0,0 +1 @@ +{"segments": [{"index": 1, "type": "floor", "label": "Piso 1", "area_ratio": 0.1958}, {"index": 2, "type": "object", "label": "Objeto 1", "area_ratio": 0.0003}, {"index": 3, "type": "object", "label": "Objeto 2", "area_ratio": 0.0014}, {"index": 4, "type": "window", "label": "Ventana 1", "area_ratio": 0.0915}, {"index": 5, "type": "object", "label": "Objeto 3", "area_ratio": 0.0005}, {"index": 6, "type": "ceiling", "label": "Techo 1", "area_ratio": 0.079}, {"index": 7, "type": "object", "label": "Objeto 4", "area_ratio": 0.0003}, {"index": 8, "type": "window", "label": "Ventana 2", "area_ratio": 0.0534}, {"index": 9, "type": "profile", "label": "Perfil 1", "area_ratio": 0.0426}, {"index": 10, "type": "object", "label": "Objeto 5", "area_ratio": 0.0007}, {"index": 11, "type": "object", "label": "Objeto 6", "area_ratio": 0.0004}, {"index": 12, "type": "wall", "label": "Pared 1", "area_ratio": 0.0421}, {"index": 13, "type": "wall", "label": "Pared 2", "area_ratio": 0.0411}, {"index": 14, "type": "object", "label": "Objeto 7", "area_ratio": 0.0}, {"index": 15, "type": "window", "label": "Ventana 3", "area_ratio": 0.0386}, {"index": 16, "type": "window", "label": "Ventana 4", "area_ratio": 0.0382}, {"index": 17, "type": "wall", "label": "Pared 3", "area_ratio": 0.0304}, {"index": 18, "type": "profile", "label": "Perfil 2", "area_ratio": 0.0201}, {"index": 19, "type": "wall", "label": "Pared 4", "area_ratio": 0.027}, {"index": 20, "type": "wall", "label": "Pared 5", "area_ratio": 0.0231}, {"index": 21, "type": "object", "label": "Objeto 8", "area_ratio": 0.0}, {"index": 22, "type": "ceiling", "label": "Techo 2", "area_ratio": 0.0218}, {"index": 23, "type": "object", "label": "Objeto 9", "area_ratio": 0.0002}, {"index": 24, "type": "wall", "label": "Pared 6", "area_ratio": 0.0205}, {"index": 25, "type": "wall", "label": "Pared 7", "area_ratio": 0.0121}, {"index": 26, "type": "object", "label": "Objeto 10", "area_ratio": 0.0017}, {"index": 27, "type": "object", "label": "Objeto 11", "area_ratio": 0.0009}, {"index": 28, "type": "object", "label": "Objeto 12", "area_ratio": 0.0093}, {"index": 29, "type": "profile", "label": "Perfil 3", "area_ratio": 0.0091}, {"index": 30, "type": "object", "label": "Objeto 13", "area_ratio": 0.0089}, {"index": 31, "type": "object", "label": "Objeto 14", "area_ratio": 0.0073}, {"index": 32, "type": "floor", "label": "Piso 2", "area_ratio": 0.0069}, {"index": 33, "type": "object", "label": "Objeto 15", "area_ratio": 0.0066}, {"index": 34, "type": "object", "label": "Objeto 16", "area_ratio": 0.0051}, {"index": 35, "type": "object", "label": "Objeto 17", "area_ratio": 0.0049}, {"index": 36, "type": "object", "label": "Objeto 18", "area_ratio": 0.0049}, {"index": 37, "type": "object", "label": "Objeto 19", "area_ratio": 0.0045}, {"index": 38, "type": "object", "label": "Objeto 20", "area_ratio": 0.0039}, {"index": 39, "type": "object", "label": "Objeto 21", "area_ratio": 0.0037}, {"index": 40, "type": "profile", "label": "Perfil 4", "area_ratio": 0.0035}, {"index": 41, "type": "object", "label": "Objeto 22", "area_ratio": 0.0031}, {"index": 42, "type": "object", "label": "Objeto 23", "area_ratio": 0.0012}, {"index": 43, "type": "object", "label": "Objeto 24", "area_ratio": 0.0011}, {"index": 44, "type": "object", "label": "Objeto 25", "area_ratio": 0.0011}, {"index": 45, "type": "object", "label": "Objeto 26", "area_ratio": 0.001}, {"index": 46, "type": "object", "label": "Objeto 27", "area_ratio": 0.001}, {"index": 47, "type": "object", "label": "Objeto 28", "area_ratio": 0.0}, {"index": 48, "type": "object", "label": "Objeto 29", "area_ratio": 0.0009}, {"index": 49, "type": "object", "label": "Objeto 30", "area_ratio": 0.0009}, {"index": 50, "type": "object", "label": "Objeto 31", "area_ratio": 0.0008}, {"index": 51, "type": "object", "label": "Objeto 32", "area_ratio": 0.0007}, {"index": 52, "type": "object", "label": "Objeto 33", "area_ratio": 0.0007}, {"index": 53, "type": "object", "label": "Objeto 34", "area_ratio": 0.0006}, {"index": 54, "type": "object", "label": "Objeto 35", "area_ratio": 0.0006}, {"index": 55, "type": "object", "label": "Objeto 36", "area_ratio": 0.0006}, {"index": 56, "type": "object", "label": "Objeto 37", "area_ratio": 0.0005}, {"index": 57, "type": "object", "label": "Objeto 38", "area_ratio": 0.0004}, {"index": 58, "type": "object", "label": "Objeto 39", "area_ratio": 0.0003}]} \ No newline at end of file diff --git a/backend/uploads/masks/acm azul_edit_2e8f9183_labels.png b/backend/uploads/masks/acm azul_edit_2e8f9183_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..4bc70e25068afb81eec9b1dcbfd6da7c27f60120 Binary files /dev/null and b/backend/uploads/masks/acm azul_edit_2e8f9183_labels.png differ diff --git a/backend/uploads/masks/acm azul_edit_3d3e91af_labels.png b/backend/uploads/masks/acm azul_edit_3d3e91af_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..4bc70e25068afb81eec9b1dcbfd6da7c27f60120 Binary files /dev/null and b/backend/uploads/masks/acm azul_edit_3d3e91af_labels.png differ diff --git a/backend/uploads/masks/acm azul_edit_55fe992d_labels.png b/backend/uploads/masks/acm azul_edit_55fe992d_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..4bc70e25068afb81eec9b1dcbfd6da7c27f60120 Binary files /dev/null and b/backend/uploads/masks/acm azul_edit_55fe992d_labels.png differ diff --git a/backend/uploads/masks/acm azul_edit_83b77dfe_edit_ec003a3a_labels.png b/backend/uploads/masks/acm azul_edit_83b77dfe_edit_ec003a3a_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..4bc70e25068afb81eec9b1dcbfd6da7c27f60120 Binary files /dev/null and b/backend/uploads/masks/acm azul_edit_83b77dfe_edit_ec003a3a_labels.png differ diff --git a/backend/uploads/masks/acm azul_edit_83b77dfe_labels.png b/backend/uploads/masks/acm azul_edit_83b77dfe_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..4bc70e25068afb81eec9b1dcbfd6da7c27f60120 Binary files /dev/null and b/backend/uploads/masks/acm azul_edit_83b77dfe_labels.png differ diff --git a/backend/uploads/masks/acm azul_edit_9456ec3a_labels.png b/backend/uploads/masks/acm azul_edit_9456ec3a_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..4bc70e25068afb81eec9b1dcbfd6da7c27f60120 Binary files /dev/null and b/backend/uploads/masks/acm azul_edit_9456ec3a_labels.png differ diff --git a/backend/uploads/masks/acm azul_edit_edb46e2f_labels.png b/backend/uploads/masks/acm azul_edit_edb46e2f_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..4bc70e25068afb81eec9b1dcbfd6da7c27f60120 Binary files /dev/null and b/backend/uploads/masks/acm azul_edit_edb46e2f_labels.png differ diff --git a/backend/uploads/masks/acm gris.jpg_labels.png b/backend/uploads/masks/acm gris.jpg_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..2e62c0f107180336d01e77e97eec0079e89c1463 Binary files /dev/null and b/backend/uploads/masks/acm gris.jpg_labels.png differ diff --git a/backend/uploads/masks/acm gris.jpg_labels_meta.json b/backend/uploads/masks/acm gris.jpg_labels_meta.json new file mode 100644 index 0000000000000000000000000000000000000000..937f04591ba9168fb102cdfbc05f944c2b0db30d --- /dev/null +++ b/backend/uploads/masks/acm gris.jpg_labels_meta.json @@ -0,0 +1 @@ +{"segments": [{"index": 1, "type": "wall", "label": "Pared 1", "area_ratio": 0.115}, {"index": 2, "type": "wall", "label": "Pared 2", "area_ratio": 0.1722}, {"index": 3, "type": "ceiling", "label": "Techo 1", "area_ratio": 0.3542}, {"index": 4, "type": "wall", "label": "Pared 3", "area_ratio": 0.0132}, {"index": 5, "type": "profile", "label": "Perfil 1", "area_ratio": 0.062}, {"index": 6, "type": "door", "label": "Puerta 1", "area_ratio": 0.0368}, {"index": 7, "type": "window", "label": "Ventana 1", "area_ratio": 0.0227}, {"index": 8, "type": "door", "label": "Puerta 2", "area_ratio": 0.0253}, {"index": 9, "type": "object", "label": "Objeto 1", "area_ratio": 0.0008}, {"index": 10, "type": "wall", "label": "Pared 4", "area_ratio": 0.0154}, {"index": 11, "type": "profile", "label": "Perfil 2", "area_ratio": 0.0116}, {"index": 12, "type": "object", "label": "Objeto 2", "area_ratio": 0.0001}, {"index": 13, "type": "wall", "label": "Pared 5", "area_ratio": 0.0111}, {"index": 14, "type": "floor", "label": "Piso 1", "area_ratio": 0.0096}, {"index": 15, "type": "object", "label": "Objeto 3", "area_ratio": 0.0007}, {"index": 16, "type": "object", "label": "Objeto 4", "area_ratio": 0.0012}, {"index": 17, "type": "object", "label": "Objeto 5", "area_ratio": 0.0042}, {"index": 18, "type": "object", "label": "Objeto 6", "area_ratio": 0.004}, {"index": 19, "type": "object", "label": "Objeto 7", "area_ratio": 0.0031}, {"index": 20, "type": "object", "label": "Objeto 8", "area_ratio": 0.0031}, {"index": 21, "type": "object", "label": "Objeto 9", "area_ratio": 0.0029}, {"index": 22, "type": "object", "label": "Objeto 10", "area_ratio": 0.0029}, {"index": 23, "type": "object", "label": "Objeto 11", "area_ratio": 0.0}, {"index": 24, "type": "object", "label": "Objeto 12", "area_ratio": 0.0023}, {"index": 25, "type": "object", "label": "Objeto 13", "area_ratio": 0.0023}, {"index": 26, "type": "object", "label": "Objeto 14", "area_ratio": 0.0023}, {"index": 27, "type": "object", "label": "Objeto 15", "area_ratio": 0.0022}, {"index": 28, "type": "object", "label": "Objeto 16", "area_ratio": 0.0022}, {"index": 29, "type": "object", "label": "Objeto 17", "area_ratio": 0.0021}, {"index": 30, "type": "object", "label": "Objeto 18", "area_ratio": 0.0021}, {"index": 31, "type": "object", "label": "Objeto 19", "area_ratio": 0.0021}, {"index": 32, "type": "object", "label": "Objeto 20", "area_ratio": 0.0021}, {"index": 33, "type": "object", "label": "Objeto 21", "area_ratio": 0.002}, {"index": 34, "type": "object", "label": "Objeto 22", "area_ratio": 0.002}, {"index": 35, "type": "object", "label": "Objeto 23", "area_ratio": 0.002}, {"index": 36, "type": "object", "label": "Objeto 24", "area_ratio": 0.0019}, {"index": 37, "type": "object", "label": "Objeto 25", "area_ratio": 0.0019}, {"index": 38, "type": "object", "label": "Objeto 26", "area_ratio": 0.0019}, {"index": 39, "type": "object", "label": "Objeto 27", "area_ratio": 0.0019}, {"index": 40, "type": "object", "label": "Objeto 28", "area_ratio": 0.0019}, {"index": 41, "type": "object", "label": "Objeto 29", "area_ratio": 0.0018}, {"index": 42, "type": "object", "label": "Objeto 30", "area_ratio": 0.0018}, {"index": 43, "type": "object", "label": "Objeto 31", "area_ratio": 0.0018}, {"index": 44, "type": "object", "label": "Objeto 32", "area_ratio": 0.0017}, {"index": 45, "type": "object", "label": "Objeto 33", "area_ratio": 0.0017}, {"index": 46, "type": "object", "label": "Objeto 34", "area_ratio": 0.0016}, {"index": 47, "type": "object", "label": "Objeto 35", "area_ratio": 0.0016}, {"index": 48, "type": "object", "label": "Objeto 36", "area_ratio": 0.0016}, {"index": 49, "type": "object", "label": "Objeto 37", "area_ratio": 0.0016}, {"index": 50, "type": "object", "label": "Objeto 38", "area_ratio": 0.0016}, {"index": 51, "type": "object", "label": "Objeto 39", "area_ratio": 0.0016}, {"index": 52, "type": "object", "label": "Objeto 40", "area_ratio": 0.0015}, {"index": 53, "type": "object", "label": "Objeto 41", "area_ratio": 0.0015}, {"index": 54, "type": "object", "label": "Objeto 42", "area_ratio": 0.0015}, {"index": 55, "type": "object", "label": "Objeto 43", "area_ratio": 0.0015}, {"index": 56, "type": "object", "label": "Objeto 44", "area_ratio": 0.0014}, {"index": 57, "type": "object", "label": "Objeto 45", "area_ratio": 0.0014}, {"index": 58, "type": "object", "label": "Objeto 46", "area_ratio": 0.0014}, {"index": 59, "type": "object", "label": "Objeto 47", "area_ratio": 0.0014}, {"index": 60, "type": "object", "label": "Objeto 48", "area_ratio": 0.0013}, {"index": 61, "type": "object", "label": "Objeto 49", "area_ratio": 0.0013}, {"index": 62, "type": "object", "label": "Objeto 50", "area_ratio": 0.0013}, {"index": 63, "type": "object", "label": "Objeto 51", "area_ratio": 0.0013}, {"index": 64, "type": "object", "label": "Objeto 52", "area_ratio": 0.0013}, {"index": 65, "type": "object", "label": "Objeto 53", "area_ratio": 0.0012}, {"index": 66, "type": "object", "label": "Objeto 54", "area_ratio": 0.0012}, {"index": 67, "type": "object", "label": "Objeto 55", "area_ratio": 0.0012}, {"index": 68, "type": "object", "label": "Objeto 56", "area_ratio": 0.0009}, {"index": 69, "type": "object", "label": "Objeto 57", "area_ratio": 0.0009}, {"index": 70, "type": "object", "label": "Objeto 58", "area_ratio": 0.0008}, {"index": 71, "type": "object", "label": "Objeto 59", "area_ratio": 0.0006}, {"index": 72, "type": "object", "label": "Objeto 60", "area_ratio": 0.0006}, {"index": 73, "type": "object", "label": "Objeto 61", "area_ratio": 0.0004}, {"index": 74, "type": "object", "label": "Objeto 62", "area_ratio": 0.0004}]} \ No newline at end of file diff --git a/backend/uploads/masks/acm gris_edit_09f2e747_labels.png b/backend/uploads/masks/acm gris_edit_09f2e747_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..2e62c0f107180336d01e77e97eec0079e89c1463 Binary files /dev/null and b/backend/uploads/masks/acm gris_edit_09f2e747_labels.png differ diff --git a/backend/uploads/masks/acm gris_edit_19d266a8_labels.png b/backend/uploads/masks/acm gris_edit_19d266a8_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..2e62c0f107180336d01e77e97eec0079e89c1463 Binary files /dev/null and b/backend/uploads/masks/acm gris_edit_19d266a8_labels.png differ diff --git a/backend/uploads/masks/hyper-reality-1778254222964.jpg_labels.png b/backend/uploads/masks/hyper-reality-1778254222964.jpg_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..6a487caef693def9cd8fe65ce0de89574780ff72 Binary files /dev/null and b/backend/uploads/masks/hyper-reality-1778254222964.jpg_labels.png differ diff --git a/backend/uploads/masks/hyper-reality-1778254222964.jpg_labels_meta.json b/backend/uploads/masks/hyper-reality-1778254222964.jpg_labels_meta.json new file mode 100644 index 0000000000000000000000000000000000000000..c42735cb944caea26785c78505b00d0525dfaf4b --- /dev/null +++ b/backend/uploads/masks/hyper-reality-1778254222964.jpg_labels_meta.json @@ -0,0 +1 @@ +{"segments": [{"index": 1, "type": "wall", "label": "Pared 1", "area_ratio": 0.0115}, {"index": 2, "type": "window", "label": "Ventana 1", "area_ratio": 0.1937}, {"index": 3, "type": "profile", "label": "Perfil 1", "area_ratio": 0.007}, {"index": 4, "type": "floor", "label": "Piso 1", "area_ratio": 0.0973}, {"index": 5, "type": "object", "label": "Objeto 1", "area_ratio": 0.0003}, {"index": 6, "type": "floor", "label": "Piso 2", "area_ratio": 0.0659}, {"index": 7, "type": "object", "label": "Objeto 2", "area_ratio": 0.0003}, {"index": 8, "type": "ceiling", "label": "Techo 1", "area_ratio": 0.0452}, {"index": 9, "type": "wall", "label": "Pared 2", "area_ratio": 0.0444}, {"index": 10, "type": "wall", "label": "Pared 3", "area_ratio": 0.0337}, {"index": 11, "type": "wall", "label": "Pared 4", "area_ratio": 0.0317}, {"index": 12, "type": "object", "label": "Objeto 3", "area_ratio": 0.0001}, {"index": 13, "type": "window", "label": "Ventana 2", "area_ratio": 0.0284}, {"index": 14, "type": "wall", "label": "Pared 5", "area_ratio": 0.003}, {"index": 15, "type": "wall", "label": "Pared 6", "area_ratio": 0.0238}, {"index": 16, "type": "wall", "label": "Pared 7", "area_ratio": 0.0237}, {"index": 17, "type": "profile", "label": "Perfil 2", "area_ratio": 0.0125}, {"index": 18, "type": "object", "label": "Objeto 4", "area_ratio": 0.0004}, {"index": 19, "type": "wall", "label": "Pared 8", "area_ratio": 0.0154}, {"index": 20, "type": "window", "label": "Ventana 3", "area_ratio": 0.014}, {"index": 21, "type": "object", "label": "Objeto 5", "area_ratio": 0.0001}, {"index": 22, "type": "window", "label": "Ventana 4", "area_ratio": 0.0101}, {"index": 23, "type": "profile", "label": "Perfil 3", "area_ratio": 0.009}, {"index": 24, "type": "object", "label": "Objeto 6", "area_ratio": 0.0084}, {"index": 25, "type": "ceiling", "label": "Techo 2", "area_ratio": 0.0081}, {"index": 26, "type": "window", "label": "Ventana 5", "area_ratio": 0.0077}, {"index": 27, "type": "object", "label": "Objeto 7", "area_ratio": 0.0014}, {"index": 28, "type": "wall", "label": "Pared 9", "area_ratio": 0.0067}, {"index": 29, "type": "ceiling", "label": "Techo 3", "area_ratio": 0.0066}, {"index": 30, "type": "window", "label": "Ventana 6", "area_ratio": 0.0055}, {"index": 31, "type": "window", "label": "Ventana 7", "area_ratio": 0.0055}, {"index": 32, "type": "ceiling", "label": "Techo 4", "area_ratio": 0.0053}, {"index": 33, "type": "object", "label": "Objeto 8", "area_ratio": 0.0053}, {"index": 34, "type": "window", "label": "Ventana 8", "area_ratio": 0.0052}, {"index": 35, "type": "window", "label": "Ventana 9", "area_ratio": 0.004}, {"index": 36, "type": "ceiling", "label": "Techo 5", "area_ratio": 0.004}, {"index": 37, "type": "window", "label": "Ventana 10", "area_ratio": 0.0038}, {"index": 38, "type": "ceiling", "label": "Techo 6", "area_ratio": 0.0036}, {"index": 39, "type": "object", "label": "Objeto 9", "area_ratio": 0.0034}, {"index": 40, "type": "window", "label": "Ventana 11", "area_ratio": 0.0034}, {"index": 41, "type": "object", "label": "Objeto 10", "area_ratio": 0.0001}, {"index": 42, "type": "object", "label": "Objeto 11", "area_ratio": 0.0007}, {"index": 43, "type": "object", "label": "Objeto 12", "area_ratio": 0.0025}, {"index": 44, "type": "object", "label": "Objeto 13", "area_ratio": 0.0003}, {"index": 45, "type": "object", "label": "Objeto 14", "area_ratio": 0.0021}, {"index": 46, "type": "object", "label": "Objeto 15", "area_ratio": 0.0}, {"index": 47, "type": "object", "label": "Objeto 16", "area_ratio": 0.002}, {"index": 48, "type": "object", "label": "Objeto 17", "area_ratio": 0.0019}, {"index": 49, "type": "object", "label": "Objeto 18", "area_ratio": 0.0019}, {"index": 50, "type": "object", "label": "Objeto 19", "area_ratio": 0.0019}, {"index": 51, "type": "object", "label": "Objeto 20", "area_ratio": 0.0003}, {"index": 52, "type": "object", "label": "Objeto 21", "area_ratio": 0.0017}, {"index": 53, "type": "object", "label": "Objeto 22", "area_ratio": 0.0016}, {"index": 54, "type": "object", "label": "Objeto 23", "area_ratio": 0.0015}, {"index": 55, "type": "object", "label": "Objeto 24", "area_ratio": 0.0014}, {"index": 56, "type": "object", "label": "Objeto 25", "area_ratio": 0.0014}, {"index": 57, "type": "object", "label": "Objeto 26", "area_ratio": 0.0014}, {"index": 58, "type": "object", "label": "Objeto 27", "area_ratio": 0.0011}, {"index": 59, "type": "object", "label": "Objeto 28", "area_ratio": 0.001}, {"index": 60, "type": "object", "label": "Objeto 29", "area_ratio": 0.0004}, {"index": 61, "type": "object", "label": "Objeto 30", "area_ratio": 0.0004}]} \ No newline at end of file diff --git a/backend/uploads/masks/hyper-reality-1778254222964_edit_227e7927_labels.png b/backend/uploads/masks/hyper-reality-1778254222964_edit_227e7927_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..4a8c27fc27941268199871b8c441178fb62478f7 Binary files /dev/null and b/backend/uploads/masks/hyper-reality-1778254222964_edit_227e7927_labels.png differ diff --git a/backend/uploads/masks/hyper-reality-1778254222964_edit_2ea5bcda_labels.png b/backend/uploads/masks/hyper-reality-1778254222964_edit_2ea5bcda_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..4a8c27fc27941268199871b8c441178fb62478f7 Binary files /dev/null and b/backend/uploads/masks/hyper-reality-1778254222964_edit_2ea5bcda_labels.png differ diff --git a/backend/uploads/masks/hyper-reality-1778254222964_edit_7d919375_labels.png b/backend/uploads/masks/hyper-reality-1778254222964_edit_7d919375_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..4a8c27fc27941268199871b8c441178fb62478f7 Binary files /dev/null and b/backend/uploads/masks/hyper-reality-1778254222964_edit_7d919375_labels.png differ diff --git a/backend/uploads/masks/hyper-reality-1778254222964_edit_9c5de50a_edit_7e5043d5_labels.png b/backend/uploads/masks/hyper-reality-1778254222964_edit_9c5de50a_edit_7e5043d5_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..6a487caef693def9cd8fe65ce0de89574780ff72 Binary files /dev/null and b/backend/uploads/masks/hyper-reality-1778254222964_edit_9c5de50a_edit_7e5043d5_labels.png differ diff --git a/backend/uploads/masks/hyper-reality-1778254222964_edit_9c5de50a_labels.png b/backend/uploads/masks/hyper-reality-1778254222964_edit_9c5de50a_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..6a487caef693def9cd8fe65ce0de89574780ff72 Binary files /dev/null and b/backend/uploads/masks/hyper-reality-1778254222964_edit_9c5de50a_labels.png differ diff --git a/backend/uploads/masks/hyper-reality-1778254222964_edit_a25fb848_labels.png b/backend/uploads/masks/hyper-reality-1778254222964_edit_a25fb848_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..6a487caef693def9cd8fe65ce0de89574780ff72 Binary files /dev/null and b/backend/uploads/masks/hyper-reality-1778254222964_edit_a25fb848_labels.png differ diff --git a/backend/uploads/masks/hyper-reality-1778254222964_edit_a92f4266_edit_161ee5c4_labels.png b/backend/uploads/masks/hyper-reality-1778254222964_edit_a92f4266_edit_161ee5c4_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..6a487caef693def9cd8fe65ce0de89574780ff72 Binary files /dev/null and b/backend/uploads/masks/hyper-reality-1778254222964_edit_a92f4266_edit_161ee5c4_labels.png differ diff --git a/backend/uploads/masks/hyper-reality-1778254222964_edit_a92f4266_labels.png b/backend/uploads/masks/hyper-reality-1778254222964_edit_a92f4266_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..6a487caef693def9cd8fe65ce0de89574780ff72 Binary files /dev/null and b/backend/uploads/masks/hyper-reality-1778254222964_edit_a92f4266_labels.png differ diff --git a/backend/uploads/masks/hyper-reality-1778254222964_edit_eb8100f4_labels.png b/backend/uploads/masks/hyper-reality-1778254222964_edit_eb8100f4_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..4a8c27fc27941268199871b8c441178fb62478f7 Binary files /dev/null and b/backend/uploads/masks/hyper-reality-1778254222964_edit_eb8100f4_labels.png differ diff --git a/frontend/dist/assets/index-BDf3OMM_.css b/frontend/dist/assets/index-BDf3OMM_.css new file mode 100644 index 0000000000000000000000000000000000000000..0b4526a22a1fab32cdf7262dae2a577338587517 --- /dev/null +++ b/frontend/dist/assets/index-BDf3OMM_.css @@ -0,0 +1 @@ +*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border:0 solid #e5e7eb}:before,:after{--tw-content:""}html,:host{-webkit-text-size-adjust:100%;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{line-height:inherit;margin:0}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-feature-settings:normal;font-variation-settings:normal;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-feature-settings:inherit;font-variation-settings:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:#0000;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder{opacity:1;color:#9ca3af}textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (width>=640px){.container{max-width:640px}}@media (width>=768px){.container{max-width:768px}}@media (width>=1024px){.container{max-width:1024px}}@media (width>=1280px){.container{max-width:1280px}}@media (width>=1536px){.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.pointer-events-auto{pointer-events:auto}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.inset-x-0{left:0;right:0}.-bottom-1{bottom:-.25rem}.-top-2{top:-.5rem}.bottom-0{bottom:0}.bottom-10{bottom:2.5rem}.bottom-12{bottom:3rem}.left-0{left:0}.left-1\/2{left:50%}.left-3{left:.75rem}.right-0{right:0}.right-2{right:.5rem}.right-3{right:.75rem}.top-0{top:0}.top-1\/2{top:50%}.top-12{top:3rem}.top-2{top:.5rem}.z-10{z-index:10}.z-50{z-index:50}.mx-auto{margin-left:auto;margin-right:auto}.my-5{margin-top:1.25rem;margin-bottom:1.25rem}.mb-1{margin-bottom:.25rem}.mb-10{margin-bottom:2.5rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-auto{margin-left:auto}.mt-1{margin-top:.25rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-8{margin-top:2rem}.mt-\[3px\]{margin-top:3px}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.aspect-\[4\/3\]{aspect-ratio:4/3}.aspect-square{aspect-ratio:1}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-32{height:8rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-\[1px\]{height:1px}.h-full{height:100%}.h-screen{height:100vh}.max-h-full{max-height:100%}.min-h-0{min-height:0}.min-h-\[220px\]{min-height:220px}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-14{width:3.5rem}.w-2{width:.5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-\[80\%\]{width:80%}.w-fit{width:fit-content}.w-full{width:100%}.min-w-0{min-width:0}.max-w-7xl{max-width:80rem}.max-w-full{max-width:100%}.max-w-xs{max-width:20rem}.flex-1{flex:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.-translate-x-1\/2{--tw-translate-x:-50%;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y:-50%;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-180{--tw-rotate:180deg;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-45{--tw-rotate:45deg;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}.animate-bounce{animation:1s infinite bounce}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:2s cubic-bezier(.4,0,.6,1) infinite pulse}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:1s linear infinite spin}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.gap-6{gap:1.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.rounded-b-md{border-bottom-right-radius:.375rem;border-bottom-left-radius:.375rem}.rounded-t-md{border-top-left-radius:.375rem;border-top-right-radius:.375rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-\[\#0047AB\]{--tw-border-opacity:1;border-color:rgb(0 71 171/var(--tw-border-opacity,1))}.border-\[\#0047AB\]\/10{border-color:#0047ab1a}.border-\[\#0047AB\]\/20{border-color:#0047ab33}.border-\[\#dbe7ff\]{--tw-border-opacity:1;border-color:rgb(219 231 255/var(--tw-border-opacity,1))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity,1))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity,1))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity,1))}.border-transparent{border-color:#0000}.bg-\[\#0047AB\]{--tw-bg-opacity:1;background-color:rgb(0 71 171/var(--tw-bg-opacity,1))}.bg-\[\#0047AB\]\/20{background-color:#0047ab33}.bg-\[\#0047AB\]\/80{background-color:#0047abcc}.bg-\[\#333333\]{--tw-bg-opacity:1;background-color:rgb(51 51 51/var(--tw-bg-opacity,1))}.bg-\[\#eaf1ff\]{--tw-bg-opacity:1;background-color:rgb(234 241 255/var(--tw-bg-opacity,1))}.bg-\[\#edf4ff\]{--tw-bg-opacity:1;background-color:rgb(237 244 255/var(--tw-bg-opacity,1))}.bg-\[\#f4f8ff\]{--tw-bg-opacity:1;background-color:rgb(244 248 255/var(--tw-bg-opacity,1))}.bg-black\/0{background-color:#0000}.bg-black\/40{background-color:#0006}.bg-black\/50{background-color:#00000080}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.bg-gray-950{--tw-bg-opacity:1;background-color:rgb(3 7 18/var(--tw-bg-opacity,1))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.bg-transparent{background-color:#0000}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-white\/80{background-color:#fffc}.bg-yellow-400{--tw-bg-opacity:1;background-color:rgb(250 204 21/var(--tw-bg-opacity,1))}.bg-zinc-400{--tw-bg-opacity:1;background-color:rgb(161 161 170/var(--tw-bg-opacity,1))}.bg-gradient-to-t{background-image:linear-gradient(to top, var(--tw-gradient-stops))}.from-black\/60{--tw-gradient-from:#0009 var(--tw-gradient-from-position);--tw-gradient-to:#0000 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), var(--tw-gradient-to)}.via-transparent{--tw-gradient-to:#0000 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from), transparent var(--tw-gradient-via-position), var(--tw-gradient-to)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pb-2{padding-bottom:.5rem}.pb-4{padding-bottom:1rem}.pl-11{padding-left:2.75rem}.pr-10{padding-right:2.5rem}.pr-12{padding-right:3rem}.text-left{text-align:left}.text-center{text-align:center}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[10px\]{font-size:10px}.text-\[13px\]{font-size:13px}.text-\[15px\]{font-size:15px}.text-\[16px\]{font-size:16px}.text-\[17px\]{font-size:17px}.text-\[9px\]{font-size:9px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-light{font-weight:300}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-tight{line-height:1.25}.tracking-tight{letter-spacing:-.025em}.tracking-tighter{letter-spacing:-.05em}.tracking-widest{letter-spacing:.1em}.text-\[\#0047AB\]{--tw-text-opacity:1;color:rgb(0 71 171/var(--tw-text-opacity,1))}.text-\[\#333333\],.text-\[\#333\]{--tw-text-opacity:1;color:rgb(51 51 51/var(--tw-text-opacity,1))}.text-\[\#4a7fd4\]{--tw-text-opacity:1;color:rgb(74 127 212/var(--tw-text-opacity,1))}.text-\[\#555\]{--tw-text-opacity:1;color:rgb(85 85 85/var(--tw-text-opacity,1))}.text-\[\#707070\]{--tw-text-opacity:1;color:rgb(112 112 112/var(--tw-text-opacity,1))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-slate-800{--tw-text-opacity:1;color:rgb(30 41 59/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/80{color:#fffc}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-30{opacity:.3}.opacity-80{opacity:.8}.opacity-90{opacity:.9}.shadow-2xl{--tw-shadow:0 25px 50px -12px #00000040;--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000), var(--tw-ring-shadow,0 0 #0000), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000), var(--tw-ring-shadow,0 0 #0000), var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000), var(--tw-ring-shadow,0 0 #0000), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000), var(--tw-ring-shadow,0 0 #0000), var(--tw-shadow)}.shadow-\[\#0047AB\]\/20{--tw-shadow-color:#0047ab33;--tw-shadow:var(--tw-shadow-colored)}.outline{outline-style:solid}.drop-shadow-sm{--tw-drop-shadow:drop-shadow(0 1px 1px #0000000d);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter,backdrop-filter;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-property:all;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-opacity{transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-property:transform;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.\[animation-delay\:-0\.15s\]{animation-delay:-.15s}.\[animation-delay\:-0\.3s\]{animation-delay:-.3s}:root{--brand-blue:#0047ab;--brand-black:#333;--brand-gray:#707070;--brand-surface:#fff;--brand-light:#f4f8ff;--brand-border:#dbe7ff;--brand-contrast:#25d366;color:var(--brand-black);background:var(--brand-light);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-size:16px}*,:before,:after{box-sizing:border-box}html{overscroll-behavior:none;height:100%}body{color:#111827;overscroll-behavior:none;background:#f8fafc;height:100%;min-height:100dvh;margin:0}#root{flex-direction:column;height:100%;min-height:100dvh;display:flex}a{color:inherit;text-decoration:none}img{max-width:100%;display:block}h1,h2,h3,p{margin:0}ul{margin:0;padding:0;list-style:none}.last\:border-0:last-child{border-width:0}.hover\:border-\[\#0047AB\]:hover{--tw-border-opacity:1;border-color:rgb(0 71 171/var(--tw-border-opacity,1))}.hover\:border-\[\#0047AB\]\/40:hover{border-color:#0047ab66}.hover\:border-\[\#0047AB\]\/50:hover{border-color:#0047ab80}.hover\:bg-\[\#003a94\]:hover{--tw-bg-opacity:1;background-color:rgb(0 58 148/var(--tw-bg-opacity,1))}.hover\:bg-\[\#eaf1ff\]:hover{--tw-bg-opacity:1;background-color:rgb(234 241 255/var(--tw-bg-opacity,1))}.hover\:bg-\[\#eef4ff\]:hover{--tw-bg-opacity:1;background-color:rgb(238 244 255/var(--tw-bg-opacity,1))}.hover\:bg-\[\#f4f8ff\]:hover{--tw-bg-opacity:1;background-color:rgb(244 248 255/var(--tw-bg-opacity,1))}.hover\:bg-black:hover{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity,1))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.hover\:bg-red-500:hover{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.hover\:text-\[\#002c75\]:hover{--tw-text-opacity:1;color:rgb(0 44 117/var(--tw-text-opacity,1))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.focus\:outline-none:focus{outline-offset:2px;outline:2px solid #0000}.disabled\:opacity-60:disabled{opacity:.6}.group:hover .group-hover\:translate-x-1{--tw-translate-x:.25rem;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:border-\[\#0047AB\]{--tw-border-opacity:1;border-color:rgb(0 71 171/var(--tw-border-opacity,1))}.group:hover .group-hover\:bg-black\/10{background-color:#0000001a}.group:hover .group-hover\:opacity-100{opacity:1}@media (width>=640px){.sm\:mb-14{margin-bottom:3.5rem}.sm\:mb-16{margin-bottom:4rem}.sm\:mb-4{margin-bottom:1rem}.sm\:mb-6{margin-bottom:1.5rem}.sm\:mb-8{margin-bottom:2rem}.sm\:mt-4{margin-top:1rem}.sm\:aspect-\[4\/3\]{aspect-ratio:4/3}.sm\:h-16{height:4rem}.sm\:h-32{height:8rem}.sm\:h-6{height:1.5rem}.sm\:h-8{height:2rem}.sm\:min-h-0{min-height:0}.sm\:w-16{width:4rem}.sm\:w-32{width:8rem}.sm\:w-6{width:1.5rem}.sm\:w-72{width:18rem}.sm\:w-8{width:2rem}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:gap-6{gap:1.5rem}.sm\:space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-4{padding-top:1rem;padding-bottom:1rem}.sm\:py-8{padding-top:2rem;padding-bottom:2rem}.sm\:text-2xl{font-size:1.5rem;line-height:2rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-base{font-size:1rem;line-height:1.5rem}.sm\:text-lg{font-size:1.125rem;line-height:1.75rem}.sm\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (width>=768px){.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (width>=1024px){.lg\:bottom-16{bottom:4rem}.lg\:top-16{top:4rem}.lg\:mb-10{margin-bottom:2.5rem}.lg\:mb-20{margin-bottom:5rem}.lg\:mb-8{margin-bottom:2rem}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:gap-12{gap:3rem}.lg\:rounded-lg{border-radius:.5rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:text-4xl{font-size:2.25rem;line-height:2.5rem}}:root{color:var(--brand-black);background:var(--brand-light);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;line-height:1.5}*{box-sizing:border-box}html,body,#root{min-height:100%}body{background:var(--brand-light);color:var(--brand-black);margin:0}button,input,select,textarea{font:inherit}button{cursor:pointer}.app-shell{max-width:1200px;margin:0 auto;padding:24px}.topbar{background:var(--brand-surface);border:1px solid var(--brand-border);border-radius:18px;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:16px;padding:22px 24px;display:flex}.topbar h1{margin:0;font-size:clamp(1.9rem,2.5vw,2.6rem)}.topbar p{color:var(--brand-gray);margin:6px 0 0}.topbar-nav{flex-wrap:wrap;gap:10px;display:flex}.topbar-nav a{background:var(--brand-surface);color:var(--brand-black);border:1px solid #0000;border-radius:14px;justify-content:center;align-items:center;padding:10px 14px;text-decoration:none;transition:background .2s,border-color .2s;display:inline-flex}.topbar-nav a:hover{background:#fff;border-color:#d1d5db}.content{grid-template-columns:1.2fr 1.8fr;gap:24px;margin-top:24px;display:grid}.route-index,.panel,.viewer-panel,.bridge-panel,.room-setup-dropzone,.room-setup-card,.room-setup-preview{background:var(--brand-surface);border:1px solid var(--brand-border);border-radius:20px}.route-index,.panel,.viewer-panel,.bridge-panel,.room-setup-card{padding:22px}.route-index h2,.panel h2,.viewer-panel h2,.room-setup-copy h1,.room-setup-bottom h2{margin:0 0 16px}.route-index p,.room-setup-features li,.room-setup-drop-content p,.room-setup-drop-content small,.room-setup-card h3,.topbar p,.error-box,.empty-state{color:#475569}.route-index ul,.room-setup-features,.room-setup-filters,.room-setup-grid{margin:0;padding:0;list-style:none}.route-index ul{gap:12px;display:grid}.form-row{gap:14px;margin-bottom:20px;display:grid}label{color:var(--brand-black);font-size:.95rem}input,select,textarea{color:#111827;background:#fff;border:1px solid #d1d5db;border-radius:14px;width:100%;padding:14px 16px}input:focus,select:focus,textarea:focus{outline:2px solid var(--brand-blue);outline-offset:2px}.button,.button-primary,.button-secondary{border:1px solid #0000;border-radius:16px;justify-content:center;align-items:center;gap:10px;padding:14px 18px;font-weight:700;transition:all .2s;display:inline-flex}.button{background:var(--brand-black);color:var(--brand-surface)}.button:hover{opacity:.95}.button.secondary,.button-secondary{background:var(--brand-surface);color:var(--brand-black);border-color:var(--brand-border)}.error-box{color:#991b1b;background:#fef2f2;border:1px solid #fecaca;border-radius:16px;margin-top:16px;padding:16px}.empty-state{border:1px dashed #d1d5db;border-radius:18px;place-content:center;min-height:220px;display:grid}.room-setup{background:var(--brand-light)}.room-setup-inner{max-width:1200px;margin:0 auto;padding:32px 24px}.room-setup-header{justify-content:flex-end;margin-bottom:24px;display:flex}.room-setup-close{color:var(--brand-black);cursor:pointer;background:0 0;border:none;border-radius:999px;padding:10px}.room-setup-close:hover{background:var(--brand-border)}.room-setup-top{grid-template-columns:1.1fr .9fr;gap:24px;margin-bottom:40px;display:grid}.room-setup-copy h1{color:#111827;margin:0 0 24px;font-size:clamp(2rem,2.5vw,3rem)}.room-setup-features{gap:16px;display:grid}.room-setup-features li{color:#475569;align-items:center;gap:12px;display:flex}.room-setup-actions{gap:16px;display:grid}.button-primary{color:#fff;background:#111827}.button-primary:hover{opacity:.95}.button-secondary{color:#334155;background:#fff;border-color:#d1d5db}.button-secondary:hover{background:#f3f4f6}.button-icon-border{border:1px solid #d1d5db;border-radius:10px;justify-content:center;align-items:center;width:30px;height:30px;display:inline-flex}.room-setup-dropzone{border:1px dashed #d1d5db;border-radius:28px;justify-content:center;align-items:center;min-height:420px;padding:24px;transition:all .2s;display:flex;position:relative}.room-setup-dropzone.dragging{background:#f3f4f6}.room-setup-file-input{opacity:0;cursor:pointer;width:100%;height:100%;position:absolute;inset:0}.room-setup-drop-content{text-align:center}.room-setup-drop-icon{color:#64748b;background:#e2e8f0;border-radius:999px;justify-content:center;align-items:center;width:72px;height:72px;margin-bottom:18px;display:inline-flex}.room-setup-drop-icon.active{color:#1e293b;background:#dbeafe}.room-setup-drop-content h3{margin:0 0 8px;font-size:1.2rem}.room-setup-drop-content p,.room-setup-drop-content small{color:#64748b;margin:0}.room-setup-preview{width:100%;height:100%;position:relative}.room-setup-preview img{-o-object-fit:cover;object-fit:cover;width:100%;height:100%}.room-setup-preview-overlay{opacity:0;background:#ffffffd9;justify-content:center;align-items:center;transition:opacity .2s;display:flex;position:absolute;inset:0}.room-setup-preview:hover .room-setup-preview-overlay{opacity:1}.button-delete{color:#111827;background:#fff;border:1px solid #d1d5db;border-radius:14px;padding:12px 16px;font-weight:600}.room-setup-bottom h2{margin:0 0 24px}.room-setup-filters{flex-wrap:wrap;gap:10px;margin-bottom:24px;display:flex}.room-setup-filter{color:#334155;cursor:pointer;background:#fff;border:1px solid #d1d5db;border-radius:16px;padding:10px 16px;transition:all .2s}.room-setup-filter.active{background:#f3f4f6}.room-setup-grid{grid-template-columns:repeat(1,minmax(0,1fr));gap:20px;display:grid}.room-setup-card{background:#fff;border-radius:24px;overflow:hidden;box-shadow:0 8px 20px #0f172a0f}.room-setup-card-image{aspect-ratio:4/3;position:relative;overflow:hidden}.room-setup-card-image img{-o-object-fit:cover;object-fit:cover;width:100%;height:100%;transition:transform .4s}.room-setup-card:hover .room-setup-card-image img{transform:scale(1.04)}.room-setup-card h3{color:#334155;margin:16px;font-size:1rem}@media (width<=960px){.room-setup-top{grid-template-columns:1fr}.room-setup-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (width<=640px){.room-setup-inner{padding:20px 16px}.room-setup-grid{grid-template-columns:1fr}}.app-version-badge{color:#475569;text-align:right;z-index:1000;background:#fffffff2;border:1px solid #e5e7eb;border-radius:12px 0 0;padding:6px 10px;font-size:.78rem;position:fixed;bottom:0;right:0;box-shadow:0 8px 20px #0f172a14}.app-version-badge:hover{opacity:1}.app-version-badge span{color:#111827;font-weight:700;display:block}.app-version-badge small{color:#6b7280;margin-top:1px;display:block}@media (width<=640px){.app-version-badge{padding:5px 8px;font-size:.72rem;bottom:6px;right:6px}.app-version-badge small{margin-top:.5px}}@media (width<=900px){.content{grid-template-columns:1fr}} diff --git a/frontend/dist/assets/index-Ca5g_fre.js b/frontend/dist/assets/index-Ca5g_fre.js new file mode 100644 index 0000000000000000000000000000000000000000..d2d5065ad6ab4560f64d68209ce283e5fe77fb35 --- /dev/null +++ b/frontend/dist/assets/index-Ca5g_fre.js @@ -0,0 +1,90 @@ +var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},c=(n,r,a)=>(a=n==null?{}:e(i(n)),s(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var l=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var ee=Array.isArray;function S(){}var C={H:null,A:null,T:null,S:null},w=Object.prototype.hasOwnProperty;function T(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function te(e,t){return T(e.type,t,e.props)}function ne(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function re(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var ie=/\/+/g;function ae(e,t){return typeof e==`object`&&e&&e.key!=null?re(``+e.key):t.toString(36)}function oe(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(S,S):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function se(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,se(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+ae(e,0):a,ee(o)?(i=``,c!=null&&(i=c.replace(ie,`$&/`)+`/`),se(o,r,i,``,function(e){return e})):o!=null&&(ne(o)&&(o=te(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(ie,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(ee(e))for(var u=0;u{t.exports=l()})),d=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m)if(n(c)!==null)m=!0,ee||(ee=!0,ne());else{var t=n(l);t!==null&&ae(x,t.startTime-e)}}var ee=!1,S=-1,C=5,w=-1;function T(){return g?!0:!(e.unstable_now()-wt&&T());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&ae(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?ne():ee=!1}}}var ne;if(typeof y==`function`)ne=function(){y(te)};else if(typeof MessageChannel<`u`){var re=new MessageChannel,ie=re.port2;re.port1.onmessage=te,ne=function(){ie.postMessage(null)}}else ne=function(){_(te,0)};function ae(t,n){S=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(S),S=-1):h=!0,ae(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,ee||(ee=!0,ne()))),r},e.unstable_shouldYield=T,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),f=o(((e,t)=>{t.exports=d()})),p=o((e=>{var t=u();function n(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=p()})),h=o((e=>{var t=f(),n=u(),r=m();function i(e){var t=`https://react.dev/errors/`+e;if(1fe||(e.current=de[fe],de[fe]=null,fe--)}function O(e,t){fe++,de[fe]=e.current,e.current=t}var he=pe(null),ge=pe(null),_e=pe(null),ve=pe(null);function ye(e,t){switch(O(_e,t),O(ge,e),O(he,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Vd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Vd(t),e=Hd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}me(he),O(he,e)}function be(){me(he),me(ge),me(_e)}function k(e){e.memoizedState!==null&&O(ve,e);var t=he.current,n=Hd(t,e.type);t!==n&&(O(ge,e),O(he,n))}function xe(e){ge.current===e&&(me(he),me(ge)),ve.current===e&&(me(ve),Qf._currentValue=ue)}var Se,Ce;function A(e){if(Se===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);Se=t&&t[1]||``,Ce=-1)`:-1i||c[r]!==l[i]){var u=` +`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{we=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?A(n):``}function Ee(e,t){switch(e.tag){case 26:case 27:case 5:return A(e.type);case 16:return A(`Lazy`);case 13:return e.child!==t&&t!==null?A(`Suspense Fallback`):A(`Suspense`);case 19:return A(`SuspenseList`);case 0:case 15:return Te(e.type,!1);case 11:return Te(e.type.render,!1);case 1:return Te(e.type,!0);case 31:return A(`Activity`);default:return``}}function De(e){try{var t=``,n=null;do t+=Ee(e,n),n=e,e=e.return;while(e);return t}catch(e){return` +Error generating stack: `+e.message+` +`+e.stack}}var Oe=Object.prototype.hasOwnProperty,ke=t.unstable_scheduleCallback,Ae=t.unstable_cancelCallback,je=t.unstable_shouldYield,Me=t.unstable_requestPaint,Ne=t.unstable_now,Pe=t.unstable_getCurrentPriorityLevel,Fe=t.unstable_ImmediatePriority,Ie=t.unstable_UserBlockingPriority,Le=t.unstable_NormalPriority,Re=t.unstable_LowPriority,ze=t.unstable_IdlePriority,Be=t.log,Ve=t.unstable_setDisableYieldValue,He=null,Ue=null;function We(e){if(typeof Be==`function`&&Ve(e),Ue&&typeof Ue.setStrictMode==`function`)try{Ue.setStrictMode(He,e)}catch{}}var Ge=Math.clz32?Math.clz32:Je,Ke=Math.log,qe=Math.LN2;function Je(e){return e>>>=0,e===0?32:31-(Ke(e)/qe|0)|0}var Ye=256,Xe=262144,Ze=4194304;function Qe(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function $e(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=Qe(n))):i=Qe(o):i=Qe(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Qe(n))):i=Qe(o)):i=Qe(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function et(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function tt(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function nt(){var e=Ze;return Ze<<=1,!(Ze&62914560)&&(Ze=4194304),e}function rt(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function it(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function at(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),_n=!1;if(gn)try{var vn={};Object.defineProperty(vn,`passive`,{get:function(){_n=!0}}),window.addEventListener(`test`,vn,vn),window.removeEventListener(`test`,vn,vn)}catch{_n=!1}var yn=null,bn=null,xn=null;function Sn(){if(xn)return xn;var e,t=bn,n=t.length,r,i=`value`in yn?yn.value:yn.textContent,a=i.length;for(e=0;e=er),rr=` `,ir=!1;function ar(e,t){switch(e){case`keyup`:return Qn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function or(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var sr=!1;function cr(e,t){switch(e){case`compositionend`:return or(t);case`keypress`:return t.which===32?(ir=!0,rr):null;case`textInput`:return e=t.data,e===rr&&ir?null:e;default:return null}}function lr(e,t){if(sr)return e===`compositionend`||!$n&&ar(e,t)?(e=Sn(),xn=bn=yn=null,sr=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=jr(n)}}function Nr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Nr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Pr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Ut(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=Ut(e.document)}return t}function Fr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var Ir=gn&&`documentMode`in document&&11>=document.documentMode,Lr=null,Rr=null,zr=null,Br=!1;function Vr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Br||Lr==null||Lr!==Ut(r)||(r=Lr,`selectionStart`in r&&Fr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),zr&&Ar(zr,r)||(zr=r,r=Ed(Rr,`onSelect`),0>=o,i-=o,Ai=1<<32-Ge(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),I&&Mi(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),I&&Mi(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return I&&Mi(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),I&&Mi(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===ne&&Ta(l)===r.type){n(e,r.sibling),c=a(r,o.props),Ma(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=vi(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=_i(o.type,o.key,o.props,null,e.mode,c),Ma(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}else{n(e,r);break}else t(e,r);r=r.sibling}c=P(o,e.mode,c),c.return=e,e=c}return s(e);case ne:return o=Ta(o),b(e,r,o,c)}if(le(o))return h(e,r,o,c);if(oe(o)){if(l=oe(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,ja(o),c);if(o.$$typeof===S)return b(e,r,aa(e,o),c);Na(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=yi(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Aa=0;var i=b(e,t,n,r);return ka=null,i}catch(t){if(t===ba||t===Sa)throw t;var a=pi(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Fa=Pa(!0),Ia=Pa(!1),La=!1;function Ra(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function za(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Ba(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Va(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,K&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=ui(e),N(e,null,n),t}return si(e,r,t,n),ui(e)}function Ha(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,j(e,n)}}function Ua(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Wa=!1;function Ga(){if(Wa){var e=pa;if(e!==null)throw e}}function Ka(e,t,n,r){Wa=!1;var i=e.updateQueue;La=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(Y&f)===f:(r&f)===f){f!==0&&f===z&&(Wa=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var m=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(m=g.payload,typeof m==`function`){d=m.call(_,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=g.payload,f=typeof m==`function`?m.call(_,d,f):m,f==null)break a;d=h({},d,f);break a;case 2:La=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Gl|=o,e.lanes=o,e.memoizedState=d}}function qa(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function Ja(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=E.T,s={};E.T=s,Ns(e,!1,t,n);try{var c=i(),l=E.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Ms(e,t,B(c,r),pu(e)):Ms(e,t,r,pu(e))}catch(n){Ms(e,t,{then:function(){},status:`rejected`,reason:n},pu())}finally{D.p=a,o!==null&&s.types!==null&&(o.types=s.types),E.T=o}}function Ss(){}function Cs(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=ws(e).queue;xs(e,a,t,ue,n===null?Ss:function(){return Ts(e),n(r)})}function ws(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:ue,baseState:ue,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Po,lastRenderedState:ue},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Po,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Ts(e){var t=ws(e);t.next===null&&(t=e.alternate.memoizedState),Ms(e,t.next.queue,{},pu())}function Es(){return ia(Qf)}function Ds(){return ko().memoizedState}function Os(){return ko().memoizedState}function ks(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=pu();e=Ba(n);var r=Va(t,e,n);r!==null&&(hu(r,t,n),Ha(r,t,n)),t={cache:da()},e.payload=t;return}t=t.return}}function As(e,t,n){var r=pu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Ps(e)?Fs(t,n):(n=ci(e,t,n,r),n!==null&&(hu(n,e,r),Is(n,t,r)))}function js(e,t,n){Ms(e,t,n,pu())}function Ms(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Ps(e))Fs(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,kr(s,o))return si(e,t,i,0),q===null&&oi(),!1}catch{}if(n=ci(e,t,i,r),n!==null)return hu(n,e,r),Is(n,t,r),!0}return!1}function Ns(e,t,n,r){if(r={lane:2,revertLane:dd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Ps(e)){if(t)throw Error(i(479))}else t=ci(e,n,r,2),t!==null&&hu(t,e,2)}function Ps(e){var t=e.alternate;return e===U||t!==null&&t===U}function Fs(e,t){po=fo=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Is(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,j(e,n)}}var Ls={readContext:ia,use:Mo,useCallback:yo,useContext:yo,useEffect:yo,useImperativeHandle:yo,useLayoutEffect:yo,useInsertionEffect:yo,useMemo:yo,useReducer:yo,useRef:yo,useState:yo,useDebugValue:yo,useDeferredValue:yo,useTransition:yo,useSyncExternalStore:yo,useId:yo,useHostTransitionStatus:yo,useFormState:yo,useActionState:yo,useOptimistic:yo,useMemoCache:yo,useCacheRefresh:yo};Ls.useEffectEvent=yo;var Rs={readContext:ia,use:Mo,useCallback:function(e,t){return Oo().memoizedState=[e,t===void 0?null:t],e},useContext:ia,useEffect:cs,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),os(4194308,4,ms.bind(null,t,e),n)},useLayoutEffect:function(e,t){return os(4194308,4,e,t)},useInsertionEffect:function(e,t){os(4,2,e,t)},useMemo:function(e,t){var n=Oo();t=t===void 0?null:t;var r=e();if(mo){We(!0);try{e()}finally{We(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=Oo();if(n!==void 0){var i=n(t);if(mo){We(!0);try{n(t)}finally{We(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=As.bind(null,U,e),[r.memoizedState,e]},useRef:function(e){var t=Oo();return e={current:e},t.memoizedState=e},useState:function(e){e=Wo(e);var t=e.queue,n=js.bind(null,U,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:gs,useDeferredValue:function(e,t){return ys(Oo(),e,t)},useTransition:function(){var e=Wo(!1);return e=xs.bind(null,U,e.queue,!0,!1),Oo().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=U,a=Oo();if(I){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),q===null)throw Error(i(349));Y&127||zo(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,cs(Vo.bind(null,r,o,e),[e]),r.flags|=2048,is(9,{destroy:void 0},Bo.bind(null,r,o,n,t),null),n},useId:function(){var e=Oo(),t=q.identifierPrefix;if(I){var n=ji,r=Ai;n=(r&~(1<<32-Ge(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=ho++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[pt]=t,o[mt]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Pd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Mc(t)}}return G(t),Nc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Mc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=_e.current,Wi(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Li,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[pt]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Md(e.nodeValue,n)),e||Vi(t,!0)}else e=Bd(e).createTextNode(r),e[pt]=t,t.stateNode=e}return G(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Wi(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[pt]=t}else Gi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;G(t),e=!1}else n=Ki(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(oo(t),t):(oo(t),null);if(t.flags&128)throw Error(i(558))}return G(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Wi(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[pt]=t}else Gi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;G(t),a=!1}else a=Ki(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(oo(t),t):(oo(t),null)}return oo(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Fc(t,t.updateQueue),G(t),null);case 4:return be(),e===null&&Sd(t.stateNode.containerInfo),G(t),null;case 10:return Qi(t.type),G(t),null;case 19:if(me(so),r=t.memoizedState,r===null)return G(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)Ic(r,!1);else{if(Wl!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=co(e),o!==null){for(t.flags|=128,Ic(r,!1),e=o.updateQueue,t.updateQueue=e,Fc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)gi(n,e),n=n.sibling;return O(so,so.current&1|2),I&&Mi(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&Ne()>tu&&(t.flags|=128,a=!0,Ic(r,!1),t.lanes=4194304)}else{if(!a)if(e=co(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Fc(t,e),Ic(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!I)return G(t),null}else 2*Ne()-r.renderingStartTime>tu&&n!==536870912&&(t.flags|=128,a=!0,Ic(r,!1),t.lanes=4194304);r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(G(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=Ne(),e.sibling=null,n=so.current,O(so,a?n&1|2:n&1),I&&Mi(t,r.treeForkCount),e);case 22:case 23:return oo(t),$a(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(G(t),t.subtreeFlags&6&&(t.flags|=8192)):G(t),n=t.updateQueue,n!==null&&Fc(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&me(ga),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Qi(ua),G(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Rc(e,t){switch(Fi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Qi(ua),be(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return xe(t),null;case 31:if(t.memoizedState!==null){if(oo(t),t.alternate===null)throw Error(i(340));Gi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(oo(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Gi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return me(so),null;case 4:return be(),null;case 10:return Qi(t.type),null;case 22:case 23:return oo(t),$a(),e!==null&&me(ga),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Qi(ua),null;case 25:return null;default:return null}}function zc(e,t){switch(Fi(t),t.tag){case 3:Qi(ua),be();break;case 26:case 27:case 5:xe(t);break;case 4:be();break;case 31:t.memoizedState!==null&&oo(t);break;case 13:oo(t);break;case 19:me(so);break;case 10:Qi(t.type);break;case 22:case 23:oo(t),$a(),e!==null&&me(ga);break;case 24:Qi(ua)}}function Bc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Z(t,t.return,e)}}function Vc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Z(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Z(t,t.return,e)}}function Hc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{Ja(t,n)}catch(t){Z(e,e.return,t)}}}function Uc(e,t,n){n.props=Gs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Z(e,t,n)}}function Wc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Z(e,t,n)}}function Gc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Z(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Z(e,t,n)}else n.current=null}function Kc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Z(e,e.return,t)}}function qc(e,t,n){try{var r=e.stateNode;Fd(r,e.type,n,t),r[mt]=t}catch(t){Z(e,e.return,t)}}function Jc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Zd(e.type)||e.tag===4}function Yc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Jc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Zd(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Xc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=sn));else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Xc(e,t,n),e=e.sibling;e!==null;)Xc(e,t,n),e=e.sibling}function Zc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode),e=e.child,e!==null))for(Zc(e,t,n),e=e.sibling;e!==null;)Zc(e,t,n),e=e.sibling}function Qc(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Pd(t,r,n),t[pt]=e,t[mt]=n}catch(t){Z(e,e.return,t)}}var $c=!1,el=!1,tl=!1,nl=typeof WeakSet==`function`?WeakSet:Set,rl=null;function il(e,t){if(e=e.containerInfo,Rd=sp,e=Pr(e),Fr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(zd={focusedElem:e,selectionRange:n},sp=!1,rl=t;rl!==null;)if(t=rl,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,rl=e;else for(;rl!==null;){switch(t=rl,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Pd(o,r,n),o[pt]=e,Et(o),r=o;break a;case`link`:var s=Vf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=Mr(s,h),v=Mr(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,E.T=null,n=lu,lu=null;var o=au,s=su;if(iu=0,ou=au=null,su=0,K&6)throw Error(i(331));var c=K;if(K|=4,Pl(o.current),El(o,o.current,s,n),K=c,id(0,!1),Ue&&typeof Ue.onPostCommitFiberRoot==`function`)try{Ue.onPostCommitFiberRoot(He,o)}catch{}return!0}finally{D.p=a,E.T=r,Vu(e,t)}}function Wu(e,t,n){t=Si(n,t),t=Zs(e.stateNode,t,2),e=Va(e,t,2),e!==null&&(it(e,2),rd(e))}function Z(e,t,n){if(e.tag===3)Wu(e,e,n);else for(;t!==null;){if(t.tag===3){Wu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(ru===null||!ru.has(r))){e=Si(n,e),n=Qs(2),r=Va(t,n,2),r!==null&&($s(n,r,t,e),it(r,2),rd(r));break}}t=t.return}}function Gu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Rl;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Hl=!0,i.add(n),e=Ku.bind(null,e,t,n),t.then(e,e))}function Ku(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,q===e&&(Y&n)===n&&(Wl===4||Wl===3&&(Y&62914560)===Y&&300>Ne()-$l?!(K&2)&&Su(e,0):ql|=n,Yl===Y&&(Yl=0)),rd(e)}function qu(e,t){t===0&&(t=nt()),e=li(e,t),e!==null&&(it(e,t),rd(e))}function Ju(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),qu(e,n)}function Yu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),qu(e,n)}function Xu(e,t){return ke(e,t)}var Zu=null,Qu=null,$u=!1,ed=!1,td=!1,nd=0;function rd(e){e!==Qu&&e.next===null&&(Qu===null?Zu=Qu=e:Qu=Qu.next=e),ed=!0,$u||($u=!0,ud())}function id(e,t){if(!td&&ed){td=!0;do for(var n=!1,r=Zu;r!==null;){if(!t)if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-Ge(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,ld(r,a))}else a=Y,a=$e(r,r===q?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||et(r,a)||(n=!0,ld(r,a));r=r.next}while(n);td=!1}}function ad(){od()}function od(){ed=$u=!1;var e=0;nd!==0&&Gd()&&(e=nd);for(var t=Ne(),n=null,r=Zu;r!==null;){var i=r.next,a=sd(r,t);a===0?(r.next=null,n===null?Zu=i:n.next=i,i===null&&(Qu=n)):(n=r,(e!==0||a&3)&&(ed=!0)),r=i}iu!==0&&iu!==5||id(e,!1),nd!==0&&(nd=0)}function sd(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Id(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function xf(e,t,n){var r=bf;if(r&&typeof t==`string`&&t){var i=Gt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),hf.has(i)||(hf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Pd(t,`link`,e),Et(t),r.head.appendChild(t)))}}function Sf(e){_f.D(e),xf(`dns-prefetch`,e,null)}function Cf(e,t){_f.C(e,t),xf(`preconnect`,e,t)}function wf(e,t,n){_f.L(e,t,n);var r=bf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Gt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Gt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Gt(n.imageSizes)+`"]`)):i+=`[href="`+Gt(e)+`"]`;var a=i;switch(t){case`style`:a=Af(e);break;case`script`:a=Pf(e)}mf.has(a)||(e=h({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),mf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(jf(a))||t===`script`&&r.querySelector(Ff(a))||(t=r.createElement(`link`),Pd(t,`link`,e),Et(t),r.head.appendChild(t)))}}function Tf(e,t){_f.m(e,t);var n=bf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Gt(r)+`"][href="`+Gt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Pf(e)}if(!mf.has(a)&&(e=h({rel:`modulepreload`,href:e},t),mf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Ff(a)))return}r=n.createElement(`link`),Pd(r,`link`,e),Et(r),n.head.appendChild(r)}}}function Ef(e,t,n){_f.S(e,t,n);var r=bf;if(r&&e){var i=Tt(r).hoistableStyles,a=Af(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(jf(a)))s.loading=5;else{e=h({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=mf.get(a))&&Rf(e,n);var c=o=r.createElement(`link`);Et(c),Pd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Lf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Df(e,t){_f.X(e,t);var n=bf;if(n&&e){var r=Tt(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=h({src:e,async:!0},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),Et(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Of(e,t){_f.M(e,t);var n=bf;if(n&&e){var r=Tt(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=h({src:e,async:!0,type:`module`},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),Et(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function kf(e,t,n,r){var a=(a=_e.current)?gf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Af(n.href),n=Tt(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Af(n.href);var o=Tt(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(jf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),mf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},mf.set(e,n),o||Nf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Pf(n),n=Tt(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Af(e){return`href="`+Gt(e)+`"`}function jf(e){return`link[rel="stylesheet"][`+e+`]`}function Mf(e){return h({},e,{"data-precedence":e.precedence,precedence:null})}function Nf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Pd(t,`link`,n),Et(t),e.head.appendChild(t))}function Pf(e){return`[src="`+Gt(e)+`"]`}function Ff(e){return`script[async]`+e}function If(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Gt(n.href)+`"]`);if(r)return t.instance=r,Et(r),r;var a=h({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),Et(r),Pd(r,`style`,a),Lf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Af(n.href);var o=e.querySelector(jf(a));if(o)return t.state.loading|=4,t.instance=o,Et(o),o;r=Mf(n),(a=mf.get(a))&&Rf(r,a),o=(e.ownerDocument||e).createElement(`link`),Et(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Pd(o,`link`,r),t.state.loading|=4,Lf(o,n.precedence,e),t.instance=o;case`script`:return o=Pf(n.src),(a=e.querySelector(Ff(o)))?(t.instance=a,Et(a),a):(r=n,(a=mf.get(o))&&(r=h({},n),zf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),Et(a),Pd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Lf(r,n.precedence,e));return t.instance}function Lf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Uf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Wf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Gf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Af(r.href),a=t.querySelector(jf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Jf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,Et(a);return}a=t.ownerDocument||t,r=Mf(r),(i=mf.get(i))&&Rf(r,i),a=a.createElement(`link`),Et(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Pd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Jf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Kf=0;function qf(e,t){return e.stylesheets&&e.count===0&&Xf(e,e.stylesheets),0Kf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Jf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Xf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Yf=null;function Xf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Yf=new Map,t.forEach(Zf,e),Yf=null,Jf.call(e))}function Zf(e,t){if(!(t.state.loading&4)){var n=Yf.get(e);if(n)var r=n.get(null);else{n=new Map,Yf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=h()})),_=`modulepreload`,v=function(e){return`/`+e},y={},b=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=v(t,n),t in y)return;y[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:_,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},x=c(u(),1),ee=`popstate`;function S(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function C(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return re(``,{pathname:r,search:i,hash:a},t.state&&t.state.usr||null,t.state&&t.state.key||`default`,n?{pathname:e.location.pathname,search:e.location.search,hash:e.location.hash}:void 0)}function n(e,t){return typeof t==`string`?t:ie(t)}return oe(t,n,null,e)}function w(e,t){if(e===!1||e==null)throw Error(t)}function T(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function te(){return Math.random().toString(36).substring(2,10)}function ne(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.unstable_mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function re(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?ae(t):t,state:n,key:t&&t.key||r||te(),unstable_mask:i}}function ie({pathname:e=`/`,search:t=``,hash:n=``}){return t&&t!==`?`&&(e+=t.charAt(0)===`?`?t:`?`+t),n&&n!==`#`&&(e+=n.charAt(0)===`#`?n:`#`+n),e}function ae(e){let t={};if(e){let n=e.indexOf(`#`);n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf(`?`);r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function oe(e,t,n,r={}){let{window:i=document.defaultView,v5Compat:a=!1}=r,o=i.history,s=`POP`,c=null,l=u();l??(l=0,o.replaceState({...o.state,idx:l},``));function u(){return(o.state||{idx:null}).idx}function d(){s=`POP`;let e=u(),t=e==null?null:e-l;l=e,c&&c({action:s,location:h.location,delta:t})}function f(e,t){s=`PUSH`;let r=S(e)?e:re(h.location,e,t);n&&n(r,e),l=u()+1;let d=ne(r,l),f=h.createHref(r.unstable_mask||r);try{o.pushState(d,``,f)}catch(e){if(e instanceof DOMException&&e.name===`DataCloneError`)throw e;i.location.assign(f)}a&&c&&c({action:s,location:h.location,delta:1})}function p(e,t){s=`REPLACE`;let r=S(e)?e:re(h.location,e,t);n&&n(r,e),l=u();let i=ne(r,l),d=h.createHref(r.unstable_mask||r);o.replaceState(i,``,d),a&&c&&c({action:s,location:h.location,delta:0})}function m(e){return se(e)}let h={get action(){return s},get location(){return e(i,o)},listen(e){if(c)throw Error(`A history only accepts one active listener`);return i.addEventListener(ee,d),c=e,()=>{i.removeEventListener(ee,d),c=null}},createHref(e){return t(i,e)},createURL:m,encodeLocation(e){let t=m(e);return{pathname:t.pathname,search:t.search,hash:t.hash}},push:f,replace:p,go(e){return o.go(e)}};return h}function se(e,t=!1){let n=`http://localhost`;typeof window<`u`&&(n=window.location.origin===`null`?window.location.href:window.location.origin),w(n,`No window.location.(origin|href) available to create URL`);let r=typeof e==`string`?e:ie(e);return r=r.replace(/ $/,`%20`),!t&&r.startsWith(`//`)&&(r=n+r),new URL(r,n)}function ce(e,t,n=`/`){return le(e,t,n,!1)}function le(e,t,n,r){let i=Ce((typeof t==`string`?ae(t):t).pathname||`/`,n);if(i==null)return null;let a=D(e);de(a);let o=null;for(let e=0;o==null&&e{let c={relativePath:s===void 0?e.path||``:s,caseSensitive:e.caseSensitive===!0,childrenIndex:a,route:e};if(c.relativePath.startsWith(`/`)){if(!c.relativePath.startsWith(r)&&o)return;w(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length)}let l=je([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(w(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),D(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:ve(l,e.index),routesMeta:u})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of ue(e.path))a(e,t,!0,n)}),t}function ue(e){let t=e.split(`/`);if(t.length===0)return[];let[n,...r]=t,i=n.endsWith(`?`),a=n.replace(/\?$/,``);if(r.length===0)return i?[a,``]:[a];let o=ue(r.join(`/`)),s=[];return s.push(...o.map(e=>e===``?a:[a,e].join(`/`))),i&&s.push(...o),s.map(t=>e.startsWith(`/`)&&t===``?`/`:t)}function de(e){e.sort((e,t)=>e.score===t.score?ye(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var fe=/^:[\w-]+$/,pe=3,me=2,O=1,he=10,ge=-2,_e=e=>e===`*`;function ve(e,t){let n=e.split(`/`),r=n.length;return n.some(_e)&&(r+=ge),t&&(r+=me),n.filter(e=>!_e(e)).reduce((e,t)=>e+(fe.test(t)?pe:t===``?O:he),r)}function ye(e,t){return e.length===t.length&&e.slice(0,-1).every((e,n)=>e===t[n])?e[e.length-1]-t[t.length-1]:0}function be(e,t,n=!1){let{routesMeta:r}=e,i={},a=`/`,o=[];for(let e=0;e{if(t===`*`){let e=s[r]||``;o=a.slice(0,a.length-e.length).replace(/(.)\/+$/,`$1`)}let i=s[r];return n&&!i?e[t]=void 0:e[t]=(i||``).replace(/%2F/g,`/`),e},{}),pathname:a,pathnameBase:o,pattern:e}}function xe(e,t=!1,n=!0){T(e===`*`||!e.endsWith(`*`)||e.endsWith(`/*`),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,`/*`)}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,`/*`)}".`);let r=[],i=`^`+e.replace(/\/*\*?$/,``).replace(/^\/*/,`/`).replace(/[\\.*+^${}|()[\]]/g,`\\$&`).replace(/\/:([\w-]+)(\?)?/g,(e,t,n,i,a)=>{if(r.push({paramName:t,isOptional:n!=null}),n){let t=a.charAt(i+e.length);return t&&t!==`/`?`/([^\\/]*)`:`(?:/([^\\/]*))?`}return`/([^\\/]+)`}).replace(/\/([\w-]+)\?(\/|$)/g,`(/$1)?$2`);return e.endsWith(`*`)?(r.push({paramName:`*`}),i+=e===`*`||e===`/*`?`(.*)$`:`(?:\\/(.+)|\\/*)$`):n?i+=`\\/*$`:e!==``&&e!==`/`&&(i+=`(?:(?=\\/|$))`),[new RegExp(i,t?void 0:`i`),r]}function Se(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return T(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function Ce(e,t){if(t===`/`)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith(`/`)?t.length-1:t.length,r=e.charAt(n);return r&&r!==`/`?null:e.slice(n)||`/`}var A=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;function we(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?ae(e):e,a;return n?(n=Ae(n),a=n.startsWith(`/`)?Te(n.substring(1),`/`):Te(n,t)):a=t,{pathname:a,search:Pe(r),hash:Fe(i)}}function Te(e,t){let n=Me(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function Ee(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function De(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function Oe(e){let t=De(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function ke(e,t,n,r=!1){let i;typeof e==`string`?i=ae(e):(i={...e},w(!i.pathname||!i.pathname.includes(`?`),Ee(`?`,`pathname`,`search`,i)),w(!i.pathname||!i.pathname.includes(`#`),Ee(`#`,`pathname`,`hash`,i)),w(!i.search||!i.search.includes(`#`),Ee(`#`,`search`,`hash`,i)));let a=e===``||i.pathname===``,o=a?`/`:i.pathname,s;if(o==null)s=n;else{let e=t.length-1;if(!r&&o.startsWith(`..`)){let t=o.split(`/`);for(;t[0]===`..`;)t.shift(),--e;i.pathname=t.join(`/`)}s=e>=0?t[e]:`/`}let c=we(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var Ae=e=>e.replace(/\/\/+/g,`/`),je=e=>Ae(e.join(`/`)),Me=e=>e.replace(/\/+$/,``),Ne=e=>Me(e).replace(/^\/*/,`/`),Pe=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,Fe=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,Ie=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||``,this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function Le(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function Re(e){return je(e.map(e=>e.route.path).filter(Boolean))||`/`}var ze=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function Be(e,t){let n=e;if(typeof n!=`string`||!A.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(ze)try{let e=new URL(window.location.href),r=n.startsWith(`//`)?new URL(e.protocol+n):new URL(n),a=Ce(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{T(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:i,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join(`\0`);var Ve=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(Ve);var He=[`GET`,...Ve];new Set(He);var Ue=x.createContext(null);Ue.displayName=`DataRouter`;var We=x.createContext(null);We.displayName=`DataRouterState`;var Ge=x.createContext(!1);function Ke(){return x.useContext(Ge)}var qe=x.createContext({isTransitioning:!1});qe.displayName=`ViewTransition`;var Je=x.createContext(new Map);Je.displayName=`Fetchers`;var Ye=x.createContext(null);Ye.displayName=`Await`;var Xe=x.createContext(null);Xe.displayName=`Navigation`;var Ze=x.createContext(null);Ze.displayName=`Location`;var Qe=x.createContext({outlet:null,matches:[],isDataRoute:!1});Qe.displayName=`Route`;var $e=x.createContext(null);$e.displayName=`RouteError`;var et=`REACT_ROUTER_ERROR`,tt=`REDIRECT`,nt=`ROUTE_ERROR_RESPONSE`;function rt(e){if(e.startsWith(`${et}:${tt}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`&&typeof t.location==`string`&&typeof t.reloadDocument==`boolean`&&typeof t.replace==`boolean`)return t}catch{}}function it(e){if(e.startsWith(`${et}:${nt}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new Ie(t.status,t.statusText,t.data)}catch{}}function at(e,{relative:t}={}){w(ot(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=x.useContext(Xe),{hash:i,pathname:a,search:o}=ft(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:je([n,a])),r.createHref({pathname:s,search:o,hash:i})}function ot(){return x.useContext(Ze)!=null}function j(){return w(ot(),`useLocation() may be used only in the context of a component.`),x.useContext(Ze).location}var st=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function ct(e){x.useContext(Xe).static||x.useLayoutEffect(e)}function lt(){let{isDataRoute:e}=x.useContext(Qe);return e?jt():ut()}function ut(){w(ot(),`useNavigate() may be used only in the context of a component.`);let e=x.useContext(Ue),{basename:t,navigator:n}=x.useContext(Xe),{matches:r}=x.useContext(Qe),{pathname:i}=j(),a=JSON.stringify(Oe(r)),o=x.useRef(!1);return ct(()=>{o.current=!0}),x.useCallback((r,s={})=>{if(T(o.current,st),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=ke(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:je([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}x.createContext(null);function dt(){let{matches:e}=x.useContext(Qe);return e[e.length-1]?.params??{}}function ft(e,{relative:t}={}){let{matches:n}=x.useContext(Qe),{pathname:r}=j(),i=JSON.stringify(Oe(n));return x.useMemo(()=>ke(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function pt(e,t){return mt(e,t)}function mt(e,t,n){w(ot(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=x.useContext(Xe),{matches:i}=x.useContext(Qe),a=i[i.length-1],o=a?a.params:{},s=a?a.pathname:`/`,c=a?a.pathnameBase:`/`,l=a&&a.route;{let e=l&&l.path||``;Nt(s,!l||e.endsWith(`*`)||e.endsWith(`*?`),`You rendered descendant (or called \`useRoutes()\`) at "${s}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. + +Please change the parent to .`)}let u=j(),d;if(t){let e=typeof t==`string`?ae(t):t;w(c===`/`||e.pathname?.startsWith(c),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${c}" but pathname "${e.pathname}" was given in the \`location\` prop.`),d=e}else d=u;let f=d.pathname||`/`,p=f;if(c!==`/`){let e=c.replace(/^\//,``).split(`/`);p=`/`+f.replace(/^\//,``).split(`/`).slice(e.length).join(`/`)}let m=ce(e,{pathname:p});T(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),T(m==null||m[m.length-1].route.element!==void 0||m[m.length-1].route.Component!==void 0||m[m.length-1].route.lazy!==void 0,`Matched leaf route at location "${d.pathname}${d.search}${d.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let h=xt(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:je([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:je([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?x.createElement(Ze.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,unstable_mask:void 0,...d},navigationType:`POP`}},h):h}function ht(){let e=At(),t=Le(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r=`rgba(200,200,200, 0.5)`,i={padding:`0.5rem`,backgroundColor:r},a={padding:`2px 4px`,backgroundColor:r},o=null;return console.error(`Error handled by React Router default ErrorBoundary:`,e),o=x.createElement(x.Fragment,null,x.createElement(`p`,null,`💿 Hey developer 👋`),x.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,x.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,x.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),x.createElement(x.Fragment,null,x.createElement(`h2`,null,`Unexpected Application Error!`),x.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?x.createElement(`pre`,{style:i},n):null,o)}var gt=x.createElement(ht,null),_t=class extends x.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!==`idle`&&e.revalidation===`idle`?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error===void 0?t.error:e.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error(`React Router caught the following error during render`,e)}render(){let e=this.state.error;if(this.context&&typeof e==`object`&&e&&`digest`in e&&typeof e.digest==`string`){let t=it(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:x.createElement(Qe.Provider,{value:this.props.routeContext},x.createElement($e.Provider,{value:e,children:this.props.component}));return this.context?x.createElement(yt,{error:e},t):t}};_t.contextType=Ge;var vt=new WeakMap;function yt({children:e,error:t}){let{basename:n}=x.useContext(Xe);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=rt(t.digest);if(e){let r=vt.get(t);if(r)throw r;let i=Be(e.location,n);if(ze&&!vt.get(t))if(i.isExternal||e.reloadDocument)window.location.href=i.absoluteURL||i.to;else{let n=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:e.replace}));throw vt.set(t,n),n}return x.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${i.absoluteURL||i.to}`})}}return e}function bt({routeContext:e,match:t,children:n}){let r=x.useContext(Ue);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),x.createElement(Qe.Provider,{value:e},n)}function xt(e,t=[],n){let r=n?.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,a=r?.errors;if(a!=null){let e=i.findIndex(e=>e.route.id&&a?.[e.route.id]!==void 0);w(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(`,`)}`),i=i.slice(0,Math.min(i.length,e+1))}let o=!1,s=-1;if(n&&r){o=r.renderFallback;for(let e=0;e=0?i.slice(0,s+1):[i[0]];break}}}}let c=n?.onError,l=r&&c?(e,t)=>{c(e,{location:r.location,params:r.matches?.[0]?.params??{},unstable_pattern:Re(r.matches),errorInfo:t})}:void 0;return i.reduceRight((e,n,c)=>{let u,d=!1,f=null,p=null;r&&(u=a&&n.route.id?a[n.route.id]:void 0,f=n.route.errorElement||gt,o&&(s<0&&c===0?(Nt(`route-fallback`,!1,"No `HydrateFallback` element provided to render during initial hydration"),d=!0,p=null):s===c&&(d=!0,p=n.route.hydrateFallbackElement||null)));let m=t.concat(i.slice(0,c+1)),h=()=>{let t;return t=u?f:d?p:n.route.Component?x.createElement(n.route.Component,null):n.route.element?n.route.element:e,x.createElement(bt,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?x.createElement(_t,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function St(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Ct(e){let t=x.useContext(Ue);return w(t,St(e)),t}function wt(e){let t=x.useContext(We);return w(t,St(e)),t}function Tt(e){let t=x.useContext(Qe);return w(t,St(e)),t}function Et(e){let t=Tt(e),n=t.matches[t.matches.length-1];return w(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function Dt(){return Et(`useRouteId`)}function Ot(){return wt(`useNavigation`).navigation}function kt(){let{matches:e,loaderData:t}=wt(`useMatches`);return x.useMemo(()=>e.map(e=>E(e,t)),[e,t])}function At(){let e=x.useContext($e),t=wt(`useRouteError`),n=Et(`useRouteError`);return e===void 0?t.errors?.[n]:e}function jt(){let{router:e}=Ct(`useNavigate`),t=Et(`useNavigate`),n=x.useRef(!1);return ct(()=>{n.current=!0}),x.useCallback(async(r,i={})=>{T(n.current,st),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var Mt={};function Nt(e,t,n){!t&&!Mt[e]&&(Mt[e]=!0,T(!1,n))}x.memo(Pt);function Pt({routes:e,future:t,state:n,isStatic:r,onError:i}){return mt(e,void 0,{state:n,isStatic:r,onError:i,future:t})}function Ft({to:e,replace:t,state:n,relative:r}){w(ot(),` may be used only in the context of a component.`);let{static:i}=x.useContext(Xe);T(!i,` must not be used on the initial render in a . This is a no-op, but you should modify your code so the is only ever rendered in response to some user interaction or state change.`);let{matches:a}=x.useContext(Qe),{pathname:o}=j(),s=lt(),c=ke(e,Oe(a),o,r===`path`),l=JSON.stringify(c);return x.useEffect(()=>{s(JSON.parse(l),{replace:t,state:n,relative:r})},[s,l,r,t,n]),null}function It(e){w(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function Lt({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,unstable_useTransitions:o}){w(!ot(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=x.useMemo(()=>({basename:s,navigator:i,static:a,unstable_useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=ae(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,unstable_mask:m}=n,h=x.useMemo(()=>{let e=Ce(l,s);return e==null?null:{location:{pathname:e,search:u,hash:d,state:f,key:p,unstable_mask:m},navigationType:r}},[s,l,u,d,f,p,r,m]);return T(h!=null,` is not able to match the URL "${l}${u}${d}" because it does not start with the basename, so the won't render anything.`),h==null?null:x.createElement(Xe.Provider,{value:c},x.createElement(Ze.Provider,{children:t,value:h}))}function Rt({children:e,location:t}){return pt(zt(e),t)}x.Component;function zt(e,t=[]){let n=[];return x.Children.forEach(e,(e,r)=>{if(!x.isValidElement(e))return;let i=[...t,r];if(e.type===x.Fragment){n.push.apply(n,zt(e.props.children,i));return}w(e.type===It,`[${typeof e.type==`string`?e.type:e.type.name}] is not a component. All component children of must be a or `),w(!e.props.index||!e.props.children,`An index route cannot have child routes.`);let a={id:e.props.id||i.join(`-`),caseSensitive:e.props.caseSensitive,element:e.props.element,Component:e.props.Component,index:e.props.index,path:e.props.path,middleware:e.props.middleware,loader:e.props.loader,action:e.props.action,hydrateFallbackElement:e.props.hydrateFallbackElement,HydrateFallback:e.props.HydrateFallback,errorElement:e.props.errorElement,ErrorBoundary:e.props.ErrorBoundary,hasErrorBoundary:e.props.hasErrorBoundary===!0||e.props.ErrorBoundary!=null||e.props.errorElement!=null,shouldRevalidate:e.props.shouldRevalidate,handle:e.props.handle,lazy:e.props.lazy};e.props.children&&(a.children=zt(e.props.children,i)),n.push(a)}),n}var Bt=`get`,Vt=`application/x-www-form-urlencoded`;function Ht(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function Ut(e){return Ht(e)&&e.tagName.toLowerCase()===`button`}function Wt(e){return Ht(e)&&e.tagName.toLowerCase()===`form`}function Gt(e){return Ht(e)&&e.tagName.toLowerCase()===`input`}function Kt(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function qt(e,t){return e.button===0&&(!t||t===`_self`)&&!Kt(e)}var Jt=null;function Yt(){if(Jt===null)try{new FormData(document.createElement(`form`),0),Jt=!1}catch{Jt=!0}return Jt}var Xt=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function Zt(e){return e!=null&&!Xt.has(e)?(T(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${Vt}"`),null):e}function Qt(e,t){let n,r,i,a,o;if(Wt(e)){let o=e.getAttribute(`action`);r=o?Ce(o,t):null,n=e.getAttribute(`method`)||Bt,i=Zt(e.getAttribute(`enctype`))||Vt,a=new FormData(e)}else if(Ut(e)||Gt(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a +
    +
    + +

    +
    + + +
    + + +
    + +
    + + +
    +
    +
    + + + +
    +
    +
    +
    +
    + +`.replace(/(^|\n)\s*/g,``),Ve=()=>{let e=S();return e?(e.remove(),we([document.documentElement,document.body],[l[`no-backdrop`],l[`toast-shown`],l[`has-column`]]),!0):!1},He=()=>{a.currentInstance&&a.currentInstance.resetValidationMessage()},Ue=()=>{let e=T();if(!e)return;let t=Te(e,l.input),n=Te(e,l.file),r=e.querySelector(`.${l.range} input`),i=e.querySelector(`.${l.range} output`),a=Te(e,l.select),o=e.querySelector(`.${l.checkbox} input`),s=Te(e,l.textarea);t&&(t.oninput=He),n&&(n.onchange=He),a&&(a.onchange=He),o&&(o.onchange=He),s&&(s.oninput=He),r&&i&&(r.oninput=()=>{He(),i.value=r.value},r.onchange=()=>{He(),i.value=r.value})},We=e=>{if(typeof e==`string`){let t=document.querySelector(e);if(!t)throw Error(`Target element "${e}" not found`);return t}return e},Ge=e=>{let t=T();t&&(t.setAttribute(`role`,e.toast?`alert`:`dialog`),t.setAttribute(`aria-live`,e.toast?`polite`:`assertive`),e.toast||t.setAttribute(`aria-modal`,`true`))},Ke=e=>{window.getComputedStyle(e).direction===`rtl`&&(A(S(),l.rtl),a.isRTL=!0)},qe=e=>{let t=Ve();if(ze()){m(`SweetAlert2 requires document to initialize`);return}let n=document.createElement(`div`);n.className=l.container,t&&A(n,l[`no-transition`]),ve(n,Be),n.dataset.swal2Theme=e.theme;let r=We(e.target||`body`);r.appendChild(n),e.topLayer&&(n.setAttribute(`popover`,``),n.showPopover()),Ge(e),Ke(r),Ue()},Je=(e,t)=>{e instanceof HTMLElement?t.appendChild(e):typeof e==`object`?Ye(e,t):e&&ve(t,e)},Ye=(e,t)=>{`jquery`in e?Xe(t,e):ve(t,e.toString())},Xe=(e,t)=>{if(e.textContent=``,0 in t)for(let n=0;n in t;n++)e.appendChild(t[n].cloneNode(!0));else e.appendChild(t.cloneNode(!0))},Ze=(e,t)=>{let n=de(),r=ue();!n||!r||(!t.showConfirmButton&&!t.showDenyButton&&!t.showCancelButton?Oe(n):De(n),k(n,t,`actions`),Qe(n,r,t),ve(r,t.loaderHtml||``),k(r,t,`loader`))};function Qe(e,t,n){let r=ce(),i=E(),a=le();!r||!i||!a||(tt(r,`confirm`,n),tt(i,`deny`,n),tt(a,`cancel`,n),$e(r,i,a,n),n.reverseButtons&&(n.toast?(e.insertBefore(a,r),e.insertBefore(i,r)):(e.insertBefore(a,t),e.insertBefore(i,t),e.insertBefore(r,t))))}function $e(e,t,n,r){if(!r.buttonsStyling){we([e,t,n],l.styled);return}A([e,t,n],l.styled),[[e,`confirm`,r.confirmButtonColor],[t,`deny`,r.denyButtonColor],[n,`cancel`,r.cancelButtonColor]].forEach(([e,t,n])=>{n&&e.style.setProperty(`--swal2-${t}-button-background-color`,n),et(e)})}function et(e){let t=window.getComputedStyle(e);if(t.getPropertyValue(`--swal2-action-button-focus-box-shadow`))return;let n=t.backgroundColor.replace(/rgba?\((\d+), (\d+), (\d+).*/,`rgba($1, $2, $3, 0.5)`);e.style.setProperty(`--swal2-action-button-focus-box-shadow`,t.getPropertyValue(`--swal2-outline`).replace(/ rgba\(.*/,` ${n}`))}function tt(e,t,n){je(e,n[`show${f(t)}Button`],`inline-block`),ve(e,n[`${t}ButtonText`]||``),e.setAttribute(`aria-label`,n[`${t}ButtonAriaLabel`]||``),e.className=l[t],k(e,n,`${t}Button`)}let nt=(e,t)=>{let n=me();n&&(ve(n,t.closeButtonHtml||``),k(n,t,`closeButton`),je(n,t.showCloseButton),n.setAttribute(`aria-label`,t.closeButtonAriaLabel||``))},rt=(e,t)=>{let n=S();n&&(it(n,t.backdrop),at(n,t.position),ot(n,t.grow),k(n,t,`container`))};function it(e,t){typeof t==`string`?e.style.background=t:t||A([document.documentElement,document.body],l[`no-backdrop`])}function at(e,t){t&&(t in l?A(e,l[t]):(p(`The "position" parameter is not valid, defaulting to "center"`),A(e,l.center)))}function ot(e,t){t&&A(e,l[`grow-${t}`])}var j={innerParams:new WeakMap,domCache:new WeakMap,focusedElement:new WeakMap};let st=[`input`,`file`,`range`,`select`,`radio`,`checkbox`,`textarea`],ct=(e,t)=>{let n=T();if(!n)return;let r=j.innerParams.get(e),i=!r||t.input!==r.input;st.forEach(e=>{let r=Te(n,l[e]);r&&(dt(e,t.inputAttributes),r.className=l[e],i&&Oe(r))}),t.input&&(i&<(t),ft(t))},lt=e=>{if(!e.input)return;if(!_t[e.input]){m(`Unexpected type of input! Expected ${Object.keys(_t).join(` | `)}, got "${e.input}"`);return}let t=ht(e.input);if(!t)return;let n=_t[e.input](t,e);De(t),e.inputAutoFocus&&setTimeout(()=>{Se(n)})},ut=e=>{for(let t=0;t{let n=T();if(!n)return;let r=xe(n,e);if(r){ut(r);for(let e in t)r.setAttribute(e,t[e])}},ft=e=>{if(!e.input)return;let t=ht(e.input);t&&k(t,e,`input`)},pt=(e,t)=>{!e.placeholder&&t.inputPlaceholder&&(e.placeholder=t.inputPlaceholder)},mt=(e,t,n)=>{if(n.inputLabel){let r=document.createElement(`label`),i=l[`input-label`];r.setAttribute(`for`,e.id),r.className=i,typeof n.customClass==`object`&&A(r,n.customClass.inputLabel),r.innerText=n.inputLabel,t.insertAdjacentElement(`beforebegin`,r)}},ht=e=>{let t=T();if(t)return Te(t,l[e]||l.input)},gt=(e,t)=>{[`string`,`number`].includes(typeof t)?e.value=`${t}`:x(t)||p(`Unexpected type of inputValue! Expected "string", "number" or "Promise", got "${typeof t}"`)},_t={};_t.text=_t.email=_t.password=_t.number=_t.tel=_t.url=_t.search=_t.date=_t[`datetime-local`]=_t.time=_t.week=_t.month=(e,t)=>{let n=e;return gt(n,t.inputValue),mt(n,n,t),pt(n,t),n.type=t.input,n},_t.file=(e,t)=>{let n=e;return mt(n,n,t),pt(n,t),n},_t.range=(e,t)=>{let n=e,r=n.querySelector(`input`),i=n.querySelector(`output`);return r&&(gt(r,t.inputValue),r.type=t.input,mt(r,e,t)),i&>(i,t.inputValue),e},_t.select=(e,t)=>{let n=e;if(n.textContent=``,t.inputPlaceholder){let e=document.createElement(`option`);ve(e,t.inputPlaceholder),e.value=``,e.disabled=!0,e.selected=!0,n.appendChild(e)}return mt(n,n,t),n},_t.radio=e=>{let t=e;return t.textContent=``,e},_t.checkbox=(e,t)=>{let n=T();if(!n)throw Error(`Popup not found`);let r=xe(n,`checkbox`);if(!r)throw Error(`Checkbox input not found`);r.value=`1`,r.checked=!!t.inputValue;let i=e.querySelector(`span`);if(i){let e=t.inputPlaceholder||t.inputLabel;e&&ve(i,e)}return r},_t.textarea=(e,t)=>{let n=e;gt(n,t.inputValue),pt(n,t),mt(n,n,t);let r=e=>parseInt(window.getComputedStyle(e).marginLeft)+parseInt(window.getComputedStyle(e).marginRight);return setTimeout(()=>{if(`MutationObserver`in window){let e=T();if(!e)return;let i=parseInt(window.getComputedStyle(e).width);new MutationObserver(()=>{if(!document.body.contains(n))return;let e=n.offsetWidth+r(n),a=T();a&&(e>i?a.style.width=`${e}px`:Ee(a,`width`,t.width))}).observe(n,{attributes:!0,attributeFilter:[`style`]})}}),n};let vt=(e,t)=>{let n=ie();n&&(ke(n),k(n,t,`htmlContainer`),t.html?(Je(t.html,n),De(n,`block`)):t.text?(n.textContent=t.text,De(n,`block`)):Oe(n),ct(e,t))},yt=(e,t)=>{let n=fe();n&&(ke(n),je(n,!!t.footer,`block`),t.footer&&Je(t.footer,n),k(n,t,`footer`))},bt=(e,t)=>{let n=j.innerParams.get(e),r=te();if(r){if(n&&t.icon===n.icon){wt(r,t),xt(r,t);return}if(!t.icon&&!t.iconHtml){Oe(r);return}if(t.icon&&Object.keys(u).indexOf(t.icon)===-1){m(`Unknown icon! Expected "success", "error", "warning", "info" or "question", got "${t.icon}"`),Oe(r);return}De(r),wt(r,t),xt(r,t),A(r,t.showClass&&t.showClass.icon),window.matchMedia(`(prefers-color-scheme: dark)`).addEventListener(`change`,St)}},xt=(e,t)=>{for(let[n,r]of Object.entries(u))t.icon!==n&&we(e,r);A(e,t.icon&&u[t.icon]),Tt(e,t),St(),k(e,t,`icon`)},St=()=>{let e=T();if(!e)return;let t=window.getComputedStyle(e).getPropertyValue(`background-color`),n=e.querySelectorAll(`[class^=swal2-success-circular-line], .swal2-success-fix`);for(let e=0;e` + ${e.animation?`
    `:``} + +
    + ${e.animation?`
    `:``} + ${e.animation?`
    `:``} +`,wt=(e,t)=>{if(!t.icon&&!t.iconHtml)return;let n=e.innerHTML,r=``;t.iconHtml?r=Et(t.iconHtml):t.icon===`success`?(r=Ct(t),n=n.replace(/ style=".*?"/g,``)):t.icon===`error`?r=` + + + + +`:t.icon&&(r=Et({question:`?`,warning:`!`,info:`i`}[t.icon])),n.trim()!==r.trim()&&ve(e,r)},Tt=(e,t)=>{if(t.iconColor){e.style.color=t.iconColor,e.style.borderColor=t.iconColor;for(let n of[`.swal2-success-line-tip`,`.swal2-success-line-long`,`.swal2-x-mark-line-left`,`.swal2-x-mark-line-right`])Ae(e,n,`background-color`,t.iconColor);Ae(e,`.swal2-success-ring`,`border-color`,t.iconColor)}},Et=e=>`
    ${e}
    `,Dt=(e,t)=>{let n=ae();if(n){if(!t.imageUrl){Oe(n);return}De(n,``),n.setAttribute(`src`,t.imageUrl),n.setAttribute(`alt`,t.imageAlt||``),Ee(n,`width`,t.imageWidth),Ee(n,`height`,t.imageHeight),n.className=l.image,k(n,t,`image`)}},Ot=!1,kt=0,At=0,jt=0,Mt=0,Nt=e=>{e.addEventListener(`mousedown`,Ft),document.body.addEventListener(`mousemove`,It),e.addEventListener(`mouseup`,Lt),e.addEventListener(`touchstart`,Ft),document.body.addEventListener(`touchmove`,It),e.addEventListener(`touchend`,Lt)},Pt=e=>{e.removeEventListener(`mousedown`,Ft),document.body.removeEventListener(`mousemove`,It),e.removeEventListener(`mouseup`,Lt),e.removeEventListener(`touchstart`,Ft),document.body.removeEventListener(`touchmove`,It),e.removeEventListener(`touchend`,Lt)},Ft=e=>{let t=T();if(!t)return;let n=te();if(e.target===t||n&&n.contains(e.target)){Ot=!0;let n=Rt(e);kt=n.clientX,At=n.clientY,jt=parseInt(t.style.insetInlineStart)||0,Mt=parseInt(t.style.insetBlockStart)||0,A(t,`swal2-dragging`)}},It=e=>{let t=T();if(t&&Ot){let{clientX:n,clientY:r}=Rt(e),i=n-kt;t.style.insetInlineStart=`${jt+(a.isRTL?-i:i)}px`,t.style.insetBlockStart=`${Mt+(r-At)}px`}},Lt=()=>{let e=T();Ot=!1,we(e,`swal2-dragging`)},Rt=e=>{let t=e.type.startsWith(`touch`)?e.touches[0]:e;return{clientX:t.clientX,clientY:t.clientY}},zt=(e,t)=>{let n=S(),r=T();if(!(!n||!r)){if(t.toast){Ee(n,`width`,t.width),r.style.width=`100%`;let e=ue();e&&r.insertBefore(e,te())}else Ee(r,`width`,t.width);Ee(r,`padding`,t.padding),t.color&&(r.style.color=t.color),t.background&&(r.style.background=t.background),Oe(se()),Bt(r,t),t.draggable&&!t.toast?(A(r,l.draggable),Nt(r)):(we(r,l.draggable),Pt(r))}},Bt=(e,t)=>{let n=t.showClass||{};e.className=`${l.popup} ${Me(e)?n.popup:``}`,t.toast?(A([document.documentElement,document.body],l[`toast-shown`]),A(e,l.toast)):A(e,l.modal),k(e,t,`popup`),typeof t.customClass==`string`&&A(e,t.customClass),t.icon&&A(e,l[`icon-${t.icon}`])},Vt=(e,t)=>{let n=oe();if(!n)return;let{progressSteps:r,currentProgressStep:i}=t;if(!r||r.length===0||i===void 0){Oe(n);return}De(n),n.textContent=``,i>=r.length&&p(`Invalid currentProgressStep parameter, it should be less than progressSteps.length (currentProgressStep like JS arrays starts from 0)`),r.forEach((e,a)=>{let o=Ht(e);if(n.appendChild(o),a===i&&A(o,l[`active-progress-step`]),a!==r.length-1){let e=Ut(t);n.appendChild(e)}})},Ht=e=>{let t=document.createElement(`li`);return A(t,l[`progress-step`]),ve(t,e),t},Ut=e=>{let t=document.createElement(`li`);return A(t,l[`progress-step-line`]),e.progressStepsDistance&&Ee(t,`width`,e.progressStepsDistance),t},Wt=(e,t)=>{let n=re();n&&(ke(n),je(n,!!(t.title||t.titleText),`block`),t.title&&Je(t.title,n),t.titleText&&(n.innerText=t.titleText),k(n,t,`title`))},Gt=(e,t)=>{var n;zt(e,t),rt(e,t),Vt(e,t),bt(e,t),Dt(e,t),Wt(e,t),nt(e,t),vt(e,t),Ze(e,t),yt(e,t);let r=T();typeof t.didRender==`function`&&r&&t.didRender(r),(n=a.eventEmitter)==null||n.emit(`didRender`,r)},Kt=()=>Me(T()),qt=()=>ce()?.click(),Jt=()=>E()?.click(),Yt=()=>le()?.click(),Xt=Object.freeze({cancel:`cancel`,backdrop:`backdrop`,close:`close`,esc:`esc`,timer:`timer`}),Zt=e=>{if(e.keydownTarget&&e.keydownHandlerAdded&&e.keydownHandler){let t=e.keydownHandler;e.keydownTarget.removeEventListener(`keydown`,t,{capture:e.keydownListenerCapture}),e.keydownHandlerAdded=!1}},Qt=(e,t,n)=>{if(Zt(e),!t.toast){let r=e=>nn(t,e,n);e.keydownHandler=r;let i=t.keydownListenerCapture?window:T();if(i){e.keydownTarget=i,e.keydownListenerCapture=t.keydownListenerCapture;let n=r;e.keydownTarget.addEventListener(`keydown`,n,{capture:e.keydownListenerCapture}),e.keydownHandlerAdded=!0}}},$t=(e,t)=>{var n;let r=O();return r.length?(e+=t,e===-2&&(e=r.length-1),e===r.length?e=0:e===-1&&(e=r.length-1),r[e].focus(),!(ee()&&r[e]instanceof HTMLIFrameElement)):((n=T())==null||n.focus(),!0)},en=[`ArrowRight`,`ArrowDown`],tn=[`ArrowLeft`,`ArrowUp`],nn=(e,t,n)=>{e&&(t.isComposing||t.keyCode===229||(e.stopKeydownPropagation&&t.stopPropagation(),t.key===`Enter`?rn(t,e):t.key===`Tab`?an(t):[...en,...tn].includes(t.key)?on(t.key):t.key===`Escape`&&sn(t,e,n)))},rn=(e,t)=>{if(!v(t.allowEnterKey))return;let n=T();if(!n||!t.input)return;let r=xe(n,t.input);if(e.target&&r&&e.target instanceof HTMLElement&&e.target.outerHTML===r.outerHTML){if([`textarea`,`file`].includes(t.input))return;qt(),e.preventDefault()}},an=e=>{let t=e.target,n=O(),r=-1;for(let e=0;e{let t=de(),n=ce(),r=E(),i=le();if(!t||!n||!r||!i)return;let a=[n,r,i];if(document.activeElement instanceof HTMLElement&&!a.includes(document.activeElement))return;let o=en.includes(e)?`nextElementSibling`:`previousElementSibling`,s=document.activeElement;if(s){for(let e=0;e{e.preventDefault(),v(t.allowEscapeKey)&&n(Xt.esc)};var cn={swalPromiseResolve:new WeakMap,swalPromiseReject:new WeakMap};let ln=()=>{let e=S();Array.from(document.body.children).forEach(t=>{t.contains(e)||(t.hasAttribute(`aria-hidden`)&&t.setAttribute(`data-previous-aria-hidden`,t.getAttribute(`aria-hidden`)||``),t.setAttribute(`aria-hidden`,`true`))})},un=()=>{Array.from(document.body.children).forEach(e=>{e.hasAttribute(`data-previous-aria-hidden`)?(e.setAttribute(`aria-hidden`,e.getAttribute(`data-previous-aria-hidden`)||``),e.removeAttribute(`data-previous-aria-hidden`)):e.removeAttribute(`aria-hidden`)})},dn=typeof window<`u`&&!!window.GestureEvent,fn=dn&&/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream,pn=()=>{if(dn&&!ye(document.body,l.iosfix)){let e=document.body.scrollTop;document.body.style.top=`${e*-1}px`,A(document.body,l.iosfix),mn()}},mn=()=>{let e=S();if(!e)return;let t;e.ontouchstart=e=>{t=hn(e)},e.ontouchmove=e=>{t&&(e.preventDefault(),e.stopPropagation())}},hn=e=>{let t=e.target,n=S(),r=ie();return!n||!r||gn(e)||_n(e)?!1:t===n||!Pe(n)&&t instanceof HTMLElement&&!Fe(t,r)&&t.tagName!==`INPUT`&&t.tagName!==`TEXTAREA`&&!(Pe(r)&&r.contains(t))},gn=e=>!!(e.touches&&e.touches.length&&e.touches[0].touchType===`stylus`),_n=e=>e.touches&&e.touches.length>1,vn=()=>{if(ye(document.body,l.iosfix)){let e=parseInt(document.body.style.top,10);we(document.body,l.iosfix),document.body.style.top=``,document.body.scrollTop=e*-1}},yn=()=>{let e=document.createElement(`div`);e.className=l[`scrollbar-measure`],document.body.appendChild(e);let t=e.getBoundingClientRect().width-e.clientWidth;return document.body.removeChild(e),t},bn=null,xn=e=>{bn===null&&(document.body.scrollHeight>window.innerHeight||e===`scroll`)&&(bn=parseInt(window.getComputedStyle(document.body).getPropertyValue(`padding-right`)),document.body.style.paddingRight=`${bn+yn()}px`)},Sn=()=>{bn!==null&&(document.body.style.paddingRight=`${bn}px`,bn=null)};function Cn(e,t,n,r){ge()?Mn(e,r):(s(n).then(()=>Mn(e,r)),Zt(a)),dn?(t.setAttribute(`style`,`display:none !important`),t.removeAttribute(`class`),t.innerHTML=``):t.remove(),he()&&(Sn(),vn(),un()),wn()}function wn(){we([document.documentElement,document.body],[l.shown,l[`height-auto`],l[`no-backdrop`],l[`toast-shown`]])}function Tn(e){e=kn(e);let t=cn.swalPromiseResolve.get(this),n=En(this);this.isAwaitingPromise?e.isDismissed||(On(this),t(e)):n&&t(e)}let En=e=>{let t=T();if(!t)return!1;let n=j.innerParams.get(e);if(!n||ye(t,n.hideClass.popup))return!1;we(t,n.showClass.popup),A(t,n.hideClass.popup);let r=S();return we(r,n.showClass.backdrop),A(r,n.hideClass.backdrop),An(e,t,n),!0};function Dn(e){let t=cn.swalPromiseReject.get(this);On(this),t&&t(e)}let On=e=>{e.isAwaitingPromise&&(delete e.isAwaitingPromise,j.innerParams.get(e)||e._destroy())},kn=e=>e===void 0?{isConfirmed:!1,isDenied:!1,isDismissed:!0}:Object.assign({isConfirmed:!1,isDenied:!1,isDismissed:!1},e),An=(e,t,n)=>{var r;let i=S(),o=Ie(t);typeof n.willClose==`function`&&n.willClose(t),(r=a.eventEmitter)==null||r.emit(`willClose`,t),o&&i?jn(e,t,i,!!n.returnFocus,n.didClose):i&&Cn(e,i,!!n.returnFocus,n.didClose)},jn=(e,t,n,r,i)=>{a.swalCloseEventFinishedCallback=Cn.bind(null,e,n,r,i);let o=function(e){if(e.target===t){var n;(n=a.swalCloseEventFinishedCallback)==null||n.call(a),delete a.swalCloseEventFinishedCallback,t.removeEventListener(`animationend`,o),t.removeEventListener(`transitionend`,o)}};t.addEventListener(`animationend`,o),t.addEventListener(`transitionend`,o)},Mn=(e,t)=>{setTimeout(()=>{var n;typeof t==`function`&&t.bind(e.params)(),(n=a.eventEmitter)==null||n.emit(`didClose`),e._destroy&&e._destroy()})},Nn=e=>{let t=T();if(t||new Oi,t=T(),!t)return;let n=ue();ge()?Oe(te()):Pn(t,e),De(n),t.setAttribute(`data-loading`,`true`),t.setAttribute(`aria-busy`,`true`),t.focus()},Pn=(e,t)=>{let n=de(),r=ue();!n||!r||(!t&&Me(ce())&&(t=ce()),De(n),t&&(Oe(t),r.setAttribute(`data-button-to-replace`,t.className),n.insertBefore(r,t)),A([e,n],l.loading))},Fn=(e,t)=>{t.input===`select`||t.input===`radio`?Bn(e,t):[`text`,`email`,`number`,`tel`,`textarea`].some(e=>e===t.input)&&(y(t.inputValue)||x(t.inputValue))&&(Nn(ce()),Vn(e,t))},In=(e,t)=>{let n=e.getInput();if(!n)return null;switch(t.input){case`checkbox`:return Ln(n);case`radio`:return Rn(n);case`file`:return zn(n);default:return t.inputAutoTrim?n.value.trim():n.value}},Ln=e=>+!!e.checked,Rn=e=>e.checked?e.value:null,zn=e=>e.files&&e.files.length?e.getAttribute(`multiple`)===null?e.files[0]:e.files:null,Bn=(e,t)=>{let n=T();if(!n)return;let r=e=>{t.input===`select`?Hn(n,Wn(e),t):t.input===`radio`&&Un(n,Wn(e),t)};y(t.inputOptions)||x(t.inputOptions)?(Nn(ce()),b(t.inputOptions).then(t=>{e.hideLoading(),r(t)})):typeof t.inputOptions==`object`?r(t.inputOptions):m(`Unexpected type of inputOptions! Expected object, Map or Promise, got ${typeof t.inputOptions}`)},Vn=(e,t)=>{let n=e.getInput();n&&(Oe(n),b(t.inputValue).then(r=>{n.value=t.input===`number`?`${parseFloat(r)||0}`:`${r}`,De(n),n.focus(),e.hideLoading()}).catch(t=>{m(`Error in inputValue promise: ${t}`),n.value=``,De(n),n.focus(),e.hideLoading()}))};function Hn(e,t,n){let r=Te(e,l.select);if(!r)return;let i=(e,t,r)=>{let i=document.createElement(`option`);i.value=r,ve(i,t),i.selected=Gn(r,n.inputValue),e.appendChild(i)};t.forEach(e=>{let t=e[0],n=e[1];if(Array.isArray(n)){let e=document.createElement(`optgroup`);e.label=t,e.disabled=!1,r.appendChild(e),n.forEach(t=>i(e,t[1],t[0]))}else i(r,n,t)}),r.focus()}function Un(e,t,n){let r=Te(e,l.radio);if(!r)return;t.forEach(e=>{let t=e[0],i=e[1],a=document.createElement(`input`),o=document.createElement(`label`);a.type=`radio`,a.name=l.radio,a.value=t,Gn(t,n.inputValue)&&(a.checked=!0);let s=document.createElement(`span`);ve(s,i),s.className=l.label,o.appendChild(a),o.appendChild(s),r.appendChild(o)});let i=r.querySelectorAll(`input`);i.length&&i[0].focus()}let Wn=e=>(e instanceof Map?Array.from(e):Object.entries(e)).map(([e,t])=>[e,typeof t==`object`?Wn(t):t]),Gn=(e,t)=>!!t&&t!=null&&t.toString()===e.toString(),Kn=e=>{let t=j.innerParams.get(e);e.disableButtons(),t.input?Yn(e,`confirm`):er(e,!0)},qn=e=>{let t=j.innerParams.get(e);e.disableButtons(),t.returnInputValueOnDeny?Yn(e,`deny`):Zn(e,!1)},Jn=(e,t)=>{e.disableButtons(),t(Xt.cancel)},Yn=(e,t)=>{let n=j.innerParams.get(e);if(!n.input){m(`The "input" parameter is needed to be set when using returnInputValueOn${f(t)}`);return}let r=e.getInput(),i=In(e,n);n.inputValidator?Xn(e,i,t):r&&!r.checkValidity()?(e.enableButtons(),e.showValidationMessage(n.validationMessage||r.validationMessage)):t===`deny`?Zn(e,i):er(e,i)},Xn=(e,t,n)=>{let r=j.innerParams.get(e);e.disableInput(),Promise.resolve().then(()=>b(r.inputValidator(t,r.validationMessage))).then(r=>{e.enableButtons(),e.enableInput(),r?e.showValidationMessage(r):n===`deny`?Zn(e,t):er(e,t)})},Zn=(e,t)=>{let n=j.innerParams.get(e);n.showLoaderOnDeny&&Nn(E()),n.preDeny?(e.isAwaitingPromise=!0,Promise.resolve().then(()=>b(n.preDeny(t,n.validationMessage))).then(n=>{n===!1?(e.hideLoading(),On(e)):e.close({isDenied:!0,value:n===void 0?t:n})}).catch(t=>$n(e,t))):e.close({isDenied:!0,value:t})},Qn=(e,t)=>{e.close({isConfirmed:!0,value:t})},$n=(e,t)=>{e.rejectPromise(t)},er=(e,t)=>{let n=j.innerParams.get(e);n.showLoaderOnConfirm&&Nn(),n.preConfirm?(e.resetValidationMessage(),e.isAwaitingPromise=!0,Promise.resolve().then(()=>b(n.preConfirm(t,n.validationMessage))).then(n=>{Me(se())||n===!1?(e.hideLoading(),On(e)):Qn(e,n===void 0?t:n)}).catch(t=>$n(e,t))):Qn(e,t)};function tr(){let e=j.innerParams.get(this);if(!e)return;let t=j.domCache.get(this);Oe(t.loader),ge()?e.icon&&De(te()):nr(t),we([t.popup,t.actions],l.loading),t.popup.removeAttribute(`aria-busy`),t.popup.removeAttribute(`data-loading`),t.confirmButton.disabled=!1,t.denyButton.disabled=!1,t.cancelButton.disabled=!1;let n=j.focusedElement.get(this);n instanceof HTMLElement&&document.activeElement===document.body&&n.focus(),j.focusedElement.delete(this)}let nr=e=>{let t=e.loader.getAttribute(`data-button-to-replace`),n=t?e.popup.getElementsByClassName(t):[];n.length?De(n[0],`inline-block`):Ne()&&Oe(e.actions)};function rr(){let e=j.innerParams.get(this),t=j.domCache.get(this);return t?xe(t.popup,e.input):null}function ir(e,t,n){let r=j.domCache.get(e);t.forEach(e=>{r[e].disabled=n})}function ar(e,t){let n=T();if(!(!n||!e))if(e.type===`radio`){let e=n.querySelectorAll(`[name="${l.radio}"]`);for(let n=0;nObject.prototype.hasOwnProperty.call(fr,e),_r=e=>pr.indexOf(e)!==-1,vr=e=>mr[e],yr=e=>{gr(e)||p(`Unknown parameter "${e}"`)},br=e=>{hr.includes(e)&&p(`The parameter "${e}" is incompatible with toasts`)},xr=e=>{let t=vr(e);t&&_(e,t)},Sr=e=>{e.backdrop===!1&&e.allowOutsideClick&&p('"allowOutsideClick" parameter requires `backdrop` parameter to be set to `true`'),e.theme&&![`light`,`dark`,`auto`,`minimal`,`borderless`,`bootstrap-4`,`bootstrap-4-light`,`bootstrap-4-dark`,`bootstrap-5`,`bootstrap-5-light`,`bootstrap-5-dark`,`material-ui`,`material-ui-light`,`material-ui-dark`,`embed-iframe`,`bulma`,`bulma-light`,`bulma-dark`].includes(e.theme)&&p(`Invalid theme "${e.theme}"`);for(let t in e)yr(t),e.toast&&br(t),xr(t)};function Cr(e){let t=S(),n=T(),r=j.innerParams.get(this);if(!n||ye(n,r.hideClass.popup)){p(`You're trying to update the closed or closing popup, that won't work. Use the update() method in preConfirm parameter or show a new popup.`);return}let i=wr(e),a=Object.assign({},r,i);Sr(a),t&&(t.dataset.swal2Theme=a.theme),Gt(this,a),j.innerParams.set(this,a),Object.defineProperties(this,{params:{value:Object.assign({},this.params,e),writable:!1,enumerable:!0}})}let wr=e=>{let t={};return Object.keys(e).forEach(n=>{_r(n)?t[n]=e[n]:p(`Invalid parameter to update: ${n}`)}),t};function Tr(){var e;let t=j.domCache.get(this),n=j.innerParams.get(this);if(!n){Dr(this);return}t.popup&&a.swalCloseEventFinishedCallback&&(a.swalCloseEventFinishedCallback(),delete a.swalCloseEventFinishedCallback),typeof n.didDestroy==`function`&&n.didDestroy(),(e=a.eventEmitter)==null||e.emit(`didDestroy`),Er(this)}let Er=e=>{Dr(e),delete e.params,delete a.keydownHandler,delete a.keydownTarget,delete a.currentInstance},Dr=e=>{e.isAwaitingPromise?(Or(j,e),e.isAwaitingPromise=!0):(Or(cn,e),Or(j,e),delete e.isAwaitingPromise,delete e.disableButtons,delete e.enableButtons,delete e.getInput,delete e.disableInput,delete e.enableInput,delete e.hideLoading,delete e.disableLoading,delete e.showValidationMessage,delete e.resetValidationMessage,delete e.close,delete e.closePopup,delete e.closeModal,delete e.closeToast,delete e.rejectPromise,delete e.update,delete e._destroy)},Or=(e,t)=>{for(let n in e)e[n].delete(t)};var kr=Object.freeze({__proto__:null,_destroy:Tr,close:Tn,closeModal:Tn,closePopup:Tn,closeToast:Tn,disableButtons:sr,disableInput:lr,disableLoading:tr,enableButtons:or,enableInput:cr,getInput:rr,handleAwaitingPromise:On,hideLoading:tr,rejectPromise:Dn,resetValidationMessage:dr,showValidationMessage:ur,update:Cr});let Ar=(e,t,n)=>{e.toast?jr(e,t,n):(Pr(t),Fr(t),Ir(e,t,n))},jr=(e,t,n)=>{t.popup.onclick=()=>{e&&(Mr(e)||e.timer||e.input)||n(Xt.close)}},Mr=e=>!!(e.showConfirmButton||e.showDenyButton||e.showCancelButton||e.showCloseButton),Nr=!1,Pr=e=>{e.popup.onmousedown=()=>{e.container.onmouseup=function(t){e.container.onmouseup=()=>{},t.target===e.container&&(Nr=!0)}}},Fr=e=>{e.container.onmousedown=t=>{t.target===e.container&&t.preventDefault(),e.popup.onmouseup=function(t){e.popup.onmouseup=()=>{},(t.target===e.popup||t.target instanceof HTMLElement&&e.popup.contains(t.target))&&(Nr=!0)}}},Ir=(e,t,n)=>{t.container.onclick=r=>{if(Nr){Nr=!1;return}r.target===t.container&&v(e.allowOutsideClick)&&n(Xt.backdrop)}},Lr=e=>typeof e==`object`&&!!e&&`jquery`in e,Rr=e=>e instanceof Element||Lr(e),zr=e=>{let t={};return typeof e[0]==`object`&&!Rr(e[0])?Object.assign(t,e[0]):[`title`,`html`,`icon`].forEach((n,r)=>{let i=e[r];typeof i==`string`||Rr(i)?t[n]=i:i!==void 0&&m(`Unexpected type of ${n}! Expected "string" or "Element", got ${typeof i}`)}),t};function Br(...e){return new this(...e)}function Vr(e){class t extends this{_main(t,n){return super._main(t,Object.assign({},e,n))}}return t}let Hr=()=>a.timeout&&a.timeout.getTimerLeft(),Ur=()=>{if(a.timeout)return Re(),a.timeout.stop()},Wr=()=>{if(a.timeout){let e=a.timeout.start();return Le(e),e}},Gr=()=>{let e=a.timeout;return e&&(e.running?Ur():Wr())},Kr=e=>{if(a.timeout){let t=a.timeout.increase(e);return Le(t,!0),t}},M=()=>!!(a.timeout&&a.timeout.isRunning()),qr=!1,Jr={};function Yr(e=`data-swal-template`){Jr[e]=this,qr||=(document.body.addEventListener(`click`,Xr),!0)}let Xr=e=>{for(let t=e.target;t&&t!==document;t=t.parentNode)for(let e in Jr){let n=t.getAttribute&&t.getAttribute(e);if(n){Jr[e].fire({template:n});return}}};class Zr{constructor(){this.events={}}_getHandlersByEventName(e){return this.events[e]===void 0&&(this.events[e]=[]),this.events[e]}on(e,t){let n=this._getHandlersByEventName(e);n.includes(t)||n.push(t)}once(e,t){let n=(...r)=>{this.removeListener(e,n),t.apply(this,r)};this.on(e,n)}emit(e,...t){this._getHandlersByEventName(e).forEach(e=>{try{e.apply(this,t)}catch(e){console.error(e)}})}removeListener(e,t){let n=this._getHandlersByEventName(e),r=n.indexOf(t);r>-1&&n.splice(r,1)}removeAllListeners(e){this.events[e]!==void 0&&(this.events[e].length=0)}reset(){this.events={}}}a.eventEmitter=new Zr;var Qr=Object.freeze({__proto__:null,argsToParams:zr,bindClickHandler:Yr,clickCancel:Yt,clickConfirm:qt,clickDeny:Jt,enableLoading:Nn,fire:Br,getActions:de,getCancelButton:le,getCloseButton:me,getConfirmButton:ce,getContainer:S,getDenyButton:E,getFocusableElements:O,getFooter:fe,getHtmlContainer:ie,getIcon:te,getIconContent:ne,getImage:ae,getInputLabel:D,getLoader:ue,getPopup:T,getProgressSteps:oe,getTimerLeft:Hr,getTimerProgressBar:pe,getTitle:re,getValidationMessage:se,increaseTimer:Kr,isDeprecatedParameter:vr,isLoading:_e,isTimerRunning:M,isUpdatableParameter:_r,isValidParameter:gr,isVisible:Kt,mixin:Vr,off:(e,t)=>{if(a.eventEmitter){if(!e){a.eventEmitter.reset();return}t?a.eventEmitter.removeListener(e,t):a.eventEmitter.removeAllListeners(e)}},on:(e,t)=>{a.eventEmitter&&a.eventEmitter.on(e,t)},once:(e,t)=>{a.eventEmitter&&a.eventEmitter.once(e,t)},resumeTimer:Wr,showLoading:Nn,stopTimer:Ur,toggleTimer:Gr});class $r{constructor(e,t){this.callback=e,this.remaining=t,this.running=!1,this.start()}start(){return this.running||(this.running=!0,this.started=new Date,this.id=setTimeout(this.callback,this.remaining)),this.remaining}stop(){return this.started&&this.running&&(this.running=!1,clearTimeout(this.id),this.remaining-=new Date().getTime()-this.started.getTime()),this.remaining}increase(e){let t=this.running;return t&&this.stop(),this.remaining+=e,t&&this.start(),this.remaining}getTimerLeft(){return this.running&&(this.stop(),this.start()),this.remaining}isRunning(){return this.running}}let ei=[`swal-title`,`swal-html`,`swal-footer`],ti=e=>{let t=typeof e.template==`string`?document.querySelector(e.template):e.template;if(!t)return{};let n=t.content;return li(n),Object.assign(ni(n),ri(n),ii(n),ai(n),oi(n),si(n),ci(n,ei))},ni=e=>{let t={};return Array.from(e.querySelectorAll(`swal-param`)).forEach(e=>{N(e,[`name`,`value`]);let n=e.getAttribute(`name`),r=e.getAttribute(`value`);!n||!r||(n in fr&&typeof fr[n]==`boolean`?t[n]=r!==`false`:n in fr&&typeof fr[n]==`object`?t[n]=JSON.parse(r):t[n]=r)}),t},ri=e=>{let t={};return Array.from(e.querySelectorAll(`swal-function-param`)).forEach(e=>{let n=e.getAttribute(`name`),r=e.getAttribute(`value`);!n||!r||(t[n]=Function(`return ${r}`)())}),t},ii=e=>{let t={};return Array.from(e.querySelectorAll(`swal-button`)).forEach(e=>{N(e,[`type`,`color`,`aria-label`]);let n=e.getAttribute(`type`);if(!n||![`confirm`,`cancel`,`deny`].includes(n))return;t[`${n}ButtonText`]=e.innerHTML,t[`show${f(n)}Button`]=!0;let r=e.getAttribute(`color`);r!==null&&(t[`${n}ButtonColor`]=r);let i=e.getAttribute(`aria-label`);i!==null&&(t[`${n}ButtonAriaLabel`]=i)}),t},ai=e=>{let t={},n=e.querySelector(`swal-image`);if(n){N(n,[`src`,`width`,`height`,`alt`]);let e=n.getAttribute(`src`);e!==null&&(t.imageUrl=e||void 0);let r=n.getAttribute(`width`);r!==null&&(t.imageWidth=r||void 0);let i=n.getAttribute(`height`);i!==null&&(t.imageHeight=i||void 0);let a=n.getAttribute(`alt`);a!==null&&(t.imageAlt=a||void 0)}return t},oi=e=>{let t={},n=e.querySelector(`swal-icon`);return n&&(N(n,[`type`,`color`]),n.hasAttribute(`type`)&&(t.icon=n.getAttribute(`type`)),n.hasAttribute(`color`)&&(t.iconColor=n.getAttribute(`color`)),t.iconHtml=n.innerHTML),t},si=e=>{let t={},n=e.querySelector(`swal-input`);n&&(N(n,[`type`,`label`,`placeholder`,`value`]),t.input=n.getAttribute(`type`)||`text`,n.hasAttribute(`label`)&&(t.inputLabel=n.getAttribute(`label`)),n.hasAttribute(`placeholder`)&&(t.inputPlaceholder=n.getAttribute(`placeholder`)),n.hasAttribute(`value`)&&(t.inputValue=n.getAttribute(`value`)));let r=Array.from(e.querySelectorAll(`swal-input-option`));return r.length&&(t.inputOptions={},r.forEach(e=>{N(e,[`value`]);let n=e.getAttribute(`value`);if(!n)return;let r=e.innerHTML;t.inputOptions[n]=r})),t},ci=(e,t)=>{let n={};for(let r in t){let i=t[r],a=e.querySelector(i);a&&(N(a,[]),n[i.replace(/^swal-/,``)]=a.innerHTML.trim())}return n},li=e=>{let t=ei.concat([`swal-param`,`swal-function-param`,`swal-button`,`swal-image`,`swal-icon`,`swal-input`,`swal-input-option`]);Array.from(e.children).forEach(e=>{let n=e.tagName.toLowerCase();t.includes(n)||p(`Unrecognized element <${n}>`)})},N=(e,t)=>{Array.from(e.attributes).forEach(n=>{t.indexOf(n.name)===-1&&p([`Unrecognized attribute "${n.name}" on <${e.tagName.toLowerCase()}>.`,`${t.length?`Allowed attributes are: ${t.join(`, `)}`:`To set the value, use HTML within the element.`}`])})},ui=e=>{var t,n;let r=S(),i=T();if(!r||!i)return;typeof e.willOpen==`function`&&e.willOpen(i),(t=a.eventEmitter)==null||t.emit(`willOpen`,i);let o=window.getComputedStyle(document.body).overflowY;if(mi(r,i,e),setTimeout(()=>{fi(r,i)},10),he()&&(pi(r,e.scrollbarPadding===void 0?!1:e.scrollbarPadding,o),ln()),fn&&e.backdrop===!1&&i.scrollHeight>r.clientHeight&&(r.style.pointerEvents=`auto`),!ge()&&!a.previousActiveElement&&(a.previousActiveElement=document.activeElement),typeof e.didOpen==`function`){let t=e.didOpen;setTimeout(()=>t(i))}(n=a.eventEmitter)==null||n.emit(`didOpen`,i)},di=e=>{let t=T();if(!t||e.target!==t)return;let n=S();n&&(t.removeEventListener(`animationend`,di),t.removeEventListener(`transitionend`,di),n.style.overflowY=`auto`,we(n,l[`no-transition`]))},fi=(e,t)=>{Ie(t)?(e.style.overflowY=`hidden`,t.addEventListener(`animationend`,di),t.addEventListener(`transitionend`,di)):e.style.overflowY=`auto`},pi=(e,t,n)=>{pn(),t&&n!==`hidden`&&xn(n),setTimeout(()=>{e.scrollTop=0})},mi=(e,t,n)=>{var r;(r=n.showClass)!=null&&r.backdrop&&A(e,n.showClass.backdrop),n.animation?(t.style.setProperty(`opacity`,`0`,`important`),De(t,`grid`),setTimeout(()=>{var e;(e=n.showClass)!=null&&e.popup&&A(t,n.showClass.popup),t.style.removeProperty(`opacity`)},10)):De(t,`grid`),A([document.documentElement,document.body],l.shown),n.heightAuto&&n.backdrop&&!n.toast&&A([document.documentElement,document.body],l[`height-auto`])};var hi={email:(e,t)=>/^[a-zA-Z0-9.+_'-]+@[a-zA-Z0-9.-]+\.[a-zA-Z0-9-]+$/.test(e)?Promise.resolve():Promise.resolve(t||`Invalid email address`),url:(e,t)=>/^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-z]{2,63}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)$/.test(e)?Promise.resolve():Promise.resolve(t||`Invalid URL`)};function gi(e){e.inputValidator||(e.input===`email`&&(e.inputValidator=hi.email),e.input===`url`&&(e.inputValidator=hi.url))}function _i(e){(!e.target||typeof e.target==`string`&&!document.querySelector(e.target)||typeof e.target!=`string`&&!e.target.appendChild)&&(p(`Target parameter is not valid, defaulting to "body"`),e.target=`body`)}function vi(e){gi(e),e.showLoaderOnConfirm&&!e.preConfirm&&p(`showLoaderOnConfirm is set to true, but preConfirm is not defined. +showLoaderOnConfirm should be used together with preConfirm, see usage example: +https://sweetalert2.github.io/#ajax-request`),_i(e),typeof e.title==`string`&&(e.title=e.title.split(` +`).join(`
    `)),qe(e)}let yi;var bi=new WeakMap;class P{constructor(...e){if(r(this,bi,Promise.resolve({isConfirmed:!1,isDenied:!1,isDismissed:!0})),typeof window>`u`)return;yi=this;let t=Object.freeze(this.constructor.argsToParams(e));this.params=t,this.isAwaitingPromise=!1,i(bi,this,this._main(yi.params))}_main(e,t={}){if(Sr(Object.assign({},t,e)),a.currentInstance){let e=cn.swalPromiseResolve.get(a.currentInstance),{isAwaitingPromise:t}=a.currentInstance;a.currentInstance._destroy(),t||e({isDismissed:!0}),he()&&un()}a.currentInstance=yi;let n=Si(e,t);vi(n),Object.freeze(n),a.timeout&&(a.timeout.stop(),delete a.timeout),clearTimeout(a.restoreFocusTimeout);let r=Ci(yi);return Gt(yi,n),j.innerParams.set(yi,n),xi(yi,r,n)}then(e){return n(bi,this).then(e)}finally(e){return n(bi,this).finally(e)}}let xi=(e,t,n)=>new Promise((r,i)=>{let o=t=>{e.close({isDismissed:!0,dismiss:t,isConfirmed:!1,isDenied:!1})};cn.swalPromiseResolve.set(e,r),cn.swalPromiseReject.set(e,i),t.confirmButton.onclick=()=>{Kn(e)},t.denyButton.onclick=()=>{qn(e)},t.cancelButton.onclick=()=>{Jn(e,o)},t.closeButton.onclick=()=>{o(Xt.close)},Ar(n,t,o),Qt(a,n,o),Fn(e,n),ui(n),wi(a,n,o),Ti(t,n),setTimeout(()=>{t.container.scrollTop=0})}),Si=(e,t)=>{let n=ti(e),r=Object.assign({},fr,t,n,e);return r.showClass=Object.assign({},fr.showClass,r.showClass),r.hideClass=Object.assign({},fr.hideClass,r.hideClass),r.animation===!1&&(r.showClass={backdrop:`swal2-noanimation`},r.hideClass={}),r},Ci=e=>{let t={popup:T(),container:S(),actions:de(),confirmButton:ce(),denyButton:E(),cancelButton:le(),loader:ue(),closeButton:me(),validationMessage:se(),progressSteps:oe()};return j.domCache.set(e,t),t},wi=(e,t,n)=>{let r=pe();Oe(r),t.timer&&(e.timeout=new $r(()=>{n(`timer`),delete e.timeout},t.timer),t.timerProgressBar&&r&&(De(r),k(r,t,`timerProgressBar`),setTimeout(()=>{e.timeout&&e.timeout.running&&Le(t.timer)})))},Ti=(e,t)=>{if(!t.toast){if(!v(t.allowEnterKey)){_(`allowEnterKey`,`preConfirm: () => false`),e.popup.focus();return}Ei(e)||Di(e,t)||$t(-1,1)}},Ei=e=>{let t=Array.from(e.popup.querySelectorAll(`[autofocus]`));for(let e of t)if(e instanceof HTMLElement&&Me(e))return e.focus(),!0;return!1},Di=(e,t)=>t.focusDeny&&Me(e.denyButton)?(e.denyButton.focus(),!0):t.focusCancel&&Me(e.cancelButton)?(e.cancelButton.focus(),!0):t.focusConfirm&&Me(e.confirmButton)?(e.confirmButton.focus(),!0):!1;P.prototype.disableButtons=sr,P.prototype.enableButtons=or,P.prototype.getInput=rr,P.prototype.disableInput=lr,P.prototype.enableInput=cr,P.prototype.hideLoading=tr,P.prototype.disableLoading=tr,P.prototype.showValidationMessage=ur,P.prototype.resetValidationMessage=dr,P.prototype.close=Tn,P.prototype.closePopup=Tn,P.prototype.closeModal=Tn,P.prototype.closeToast=Tn,P.prototype.rejectPromise=Dn,P.prototype.update=Cr,P.prototype._destroy=Tr,Object.assign(P,Qr),Object.keys(kr).forEach(e=>{P[e]=function(...t){if(yi&&yi[e])return yi[e](...t)}}),P.DismissReason=Xt,P.version=`11.26.24`;let Oi=P;return Oi.default=Oi,Oi})),e!==void 0&&e.Sweetalert2&&(e.swal=e.sweetAlert=e.Swal=e.SweetAlert=e.Sweetalert2),typeof document<`u`&&function(e,t){var n=e.createElement(`style`);if(e.getElementsByTagName(`head`)[0].appendChild(n),n.styleSheet)n.styleSheet.disabled||(n.styleSheet.cssText=t);else try{n.innerHTML=t}catch{n.innerText=t}}(document,`:root{--swal2-outline: 0 0 0 3px rgba(100, 150, 200, 0.5);--swal2-container-padding: 0.625em;--swal2-backdrop: rgba(0, 0, 0, 0.4);--swal2-backdrop-transition: background-color 0.15s;--swal2-width: 32em;--swal2-padding: 0 0 1.25em;--swal2-border: none;--swal2-border-radius: 0.3125rem;--swal2-background: white;--swal2-color: #545454;--swal2-show-animation: swal2-show 0.3s;--swal2-hide-animation: swal2-hide 0.15s forwards;--swal2-icon-zoom: 1;--swal2-icon-animations: true;--swal2-title-padding: 0.8em 1em 0;--swal2-html-container-padding: 1em 1.6em 0.3em;--swal2-input-border: 1px solid #d9d9d9;--swal2-input-border-radius: 0.1875em;--swal2-input-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.06), 0 0 0 3px transparent;--swal2-input-background: transparent;--swal2-input-transition: border-color 0.2s, box-shadow 0.2s;--swal2-input-hover-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.06), 0 0 0 3px transparent;--swal2-input-focus-border: 1px solid #b4dbed;--swal2-input-focus-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.06), 0 0 0 3px rgba(100, 150, 200, 0.5);--swal2-progress-step-background: #add8e6;--swal2-validation-message-background: #f0f0f0;--swal2-validation-message-color: #666;--swal2-footer-border-color: #eee;--swal2-footer-background: transparent;--swal2-footer-color: inherit;--swal2-timer-progress-bar-background: rgba(0, 0, 0, 0.3);--swal2-close-button-position: initial;--swal2-close-button-inset: auto;--swal2-close-button-font-size: 2.5em;--swal2-close-button-color: #ccc;--swal2-close-button-transition: color 0.2s, box-shadow 0.2s;--swal2-close-button-outline: initial;--swal2-close-button-box-shadow: inset 0 0 0 3px transparent;--swal2-close-button-focus-box-shadow: inset var(--swal2-outline);--swal2-close-button-hover-transform: none;--swal2-actions-justify-content: center;--swal2-actions-width: auto;--swal2-actions-margin: 1.25em auto 0;--swal2-actions-padding: 0;--swal2-actions-border-radius: 0;--swal2-actions-background: transparent;--swal2-action-button-transition: background-color 0.2s, box-shadow 0.2s;--swal2-action-button-hover: black 10%;--swal2-action-button-active: black 10%;--swal2-confirm-button-box-shadow: none;--swal2-confirm-button-border-radius: 0.25em;--swal2-confirm-button-background-color: #7066e0;--swal2-confirm-button-color: #fff;--swal2-deny-button-box-shadow: none;--swal2-deny-button-border-radius: 0.25em;--swal2-deny-button-background-color: #dc3741;--swal2-deny-button-color: #fff;--swal2-cancel-button-box-shadow: none;--swal2-cancel-button-border-radius: 0.25em;--swal2-cancel-button-background-color: #6e7881;--swal2-cancel-button-color: #fff;--swal2-toast-show-animation: swal2-toast-show 0.5s;--swal2-toast-hide-animation: swal2-toast-hide 0.1s forwards;--swal2-toast-border: none;--swal2-toast-box-shadow: 0 0 1px hsl(0deg 0% 0% / 0.075), 0 1px 2px hsl(0deg 0% 0% / 0.075), 1px 2px 4px hsl(0deg 0% 0% / 0.075), 1px 3px 8px hsl(0deg 0% 0% / 0.075), 2px 4px 16px hsl(0deg 0% 0% / 0.075)}[data-swal2-theme=dark]{--swal2-dark-theme-black: #19191a;--swal2-dark-theme-white: #e1e1e1;--swal2-background: var(--swal2-dark-theme-black);--swal2-color: var(--swal2-dark-theme-white);--swal2-footer-border-color: #555;--swal2-input-background: color-mix(in srgb, var(--swal2-dark-theme-black), var(--swal2-dark-theme-white) 10%);--swal2-validation-message-background: color-mix( in srgb, var(--swal2-dark-theme-black), var(--swal2-dark-theme-white) 10% );--swal2-validation-message-color: var(--swal2-dark-theme-white);--swal2-timer-progress-bar-background: rgba(255, 255, 255, 0.7)}@media(prefers-color-scheme: dark){[data-swal2-theme=auto]{--swal2-dark-theme-black: #19191a;--swal2-dark-theme-white: #e1e1e1;--swal2-background: var(--swal2-dark-theme-black);--swal2-color: var(--swal2-dark-theme-white);--swal2-footer-border-color: #555;--swal2-input-background: color-mix(in srgb, var(--swal2-dark-theme-black), var(--swal2-dark-theme-white) 10%);--swal2-validation-message-background: color-mix( in srgb, var(--swal2-dark-theme-black), var(--swal2-dark-theme-white) 10% );--swal2-validation-message-color: var(--swal2-dark-theme-white);--swal2-timer-progress-bar-background: rgba(255, 255, 255, 0.7)}}body.swal2-shown:not(.swal2-no-backdrop,.swal2-toast-shown){overflow:hidden}body.swal2-height-auto{height:auto !important}body.swal2-no-backdrop .swal2-container{background-color:rgba(0,0,0,0) !important;pointer-events:none}body.swal2-no-backdrop .swal2-container .swal2-popup{pointer-events:auto}body.swal2-no-backdrop .swal2-container .swal2-modal{box-shadow:0 0 10px var(--swal2-backdrop)}body.swal2-toast-shown .swal2-container{box-sizing:border-box;width:360px;max-width:100%;background-color:rgba(0,0,0,0);pointer-events:none}body.swal2-toast-shown .swal2-container.swal2-top{inset:0 auto auto 50%;transform:translateX(-50%)}body.swal2-toast-shown .swal2-container.swal2-top-end,body.swal2-toast-shown .swal2-container.swal2-top-right{inset:0 0 auto auto}body.swal2-toast-shown .swal2-container.swal2-top-start,body.swal2-toast-shown .swal2-container.swal2-top-left{inset:0 auto auto 0}body.swal2-toast-shown .swal2-container.swal2-center-start,body.swal2-toast-shown .swal2-container.swal2-center-left{inset:50% auto auto 0;transform:translateY(-50%)}body.swal2-toast-shown .swal2-container.swal2-center{inset:50% auto auto 50%;transform:translate(-50%, -50%)}body.swal2-toast-shown .swal2-container.swal2-center-end,body.swal2-toast-shown .swal2-container.swal2-center-right{inset:50% 0 auto auto;transform:translateY(-50%)}body.swal2-toast-shown .swal2-container.swal2-bottom-start,body.swal2-toast-shown .swal2-container.swal2-bottom-left{inset:auto auto 0 0}body.swal2-toast-shown .swal2-container.swal2-bottom{inset:auto auto 0 50%;transform:translateX(-50%)}body.swal2-toast-shown .swal2-container.swal2-bottom-end,body.swal2-toast-shown .swal2-container.swal2-bottom-right{inset:auto 0 0 auto}@media print{body.swal2-shown:not(.swal2-no-backdrop,.swal2-toast-shown){overflow-y:scroll !important}body.swal2-shown:not(.swal2-no-backdrop,.swal2-toast-shown)>[aria-hidden=true]{display:none}body.swal2-shown:not(.swal2-no-backdrop,.swal2-toast-shown) .swal2-container{position:static !important}}div:where(.swal2-container){display:grid;position:fixed;z-index:1060;inset:0;box-sizing:border-box;grid-template-areas:"top-start top top-end" "center-start center center-end" "bottom-start bottom-center bottom-end";grid-template-rows:minmax(min-content, auto) minmax(min-content, auto) minmax(min-content, auto);height:100%;padding:var(--swal2-container-padding);overflow-x:hidden;transition:var(--swal2-backdrop-transition);-webkit-overflow-scrolling:touch}div:where(.swal2-container).swal2-backdrop-show,div:where(.swal2-container).swal2-noanimation{background:var(--swal2-backdrop)}div:where(.swal2-container).swal2-backdrop-hide{background:rgba(0,0,0,0) !important}div:where(.swal2-container).swal2-top-start,div:where(.swal2-container).swal2-center-start,div:where(.swal2-container).swal2-bottom-start{grid-template-columns:minmax(0, 1fr) auto auto}div:where(.swal2-container).swal2-top,div:where(.swal2-container).swal2-center,div:where(.swal2-container).swal2-bottom{grid-template-columns:auto minmax(0, 1fr) auto}div:where(.swal2-container).swal2-top-end,div:where(.swal2-container).swal2-center-end,div:where(.swal2-container).swal2-bottom-end{grid-template-columns:auto auto minmax(0, 1fr)}div:where(.swal2-container).swal2-top-start>.swal2-popup{align-self:start}div:where(.swal2-container).swal2-top>.swal2-popup{grid-column:2;place-self:start center}div:where(.swal2-container).swal2-top-end>.swal2-popup,div:where(.swal2-container).swal2-top-right>.swal2-popup{grid-column:3;place-self:start end}div:where(.swal2-container).swal2-center-start>.swal2-popup,div:where(.swal2-container).swal2-center-left>.swal2-popup{grid-row:2;align-self:center}div:where(.swal2-container).swal2-center>.swal2-popup{grid-column:2;grid-row:2;place-self:center center}div:where(.swal2-container).swal2-center-end>.swal2-popup,div:where(.swal2-container).swal2-center-right>.swal2-popup{grid-column:3;grid-row:2;place-self:center end}div:where(.swal2-container).swal2-bottom-start>.swal2-popup,div:where(.swal2-container).swal2-bottom-left>.swal2-popup{grid-column:1;grid-row:3;align-self:end}div:where(.swal2-container).swal2-bottom>.swal2-popup{grid-column:2;grid-row:3;place-self:end center}div:where(.swal2-container).swal2-bottom-end>.swal2-popup,div:where(.swal2-container).swal2-bottom-right>.swal2-popup{grid-column:3;grid-row:3;place-self:end end}div:where(.swal2-container).swal2-grow-row>.swal2-popup,div:where(.swal2-container).swal2-grow-fullscreen>.swal2-popup{grid-column:1/4;width:100%}div:where(.swal2-container).swal2-grow-column>.swal2-popup,div:where(.swal2-container).swal2-grow-fullscreen>.swal2-popup{grid-row:1/4;align-self:stretch}div:where(.swal2-container).swal2-no-transition{transition:none !important}div:where(.swal2-container)[popover]{width:auto;border:0}div:where(.swal2-container) div:where(.swal2-popup){display:none;position:relative;box-sizing:border-box;grid-template-columns:minmax(0, 100%);width:var(--swal2-width);max-width:100%;padding:var(--swal2-padding);border:var(--swal2-border);border-radius:var(--swal2-border-radius);background:var(--swal2-background);color:var(--swal2-color);font-family:inherit;font-size:1rem;container-name:swal2-popup}div:where(.swal2-container) div:where(.swal2-popup):focus{outline:none}div:where(.swal2-container) div:where(.swal2-popup).swal2-loading{overflow-y:hidden}div:where(.swal2-container) div:where(.swal2-popup).swal2-draggable{cursor:grab}div:where(.swal2-container) div:where(.swal2-popup).swal2-draggable div:where(.swal2-icon){cursor:grab}div:where(.swal2-container) div:where(.swal2-popup).swal2-dragging{cursor:grabbing}div:where(.swal2-container) div:where(.swal2-popup).swal2-dragging div:where(.swal2-icon){cursor:grabbing}div:where(.swal2-container) h2:where(.swal2-title){position:relative;max-width:100%;margin:0;padding:var(--swal2-title-padding);color:inherit;font-size:1.875em;font-weight:600;text-align:center;text-transform:none;overflow-wrap:break-word;cursor:initial}div:where(.swal2-container) div:where(.swal2-actions){display:flex;z-index:1;box-sizing:border-box;flex-wrap:wrap;align-items:center;justify-content:var(--swal2-actions-justify-content);width:var(--swal2-actions-width);margin:var(--swal2-actions-margin);padding:var(--swal2-actions-padding);border-radius:var(--swal2-actions-border-radius);background:var(--swal2-actions-background)}div:where(.swal2-container) div:where(.swal2-loader){display:none;align-items:center;justify-content:center;width:2.2em;height:2.2em;margin:0 1.875em;animation:swal2-rotate-loading 1.5s linear 0s infinite normal;border-width:.25em;border-style:solid;border-radius:100%;border-color:#2778c4 rgba(0,0,0,0) #2778c4 rgba(0,0,0,0)}div:where(.swal2-container) button:where(.swal2-styled){margin:.3125em;padding:.625em 1.1em;transition:var(--swal2-action-button-transition);border:none;box-shadow:0 0 0 3px rgba(0,0,0,0);font-weight:500}div:where(.swal2-container) button:where(.swal2-styled):not([disabled]){cursor:pointer}div:where(.swal2-container) button:where(.swal2-styled):where(.swal2-confirm){border-radius:var(--swal2-confirm-button-border-radius);background:initial;background-color:var(--swal2-confirm-button-background-color);box-shadow:var(--swal2-confirm-button-box-shadow);color:var(--swal2-confirm-button-color);font-size:1em}div:where(.swal2-container) button:where(.swal2-styled):where(.swal2-confirm):hover{background-color:color-mix(in srgb, var(--swal2-confirm-button-background-color), var(--swal2-action-button-hover))}div:where(.swal2-container) button:where(.swal2-styled):where(.swal2-confirm):active{background-color:color-mix(in srgb, var(--swal2-confirm-button-background-color), var(--swal2-action-button-active))}div:where(.swal2-container) button:where(.swal2-styled):where(.swal2-deny){border-radius:var(--swal2-deny-button-border-radius);background:initial;background-color:var(--swal2-deny-button-background-color);box-shadow:var(--swal2-deny-button-box-shadow);color:var(--swal2-deny-button-color);font-size:1em}div:where(.swal2-container) button:where(.swal2-styled):where(.swal2-deny):hover{background-color:color-mix(in srgb, var(--swal2-deny-button-background-color), var(--swal2-action-button-hover))}div:where(.swal2-container) button:where(.swal2-styled):where(.swal2-deny):active{background-color:color-mix(in srgb, var(--swal2-deny-button-background-color), var(--swal2-action-button-active))}div:where(.swal2-container) button:where(.swal2-styled):where(.swal2-cancel){border-radius:var(--swal2-cancel-button-border-radius);background:initial;background-color:var(--swal2-cancel-button-background-color);box-shadow:var(--swal2-cancel-button-box-shadow);color:var(--swal2-cancel-button-color);font-size:1em}div:where(.swal2-container) button:where(.swal2-styled):where(.swal2-cancel):hover{background-color:color-mix(in srgb, var(--swal2-cancel-button-background-color), var(--swal2-action-button-hover))}div:where(.swal2-container) button:where(.swal2-styled):where(.swal2-cancel):active{background-color:color-mix(in srgb, var(--swal2-cancel-button-background-color), var(--swal2-action-button-active))}div:where(.swal2-container) button:where(.swal2-styled):focus-visible{outline:none;box-shadow:var(--swal2-action-button-focus-box-shadow)}div:where(.swal2-container) button:where(.swal2-styled)[disabled]:not(.swal2-loading){opacity:.4}div:where(.swal2-container) button:where(.swal2-styled)::-moz-focus-inner{border:0}div:where(.swal2-container) div:where(.swal2-footer){margin:1em 0 0;padding:1em 1em 0;border-top:1px solid var(--swal2-footer-border-color);background:var(--swal2-footer-background);color:var(--swal2-footer-color);font-size:1em;text-align:center;cursor:initial}div:where(.swal2-container) .swal2-timer-progress-bar-container{position:absolute;right:0;bottom:0;left:0;grid-column:auto !important;overflow:hidden;border-bottom-right-radius:var(--swal2-border-radius);border-bottom-left-radius:var(--swal2-border-radius)}div:where(.swal2-container) div:where(.swal2-timer-progress-bar){width:100%;height:.25em;background:var(--swal2-timer-progress-bar-background)}div:where(.swal2-container) img:where(.swal2-image){max-width:100%;margin:2em auto 1em;cursor:initial}div:where(.swal2-container) button:where(.swal2-close){position:var(--swal2-close-button-position);inset:var(--swal2-close-button-inset);z-index:2;align-items:center;justify-content:center;width:1.2em;height:1.2em;margin-top:0;margin-right:0;margin-bottom:-1.2em;padding:0;overflow:hidden;transition:var(--swal2-close-button-transition);border:none;border-radius:var(--swal2-border-radius);outline:var(--swal2-close-button-outline);background:rgba(0,0,0,0);color:var(--swal2-close-button-color);font-family:monospace;font-size:var(--swal2-close-button-font-size);cursor:pointer;justify-self:end}div:where(.swal2-container) button:where(.swal2-close):hover{transform:var(--swal2-close-button-hover-transform);background:rgba(0,0,0,0);color:#f27474}div:where(.swal2-container) button:where(.swal2-close):focus-visible{outline:none;box-shadow:var(--swal2-close-button-focus-box-shadow)}div:where(.swal2-container) button:where(.swal2-close)::-moz-focus-inner{border:0}div:where(.swal2-container) div:where(.swal2-html-container){z-index:1;justify-content:center;margin:0;padding:var(--swal2-html-container-padding);overflow:auto;color:inherit;font-size:1.125em;font-weight:normal;line-height:normal;text-align:center;overflow-wrap:break-word;word-break:break-word;cursor:initial}div:where(.swal2-container) input:where(.swal2-input),div:where(.swal2-container) input:where(.swal2-file),div:where(.swal2-container) textarea:where(.swal2-textarea),div:where(.swal2-container) select:where(.swal2-select),div:where(.swal2-container) div:where(.swal2-radio),div:where(.swal2-container) label:where(.swal2-checkbox){margin:1em 2em 3px}div:where(.swal2-container) input:where(.swal2-input),div:where(.swal2-container) input:where(.swal2-file),div:where(.swal2-container) textarea:where(.swal2-textarea){box-sizing:border-box;width:auto;transition:var(--swal2-input-transition);border:var(--swal2-input-border);border-radius:var(--swal2-input-border-radius);background:var(--swal2-input-background);box-shadow:var(--swal2-input-box-shadow);color:inherit;font-size:1.125em}div:where(.swal2-container) input:where(.swal2-input).swal2-inputerror,div:where(.swal2-container) input:where(.swal2-file).swal2-inputerror,div:where(.swal2-container) textarea:where(.swal2-textarea).swal2-inputerror{border-color:#f27474 !important;box-shadow:0 0 2px #f27474 !important}div:where(.swal2-container) input:where(.swal2-input):hover,div:where(.swal2-container) input:where(.swal2-file):hover,div:where(.swal2-container) textarea:where(.swal2-textarea):hover{box-shadow:var(--swal2-input-hover-box-shadow)}div:where(.swal2-container) input:where(.swal2-input):focus,div:where(.swal2-container) input:where(.swal2-file):focus,div:where(.swal2-container) textarea:where(.swal2-textarea):focus{border:var(--swal2-input-focus-border);outline:none;box-shadow:var(--swal2-input-focus-box-shadow)}div:where(.swal2-container) input:where(.swal2-input)::placeholder,div:where(.swal2-container) input:where(.swal2-file)::placeholder,div:where(.swal2-container) textarea:where(.swal2-textarea)::placeholder{color:#ccc}div:where(.swal2-container) .swal2-range{margin:1em 2em 3px;background:var(--swal2-background)}div:where(.swal2-container) .swal2-range input{width:80%}div:where(.swal2-container) .swal2-range output{width:20%;color:inherit;font-weight:600;text-align:center}div:where(.swal2-container) .swal2-range input,div:where(.swal2-container) .swal2-range output{height:2.625em;padding:0;font-size:1.125em;line-height:2.625em}div:where(.swal2-container) .swal2-input{height:2.625em;padding:0 .75em}div:where(.swal2-container) .swal2-file{width:75%;margin-right:auto;margin-left:auto;background:var(--swal2-input-background);font-size:1.125em}div:where(.swal2-container) .swal2-textarea{height:6.75em;padding:.75em}div:where(.swal2-container) .swal2-select{min-width:50%;max-width:100%;padding:.375em .625em;background:var(--swal2-input-background);color:inherit;font-size:1.125em}div:where(.swal2-container) .swal2-radio,div:where(.swal2-container) .swal2-checkbox{align-items:center;justify-content:center;background:var(--swal2-background);color:inherit}div:where(.swal2-container) .swal2-radio label,div:where(.swal2-container) .swal2-checkbox label{margin:0 .6em;font-size:1.125em}div:where(.swal2-container) .swal2-radio input,div:where(.swal2-container) .swal2-checkbox input{flex-shrink:0;margin:0 .4em}div:where(.swal2-container) label:where(.swal2-input-label){display:flex;justify-content:center;margin:1em auto 0}div:where(.swal2-container) div:where(.swal2-validation-message){align-items:center;justify-content:center;margin:1em 0 0;padding:.625em;overflow:hidden;background:var(--swal2-validation-message-background);color:var(--swal2-validation-message-color);font-size:1em;font-weight:300}div:where(.swal2-container) div:where(.swal2-validation-message)::before{content:"!";display:inline-block;width:1.5em;min-width:1.5em;height:1.5em;margin:0 .625em;border-radius:50%;background-color:#f27474;color:#fff;font-weight:600;line-height:1.5em;text-align:center}div:where(.swal2-container) .swal2-progress-steps{flex-wrap:wrap;align-items:center;max-width:100%;margin:1.25em auto;padding:0;background:rgba(0,0,0,0);font-weight:600}div:where(.swal2-container) .swal2-progress-steps li{display:inline-block;position:relative}div:where(.swal2-container) .swal2-progress-steps .swal2-progress-step{z-index:20;flex-shrink:0;width:2em;height:2em;border-radius:2em;background:#2778c4;color:#fff;line-height:2em;text-align:center}div:where(.swal2-container) .swal2-progress-steps .swal2-progress-step.swal2-active-progress-step{background:#2778c4}div:where(.swal2-container) .swal2-progress-steps .swal2-progress-step.swal2-active-progress-step~.swal2-progress-step{background:var(--swal2-progress-step-background);color:#fff}div:where(.swal2-container) .swal2-progress-steps .swal2-progress-step.swal2-active-progress-step~.swal2-progress-step-line{background:var(--swal2-progress-step-background)}div:where(.swal2-container) .swal2-progress-steps .swal2-progress-step-line{z-index:10;flex-shrink:0;width:2.5em;height:.4em;margin:0 -1px;background:#2778c4}div:where(.swal2-icon){position:relative;box-sizing:content-box;justify-content:center;width:5em;height:5em;margin:2.5em auto .6em;zoom:var(--swal2-icon-zoom);border:.25em solid rgba(0,0,0,0);border-radius:50%;border-color:#000;font-family:inherit;line-height:5em;cursor:default;user-select:none}div:where(.swal2-icon) .swal2-icon-content{display:flex;align-items:center;font-size:3.75em}div:where(.swal2-icon).swal2-error{border-color:#f27474;color:#f27474}div:where(.swal2-icon).swal2-error .swal2-x-mark{position:relative;flex-grow:1}div:where(.swal2-icon).swal2-error [class^=swal2-x-mark-line]{display:block;position:absolute;top:2.3125em;width:2.9375em;height:.3125em;border-radius:.125em;background-color:#f27474}div:where(.swal2-icon).swal2-error [class^=swal2-x-mark-line][class$=left]{left:1.0625em;transform:rotate(45deg)}div:where(.swal2-icon).swal2-error [class^=swal2-x-mark-line][class$=right]{right:1em;transform:rotate(-45deg)}@container swal2-popup style(--swal2-icon-animations:true){div:where(.swal2-icon).swal2-error.swal2-icon-show{animation:swal2-animate-error-icon .5s}div:where(.swal2-icon).swal2-error.swal2-icon-show .swal2-x-mark{animation:swal2-animate-error-x-mark .5s}}div:where(.swal2-icon).swal2-warning{border-color:#f8bb86;color:#f8bb86}@container swal2-popup style(--swal2-icon-animations:true){div:where(.swal2-icon).swal2-warning.swal2-icon-show{animation:swal2-animate-error-icon .5s}div:where(.swal2-icon).swal2-warning.swal2-icon-show .swal2-icon-content{animation:swal2-animate-i-mark .5s}}div:where(.swal2-icon).swal2-info{border-color:#3fc3ee;color:#3fc3ee}@container swal2-popup style(--swal2-icon-animations:true){div:where(.swal2-icon).swal2-info.swal2-icon-show{animation:swal2-animate-error-icon .5s}div:where(.swal2-icon).swal2-info.swal2-icon-show .swal2-icon-content{animation:swal2-animate-i-mark .8s}}div:where(.swal2-icon).swal2-question{border-color:#87adbd;color:#87adbd}@container swal2-popup style(--swal2-icon-animations:true){div:where(.swal2-icon).swal2-question.swal2-icon-show{animation:swal2-animate-error-icon .5s}div:where(.swal2-icon).swal2-question.swal2-icon-show .swal2-icon-content{animation:swal2-animate-question-mark .8s}}div:where(.swal2-icon).swal2-success{border-color:#a5dc86;color:#a5dc86}div:where(.swal2-icon).swal2-success [class^=swal2-success-circular-line]{position:absolute;width:3.75em;height:7.5em;border-radius:50%}div:where(.swal2-icon).swal2-success [class^=swal2-success-circular-line][class$=left]{top:-0.4375em;left:-2.0635em;transform:rotate(-45deg);transform-origin:3.75em 3.75em;border-radius:7.5em 0 0 7.5em}div:where(.swal2-icon).swal2-success [class^=swal2-success-circular-line][class$=right]{top:-0.6875em;left:1.875em;transform:rotate(-45deg);transform-origin:0 3.75em;border-radius:0 7.5em 7.5em 0}div:where(.swal2-icon).swal2-success .swal2-success-ring{position:absolute;z-index:2;top:-0.25em;left:-0.25em;box-sizing:content-box;width:100%;height:100%;border:.25em solid rgba(165,220,134,.3);border-radius:50%}div:where(.swal2-icon).swal2-success .swal2-success-fix{position:absolute;z-index:1;top:.5em;left:1.625em;width:.4375em;height:5.625em;transform:rotate(-45deg)}div:where(.swal2-icon).swal2-success [class^=swal2-success-line]{display:block;position:absolute;z-index:2;height:.3125em;border-radius:.125em;background-color:#a5dc86}div:where(.swal2-icon).swal2-success [class^=swal2-success-line][class$=tip]{top:2.875em;left:.8125em;width:1.5625em;transform:rotate(45deg)}div:where(.swal2-icon).swal2-success [class^=swal2-success-line][class$=long]{top:2.375em;right:.5em;width:2.9375em;transform:rotate(-45deg)}@container swal2-popup style(--swal2-icon-animations:true){div:where(.swal2-icon).swal2-success.swal2-icon-show .swal2-success-line-tip{animation:swal2-animate-success-line-tip .75s}div:where(.swal2-icon).swal2-success.swal2-icon-show .swal2-success-line-long{animation:swal2-animate-success-line-long .75s}div:where(.swal2-icon).swal2-success.swal2-icon-show .swal2-success-circular-line-right{animation:swal2-rotate-success-circular-line 4.25s ease-in}}[class^=swal2]{-webkit-tap-highlight-color:rgba(0,0,0,0)}.swal2-show{animation:var(--swal2-show-animation)}.swal2-hide{animation:var(--swal2-hide-animation)}.swal2-noanimation{transition:none}.swal2-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}.swal2-rtl .swal2-close{margin-right:initial;margin-left:0}.swal2-rtl .swal2-timer-progress-bar{right:0;left:auto}.swal2-toast{box-sizing:border-box;grid-column:1/4 !important;grid-row:1/4 !important;grid-template-columns:min-content auto min-content;padding:1em;overflow-y:hidden;border:var(--swal2-toast-border);background:var(--swal2-background);box-shadow:var(--swal2-toast-box-shadow);pointer-events:auto}.swal2-toast>*{grid-column:2}.swal2-toast h2:where(.swal2-title){margin:.5em 1em;padding:0;font-size:1em;text-align:initial}.swal2-toast .swal2-loading{justify-content:center}.swal2-toast input:where(.swal2-input){height:2em;margin:.5em;font-size:1em}.swal2-toast .swal2-validation-message{font-size:1em}.swal2-toast div:where(.swal2-footer){margin:.5em 0 0;padding:.5em 0 0;font-size:.8em}.swal2-toast button:where(.swal2-close){grid-column:3/3;grid-row:1/99;align-self:center;width:.8em;height:.8em;margin:0;font-size:2em}.swal2-toast div:where(.swal2-html-container){margin:.5em 1em;padding:0;overflow:initial;font-size:1em;text-align:initial}.swal2-toast div:where(.swal2-html-container):empty{padding:0}.swal2-toast .swal2-loader{grid-column:1;grid-row:1/99;align-self:center;width:2em;height:2em;margin:.25em}.swal2-toast .swal2-icon{grid-column:1;grid-row:1/99;align-self:center;width:2em;min-width:2em;height:2em;margin:0 .5em 0 0}.swal2-toast .swal2-icon .swal2-icon-content{display:flex;align-items:center;font-size:1.8em;font-weight:bold}.swal2-toast .swal2-icon.swal2-success .swal2-success-ring{width:2em;height:2em}.swal2-toast .swal2-icon.swal2-error [class^=swal2-x-mark-line]{top:.875em;width:1.375em}.swal2-toast .swal2-icon.swal2-error [class^=swal2-x-mark-line][class$=left]{left:.3125em}.swal2-toast .swal2-icon.swal2-error [class^=swal2-x-mark-line][class$=right]{right:.3125em}.swal2-toast div:where(.swal2-actions){justify-content:flex-start;height:auto;margin:0;margin-top:.5em;padding:0 .5em}.swal2-toast button:where(.swal2-styled){margin:.25em .5em;padding:.4em .6em;font-size:1em}.swal2-toast .swal2-success{border-color:#a5dc86}.swal2-toast .swal2-success [class^=swal2-success-circular-line]{position:absolute;width:1.6em;height:3em;border-radius:50%}.swal2-toast .swal2-success [class^=swal2-success-circular-line][class$=left]{top:-0.8em;left:-0.5em;transform:rotate(-45deg);transform-origin:2em 2em;border-radius:4em 0 0 4em}.swal2-toast .swal2-success [class^=swal2-success-circular-line][class$=right]{top:-0.25em;left:.9375em;transform-origin:0 1.5em;border-radius:0 4em 4em 0}.swal2-toast .swal2-success .swal2-success-ring{width:2em;height:2em}.swal2-toast .swal2-success .swal2-success-fix{top:0;left:.4375em;width:.4375em;height:2.6875em}.swal2-toast .swal2-success [class^=swal2-success-line]{height:.3125em}.swal2-toast .swal2-success [class^=swal2-success-line][class$=tip]{top:1.125em;left:.1875em;width:.75em}.swal2-toast .swal2-success [class^=swal2-success-line][class$=long]{top:.9375em;right:.1875em;width:1.375em}@container swal2-popup style(--swal2-icon-animations:true){.swal2-toast .swal2-success.swal2-icon-show .swal2-success-line-tip{animation:swal2-toast-animate-success-line-tip .75s}.swal2-toast .swal2-success.swal2-icon-show .swal2-success-line-long{animation:swal2-toast-animate-success-line-long .75s}}.swal2-toast.swal2-show{animation:var(--swal2-toast-show-animation)}.swal2-toast.swal2-hide{animation:var(--swal2-toast-hide-animation)}@keyframes swal2-show{0%{transform:translate3d(0, -50px, 0) scale(0.9);opacity:0}100%{transform:translate3d(0, 0, 0) scale(1);opacity:1}}@keyframes swal2-hide{0%{transform:translate3d(0, 0, 0) scale(1);opacity:1}100%{transform:translate3d(0, -50px, 0) scale(0.9);opacity:0}}@keyframes swal2-animate-success-line-tip{0%{top:1.1875em;left:.0625em;width:0}54%{top:1.0625em;left:.125em;width:0}70%{top:2.1875em;left:-0.375em;width:3.125em}84%{top:3em;left:1.3125em;width:1.0625em}100%{top:2.8125em;left:.8125em;width:1.5625em}}@keyframes swal2-animate-success-line-long{0%{top:3.375em;right:2.875em;width:0}65%{top:3.375em;right:2.875em;width:0}84%{top:2.1875em;right:0;width:3.4375em}100%{top:2.375em;right:.5em;width:2.9375em}}@keyframes swal2-rotate-success-circular-line{0%{transform:rotate(-45deg)}5%{transform:rotate(-45deg)}12%{transform:rotate(-405deg)}100%{transform:rotate(-405deg)}}@keyframes swal2-animate-error-x-mark{0%{margin-top:1.625em;transform:scale(0.4);opacity:0}50%{margin-top:1.625em;transform:scale(0.4);opacity:0}80%{margin-top:-0.375em;transform:scale(1.15)}100%{margin-top:0;transform:scale(1);opacity:1}}@keyframes swal2-animate-error-icon{0%{transform:rotateX(100deg);opacity:0}100%{transform:rotateX(0deg);opacity:1}}@keyframes swal2-rotate-loading{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}@keyframes swal2-animate-question-mark{0%{transform:rotateY(-360deg)}100%{transform:rotateY(0)}}@keyframes swal2-animate-i-mark{0%{transform:rotateZ(45deg);opacity:0}25%{transform:rotateZ(-25deg);opacity:.4}50%{transform:rotateZ(15deg);opacity:.8}75%{transform:rotateZ(-5deg);opacity:1}100%{transform:rotateX(0);opacity:1}}@keyframes swal2-toast-show{0%{transform:translateY(-0.625em) rotateZ(2deg)}33%{transform:translateY(0) rotateZ(-2deg)}66%{transform:translateY(0.3125em) rotateZ(2deg)}100%{transform:translateY(0) rotateZ(0deg)}}@keyframes swal2-toast-hide{100%{transform:rotateZ(1deg);opacity:0}}@keyframes swal2-toast-animate-success-line-tip{0%{top:.5625em;left:.0625em;width:0}54%{top:.125em;left:.125em;width:0}70%{top:.625em;left:-0.25em;width:1.625em}84%{top:1.0625em;left:.75em;width:.5em}100%{top:1.125em;left:.1875em;width:.75em}}@keyframes swal2-toast-animate-success-line-long{0%{top:1.625em;right:1.375em;width:0}65%{top:1.25em;right:.9375em;width:0}84%{top:.9375em;right:0;width:1.125em}100%{top:.9375em;right:.1875em;width:1.375em}}`)})),Qi=o(((e,t)=>{(function(){var e={}.hasOwnProperty;function n(){for(var e=``,t=0;t{var n=1e3,r=n*60,i=r*60,a=i*24,o=a*365.25;t.exports=function(e,t){t||={};var n=typeof e;if(n===`string`&&e.length>0)return s(e);if(n===`number`&&isNaN(e)===!1)return t.long?l(e):c(e);throw Error(`val is not a non-empty string or a valid number. val=`+JSON.stringify(e))};function s(e){if(e=String(e),!(e.length>100)){var t=/^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(e);if(t){var s=parseFloat(t[1]);switch((t[2]||`ms`).toLowerCase()){case`years`:case`year`:case`yrs`:case`yr`:case`y`:return s*o;case`days`:case`day`:case`d`:return s*a;case`hours`:case`hour`:case`hrs`:case`hr`:case`h`:return s*i;case`minutes`:case`minute`:case`mins`:case`min`:case`m`:return s*r;case`seconds`:case`second`:case`secs`:case`sec`:case`s`:return s*n;case`milliseconds`:case`millisecond`:case`msecs`:case`msec`:case`ms`:return s;default:return}}}}function c(e){return e>=a?Math.round(e/a)+`d`:e>=i?Math.round(e/i)+`h`:e>=r?Math.round(e/r)+`m`:e>=n?Math.round(e/n)+`s`:e+`ms`}function l(e){return u(e,a,`day`)||u(e,i,`hour`)||u(e,r,`minute`)||u(e,n,`second`)||e+` ms`}function u(e,t,n){if(!(e{e=t.exports=i.debug=i.default=i,e.coerce=c,e.disable=o,e.enable=a,e.enabled=s,e.humanize=$i(),e.names=[],e.skips=[],e.formatters={};var n;function r(t){var n=0,r;for(r in t)n=(n<<5)-n+t.charCodeAt(r),n|=0;return e.colors[Math.abs(n)%e.colors.length]}function i(t){function i(){if(i.enabled){var t=i,r=+new Date;t.diff=r-(n||r),t.prev=n,t.curr=r,n=r;for(var a=Array(arguments.length),o=0;o{e=t.exports=ea(),e.log=i,e.formatArgs=r,e.save=a,e.load=o,e.useColors=n,e.storage=typeof chrome<`u`&&chrome.storage!==void 0?chrome.storage.local:s(),e.colors=[`lightseagreen`,`forestgreen`,`goldenrod`,`dodgerblue`,`darkorchid`,`crimson`];function n(){return typeof window<`u`&&window.process&&window.process.type===`renderer`?!0:typeof document<`u`&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||typeof window<`u`&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||typeof navigator<`u`&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&parseInt(RegExp.$1,10)>=31||typeof navigator<`u`&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)}e.formatters.j=function(e){try{return JSON.stringify(e)}catch(e){return`[UnexpectedJSONParseError]: `+e.message}};function r(t){var n=this.useColors;if(t[0]=(n?`%c`:``)+this.namespace+(n?` %c`:` `)+t[0]+(n?`%c `:` `)+`+`+e.humanize(this.diff),n){var r=`color: `+this.color;t.splice(1,0,r,`color: inherit`);var i=0,a=0;t[0].replace(/%[a-zA-Z%]/g,function(e){e!==`%%`&&(i++,e===`%c`&&(a=i))}),t.splice(a,0,r)}}function i(){return typeof console==`object`&&console.log&&Function.prototype.apply.call(console.log,console,arguments)}function a(t){try{t==null?e.storage.removeItem(`debug`):e.storage.debug=t}catch{}}function o(){var t;try{t=e.storage.debug}catch{}return!t&&typeof process<`u`&&`env`in process&&(t={}.DEBUG),t}e.enable(o());function s(){try{return window.localStorage}catch{}}})),na=o(((e,t)=>{var n=ta()(`jsonp`);t.exports=a;var r=0;function i(){}function a(e,t,a){typeof t==`function`&&(a=t,t={}),t||={};var o=t.prefix||`__jp`,s=t.name||o+ r++,c=t.param||`callback`,l=t.timeout==null?6e4:t.timeout,u=encodeURIComponent,d=document.getElementsByTagName(`script`)[0]||document.head,f,p;l&&(p=setTimeout(function(){m(),a&&a(Error(`Timeout`))},l));function m(){f.parentNode&&f.parentNode.removeChild(f),window[s]=i,p&&clearTimeout(p)}function h(){window[s]&&m()}return window[s]=function(e){n(`jsonp got`,e),m(),a&&a(null,e)},e+=(~e.indexOf(`?`)?`&`:`?`)+c+`=`+u(s),e=e.replace(`?&`,`?`),n(`jsonp req "%s"`,e),f=document.createElement(`script`),f.src=e,d.parentNode.insertBefore(f,d),h}})),ra=c(Zi(),1),ia=c(Qi(),1),aa=c(na(),1),oa=Object.defineProperty,sa=Object.defineProperties,ca=Object.getOwnPropertyDescriptors,la=Object.getOwnPropertySymbols,ua=Object.prototype.hasOwnProperty,da=Object.prototype.propertyIsEnumerable,fa=(e,t,n)=>t in e?oa(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,L=(e,t)=>{for(var n in t||={})ua.call(t,n)&&fa(e,n,t[n]);if(la)for(var n of la(t))da.call(t,n)&&fa(e,n,t[n]);return e},R=(e,t)=>sa(e,ca(t)),z=(e,t)=>{var n={};for(var r in e)ua.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(e!=null&&la)for(var r of la(e))t.indexOf(r)<0&&da.call(e,r)&&(n[r]=e[r]);return n};function pa(e,t){return x.Children.map(e,e=>!(0,x.isValidElement)(e)||e.props.fill!==void 0?e:(0,x.cloneElement)(e,{fill:t}))}function ma(e){var t=e,{bgStyle:n={},borderRadius:r=0,children:i,color:a,iconFillColor:o=`white`,round:s=!1,size:c=64}=t,l=z(t,[`bgStyle`,`borderRadius`,`children`,`color`,`iconFillColor`,`round`,`size`]);let u=pa(i,o);return(0,M.jsxs)(`svg`,R(L({viewBox:`0 0 64 64`,width:c,height:c},l),{children:[s?(0,M.jsx)(`circle`,{cx:`32`,cy:`32`,r:`32`,fill:a,style:n}):(0,M.jsx)(`rect`,{width:`64`,height:`64`,rx:r,ry:r,fill:a,style:n}),u]}))}var ha=class extends Error{constructor(e){super(e),this.name=`AssertionError`}};function B(e,t){if(!e)throw new ha(t)}function V(e){let t=Object.entries(e).filter(([,e])=>e!=null).map(([e,t])=>`${encodeURIComponent(e)}=${encodeURIComponent(String(t))}`);return t.length>0?`?${t.join(`&`)}`:``}var ga={bluesky:`Share on Bluesky`,email:`Share by email`,facebook:`Share on Facebook`,facebookmessenger:`Share in Messenger`,gab:`Share on Gab`,hatena:`Share on Hatena`,instapaper:`Save to Instapaper`,line:`Share on Line`,linkedin:`Share on LinkedIn`,livejournal:`Share on LiveJournal`,mailru:`Share on Mail.ru`,ok:`Share on OK`,pinterest:`Pin on Pinterest`,pocket:`Save to Pocket`,reddit:`Share on Reddit`,telegram:`Share on Telegram`,threads:`Share on Threads`,tumblr:`Share on Tumblr`,twitter:`Share on X`,viber:`Share on Viber`,vk:`Share on VK`,weibo:`Share on Weibo`,whatsapp:`Share on WhatsApp`,workplace:`Share on Workplace`},_a=e=>!!e&&(typeof e==`object`||typeof e==`function`)&&`then`in e&&typeof e.then==`function`,va=(e,t)=>({left:window.outerWidth/2+(window.screenX||window.screenLeft||0)-e/2,top:window.outerHeight/2+(window.screenY||window.screenTop||0)-t/2}),ya=(e,t)=>({top:(window.screen.height-t)/2,left:(window.screen.width-e)/2});function ba(e){let t=x.Children.toArray(e);if(t.length!==1)return;let[n]=t;if((0,x.isValidElement)(n))return n.props.round?`50%`:n.props.borderRadius??0}function xa(e){return x.Children.toArray(e).some(e=>typeof e==`string`?e.trim().length>0:typeof e==`number`?!0:(0,x.isValidElement)(e)?xa(e.props.children):!1)}function Sa(e,t,n){var r=t,{height:i,width:a}=r,o=z(r,[`height`,`width`]);let s=L({height:i,width:a,location:`no`,toolbar:`no`,status:`no`,directories:`no`,menubar:`no`,scrollbars:`yes`,resizable:`no`,centerscreen:`yes`,chrome:`yes`},o),c=window.open(e,``,Object.keys(s).map(e=>`${e}=${s[e]}`).join(`, `));if(n){let e=window.setInterval(()=>{try{(c===null||c.closed)&&(window.clearInterval(e),n(c))}catch(e){console.error(e)}},1e3)}return c}function H(e){var t=e,{"aria-label":n,"aria-labelledby":r,beforeOnClick:i,children:a,className:o,disabled:s,disabledStyle:c={opacity:.6},forwardedRef:l,htmlTitle:u,networkLink:d,networkName:f,onClick:p,onShareWindowClose:m,openShareDialogOnClick:h=!0,opts:g,resetButtonStyle:_=!0,style:v,title:y,type:b=`button`,url:x,windowHeight:ee=400,windowPosition:S=`windowCenter`,windowWidth:C=550}=t,w=z(t,[`aria-label`,`aria-labelledby`,`beforeOnClick`,`children`,`className`,`disabled`,`disabledStyle`,`forwardedRef`,`htmlTitle`,`networkLink`,`networkName`,`onClick`,`onShareWindowClose`,`openShareDialogOnClick`,`opts`,`resetButtonStyle`,`style`,`title`,`type`,`url`,`windowHeight`,`windowPosition`,`windowWidth`]);let T=ba(a),te=!n&&!r&&!xa(a)?ga[f]:void 0,ne=async e=>{if(s)return;let t=d(x,g);if(e.preventDefault(),i){let e=i();_a(e)&&await e}h&&Sa(t,L({height:ee,width:C},S===`windowCenter`?va(C,ee):ya(C,ee)),m),p&&p(e,t)},re=(0,ia.default)(`react-share__ShareButton`,{"react-share__ShareButton--disabled":!!s,disabled:!!s},o),ie=L(L(_?{backgroundColor:`transparent`,border:`none`,padding:0,display:`inline-flex`,borderRadius:T,outlineOffset:2,font:`inherit`,color:`inherit`,cursor:`pointer`}:{},v),s&&c);return(0,M.jsx)(`button`,R(L({},w),{"aria-label":n||te,"aria-labelledby":r,className:re,disabled:s,onClick:ne,ref:l,style:ie,title:u,type:b,children:a}))}function Ca(e,{title:t,separator:n}){return B(e,`bluesky.url`),`https://bsky.app/intent/compose`+V({text:t?t+n+e:e})}var wa=(0,x.forwardRef)((e,t)=>{var n=e,{separator:r,title:i}=n;return(0,M.jsx)(H,R(L({},z(n,[`separator`,`title`])),{forwardedRef:t,networkName:`bluesky`,networkLink:Ca,opts:{title:i,separator:r||` `},windowHeight:460,windowPosition:`windowCenter`,windowWidth:660}))});wa.displayName=`BlueskyShareButton`;function Ta(e){return(0,M.jsx)(ma,R(L({color:`#7f7f7f`},e),{children:(0,M.jsx)(`path`,{d:`M17,22v20h30V22H17z M41.1,25L32,32.1L22.9,25H41.1z M20,39V26.6l12,9.3l12-9.3V39H20z`})}))}function Ea(e,{subject:t,body:n,separator:r}){return`mailto:`+V({subject:t,body:n?n+r+e:e})}var Da=(0,x.forwardRef)((e,t)=>{var n=e,{body:r,separator:i,subject:a}=n;return(0,M.jsx)(H,R(L({},z(n,[`body`,`separator`,`subject`])),{forwardedRef:t,networkName:`email`,networkLink:Ea,onClick:(e,t)=>{window.location.href=t},openShareDialogOnClick:!1,opts:{subject:a,body:r,separator:i||` `}}))});Da.displayName=`EmailShareButton`;function Oa(e){return(0,M.jsx)(ma,R(L({color:`#0866FF`},e),{children:(0,M.jsx)(`path`,{d:`M34.1,47V33.3h4.6l0.7-5.3h-5.3v-3.4c0-1.5,0.4-2.6,2.6-2.6l2.8,0v-4.8c-0.5-0.1-2.2-0.2-4.1-0.2 c-4.1,0-6.9,2.5-6.9,7V28H24v5.3h4.6V47H34.1z`})}))}function ka(e,{appId:t,redirectUri:n,to:r}){return`https://www.facebook.com/dialog/send`+V({link:e,redirect_uri:n||e,app_id:t,to:r})}var Aa=(0,x.forwardRef)((e,t)=>{var n=e,{appId:r,redirectUri:i,to:a}=n;return(0,M.jsx)(H,R(L({},z(n,[`appId`,`redirectUri`,`to`])),{forwardedRef:t,networkName:`facebookmessenger`,networkLink:ka,opts:{appId:r,redirectUri:i,to:a},windowHeight:820,windowWidth:1e3}))});Aa.displayName=`FacebookMessengerShareButton`;function ja(e,{hashtag:t}){return B(e,`facebook.url`),`https://www.facebook.com/sharer/sharer.php`+V({u:e,hashtag:t})}var Ma=(0,x.forwardRef)((e,t)=>{var n=e,{hashtag:r}=n;return(0,M.jsx)(H,R(L({},z(n,[`hashtag`])),{forwardedRef:t,networkName:`facebook`,networkLink:ja,opts:{hashtag:r},windowHeight:400,windowWidth:550}))});Ma.displayName=`FacebookShareButton`;function Na(){let e=(0,x.useRef)(!1);return(0,x.useEffect)(()=>(e.current=!0,()=>{e.current=!1}),[]),(0,x.useCallback)(()=>e.current,[])}function Pa(e){var t=e,{children:n=e=>e,className:r,getCount:i,url:a}=t,o=z(t,[`children`,`className`,`getCount`,`url`]);let s=Na(),[c,l]=(0,x.useState)(void 0),[u,d]=(0,x.useState)(!1);return(0,x.useEffect)(()=>{d(!0),i(a,e=>{s()&&(l(e),d(!1))})},[a]),(0,M.jsx)(`span`,R(L({className:(0,ia.default)(`react-share__ShareCount`,r)},o),{children:!u&&c!==void 0&&n(c)}))}function Fa(e){let t=t=>(0,M.jsx)(Pa,L({getCount:e},t));return t.displayName=`ShareCount(${e.name})`,t}function Ia(e,t){(0,aa.default)(`https://graph.facebook.com/?id=${e}&fields=og_object{engagement}`,(e,n)=>{t(!e&&n&&n.og_object&&n.og_object.engagement?n.og_object.engagement.count:void 0)})}Fa(Ia);function La(e,{title:t}){return B(e,`hatena.url`),`http://b.hatena.ne.jp/add?mode=confirm&url=${e}&title=${t}`}var Ra=(0,x.forwardRef)((e,t)=>{var n=e,{title:r}=n;return(0,M.jsx)(H,R(L({},z(n,[`title`])),{forwardedRef:t,networkName:`hatena`,networkLink:La,opts:{title:r},windowHeight:460,windowPosition:`windowCenter`,windowWidth:660}))});Ra.displayName=`HatenaShareButton`;function za(e,t){(0,aa.default)(`https://bookmark.hatenaapis.com/count/entry`+V({url:e}),(e,n)=>{t(n??void 0)})}Fa(za);function Ba(e,{title:t,description:n}){return B(e,`instapaper.url`),`http://www.instapaper.com/hello2`+V({url:e,title:t,description:n})}var Va=(0,x.forwardRef)((e,t)=>{var n=e,{description:r,title:i}=n;return(0,M.jsx)(H,R(L({},z(n,[`description`,`title`])),{forwardedRef:t,networkName:`instapaper`,networkLink:Ba,opts:{title:i,description:r},windowHeight:500,windowPosition:`windowCenter`,windowWidth:500}))});Va.displayName=`InstapaperShareButton`;function Ha(e,{title:t}){return B(e,`line.url`),`https://social-plugins.line.me/lineit/share`+V({url:e,text:t})}var Ua=(0,x.forwardRef)((e,t)=>{var n=e,{title:r}=n;return(0,M.jsx)(H,R(L({},z(n,[`title`])),{forwardedRef:t,networkName:`line`,networkLink:Ha,opts:{title:r},windowHeight:500,windowWidth:500}))});Ua.displayName=`LineShareButton`;function Wa(e,{title:t,summary:n,source:r}){return B(e,`linkedin.url`),`https://linkedin.com/shareArticle`+V({url:e,mini:`true`,title:t,summary:n,source:r})}var Ga=(0,x.forwardRef)((e,t)=>{var n=e,{source:r,summary:i,title:a}=n;return(0,M.jsx)(H,R(L({},z(n,[`source`,`summary`,`title`])),{forwardedRef:t,networkName:`linkedin`,networkLink:Wa,opts:{title:a,summary:i,source:r},windowHeight:600,windowWidth:750}))});Ga.displayName=`LinkedinShareButton`;function Ka(e,{title:t,description:n}){return B(e,`livejournal.url`),`https://www.livejournal.com/update.bml`+V({subject:t,event:n})}var qa=(0,x.forwardRef)((e,t)=>{var n=e,{description:r,title:i}=n;return(0,M.jsx)(H,R(L({},z(n,[`description`,`title`])),{forwardedRef:t,networkName:`livejournal`,networkLink:Ka,opts:{title:i,description:r},windowHeight:460,windowWidth:660}))});qa.displayName=`LivejournalShareButton`;function Ja(e,{title:t,description:n,imageUrl:r}){return B(e,`mailru.url`),`https://connect.mail.ru/share`+V({url:e,title:t,description:n,image_url:r})}var Ya=(0,x.forwardRef)((e,t)=>{var n=e,{description:r,imageUrl:i,title:a}=n;return(0,M.jsx)(H,R(L({},z(n,[`description`,`imageUrl`,`title`])),{forwardedRef:t,networkName:`mailru`,networkLink:Ja,opts:{title:a,description:r,imageUrl:i},windowHeight:460,windowWidth:660}))});Ya.displayName=`MailruShareButton`;function Xa(e,{title:t,description:n,image:r}){return B(e,`ok.url`),`https://connect.ok.ru/offer`+V({url:e,title:t,description:n,imageUrl:r})}var Za=(0,x.forwardRef)((e,t)=>{var n=e,{description:r,image:i,title:a}=n;return(0,M.jsx)(H,R(L({},z(n,[`description`,`image`,`title`])),{forwardedRef:t,networkName:`ok`,networkLink:Xa,opts:{title:a,description:r,image:i},windowHeight:480,windowPosition:`screenCenter`,windowWidth:588}))});Za.displayName=`OKShareButton`;function Qa(e,t){window.OK||(window.OK={Share:{count:function(e,t){var n,r;(r=(n=window.OK.callbacks)[e])==null||r.call(n,t)}},callbacks:[]});let n=window.OK.callbacks.length;return window.ODKL={updateCount(e,t){var n,r;let i=e===``?0:parseInt(e.replace(`react-share-`,``),10);(r=(n=window.OK.callbacks)[i])==null||r.call(n,t===``?void 0:parseInt(t,10))}},window.OK.callbacks.push(t),(0,aa.default)(`https://connect.ok.ru/dk`+V({"st.cmd":`extLike`,uid:`react-share-${n}`,ref:e}))}Fa(Qa);function $a(e,{media:t,description:n,pinId:r}){return r?`https://pinterest.com/pin/${r}/repin/x/`:(B(e,`pinterest.url`),B(t,`pinterest.media`),`https://pinterest.com/pin/create/button/`+V({url:e,media:t,description:n}))}var eo=(0,x.forwardRef)((e,t)=>{var n=e,{description:r,media:i,pinId:a}=n;return(0,M.jsx)(H,R(L({},z(n,[`description`,`media`,`pinId`])),{forwardedRef:t,networkName:`pinterest`,networkLink:$a,opts:{media:i,description:r,pinId:a},windowHeight:730,windowWidth:1e3}))});eo.displayName=`PinterestShareButton`;function to(e,t){(0,aa.default)(`https://api.pinterest.com/v1/urls/count.json`+V({url:e}),(e,n)=>{t(n?n.count:void 0)})}Fa(to);function no(e,{title:t}){return B(e,`pocket.url`),`https://getpocket.com/save`+V({url:e,title:t})}var ro=(0,x.forwardRef)((e,t)=>{var n=e,{title:r}=n;return(0,M.jsx)(H,R(L({},z(n,[`title`])),{forwardedRef:t,networkName:`pocket`,networkLink:no,opts:{title:r},windowHeight:500,windowWidth:500}))});ro.displayName=`PocketShareButton`;function io(e,{title:t}){return B(e,`reddit.url`),`https://www.reddit.com/submit`+V({url:e,title:t})}var ao=(0,x.forwardRef)((e,t)=>{var n=e,{title:r}=n;return(0,M.jsx)(H,R(L({},z(n,[`title`])),{forwardedRef:t,networkName:`reddit`,networkLink:io,opts:{title:r},windowHeight:460,windowPosition:`windowCenter`,windowWidth:660}))});ao.displayName=`RedditShareButton`;function oo(e,{title:t}){return B(e,`gab.url`),`https://gab.com/compose`+V({url:e,text:t})}var so=(0,x.forwardRef)((e,t)=>{var n=e,{title:r}=n;return(0,M.jsx)(H,R(L({},z(n,[`title`])),{forwardedRef:t,networkName:`gab`,networkLink:oo,opts:{title:r},windowHeight:640,windowPosition:`windowCenter`,windowWidth:660}))});so.displayName=`GabShareButton`;function co(e,t){(0,aa.default)(`https://www.reddit.com/api/info.json?limit=1&url=${e}`,{param:`jsonp`},(e,n)=>{t(!e&&n&&n.data&&n.data.children.length>0&&n.data.children[0].data.score?n.data.children[0].data.score:void 0)})}Fa(co);function lo(e){return(0,M.jsx)(ma,R(L({color:`#26A5E4`},e),{children:(0,M.jsx)(`path`,{d:`m45.90873,15.44335c-0.6901,-0.0281 -1.37668,0.14048 -1.96142,0.41265c-0.84989,0.32661 -8.63939,3.33986 -16.5237,6.39174c-3.9685,1.53296 -7.93349,3.06593 -10.98537,4.24067c-3.05012,1.1765 -5.34694,2.05098 -5.4681,2.09312c-0.80775,0.28096 -1.89996,0.63566 -2.82712,1.72788c-0.23354,0.27218 -0.46884,0.62161 -0.58825,1.10275c-0.11941,0.48114 -0.06673,1.09222 0.16682,1.5716c0.46533,0.96052 1.25376,1.35737 2.18443,1.71383c3.09051,0.99037 6.28638,1.93508 8.93263,2.8236c0.97632,3.44171 1.91401,6.89571 2.84116,10.34268c0.30554,0.69185 0.97105,0.94823 1.65764,0.95525l-0.00351,0.03512c0,0 0.53908,0.05268 1.06412,-0.07375c0.52679,-0.12292 1.18879,-0.42846 1.79109,-0.99212c0.662,-0.62161 2.45836,-2.38812 3.47683,-3.38552l7.6736,5.66477l0.06146,0.03512c0,0 0.84989,0.59703 2.09312,0.68132c0.62161,0.04214 1.4399,-0.07726 2.14229,-0.59176c0.70766,-0.51626 1.1765,-1.34683 1.396,-2.29506c0.65673,-2.86224 5.00979,-23.57745 5.75257,-27.00686l-0.02107,0.08077c0.51977,-1.93157 0.32837,-3.70159 -0.87096,-4.74991c-0.60054,-0.52152 -1.2924,-0.7498 -1.98425,-0.77965l0,0.00176zm-0.2072,3.29069c0.04741,0.0439 0.0439,0.0439 0.00351,0.04741c-0.01229,-0.00351 0.14048,0.2072 -0.15804,1.32576l-0.01229,0.04214l-0.00878,0.03863c-0.75858,3.50668 -5.15554,24.40802 -5.74203,26.96472c-0.08077,0.34417 -0.11414,0.31959 -0.09482,0.29852c-0.1756,-0.02634 -0.50045,-0.16506 -0.52679,-0.1756l-13.13468,-9.70175c4.4988,-4.33199 9.09945,-8.25307 13.744,-12.43229c0.8218,-0.41265 0.68483,-1.68573 -0.29852,-1.70681c-1.04305,0.24584 -1.92279,0.99564 -2.8798,1.47502c-5.49971,3.2626 -11.11882,6.13186 -16.55882,9.49279c-2.792,-0.97105 -5.57873,-1.77704 -8.15298,-2.57601c2.2336,-0.89555 4.00889,-1.55579 5.75608,-2.23009c3.05188,-1.1765 7.01687,-2.7042 10.98537,-4.24067c7.94051,-3.06944 15.92667,-6.16346 16.62028,-6.43037l0.05619,-0.02283l0.05268,-0.02283c0.19316,-0.0878 0.30378,-0.09658 0.35471,-0.10009c0,0 -0.01756,-0.05795 -0.00351,-0.04566l-0.00176,0zm-20.91715,22.0638l2.16687,1.60145c-0.93418,0.91311 -1.81743,1.77353 -2.45485,2.38812l0.28798,-3.98957`})}))}function U(e,{title:t}){return B(e,`telegram.url`),`https://telegram.me/share/url`+V({url:e,text:t})}var W=(0,x.forwardRef)((e,t)=>{var n=e,{title:r}=n;return(0,M.jsx)(H,R(L({},z(n,[`title`])),{forwardedRef:t,networkName:`telegram`,networkLink:U,opts:{title:r},windowHeight:400,windowWidth:550}))});W.displayName=`TelegramShareButton`;function uo(e,{title:t}){return B(e,`threads.url`),`https://threads.net/intent/post`+V({url:e,text:t})}var fo=(0,x.forwardRef)((e,t)=>{var n=e,{hashtags:r,related:i,title:a,via:o}=n;return(0,M.jsx)(H,R(L({},z(n,[`hashtags`,`related`,`title`,`via`])),{forwardedRef:t,networkName:`threads`,networkLink:uo,opts:{title:a},windowHeight:600,windowWidth:550}))});fo.displayName=`ThreadsShareButton`;function po(e,{title:t,caption:n,tags:r,posttype:i}){return B(e,`tumblr.url`),`https://www.tumblr.com/widgets/share/tool`+V({canonicalUrl:e,title:t,caption:n,tags:r,posttype:i})}var mo=(0,x.forwardRef)((e,t)=>{var n=e,{caption:r,posttype:i,tags:a,title:o}=n;return(0,M.jsx)(H,R(L({},z(n,[`caption`,`posttype`,`tags`,`title`])),{forwardedRef:t,networkName:`tumblr`,networkLink:po,opts:{title:o,tags:(a||[]).join(`,`),caption:r,posttype:i||`link`},windowHeight:460,windowWidth:660}))});mo.displayName=`TumblrShareButton`;function ho(e,t){return(0,aa.default)(`https://api.tumblr.com/v2/share/stats`+V({url:e}),(e,n)=>{t(!e&&n&&n.response?n.response.note_count:void 0)})}Fa(ho);function go(e,{title:t,via:n,hashtags:r=[],related:i=[]}){return B(e,`x.url`),B(Array.isArray(r),`x.hashtags is not an array`),B(Array.isArray(i),`x.related is not an array`),`https://twitter.com/intent/tweet`+V({url:e,text:t,via:n,hashtags:r.length>0?r.join(`,`):void 0,related:i.length>0?i.join(`,`):void 0})}var _o=(0,x.forwardRef)((e,t)=>{var n=e,{hashtags:r,related:i,title:a,via:o}=n;return(0,M.jsx)(H,R(L({},z(n,[`hashtags`,`related`,`title`,`via`])),{forwardedRef:t,networkName:`twitter`,networkLink:go,opts:{hashtags:r,title:a,via:o,related:i},windowHeight:400,windowWidth:550}))});_o.displayName=`XShareButton`;var vo=(0,x.forwardRef)((e,t)=>(0,M.jsx)(_o,R(L({},e),{ref:t})));vo.displayName=`TwitterShareButton`;function yo(e,{title:t,separator:n}){return B(e,`viber.url`),`viber://forward`+V({text:t?t+n+e:e})}var bo=(0,x.forwardRef)((e,t)=>{var n=e,{separator:r,title:i}=n;return(0,M.jsx)(H,R(L({},z(n,[`separator`,`title`])),{forwardedRef:t,networkName:`viber`,networkLink:yo,opts:{title:i,separator:r||` `},windowHeight:460,windowWidth:660}))});bo.displayName=`ViberShareButton`;function xo(e,{title:t,image:n,noParse:r,noVkLinks:i}){return B(e,`vk.url`),`https://vk.com/share.php`+V({url:e,title:t,image:n,noparse:+!!r,no_vk_links:+!!i})}var So=(0,x.forwardRef)((e,t)=>{var n=e,{image:r,noParse:i,noVkLinks:a,title:o}=n;return(0,M.jsx)(H,R(L({},z(n,[`image`,`noParse`,`noVkLinks`,`title`])),{forwardedRef:t,networkName:`vk`,networkLink:xo,opts:{title:o,image:r,noParse:i,noVkLinks:a},windowHeight:460,windowWidth:660}))});So.displayName=`VKShareButton`;function Co(e,t){window.VK||(window.VK={}),window.VK.Share={count:(e,t)=>{var n;return((n=window.VK.callbacks)?.[e])?.call(n,t)}},window.VK.callbacks=[];let n=window.VK.callbacks.length;return window.VK.callbacks.push(t),(0,aa.default)(`https://vk.com/share.php`+V({act:`count`,index:n,url:e}))}Fa(Co);function wo(e,{title:t,image:n}){return B(e,`weibo.url`),`http://service.weibo.com/share/share.php`+V({url:e,title:t,pic:n})}var To=(0,x.forwardRef)((e,t)=>{var n=e,{image:r,title:i}=n;return(0,M.jsx)(H,R(L({},z(n,[`image`,`title`])),{forwardedRef:t,networkName:`weibo`,networkLink:wo,opts:{title:i,image:r},windowHeight:550,windowPosition:`screenCenter`,windowWidth:660}))});To.displayName=`WeiboShareButton`;function Eo(e){return(0,M.jsx)(ma,R(L({color:`#25D366`},e),{children:(0,M.jsx)(`path`,{d:`m42.32286,33.93287c-0.5178,-0.2589 -3.04726,-1.49644 -3.52105,-1.66732c-0.4712,-0.17346 -0.81554,-0.2589 -1.15987,0.2589c-0.34175,0.51004 -1.33075,1.66474 -1.63108,2.00648c-0.30032,0.33658 -0.60064,0.36247 -1.11327,0.12945c-0.5178,-0.2589 -2.17994,-0.80259 -4.14759,-2.56312c-1.53269,-1.37217 -2.56312,-3.05503 -2.86603,-3.57283c-0.30033,-0.5178 -0.03366,-0.80259 0.22524,-1.06149c0.23301,-0.23301 0.5178,-0.59547 0.7767,-0.90616c0.25372,-0.31068 0.33657,-0.5178 0.51262,-0.85437c0.17088,-0.36246 0.08544,-0.64725 -0.04402,-0.90615c-0.12945,-0.2589 -1.15987,-2.79613 -1.58964,-3.80584c-0.41424,-1.00971 -0.84142,-0.88027 -1.15987,-0.88027c-0.29773,-0.02588 -0.64208,-0.02588 -0.98382,-0.02588c-0.34693,0 -0.90616,0.12945 -1.37736,0.62136c-0.4712,0.5178 -1.80194,1.76053 -1.80194,4.27186c0,2.51134 1.84596,4.945 2.10227,5.30747c0.2589,0.33657 3.63497,5.51458 8.80262,7.74113c1.23237,0.5178 2.1903,0.82848 2.94111,1.08738c1.23237,0.38836 2.35599,0.33657 3.24402,0.20712c0.99159,-0.15534 3.04985,-1.24272 3.47963,-2.45956c0.44013,-1.21683 0.44013,-2.22654 0.31068,-2.45955c-0.12945,-0.23301 -0.46601,-0.36247 -0.98382,-0.59548m-9.40068,12.84407l-0.02589,0c-3.05503,0 -6.08417,-0.82849 -8.72495,-2.38189l-0.62136,-0.37023l-6.47252,1.68286l1.73463,-6.29129l-0.41424,-0.64725c-1.70875,-2.71846 -2.6149,-5.85116 -2.6149,-9.07706c0,-9.39809 7.68934,-17.06155 17.15993,-17.06155c4.58253,0 8.88029,1.78642 12.11655,5.02268c3.23625,3.21036 5.02267,7.50812 5.02267,12.06476c-0.0078,9.3981 -7.69712,17.06155 -17.14699,17.06155m14.58906,-31.58846c-3.93529,-3.80584 -9.1133,-5.95471 -14.62789,-5.95471c-11.36055,0 -20.60848,9.2065 -20.61625,20.52564c0,3.61684 0.94757,7.14565 2.75211,10.26282l-2.92557,10.63564l10.93337,-2.85309c3.0136,1.63108 6.4052,2.4958 9.85634,2.49839l0.01037,0c11.36574,0 20.61884,-9.2091 20.62403,-20.53082c0,-5.48093 -2.14111,-10.64081 -6.03239,-14.51915`})}))}function Do(e,{title:t,separator:n}){return B(e,`whatsapp.url`),`https://api.whatsapp.com/send`+V({text:t?t+n+e:e})}var Oo=(0,x.forwardRef)((e,t)=>{var n=e,{separator:r,title:i}=n;return(0,M.jsx)(H,R(L({},z(n,[`separator`,`title`])),{forwardedRef:t,networkName:`whatsapp`,networkLink:Do,opts:{title:i,separator:r||` `},windowHeight:400,windowWidth:550}))});Oo.displayName=`WhatsappShareButton`;function ko(e,{quote:t,hashtag:n}){return B(e,`workplace.url`),`https://work.facebook.com/sharer.php`+V({u:e,quote:t,hashtag:n})}var Ao=(0,x.forwardRef)((e,t)=>{var n=e,{hashtag:r,quote:i}=n;return(0,M.jsx)(H,R(L({},z(n,[`hashtag`,`quote`])),{forwardedRef:t,networkName:`workplace`,networkLink:ko,opts:{quote:i,hashtag:r},windowHeight:400,windowWidth:550}))});Ao.displayName=`WorkplaceShareButton`;function jo(e){return(0,M.jsx)(ma,R(L({color:`#000000`},e),{children:(0,M.jsx)(`path`,{d:`M 41.116 18.375 h 4.962 l -10.8405 12.39 l 12.753 16.86 H 38.005 l -7.821 -10.2255 L 21.235 47.625 H 16.27 l 11.595 -13.2525 L 15.631 18.375 H 25.87 l 7.0695 9.3465 z m -1.7415 26.28 h 2.7495 L 24.376 21.189 H 21.4255 z`})}))}var Mo=` + .scrollbar-hide::-webkit-scrollbar { display: none; } + .scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; } +`;function No({items:e}){return e.length===0?null:(0,M.jsx)(`ul`,{className:`mt-3 space-y-1`,children:e.map(e=>(0,M.jsxs)(`li`,{className:`flex items-start gap-2 text-[13px] text-[#555]`,children:[(0,M.jsx)(`span`,{className:`mt-[3px] w-2 h-2 rounded-sm bg-[#0047AB] flex-shrink-0`}),e]},e))})}var Po=({group:e,openProductId:t,onSelectProduct:n})=>{let r=e.findIndex(e=>e.id===t),i=r!==-1,a=i?e[r]:null;return(0,M.jsxs)(`div`,{className:`flex flex-col w-full animate-in fade-in duration-500`,children:[(0,M.jsx)(`style`,{children:Mo}),(0,M.jsx)(`div`,{className:`grid grid-cols-3 gap-3 mb-2 px-1`,children:e.map(e=>(0,M.jsx)(`div`,{onClick:()=>n(e.id),className:`relative aspect-square rounded-xl border-2 transition-all cursor-pointer overflow-hidden ${t===e.id?`border-[#0047AB] opacity-100 z-10 shadow-sm`:`border-transparent opacity-80 hover:opacity-100 hover:border-[#0047AB]/40`}`,children:(0,M.jsx)(`img`,{src:e.image,alt:e.name,className:`w-full h-full object-cover`})},e.id))}),i&&a&&(0,M.jsxs)(`div`,{className:`relative mt-1 mb-4 animate-in slide-in-from-top-2 duration-300`,children:[(0,M.jsx)(`div`,{className:`absolute -top-2 w-4 h-4 bg-white border-t border-l border-[#0047AB]/20 rotate-45 z-10 transition-all duration-300`,style:{left:`${r*33.33+16.66}%`,transform:`translateX(-50%) rotate(45deg)`}}),(0,M.jsxs)(`div`,{className:`bg-white border border-[#0047AB]/20 rounded-xl p-6 shadow-sm flex flex-col justify-between`,children:[(0,M.jsxs)(`div`,{className:`flex justify-between items-start`,children:[(0,M.jsxs)(`div`,{children:[(0,M.jsx)(`p`,{className:`text-[#707070] text-sm font-light mb-1`,children:a.brand}),(0,M.jsx)(`h3`,{className:`text-[#333333] font-bold text-[17px] leading-tight`,children:a.name})]}),(0,M.jsx)(`button`,{onClick:e=>{e.stopPropagation(),n(null)},className:`p-1 hover:bg-gray-100 rounded-full text-gray-400 transition-colors`,children:(0,M.jsx)(ki,{className:`w-4 h-4`})})]}),(0,M.jsxs)(`div`,{className:`mt-4 space-y-2`,children:[(0,M.jsxs)(`p`,{className:`text-slate-800 text-[15px]`,children:[`Tamaño: `,(0,M.jsx)(`span`,{className:`font-bold`,children:a.size})]}),a.description&&(0,M.jsx)(No,{items:a.description})]}),(0,M.jsx)(`div`,{className:`h-[1px] bg-gray-100 w-full my-5`}),a.detailUrl?(0,M.jsxs)(`a`,{href:a.detailUrl,target:`_blank`,rel:`noopener noreferrer`,onClick:e=>e.stopPropagation(),className:`flex items-center justify-center gap-2 text-[#0047AB] hover:text-[#002c75] transition-colors text-[15px] font-medium w-fit mx-auto group`,children:[(0,M.jsx)(`span`,{className:`border-b border-transparent group-hover:border-[#0047AB]`,children:`Más detalles del producto`}),(0,M.jsx)(di,{className:`w-4 h-4 transition-transform group-hover:translate-x-1`})]}):(0,M.jsxs)(`button`,{className:`flex items-center justify-center gap-2 text-[#0047AB] hover:text-[#002c75] transition-colors text-[15px] font-medium w-fit mx-auto group`,children:[(0,M.jsx)(`span`,{className:`border-b border-transparent group-hover:border-[#0047AB]`,children:`Más detalles del producto`}),(0,M.jsx)(di,{className:`w-4 h-4 transition-transform group-hover:translate-x-1`})]})]})]})]})},Fo=({product:e,isSelected:t,onToggle:n})=>(0,M.jsxs)(`div`,{onClick:n,className:`bg-white border rounded-xl overflow-hidden shadow-sm flex flex-col p-5 w-full transition-all duration-300 cursor-pointer ${t?`border-[#0047AB]`:`border-[#dbe7ff] hover:border-[#0047AB]/50`}`,children:[(0,M.jsxs)(`div`,{className:`flex gap-5 items-center`,children:[(0,M.jsx)(`div`,{className:`relative rounded-lg overflow-hidden bg-[#edf4ff] w-24 h-24 sm:w-32 sm:h-32 flex-shrink-0`,children:(0,M.jsx)(`img`,{src:e.image,alt:e.name,className:`w-full h-full object-cover`})}),(0,M.jsx)(`div`,{className:`flex flex-col justify-center flex-1 py-1`,children:(0,M.jsxs)(`div`,{className:`flex justify-between items-start`,children:[(0,M.jsxs)(`div`,{children:[(0,M.jsx)(`p`,{className:`text-[#707070] text-xs font-light`,children:e.brand}),(0,M.jsx)(`h3`,{className:`text-[#333333] font-bold text-[16px] leading-tight mt-1`,children:e.name})]}),(0,M.jsx)(`div`,{className:`text-gray-400`,children:t?(0,M.jsx)(mi,{className:`w-5 h-5`}):(0,M.jsx)(pi,{className:`w-5 h-5`})})]})})]}),t&&(0,M.jsxs)(`div`,{className:`mt-5 animate-in slide-in-from-top-2 duration-300`,onClick:e=>e.stopPropagation(),children:[(0,M.jsxs)(`p`,{className:`text-[#333333] text-[15px] mb-3`,children:[`Tamaño: `,(0,M.jsx)(`span`,{className:`font-bold`,children:e.size})]}),e.description&&(0,M.jsx)(No,{items:e.description}),(0,M.jsx)(`div`,{className:`h-[1px] bg-gray-100 w-full mb-4 mt-4`}),e.detailUrl?(0,M.jsxs)(`a`,{href:e.detailUrl,target:`_blank`,rel:`noopener noreferrer`,onClick:e=>e.stopPropagation(),className:`flex items-center gap-2 text-[#0047AB] hover:text-[#002c75] text-sm font-medium group text-left`,children:[(0,M.jsx)(`span`,{className:`border-b border-transparent group-hover:border-[#0047AB]`,children:`Más detalles del producto`}),(0,M.jsx)(di,{className:`w-4 h-4 transition-transform group-hover:translate-x-1`})]}):(0,M.jsxs)(`button`,{className:`flex items-center gap-2 text-[#0047AB] hover:text-[#002c75] text-sm font-medium group text-left`,children:[(0,M.jsx)(`span`,{className:`border-b border-transparent group-hover:border-[#0047AB]`,children:`Más detalles del producto`}),(0,M.jsx)(di,{className:`w-4 h-4 transition-transform group-hover:translate-x-1`})]})]})]});function Io(e){let t=Ui(e=>e.viewMode),n=Ui(e=>e.setViewMode),r=Ui(e=>e.openProductId),i=Ui(e=>e.setOpenProductId),[a,o]=(0,x.useState)(!1),[s,c]=(0,x.useState)(``),l=(0,x.useMemo)(()=>{let t=s.trim().toLowerCase();return e.filter(e=>t.length===0||e.brand.toLowerCase().includes(t)||e.name.toLowerCase().includes(t)||e.ref.toLowerCase().includes(t)||e.size.toLowerCase().includes(t))},[e,s]),u=(0,x.useCallback)(e=>{i(r===e?null:e)},[r,i]),d=(0,x.useCallback)(()=>{o(!1),c(``)},[]),f=(0,x.useCallback)(()=>n(`grid`),[n]),p=(0,x.useCallback)(()=>n(`list`),[n]),m=(0,x.useCallback)((e,t)=>{let n=[];for(let r=0;re.find(e=>e.id===r)??null,[e,r]),isSearchOpen:a,setIsSearchOpen:o,searchQuery:s,setSearchQuery:c,closeSearch:d,filteredProducts:l,chunkArray:m}}var Lo=``;function Ro(e,t){return{id:e.id,brand:t.nombre,name:e.nombre,ref:e.textura,size:e.dimensiones.length>0?e.dimensiones.join(` / `):`—`,image:`${Lo}${e.url_preview}`,description:t.especificaciones,detailUrl:t.url_detalle,tipo:t.tipo??void 0,categoria:t.id}}function zo(){let[e,t]=(0,x.useState)([]),[n,r]=(0,x.useState)([]),[i,a]=(0,x.useState)(!0),[o,s]=(0,x.useState)(null);return(0,x.useEffect)(()=>{let e=!1;async function n(){try{let n=await fetch(`${Lo}/api/catalog/textures`);if(!n.ok)throw Error(`Error ${n.status}`);let i=(await n.json()).categories.map(e=>({id:e.id,nombre:e.nombre,tipo:e.tipo,descripcion:e.descripcion,especificaciones:e.especificaciones,products:e.productos.map(t=>Ro(t,e))})),a=i.flatMap(e=>e.products);e||(r(i),t(a),s(null))}catch(t){e||s(t instanceof Error?t.message:`Error al cargar productos`)}finally{e||a(!1)}}return n(),()=>{e=!0}},[]),{products:e,categories:n,loading:i,error:o}}function Bo({previewImage:e,offset:t,zoom:n,imageSize:r,wrapperRef:i,selectedProduct:a,isApplying:o,onBack:s,onPointerDown:c,onPointerMove:l,onPointerUp:u,updateImageSize:d,onApplyTexture:f,onReset:p,onDownload:m,onShare:h}){let g=a!=null;return(0,M.jsx)(`div`,{className:`w-full h-full bg-white overflow-hidden`,children:(0,M.jsxs)(`div`,{className:`relative h-full overflow-hidden lg:rounded-lg bg-[#f4f8ff]`,children:[(0,M.jsxs)(`div`,{className:`lg:hidden absolute left-0 right-0 top-0 z-10 h-12 bg-white shadow-sm flex items-center justify-between px-3 pr-12`,children:[(0,M.jsx)(`button`,{onClick:s,className:`p-2 rounded-full hover:bg-gray-100 transition-colors`,children:(0,M.jsx)(ui,{className:`h-5 w-5 text-[#333]`})}),(0,M.jsxs)(`div`,{className:`flex items-center gap-1`,children:[(0,M.jsx)(`button`,{onClick:h,className:`p-2 rounded-full hover:bg-[#eaf1ff] transition-colors`,children:(0,M.jsx)(Ti,{className:`h-5 w-5 text-[#0047AB]`})}),(0,M.jsx)(`button`,{onClick:m,className:`p-2 rounded-full hover:bg-[#eaf1ff] transition-colors`,children:(0,M.jsx)(gi,{className:`h-5 w-5 text-[#0047AB]`})})]})]}),(0,M.jsxs)(`div`,{className:`pointer-events-none hidden lg:flex absolute left-1/2 top-0 z-10 -translate-x-1/2 w-[80%] h-16 rounded-b-md bg-white shadow-sm items-center justify-center gap-4 px-3`,children:[(0,M.jsxs)(`button`,{onClick:s,className:`pointer-events-auto rounded-full bg-[#333333] px-4 py-2 text-sm font-semibold text-white hover:bg-black flex items-center gap-2`,children:[(0,M.jsx)(ui,{className:`h-4 w-4`}),` Cambiar de Habitación`]}),(0,M.jsx)(`span`,{className:`text-gray-400`,children:`|`}),(0,M.jsxs)(`button`,{onClick:h,className:`pointer-events-auto rounded-full bg-transparent px-4 py-2 text-sm font-medium text-[#0047AB] hover:bg-[#eaf1ff] flex items-center gap-2`,children:[(0,M.jsx)(Ti,{className:`h-4 w-4 text-[#0047AB]`}),`Compartir`]}),(0,M.jsxs)(`button`,{onClick:m,className:`pointer-events-auto rounded-full bg-transparent px-4 py-2 text-sm font-medium text-[#0047AB] hover:bg-[#eaf1ff] flex items-center gap-2`,children:[(0,M.jsx)(gi,{className:`h-4 w-4 text-[#0047AB]`}),` Descargar`]}),(0,M.jsxs)(`a`,{href:`https://nauffargermany.com/gt/sucursales-2/`,target:`_blank`,rel:`noopener noreferrer`,className:`pointer-events-auto rounded-full bg-transparent px-4 py-2 text-sm font-medium text-[#0047AB] hover:bg-[#eaf1ff] flex items-center gap-2`,children:[(0,M.jsx)(yi,{className:`h-4 w-4 text-[#0047AB]`}),` Encuentra tu tienda`]}),a?.detailUrl?(0,M.jsxs)(`a`,{href:a.detailUrl,target:`_blank`,rel:`noopener noreferrer`,className:`pointer-events-auto rounded-full bg-transparent px-4 py-2 text-sm font-medium text-[#0047AB] hover:bg-[#eaf1ff] flex items-center gap-2`,children:[(0,M.jsx)(Ei,{className:`h-4 w-4 text-[#0047AB]`}),` Ir a la página del producto`]}):(0,M.jsxs)(`button`,{disabled:!0,className:`pointer-events-auto rounded-full bg-transparent px-4 py-2 text-sm font-medium text-gray-300 flex items-center gap-2 cursor-default`,children:[(0,M.jsx)(Ei,{className:`h-4 w-4 text-gray-300`}),` Ir a la página del producto`]})]}),(0,M.jsx)(`div`,{ref:i,className:`absolute inset-x-0 top-12 lg:top-16 bottom-12 lg:bottom-16 flex items-center justify-center bg-[#edf4ff] overflow-hidden`,onPointerDown:c,onPointerMove:l,onPointerUp:u,children:e?(0,M.jsx)(`div`,{style:{transform:`translate(${t.x}px, ${t.y}px) scale(${n})`,transformOrigin:`center center`,position:`relative`,display:`inline-flex`,lineHeight:0,...r.width>0?{width:r.width,height:r.height}:{}},children:(0,M.jsx)(`img`,{src:e,alt:`Vista previa de la habitación`,draggable:!1,onDragStart:e=>e.preventDefault(),onLoad:e=>d(e.currentTarget),style:{display:`block`,width:r.width>0?`100%`:`auto`,height:r.height>0?`100%`:`auto`,maxWidth:r.width>0?`none`:`100%`,maxHeight:r.height>0?`none`:`100%`,objectFit:`contain`}})}):(0,M.jsx)(`div`,{className:`flex h-full w-full items-center justify-center text-[#707070] text-sm px-6 text-center`,children:`No hay vista previa disponible aún.`})}),(0,M.jsxs)(`div`,{className:`pointer-events-none lg:hidden absolute left-0 right-0 bottom-0 z-10 h-12 bg-white border-t border-gray-100 shadow-sm flex items-center px-3 gap-2`,children:[a&&(0,M.jsxs)(`div`,{className:`flex items-center gap-2 min-w-0 flex-1`,children:[(0,M.jsx)(`img`,{src:a.image,alt:a.name,className:`w-8 h-8 object-cover rounded-md border border-gray-200 shrink-0`}),(0,M.jsxs)(`div`,{className:`min-w-0`,children:[(0,M.jsx)(`p`,{className:`text-[10px] text-[#707070] truncate`,children:a.brand}),(0,M.jsx)(`p`,{className:`text-xs font-semibold text-[#333] truncate leading-tight`,children:a.name})]})]}),(0,M.jsx)(`div`,{className:`flex-1`}),(0,M.jsxs)(`div`,{className:`flex items-center gap-1 ml-auto shrink-0`,children:[g&&(0,M.jsxs)(`button`,{onClick:f,disabled:o,className:`pointer-events-auto flex items-center gap-1.5 bg-[#0047AB] text-white px-3 py-1.5 rounded-full text-xs font-semibold hover:bg-[#003a94] disabled:opacity-60 transition-colors`,children:[o?(0,M.jsx)(vi,{className:`h-3 w-3 animate-spin`}):(0,M.jsx)(xi,{className:`h-3 w-3`}),o?`Aplicando...`:`Aplicar`]}),(0,M.jsx)(`button`,{onClick:p,className:`pointer-events-auto p-2 rounded-full hover:bg-[#eaf1ff] transition-colors`,children:(0,M.jsx)(Si,{className:`h-4 w-4 text-[#0047AB]`})})]})]}),(0,M.jsxs)(`div`,{className:`pointer-events-none hidden lg:flex absolute left-1/2 bottom-0 z-10 -translate-x-1/2 w-[80%] h-16 rounded-t-md bg-white border border-[#0047AB]/10 shadow-sm items-center justify-start gap-4 px-4`,children:[a&&(0,M.jsxs)(`div`,{className:`pointer-events-none flex items-center gap-3`,children:[(0,M.jsx)(`img`,{src:a.image,alt:a.name,className:`w-10 h-10 object-cover rounded-md border border-gray-200`}),(0,M.jsxs)(`div`,{children:[(0,M.jsx)(`p`,{className:`text-[#707070] text-xs`,children:a.brand}),(0,M.jsx)(`p`,{className:`font-semibold text-[#333333] text-sm leading-tight`,children:a.name})]})]}),(0,M.jsx)(`div`,{className:`flex-1`}),g&&(0,M.jsxs)(`button`,{onClick:f,disabled:o,className:`pointer-events-auto flex items-center gap-2 bg-[#0047AB] text-white px-4 py-2 rounded-full text-sm font-semibold hover:bg-[#003a94] disabled:opacity-60 transition-colors`,children:[o?(0,M.jsx)(vi,{className:`h-4 w-4 animate-spin`}):(0,M.jsx)(xi,{className:`h-4 w-4`}),o?`Aplicando...`:`Aplicar textura`]}),(0,M.jsxs)(`button`,{onClick:p,className:`pointer-events-auto rounded-full bg-transparent px-4 py-2 text-sm font-medium text-[#0047AB] hover:bg-[#eaf1ff] flex items-center gap-2`,children:[(0,M.jsx)(`span`,{className:`inline-flex items-center justify-center rounded-full bg-[#eaf1ff] p-2`,children:(0,M.jsx)(Si,{className:`h-4 w-4 text-[#0047AB]`})}),`Reiniciar`]}),(0,M.jsxs)(`button`,{className:`pointer-events-auto rounded-full bg-transparent px-4 py-2 text-sm font-medium text-[#0047AB] hover:bg-[#eaf1ff] flex items-center gap-2`,children:[(0,M.jsx)(`span`,{className:`inline-flex items-center justify-center rounded-full bg-[#eaf1ff] p-2`,children:(0,M.jsx)(Ci,{className:`h-4 w-4 text-[#0047AB]`})}),`Girar`]})]})]})})}function Vo(){let[e,t]=(0,x.useState)(!1),[n,r]=(0,x.useState)(null),[i,a]=(0,x.useState)(null);return{applyTexture:(0,x.useCallback)(async(e,n,i,o)=>{t(!0),a(null);try{let t=await fetch(`/seg/apply_texture_openai_proxy`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({filename:e,mask_indices:n,texture_name:i,original_filename:o??e,direction_mode:`auto`,angle_degrees:0})});if(!t.ok){let e=await t.text();throw Error(e||`Error ${t.status}`)}let a=await t.json();return r(`${a.output_url}`),a}catch(e){let t=e instanceof Error?e.message:`Error al aplicar textura`;throw a(t),Error(t)}finally{t(!1)}},[]),isApplying:e,resultUrl:n,error:i,resetResult:(0,x.useCallback)(()=>{r(null),a(null)},[])}}function Ho(){let[e,t]=(0,x.useState)(()=>window.innerWidth<1024);return(0,x.useEffect)(()=>{let e=()=>t(window.innerWidth<1024);return window.addEventListener(`resize`,e),()=>window.removeEventListener(`resize`,e)},[]),e}function Uo(){let e=j(),t=lt(),n=e.state,r=Ho(),i=Ui(e=>e.previewImage),a=Ui(e=>e.segmentFilename),o=Ui(e=>e.accumulatedFilename),s=Ui(e=>e.setAccumulatedFilename),c=Ui(e=>e.setSegmentResult),l=Ui(e=>e.setPreviewImage);(0,x.useEffect)(()=>{n?.filename&&n.filename!==a&&(c(n.filename,n.maskCount??0),s(null)),n?.previewImage&&l(n.previewImage)},[]);let[u,d]=(0,x.useState)(()=>o?`/seg/image/${o}`:n?.previewImage??i??null),[f,p]=(0,x.useState)(1),[m,h]=(0,x.useState)({x:0,y:0}),[g,_]=(0,x.useState)(null),[v,y]=(0,x.useState)({width:0,height:0}),b=(0,x.useRef)(null),{products:ee,categories:S,loading:C,error:w}=zo(),[T,te]=(0,x.useState)(null),ne=(0,x.useCallback)(e=>T===null?S.length>0&&e===S[0].id:T.has(e),[T,S]),re=(0,x.useCallback)(e=>{te(t=>{let n=t===null?S.length>0?new Set([S[0].id]):new Set:new Set(t);return n.has(e)?n.delete(e):n.add(e),n})},[S]),{viewMode:ie,showGrid:ae,showList:oe,openProductId:se,handleSelectProduct:ce,selectedProduct:le,isSearchOpen:E,setIsSearchOpen:D,searchQuery:ue,setSearchQuery:de,closeSearch:fe,filteredProducts:pe,chunkArray:me}=Io(ee);(0,x.useEffect)(()=>{oe()},[oe]);let O=new Set,he=()=>{},{applyTexture:ge,isApplying:_e,resetResult:ve}=Vo(),ye=(0,x.useCallback)(async e=>{if(!a)return;let t=o??a;try{let n=await ge(t,O.size>0?[...O]:[],e,a);n?.output_url&&(d(`${n.output_url}?t=${Date.now()}`),s(n.output_filename))}catch{}},[ge,a,O,he,o,s]),be=(0,x.useCallback)(async()=>{le&&await ye(le.ref)},[ye,le]),k=(0,x.useCallback)(async e=>{if(ce(e),!e||!a)return;let t=ee.find(t=>t.id===e);t&&await ye(t.ref)},[ce,O,a,ee,ye]),xe=(0,x.useCallback)(()=>{d(n?.previewImage??i??null),s(null),ve()},[n,i,s,he,ve]),Se=(0,x.useCallback)(async()=>{if(!u)return;let e=await(await fetch(u)).blob(),t=URL.createObjectURL(e),n=document.createElement(`a`);n.href=t,n.download=`hyper-reality-${Date.now()}.jpg`,document.body.appendChild(n),n.click(),document.body.removeChild(n),URL.revokeObjectURL(t)},[u]),Ce=(0,x.useCallback)(async()=>{let e=window.location.href,t=o;if(t)try{let n=await fetch(`/api/share`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({output_filename:t,segment_filename:a})});if(n.ok){let t=await n.json();e=`${window.location.origin}/app/share/${t.share_id}`}}catch{}let n=`My design with Hyper Reality Visualizer`,r=null;await ra.default.fire({title:`Compartir diseño`,html:` +
    +

    Enlace de tu diseño:

    +
    + + +
    +

    Compartir en:

    +
    +
    + `,showConfirmButton:!1,showCloseButton:!0,width:500,didOpen:()=>{let t=document.getElementById(`swal-copy-btn`);t?.addEventListener(`click`,async()=>{await navigator.clipboard.writeText(e).catch(()=>{}),t&&(t.textContent=`¡Copiado!`,t.style.background=`#16a34a`),setTimeout(()=>{t&&(t.textContent=`Copiar`,t.style.background=`#0047AB`)},2e3)});let i=document.getElementById(`swal-share-buttons`);i&&(r=(0,Yr.createRoot)(i),r.render((0,M.jsxs)(M.Fragment,{children:[(0,M.jsx)(Oo,{url:e,title:n,children:(0,M.jsx)(Eo,{size:48,round:!0})}),(0,M.jsx)(W,{url:e,title:n,children:(0,M.jsx)(lo,{size:48,round:!0})}),(0,M.jsx)(vo,{url:e,title:n,children:(0,M.jsx)(jo,{size:48,round:!0})}),(0,M.jsx)(Ma,{url:e,children:(0,M.jsx)(Oa,{size:48,round:!0})}),(0,M.jsx)(Da,{url:e,subject:n,body:`Mira mi diseño de habitación:`,children:(0,M.jsx)(Ta,{size:48,round:!0})})]})))},willClose:()=>{r?.unmount()}})},[a,o]),A=(0,x.useCallback)((e,t,n)=>{let r=b.current;if(!r||v.width===0||v.height===0)return{x:e,y:t};let i=r.getBoundingClientRect(),a=v.width*n,o=v.height*n,s=Math.max(0,(a-i.width)/2),c=Math.max(0,(o-i.height)/2);return{x:Math.max(-s,Math.min(s,e)),y:Math.max(-c,Math.min(c,t))}},[v]),we=(0,x.useCallback)(e=>{let t=b.current;if(!t)return;let n=t.getBoundingClientRect(),r=e.naturalWidth/e.naturalHeight,i=n.width/n.height;y({width:r>i?n.width:n.height*r,height:r>i?n.width/r:n.height}),h(e=>A(e.x,e.y,f))},[A,f]),Te=(0,x.useCallback)(e=>{e.preventDefault(),p(t=>{let n=Math.min(3,Math.max(1,t-e.deltaY*.0015));return h(e=>A(e.x,e.y,n)),n})},[A]);(0,x.useEffect)(()=>{let e=b.current;if(e)return e.addEventListener(`wheel`,Te,{passive:!1}),()=>e.removeEventListener(`wheel`,Te)},[Te]);let Ee={previewImage:u,offset:m,zoom:f,imageSize:v,wrapperRef:b,selectedProduct:le,isApplying:_e,onBack:()=>t(`/app`),onPointerDown:(0,x.useCallback)(e=>{_({x:e.clientX,y:e.clientY})},[]),onPointerMove:(0,x.useCallback)(e=>{if(!g||f<=1)return;let t=e.clientX-g.x,n=e.clientY-g.y;h(e=>A(e.x+t,e.y+n,f)),_({x:e.clientX,y:e.clientY})},[A,g,f]),onPointerUp:(0,x.useCallback)(()=>_(null),[]),updateImageSize:we,onApplyTexture:be,onReset:xe,onDownload:Se,onShare:Ce},De=(0,M.jsxs)(`div`,{style:{background:`#fff`,borderTop:`1px solid #e5e7eb`,flexShrink:0},children:[E&&(0,M.jsx)(`div`,{style:{padding:`8px 12px 4px`},children:(0,M.jsxs)(`div`,{style:{position:`relative`},children:[(0,M.jsx)(wi,{style:{position:`absolute`,left:10,top:`50%`,transform:`translateY(-50%)`,width:16,height:16,color:`#9ca3af`}}),(0,M.jsx)(`input`,{autoFocus:!0,type:`text`,placeholder:`Buscar productos...`,value:ue,onChange:e=>de(e.target.value),style:{width:`100%`,paddingLeft:34,paddingRight:32,paddingTop:8,paddingBottom:8,borderRadius:8,border:`2px solid #0047AB`,outline:`none`,fontSize:14,color:`#333`,boxSizing:`border-box`}}),(0,M.jsx)(`button`,{onClick:fe,style:{position:`absolute`,right:8,top:`50%`,transform:`translateY(-50%)`,background:`none`,border:`none`,cursor:`pointer`,padding:4},children:(0,M.jsx)(ki,{style:{width:14,height:14,color:`#9ca3af`}})})]})}),(0,M.jsx)(`div`,{style:{display:`flex`,overflowX:`auto`,gap:8,padding:`8px 12px`,scrollbarWidth:`none`},children:C?(0,M.jsx)(`div`,{style:{display:`flex`,alignItems:`center`,justifyContent:`center`,width:`100%`,height:64,fontSize:12,color:`#9ca3af`},children:`Cargando...`}):pe.map(e=>(0,M.jsx)(`button`,{onClick:()=>k(e.id),style:{flexShrink:0,width:64,height:64,borderRadius:12,overflow:`hidden`,border:se===e.id?`2.5px solid #0047AB`:`2px solid #e5e7eb`,boxShadow:se===e.id?`0 0 0 2px #dbe7ff`:`none`,cursor:`pointer`,padding:0,background:`none`,transition:`border-color 0.15s`},children:(0,M.jsx)(`img`,{src:e.image,alt:e.name,style:{width:`100%`,height:`100%`,objectFit:`cover`,display:`block`}})},e.id))}),(0,M.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,padding:`0 12px 10px`,gap:8,minHeight:36},children:[le?(0,M.jsxs)(`div`,{style:{flex:1,minWidth:0},children:[(0,M.jsx)(`p`,{style:{fontSize:10,color:`#707070`,textTransform:`uppercase`,letterSpacing:`0.05em`,margin:0,lineHeight:1},children:le.brand}),(0,M.jsx)(`p`,{style:{fontSize:12,fontWeight:600,color:`#333`,margin:0,overflow:`hidden`,textOverflow:`ellipsis`,whiteSpace:`nowrap`},children:le.name})]}):(0,M.jsx)(`div`,{style:{flex:1}}),(0,M.jsxs)(`div`,{style:{display:`flex`,gap:4,flexShrink:0},children:[(0,M.jsx)(`button`,{onClick:()=>D(!E),style:{padding:6,borderRadius:8,border:`none`,background:`none`,cursor:`pointer`},children:(0,M.jsx)(wi,{style:{width:16,height:16,color:`#0047AB`}})}),(0,M.jsx)(`button`,{style:{padding:6,borderRadius:8,border:`none`,background:`none`,cursor:`pointer`},children:(0,M.jsx)(Di,{style:{width:16,height:16,color:`#0047AB`}})})]})]})]}),Oe=(0,M.jsxs)(`div`,{style:{width:`25%`,height:`100%`,display:`flex`,flexDirection:`column`,borderRight:`1px solid rgba(0,71,171,0.1)`,background:`#fff`,flexShrink:0},children:[(0,M.jsxs)(`div`,{style:{padding:`24px 24px 0`},children:[(0,M.jsx)(`div`,{style:{height:1,background:`#e5e7eb`,width:`100%`}}),(0,M.jsxs)(`div`,{style:{display:`flex`,alignItems:`center`,gap:8,height:46},children:[!E&&(0,M.jsx)(`button`,{onClick:()=>D(!0),className:`p-3 rounded-lg border border-[#0047AB] bg-[#0047AB] text-white hover:bg-[#003a94] transition-all duration-300 flex items-center justify-center shadow-sm`,children:(0,M.jsx)(wi,{className:`w-5 h-5`})}),(0,M.jsxs)(`button`,{className:`flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg border border-[#0047AB] bg-white text-[#0047AB] hover:bg-[#eaf1ff] transition-all duration-300 shadow-sm`,children:[(0,M.jsx)(Di,{className:`w-4 h-4 text-[#0047AB]`}),(0,M.jsx)(`span`,{className:`font-medium text-sm`,children:`Filtros`})]}),(0,M.jsxs)(`div`,{className:`flex border border-gray-300 rounded-lg overflow-hidden shadow-sm`,children:[(0,M.jsx)(`button`,{onClick:oe,className:`p-2.5 flex items-center justify-center transition-colors ${ie===`list`?`bg-[#0047AB] text-white`:`bg-white text-[#0047AB] hover:bg-[#eaf1ff] border-r border-[#dbe7ff]`}`,children:(0,M.jsx)(bi,{className:`w-5 h-5`})}),(0,M.jsx)(`button`,{onClick:ae,className:`p-2.5 flex items-center justify-center transition-colors ${ie===`grid`?`bg-[#0047AB] text-white`:`bg-white text-[#0047AB] hover:bg-[#eaf1ff]`}`,children:(0,M.jsx)(_i,{className:`w-5 h-5`})})]})]}),E&&(0,M.jsx)(`div`,{className:`animate-in slide-in-from-top-2 fade-in duration-300`,children:(0,M.jsxs)(`div`,{className:`relative`,children:[(0,M.jsx)(wi,{className:`absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400`}),(0,M.jsx)(`input`,{autoFocus:!0,type:`text`,placeholder:`¿Qué estás buscando?...`,value:ue,onChange:e=>de(e.target.value),className:`w-full pl-11 pr-10 py-3 rounded-lg border-2 border-[#0047AB] bg-white focus:outline-none transition-all text-sm text-[#333333]`}),(0,M.jsx)(`button`,{onClick:fe,className:`absolute right-3 top-1/2 -translate-y-1/2 p-1 text-gray-400 hover:text-gray-600 bg-white rounded-full shadow-sm`,children:(0,M.jsx)(ki,{className:`w-4 h-4`})})]})})]}),(0,M.jsx)(`div`,{className:`flex-1 overflow-y-auto py-2`,children:C?(0,M.jsx)(`div`,{className:`flex items-center justify-center h-32 text-sm text-gray-400`,children:`Cargando productos...`}):w?(0,M.jsx)(`div`,{className:`flex items-center justify-center h-32 text-sm text-red-400`,children:w}):ue?(0,M.jsx)(`div`,{className:ie===`grid`?`px-4`:`px-2`,children:pe.length===0?(0,M.jsx)(`div`,{className:`flex items-center justify-center h-24 text-sm text-gray-400`,children:`Sin resultados`}):ie===`grid`?(0,M.jsx)(`div`,{className:`flex flex-col gap-4`,children:me(pe,3).map((e,t)=>(0,M.jsx)(Po,{group:e,openProductId:se,onSelectProduct:k},t))}):(0,M.jsx)(`div`,{className:`grid grid-cols-1 gap-3`,children:pe.map(e=>(0,M.jsx)(Fo,{product:e,isSelected:se===e.id,onToggle:()=>k(e.id)},e.id))})}):(0,M.jsx)(`div`,{className:`flex flex-col`,children:S.map(e=>{let t=ne(e.id);return(0,M.jsxs)(`div`,{className:`border-b border-gray-100 last:border-0`,children:[(0,M.jsxs)(`button`,{onClick:()=>re(e.id),className:`w-full flex items-center justify-between px-4 py-3 hover:bg-[#f4f8ff] transition-colors text-left`,children:[(0,M.jsxs)(`div`,{className:`flex items-center gap-2 min-w-0`,children:[(0,M.jsx)(`span`,{className:`font-semibold text-sm text-[#333] truncate`,children:e.nombre}),(0,M.jsx)(`span`,{className:`text-xs text-[#0047AB] bg-[#eaf1ff] px-1.5 py-0.5 rounded-full flex-shrink-0`,children:e.products.length})]}),(0,M.jsx)(pi,{className:`w-4 h-4 text-[#0047AB] flex-shrink-0 transition-transform duration-200 ${t?`rotate-180`:``}`})]}),t&&(0,M.jsx)(`div`,{className:ie===`grid`?`px-4 pb-4`:`px-2 pb-2`,children:ie===`grid`?(0,M.jsx)(`div`,{className:`flex flex-col gap-4`,children:me(e.products,3).map((e,t)=>(0,M.jsx)(Po,{group:e,openProductId:se,onSelectProduct:k},t))}):(0,M.jsx)(`div`,{className:`grid grid-cols-1 gap-3`,children:e.products.map(e=>(0,M.jsx)(Fo,{product:e,isSelected:se===e.id,onToggle:()=>k(e.id)},e.id))})})]},e.id)})})})]});return(0,M.jsx)(`div`,{style:{height:`100svh`,width:`100%`,background:`#fff`,fontFamily:`sans-serif`,color:`#000`,display:`flex`,flexDirection:`column`},children:r?(0,M.jsxs)(`div`,{style:{flex:1,display:`flex`,flexDirection:`column`,minHeight:0},children:[(0,M.jsx)(`div`,{style:{flex:1,minHeight:0,overflow:`hidden`},children:(0,M.jsx)(Bo,{...Ee})}),De]}):(0,M.jsxs)(`div`,{style:{flex:1,display:`flex`,flexDirection:`row`,minHeight:0},children:[Oe,(0,M.jsx)(`div`,{style:{flex:1,minWidth:0,overflow:`hidden`},children:(0,M.jsx)(Bo,{...Ee})})]})})}function Wo(){let{shareId:e}=dt(),[t,n]=(0,x.useState)(null),[r,i]=(0,x.useState)(null),[a,o]=(0,x.useState)(!0);return(0,x.useEffect)(()=>{if(!e){i(`ID de sesión inválido`),o(!1);return}fetch(`/api/share/${e}`).then(e=>{if(!e.ok)throw Error(`Sesión compartida no encontrada o expirada`);return e.json()}).then(e=>{n(`/seg/image/${e.output_filename}`)}).catch(e=>i(e.message)).finally(()=>o(!1))},[e]),a?(0,M.jsx)(`div`,{className:`flex items-center justify-center h-screen bg-gray-950 text-white text-sm`,children:`Cargando diseño compartido...`}):r?(0,M.jsxs)(`div`,{className:`flex flex-col items-center justify-center h-screen bg-gray-950 gap-3`,children:[(0,M.jsx)(`p`,{className:`text-red-400 text-sm`,children:r}),(0,M.jsx)(`p`,{className:`text-gray-500 text-xs`,children:`Este enlace puede haber expirado. Los diseños compartidos se mantienen mientras el servidor esté activo.`})]}):(0,M.jsxs)(`div`,{className:`flex flex-col h-screen bg-gray-950`,children:[(0,M.jsxs)(`div`,{className:`flex items-center justify-between px-5 py-3 bg-white shadow-sm shrink-0`,children:[(0,M.jsx)(`span`,{className:`text-[#0047AB] font-bold text-base tracking-tight`,children:`Hyper Reality Visualizer`}),(0,M.jsxs)(`button`,{onClick:async()=>{if(!t)return;let n=await(await fetch(t)).blob(),r=URL.createObjectURL(n),i=document.createElement(`a`);i.href=r,i.download=`hyper-reality-${e}.jpg`,document.body.appendChild(i),i.click(),document.body.removeChild(i),URL.revokeObjectURL(r)},className:`flex items-center gap-2 px-4 py-2 bg-[#0047AB] text-white rounded-full text-sm font-medium hover:bg-[#003a94] transition-colors`,children:[(0,M.jsx)(gi,{className:`w-4 h-4`}),`Descargar`]})]}),(0,M.jsx)(`div`,{className:`flex-1 flex items-center justify-center p-6 min-h-0`,children:t&&(0,M.jsx)(`img`,{src:t,alt:`Diseño de habitación compartido`,className:`max-h-full max-w-full object-contain rounded-xl shadow-2xl`})}),(0,M.jsxs)(`div`,{className:`text-center py-3 text-gray-500 text-xs shrink-0`,children:[`Diseñado con`,` `,(0,M.jsx)(`a`,{href:`https://hyperrealitycompany.com`,target:`_blank`,rel:`noopener noreferrer`,className:`text-[#4a7fd4] hover:underline`,children:`Hyper Reality Visualizer`})]})]})}function Go(){return(0,M.jsx)(`div`,{className:`min-h-screen bg-[#f4f8ff] text-[#333333]`,children:(0,M.jsxs)(Rt,{children:[(0,M.jsx)(It,{path:`/`,element:(0,M.jsx)(Xi,{})}),(0,M.jsx)(It,{path:`/visualizer`,element:(0,M.jsx)(Uo,{})}),(0,M.jsx)(It,{path:`/share/:shareId`,element:(0,M.jsx)(Wo,{})}),(0,M.jsx)(It,{path:`/settings`,element:(0,M.jsx)(ei,{})}),(0,M.jsx)(It,{path:`*`,element:(0,M.jsx)(Ft,{to:`/`,replace:!0})})]})})}var Ko=new Gr;(0,Yr.createRoot)(document.getElementById(`root`)).render((0,M.jsx)(x.StrictMode,{children:(0,M.jsx)(En,{basename:`/app`,children:(0,M.jsxs)(Jr,{client:Ko,children:[(0,M.jsx)(Go,{}),!1]})})})); \ No newline at end of file diff --git a/frontend/dist/index.html b/frontend/dist/index.html index c60bfa4b8cc5db6e0dd1ecd5f3dbb097edf05535..4fde99594a73634295402558104249a9eae7963e 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -20,8 +20,8 @@ Hyper Reality Visualizer - - + +
    diff --git a/frontend/src/features/roomSetup/RoomSetup.tsx b/frontend/src/features/roomSetup/RoomSetup.tsx index f637853360453d9cc2a700daf85a6bb0115e86d8..70e65806b320f52b1f1afb1b4d452e8f06295ed6 100644 --- a/frontend/src/features/roomSetup/RoomSetup.tsx +++ b/frontend/src/features/roomSetup/RoomSetup.tsx @@ -1,318 +1,318 @@ -import { useState } from "react"; -import { - X, - Camera, - Package, - Camera as CameraIcon, - UploadCloud, - Users, - ArrowRight, -} from "lucide-react"; -import { useNavigate } from "react-router-dom"; -import { categorias, habitaciones } from "../../data/roomSetupData"; -import { FilterButton, RoomCard } from "./RoomSetupComponents"; -import { useRoomSetup } from "./roomSetupHooks"; -import useAppStore from "../../store/useAppStore"; -import { useHistoryStore, type HistoryItem } from "../../store/useAppStore"; -import { useActiveSessions } from "../../hooks/useActiveSessions"; -import { API_BASE } from "../../api/client"; -import { useLoadSessionHistory, deleteSessionFromBackend } from "../../hooks/useSessionSync"; -import { useCallback } from "react"; - -export default function RoomSetup() { - useLoadSessionHistory(); - const [categoriaActiva, setCategoriaActiva] = useState("todos"); - const navigate = useNavigate(); - const segmentProgress = useAppStore((s) => s.segmentProgress); - const segmentFilename = useAppStore((s) => s.segmentFilename); - const storedPreviewImage = useAppStore((s) => s.previewImage); - const sessionHistory = useHistoryStore((s) => s.sessionHistory); - const removeFromHistory = useHistoryStore((s) => s.removeFromHistory); - const userId = useHistoryStore((s) => s.userId); - - const handleDeleteSession = useCallback( - (e: React.MouseEvent, filename: string) => { - e.stopPropagation(); - removeFromHistory(filename); - deleteSessionFromBackend(userId, filename); - }, - [removeFromHistory, userId], - ); - const { count: activeSessions } = useActiveSessions(); - const { - isDragging, - previewImage, - uploadMessage, - isUploading, - fileInputRef, - handleDragOver, - handleDragLeave, - handleDrop, - handleFileChange, - handleDemoRoomSelect, - triggerFileInput, - clearPreviewImage, - } = useRoomSetup(); - - const habitacionesFiltradas = - categoriaActiva === "todos" - ? habitaciones - : habitaciones.filter((h) => h.category === categoriaActiva); - - return ( -
    -
    - {isUploading && ( -
    -

    - Analizando imagen con IA... -

    -
    -
    -
    -

    {uploadMessage}

    -
    - )} - - {/* Badge de sesiones activas */} - {activeSessions > 0 && ( -
    -
    - - - - {activeSessions} usuario{activeSessions !== 1 ? "s" : ""} activo - {activeSessions !== 1 ? "s" : ""} - -
    -
    - )} - - {/* SECCIÓN SUPERIOR: Sube tu foto */} -
    - {/* Columna Izquierda: Textos y Botones */} -
    -

    - Ver los productos en su cuarto -

    - -
      -
    • - - Sube una foto de tu habitación -
    • -
    • - - Prueba nuestros productos -
    • -
    - -
    - - - {segmentFilename && !isUploading && ( - - )} -
    - {uploadMessage && ( -

    {uploadMessage}

    - )} -
    - - {/* Columna Derecha: Dropzone de Imagen */} -
    - {previewImage ? ( -
    - Vista previa -
    - -
    -
    - ) : ( - <> - -
    -
    - -
    -

    - {isDragging - ? "Suelta la imagen aquí" - : "Arrastra tu foto aquí"} -

    -

    - o haz clic para explorar en tu dispositivo -

    -

    - Formatos soportados: JPG, PNG -

    -
    - - )} -
    -
    - - {/* SECCIÓN HISTORIAL DE SESIÓN */} - {sessionHistory.length > 0 && ( -
    -

    - Tus espacios recientes -

    -
    - {sessionHistory.map((item: HistoryItem) => ( - - )} - {/* Hover: continuar */} -
    - - Continuar - -
    - - ))} -
    -
    - )} - - {/* SECCIÓN INFERIOR: Habitaciones de demostración */} -
    -

    - ¿No tienes una foto? Prueba nuestras habitaciones de demostración -

    - - {/* Filtros */} -
    - {categorias.map((cat) => ( - - ))} -
    - - {/* Cuadrícula de Imágenes */} -
    - {habitacionesFiltradas.map((hab) => ( - - ))} -
    -
    -
    -
    - ); -} +import { useState } from "react"; +import { + X, + Camera, + Package, + Camera as CameraIcon, + UploadCloud, + Users, + ArrowRight, +} from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { categorias, habitaciones } from "../../data/roomSetupData"; +import { FilterButton, RoomCard } from "./RoomSetupComponents"; +import { useRoomSetup } from "./roomSetupHooks"; +import useAppStore from "../../store/useAppStore"; +import { useHistoryStore, type HistoryItem } from "../../store/useAppStore"; +import { useActiveSessions } from "../../hooks/useActiveSessions"; +import { API_BASE } from "../../api/client"; +import { useLoadSessionHistory, deleteSessionFromBackend } from "../../hooks/useSessionSync"; +import { useCallback } from "react"; + +export default function RoomSetup() { + useLoadSessionHistory(); + const [categoriaActiva, setCategoriaActiva] = useState("todos"); + const navigate = useNavigate(); + const segmentProgress = useAppStore((s) => s.segmentProgress); + const segmentFilename = useAppStore((s) => s.segmentFilename); + const storedPreviewImage = useAppStore((s) => s.previewImage); + const sessionHistory = useHistoryStore((s) => s.sessionHistory); + const removeFromHistory = useHistoryStore((s) => s.removeFromHistory); + const userId = useHistoryStore((s) => s.userId); + + const handleDeleteSession = useCallback( + (e: React.MouseEvent, filename: string) => { + e.stopPropagation(); + removeFromHistory(filename); + deleteSessionFromBackend(userId, filename); + }, + [removeFromHistory, userId], + ); + const { count: activeSessions } = useActiveSessions(); + const { + isDragging, + previewImage, + uploadMessage, + isUploading, + fileInputRef, + handleDragOver, + handleDragLeave, + handleDrop, + handleFileChange, + handleDemoRoomSelect, + triggerFileInput, + clearPreviewImage, + } = useRoomSetup(); + + const habitacionesFiltradas = + categoriaActiva === "todos" + ? habitaciones + : habitaciones.filter((h) => h.category === categoriaActiva); + + return ( +
    +
    + {isUploading && ( +
    +

    + Analizando imagen con IA... +

    +
    +
    +
    +

    {uploadMessage}

    +
    + )} + + {/* Badge de sesiones activas */} + {activeSessions > 0 && ( +
    +
    + + + + {activeSessions} usuario{activeSessions !== 1 ? "s" : ""} activo + {activeSessions !== 1 ? "s" : ""} + +
    +
    + )} + + {/* SECCIÓN SUPERIOR: Sube tu foto */} +
    + {/* Columna Izquierda: Textos y Botones */} +
    +

    + Ver los productos en su cuarto +

    + +
      +
    • + + Sube una foto de tu habitación +
    • +
    • + + Prueba nuestros productos +
    • +
    + +
    + + + {segmentFilename && !isUploading && ( + + )} +
    + {uploadMessage && ( +

    {uploadMessage}

    + )} +
    + + {/* Columna Derecha: Dropzone de Imagen */} +
    + {previewImage ? ( +
    + Vista previa +
    + +
    +
    + ) : ( + <> + +
    +
    + +
    +

    + {isDragging + ? "Suelta la imagen aquí" + : "Arrastra tu foto aquí"} +

    +

    + o haz clic para explorar en tu dispositivo +

    +

    + Formatos soportados: JPG, PNG +

    +
    + + )} +
    +
    + + {/* SECCIÓN HISTORIAL DE SESIÓN */} + {sessionHistory.length > 0 && ( +
    +

    + Tus espacios recientes +

    +
    + {sessionHistory.map((item: HistoryItem) => ( + + )} + {/* Hover: continuar */} +
    + + Continuar + +
    + + ))} +
    +
    + )} + + {/* SECCIÓN INFERIOR: Habitaciones de demostración */} +
    +

    + ¿No tienes una foto? Prueba nuestras habitaciones de demostración +

    + + {/* Filtros */} +
    + {categorias.map((cat) => ( + + ))} +
    + + {/* Cuadrícula de Imágenes */} +
    + {habitacionesFiltradas.map((hab) => ( + + ))} +
    +
    +
    +
    + ); +} diff --git a/frontend/src/features/roomSetup/roomSetupHooks.ts b/frontend/src/features/roomSetup/roomSetupHooks.ts index a851d363d2997200b20da243b685025eb0cd69fb..b3fa78e09e7336275f6043fbb584ca068fec0d63 100644 --- a/frontend/src/features/roomSetup/roomSetupHooks.ts +++ b/frontend/src/features/roomSetup/roomSetupHooks.ts @@ -7,9 +7,8 @@ import { type RefObject, } from "react"; import { useNavigate } from "react-router-dom"; -import useAppStore from "../../store/useAppStore"; -import { useHistoryStore } from "../../store/useAppStore"; -import { useSegmentUpload } from "../../hooks/useSegmentUpload"; +import useAppStore, { useHistoryStore } from "../../store/useAppStore"; +import { useApplyTexture } from "../../hooks/useApplyTexture"; import { API_BASE } from "../../api/client"; import { saveSessionToBackend } from "../../hooks/useSessionSync"; @@ -40,7 +39,9 @@ export function useRoomSetup(): { const addToHistory = useHistoryStore((state) => state.addToHistory); const userId = useHistoryStore((state) => state.userId); - const { uploadAndSegment, isUploading } = useSegmentUpload(); + const setUploadedFile = useAppStore((s) => s.setUploadedFile); + const { applyTextureOpenAI } = useApplyTexture(); + const isUploading = false; const clearPreviewImage = useCallback(() => { setPreviewImage(null); @@ -53,35 +54,14 @@ export function useRoomSetup(): { // Blob URL used only for local preview while uploading; replaced with server URL after const objectUrl = URL.createObjectURL(file); setPreviewImage(objectUrl); - setUploadMessage("Iniciando segmentación..."); + setUploadMessage("Imagen lista — selecciona un producto para aplicar"); + setUploadedFile(file); try { - const { filename, maskCount } = await uploadAndSegment( - file, - (progress, message) => { - setSegmentProgress(progress); - setUploadMessage(message); - }, - ); - - setSegmentResult(filename, maskCount); - setUploadMessage(`Segmentación completa — ${maskCount} zonas detectadas`); - - // Server URL persists across page refreshes; blob URL does not - const serverImageUrl = `${API_BASE}/seg/image/${filename}`; - setPreviewImage(serverImageUrl); - - const historyItem = { - filename, - previewUrl: serverImageUrl, - maskCount, - uploadedAt: Date.now(), - }; - addToHistory(historyItem); - saveSessionToBackend(userId, historyItem); - + // Ahora el flujo es: usuario sube imagen → selecciona producto → backend /api/generate-image + // Navegamos al visualizador con la preview local y el usuario selecciona producto allí. navigate("/visualizer", { - state: { previewImage: serverImageUrl }, + state: { previewImage: objectUrl }, }); } catch (err) { const message = @@ -95,7 +75,6 @@ export function useRoomSetup(): { setUploadMessage, setSegmentResult, setSegmentProgress, - uploadAndSegment, addToHistory, userId, ], diff --git a/frontend/src/features/roomVisualizer/RoomPreviewPanel.tsx b/frontend/src/features/roomVisualizer/RoomPreviewPanel.tsx index 07231bdd9c8c8b5d55f7ddc059880954dadd7eac..a7274afc59a48c66ee166add6333d49648c0ae81 100644 --- a/frontend/src/features/roomVisualizer/RoomPreviewPanel.tsx +++ b/frontend/src/features/roomVisualizer/RoomPreviewPanel.tsx @@ -1,329 +1,338 @@ -import { - type PointerEvent, - type SyntheticEvent, - type RefObject, -} from "react"; -import { - ArrowLeft, - Share2, - Download, - MapPin, - ShoppingCart, - RefreshCw, - RotateCcw, - Paintbrush, - Loader2, -} from "lucide-react"; -import type { Product } from "../../types"; -import type { SegmentMeta } from "../../hooks/useSegmentCanvas"; - -interface RoomPreviewPanelProps { - previewImage?: string | null; - offset: { x: number; y: number }; - zoom: number; - imageSize: { width: number; height: number }; - wrapperRef: RefObject; - canvasRef: RefObject; - selectedProduct: Product | null; - selectedMasks: Set; - hoveredMask: number; - segmentMeta: Map; - isApplying: boolean; - onBack: () => void; - onPointerDown: (event: PointerEvent) => void; - onPointerMove: (event: PointerEvent) => void; - onPointerUp: (event: PointerEvent) => void; - updateImageSize: (img: HTMLImageElement) => void; - onCanvasMouseMove: (e: React.MouseEvent) => void; - onCanvasMouseLeave: () => void; - onCanvasClick: (e: React.MouseEvent) => void; - onApplyTexture: () => void; - onReset: () => void; - onDownload: () => Promise; - onShare: () => Promise; -} - -export function RoomPreviewPanel({ - previewImage, - offset, - zoom, - imageSize, - wrapperRef, - canvasRef, - selectedProduct, - selectedMasks, - hoveredMask, - segmentMeta, - isApplying, - onBack, - onPointerDown, - onPointerMove, - onPointerUp, - updateImageSize, - onCanvasMouseMove, - onCanvasMouseLeave, - onCanvasClick, - onApplyTexture, - onReset, - onDownload, - onShare, -}: RoomPreviewPanelProps) { - const canApply = selectedMasks.size > 0 && selectedProduct != null; - - const getLabel = (index: number) => - segmentMeta.get(index)?.label ?? `Zona ${index}`; - - return ( -
    -
    - - {/* ── Mobile top bar ───────────────────────────────────────── */} - {/* pr-12 reserva espacio a la derecha para el botón .hr-close del padre */} -
    - -
    - - -
    -
    - - {/* ── Desktop top bar ──────────────────────────────────────── */} -
    - - | - - - - Encuentra tu tienda - - {selectedProduct?.detailUrl ? ( - - Ir a la página del producto - - ) : ( - - )} -
    - - {/* ── Área de imagen + canvas ──────────────────────────────── */} -
    - {previewImage ? ( -
    0 - ? { width: imageSize.width, height: imageSize.height } - : {}), - }} - > - Vista previa de la habitación e.preventDefault()} - onLoad={(event: SyntheticEvent) => - updateImageSize(event.currentTarget) - } - style={{ - display: "block", - width: imageSize.width > 0 ? "100%" : "auto", - height: imageSize.height > 0 ? "100%" : "auto", - maxWidth: imageSize.width > 0 ? "none" : "100%", - maxHeight: imageSize.height > 0 ? "none" : "100%", - objectFit: "contain", - }} - /> - -
    - ) : ( -
    - No hay vista previa disponible aún. -
    - )} -
    - - {/* ── Hint de selección ────────────────────────────────────── */} - {previewImage && selectedMasks.size === 0 && ( -
    - {hoveredMask > 0 - ? `${getLabel(hoveredMask)} — haz clic para seleccionar` - : "Haz clic sobre una zona de la imagen para seleccionarla"} -
    - )} - - {/* ── Mobile bottom bar ────────────────────────────────────── */} -
    - {selectedProduct && ( -
    - {selectedProduct.name} -
    -

    {selectedProduct.brand}

    -

    {selectedProduct.name}

    -
    -
    - )} - {!selectedProduct && selectedMasks.size > 0 && ( -

    - {[...selectedMasks].map(getLabel).join(", ")} -

    - )} - {!selectedProduct && selectedMasks.size === 0 &&
    } - -
    - {canApply && ( - - )} - -
    -
    - - {/* ── Desktop bottom bar ───────────────────────────────────── */} -
    - {selectedProduct && ( -
    - {selectedProduct.name} -
    -

    {selectedProduct.brand}

    -

    - {selectedProduct.name} -

    -
    -
    - )} - - {selectedMasks.size > 0 && ( -

    - {[...selectedMasks].map(getLabel).join(", ")} -

    - )} - -
    - - {canApply && ( - - )} - - - -
    - -
    -
    - ); -} +import { + type PointerEvent, + type SyntheticEvent, + type RefObject, +} from "react"; +import { + ArrowLeft, + Share2, + Download, + MapPin, + ShoppingCart, + RefreshCw, + RotateCcw, + Paintbrush, + Loader2, +} from "lucide-react"; +import type { Product } from "../../types"; +import type { SegmentMeta } from "../../hooks/useSegmentCanvas"; + +interface RoomPreviewPanelProps { + previewImage?: string | null; + offset: { x: number; y: number }; + zoom: number; + imageSize: { width: number; height: number }; + wrapperRef: RefObject; + canvasRef: RefObject; + selectedProduct: Product | null; + selectedMasks: Set; + hoveredMask: number; + segmentMeta: Map; + isApplying: boolean; + onBack: () => void; + onPointerDown: (event: PointerEvent) => void; + onPointerMove: (event: PointerEvent) => void; + onPointerUp: (event: PointerEvent) => void; + updateImageSize: (img: HTMLImageElement) => void; + onCanvasMouseMove: (e: React.MouseEvent) => void; + onCanvasMouseLeave: () => void; + onCanvasClick: (e: React.MouseEvent) => void; + onApplyTexture: () => void; + onReset: () => void; + onDownload: () => Promise; + onShare: () => Promise; + maskCount?: number; +} + + +export function RoomPreviewPanel({ + previewImage, + offset, + zoom, + imageSize, + wrapperRef, + canvasRef, + selectedProduct, + selectedMasks, + hoveredMask, + segmentMeta, + isApplying, + onBack, + onPointerDown, + onPointerMove, + onPointerUp, + updateImageSize, + onCanvasMouseMove, + onCanvasMouseLeave, + onCanvasClick, + onApplyTexture, + onReset, + onDownload, + onShare, + maskCount = 0, +}: RoomPreviewPanelProps) { + const canApply = (selectedMasks.size > 0 || maskCount === 0) && selectedProduct != null; + + + const getLabel = (index: number) => + segmentMeta.get(index)?.label ?? `Zona ${index}`; + + return ( +
    +
    + + {/* ── Mobile top bar ───────────────────────────────────────── */} + {/* pr-12 reserva espacio a la derecha para el botón .hr-close del padre */} +
    + +
    + + +
    +
    + + {/* ── Desktop top bar ──────────────────────────────────────── */} +
    + + | + + + + Encuentra tu tienda + + {selectedProduct?.detailUrl ? ( + + Ir a la página del producto + + ) : ( + + )} +
    + + {/* ── Área de imagen + canvas ──────────────────────────────── */} +
    + {previewImage ? ( +
    0 + ? { width: imageSize.width, height: imageSize.height } + : {}), + }} + > + Vista previa de la habitación e.preventDefault()} + onLoad={(event: SyntheticEvent) => + updateImageSize(event.currentTarget) + } + style={{ + display: "block", + width: imageSize.width > 0 ? "100%" : "auto", + height: imageSize.height > 0 ? "100%" : "auto", + maxWidth: imageSize.width > 0 ? "none" : "100%", + maxHeight: imageSize.height > 0 ? "none" : "100%", + objectFit: "contain", + }} + /> + +
    + ) : ( +
    + No hay vista previa disponible aún. +
    + )} +
    + + {/* ── Hint de selección ────────────────────────────────────── */} + {previewImage && selectedMasks.size === 0 && ( +
    + {maskCount > 0 ? ( + hoveredMask > 0 + ? `${getLabel(hoveredMask)} — haz clic para seleccionar` + : "Haz clic sobre una zona de la imagen para seleccionarla" + ) : ( + "Selecciona un producto de la lista para visualizarlo con IA" + )} +
    + )} + + + {/* ── Mobile bottom bar ────────────────────────────────────── */} +
    + {selectedProduct && ( +
    + {selectedProduct.name} +
    +

    {selectedProduct.brand}

    +

    {selectedProduct.name}

    +
    +
    + )} + {!selectedProduct && selectedMasks.size > 0 && ( +

    + {[...selectedMasks].map(getLabel).join(", ")} +

    + )} + {!selectedProduct && selectedMasks.size === 0 &&
    } + +
    + {canApply && ( + + )} + +
    +
    + + {/* ── Desktop bottom bar ───────────────────────────────────── */} +
    + {selectedProduct && ( +
    + {selectedProduct.name} +
    +

    {selectedProduct.brand}

    +

    + {selectedProduct.name} +

    +
    +
    + )} + + {selectedMasks.size > 0 && ( +

    + {[...selectedMasks].map(getLabel).join(", ")} +

    + )} + +
    + + {canApply && ( + + )} + + + +
    + +
    +
    + ); +} diff --git a/frontend/src/features/roomVisualizer/RoomVisualizer.tsx b/frontend/src/features/roomVisualizer/RoomVisualizer.tsx index fa8f365fe8b33f3b1ac6c04521b1e5ded1795538..171a703ae71fbd9a20b69c4e52f0854889e4ef71 100644 --- a/frontend/src/features/roomVisualizer/RoomVisualizer.tsx +++ b/frontend/src/features/roomVisualizer/RoomVisualizer.tsx @@ -1,617 +1,933 @@ -import { - useCallback, - useEffect, - useRef, - useState, - type PointerEvent, -} from "react"; -import { ChevronDown } from "lucide-react"; -import { createRoot } from "react-dom/client"; -import { useLocation, useNavigate } from "react-router-dom"; -import { - LayoutGrid, - Search, - SlidersHorizontal, - Menu, - X, -} from "lucide-react"; -import Swal from "sweetalert2"; -import { - WhatsappShareButton, WhatsappIcon, - TelegramShareButton, TelegramIcon, - TwitterShareButton, XIcon, - FacebookShareButton, FacebookIcon, - EmailShareButton, EmailIcon, -} from "react-share"; -import { ProductGroupCard, IndividualProductCard } from "./ProductCards"; -import { useRoomVisualizer } from "./roomVisualizerHooks"; -import { useCatalogProducts } from "./useCatalogProducts"; -import { RoomPreviewPanel } from "./RoomPreviewPanel"; -import useAppStore from "../../store/useAppStore"; -import { useSegmentCanvas } from "../../hooks/useSegmentCanvas"; -import { useApplyTexture } from "../../hooks/useApplyTexture"; -import { API_BASE } from "../../api/client"; - -type RoomVisualizerState = { - previewImage?: string; - filename?: string; - maskCount?: number; -}; - -// ── Hook para detectar mobile (< 1024px) ───────────────────────────────────── -function useIsMobile() { - const [isMobile, setIsMobile] = useState(() => window.innerWidth < 1024); - useEffect(() => { - const handler = () => setIsMobile(window.innerWidth < 1024); - window.addEventListener("resize", handler); - return () => window.removeEventListener("resize", handler); - }, []); - return isMobile; -} - -export default function RoomVisualizer() { - const location = useLocation(); - const navigate = useNavigate(); - const state = location.state as RoomVisualizerState | null; - const isMobile = useIsMobile(); - - const storedPreviewImage = useAppStore((store) => store.previewImage); - const segmentFilename = useAppStore((store) => store.segmentFilename); - const accumulatedFilename = useAppStore((store) => store.accumulatedFilename); - const setAccumulatedFilename = useAppStore((store) => store.setAccumulatedFilename); - const setSegmentResult = useAppStore((store) => store.setSegmentResult); - const setPreviewImage = useAppStore((store) => store.setPreviewImage); - - // Restaurar segmentFilename y previewImage cuando se abre una sesión del historial - useEffect(() => { - if (state?.filename && state.filename !== segmentFilename) { - setSegmentResult(state.filename, state.maskCount ?? 0); - setAccumulatedFilename(null); // limpiar ediciones de sesión anterior - } - if (state?.previewImage) { - setPreviewImage(state.previewImage); - } - // Solo al montar — state no cambia tras la navegación - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const [currentPreviewImage, setCurrentPreviewImage] = useState(() => { - if (accumulatedFilename) return `${API_BASE}/seg/image/${accumulatedFilename}`; - return state?.previewImage ?? storedPreviewImage ?? null; - }); - - const [zoom, setZoom] = useState(1); - const [offset, setOffset] = useState({ x: 0, y: 0 }); - const [dragStart, setDragStart] = useState<{ x: number; y: number } | null>(null); - const [imageSize, setImageSize] = useState({ width: 0, height: 0 }); - const wrapperRef = useRef(null); - - const { products, categories, loading, error } = useCatalogProducts(); - - // null = sin interacción (primera categoría abierta por defecto) - // Set vacío = usuario cerró todo deliberadamente - const [openCategoryIds, setOpenCategoryIds] = useState | null>(null); - - const isCategoryOpen = useCallback( - (id: string) => { - if (openCategoryIds === null) { - return categories.length > 0 && id === categories[0].id; - } - return openCategoryIds.has(id); - }, - [openCategoryIds, categories], - ); - - const toggleCategory = useCallback((id: string) => { - setOpenCategoryIds((prev) => { - const base = - prev === null - ? categories.length > 0 - ? new Set([categories[0].id]) - : new Set() - : new Set(prev); - if (base.has(id)) base.delete(id); - else base.add(id); - return base; - }); - }, [categories]); - - const { - viewMode, showGrid, showList, - openProductId, handleSelectProduct, selectedProduct, - isSearchOpen, setIsSearchOpen, - searchQuery, setSearchQuery, closeSearch, - filteredProducts, chunkArray, - } = useRoomVisualizer(products); - - useEffect(() => { showList(); }, [showList]); - - - const { - canvasRef, - hoveredMask, - selectedMasks, - segmentMeta, - handleCanvasMouseMove, - handleCanvasMouseLeave, - handleCanvasClick, - clearSelection, - } = useSegmentCanvas(segmentFilename); - - const { applyTexture, isApplying, resetResult } = useApplyTexture(); - - const applyTextureWith = useCallback( - async (texturePath: string) => { - if (!segmentFilename || selectedMasks.size === 0) return; - const baseFilename = accumulatedFilename ?? segmentFilename; - try { - const data = await applyTexture(baseFilename, [...selectedMasks], texturePath, segmentFilename); - if (data?.output_url) { - setCurrentPreviewImage(`${API_BASE}${data.output_url}?t=${Date.now()}`); - setAccumulatedFilename(data.output_filename); - clearSelection(); - } - } catch { - // error ya guardado en el hook - } - }, - [applyTexture, segmentFilename, selectedMasks, clearSelection, accumulatedFilename, setAccumulatedFilename], - ); - - const handleApplyTexture = useCallback(async () => { - if (selectedProduct) await applyTextureWith(selectedProduct.ref); - }, [applyTextureWith, selectedProduct]); - - const handleProductSelect = useCallback( - async (id: string | number | null) => { - handleSelectProduct(id); - if (!id || selectedMasks.size === 0 || !segmentFilename) return; - const product = products.find((p) => p.id === id); - if (product) await applyTextureWith(product.ref); - }, - [handleSelectProduct, selectedMasks, segmentFilename, products, applyTextureWith], - ); - - const handleReset = useCallback(() => { - const original = state?.previewImage ?? storedPreviewImage ?? null; - setCurrentPreviewImage(original); - setAccumulatedFilename(null); - clearSelection(); - resetResult(); - }, [state, storedPreviewImage, setAccumulatedFilename, clearSelection, resetResult]); - - const handleDownload = useCallback(async () => { - if (!currentPreviewImage) return; - const response = await fetch(currentPreviewImage); - const blob = await response.blob(); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `hyper-reality-${Date.now()}.jpg`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - }, [currentPreviewImage]); - - const handleShare = useCallback(async (): Promise => { - let shareUrl = window.location.href; - const outputFilename = accumulatedFilename; - if (outputFilename) { - try { - const res = await fetch(`${API_BASE}/api/share`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ output_filename: outputFilename, segment_filename: segmentFilename }), - }); - if (res.ok) { - const data = (await res.json()) as { share_id: string }; - shareUrl = `${window.location.origin}/app/share/${data.share_id}`; - } - } catch { - // fallback to current URL - } - } - - const title = "My design with Hyper Reality Visualizer"; - let reactRoot: ReturnType | null = null; - - await Swal.fire({ - title: "Compartir diseño", - html: ` -
    -

    Enlace de tu diseño:

    -
    - - -
    -

    Compartir en:

    -
    -
    - `, - showConfirmButton: false, - showCloseButton: true, - width: 500, - didOpen: () => { - const copyBtn = document.getElementById("swal-copy-btn"); - copyBtn?.addEventListener("click", async () => { - await navigator.clipboard.writeText(shareUrl).catch(() => {}); - if (copyBtn) { copyBtn.textContent = "¡Copiado!"; copyBtn.style.background = "#16a34a"; } - setTimeout(() => { - if (copyBtn) { copyBtn.textContent = "Copiar"; copyBtn.style.background = "#0047AB"; } - }, 2000); - }); - const container = document.getElementById("swal-share-buttons"); - if (container) { - reactRoot = createRoot(container); - reactRoot.render( - <> - - - - - - , - ); - } - }, - willClose: () => { reactRoot?.unmount(); }, - }); - }, [segmentFilename, accumulatedFilename]); - - const clampOffset = useCallback( - (x: number, y: number, zoomValue: number) => { - const wrapper = wrapperRef.current; - if (!wrapper || imageSize.width === 0 || imageSize.height === 0) return { x, y }; - const containerRect = wrapper.getBoundingClientRect(); - const scaledWidth = imageSize.width * zoomValue; - const scaledHeight = imageSize.height * zoomValue; - const maxX = Math.max(0, (scaledWidth - containerRect.width) / 2); - const maxY = Math.max(0, (scaledHeight - containerRect.height) / 2); - return { - x: Math.max(-maxX, Math.min(maxX, x)), - y: Math.max(-maxY, Math.min(maxY, y)), - }; - }, - [imageSize], - ); - - const updateImageSize = useCallback( - (img: HTMLImageElement) => { - const wrapper = wrapperRef.current; - if (!wrapper) return; - const containerRect = wrapper.getBoundingClientRect(); - const naturalRatio = img.naturalWidth / img.naturalHeight; - const containerRatio = containerRect.width / containerRect.height; - const width = naturalRatio > containerRatio ? containerRect.width : containerRect.height * naturalRatio; - const height = naturalRatio > containerRatio ? containerRect.width / naturalRatio : containerRect.height; - setImageSize({ width, height }); - setOffset((current) => clampOffset(current.x, current.y, zoom)); - }, - [clampOffset, zoom], - ); - - const handleWheel = useCallback( - (event: WheelEvent) => { - event.preventDefault(); - setZoom((currentZoom) => { - const next = Math.min(3, Math.max(1, currentZoom - event.deltaY * 0.0015)); - setOffset((current) => clampOffset(current.x, current.y, next)); - return next; - }); - }, - [clampOffset], - ); - - useEffect(() => { - const el = wrapperRef.current; - if (!el) return; - el.addEventListener("wheel", handleWheel, { passive: false }); - return () => el.removeEventListener("wheel", handleWheel); - }, [handleWheel]); - - const handlePointerDown = useCallback((event: PointerEvent) => { - setDragStart({ x: event.clientX, y: event.clientY }); - }, []); - - const handlePointerMove = useCallback( - (event: PointerEvent) => { - if (!dragStart || zoom <= 1) return; - const dx = event.clientX - dragStart.x; - const dy = event.clientY - dragStart.y; - setOffset((current) => clampOffset(current.x + dx, current.y + dy, zoom)); - setDragStart({ x: event.clientX, y: event.clientY }); - }, - [clampOffset, dragStart, zoom], - ); - - const handlePointerUp = useCallback(() => setDragStart(null), []); - - // Props compartidos entre mobile y desktop para RoomPreviewPanel - const previewPanelProps = { - previewImage: currentPreviewImage, - offset, - zoom, - imageSize, - wrapperRef, - canvasRef, - selectedProduct, - selectedMasks, - hoveredMask, - segmentMeta, - isApplying, - onBack: () => navigate("/app"), - onPointerDown: handlePointerDown, - onPointerMove: handlePointerMove, - onPointerUp: handlePointerUp, - updateImageSize, - onCanvasMouseMove: handleCanvasMouseMove, - onCanvasMouseLeave: handleCanvasMouseLeave, - onCanvasClick: handleCanvasClick, - onApplyTexture: handleApplyTexture, - onReset: handleReset, - onDownload: handleDownload, - onShare: handleShare, - }; - - // ── Mobile strip: thumbnails + búsqueda ────────────────────────────────── - const MobileProductStrip = ( -
    - {isSearchOpen && ( -
    -
    - - setSearchQuery(e.target.value)} - style={{ - width: "100%", - paddingLeft: 34, - paddingRight: 32, - paddingTop: 8, - paddingBottom: 8, - borderRadius: 8, - border: "2px solid #0047AB", - outline: "none", - fontSize: 14, - color: "#333", - boxSizing: "border-box", - }} - /> - -
    -
    - )} - - {/* Thumbnails con scroll horizontal */} -
    - {loading ? ( -
    - Cargando... -
    - ) : ( - filteredProducts.map((product) => ( - - )) - )} -
    - - {/* Info del producto + íconos */} -
    - {selectedProduct ? ( -
    -

    - {selectedProduct.brand} -

    -

    - {selectedProduct.name} -

    -
    - ) : ( -
    - )} -
    - - -
    -
    -
    - ); - - // ── Desktop sidebar ─────────────────────────────────────────────────────── - const DesktopSidebar = ( -
    -
    -
    - - {/* Barra de herramientas */} -
    - {!isSearchOpen && ( - - )} - -
    - - -
    -
    - - {isSearchOpen && ( -
    -
    - - setSearchQuery(e.target.value)} - className="w-full pl-11 pr-10 py-3 rounded-lg border-2 border-[#0047AB] bg-white focus:outline-none transition-all text-sm text-[#333333]" - /> - -
    -
    - )} -
    - - {/* Lista de productos con acordeón por categoría */} -
    - {loading ? ( -
    Cargando productos...
    - ) : error ? ( -
    {error}
    - ) : searchQuery ? ( - /* Búsqueda activa → lista plana de resultados */ -
    - {filteredProducts.length === 0 ? ( -
    Sin resultados
    - ) : viewMode === "grid" ? ( -
    - {chunkArray(filteredProducts, 3).map((group, i) => ( - - ))} -
    - ) : ( -
    - {filteredProducts.map((product) => ( - handleProductSelect(product.id)} /> - ))} -
    - )} -
    - ) : ( - /* Sin búsqueda → acordeón por categoría */ -
    - {categories.map((cat) => { - const isOpen = isCategoryOpen(cat.id); - return ( -
    - - - {isOpen && ( -
    - {viewMode === "grid" ? ( -
    - {chunkArray(cat.products, 3).map((group, i) => ( - - ))} -
    - ) : ( -
    - {cat.products.map((product) => ( - handleProductSelect(product.id)} /> - ))} -
    - )} -
    - )} -
    - ); - })} -
    - )} -
    -
    - ); - - return ( -
    - {isMobile ? ( - // ── Layout Mobile: imagen arriba, strip de thumbnails abajo ────────── -
    -
    - -
    - {MobileProductStrip} -
    - ) : ( - // ── Layout Desktop: sidebar izquierda + imagen derecha ─────────────── -
    - {DesktopSidebar} -
    - -
    -
    - )} -
    - ); -} +import { + useCallback, + useEffect, + useRef, + useState, + type PointerEvent, +} from "react"; +import { ChevronDown } from "lucide-react"; +import { createRoot } from "react-dom/client"; +import { useLocation, useNavigate } from "react-router-dom"; +import { LayoutGrid, Search, SlidersHorizontal, Menu, X } from "lucide-react"; +import Swal from "sweetalert2"; +import { + WhatsappShareButton, + WhatsappIcon, + TelegramShareButton, + TelegramIcon, + TwitterShareButton, + XIcon, + FacebookShareButton, + FacebookIcon, + EmailShareButton, + EmailIcon, +} from "react-share"; +import { ProductGroupCard, IndividualProductCard } from "./ProductCards"; +import { useRoomVisualizer } from "./roomVisualizerHooks"; +import { useCatalogProducts } from "./useCatalogProducts"; +import { RoomPreviewPanel } from "./RoomPreviewPanel"; +import useAppStore from "../../store/useAppStore"; +// No segmentation canvas: we remove mask-based flows and use defaults +import { useApplyTexture } from "../../hooks/useApplyTexture"; +import { API_BASE } from "../../api/client"; + +type RoomVisualizerState = { + previewImage?: string; + filename?: string; + maskCount?: number; +}; + +// ── Hook para detectar mobile (< 1024px) ───────────────────────────────────── +function useIsMobile() { + const [isMobile, setIsMobile] = useState(() => window.innerWidth < 1024); + useEffect(() => { + const handler = () => setIsMobile(window.innerWidth < 1024); + window.addEventListener("resize", handler); + return () => window.removeEventListener("resize", handler); + }, []); + return isMobile; +} + +export default function RoomVisualizer() { + const location = useLocation(); + const navigate = useNavigate(); + const state = location.state as RoomVisualizerState | null; + const isMobile = useIsMobile(); + + const storedPreviewImage = useAppStore((store) => store.previewImage); + const segmentFilename = useAppStore((store) => store.segmentFilename); + const accumulatedFilename = useAppStore((store) => store.accumulatedFilename); + const setAccumulatedFilename = useAppStore( + (store) => store.setAccumulatedFilename, + ); + const setSegmentResult = useAppStore((store) => store.setSegmentResult); + const setPreviewImage = useAppStore((store) => store.setPreviewImage); + + // Restaurar segmentFilename y previewImage cuando se abre una sesión del historial + useEffect(() => { + if (state?.filename && state.filename !== segmentFilename) { + setSegmentResult(state.filename, state.maskCount ?? 0); + setAccumulatedFilename(null); // limpiar ediciones de sesión anterior + } + if (state?.previewImage) { + setPreviewImage(state.previewImage); + } + // Solo al montar — state no cambia tras la navegación + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const [currentPreviewImage, setCurrentPreviewImage] = useState( + () => { + if (accumulatedFilename) + return `${API_BASE}/seg/image/${accumulatedFilename}`; + return state?.previewImage ?? storedPreviewImage ?? null; + }, + ); + + const [zoom, setZoom] = useState(1); + const [offset, setOffset] = useState({ x: 0, y: 0 }); + const [dragStart, setDragStart] = useState<{ x: number; y: number } | null>( + null, + ); + const [imageSize, setImageSize] = useState({ width: 0, height: 0 }); + const wrapperRef = useRef(null); + + const { products, categories, loading, error } = useCatalogProducts(); + + // null = sin interacción (primera categoría abierta por defecto) + // Set vacío = usuario cerró todo deliberadamente + const [openCategoryIds, setOpenCategoryIds] = useState | null>( + null, + ); + + const isCategoryOpen = useCallback( + (id: string) => { + if (openCategoryIds === null) { + return categories.length > 0 && id === categories[0].id; + } + return openCategoryIds.has(id); + }, + [openCategoryIds, categories], + ); + + const toggleCategory = useCallback( + (id: string) => { + setOpenCategoryIds((prev) => { + const base = + prev === null + ? categories.length > 0 + ? new Set([categories[0].id]) + : new Set() + : new Set(prev); + if (base.has(id)) base.delete(id); + else base.add(id); + return base; + }); + }, + [categories], + ); + + const { + viewMode, + showGrid, + showList, + openProductId, + handleSelectProduct, + selectedProduct, + isSearchOpen, + setIsSearchOpen, + searchQuery, + setSearchQuery, + closeSearch, + filteredProducts, + chunkArray, + } = useRoomVisualizer(products); + + useEffect(() => { + showList(); + }, [showList]); + + // Masks/segmentation removed: provide safe defaults/noops so panels keep working + const canvasRef = useRef(null); + const hoveredMask = -1; + const selectedMasks = new Set(); + const segmentMeta = new Map(); + const handleCanvasMouseMove = () => {}; + const handleCanvasMouseLeave = () => {}; + const handleCanvasClick = () => {}; + + const { + applyTexture, + applyTextureAI, + applyTextureOpenAI, + isApplying, + resetResult, + } = useApplyTexture(); + const uploadedFile = useAppStore((s) => s.uploadedFile); + + const applyTextureWith = useCallback( + async (texturePath: string) => { + // Si el usuario subió una imagen en esta sesión, preferimos usar el endpoint OpenAI + if (uploadedFile) { + try { + const data = await applyTextureOpenAI(uploadedFile, texturePath); + if (data?.url) { + setCurrentPreviewImage(data.url); + } + } catch (err) { + Swal.fire( + "Error", + err instanceof Error ? err.message : "Error al procesar", + "error", + ); + } + return; + } + // Si no hay imagen subida y no hay segmentFilename, no hay flujo sin máscaras disponible + if (!segmentFilename) { + Swal.fire( + "Atención", + "Sube primero una imagen para aplicar el producto.", + "info", + ); + return; + } + const baseFilename = accumulatedFilename ?? segmentFilename; + try { + // Si no hay máscaras seleccionadas (o no existen en el flujo simplificado), usamos IA + if (selectedMasks.size === 0) { + const data = await applyTextureAI( + baseFilename, + texturePath, + segmentFilename, + ); + + let resultData = data; + // Si es un job asíncrono, poll para esperar el resultado + if (data.job_id) { + const poll = async () => { + while (true) { + const res = await fetch(`${API_BASE}/seg/jobs/${data.job_id}`); + const job = await res.json(); + if (job.status === "done") return job.result; + if (job.status === "failed") + throw new Error(job.message || "AI failed"); + await new Promise((r) => setTimeout(r, 2000)); + } + }; + resultData = await poll(); + } + + if (resultData?.url) { + setCurrentPreviewImage( + `${API_BASE}${resultData.url}?t=${Date.now()}`, + ); + setAccumulatedFilename(resultData.filename); + } + return; + } + + // Flujo tradicional (si hubiera máscaras) + const data = await applyTexture( + baseFilename, + [...selectedMasks], + texturePath, + segmentFilename, + ); + if (data?.output_url) { + setCurrentPreviewImage( + `${API_BASE}${data.output_url}?t=${Date.now()}`, + ); + setAccumulatedFilename(data.output_filename); + } + } catch (err) { + Swal.fire( + "Error", + err instanceof Error ? err.message : "Error al procesar", + "error", + ); + } + }, + [ + applyTexture, + applyTextureAI, + segmentFilename, + selectedMasks, + accumulatedFilename, + setAccumulatedFilename, + ], + ); + + const handleApplyTexture = useCallback(async () => { + if (selectedProduct) await applyTextureWith(selectedProduct.ref); + }, [applyTextureWith, selectedProduct]); + + const handleProductSelect = useCallback( + async (id: string | number | null) => { + handleSelectProduct(id); + if (!id) return; + const product = products.find((p) => p.id === id); + if (!product) return; + // If user uploaded a file, use OpenAI flow; otherwise require existing segmentFilename + if (uploadedFile) { + await applyTextureWith(product.ref); + return; + } + if (!segmentFilename) { + Swal.fire( + "Atención", + "Sube primero una imagen para aplicar el producto.", + "info", + ); + return; + } + await applyTextureWith(product.ref); + }, + [handleSelectProduct, segmentFilename, products, applyTextureWith], + ); + + const handleReset = useCallback(() => { + const original = state?.previewImage ?? storedPreviewImage ?? null; + setCurrentPreviewImage(original); + setAccumulatedFilename(null); + resetResult(); + }, [state, storedPreviewImage, setAccumulatedFilename, resetResult]); + + const handleDownload = useCallback(async () => { + if (!currentPreviewImage) return; + const response = await fetch(currentPreviewImage); + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `hyper-reality-${Date.now()}.jpg`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, [currentPreviewImage]); + + const handleShare = useCallback(async (): Promise => { + let shareUrl = window.location.href; + const outputFilename = accumulatedFilename; + if (outputFilename) { + try { + const res = await fetch(`${API_BASE}/api/share`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + output_filename: outputFilename, + segment_filename: segmentFilename, + }), + }); + if (res.ok) { + const data = (await res.json()) as { share_id: string }; + shareUrl = `${window.location.origin}/app/share/${data.share_id}`; + } + } catch { + // fallback to current URL + } + } + + const title = "My design with Hyper Reality Visualizer"; + let reactRoot: ReturnType | null = null; + + await Swal.fire({ + title: "Compartir diseño", + html: ` +
    +

    Enlace de tu diseño:

    +
    + + +
    +

    Compartir en:

    +
    +
    + `, + showConfirmButton: false, + showCloseButton: true, + width: 500, + didOpen: () => { + const copyBtn = document.getElementById("swal-copy-btn"); + copyBtn?.addEventListener("click", async () => { + await navigator.clipboard.writeText(shareUrl).catch(() => {}); + if (copyBtn) { + copyBtn.textContent = "¡Copiado!"; + copyBtn.style.background = "#16a34a"; + } + setTimeout(() => { + if (copyBtn) { + copyBtn.textContent = "Copiar"; + copyBtn.style.background = "#0047AB"; + } + }, 2000); + }); + const container = document.getElementById("swal-share-buttons"); + if (container) { + reactRoot = createRoot(container); + reactRoot.render( + <> + + + + + + + + + + + + + + + + , + ); + } + }, + willClose: () => { + reactRoot?.unmount(); + }, + }); + }, [segmentFilename, accumulatedFilename]); + + const clampOffset = useCallback( + (x: number, y: number, zoomValue: number) => { + const wrapper = wrapperRef.current; + if (!wrapper || imageSize.width === 0 || imageSize.height === 0) + return { x, y }; + const containerRect = wrapper.getBoundingClientRect(); + const scaledWidth = imageSize.width * zoomValue; + const scaledHeight = imageSize.height * zoomValue; + const maxX = Math.max(0, (scaledWidth - containerRect.width) / 2); + const maxY = Math.max(0, (scaledHeight - containerRect.height) / 2); + return { + x: Math.max(-maxX, Math.min(maxX, x)), + y: Math.max(-maxY, Math.min(maxY, y)), + }; + }, + [imageSize], + ); + + const updateImageSize = useCallback( + (img: HTMLImageElement) => { + const wrapper = wrapperRef.current; + if (!wrapper) return; + const containerRect = wrapper.getBoundingClientRect(); + const naturalRatio = img.naturalWidth / img.naturalHeight; + const containerRatio = containerRect.width / containerRect.height; + const width = + naturalRatio > containerRatio + ? containerRect.width + : containerRect.height * naturalRatio; + const height = + naturalRatio > containerRatio + ? containerRect.width / naturalRatio + : containerRect.height; + setImageSize({ width, height }); + setOffset((current) => clampOffset(current.x, current.y, zoom)); + }, + [clampOffset, zoom], + ); + + const handleWheel = useCallback( + (event: WheelEvent) => { + event.preventDefault(); + setZoom((currentZoom) => { + const next = Math.min( + 3, + Math.max(1, currentZoom - event.deltaY * 0.0015), + ); + setOffset((current) => clampOffset(current.x, current.y, next)); + return next; + }); + }, + [clampOffset], + ); + + useEffect(() => { + const el = wrapperRef.current; + if (!el) return; + el.addEventListener("wheel", handleWheel, { passive: false }); + return () => el.removeEventListener("wheel", handleWheel); + }, [handleWheel]); + + const handlePointerDown = useCallback( + (event: PointerEvent) => { + setDragStart({ x: event.clientX, y: event.clientY }); + }, + [], + ); + + const handlePointerMove = useCallback( + (event: PointerEvent) => { + if (!dragStart || zoom <= 1) return; + const dx = event.clientX - dragStart.x; + const dy = event.clientY - dragStart.y; + setOffset((current) => clampOffset(current.x + dx, current.y + dy, zoom)); + setDragStart({ x: event.clientX, y: event.clientY }); + }, + [clampOffset, dragStart, zoom], + ); + + const handlePointerUp = useCallback(() => setDragStart(null), []); + + // Props compartidos entre mobile y desktop para RoomPreviewPanel + const previewPanelProps = { + previewImage: currentPreviewImage, + offset, + zoom, + imageSize, + wrapperRef, + canvasRef, + selectedProduct, + selectedMasks, + hoveredMask, + segmentMeta, + isApplying, + onBack: () => navigate("/app"), + onPointerDown: handlePointerDown, + onPointerMove: handlePointerMove, + onPointerUp: handlePointerUp, + updateImageSize, + onCanvasMouseMove: handleCanvasMouseMove, + onCanvasMouseLeave: handleCanvasMouseLeave, + onCanvasClick: handleCanvasClick, + onApplyTexture: handleApplyTexture, + onReset: handleReset, + onDownload: handleDownload, + onShare: handleShare, + maskCount: state?.maskCount ?? 0, + }; + + // ── Mobile strip: thumbnails + búsqueda ────────────────────────────────── + const MobileProductStrip = ( +
    + {isSearchOpen && ( +
    +
    + + setSearchQuery(e.target.value)} + style={{ + width: "100%", + paddingLeft: 34, + paddingRight: 32, + paddingTop: 8, + paddingBottom: 8, + borderRadius: 8, + border: "2px solid #0047AB", + outline: "none", + fontSize: 14, + color: "#333", + boxSizing: "border-box", + }} + /> + +
    +
    + )} + + {/* Thumbnails con scroll horizontal */} +
    + {loading ? ( +
    + Cargando... +
    + ) : ( + filteredProducts.map((product) => ( + + )) + )} +
    + + {/* Info del producto + íconos */} +
    + {selectedProduct ? ( +
    +

    + {selectedProduct.brand} +

    +

    + {selectedProduct.name} +

    +
    + ) : ( +
    + )} +
    + + +
    +
    +
    + ); + + // ── Desktop sidebar ─────────────────────────────────────────────────────── + const DesktopSidebar = ( +
    +
    +
    + + {/* Barra de herramientas */} +
    + {!isSearchOpen && ( + + )} + +
    + + +
    +
    + + {isSearchOpen && ( +
    +
    + + setSearchQuery(e.target.value)} + className="w-full pl-11 pr-10 py-3 rounded-lg border-2 border-[#0047AB] bg-white focus:outline-none transition-all text-sm text-[#333333]" + /> + +
    +
    + )} +
    + + {/* Lista de productos con acordeón por categoría */} +
    + {loading ? ( +
    + Cargando productos... +
    + ) : error ? ( +
    + {error} +
    + ) : searchQuery ? ( + /* Búsqueda activa → lista plana de resultados */ +
    + {filteredProducts.length === 0 ? ( +
    + Sin resultados +
    + ) : viewMode === "grid" ? ( +
    + {chunkArray(filteredProducts, 3).map((group, i) => ( + + ))} +
    + ) : ( +
    + {filteredProducts.map((product) => ( + handleProductSelect(product.id)} + /> + ))} +
    + )} +
    + ) : ( + /* Sin búsqueda → acordeón por categoría */ +
    + {categories.map((cat) => { + const isOpen = isCategoryOpen(cat.id); + return ( +
    + + + {isOpen && ( +
    + {viewMode === "grid" ? ( +
    + {chunkArray(cat.products, 3).map((group, i) => ( + + ))} +
    + ) : ( +
    + {cat.products.map((product) => ( + handleProductSelect(product.id)} + /> + ))} +
    + )} +
    + )} +
    + ); + })} +
    + )} +
    +
    + ); + + return ( +
    + {isMobile ? ( + // ── Layout Mobile: imagen arriba, strip de thumbnails abajo ────────── +
    +
    + +
    + {MobileProductStrip} +
    + ) : ( + // ── Layout Desktop: sidebar izquierda + imagen derecha ─────────────── +
    + {DesktopSidebar} +
    + +
    +
    + )} +
    + ); +} diff --git a/frontend/src/features/roomVisualizer/useCatalogProducts.ts b/frontend/src/features/roomVisualizer/useCatalogProducts.ts index 37dd7008e8bb9da7653168f2606e1d1e27bdfb60..6a539ca74e1f5a448d15d8539078bd58184df50c 100644 --- a/frontend/src/features/roomVisualizer/useCatalogProducts.ts +++ b/frontend/src/features/roomVisualizer/useCatalogProducts.ts @@ -1,97 +1,97 @@ -import { useEffect, useState } from "react"; -import type { Product } from "../../types"; - -const API_BASE = import.meta.env.VITE_API_URL ?? ""; - -interface CatalogProduct { - id: string; - nombre: string; - textura: string; - url_preview: string; - dimensiones: string[]; -} - -interface CatalogCategory { - id: string; - nombre: string; - tipo?: string; - descripcion: string; - especificaciones?: string[]; - url_detalle?: string; - productos: CatalogProduct[]; -} - -interface CatalogResponse { - categories: CatalogCategory[]; -} - -function mapToProduct(item: CatalogProduct, category: CatalogCategory): Product { - return { - id: item.id, - brand: category.nombre, - name: item.nombre, - ref: item.textura, - size: item.dimensiones.length > 0 ? item.dimensiones.join(" / ") : "—", - image: `${API_BASE}${item.url_preview}`, - description: category.especificaciones, - detailUrl: category.url_detalle, - tipo: (category.tipo as "suelos" | "paredes") ?? undefined, - categoria: category.id, - }; -} - -export interface ProductCategory { - id: string; - nombre: string; - tipo?: string; - descripcion: string; - especificaciones?: string[]; - products: Product[]; -} - -export function useCatalogProducts() { - const [products, setProducts] = useState([]); - const [categories, setCategories] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - let cancelled = false; - - async function fetchCatalog() { - try { - const res = await fetch(`${API_BASE}/api/catalog/textures`); - if (!res.ok) throw new Error(`Error ${res.status}`); - const data: CatalogResponse = await res.json(); - - const cats: ProductCategory[] = data.categories.map((category) => ({ - id: category.id, - nombre: category.nombre, - tipo: category.tipo, - descripcion: category.descripcion, - especificaciones: category.especificaciones, - products: category.productos.map((item) => mapToProduct(item, category)), - })); - - const flat = cats.flatMap((c) => c.products); - - if (!cancelled) { - setCategories(cats); - setProducts(flat); - setError(null); - } - } catch (err) { - if (!cancelled) { - setError(err instanceof Error ? err.message : "Error al cargar productos"); - } - } finally { - if (!cancelled) setLoading(false); - } - } - - fetchCatalog(); - return () => { cancelled = true; }; - }, []); - - return { products, categories, loading, error }; -} +import { useEffect, useState } from "react"; +import type { Product } from "../../types"; + +const API_BASE = import.meta.env.VITE_API_URL ?? ""; + +interface CatalogProduct { + id: string; + nombre: string; + textura: string; + url_preview: string; + dimensiones: string[]; +} + +interface CatalogCategory { + id: string; + nombre: string; + tipo?: string; + descripcion: string; + especificaciones?: string[]; + url_detalle?: string; + productos: CatalogProduct[]; +} + +interface CatalogResponse { + categories: CatalogCategory[]; +} + +function mapToProduct(item: CatalogProduct, category: CatalogCategory): Product { + return { + id: item.id, + brand: category.nombre, + name: item.nombre, + ref: item.textura, + size: item.dimensiones.length > 0 ? item.dimensiones.join(" / ") : "—", + image: `${API_BASE}${item.url_preview}`, + description: category.especificaciones, + detailUrl: category.url_detalle, + tipo: (category.tipo as "suelos" | "paredes") ?? undefined, + categoria: category.id, + }; +} + +export interface ProductCategory { + id: string; + nombre: string; + tipo?: string; + descripcion: string; + especificaciones?: string[]; + products: Product[]; +} + +export function useCatalogProducts() { + const [products, setProducts] = useState([]); + const [categories, setCategories] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + + async function fetchCatalog() { + try { + const res = await fetch(`${API_BASE}/api/catalog/textures`); + if (!res.ok) throw new Error(`Error ${res.status}`); + const data: CatalogResponse = await res.json(); + + const cats: ProductCategory[] = data.categories.map((category) => ({ + id: category.id, + nombre: category.nombre, + tipo: category.tipo, + descripcion: category.descripcion, + especificaciones: category.especificaciones, + products: category.productos.map((item) => mapToProduct(item, category)), + })); + + const flat = cats.flatMap((c) => c.products); + + if (!cancelled) { + setCategories(cats); + setProducts(flat); + setError(null); + } + } catch (err) { + if (!cancelled) { + setError(err instanceof Error ? err.message : "Error al cargar productos"); + } + } finally { + if (!cancelled) setLoading(false); + } + } + + fetchCatalog(); + return () => { cancelled = true; }; + }, []); + + return { products, categories, loading, error }; +} diff --git a/frontend/src/hooks/useApplyTexture.ts b/frontend/src/hooks/useApplyTexture.ts index c2b5f3813bffe2655a3db0844c7dd21abe044749..8791b451bfbcc02e32a0d256219797d6a18a953b 100644 --- a/frontend/src/hooks/useApplyTexture.ts +++ b/frontend/src/hooks/useApplyTexture.ts @@ -1,57 +1,178 @@ -import { useCallback, useState } from "react"; -import { API_BASE } from "../api/client"; - -interface ApplyTextureResult { - output_url: string; - output_filename: string; -} - -export function useApplyTexture() { - const [isApplying, setIsApplying] = useState(false); - const [resultUrl, setResultUrl] = useState(null); - const [error, setError] = useState(null); - - const applyTexture = useCallback( - async (filename: string, maskIndices: number[], textureName: string, originalFilename?: string) => { - setIsApplying(true); - setError(null); - try { - const res = await fetch(`${API_BASE}/seg/apply_texture`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - filename, - mask_indices: maskIndices, - texture_name: textureName, - original_filename: originalFilename ?? filename, - direction_mode: "auto", - angle_degrees: 0, - }), - }); - - if (!res.ok) { - const text = await res.text(); - throw new Error(text || `Error ${res.status}`); - } - - const data: ApplyTextureResult = await res.json(); - setResultUrl(`${API_BASE}${data.output_url}`); - return data; - } catch (err) { - const message = err instanceof Error ? err.message : "Error al aplicar textura"; - setError(message); - throw new Error(message); - } finally { - setIsApplying(false); - } - }, - [], - ); - - const resetResult = useCallback(() => { - setResultUrl(null); - setError(null); - }, []); - - return { applyTexture, isApplying, resultUrl, error, resetResult }; -} +import { useCallback, useState } from "react"; +import { API_BASE } from "../api/client"; + +interface ApplyTextureResult { + output_url: string; + output_filename: string; +} + +export function useApplyTexture() { + const [isApplying, setIsApplying] = useState(false); + const [resultUrl, setResultUrl] = useState(null); + const [error, setError] = useState(null); + + const applyTexture = useCallback( + async ( + filename: string, + maskIndices: number[], + textureName: string, + originalFilename?: string, + ) => { + setIsApplying(true); + setError(null); + try { + const res = await fetch(`${API_BASE}/seg/apply_texture`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + filename, + mask_indices: maskIndices, + texture_name: textureName, + original_filename: originalFilename ?? filename, + direction_mode: "auto", + angle_degrees: 0, + }), + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(text || `Error ${res.status}`); + } + + const data: ApplyTextureResult = await res.json(); + setResultUrl(`${API_BASE}${data.output_url}`); + return data; + } catch (err) { + const message = + err instanceof Error ? err.message : "Error al aplicar textura"; + setError(message); + throw new Error(message); + } finally { + setIsApplying(false); + } + }, + [], + ); + + const applyTextureAI = useCallback( + async ( + filename: string, + textureName: string, + originalFilename?: string, + ) => { + setIsApplying(true); + setError(null); + try { + const res = await fetch(`${API_BASE}/seg/apply_texture_ai`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + filename, + texture_name: textureName, + original_filename: originalFilename ?? filename, + prompt: "", // El backend generará uno por defecto basado en texture_name + }), + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(text || `Error ${res.status}`); + } + + const data = await res.json(); + + // Si es asíncrono, necesitamos esperar/poll. + // Pero por ahora asumiremos que devuelve el resultado o un job_id. + if (data.job_id) { + // Poll the job status until it's done or failed, then return result + const jobId = data.job_id as string; + const poll = async () => { + const start = Date.now(); + const timeoutMs = 3 * 60 * 1000; // 3 minutes max + while (Date.now() - start < timeoutMs) { + const res = await fetch(`${API_BASE}/seg/jobs/${jobId}`); + if (!res.ok) throw new Error(`Job status error ${res.status}`); + const job = await res.json(); + if (job.status === "done") return job.result; + if (job.status === "failed") + throw new Error(job.message || "AI job failed"); + await new Promise((r) => setTimeout(r, 2000)); + } + throw new Error("Timed out waiting for AI job"); + }; + + const result = await poll(); + if (result?.url) { + setResultUrl(`${API_BASE}${result.url}`); + } + return result; + } + + setResultUrl(`${API_BASE}${data.url}`); + return data; + } catch (err) { + const message = + err instanceof Error + ? err.message + : "Error al aplicar textura con IA"; + setError(message); + throw new Error(message); + } finally { + setIsApplying(false); + } + }, + [], + ); + + const applyTextureOpenAI = useCallback( + async (file: File, textureName: string, apiKey?: string | null) => { + setIsApplying(true); + setError(null); + try { + const form = new FormData(); + form.append("file", file); + form.append("texture", textureName); + if (apiKey) form.append("api_key", apiKey); + + const res = await fetch(`${API_BASE}/api/generate-image`, { + method: "POST", + body: form, + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(text || `Error ${res.status}`); + } + + const data = await res.json(); + if (!data.result_b64) throw new Error("No result returned from server"); + const dataUrl = `data:image/png;base64,${data.result_b64}`; + setResultUrl(dataUrl); + return { url: dataUrl, message: data.message }; + } catch (err) { + const message = + err instanceof Error ? err.message : "Error al generar imagen con IA"; + setError(message); + throw new Error(message); + } finally { + setIsApplying(false); + } + }, + [], + ); + + const resetResult = useCallback(() => { + setResultUrl(null); + setError(null); + }, []); + + return { + applyTexture, + applyTextureAI, + applyTextureOpenAI, + isApplying, + resultUrl, + error, + resetResult, + }; +} diff --git a/frontend/src/hooks/useSegmentUpload.ts b/frontend/src/hooks/useSegmentUpload.ts index 1077bb3941d83d287c2ddd6fc085d5e8be49bdce..ffa4cc49cdd2b5c67eab36aaca2d818b2a984ac4 100644 --- a/frontend/src/hooks/useSegmentUpload.ts +++ b/frontend/src/hooks/useSegmentUpload.ts @@ -1,81 +1,81 @@ -import { useCallback, useState } from "react"; -import { API_BASE } from "../api/client"; - -interface JobStatus { - status: "processing" | "done" | "failed" | "timeout"; - progress?: number; - stage?: string; - eta_seconds?: number; - filename?: string; - mask_count?: number; - message?: string; -} - -async function pollJob( - jobId: string, - onProgress: (progress: number, message: string) => void, - intervalMs = 2000, -): Promise<{ filename: string; mask_count: number }> { - while (true) { - const res = await fetch(`${API_BASE}/seg/jobs/${jobId}`); - if (!res.ok) throw new Error(`Error al consultar el job: ${res.status}`); - const job: JobStatus = await res.json(); - - if (job.status === "done" && job.filename && job.mask_count != null) { - return { filename: job.filename, mask_count: job.mask_count }; - } - if (job.status === "failed" || job.status === "timeout") { - throw new Error(job.message ?? "La segmentación falló"); - } - - onProgress(job.progress ?? 0, job.message ?? "Procesando..."); - await new Promise((r) => setTimeout(r, intervalMs)); - } -} - -export function useSegmentUpload() { - const [isUploading, setIsUploading] = useState(false); - const [error, setError] = useState(null); - - const uploadAndSegment = useCallback( - async ( - file: File, - onProgress: (progress: number, message: string) => void, - ): Promise<{ filename: string; maskCount: number }> => { - setIsUploading(true); - setError(null); - - try { - const form = new FormData(); - form.append("file", file); - - onProgress(2, "Subiendo imagen..."); - const res = await fetch(`${API_BASE}/seg/upload_async`, { - method: "POST", - body: form, - }); - - if (!res.ok) { - const text = await res.text(); - throw new Error(text || `Error al subir imagen: ${res.status}`); - } - - const { job_id } = await res.json(); - onProgress(10, "Imagen recibida. Iniciando segmentación..."); - - const result = await pollJob(job_id, onProgress); - return { filename: result.filename, maskCount: result.mask_count }; - } catch (err) { - const message = - err instanceof Error ? err.message : "Error en la segmentación"; - setError(message); - throw new Error(message); - } finally { - setIsUploading(false); - } - }, - [], - ); - - return { uploadAndSegment, isUploading, error }; -} +import { useCallback, useState } from "react"; +import { API_BASE } from "../api/client"; + +interface JobStatus { + status: "processing" | "done" | "failed" | "timeout"; + progress?: number; + stage?: string; + eta_seconds?: number; + filename?: string; + mask_count?: number; + message?: string; +} + +async function pollJob( + jobId: string, + onProgress: (progress: number, message: string) => void, + intervalMs = 2000, +): Promise<{ filename: string; mask_count: number }> { + while (true) { + const res = await fetch(`${API_BASE}/seg/jobs/${jobId}`); + if (!res.ok) throw new Error(`Error al consultar el job: ${res.status}`); + const job: JobStatus = await res.json(); + + if (job.status === "done" && job.filename && job.mask_count != null) { + return { filename: job.filename, mask_count: job.mask_count }; + } + if (job.status === "failed" || job.status === "timeout") { + throw new Error(job.message ?? "La segmentación falló"); + } + + onProgress(job.progress ?? 0, job.message ?? "Procesando..."); + await new Promise((r) => setTimeout(r, intervalMs)); + } +} + +export function useSegmentUpload() { + const [isUploading, setIsUploading] = useState(false); + const [error, setError] = useState(null); + + const uploadAndSegment = useCallback( + async ( + file: File, + onProgress: (progress: number, message: string) => void, + ): Promise<{ filename: string; maskCount: number }> => { + setIsUploading(true); + setError(null); + + try { + const form = new FormData(); + form.append("file", file); + + onProgress(2, "Subiendo imagen..."); + const res = await fetch(`${API_BASE}/seg/upload_async`, { + method: "POST", + body: form, + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(text || `Error al subir imagen: ${res.status}`); + } + + const { job_id } = await res.json(); + onProgress(10, "Imagen recibida. Iniciando segmentación..."); + + const result = await pollJob(job_id, onProgress); + return { filename: result.filename, maskCount: result.mask_count }; + } catch (err) { + const message = + err instanceof Error ? err.message : "Error en la segmentación"; + setError(message); + throw new Error(message); + } finally { + setIsUploading(false); + } + }, + [], + ); + + return { uploadAndSegment, isUploading, error }; +} diff --git a/frontend/src/store/useAppStore.ts b/frontend/src/store/useAppStore.ts index 406374bd20dfc66f7bd3285d53cf5b6043bd75cc..1c8fd27395ee7011c28a5972c0d3fd68aeac0b76 100644 --- a/frontend/src/store/useAppStore.ts +++ b/frontend/src/store/useAppStore.ts @@ -15,9 +15,10 @@ function getOrCreateUserId(): string { const key = "hr-user-id"; let id = localStorage.getItem(key); if (!id) { - id = typeof crypto !== "undefined" && crypto.randomUUID - ? crypto.randomUUID() - : `u-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; + id = + typeof crypto !== "undefined" && crypto.randomUUID + ? crypto.randomUUID() + : `u-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; localStorage.setItem(key, id); } return id; @@ -49,7 +50,9 @@ export const useHistoryStore = create()( setSessionHistory: (items) => set({ sessionHistory: items }), removeFromHistory: (filename) => set((state) => ({ - sessionHistory: state.sessionHistory.filter((h) => h.filename !== filename), + sessionHistory: state.sessionHistory.filter( + (h) => h.filename !== filename, + ), })), }), { @@ -63,6 +66,7 @@ export const useHistoryStore = create()( type AppStore = { previewImage: string | null; uploadMessage: string | null; + uploadedFile: File | null; openProductId: string | number | null; viewMode: ViewMode; segmentFilename: string | null; @@ -71,6 +75,7 @@ type AppStore = { accumulatedFilename: string | null; setPreviewImage: (previewImage: string | null) => void; setUploadMessage: (uploadMessage: string | null) => void; + setUploadedFile: (f: File | null) => void; setOpenProductId: (openProductId: string | number | null) => void; setViewMode: (viewMode: ViewMode) => void; setSegmentResult: (filename: string, maskCount: number) => void; @@ -84,6 +89,7 @@ const useAppStore = create()( (set) => ({ previewImage: null, uploadMessage: null, + uploadedFile: null, openProductId: null, viewMode: "list", segmentFilename: null, @@ -92,16 +98,19 @@ const useAppStore = create()( accumulatedFilename: null, setPreviewImage: (previewImage) => set({ previewImage }), setUploadMessage: (uploadMessage) => set({ uploadMessage }), + setUploadedFile: (uploadedFile) => set({ uploadedFile }), setOpenProductId: (openProductId) => set({ openProductId }), setViewMode: (viewMode) => set({ viewMode }), setSegmentResult: (segmentFilename, maskCount) => set({ segmentFilename, maskCount }), setSegmentProgress: (segmentProgress) => set({ segmentProgress }), - setAccumulatedFilename: (accumulatedFilename) => set({ accumulatedFilename }), + setAccumulatedFilename: (accumulatedFilename) => + set({ accumulatedFilename }), reset: () => set({ previewImage: null, uploadMessage: null, + uploadedFile: null, openProductId: null, viewMode: "list", segmentFilename: null, diff --git a/frontend/src/version.ts b/frontend/src/version.ts index be5b82823f0cd085838f97f7061e2c6b0b742b5c..ecb7d67590e1d5c7199b7ad491010f890801ee17 100644 --- a/frontend/src/version.ts +++ b/frontend/src/version.ts @@ -1 +1 @@ -export const appVersion = "0.1.0-dev.20260511T131335"; +export const appVersion = "0.1.0-dev.20260511T231034";