From fd60106ec5e68618b01a39d2e27151543d267aed Mon Sep 17 00:00:00 2001 From: koaning Date: Tue, 12 May 2026 17:03:17 +0200 Subject: [PATCH] openrouter images added --- README.md | 1 + .../session/openrouter-image-bakeoff.py.json | 270 ++++++++++++ .../external/openrouter-image-bakeoff.py | 416 ++++++++++++++++++ 3 files changed, 687 insertions(+) create mode 100644 notebooks/external/__marimo__/session/openrouter-image-bakeoff.py.json create mode 100644 notebooks/external/openrouter-image-bakeoff.py diff --git a/README.md b/README.md index 9c8badc..e153f75 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ uvx marimo edit --sandbox | Manim Slides | Trigonometric identity proof animated with manim-slides. | [![Open in molab](https://marimo.io/molab-shield.svg)](https://molab.marimo.io/github/marimo-team/gallery-examples/blob/main/notebooks/external/manim-slides.py) | | dltHub + Hugging Face | Curate Hugging Face datasets with dltHub pipelines and data quality checks. | [![Open in molab](https://marimo.io/molab-shield.svg)](https://molab.marimo.io/github/marimo-team/gallery-examples/blob/main/notebooks/external/dlthub-huggingface.py) | | Sketch Vectorization | Convert hand-drawn sketches to SVG curves with a CNN and hypergraph optimization. | [![Open in molab](https://marimo.io/molab-shield.svg)](https://molab.marimo.io/github/marimo-team/gallery-examples/blob/main/notebooks/external/sketch-vectorization.py) | +| OpenRouter Image Bake-off | Compare three OpenRouter image models side-by-side, then refine the winner with sketch annotations. | [![Open in molab](https://marimo.io/molab-shield.svg)](https://molab.marimo.io/github/marimo-team/gallery-examples/blob/main/notebooks/external/openrouter-image-bakeoff.py) | ## Geo diff --git a/notebooks/external/__marimo__/session/openrouter-image-bakeoff.py.json b/notebooks/external/__marimo__/session/openrouter-image-bakeoff.py.json new file mode 100644 index 0000000..2f79523 --- /dev/null +++ b/notebooks/external/__marimo__/session/openrouter-image-bakeoff.py.json @@ -0,0 +1,270 @@ +{ + "version": "1", + "metadata": { + "marimo_version": "0.23.5", + "script_metadata_hash": null + }, + "cells": [ + { + "id": "Hbol", + "code_hash": "c5da667cd0eab2b9531e2ce1a7c07d05", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "MJUe", + "code_hash": "c464444b0a5ccc50a234b22a9094f9dc", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

OpenRouter: image-model bake-off

\nOpenRouter now exposes image generation through its chat-completions API by\npassing extra_body={\"modalities\": [\"image\"]}. That means we can compare\nimage output across very different providers using one API surface and one\nkey.\nThis notebook lets you pick three image models, type a prompt, and\noptionally sketch a reference, then see the results side-by-side. When you\nlike one of the results, promote it to stage 2 to draw annotations on\ntop and iterate. The three model calls run concurrently via\nasyncio.gather, so wall time is roughly the slowest model, not the sum.
" + } + } + ], + "console": [] + }, + { + "id": "vblA", + "code_hash": "3da490f74704893d8a7a44a1c7cd1b1c", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "" + } + } + ], + "console": [] + }, + { + "id": "bkHC", + "code_hash": "23293033e02077876248b0348e2eefc2", + "outputs": [ + { + "type": "error", + "ename": "exception", + "evalue": "Environment configuration incomplete. Missing: OPENROUTER_API_KEY. Please set all required variables using the widget above.", + "traceback": null + } + ], + "console": [ + { + "type": "stream", + "name": "stderr", + "text": "
Traceback (most recent call last):\n  File "/var/folders/rp/pnh9h2pn51x7t7157l_vh14m0000gp/T/marimo_9930/__marimo__cell_bkHC_.py", line 1, in <module>\n    env_config.require_valid()\n    ~~~~~~~~~~~~~~~~~~~~~~~~^^\n  File "/Users/vwarmerdam/.cache/uv/archive-v0/JfF4_2esSnRCTWGeLOuo0/lib/python3.14/site-packages/wigglystuff/env_config.py", line 189, in require_valid\n    raise EnvironmentError(\n    ...<2 lines>...\n    )\nOSError: Environment configuration incomplete. Missing: OPENROUTER_API_KEY. Please set all required variables using the widget above.\n
\n
", + "mimetype": "application/vnd.marimo+traceback" + } + ] + }, + { + "id": "lEQa", + "code_hash": "11d7358fcd5c2cb1dbbf31d954e5ab01", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "PKri", + "code_hash": "88ba2cd200c2880ac24259bc45b73983", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "Xref", + "code_hash": "b04c36a137f26fd3ed8b01389b49355c", + "outputs": [ + { + "type": "data", + "data": { + "text/html": "

Stage 1: Generate

" + } + } + ], + "console": [] + }, + { + "id": "SFPL", + "code_hash": "10d275586164e4703f1fd7493c5ac32d", + "outputs": [ + { + "type": "error", + "ename": "exception", + "evalue": "An ancestor raised an exception (KeyError): ", + "traceback": null + } + ], + "console": [] + }, + { + "id": "BYtC", + "code_hash": "d37b1add2e7a1db0bd63393d480d7957", + "outputs": [ + { + "type": "error", + "ename": "exception", + "evalue": "\"'OPENROUTER_API_KEY' is not set\"", + "traceback": null + } + ], + "console": [ + { + "type": "stream", + "name": "stderr", + "text": "
Traceback (most recent call last):\n  File "/var/folders/rp/pnh9h2pn51x7t7157l_vh14m0000gp/T/marimo_9930/__marimo__cell_BYtC_.py", line 3, in <module>\n    api_key=env_config.widget["OPENROUTER_API_KEY"],\n            ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^\n  File "/Users/vwarmerdam/.cache/uv/archive-v0/JfF4_2esSnRCTWGeLOuo0/lib/python3.14/site-packages/wigglystuff/env_config.py", line 209, in __getitem__\n    raise KeyError(f"{name!r} is not set")\nKeyError: "'OPENROUTER_API_KEY' is not set"\n
\n
", + "mimetype": "application/vnd.marimo+traceback" + } + ] + }, + { + "id": "RGSE", + "code_hash": "193d9c0120b97045698dce0ffbb5af62", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "Kclp", + "code_hash": "9970b46fb1c985fc46a17c2fe0462d79", + "outputs": [ + { + "type": "error", + "ename": "exception", + "evalue": "An ancestor raised an exception (KeyError): ", + "traceback": null + } + ], + "console": [] + }, + { + "id": "emfo", + "code_hash": "54707d6a55ffea2703e97162dd365f58", + "outputs": [ + { + "type": "error", + "ename": "exception", + "evalue": "An ancestor raised an exception (KeyError): ", + "traceback": null + } + ], + "console": [] + }, + { + "id": "Hstk", + "code_hash": "c6d2e540d1b3eeb3eb59c0a340028fdc", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "nWHF", + "code_hash": "6b2ef4d4c07f5baf31436cd790fb4328", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "
\n

Stage 2: Refine

\nClick Use this \u2192 under any image above to bring it here.
" + } + } + ], + "console": [] + }, + { + "id": "iLit", + "code_hash": "48a736616b4b36fa9fe65838700799ba", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "ZHCJ", + "code_hash": "30f082cd1590d7d71da0795945537b86", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "ROlb", + "code_hash": "1098834a8d44d3ec91476c14be873dfb", + "outputs": [ + { + "type": "error", + "ename": "exception", + "evalue": "An ancestor raised an exception (KeyError): ", + "traceback": null + } + ], + "console": [] + }, + { + "id": "qnkX", + "code_hash": "4d3f9ceaec736cf586db93e57be4073e", + "outputs": [ + { + "type": "error", + "ename": "exception", + "evalue": "An ancestor raised an exception (KeyError): ", + "traceback": null + } + ], + "console": [] + }, + { + "id": "TqIu", + "code_hash": "27d8354a9efdb3a7a091bcc0ba753082", + "outputs": [ + { + "type": "error", + "ename": "exception", + "evalue": "An ancestor raised an exception (KeyError): ", + "traceback": null + } + ], + "console": [] + } + ] +} \ No newline at end of file diff --git a/notebooks/external/openrouter-image-bakeoff.py b/notebooks/external/openrouter-image-bakeoff.py new file mode 100644 index 0000000..af24870 --- /dev/null +++ b/notebooks/external/openrouter-image-bakeoff.py @@ -0,0 +1,416 @@ +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "marimo>=0.23.2,<0.23.6", +# "openai>=1.50.0", +# "wigglystuff>=0.2.39", +# "pillow>=10.0.0", +# ] +# /// + +import marimo + +__generated_with = "0.23.5" +app = marimo.App(width="medium") + + +@app.cell +def _(): + import asyncio + import base64 + import io + import time + + import marimo as mo + from openai import AsyncOpenAI, OpenAI + from PIL import Image, ImageOps + from wigglystuff import EnvConfig, Paint + + return ( + AsyncOpenAI, + EnvConfig, + Image, + ImageOps, + OpenAI, + Paint, + asyncio, + base64, + io, + mo, + time, + ) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(""" + # OpenRouter: image-model bake-off + + [OpenRouter](https://openrouter.ai/) now exposes [image generation](https://openrouter.ai/collections/image-models) through its chat-completions API by + passing `extra_body={"modalities": ["image"]}`. That means we can compare + image output across very different providers using one API surface and one + key. + + This notebook lets you pick **three** image models, type a prompt, and + optionally sketch a reference, then see the results side-by-side. When you + like one of the results, promote it to **stage 2** to draw annotations on + top and iterate. The three model calls run concurrently via + `asyncio.gather`, so wall time is roughly the slowest model, not the sum. + """) + return + + +@app.cell(hide_code=True) +def _(EnvConfig, OpenAI, mo): + def check_openrouter_key(k): + OpenAI(base_url="https://openrouter.ai/api/v1", api_key=k).models.list() + + env_config = mo.ui.anywidget( + EnvConfig({"OPENROUTER_API_KEY": check_openrouter_key}) + ) + env_config + return (env_config,) + + +@app.cell(hide_code=True) +def _(env_config): + env_config.require_valid() + return + + +@app.cell(hide_code=True) +def _(mo): + MODELS = [ + "bytedance-seed/seedream-4.5", + "google/gemini-2.5-flash-image", + "black-forest-labs/flux.2-pro", + "black-forest-labs/flux.2-klein-4b", + "openai/gpt-5-image-mini", + "openai/gpt-5-image", + "stability-ai/stable-diffusion-3.5-large", + ] + + model_a = mo.ui.dropdown(MODELS, value=MODELS[0], label="Model A") + model_b = mo.ui.dropdown(MODELS, value=MODELS[1], label="Model B") + model_c = mo.ui.dropdown(MODELS, value=MODELS[2], label="Model C") + return model_a, model_b, model_c + + +@app.cell(hide_code=True) +def _(Paint, mo): + ASPECTS = { + "1:1": {"api": "1024x1024", "display": (400, 400)}, + "16:9": {"api": "1280x720", "display": (480, 270)}, + "9:16": {"api": "720x1280", "display": (270, 480)}, + "4:3": {"api": "1152x864", "display": (400, 300)}, + "3:4": {"api": "864x1152", "display": (300, 400)}, + } + + prompt = mo.ui.text_area( + value="A beautiful sunset over mountains", + label="Prompt", + rows=3, + full_width=True, + ) + paint = mo.ui.anywidget(Paint(width=320, height=200)) + aspect = mo.ui.dropdown(list(ASPECTS.keys()), value="1:1", label="Aspect") + button = mo.ui.run_button(label="Generate") + return ASPECTS, aspect, button, paint, prompt + + +@app.cell(hide_code=True) +def _(aspect, button, mo, model_a, model_b, model_c, paint, prompt): + mo.vstack( + [ + mo.md("## Stage 1: Generate"), + paint, + mo.hstack([model_a, model_b, model_c], justify="start"), + prompt, + mo.hstack([aspect, button], justify="start"), + ] + ) + return + + +@app.cell(hide_code=True) +def _(Image, header, mo, results, use_buttons): + _columns = [ + mo.vstack( + [ + header(name, elapsed, cost), + content if isinstance(content, Image.Image) else mo.md(f"_{content}_"), + use_buttons[idx], + ], + gap=0.25, + ) + for idx, (name, content, cost, elapsed) in enumerate(results) + ] + mo.hstack(_columns, widths="equal") + return + + +@app.cell(hide_code=True) +def _(ASPECTS, AsyncOpenAI, Image, ImageOps, base64, env_config, io, mo, time): + client = AsyncOpenAI( + base_url="https://openrouter.ai/api/v1", + api_key=env_config.widget["OPENROUTER_API_KEY"], + ) + + def image_to_data_url(pil_img): + buf = io.BytesIO() + pil_img.save(buf, format="PNG") + b64 = base64.b64encode(buf.getvalue()).decode() + return f"data:image/png;base64,{b64}" + + def paint_to_data_url(pil_img): + if pil_img is None: + return None + rgb = pil_img.convert("RGB") + extrema = rgb.getextrema() + if all(lo == 255 and hi == 255 for lo, hi in extrema): + return None + return image_to_data_url(pil_img) + + async def generate_image(model, prompt_text, aspect_key, image_data_url=None): + spec = ASPECTS[aspect_key] + if image_data_url: + content = [ + {"type": "text", "text": prompt_text}, + {"type": "image_url", "image_url": {"url": image_data_url}}, + ] + else: + content = prompt_text + start = time.perf_counter() + try: + response = await client.chat.completions.create( + model=model, + messages=[{"role": "user", "content": content}], + extra_body={ + "modalities": ["image"], + "usage": {"include": True}, + "size": spec["api"], + }, + ) + elapsed = time.perf_counter() - start + usage = response.usage + cost = getattr(usage, "cost", None) + if cost is None and usage is not None: + cost = (getattr(usage, "model_extra", None) or {}).get("cost") + message = response.choices[0].message + images = getattr(message, "images", None) or [] + if not images: + return f"No image returned. Text reply: {message.content!r}", cost, elapsed + url = images[0]["image_url"]["url"] + if url.startswith("data:"): + _, b64 = url.split(",", 1) + img = Image.open(io.BytesIO(base64.b64decode(b64))) + img = ImageOps.fit(img, spec["display"], Image.LANCZOS) + return img, cost, elapsed + return url, cost, elapsed + except Exception as exc: + return f"{type(exc).__name__}: {exc}", None, time.perf_counter() - start + + def header(name, elapsed, cost): + cost_str = f"${cost:.4f}" if cost is not None else "cost n/a" + return mo.Html( + f'
' + f'
{name}
' + f'
' + f'{elapsed:.1f}s · {cost_str}' + f'
' + ) + + return generate_image, header, image_to_data_url, paint_to_data_url + + +@app.cell +def _(mo): + selected_image, set_selected_image = mo.state(None) + return selected_image, set_selected_image + + +@app.cell(hide_code=True) +async def _( + Image, + aspect, + asyncio, + button, + generate_image, + mo, + model_a, + model_b, + model_c, + paint, + paint_to_data_url, + prompt, +): + mo.stop( + not button.value, + mo.md("*Pick three models, write a prompt (optionally sketch a reference), then click **Generate**.*"), + ) + + image_data_url = paint_to_data_url(paint.get_pil()) + selections = [model_a.value, model_b.value, model_c.value] + raw = await asyncio.gather( + *[generate_image(name, prompt.value, aspect.value, image_data_url) for name in selections] + ) + results = [(name, *r) for name, r in zip(selections, raw)] + images = [r[1] if isinstance(r[1], Image.Image) else None for r in results] + return images, results + + +@app.cell(hide_code=True) +def _(images, mo, set_selected_image): + def make_cb(img): + def cb(v): + if img is not None: + set_selected_image(img) + return v + 1 + return cb + + use_buttons = mo.ui.array( + [ + mo.ui.button( + value=0, + on_click=make_cb(img), + label="Use this →", + disabled=img is None, + ) + for img in images + ] + ) + return (use_buttons,) + + +@app.cell(hide_code=True) +def _(mo): + prompt2 = mo.ui.text_area( + value="Improve this image, following the annotations drawn on top.", + label="Refinement prompt", + rows=2, + full_width=True, + ) + button2 = mo.ui.run_button(label="Generate refinements") + return button2, prompt2 + + +@app.cell(hide_code=True) +def _(mo, selected_image): + if selected_image() is None: + header2 = mo.md( + "---\n\n" + "## Stage 2: Refine\n\n" + "*Click **Use this →** under any image above to bring it here.*" + ) + else: + header2 = mo.md( + "---\n\n" + "## Stage 2: Refine\n\n" + "*Sketch annotations on top of the image below, then generate.*" + ) + header2 + return + + +@app.cell(hide_code=True) +def _(Paint, mo, selected_image): + sel = selected_image() + if sel is None: + paint2 = None + out = None + else: + paint2 = mo.ui.anywidget(Paint(init_image=sel, store_background=True)) + out = paint2 + out + return (paint2,) + + +@app.cell(hide_code=True) +def _(aspect, button2, mo, model_a, model_b, model_c, paint2, prompt2): + if paint2 is None: + controls2 = None + else: + controls2 = mo.vstack( + [ + mo.hstack([model_a, model_b, model_c], justify="start"), + prompt2, + mo.hstack([aspect, button2], justify="start"), + ] + ) + controls2 + return + + +@app.cell(hide_code=True) +async def _( + Image, + aspect, + asyncio, + button2, + generate_image, + image_to_data_url, + mo, + model_a, + model_b, + model_c, + paint2, + prompt2, +): + mo.stop(paint2 is None, None) + mo.stop( + not button2.value, + mo.md("*Draw on the image and click **Generate refinements**.*"), + ) + + refined_url = image_to_data_url(paint2.get_pil()) + selections2 = [model_a.value, model_b.value, model_c.value] + raw2 = await asyncio.gather( + *[generate_image(name, prompt2.value, aspect.value, refined_url) for name in selections2] + ) + results2 = [(name, *r) for name, r in zip(selections2, raw2)] + images2 = [r[1] if isinstance(r[1], Image.Image) else None for r in results2] + return images2, results2 + + +@app.cell(hide_code=True) +def _(images2, mo, set_selected_image): + def make_cb2(img): + def cb(v): + if img is not None: + set_selected_image(img) + return v + 1 + return cb + + use_buttons2 = mo.ui.array( + [ + mo.ui.button( + value=0, + on_click=make_cb2(img), + label="Use this →", + disabled=img is None, + ) + for img in images2 + ] + ) + return (use_buttons2,) + + +@app.cell(hide_code=True) +def _(Image, header, mo, results2, use_buttons2): + _columns2 = [ + mo.vstack( + [ + header(name, elapsed, cost), + content if isinstance(content, Image.Image) else mo.md(f"_{content}_"), + use_buttons2[idx], + ], + gap=0.25, + ) + for idx, (name, content, cost, elapsed) in enumerate(results2) + ] + mo.hstack(_columns2, widths="equal") + return + + +if __name__ == "__main__": + app.run()