| from __future__ import annotations | |
| from pathlib import Path | |
| from typing import List, Tuple | |
| import time, random | |
| import numpy as np | |
| from PIL import Image, ImageFilter, ImageOps | |
| TMP_DIR = Path("/tmp/bgfx"); TMP_DIR.mkdir(parents=True, exist_ok=True) | |
| _PALETTES = { | |
| "office": [(240,245,250),(210,220,230),(180,190,200)], | |
| "studio": [(18,18,20),(32,32,36),(58,60,64)], | |
| "sunset": [(255,183,77),(255,138,101),(244,143,177)], | |
| "forest": [(46,125,50),(102,187,106),(165,214,167)], | |
| "ocean": [(33,150,243),(3,169,244),(0,188,212)], | |
| "minimal": [(245,246,248),(230,232,236),(214,218,224)], | |
| "warm": [(255,224,178),(255,204,128),(255,171,145)], | |
| "cool": [(197,202,233),(179,229,252),(178,235,242)], | |
| "royal": [(63,81,181),(121,134,203),(159,168,218)], | |
| } | |
| def _save_pil(img: Image.Image, stem: str = "ai_bg", ext: str = "png") -> str: | |
| ts = int(time.time() * 1000) | |
| p = TMP_DIR / f"{stem}_{ts}.{ext}" | |
| img.save(p) | |
| return str(p) | |
| def _palette_from_prompt(prompt: str) -> List[tuple]: | |
| p = (prompt or "").lower() | |
| for key, pal in _PALETTES.items(): | |
| if key in p: | |
| return pal | |
| random.seed(hash(p) % (2**32 - 1)) | |
| return [tuple(random.randint(90, 200) for _ in range(3)) for _ in range(3)] | |
| def _perlin_like_noise(h: int, w: int, octaves: int = 4) -> np.ndarray: | |
| acc = np.zeros((h, w), dtype=np.float32) | |
| for o in range(octaves): | |
| scale = 2 ** o | |
| small = np.random.rand(h // scale + 1, w // scale + 1).astype(np.float32) | |
| small = Image.fromarray((small * 255).astype(np.uint8)).resize((w, h), Image.BILINEAR) | |
| acc += np.array(small, dtype=np.float32) / 255.0 / (o + 1) | |
| acc /= max(1e-6, acc.max()) | |
| return acc | |
| def _blend_palette(noise: np.ndarray, palette: List[tuple]) -> Image.Image: | |
| h, w = noise.shape | |
| img = np.zeros((h, w, 3), dtype=np.float32) | |
| t1, t2 = 0.33, 0.66 | |
| c0, c1, c2 = [np.array(c, dtype=np.float32) for c in palette] | |
| m0, m1, m2 = noise < t1, (noise >= t1) & (noise < t2), noise >= t2 | |
| img[m0], img[m1], img[m2] = c0, c1, c2 | |
| return Image.fromarray(np.clip(img, 0, 255).astype(np.uint8)) | |
| def generate_ai_background( | |
| prompt: str, width: int = 1280, height: int = 720, | |
| bokeh: float = 0.0, vignette: float = 0.15, contrast: float = 1.05 | |
| ) -> Tuple[Image.Image, str]: | |
| palette = _palette_from_prompt(prompt) | |
| noise = _perlin_like_noise(height, width, octaves=4) | |
| img = _blend_palette(noise, palette) | |
| if bokeh > 0: | |
| img = img.filter(ImageFilter.GaussianBlur(radius=max(0, min(50, bokeh)))) | |
| if vignette > 0: | |
| import numpy as np | |
| base = np.array(img).astype(np.float32) / 255.0 | |
| y, x = np.ogrid[:height, :width] | |
| cx, cy = width / 2, height / 2 | |
| r = np.sqrt((x - cx) ** 2 + (y - cy) ** 2) | |
| mask = 1 - np.clip(r / (max(width, height) / 1.2), 0, 1) | |
| mask = (mask ** 2) * (1 - vignette) + vignette | |
| out = base * mask[..., None] | |
| img = Image.fromarray(np.clip(out * 255, 0, 255).astype(np.uint8)) | |
| if contrast != 1.0: | |
| img = ImageOps.autocontrast(img, cutoff=1) | |
| arr = np.array(img).astype(np.float32) | |
| mean = arr.mean(axis=(0, 1), keepdims=True) | |
| arr = (arr - mean) * float(contrast) + mean | |
| img = Image.fromarray(np.clip(arr, 0, 255).astype(np.uint8)) | |
| path = _save_pil(img) | |
| return img, path | |