daydream / agents /vision.py
kaiser-data's picture
Deploy DAYDREAM β€” Thousand Token Wood (small-model dream fleet)
125f9c0 verified
Raw
History Blame Contribute Delete
3.83 kB
"""Dream-image painter β€” turns one beat into a picture via the FLUX Modal endpoint.
Kept out of the engine on purpose: the engine owns narrative + game math, the app
owns presentation, and a picture is presentation. The app fires `dream_image()` in
a thread the moment a beat's outcome is known, so the image paints *under* the
Dreamweaver's narration and lands by the time the prose finishes.
Determinism stays honest: the picture is seeded by (dream seed, turn), so the same
shared seed replays the same images alongside the same dice.
Offline: with DAYDREAM_MOCK=1 (or no MODAL_FLUX_URL set) this returns a tinted
placeholder so the whole UI is playable with no GPU backend.
"""
from __future__ import annotations
import io
import os
import sys
import pathlib
import hashlib
import httpx
from .world import ENVIRONMENTS
# Derive the FLUX URL from MODAL_WORKSPACE when not set explicitly (see endpoints.py).
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
from endpoints import derive as _derive_urls # noqa: E402
_WS = os.environ.get("MODAL_WORKSPACE", "").strip()
FLUX_URL = (os.environ.get("MODAL_FLUX_URL", "").strip()
or (_derive_urls(_WS)["flux"] if _WS else ""))
FLUX_KEY = os.environ.get("MODAL_FLUX_API_KEY", "local-dev-key")
MOCK = bool(os.environ.get("DAYDREAM_MOCK")) or not FLUX_URL
TIMEOUT = float(os.environ.get("DAYDREAM_IMG_TIMEOUT", "60"))
# A beat's verdict tints the whole frame β€” the same lever the Nightmare uses in prose.
OUTCOME_TINT = {
"success": "luminous hopeful light, vivid and triumphant",
"partial": "uncertain wavering half-light, shifting and unresolved",
"fail": "ominous encroaching shadows, the nightmare creeping in, cold dread",
}
STYLE = "dreamlike, painterly, cinematic lighting, highly detailed, surreal, no text"
def _seed_int(seed: str, turn: int) -> int:
"""Stable per-(seed, turn) image seed β€” mirrors resolver._rng's contract."""
return int(hashlib.sha256(f"{seed}:{turn}".encode()).hexdigest(), 16) % (2**32)
def build_prompt(state, intent: str, outcome) -> str:
"""World art-direction + what the dreamer did + the verdict's tint."""
env = ENVIRONMENTS.get(state.env_id)
base = (env.visual if env and env.visual else f"a surreal dream of {state.env_name}")
tint = OUTCOME_TINT.get(getattr(outcome, "result", None), "dawning first light, arrival")
deed = (intent or "").strip()[:160]
return f"{base}. {deed}. {tint}. {STYLE}"
def _placeholder(state):
"""A tinted gradient stand-in so the UI works with no backend (mock mode)."""
try:
from PIL import Image
except Exception:
return None
tints = {"candy_desert": (60, 40, 70), "sunken_city": (12, 40, 60),
"noir_alley": (20, 16, 40), "red_planet": (70, 30, 24),
"token_wood": (28, 40, 30)}
top = tints.get(getattr(state, "env_id", ""), (40, 30, 70))
h = w = 256
img = Image.new("RGB", (w, h))
px = img.load()
for y in range(h): # simple vertical gradient -> dark
f = y / h
row = tuple(int(c * (1 - 0.7 * f)) for c in top)
for x in range(w):
px[x, y] = row
return img
def dream_image(state, intent: str, outcome):
"""Paint this beat. Returns a PIL image, or None on any failure (UI tolerates None)."""
if MOCK:
return _placeholder(state)
payload = {"prompt": build_prompt(state, intent, outcome),
"key": FLUX_KEY,
"seed": _seed_int(state.seed, state.turn)}
try:
r = httpx.post(FLUX_URL, json=payload, timeout=TIMEOUT)
r.raise_for_status()
from PIL import Image
return Image.open(io.BytesIO(r.content)).convert("RGB")
except Exception:
return None # never break a turn for a missing picture