Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 248 additions & 0 deletions notebooks/working-with-images.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Celeste AI - Working With Images\n",
"\n",
"Unified interface for image **generation**, **editing**, and **analysis** across providers.\n",
"\n",
"Star on GitHub 👉 [withceleste/celeste-python](https://github.com/withceleste/celeste-python)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Setup"
]
},
{
"cell_type": "code",
"metadata": {},
"source": [
"import celeste\n",
"from IPython.display import Image, display"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## Generate\n",
"\n",
"Create images from text prompts."
]
},
{
"cell_type": "code",
"metadata": {},
"source": [
"img_gen_result = await celeste.images.generate(\n",
" \"A nano banana on the beach\",\n",
" model=\"gemini-2.5-flash-image\",\n",
")"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"metadata": {},
"source": [
"display(Image(data=img_gen_result.content.data))"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## Edit\n",
"\n",
"Modify existing images with text instructions."
]
},
{
"cell_type": "code",
"metadata": {},
"source": [
"img_edit_result = await celeste.images.edit(\n",
" image=img_gen_result.content,\n",
" prompt=\"Make it night time\",\n",
" model=\"gemini-2.5-flash-image\",\n",
")"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"metadata": {},
"source": [
"display(Image(data=img_edit_result.content.data))"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"## Analyze\n",
"\n",
"Extract information from images."
]
},
{
"cell_type": "code",
"metadata": {},
"source": [
"analyze_result = await celeste.images.analyze(\n",
" prompt=\"What fruit is in this image and what color is it?\",\n",
" image=img_gen_result.content,\n",
" model=\"gemini-2.5-flash-lite\",\n",
")"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"metadata": {},
"source": [
"print(analyze_result.content)"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"ExecuteTime": {
"end_time": "2026-01-12T21:53:45.320259Z",
"start_time": "2026-01-12T21:53:45.313508Z"
}
},
"source": [
"---\n",
"\n",
"## Local Generation with Ollama\n",
"\n",
"Generate images locally using Ollama. No API key needed."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"1. Start the server (in a terminal):\n",
"```bash\n",
"ollama serve\n",
"```\n",
"\n",
"2. Pull the image model:\n",
"```bash\n",
"ollama pull x/flux2-klein\n",
"```"
]
},
{
"cell_type": "code",
"metadata": {},
"source": [
"prompt = \"A blurry iPhone-style photograph showing the window of a moving train. Through the window, a scenic landscape appears: tall green cliffs running alongside a river, with a small European village built on the slopes. The motion blur suggests the train is moving quickly, with soft reflections on the glass, natural daylight, and a casual handheld phone-camera aesthetic. Sharp textures where possible, rich colors, and a realistic sense of depth and distance.\"\n",
"\n",
"local_result = await celeste.images.generate(\n",
" prompt=prompt,\n",
" model=\"x/flux2-klein\",\n",
" provider=\"ollama\",\n",
" steps=1,\n",
")\n",
"display(Image(data=local_result.content.data))"
],
"outputs": [],
"execution_count": null
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"---\n",
"\n",
"## Streaming (Ollama)\n",
"\n",
"Ollama streams NDJSON progress events. Celeste exposes these as image stream chunks with `metadata` (progress) and a final chunk containing the image."
]
},
{
"cell_type": "code",
"metadata": {},
"source": [
"from tqdm.asyncio import tqdm\n",
"\n",
"steps = 4\n",
"\n",
"stream = celeste.images.stream.generate(\n",
" prompt=prompt,\n",
" model=\"x/flux2-klein\",\n",
" provider=\"ollama\",\n",
" steps=steps,\n",
")\n",
"\n",
"async for chunk in tqdm(stream, total=steps+1):\n",
" pass\n",
"\n",
"display(Image(data=chunk.content.data))"
],
"outputs": [],
"execution_count": null
},
{
"metadata": {},
"cell_type": "markdown",
"source": [
"---\n",
"Star on GitHub 👉 [withceleste/celeste-python](https://github.com/withceleste/celeste-python)"
]
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": ""
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "celeste-ai"
version = "0.9.2"
version = "0.9.3"
description = "Open source, type-safe primitives for multi-modal AI. All capabilities, all providers, one interface"
authors = [{name = "Kamilbenkirane", email = "kamil@withceleste.ai"}]
readme = "README.md"
Expand Down
5 changes: 3 additions & 2 deletions src/celeste/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def _resolve_model(
msg = f"Model '{model}' not registered. Specify 'modality' explicitly."
raise ValueError(msg)
warnings.warn(
f"Model '{model}' not registered in Celeste for provider {provider.value}. "
f"Model '{model}' not registered in Celeste for provider {provider}. "
"Parameter validation disabled.",
UserWarning,
stacklevel=3,
Expand Down Expand Up @@ -209,11 +209,12 @@ def create_client(
resolved_operation = (
Operation(operation) if isinstance(operation, str) else operation
)
resolved_provider = Provider(provider) if isinstance(provider, str) else provider

resolved_model = _resolve_model(
modality=resolved_modality,
operation=resolved_operation,
provider=provider,
provider=resolved_provider,
model=model,
)

Expand Down
34 changes: 34 additions & 0 deletions src/celeste/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,40 @@ async def stream_post(
except json.JSONDecodeError:
continue # Skip non-JSON control messages (provider-agnostic)

async def stream_post_ndjson(
self,
url: str,
headers: dict[str, str],
json_body: dict[str, Any],
timeout: float = DEFAULT_TIMEOUT,
) -> AsyncIterator[dict[str, Any]]:
"""Stream POST request using NDJSON (newline-delimited JSON).

Unlike SSE (stream_post), NDJSON returns one JSON object per line.
Used by Ollama's native API.

Args:
url: API endpoint URL.
headers: HTTP headers (including authentication).
json_body: JSON request body.
timeout: Timeout in seconds (default: DEFAULT_TIMEOUT).

Yields:
Parsed JSON objects from NDJSON stream.
"""
client = await self._get_client()
async with client.stream(
"POST",
url,
json=json_body,
headers=headers,
timeout=timeout,
) as response:
response.raise_for_status()
async for line in response.aiter_lines():
if line:
yield json.loads(line)

async def aclose(self) -> None:
"""Close HTTP client and cleanup all connections."""
if self._client is not None:
Expand Down
2 changes: 2 additions & 0 deletions src/celeste/modalities/images/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class ImageParameter(StrEnum):
WATERMARK = "watermark"
REFERENCE_IMAGES = "reference_images"
PROMPT_UPSAMPLING = "prompt_upsampling"
NEGATIVE_PROMPT = "negative_prompt"
SEED = "seed"
SAFETY_TOLERANCE = "safety_tolerance"
OUTPUT_FORMAT = "output_format"
Expand All @@ -34,6 +35,7 @@ class ImageParameters(Parameters):
watermark: bool
reference_images: list[ImageArtifact]
prompt_upsampling: bool
negative_prompt: str
seed: int
safety_tolerance: int
output_format: str
Expand Down
2 changes: 2 additions & 0 deletions src/celeste/modalities/images/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
from .bfl import BFLImagesClient
from .byteplus import BytePlusImagesClient
from .google import GoogleImagesClient
from .ollama import OllamaImagesClient
from .openai import OpenAIImagesClient

PROVIDERS: dict[Provider, type[ImagesClient]] = {
Provider.BFL: BFLImagesClient,
Provider.BYTEPLUS: BytePlusImagesClient,
Provider.GOOGLE: GoogleImagesClient,
Provider.OLLAMA: OllamaImagesClient,
Provider.OPENAI: OpenAIImagesClient,
}
6 changes: 6 additions & 0 deletions src/celeste/modalities/images/providers/ollama/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Ollama provider for images modality."""

from .client import OllamaImagesClient
from .models import MODELS

__all__ = ["MODELS", "OllamaImagesClient"]
Loading
Loading