| """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) |
|
|
| |
| 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 |
|
|