multi-agent-lab / src /media /stub_image.py
agharsallah
feat(media): introduce MediaRouter and stubs for image and speech generation
8400d8c
Raw
History Blame Contribute Delete
3.4 kB
"""Offline image stub — a deterministic, procedurally-drawn forest scene.
Mirrors :class:`~src.models.provider.DeterministicTinyModel`: hash the prompt, draw
something charming and byte-identical, no network. This is what keeps the no-key demo
*delightful* (a real little Thousand Token Wood vista) rather than a grey placeholder.
Pure Pillow + stdlib, in-process, cross-platform. Same prompt → same PNG bytes.
"""
from __future__ import annotations
import colorsys
import hashlib
from io import BytesIO
_W, _H = 448, 256
class _Bytes:
"""A tiny deterministic source of ints/units, cycling a SHA-256 digest."""
def __init__(self, seed: str) -> None:
self._d = hashlib.sha256(seed.encode("utf-8")).digest()
self._i = 0
def _next(self) -> int:
b = self._d[self._i % len(self._d)]
self._i += 1
return b
def unit(self) -> float:
return self._next() / 255.0
def span(self, lo: int, hi: int) -> int:
if hi <= lo:
return lo
r = (self._next() << 8) | self._next()
return lo + r * (hi - lo) // 65535
def _hsl(h: float, s: float, lightness: float) -> tuple[int, int, int]:
r, g, b = colorsys.hls_to_rgb(h, lightness, s)
return (int(r * 255), int(g * 255), int(b * 255))
def _lerp(a: tuple[int, int, int], b: tuple[int, int, int], t: float) -> tuple[int, int, int]:
return (int(a[0] + (b[0] - a[0]) * t), int(a[1] + (b[1] - a[1]) * t), int(a[2] + (b[2] - a[2]) * t))
def draw_forest(prompt: str, style: str | None = None) -> bytes:
"""Render a deterministic phosphor-toned forest scene as PNG bytes."""
from PIL import Image, ImageDraw
rnd = _Bytes(f"{prompt}|{style or ''}")
img = Image.new("RGB", (_W, _H))
draw = ImageDraw.Draw(img)
# Sky: a vertical gradient between two seeded twilight hues.
top = _hsl(rnd.unit(), 0.55, 0.16)
bottom = _hsl(rnd.unit(), 0.5, 0.34)
for y in range(_H):
draw.line([(0, y), (_W, y)], fill=_lerp(top, bottom, y / (_H - 1)))
# A moon, sometimes.
if rnd._next() % 2:
mx, my, r = rnd.span(40, _W - 40), rnd.span(22, 84), rnd.span(13, 26)
draw.ellipse([mx - r, my - r, mx + r, my + r], fill=(244, 243, 222))
# Three layers of trees, back (dark) to front (lighter), rooted near the horizon.
horizon = int(_H * 0.6)
for layer in range(3):
shade = 16 + layer * 9
for _ in range(rnd.span(5, 9)):
x = rnd.span(0, _W)
base = horizon + layer * 17 + rnd.span(0, 10)
tw, th = rnd.span(13, 30), rnd.span(38, 92)
draw.polygon([(x, base - th), (x - tw, base), (x + tw, base)], fill=(shade, shade + 20, shade + 8))
draw.rectangle([x - 3, base, x + 3, base + 9], fill=(40, 28, 20))
# Fireflies above the canopy.
for _ in range(rnd.span(18, 38)):
fx, fy = rnd.span(0, _W), rnd.span(0, horizon)
draw.ellipse([fx - 1, fy - 1, fx + 1, fy + 1], fill=(190, 240, 120))
# A caption strip carrying a slice of the prompt (Pillow's bundled bitmap font —
# always present, so rendering is deterministic across machines).
caption = " ".join((prompt or "").split())[:54]
draw.rectangle([0, _H - 22, _W, _H], fill=(5, 18, 25))
draw.text((8, _H - 16), caption, fill=(160, 230, 215))
buf = BytesIO()
img.save(buf, format="PNG")
return buf.getvalue()