"""Soul portrait generation via HuggingFace Inference API (FLUX.1-schnell).""" from __future__ import annotations import os from pathlib import Path PORTRAITS_DIR = Path(__file__).parent.parent / "assets" / "generated" def _portraits_dir() -> Path: PORTRAITS_DIR.mkdir(parents=True, exist_ok=True) return PORTRAITS_DIR def _build_prompt(entity: dict) -> str: """Build a vivid image prompt from entity data.""" appearance = (entity.get("appearance") or "")[:220] etype = entity.get("type", "character") traits = entity.get("personality_traits") or [] trait_str = ", ".join(t for t in traits[:2]) if traits else "" type_style = { "character": "fantasy portrait, close-up, mysterious figure", "creature": "fantasy creature, full body, magical beast", "object": "magical enchanted artifact, studio lighting, detailed", "place": "mystical location concept art, atmospheric, wide view", }.get(etype, "fantasy entity") prompt = ( f"{appearance}. {type_style}. {trait_str}. " "Dark fantasy art style, painterly, dramatic atmospheric lighting, " "highly detailed, moody, cinematic, oil painting, intricate details, " "no text, no watermark, masterpiece quality" ) return prompt[:500] def generate_soul_portrait(entity: dict) -> str | None: """ Generate a portrait image for the given entity using HF Inference API. Returns the local file path (string) on success, or None on failure/skip. Silently skips if HF_TOKEN is not set. """ import requests hf_token = os.environ.get("HF_TOKEN") if not hf_token: return None entity_id = entity.get("id") if not entity_id: return None out_path = _portraits_dir() / f"{entity_id}.jpg" if out_path.exists(): return str(out_path) prompt = _build_prompt(entity) # Try FLUX.1-schnell first (best quality, still fast) models = [ "black-forest-labs/FLUX.1-schnell", "stabilityai/sdxl-turbo", ] for model in models: try: resp = requests.post( f"https://api-inference.huggingface.co/models/{model}", headers={"Authorization": f"Bearer {hf_token}"}, json={ "inputs": prompt, "parameters": { "num_inference_steps": 4, "width": 512, "height": 512, "guidance_scale": 0.0, }, }, timeout=45, ) if resp.status_code == 200 and resp.content: out_path.write_bytes(resp.content) return str(out_path) except Exception: continue return None def portrait_url_for(entity: dict) -> str | None: """Return the Gradio-servable URL for a portrait if it exists on disk.""" entity_id = entity.get("id") if not entity_id: return None p = PORTRAITS_DIR / f"{entity_id}.jpg" return str(p) if p.exists() else None