Spaces:
Runtime error
Runtime error
Update forge-texture from local forge-bricks (with vendored common)
Browse files- app.py +16 -5
- common/README.md +10 -0
- common/__init__.py +9 -0
- common/health.py +74 -0
- common/manifest.py +70 -0
- common/pyproject.toml +16 -0
app.py
CHANGED
|
@@ -14,14 +14,25 @@ import gradio as gr
|
|
| 14 |
import spaces
|
| 15 |
from PIL import Image
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
try:
|
| 18 |
-
from ..common.manifest import create_manifest
|
| 19 |
-
from ..common.health import SpaceHealth
|
| 20 |
-
except Exception:
|
| 21 |
-
import sys
|
| 22 |
-
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 23 |
from common.manifest import create_manifest
|
| 24 |
from common.health import SpaceHealth
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
health = SpaceHealth()
|
| 27 |
|
|
|
|
| 14 |
import spaces
|
| 15 |
from PIL import Image
|
| 16 |
|
| 17 |
+
# Robust import for local workspace + HF Space deployment (common is vendored on push)
|
| 18 |
+
import sys
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
|
| 21 |
+
HERE = Path(__file__).resolve().parent
|
| 22 |
+
for candidate in (HERE.parent, HERE):
|
| 23 |
+
if (candidate / "common" / "manifest.py").exists():
|
| 24 |
+
sys.path.insert(0, str(candidate))
|
| 25 |
+
break
|
| 26 |
+
|
| 27 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
from common.manifest import create_manifest
|
| 29 |
from common.health import SpaceHealth
|
| 30 |
+
except ImportError as e:
|
| 31 |
+
raise ImportError(
|
| 32 |
+
"Failed to import shared 'common' package (manifest.py / health.py).\n"
|
| 33 |
+
"For local development: run ./install.sh from the forge-bricks/ root.\n"
|
| 34 |
+
"For HF Spaces: re-run scripts/push_to_hf.py so it vendors common/."
|
| 35 |
+
) from e
|
| 36 |
|
| 37 |
health = SpaceHealth()
|
| 38 |
|
common/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# forge-bricks-common
|
| 2 |
+
|
| 3 |
+
Shared utilities used by all Forge Bricks:
|
| 4 |
+
|
| 5 |
+
- `manifest.py` — `AssetManifest` + `create_manifest()` (lineage, provenance, commercial license flags, game-asset friendly metadata)
|
| 6 |
+
- `health.py` — `SpaceHealth` background monitor (adapted from 3d-creator-suite) for robust fallback behavior when calling other HF Spaces
|
| 7 |
+
|
| 8 |
+
This package is installed in editable mode when you run the root `install.sh`.
|
| 9 |
+
|
| 10 |
+
It is a dependency of the individual bricks via their `pyproject.toml` workspace configuration.
|
common/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Common utilities for Forge Bricks (HF Gradio ZeroGPU Spaces).
|
| 2 |
+
|
| 3 |
+
Reusable: manifest schema, health monitoring (adapted from 3d-creator-suite),
|
| 4 |
+
asset metadata helpers, lineage tracking, local fallback support.
|
| 5 |
+
"""
|
| 6 |
+
from .manifest import AssetManifest, create_manifest
|
| 7 |
+
from .health import SpaceHealth
|
| 8 |
+
|
| 9 |
+
__all__ = ["AssetManifest", "create_manifest", "SpaceHealth"]
|
common/health.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""SpaceHealth monitor (thread-safe periodic checks + cache).
|
| 2 |
+
|
| 3 |
+
Directly adapted from 3d-creator-suite/src/cloud/health.py for robustness.
|
| 4 |
+
Use inside bricks and daggr clients to avoid calling dead/sleeping providers.
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
import logging
|
| 8 |
+
import threading
|
| 9 |
+
import time
|
| 10 |
+
from typing import Any, Optional
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class SpaceHealth:
|
| 16 |
+
"""Thread-safe health status cache for HF Spaces (or any gradio endpoint)."""
|
| 17 |
+
|
| 18 |
+
def __init__(self, check_interval: int = 120):
|
| 19 |
+
self._interval = check_interval
|
| 20 |
+
self._lock = threading.Lock()
|
| 21 |
+
self._status: dict[str, dict[str, Any]] = {}
|
| 22 |
+
self._thread: threading.Thread | None = None
|
| 23 |
+
self._running = False
|
| 24 |
+
|
| 25 |
+
def start(self, space_ids: list[str]) -> None:
|
| 26 |
+
self._running = True
|
| 27 |
+
self._thread = threading.Thread(
|
| 28 |
+
target=self._check_loop, args=(space_ids,), daemon=True
|
| 29 |
+
)
|
| 30 |
+
self._thread.start()
|
| 31 |
+
logger.info(f"Health monitor started for {len(space_ids)} targets")
|
| 32 |
+
|
| 33 |
+
def stop(self) -> None:
|
| 34 |
+
self._running = False
|
| 35 |
+
if self._thread:
|
| 36 |
+
self._thread.join(timeout=5)
|
| 37 |
+
|
| 38 |
+
def is_ok(self, space_id: str) -> Optional[bool]:
|
| 39 |
+
with self._lock:
|
| 40 |
+
return self._status.get(space_id, {}).get("ok")
|
| 41 |
+
|
| 42 |
+
def get_all(self) -> dict[str, dict[str, Any]]:
|
| 43 |
+
with self._lock:
|
| 44 |
+
return dict(self._status)
|
| 45 |
+
|
| 46 |
+
def _check_loop(self, space_ids: list[str]) -> None:
|
| 47 |
+
while self._running:
|
| 48 |
+
for sid in space_ids:
|
| 49 |
+
self._check_one(sid)
|
| 50 |
+
time.sleep(self._interval)
|
| 51 |
+
|
| 52 |
+
def _check_one(self, space_id: str) -> None:
|
| 53 |
+
try:
|
| 54 |
+
from gradio_client import Client
|
| 55 |
+
start = time.monotonic()
|
| 56 |
+
client = Client(space_id, timeout=10)
|
| 57 |
+
latency = time.monotonic() - start
|
| 58 |
+
del client
|
| 59 |
+
with self._lock:
|
| 60 |
+
self._status[space_id] = {
|
| 61 |
+
"ok": True,
|
| 62 |
+
"latency": round(latency, 2),
|
| 63 |
+
"last_check": time.time(),
|
| 64 |
+
"error": None,
|
| 65 |
+
}
|
| 66 |
+
except Exception as e:
|
| 67 |
+
with self._lock:
|
| 68 |
+
self._status[space_id] = {
|
| 69 |
+
"ok": False,
|
| 70 |
+
"latency": None,
|
| 71 |
+
"last_check": time.time(),
|
| 72 |
+
"error": str(e)[:200],
|
| 73 |
+
}
|
| 74 |
+
logger.debug(f"Health check failed for {space_id}: {e}")
|
common/manifest.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Asset manifest + lineage for Forge Bricks outputs.
|
| 2 |
+
|
| 3 |
+
Based on patterns from daggr-pipelines packaging + aetherium-hub asset_registry + ForgeDNA descriptors.
|
| 4 |
+
Every brick output includes one of these for traceability, editing lineage, license, and editor integration.
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
import json
|
| 8 |
+
from dataclasses import dataclass, field, asdict
|
| 9 |
+
from datetime import datetime, timezone
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Any, Optional
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass
|
| 15 |
+
class AssetManifest:
|
| 16 |
+
"""Standard output manifest for any generated/edited game asset brick."""
|
| 17 |
+
id: str
|
| 18 |
+
name: str
|
| 19 |
+
type: str # e.g. "concept_image", "sprite", "model_3d", "texture_pbr", "audio_sfx", "voice_clip", "rig", "anim_clip", "shader"
|
| 20 |
+
version: int = 1
|
| 21 |
+
parent_id: Optional[str] = None # For edits/refines (lineage)
|
| 22 |
+
source_brick: str = ""
|
| 23 |
+
prompt_or_desc: str = ""
|
| 24 |
+
params: dict[str, Any] = field(default_factory=dict) # seed, strength, steps, voice_description, etc.
|
| 25 |
+
files: dict[str, str] = field(default_factory=dict) # role -> path (e.g. "sprite": "xxx.png", "glb": "...")
|
| 26 |
+
metadata: dict[str, Any] = field(default_factory=dict) # tags, lod, commercial_ok, model, timings, etc.
|
| 27 |
+
created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
| 28 |
+
license_note: str = ""
|
| 29 |
+
commercial_ok: bool = False
|
| 30 |
+
|
| 31 |
+
def to_dict(self) -> dict:
|
| 32 |
+
return asdict(self)
|
| 33 |
+
|
| 34 |
+
def save(self, path: str | Path) -> None:
|
| 35 |
+
Path(path).write_text(json.dumps(self.to_dict(), indent=2))
|
| 36 |
+
|
| 37 |
+
@classmethod
|
| 38 |
+
def load(cls, path: str | Path) -> "AssetManifest":
|
| 39 |
+
data = json.loads(Path(path).read_text())
|
| 40 |
+
return cls(**data)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def create_manifest(
|
| 44 |
+
*,
|
| 45 |
+
name: str,
|
| 46 |
+
type: str,
|
| 47 |
+
source_brick: str,
|
| 48 |
+
prompt_or_desc: str = "",
|
| 49 |
+
files: dict[str, str] | None = None,
|
| 50 |
+
params: dict[str, Any] | None = None,
|
| 51 |
+
metadata: dict[str, Any] | None = None,
|
| 52 |
+
parent_id: str | None = None,
|
| 53 |
+
license_note: str = "Check brick README for model licenses",
|
| 54 |
+
commercial_ok: bool = False,
|
| 55 |
+
) -> AssetManifest:
|
| 56 |
+
"""Helper to create a properly populated manifest (used by all bricks)."""
|
| 57 |
+
mid = f"{source_brick}_{int(datetime.now(timezone.utc).timestamp() * 1000)}"
|
| 58 |
+
return AssetManifest(
|
| 59 |
+
id=mid,
|
| 60 |
+
name=name,
|
| 61 |
+
type=type,
|
| 62 |
+
source_brick=source_brick,
|
| 63 |
+
prompt_or_desc=prompt_or_desc,
|
| 64 |
+
parent_id=parent_id,
|
| 65 |
+
files=files or {},
|
| 66 |
+
params=params or {},
|
| 67 |
+
metadata=metadata or {},
|
| 68 |
+
license_note=license_note,
|
| 69 |
+
commercial_ok=commercial_ok,
|
| 70 |
+
)
|
common/pyproject.toml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "forge-bricks-common"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Shared utilities for Forge Bricks (manifests, health checks, etc.)"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.10"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"pillow>=10.0",
|
| 9 |
+
]
|
| 10 |
+
|
| 11 |
+
[build-system]
|
| 12 |
+
requires = ["hatchling"]
|
| 13 |
+
build-backend = "hatchling.build"
|
| 14 |
+
|
| 15 |
+
[tool.hatch.build.targets.wheel]
|
| 16 |
+
packages = ["."]
|