File size: 7,178 Bytes
186ad3c 3b7d0f4 186ad3c 3b7d0f4 186ad3c 3b7d0f4 186ad3c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | """Compare image models side by side on a few fixed prompts, with latency and a contact sheet.
uv run python -m scripts.bakeoff t2i black-forest-labs/FLUX.1-schnell Tongyi-MAI/Z-Image-Turbo
uv run python -m scripts.bakeoff edit black-forest-labs/FLUX.2-klein-4B@fal-ai --source fox-snow
Each model is `huggingface-id@provider` (leave out `@provider` to let HF choose). Needs HF_TOKEN in
the environment or the .env file one folder above the repo, and costs real money (cents per run).
The prompts are fixed and harmless and bypass the app's safety checks, so this is a private
evaluation tool, never part of the app. Models with a non-commercial licence may be tried here for
evaluation only; do not enable them for visitors without permission. Images and the contact
sheet go to a temporary folder; compare actual cost on the Hugging Face billing page afterwards.
"""
from __future__ import annotations
import argparse
import asyncio
import io
import os
import sys
import tempfile
import time
from pathlib import Path
from PIL import Image, ImageDraw
from src.config import load_env_file
from src.errors import AppError
from src.logging_setup import configure_logging
from src.providers.hf_inference import SIZES, HFRunner, safety_extra
from src.services.library import LIBRARY_DIR
TIMEOUT_SECONDS = 90.0
CELL = 320
LABEL_WIDTH = 230
HEADER_HEIGHT = 40
T2I_PROMPTS = {
"photo": "A candid photo of a home kitchen in the morning, steam rising from a coffee mug, "
"crumbs on the counter, soft window light",
"illustration": "A friendly dragon flipping pancakes in a small cottage kitchen, "
"colorful illustration",
"text": 'A wooden shop sign that reads "FIKA" above a cosy cafe door, rainy evening, photo',
}
EDIT_INSTRUCTIONS = {
"evening": "Make it a warm evening scene with golden light. Keep the subject the same.",
"playful": "Make it more playful and colorful. Keep the subject the same.",
"painting": "Turn it into an oil painting. Keep the subject and composition the same.",
}
Cell = tuple[float, bytes | None, str] # seconds, PNG bytes, error name
def parse(spec: str) -> tuple[str, str | None]:
model, _, provider = spec.partition("@")
return model, provider or None
async def run_cell(runner: HFRunner, spec: str, method: str, *args, **kwargs) -> Cell:
model, provider = parse(spec)
started = time.perf_counter()
try:
png = await runner.png(
f"bakeoff {model}",
provider,
method,
*args,
model=model,
extra_body=safety_extra(provider),
**kwargs,
)
except AppError as error:
return time.perf_counter() - started, None, type(error).__name__
return time.perf_counter() - started, png, ""
def load_source(name: str) -> bytes:
path = LIBRARY_DIR / f"{name}.webp"
if not path.is_file():
raise SystemExit(
f"No library image '{name}' (run `git lfs checkout` if the files are pointers)"
)
buffer = io.BytesIO()
Image.open(path).convert("RGB").save(buffer, format="PNG")
return buffer.getvalue()
def sheet(
rows: list[str], columns: list[str], cells: dict[tuple[str, str], Cell], source: bytes | None
) -> Image.Image:
offset = 1 if source else 0
width = LABEL_WIDTH + (len(columns) + offset) * CELL
image = Image.new("RGB", (width, HEADER_HEIGHT + len(rows) * CELL), "white")
draw = ImageDraw.Draw(image)
if source:
draw.text((LABEL_WIDTH + 6, 12), "source", fill="black")
for index, column in enumerate(columns):
draw.text((LABEL_WIDTH + (index + offset) * CELL + 6, 12), column, fill="black")
for row_index, row in enumerate(rows):
top = HEADER_HEIGHT + row_index * CELL
draw.text((6, top + 6), row, fill="black")
if source:
with Image.open(io.BytesIO(source)) as original:
image.paste(original.convert("RGB").resize((CELL, CELL)), (LABEL_WIDTH, top))
for index, column in enumerate(columns):
seconds, png, error = cells[(row, column)]
left = LABEL_WIDTH + (index + offset) * CELL
if png:
with Image.open(io.BytesIO(png)) as result:
image.paste(result.convert("RGB").resize((CELL, CELL)), (left, top))
draw.text((left + 4, top + 4), f"{seconds:.1f} s", fill="yellow")
else:
draw.text((left + 4, top + 4), f"failed: {error}", fill="red")
return image
async def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("mode", choices=["t2i", "edit"])
parser.add_argument("models", nargs="+", help="huggingface-id@provider")
parser.add_argument("--source", default="fox-snow", help="library image to edit (edit mode)")
parser.add_argument(
"--instruction",
action="append",
default=[],
help="edit instruction to try instead of the defaults (repeatable)",
)
args = parser.parse_args()
sys.stdout.reconfigure(encoding="utf-8")
configure_logging()
load_env_file()
token = os.environ.get("HF_TOKEN")
if not token:
print("Missing HF_TOKEN (set it in the shell or the .env file).")
return 2
runner = HFRunner(token, timeout=TIMEOUT_SECONDS)
source = load_source(args.source) if args.mode == "edit" else None
instructions = (
{f"#{n}": text for n, text in enumerate(args.instruction, 1)}
if args.instruction
else EDIT_INSTRUCTIONS
)
for label, text in instructions.items() if args.instruction else []:
print(f"{label}: {text}")
columns = list(instructions if source else T2I_PROMPTS)
cells: dict[tuple[str, str], Cell] = {}
for spec in args.models:
for column in columns:
if source:
cell = await run_cell(
runner, spec, "image_to_image", source, prompt=instructions[column]
)
else:
width, height = SIZES["square"]
cell = await run_cell(
runner,
spec,
"text_to_image",
T2I_PROMPTS[column],
width=width,
height=height,
)
cells[(spec, column)] = cell
seconds, _, error = cell
print(f"{spec:55} {column:13} {seconds:5.1f} s {error or 'ok'}")
folder = Path(tempfile.mkdtemp(prefix="bakeoff_"))
for (spec, column), (_, png, _) in cells.items():
if png:
safe = spec.replace("/", "_").replace("@", "_at_")
(folder / f"{safe}__{column}.png").write_bytes(png)
sheet(args.models, columns, cells, source).save(folder / "sheet.png")
ok = [seconds for seconds, png, _ in cells.values() if png]
if ok:
print(f"\nMedian latency {sorted(ok)[len(ok) // 2]:.1f} s over {len(ok)} images")
print(f"Images and sheet.png: {folder}")
return 0 if all(png for _, png, _ in cells.values()) else 1
if __name__ == "__main__":
raise SystemExit(asyncio.run(main()))
|