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