"""Central registry for image assets and Gradio-version-safe asset URLs. Gradio 5+ serves user files under ``/gradio_api/file=``; Gradio 4 used ``/file=``. We build the prefix dynamically so the same code works on the local install (6.x) and on older Hugging Face Space runtimes. """ from __future__ import annotations from pathlib import Path import gradio as gr ASSET_DIR = Path(__file__).resolve().parent.parent / "assets" _GRADIO_MAJOR = int(gr.__version__.split(".")[0]) _FILE_PREFIX = "/gradio_api/file=" if _GRADIO_MAJOR >= 5 else "/file=" def file_url(relative_path: str) -> str: """Return a servable URL for a file inside ``assets/``.""" return f"{_FILE_PREFIX}assets/{relative_path}" def file_url_from_path(absolute_path: str) -> str: """Convert an absolute file path to a Gradio-servable URL.""" if not absolute_path: return "" return f"{_FILE_PREFIX}{absolute_path.lstrip('/')}" def allowed_paths() -> list[str]: return [str(ASSET_DIR), str(ASSET_DIR / "generated")] # --- Image library ------------------------------------------------------- # Bespoke art generated for each sacred location, matching its specific lore # in a single consistent painterly style (see scripts that produced loc_*.jpg). LOCATION_IMAGES = { "library": "loc_library.jpg", # infinite shelves, self-writing books "sea": "loc_sea.jpg", # silver sea of drifting name-lanterns "clock-forest": "loc_clock.jpg", # gear-trees with clock-face leaves "moon-market": "loc_market.jpg", # lantern bazaar under a crescent moon "valley": "loc_valley.jpg", # sleeping giants forming the hills "crossroads": "loc_crossroads.jpg", # the eternal bonfire at the center "mirror-bogs": "loc_bogs.jpg", # still black water, wrong reflections "hollow-mountain": "loc_mountain.jpg", # vast hollow ancient stone interior } BANNER = "banner.png" def location_image_url(slug: str | None) -> str | None: if not slug: return None name = LOCATION_IMAGES.get(slug) return file_url(name) if name else None def banner_url() -> str: return file_url(BANNER) # --- Character sprites --------------------------------------------------- # Bespoke, transparent-cutout art generated for each named seed soul (see # scripts/cutout_sprites.py). These are the "souls" that wander the 3D # diorama and appear as portraits — purpose-painted for this world, not the # stock reference plates that used to stand in for them. NAMED_SPRITES = { "Crystal Tree": "char_crystal_tree.png", "Joke Dragon": "char_joke_dragon.png", "Blind Cartographer": "char_blind_cartographer.png", "Fog Merchant": "char_fog_merchant.png", "Lantern Fox": "char_lantern_fox.png", "Thirteen Sighs": "char_thirteen_sighs.png", "Silent Gear": "char_silent_gear.png", "Disagreeing Reflection": "char_disagreeing_reflection.png", "Echo Counter": "char_echo_counter.png", "Unfed Bonfire": "char_unfed_bonfire.png", } # Fallback pools for visitor-summoned souls, keyed by entity type. We reuse # the bespoke cutouts so every soul — seeded or summoned — looks hand-made. TYPE_SPRITES: dict[str, list[str]] = { "character": [ "char_blind_cartographer.png", "char_echo_counter.png", "char_fog_merchant.png", "char_disagreeing_reflection.png", ], "creature": [ "char_lantern_fox.png", "char_joke_dragon.png", "char_crystal_tree.png", ], "object": [ "char_silent_gear.png", "char_thirteen_sighs.png", ], "place": [ "char_unfed_bonfire.png", "char_silent_gear.png", ], } _FALLBACK_SPRITE = "char_blind_cartographer.png" def sprite_filename(entity: dict) -> str: """Stable sprite filename for an entity (named art, else type pool).""" name = (entity.get("display_name") or "").strip() if name in NAMED_SPRITES and (ASSET_DIR / NAMED_SPRITES[name]).exists(): return NAMED_SPRITES[name] etype = entity.get("type", "character") pool = TYPE_SPRITES.get(etype, TYPE_SPRITES["character"]) uid = entity.get("id") or name or "x" chosen = pool[sum(ord(c) for c in uid) % len(pool)] if (ASSET_DIR / chosen).exists(): return chosen return _FALLBACK_SPRITE def sprite_url(entity: dict) -> str: """Served URL for a diorama billboard sprite (HTML ).""" return file_url(sprite_filename(entity)) def sprite_path(entity: dict) -> str: """Filesystem path for a sprite (gr.Gallery needs paths, not URLs).""" return str(ASSET_DIR / sprite_filename(entity))