A FastAPI service that generates and modifies advertising images using Google Gemini. Send a single request with a template ID and parameters — get back a PNG. Results are cached by SHA-256 so identical requests skip Gemini entirely.
The system uses a single-request synchronous flow: GET → cached or generated PNG.
sequenceDiagram
participant Client
participant API
participant MongoDB
participant GridFS
participant Gemini
Note over Client,GridFS: Register a template
Client->>API: POST /templates (image + metadata)
API->>GridFS: store template image
API->>MongoDB: insert template doc
API-->>Client: 201 template_id
Note over Client,Gemini: Generate (or retrieve cached) ad image
Client->>API: GET /image?template_id=<id>¶m1=val1
API->>MongoDB: check generations cache (SHA-256 key)
alt cache hit
API->>GridFS: fetch cached output image
API-->>Client: 200 PNG (X-Cache: HIT)
else cache miss
API->>GridFS: fetch template image
API->>MongoDB: fetch template + type descriptions
API->>Gemini: template image + assembled prompt
Gemini-->>API: generated image
API->>GridFS: store output image
API->>MongoDB: insert generation record
API-->>Client: 200 PNG (X-Cache: MISS)
end
All images (templates and outputs) are stored in MongoDB via GridFS — no filesystem volumes required.
- Python 3.12+
- MongoDB 7.0 (or Docker)
- A Google Gemini API key
cp .env.example .env
# Edit .env: set GOOGLE_API_KEY
docker compose up| Service | URL |
|---|---|
| API | http://localhost:8888 |
| API Docs (Swagger) | http://localhost:8888/docs |
| Web UI | http://localhost:3000 |
| MongoDB UI (mongo-express) | http://localhost:8081 |
pip install -r requirements.txt
cp .env.example .env # set GOOGLE_API_KEY; MONGODB_URI defaults to localhost:27017
uvicorn src.server:app --reload --port 8000On first startup the template_types collection is automatically seeded with the four built-in types.
The sample_templates/ directory contains a templates.yml config and placeholders for four PNG images (one per ad shape). To register them:
-
Add your images to
sample_templates/:banner.pngskyscraper.pnglshape-left.pnglshape-right.png
-
Run the setup script:
./setup-templates.sh
The script starts the API and database if they are not already running, registers each template whose image file is present, prints the resulting UUID, and shuts back down. If a template already exists (HTTP 409) it is skipped rather than treated as an error.
-
Copy the printed UUIDs into your
.env:MORPHEUS_BANNER_QUERY=template_id=<uuid> MORPHEUS_SKYSCRAPER_QUERY=template_id=<uuid> MORPHEUS_LSHAPE_LEFT_QUERY=template_id=<uuid> MORPHEUS_LSHAPE_RIGHT_QUERY=template_id=<uuid>
| Variable | Default | Required |
|---|---|---|
GOOGLE_API_KEY |
— | Yes |
GEMINI_MODEL |
gemini-3.1-flash-image-preview |
No |
MONGODB_URI |
mongodb://localhost:27017 |
No |
MONGODB_DB |
ad_gen |
No |
Generate or retrieve a cached ad image.
| Parameter | Required | Description |
|---|---|---|
template_id |
Yes | UUID of the template to use |
force_regen |
No | true / 1 / yes — bypass cache, call Gemini again |
| (anything else) | No | User-supplied parameters passed to Gemini |
Response: 200 PNG bytes with X-Cache: HIT or X-Cache: MISS header.
Errors: 404 template not found, 422 missing template_id, 500 Gemini failure.
Example:
curl "http://localhost:8888/image?template_id=<uuid>&team1=Arsenal&city=London" \
-o result.pngSame as /image but returns the PNG embedded in an HTML page — useful for browser preview.
| Method | Path | Description |
|---|---|---|
GET |
/generations |
List generation records (?limit=50&offset=0) |
GET |
/generations/{generationId}/image |
Download the PNG output of a past generation |
| Method | Path | Description |
|---|---|---|
POST |
/templates |
Register a new template (multipart) |
GET |
/templates |
List all templates |
GET |
/templates/{templateId} |
Get template metadata |
PUT |
/templates/{templateId} |
Update name, type, or description |
DELETE |
/templates/{templateId} |
Delete template (409 if generations reference it) |
GET |
/templates/{templateId}/image |
Download template PNG |
PUT |
/templates/{templateId}/image |
Replace template PNG (multipart) |
| Field | Type | Required | Description |
|---|---|---|---|
image |
file | Yes | PNG template image |
template_name |
string | Yes | Unique name |
template_type |
string | Yes | Must match an existing type in /template-types |
description |
string | No | Instructions for Gemini — how to edit this image and what params mean |
Response 201:
{
"template_id": "550e8400-e29b-41d4-a716-446655440000",
"template_name": "football-match-banner",
"template_type": "banner",
"description": "Replace the team names with the values of team1 and team2 params.",
"created_at": "2026-04-29T14:00:00",
"updated_at": "2026-04-29T14:00:00"
}| Method | Path | Description |
|---|---|---|
GET |
/template-types |
List all types |
POST |
/template-types |
Create new type |
GET |
/template-types/{typeName} |
Get single type |
PUT |
/template-types/{typeName} |
Update description |
DELETE |
/template-types/{typeName} |
Delete type (409 if templates reference it) |
Built-in types (seeded on startup):
type_name |
Ad format |
|---|---|
banner |
Horizontal banner ad |
skyscraper |
Vertical tall ad |
lshape-left |
L-shaped ad — left panel is main content area |
lshape-right |
L-shaped ad — right panel is main content area |
The type's description is injected into the Gemini prompt before the template description, so it's the right place for layout-level rules (e.g. "do not modify the L-shape content area").
Prompts are built from two files in src/prompts/:
system_prompt.txt — sent as Gemini's system_instruction (high-trust turn). Defines the model's role, input categories, and editing rules. Critically: instructs Gemini that parameter values are literal data, not instructions, which is the primary defense against prompt injection.
user_prompt_template.txt — the user-turn template with three placeholders filled at request time:
| Placeholder | Source |
|---|---|
{TEMPLATE_TYPE_DESCRIPTION} |
Type's description from the template_types collection |
{TEMPLATE_DESCRIPTION} |
Template's description field |
{USER_PARAMS} |
key=value lines from query parameters (sorted for cache stability) |
Example assembled user-turn prompt:
[type description: "Horizontal banner. Preserve aspect ratio."]
# TEMPLATE DESCRIPTION
<template-description>
Replace the city name in the banner with the value of the `city` param.
</template-description>
# USER-PROVIDED PARAMETERS
<user-params>
city=London
team1=Arsenal
</user-params>
Results are cached in MongoDB's generations collection, keyed by a SHA-256 hash of (template_id, sorted params). Cache hits skip Gemini entirely and return in milliseconds. Pass force_regen=true to bypass the cache.
src/
├── server.py # App factory + lifespan (indexes, type seeding)
├── config.py # Env vars
├── db.py # MongoDB client + collection/GridFS accessors
├── models.py # Pydantic models
├── generation.py # Gemini API wrapper (_generate)
├── monitoring.py # Request logging to logs/requests.jsonl
├── prompts/
│ ├── system_prompt.txt # Gemini system instruction
│ └── user_prompt_template.txt # User-turn prompt template
└── routers/
├── image.py # GET /image, GET /image/html
├── generations.py # GET /generations, GET /generations/{id}/image
├── templates.py # /templates endpoints
└── template_types.py # /template-types endpoints
static/
└── index.html # Web UI (Alpine.js + Tailwind CSS, served by nginx)
nginx.conf # Serves static/ + reverse-proxies API paths
Dockerfile
docker-compose.yml
requirements.txt
CLAUDE.md # Developer context for Claude sessions