Skip to content

qualabs/real-time-ad-gen

Repository files navigation

Real-time Ad Generator

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.


Architecture

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>&param1=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
Loading

All images (templates and outputs) are stored in MongoDB via GridFS — no filesystem volumes required.


Setup

Prerequisites

  • Python 3.12+
  • MongoDB 7.0 (or Docker)
  • A Google Gemini API key

With Docker Compose

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

Local (without Docker)

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 8000

On first startup the template_types collection is automatically seeded with the four built-in types.


Sample templates

The sample_templates/ directory contains a templates.yml config and placeholders for four PNG images (one per ad shape). To register them:

  1. Add your images to sample_templates/:

    • banner.png
    • skyscraper.png
    • lshape-left.png
    • lshape-right.png
  2. 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.

  3. 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>
    

Environment Variables

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

API Reference

Image Generation

GET /image

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.png

GET /image/html

Same as /image but returns the PNG embedded in an HTML page — useful for browser preview.


Generations

Method Path Description
GET /generations List generation records (?limit=50&offset=0)
GET /generations/{generationId}/image Download the PNG output of a past generation

Templates

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)

POST /templates — multipart form

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"
}

Template Types

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").


Prompt System

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>

Caching

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.


Project Structure

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

About

A near-real-time ad generation server for custom ads

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors