Portfolio demonstration project. Runs entirely locally with Flask. IBM watsonx.ai (Granite) acts as the marketing reasoning engine, orchestrating a complete campaign across copy, imagery, and distribution in a single cinematic, five-step workflow.
Small business owners are their own marketers — but consistent local marketing demands time, budget, and expertise most don't have. Identifying relevant nearby events, writing platform-specific copy for Instagram, Facebook, and email, generating professional imagery, and coordinating publishing across channels currently takes hours of manual work per campaign. Most small businesses either skip it entirely or produce inconsistent, low-impact content.
Lumina is an end-to-end AI marketing platform purpose-built for small businesses. A business owner describes their business once; Lumina's 5-step guided workflow takes them from that description to a fully drafted, editable, ready-to-publish campaign — marketing strategy, Instagram caption, Facebook post, HTML email, campaign image, and direct one-click publishing — in minutes, not hours. Every generated field is editable and individually regeneratable before anything leaves the app.
Challenge Theme: Reimagine Creative Industries with AI (July Challenge)
Lumina was built end-to-end using IBM Bob as the primary development partner throughout the entire project.
Bob was used to:
- Plan the architecture — designing the 5-step wizard flow, the Flask app factory structure, the service layer separation, and the session/storage strategy
- Generate and iterate on the Flask backend — all route handlers in
app/routes/api.py, the storage layer instorage.py, and the app factory inapp/__init__.pywere written and refined through Bob - Build the multi-provider image fallback logic — the waterfall chain in
app/services/image_gen.py(OpenAI → Google Gemini → Hugging Face → mock) was designed and implemented with Bob - Debug across integration modules — fixing the Instagram async container-readiness polling bug, the Granite JSON repair pipeline, the session cookie overflow caused by storing base64 image data, and the BeautifulSoup email HTML sanitisation were all diagnosed and resolved with Bob
- Refactor throughout development — dead code removal, consolidating duplicate route files, moving large session payloads to
campaign_history.json, and tightening the prompt template were all driven by Bob
Important distinction: IBM Bob is the development tool used to build Lumina. IBM Granite (
ibm/granite-3-8b-instructvia watsonx.ai) is the runtime AI model running inside the finished app — it generates the marketing strategy, captions, email copy, and image prompts for each campaign. Bob built the platform; Granite powers it.
Business Owner
│
▼
Describe Business ← Step 1 (index.html · Step 1 form)
│
▼
Discover Nearby Events ← Step 2 (app/services/events.py)
• PredictHQ API real festivals, concerts, sports, markets
• Nager.Date API public holidays (free, no key required)
• Universal Observances worldwide awareness days, seasons, shopping events
│
▼
IBM Granite Analyses ← Step 3 (app/services/granite.py → watsonx.ai)
• business context
• current promotions
• target audience
• selected nearby events
│
▼
Granite Generates
• Marketing strategy
• Instagram caption
• Facebook post
• HTML email body
• Campaign slogan
• Detailed image prompt
│
▼
Owner Reviews & Edits ← Step 3 (every field is editable; regenerate any piece)
│
▼
Generate Campaign Image ← Step 4 (app/services/image_gen.py)
Multi-Provider Waterfall:
1. OpenAI DALL-E 3 / DALL-E 2
2. Google Gemini image generation
3. Hugging Face Inference API (SDXL or configured model)
4. Mock PNG — built-in, zero keys required
│
▼
Owner Previews & Publishes ← Step 5
• Post to Instagram (Meta Graph API)
• Post to Facebook (Meta Graph API)
• Send email (SendGrid → predetermined EMAIL_TO recipient)
lumina/
│
├── app/
│ ├── __init__.py # Flask app factory
│ ├── routes/
│ │ ├── main.py # Page routes
│ │ └── api.py # JSON API consumed by the frontend
│ ├── services/
│ │ ├── events.py # PredictHQ + Nager.Date + observances
│ │ ├── granite.py # IBM watsonx.ai Granite integration
│ │ ├── image_gen.py # Multi-provider image waterfall
│ │ └── social.py # Instagram · Facebook · SendGrid
│ ├── static/
│ │ ├── css/lumina.css # Full cinematic design system + motion
│ │ └── js/lumina.js # Frontend orchestration (vanilla JS)
│ └── templates/
│ ├── index.html # 5-step workflow UI
│ ├── history.html # Campaign history list
│ ├── campaign_detail.html # Single campaign view
│ ├── 404.html
│ └── 500.html
│
├── config.py # Centralised config loaded from .env
├── demo_mode.py # Realistic mock data when API keys are absent
├── utils.py # Logging setup, ID generation, helpers
├── storage.py # campaign_history.json read/write (no database)
│
├── campaign_history.json # All campaigns stored here as JSON
├── generated/ # Generated campaign images saved here
│
├── requirements.txt
├── .env.example
├── .gitignore
└── README.md
git clone <repo-url>
cd lumina
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activatepip install -r requirements.txtcp .env.example .env
# Edit .env — fill in any keys you have. The app runs without all of them.python run.py| Service | Environment Variable(s) | Where to get it | Required? |
|---|---|---|---|
| IBM watsonx.ai (Granite) | WATSONX_API_KEY, WATSONX_PROJECT_ID |
cloud.ibm.com | Recommended — falls back to rich mock data |
| PredictHQ (events, concerts, festivals, sports) | PREDICTHQ_API_KEY |
control.predicthq.com | Optional — free public holidays always load |
| API Ninjas (public holidays) | API_NINJAS_KEY |
api-ninjas.com | Optional |
| OpenAI Images (DALL-E 3 / DALL-E 2) | OPENAI_API_KEY |
platform.openai.com/api-keys | Optional — waterfall falls through to next provider |
| Google Gemini image generation | GOOGLE_API_KEY |
aistudio.google.com/apikey | Optional |
| Hugging Face Inference API | HUGGINGFACE_API_KEY |
huggingface.co/settings/tokens | Optional |
| Instagram Graph API | INSTAGRAM_ACCESS_TOKEN, INSTAGRAM_ACCOUNT_ID |
Meta for Developers | Optional |
| Facebook Graph API | FACEBOOK_ACCESS_TOKEN, FACEBOOK_PAGE_ID |
Meta for Developers | Optional |
| SendGrid Email | SENDGRID_API_KEY, EMAIL_FROM, EMAIL_TO |
sendgrid.com | Optional — email sends to the predetermined EMAIL_TO address |
Zero-key mode: the app runs end-to-end without a single API key configured. Granite falls back to realistic mock campaigns; images fall back to a built-in mock PNG; events load from free public APIs (Nager.Date + universal observances). Missing credentials surface as clear UI messages rather than crashes.
Lumina tries image providers in priority order and uses the first one that succeeds:
- OpenAI DALL-E 3 / DALL-E 2 — highest quality; requires
OPENAI_API_KEY - Google Gemini (
gemini-2.0-flash-preview-image-generation) — requiresGOOGLE_API_KEY - Hugging Face Inference API (default model:
stabilityai/stable-diffusion-xl-base-1.0) — requiresHUGGINGFACE_API_KEY - Mock PNG — a valid 64×64 gold PNG generated entirely in Python; always works with zero keys
Configure which model each provider uses via the corresponding env variables in .env:
OPENAI_IMAGE_MODEL=dall-e-3
OPENAI_IMAGE_SIZE=1024x1024
GOOGLE_IMAGE_MODEL=gemini-2.0-flash-preview-image-generation
HUGGINGFACE_MODEL=stabilityai/stable-diffusion-xl-base-1.0
Set DRY_RUN=true in your .env to test the full publishing workflow without
sending any real requests to Meta or SendGrid.
DRY_RUN=trueWhen DRY_RUN is enabled:
- The
/api/publishendpoint logs the full API payload to the console instead of making live calls. - The UI receives a simulated success response so you can verify the end-to-end flow.
- No posts are published to Instagram or Facebook.
- No emails are sent via SendGrid.
This is the recommended setting when demoing the app, running CI checks, or testing with incomplete credentials.
Events are fetched from three sources simultaneously, then scored, deduplicated, and ranked:
| Source | Notes |
|---|---|
| PredictHQ | Real nearby events (festivals, concerts, sports, expos, community) within a configurable radius. Requires PREDICTHQ_API_KEY. |
| Nager.Date | Worldwide public holidays — free, no key required. |
| Universal Observances | 25+ curated global awareness days and shopping events (Valentine's Day, Earth Day, Black Friday, etc.) that always apply regardless of location. |
Scoring weights recency, proximity, and event rank to surface the most impactful opportunities.
Every campaign generated is automatically appended to campaign_history.json.
No database required. The history page at /history lists all past campaigns;
individual campaigns can be viewed at /history/<campaign_id> and deleted.
- Cinematic and minimal —
#0a0908dark background, warm gold accents, Playfair Display wordmark - IBM Granite as creative partner — AI writes, the owner approves and edits
- No dashboards — pure linear workflow, step by step
- Owner always in control — every generated field is editable and individually regeneratable before anything is published
- Motion with restraint — ambient light, animated step connector, shimmer loading states; all wrapped in
@media (prefers-reduced-motion: no-preference)
Logs are written to both the console and lumina.log (rotating, 5 MB × 3 backups).
Set LOG_LEVEL=DEBUG in .env for verbose output including full Granite prompts and responses.
Built as a portfolio demonstration of IBM watsonx.ai Granite capabilities in a real-world, production-grade marketing workflow.