Jordancole21's picture
Deploy native Gradio SEO page and tool
5c1dcf0 verified
Raw
History Blame Contribute Delete
4.5 kB
"""AI art generator: prompt assembly, generation, file handling."""
import html
import os
import random
import tempfile
from pathlib import Path
from .shared import image_client
from .shared.ai_client import AIUnavailable
from .shared.ratelimit import RateLimiter
# Images cost real money per generation (~$0.05-0.09 on krea-2-large), so this
# Space runs tighter limits than the text tools. Overridable via Space vars.
limiter = RateLimiter(
per_ip_limit=int(os.environ.get("LF_IP_LIMIT", "5")),
window_seconds=int(os.environ.get("LF_IP_WINDOW_SECONDS", "3600")),
daily_cap=int(os.environ.get("LF_DAILY_CAP", "200")),
)
RACES = [
"Human", "Elf", "Half-Elf", "Dwarf", "Halfling", "Dragonborn", "Tiefling",
"Half-Orc", "Orc", "Gnome", "Goliath", "Aasimar", "Tabaxi", "Kenku", "Firbolg",
]
CLASSES = [
"Fighter", "Wizard", "Rogue", "Cleric", "Ranger", "Paladin", "Barbarian",
"Bard", "Druid", "Monk", "Sorcerer", "Warlock", "Artificer",
]
STYLES = {
"Classic fantasy oil painting":
"classic high-fantasy oil painting, rich color, dramatic lighting, detailed brushwork",
"Character-sheet portrait":
"clean character-sheet portrait illustration, neutral parchment background, crisp linework, soft studio lighting",
"Dark gritty fantasy":
"dark gritty fantasy illustration, moody chiaroscuro lighting, weathered textures, muted palette",
"Painterly watercolor":
"loose painterly watercolor illustration, soft washes, ink linework, white paper background",
"Comic / animated":
"stylized comic-book illustration, bold outlines, cel shading, expressive pose",
"Isometric token art":
"top-down VTT token art, centered figure, circular composition, high contrast against plain background",
"Pencil sketch":
"detailed graphite pencil sketch, cross-hatching, sketchbook style, monochrome",
}
FRAMINGS = {
"Portrait (head & shoulders)": "head-and-shoulders portrait, face in sharp focus",
"Half body": "half-body composition from the waist up, hands visible",
"Full body": "full-body character illustration, complete costume and gear visible",
"Round token": "circular VTT token crop, figure centered and facing forward",
}
MAX_DESC = 600
MAX_FIELD = 120
PROMPT_SUFFIX = (
"tasteful fantasy character artwork, high detail, no text, no watermark, "
"no signature, no frame"
)
def randomize():
return (
random.choice(RACES),
random.choice(CLASSES),
random.choice(list(STYLES)),
random.choice(list(FRAMINGS)),
)
def _clip(value: str, limit: int) -> str:
return (value or "").strip()[:limit]
def build_prompt(description, race, char_class, style, framing, palette) -> str:
description = _clip(description, MAX_DESC)
race = _clip(race, MAX_FIELD)
char_class = _clip(char_class, MAX_FIELD)
palette = _clip(palette, MAX_FIELD)
style_text = STYLES.get(style, STYLES["Classic fantasy oil painting"])
framing_text = FRAMINGS.get(framing, FRAMINGS["Portrait (head & shoulders)"])
subject_bits = [b for b in (race, char_class) if b]
subject = " ".join(subject_bits) if subject_bits else "adventurer"
if description:
subject = f"{subject}: {description}"
else:
subject = f"a heroic {subject} with a distinctive, memorable look"
parts = [
f"Dungeons and Dragons character art of a {subject}",
framing_text,
style_text,
]
if palette:
parts.append(f"color palette: {palette}")
parts.append(PROMPT_SUFFIX)
return ". ".join(parts)
def generate(description, race, char_class, style, framing, palette, request=None):
"""Returns (image_path, download_path, error_message)."""
allowed, message = limiter.check(request)
if not allowed:
return None, None, message
prompt = build_prompt(description, race, char_class, style, framing, palette)
try:
image_bytes, media_type = image_client.generate_image(prompt)
except AIUnavailable as exc:
return None, None, str(exc)
ext = {"image/png": "png", "image/jpeg": "jpg", "image/webp": "webp"}.get(media_type, "png")
path = Path(tempfile.mkdtemp()) / f"dnd-character-art.{ext}"
path.write_bytes(image_bytes)
return str(path), str(path), None
def render_error(message: str) -> str:
return (
'<div class="lf-output" role="alert" style="border-color:#A7343B;">'
f"<p>{html.escape(message)}</p></div>"
)