thangvip's picture
docs(codex): publish Build Small submission package (#4)
b535bf5
Raw
History Blame Contribute Delete
15.1 kB
from __future__ import annotations
import base64
import hashlib
import hmac
import io
import random
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Protocol
import httpx
from huggingface_hub import InferenceClient
from PIL import Image, ImageDraw, ImageFilter, ImageOps
from compliment_forest.schema import ForestStyle
@dataclass(frozen=True)
class StyleProfile:
label: str
trigger: str
prefix: str
suffix: str
negative_prompt: str
_COMMON_NEGATIVE = (
"text, letters, watermark, logo, frame, collage, duplicate animal, extra limbs, "
"deformed face, distorted hands, photorealistic stock photo, 3d render"
)
STYLE_PROFILES: dict[str, StyleProfile] = {
"watercolor": StyleProfile(
label="Watercolor Storybook",
trigger="cmprst_watercolor",
prefix=(
"cmprst_watercolor, soft watercolor storybook illustration, loose wet-on-wet "
"washes, visible cold-press paper grain, feathered edges, "
),
suffix=", warm dappled light, muted natural palette, generous negative space",
negative_prompt=f"hard outlines, neon, oversaturated, {_COMMON_NEGATIVE}",
),
"paper_cut": StyleProfile(
label="Layered Paper Cut",
trigger="cmprst_papercut",
prefix=(
"cmprst_papercut, handcrafted layered paper-cut illustration, deckled cut "
"edges, overlapping botanical silhouettes, tactile paper fibers, "
),
suffix=", soft dimensional shadows, moss and ochre palette, clean composition",
negative_prompt=f"paint splashes, glossy plastic, neon, {_COMMON_NEGATIVE}",
),
"moonlit_gouache": StyleProfile(
label="Moonlit Gouache",
trigger="cmprst_moonlit",
prefix=(
"cmprst_moonlit, moonlit gouache storybook painting, opaque velvety brushwork, "
"deep indigo woodland, silver rim light, "
),
suffix=", quiet luminous atmosphere, restrained jewel tones, simple composition",
negative_prompt=f"daylight, washed-out contrast, digital gradients, {_COMMON_NEGATIVE}",
),
"botanical_ink": StyleProfile(
label="Botanical Ink Wash",
trigger="cmprst_inkwash",
prefix=(
"cmprst_inkwash, botanical ink-wash illustration, expressive sepia and moss "
"brush lines, diluted ink blooms, cream washi paper, "
),
suffix=", sparse wildflower details, calm asymmetry, airy negative space",
negative_prompt=f"thick cartoon outlines, saturated blocks, neon, {_COMMON_NEGATIVE}",
),
}
STYLE_LORA_V1_IDS = {
"watercolor": "thangvip/compliment-forest-watercolor-flux-lora",
"paper_cut": "thangvip/compliment-forest-paper-cut-flux-lora",
"moonlit_gouache": "thangvip/compliment-forest-moonlit-gouache-flux-lora",
"botanical_ink": "thangvip/compliment-forest-botanical-ink-flux-lora",
}
STYLE_LORA_IDS = {
style: f"{model_id}-v2"
for style, model_id in STYLE_LORA_V1_IDS.items()
}
STYLE_ADAPTER_WEIGHT = 0.45
STYLE_PREFIX = (
"cmprst_forest, cmprst_watercolor, soft watercolor storybook illustration, "
"loose wet-on-wet washes, "
"visible cold-press paper grain, no hard outlines, soft feathered edges, "
)
STYLE_SUFFIX = (
", warm dappled light, kind expression, lots of negative space, muted warm palette"
)
NEGATIVE_PROMPT = (
"hard outlines, photorealistic, 3d render, neon, oversaturated, busy background, "
"text, watermark, deformed"
)
class ImageBackend(Protocol):
def generate(self, prompt: str, seed: int, style: ForestStyle) -> str: ...
def resolve_style(style: ForestStyle, seed: int) -> str:
if style != "surprise":
return style
style_ids = tuple(STYLE_PROFILES)
return style_ids[seed % len(style_ids)]
def compose_flux_prompt(
creature_prompt: str,
style: ForestStyle = "watercolor",
*,
seed: int = 3407,
) -> str:
resolved = resolve_style(style, seed)
profile = STYLE_PROFILES[resolved]
subject = creature_prompt.strip()
style_language = profile.prefix.removeprefix(f"{profile.trigger}, ").rstrip(", ")
return (
f"{profile.trigger}, primary subject: {subject}. "
"Preserve this exact subject, action, and setting as the clear focal point. "
"No text, lettering, signature, logo, or watermark anywhere. "
f"{style_language}{profile.suffix}"
)
def image_to_data_uri(image: Image.Image) -> str:
buffer = io.BytesIO()
image.save(buffer, format="PNG", optimize=True)
encoded = base64.b64encode(buffer.getvalue()).decode("ascii")
return f"data:image/png;base64,{encoded}"
def clean_generated_image(image: Image.Image) -> Image.Image:
"""Trim common synthetic signature zones while preserving output dimensions."""
width, height = image.size
crop = image.crop(
(
round(width * 0.035),
round(height * 0.02),
round(width * 0.965),
round(height * 0.935),
)
)
return ImageOps.fit(
crop,
image.size,
method=Image.Resampling.LANCZOS,
)
def sign_modal_request(key: str, prompt: str, seed: int, style: str) -> str:
message = f"{prompt}\n{seed}\n{style}".encode()
return hmac.new(key.encode(), message, hashlib.sha256).hexdigest()
class DemoImageBackend:
"""Creates deterministic procedural placeholders for offline development."""
def __init__(
self,
width: int = 768,
height: int = 576,
asset_dir: str | Path | None = None,
) -> None:
self.width = width
self.height = height
self.asset_dir = Path(asset_dir) if asset_dir is not None else None
def generate(
self,
prompt: str,
seed: int,
style: ForestStyle = "surprise",
) -> str:
resolved_style = resolve_style(style, seed)
prompt_seed = int.from_bytes(
hashlib.sha256(f"{resolved_style}:{prompt}".encode()).digest()[:4],
byteorder="big",
)
rng = random.Random(seed ^ prompt_seed)
image = Image.new("RGB", (self.width, self.height), "#f5f0df")
wash = Image.new("RGBA", image.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(wash, "RGBA")
palette = [
(124, 145, 112, 28),
(77, 105, 78, 24),
(208, 151, 139, 22),
(225, 184, 101, 22),
(132, 172, 181, 18),
]
if resolved_style == "moonlit_gouache":
palette = [(36, 49, 92, 42), (70, 82, 124, 36), (194, 184, 151, 26)]
elif resolved_style == "paper_cut":
palette = [(83, 104, 61, 40), (205, 151, 96, 34), (201, 143, 131, 28)]
elif resolved_style == "botanical_ink":
palette = [(73, 79, 55, 32), (120, 96, 67, 28), (148, 161, 132, 20)]
for _ in range(100):
color = rng.choice(palette)
x = rng.randint(-100, self.width)
y = rng.randint(-80, self.height)
radius_x = rng.randint(35, 160)
radius_y = rng.randint(25, 110)
draw.ellipse(
(x - radius_x, y - radius_y, x + radius_x, y + radius_y),
fill=color,
)
wash = wash.filter(ImageFilter.GaussianBlur(radius=22))
image = Image.alpha_composite(image.convert("RGBA"), wash)
creature = Image.new("RGBA", image.size, (0, 0, 0, 0))
creature_draw = ImageDraw.Draw(creature, "RGBA")
center_x = self.width // 2 + rng.randint(-30, 30)
ground_y = int(self.height * 0.72)
body_color = rng.choice(
[(169, 105, 69, 205), (128, 105, 78, 205), (154, 135, 96, 205)]
)
shadow = (76, 91, 67, 35)
creature_draw.ellipse(
(center_x - 145, ground_y - 10, center_x + 145, ground_y + 38),
fill=shadow,
)
creature_draw.ellipse(
(center_x - 96, ground_y - 170, center_x + 90, ground_y + 5),
fill=body_color,
)
creature_draw.ellipse(
(center_x - 72, ground_y - 245, center_x + 74, ground_y - 100),
fill=body_color,
)
creature_draw.polygon(
[
(center_x - 62, ground_y - 210),
(center_x - 88, ground_y - 290),
(center_x - 20, ground_y - 235),
],
fill=body_color,
)
creature_draw.polygon(
[
(center_x + 58, ground_y - 210),
(center_x + 88, ground_y - 290),
(center_x + 20, ground_y - 235),
],
fill=body_color,
)
creature_draw.ellipse(
(center_x - 38, ground_y - 195, center_x - 15, ground_y - 169),
fill=(51, 53, 44, 220),
)
creature_draw.ellipse(
(center_x + 18, ground_y - 195, center_x + 41, ground_y - 169),
fill=(51, 53, 44, 220),
)
creature_draw.ellipse(
(center_x - 6, ground_y - 163, center_x + 9, ground_y - 150),
fill=(72, 57, 47, 190),
)
creature = creature.filter(ImageFilter.GaussianBlur(radius=1.2))
image = Image.alpha_composite(image, creature)
grain = Image.new("RGBA", image.size, (0, 0, 0, 0))
grain_pixels = grain.load()
for y in range(self.height):
for x in range(self.width):
value = rng.randint(0, 10)
grain_pixels[x, y] = (75, 66, 50, value)
image = Image.alpha_composite(image, grain)
return image_to_data_uri(image.convert("RGB"))
class FluxImageBackend:
"""Lazy FLUX.1-dev pipeline that loads a local or Hub LoRA."""
def __init__(
self,
model_id: str = "black-forest-labs/FLUX.1-dev",
lora_id: str = "build-small-hackathon/compliment-forest-flux-lora",
*,
local_files_only: bool = False,
width: int = 768,
height: int = 768,
steps: int = 28,
) -> None:
self.model_id = model_id
self.lora_id = lora_id
self.local_files_only = local_files_only
self.width = width
self.height = height
self.steps = steps
self._pipeline = None
def _load(self):
if self._pipeline is not None:
return self._pipeline
import torch
from diffusers import FluxPipeline
pipeline = FluxPipeline.from_pretrained(
self.model_id,
torch_dtype=torch.bfloat16,
local_files_only=self.local_files_only,
)
pipeline.load_lora_weights(
self.lora_id,
local_files_only=self.local_files_only,
)
pipeline.enable_model_cpu_offload()
self._pipeline = pipeline
return pipeline
def generate(
self,
prompt: str,
seed: int,
style: ForestStyle = "watercolor",
) -> str:
import torch
pipeline = self._load()
generator = torch.Generator(device="cpu").manual_seed(seed)
image = pipeline(
prompt=compose_flux_prompt(prompt, style, seed=seed),
negative_prompt=NEGATIVE_PROMPT,
width=self.width,
height=self.height,
num_inference_steps=self.steps,
guidance_scale=3.5,
generator=generator,
).images[0]
return image_to_data_uri(clean_generated_image(image.convert("RGB")))
class HfInferenceImageBackend:
"""Fresh FLUX.1-schnell generation through Hugging Face Inference Providers."""
def __init__(
self,
model: str = "black-forest-labs/FLUX.1-schnell",
*,
client: Any | None = None,
provider: str = "auto",
width: int = 768,
height: int = 768,
steps: int = 4,
timeout: float = 180,
) -> None:
self.model = model
self.client = client or InferenceClient(provider=provider, timeout=timeout)
self.width = width
self.height = height
self.steps = steps
def generate(
self,
prompt: str,
seed: int,
style: ForestStyle = "surprise",
) -> str:
resolved = resolve_style(style, seed)
profile = STYLE_PROFILES[resolved]
image = self.client.text_to_image(
compose_flux_prompt(prompt, resolved, seed=seed),
model=self.model,
negative_prompt=profile.negative_prompt,
width=self.width,
height=self.height,
num_inference_steps=self.steps,
guidance_scale=0.0,
seed=seed,
)
return image_to_data_uri(clean_generated_image(image.convert("RGB")))
class ZeroGpuImageBackend:
"""Route resolved styles to a Space GPU function with a hosted fallback."""
def __init__(
self,
generator: Callable[[str, int, str], str],
*,
fallback: ImageBackend | None = None,
) -> None:
self.generator = generator
self.fallback = fallback
def generate(
self,
prompt: str,
seed: int,
style: ForestStyle = "surprise",
) -> str:
resolved = resolve_style(style, seed)
try:
return self.generator(prompt, seed, resolved)
except Exception:
if self.fallback is None:
raise
return self.fallback.generate(prompt, seed, resolved)
class ModalImageBackend:
"""Call the private-token Modal service that hosts the trained adapters."""
def __init__(
self,
endpoint: str,
signing_key: str,
*,
client: Any | None = None,
fallback: ImageBackend | None = None,
timeout: float = 600,
) -> None:
self.endpoint = endpoint
self.signing_key = signing_key
self.client = client or httpx.Client(timeout=timeout, follow_redirects=True)
self.fallback = fallback
def generate(
self,
prompt: str,
seed: int,
style: ForestStyle = "surprise",
) -> str:
resolved = resolve_style(style, seed)
try:
response = self.client.post(
self.endpoint,
json={
"prompt": prompt,
"seed": seed,
"style": resolved,
"signature": sign_modal_request(
self.signing_key,
prompt,
seed,
resolved,
),
},
)
response.raise_for_status()
image = response.json()["image"]
if not isinstance(image, str) or not image.startswith("data:image/png;base64,"):
raise ValueError("Modal image response is not a PNG data URI")
return image
except Exception:
if self.fallback is None:
raise
return self.fallback.generate(prompt, seed, resolved)