Image generation (local)¶
The SDK already generated text, understood images (VLM), embedded,
transcribed and synthesized speech. What it could not do was draw.
ImageGenerator closes that gap by running a HuggingFace diffusion model on
your own hardware — no paid API, nothing leaving the machine.
It mirrors TextGenerator
Same device/precision resolution, same lazy load, same
unload_if_idle, same Hub pinning keywords
(revision=/local_files_only=/trust_remote_code=). A service that
already self-hosts an LLM gains images without learning a second set of
conventions.
[genai-image] carries an upper bound
diffusers declares httpx<1.0.0 and huggingface-hub<2.0. Neither
bites today (httpx is still on the 0.28 line), and being an optional
extra the bound only enters the resolution of whoever installs it. Still:
if your service comes to depend on httpx 1.x, this extra is the first
place to look.
The first drawing¶
import asyncio
from pathlib import Path
from tempest_fastapi_sdk.genai import ImageGenerator
generator = ImageGenerator("stabilityai/sdxl-turbo")
async def main() -> None:
"""Run this example."""
images = await generator.generate("a lighthouse at dawn")
Path("lighthouse.png").write_bytes(images[0].data)
print(images[0].seed, images[0].width, images[0].height)
asyncio.run(main())
Notice what came back: not loose bytes, a list of GeneratedImage with
the seed attached. Diffusion is deterministic given a seed, so returning
it is the difference between "nice image, gone forever" and "nice image, and
here is how to get it back". When you pass no seed, only the generator knows
which one was drawn — which is why it reports it.
Configuring: turbo and full models want opposite things¶
import asyncio
from tempest_fastapi_sdk.genai import ImageGenerationConfig, ImageGenerator
turbo = ImageGenerator("stabilityai/sdxl-turbo")
async def main() -> None:
"""Run this example."""
images = await turbo.generate(
"a lighthouse at dawn",
config=ImageGenerationConfig(steps=4, guidance_scale=0.0, seed=7),
)
asyncio.run(main())
import asyncio
from tempest_fastapi_sdk.genai import ImageGenerationConfig, ImageGenerator
full = ImageGenerator("stabilityai/stable-diffusion-xl-base-1.0")
async def main() -> None:
"""Run this example."""
images = await full.generate(
"a lighthouse at dawn",
config=ImageGenerationConfig(
steps=30,
guidance_scale=7.5,
width=1024,
height=1024,
negative_prompt="blurry, watermark",
),
)
asyncio.run(main())
| Field | Turbo / distilled model | Full model |
|---|---|---|
steps |
1–8 | 20–50 |
guidance_scale |
0.0 |
5–9 |
width/height |
the model's native size | 1024 on SDXL |
Only what you set is sent
Unset fields fall through to the model's own defaults. That matters more
here than for text: passing steps=30 to a turbo model wastes 26 of
them, and passing guidance_scale=7.5 to it degrades the image.
Reproducing¶
import asyncio
from tempest_fastapi_sdk.genai import ImageGenerationConfig, ImageGenerator
generator = ImageGenerator("stabilityai/sdxl-turbo")
async def main() -> None:
"""Run this example."""
first = await generator.generate(
"a lighthouse at dawn",
config=ImageGenerationConfig(seed=7, steps=4, guidance_scale=0.0),
)
again = await generator.generate(
"a lighthouse at dawn",
config=ImageGenerationConfig(seed=7, steps=4, guidance_scale=0.0),
)
print(first[0].data == again[0].data)
asyncio.run(main())
Same seed, same prompt, same hardware → same image. Across different GPUs the result can drift slightly (kernels and reduction order are not identical), so treat the seed as reproducibility on your host, not as a universal hash.
Redrawing an existing image¶
import asyncio
from pathlib import Path
from tempest_fastapi_sdk.genai import ImageGenerator
generator = ImageGenerator("stabilityai/sdxl-turbo")
async def main() -> None:
"""Run this example."""
edited = await generator.edit(
"the same room, at night",
"room.png",
strength=0.6,
)
Path("room-night.png").write_bytes(edited[0].data)
asyncio.run(main())
strength says how far to move from the input: near 0.0 keeps the
composition almost intact, 1.0 all but ignores the original. The input
accepts a path, bytes, a PIL.Image or a NumPy array.
The edit pipeline costs no extra VRAM
It is built with AutoPipelineForImage2Image.from_pipe, which reuses
the already-loaded UNet, VAE and text encoders instead of reading a
second copy off disk. An SDXL pipeline is ~7 GB; loading it twice on one
card is how a service OOMs at the first edit request.
Serving it over HTTP¶
from fastapi import FastAPI
from tempest_fastapi_sdk.genai import ImageGenerator, make_genai_router
app = FastAPI()
app.include_router(
make_genai_router(image_generator=ImageGenerator("stabilityai/sdxl-turbo")),
)
curl -X POST http://127.0.0.1:8000/api/genai/image \
-H "Content-Type: application/json" \
-d '{"prompt": "a lighthouse at dawn", "config": {"steps": 4, "guidance_scale": 0.0}}' \
--output lighthouse.png --dump-header -
The response body is the image, so the route returns only the first one;
the seed travels in the X-Image-Seed header. Want a batch? Use the class
directly — the route exists for the common case of one image per request.
Not holding the GPU hostage¶
from tempest_fastapi_sdk.genai import ImageGenerator
generator = ImageGenerator(
"stabilityai/sdxl-turbo",
idle_unload_seconds=300.0,
)
A periodic task calls generator.unload_if_idle() and the VRAM comes back
after five idle minutes. The next request reloads.
Concurrency defaults to 1, on purpose
Unlike an LLM, one diffusion call already saturates the GPU. Running two
concurrently makes both slower and doubles peak VRAM. Raise
max_concurrent only once you have measured spare card.
Pinning the model, like everything else¶
from tempest_fastapi_sdk.genai import ImageGenerator
generator = ImageGenerator(
"stabilityai/sdxl-turbo",
revision="f4b0486b498f84668e828044de1d0c8ba486e05b",
cache_dir="/var/lib/models",
local_files_only=True,
)
The same three keywords as every other loader. Download ahead of serving
with tempest model pull and see
Model weights (Hub lifecycle) — a diffusion pipeline is
several gigabytes, and paying for it inside the first request hurts more
here than anywhere else.
Load-time decisions: pipeline_kwargs¶
Some choices happen while loading, not while drawing — and there the
escape hatch is no help, because the cost is already paid. Pass those through
pipeline_kwargs:
from tempest_fastapi_sdk.genai import ImageGenerator
generator = ImageGenerator(
"stabilityai/stable-diffusion-2-1",
pipeline_kwargs={
"safety_checker": None,
"variant": "fp16",
"use_safetensors": True,
},
)
| Key | Why it matters |
|---|---|
safety_checker: None |
Stable Diffusion 1.x/2.x repositories bundle an extra CLIP purely to filter. It costs memory and sometimes returns a blank image. |
variant: "fp16" |
Fetches the half-precision weights — usually halves the download. |
use_safetensors: True |
Refuses a pickle checkpoint. |
Turning the filter off is your call, and it has a licence
The Stable Diffusion licence asks that unfiltered results not be exposed
publicly. Disable it knowing your use case — diffusers itself warns at
runtime when you do.
Keys in pipeline_kwargs are applied last, so they win over whatever the
SDK computed — that is also how you override torch_dtype.
Swapping the scheduler (escape hatch)¶
from tempest_fastapi_sdk.genai import ImageGenerator
generator = ImageGenerator("stabilityai/stable-diffusion-xl-base-1.0")
pipeline = generator.pipeline
print(type(pipeline).__name__)
.pipeline returns the diffusers object (loading it on first access). Use
it to swap the scheduler, attach a LoRA or enable a memory optimization the
SDK does not wrap — the SDK covers the common path and gets out of the way
for the rest.
Recap¶
generate(prompt, config=...)draws; it returnsGeneratedImagecarrying the seed that reproduces the result.ImageGenerationConfigtypessteps/guidance_scale/width/height/seed/num_images; turbo and full models want opposite values.edit(prompt, image, strength=...)redraws, reusing the already-loaded components — no extra VRAM.make_genai_router(image_generator=...)publishesPOST /image, with the seed in the header.idle_unload_secondshands the VRAM back when nobody is drawing.revision=/local_files_only=pin the model, exactly like the rest of the SDK.
Where to go next: Self-hosted generative AI for text, embeddings and RAG, and Model weights for the download lifecycle.