PiqueTickets Producer Portal - Media Assistant Feature Planning Document¶
Document Version: 1.0 Created: 2025-11-03 Status: Draft - Ready for Review Project: Media Assistant - Smart Image Processing & AI Generation
1. Executive Summary & Feature Overview¶
1.1 Feature Description¶
- Feature Name: Media Assistant - Smart Image Processing & AI Generation Tool
- Feature Type: New Feature
- Priority Level: High
- Estimated Timeline: 6-8 weeks
- Branch:
feature/media-assistant
1.2 Problem Statement¶
Current State: - Producers must manually create multiple image sizes for shows (poster, square, banner) - No automated image formatting or optimization tools - Manual resizing often results in poor quality, stretched images, or incorrect aspect ratios - No color palette extraction for show branding consistency - No AI-powered image creation tools for producers lacking design skills
Pain Points: - Time-consuming manual image creation process (30-60 minutes per show) - Producers without design skills struggle to create professional images - Inconsistent image quality across shows - No automated color scheme generation from imagery - Expensive design costs for professional image creation
User Impact: - Primary Users: Event producers creating and managing shows - Secondary Users: PiqueTickets support team assisting with image issues - Expected Impact: 80% reduction in image creation time, professional-quality automated outputs, AI-powered creative tools
Business Value: - Reduce producer onboarding friction (image creation is major pain point) - Increase show creation completion rate by 40% - Professional, consistent image quality across platform - Competitive differentiation with AI-powered tools - Reduce support burden for image-related issues - Flexible AI model support reduces vendor lock-in and costs
1.3 Expected Outcomes¶
Success Metrics: - 70% of producers use auto-format feature for show images - Average time to create show images reduced from 45 minutes to 5 minutes - 90% of auto-formatted images approved without manual edits - AI image generation usage rate > 30% of shows - User satisfaction score > 4.5/5 for Media Assistant features - 20% of power users provide their own API keys (cost reduction for platform)
User Experience Goals: - One-click image auto-formatting from single upload - Instant color palette extraction with visual preview - Intuitive AI image editor with show detail integration - Real-time processing feedback with progress indicators - Professional results requiring minimal design expertise - Flexibility to use own AI API keys for unlimited usage
Technical Goals: - Image processing < 30 seconds for all formats - Celery-based async processing with synchronous fallback - High-quality scaling with Pillow and modern algorithms - Multi-model AI support (Gemini, OpenAI, future providers) - Secure API key storage with encryption - Scalable architecture supporting 1000+ concurrent jobs
2. Stakeholder Analysis & Requirements¶
2.1 Affected Users & Systems¶
Primary Users: - Event Producers: Need fast, professional image creation tools - New Producers: Require simple, guided image creation workflow - Non-Designer Producers: Need AI assistance for professional results - Power Users: Want to use own API keys for unlimited access
Secondary Users: - Support Team: Reduced image troubleshooting workload - Administrators: Manage AI model settings and API keys - Superadmins: Configure default AI models and monitor usage
System Components:
- Django API Backend: New media_assistant app
- Producer Portal Frontend: Media Assistant UI at /dashboard/media-assistant
- PostgreSQL Database: Job tracking, palette storage, asset metadata, API key storage
- Celery Workers: Async image processing tasks
- Redis: Celery broker and result backend
- S3/CloudFront: Processed image storage
- Google Gemini API: Default AI for image generation (Gemini 2.0 Flash)
- OpenAI API: Optional AI provider
- Future AI APIs: Extensible architecture for additional providers
Integration Points:
- Authentication: Django JWT + NextAuth.js session management
- Media Storage: AWS S3 (existing configuration at apps/api/infrastructure/storage.py)
- Celery: Existing Celery setup in apps/api/infrastructure/celery_app.py
- Show Management: Integration with apps/api/portal/views.py show endpoints
- Feature Flags: ProducerFeatureFlags.media_assistant_enabled
2.2 Functional Requirements¶
Must-Have Features:
1. Auto-Format Images Feature - Upload single image of any size/aspect ratio - Auto-generate all required formats: - Show Poster: 1200x1800px (2:3 aspect ratio) - Square Asset: 1200x1200px (1:1 aspect ratio) - Banner: 2160x1089px (~2:1 aspect ratio) - Smart scaling algorithm: - Scale image to fit target dimensions (preserve aspect ratio) - If too small, scale up to match height OR width (whichever reached first) - If too large, scale down to match height OR width (whichever ensures the image fits properly) - Letterbox (horizontal bars) or pillarbox (vertical bars) for extra space - Center scaled image in output canvas - Background generation: - Use uploaded image as background - Scale background image larger than output size - Apply Gaussian blur effect (sigma: 15-20) - Drop shadow effect: - Centered drop shadow on scaled foreground image - Shadow offset: 8-12px in all directions - Shadow blur radius: 20-30px - Shadow opacity: 40-50% - Preview all formats before confirming - Download individual formats or ZIP bundle
2. Color Palette Extraction Feature - Automatic color extraction from uploaded/generated images - Extract 5-8 dominant colors using k-means clustering - Generate palette in multiple formats: - Hex codes - RGB values - HSL values - Apply palette as show theme colors: - Primary color - Secondary color - Accent color - Background color - Text color - Custom palette override: - Manual color picker for each palette position - Palette requires admin/staff approval before application - Approval workflow in Django admin - Email notification on approval/rejection - Visual palette preview in show page context
3. AI Image Creation/Editing Tool - Two modes: - Create from scratch: Generate new image from text prompt - Edit existing: Upload image and apply AI modifications - Image type selection: - Show Poster (portrait) - Square Asset - Banner (landscape) - Show detail overlay system: - Checkbox toggles for: - Show title - Venue name and address - Show date(s) and time(s) - Ticket price/price range - Performer names - Call-to-action (e.g., "Get Tickets") - Auto-fetch details from show data - Manual override text input fields - Text data input: - Free-form text input for custom overlay text - Font selection (3-5 professional fonts) - Text positioning (top, center, bottom, custom) - Text color picker with auto-contrast adjustment - AI prompt input: - Large text area for detailed prompt - Prompt templates for common scenarios - Example prompts for guidance - Negative prompt support (what to avoid) - Generation options: - Style presets (realistic, artistic, modern, vintage) - Quality setting (standard, HD) - Number of variations (1-4) - Result management: - Preview all generated variations - Select best result or regenerate - Apply auto-format to AI-generated image - Save to media library
4. AI Model Management (Admin) - Global AI Settings: - Select default AI model (Gemini, OpenAI, etc.) - Store and update API keys for each provider - Enable/disable specific AI providers - Set default quality settings per provider - Configure rate limits per provider - View usage statistics and costs per provider - API Key Management: - Encrypted storage of API keys in database - Add/edit/remove API keys for providers - Test API key validity - Set API key expiration dates - API key rotation support - Provider Configuration: - Model selection per provider (e.g., Gemini 2.0 Flash vs Pro) - Quality presets per model - Cost tracking per provider - Fallback provider selection
5. User API Key Management - Personal API Keys: - Optional: Users can provide their own API keys - Per-provider key storage (Gemini, OpenAI) - Bypass application rate limits with own keys - Encrypted storage of user-provided keys - Key validation on input - Usage tracking for user's own keys - User Settings Interface: - Toggle to use personal API key vs application key - Input fields for API keys per provider - Test API key functionality - View usage with personal keys - Cost estimation for personal key usage - Clear security warnings about key storage
Should-Have Features: - Batch upload for auto-formatting multiple images - Image history/version tracking - Favorite/bookmark generated images - Share generated images with team members - Template system for common image layouts
Could-Have Features: - Video thumbnail generation - Animated GIF support - Social media format presets (Instagram, Facebook, etc.) - Background removal tool - Advanced editing (crop, rotate, filters)
Won't-Have Features (v1): - Full-featured image editor (use dedicated tools) - Video editing capabilities - Print-ready high-resolution outputs (>4K) - Multi-language text overlay support
2.3 Non-Functional Requirements¶
Performance: - Auto-format processing < 30 seconds for 3 formats - Color palette extraction < 5 seconds - AI image generation < 60 seconds (depends on AI API) - API response time < 200ms (excluding processing) - Support 50 concurrent processing jobs - Image optimization reducing file sizes by 40-60%
Reliability: - Primary processing: Celery async tasks - Fallback mechanism: Synchronous processing if Celery unavailable - Detect Celery broker connection failure - Gracefully degrade to sync processing with user notification - Process immediately in request/response cycle - Return results directly without task queue - Job retry logic: 3 attempts with exponential backoff - Graceful error handling with user-friendly messages - 99.5% success rate for image processing jobs - Automatic cleanup of failed jobs after 24 hours
Security: - JWT authentication for all endpoints - File upload validation (type, size, dimensions) - Allowed types: JPEG, PNG, WebP - Max upload size: 10MB - Max dimensions: 4000x4000px - Image content scanning (virus/malware detection) - API Key Security: - Encrypt all API keys at rest (Django Fernet encryption) - Never expose API keys in API responses - Separate encryption keys for admin vs user API keys - Audit logging for API key access/changes - Rate limiting per user for API key operations - Rate limiting: - Auto-format: 10 requests per hour per user (application keys) - AI generation: 5 requests per hour per user (application keys) - Unlimited with valid personal API keys - Palette extraction: 20 requests per hour per user - Secure S3 upload with signed URLs - Input sanitization for prompts (prevent injection attacks)
Scalability: - Horizontal scaling of Celery workers - Redis cluster support for high availability - S3 storage with unlimited capacity - Database indexing for fast job lookups - CDN integration for processed image delivery - Multi-model support prevents vendor lock-in
Monitoring & Observability: - Celery task monitoring via Flower - Processing time metrics per operation type - Error rate tracking and alerting - AI API usage and cost tracking per provider - Storage usage monitoring (S3 costs) - User adoption metrics (feature usage rates) - API key usage tracking (admin vs user keys)
3. Current State Analysis¶
3.1 Codebase Research Methodology¶
Tools Used: - File system exploration (serena MCP tools) - Symbol-based code analysis (find_symbol, get_symbols_overview) - Django app architecture review - Existing image processing code analysis - Celery task infrastructure review
3.2 Existing Architecture & Patterns¶
Backend Tech Stack: - Framework: Django 5.1.13 - Database: PostgreSQL with connection pooling - ORM: Django ORM with select_related/prefetch_related optimizations - Storage: AWS S3 via django-storages 1.14.4 + boto3 1.36.20 - Task Queue: Celery 5.4.0 with Redis broker - Image Processing: Pillow 11.0.0 - REST API: Django REST Framework 3.15.2 - Authentication: djangorestframework-simplejwt 5.5.1 - CORS: django-cors-headers 4.6.0
Frontend Tech Stack: - Framework: Next.js 15.2.4 (App Router) - React: 19.0.0 - TypeScript: 5.7.2 - UI Library: Shadcn UI + Radix UI primitives - Styling: Tailwind CSS 4.0.0 - Forms: React Hook Form 7.54.1 + Zod 3.24.1 - State: Zustand 5.0.2 (where needed) - File Upload: react-dropzone 14.3.5 - Auth: NextAuth.js 4.24.11
3.3 Existing Relevant Code¶
Image Upload Components:
- ImageUploader (
apps/producer/src/components/image-uploader.tsx) - Single image upload with preview
- Dimension validation
- File size validation (default 5MB)
- Image preview with remove functionality
-
Supports File arrays and string URLs
-
FileUploader (
apps/producer/src/components/file-uploader.tsx) - React-dropzone integration
- Multiple file support
- Progress tracking
- Preview generation
- File validation
- Toast notifications
Show Media Form (apps/producer/src/features/shows/components/form/media-form.tsx)
- Implements ImageUploader for poster, square, banner
- Dimension validation per format
- YouTube URL support
- Form validation with Zod schemas
- FormData upload to Django API
Django Storage Configuration (apps/api/infrastructure/storage.py)
# AWS S3 configuration
AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME")
AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME")
AWS_S3_CUSTOM_DOMAIN = f"{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com"
AWS_S3_FILE_OVERWRITE = False
AWS_S3_Object_PARAMETERS = {"CacheControl": "max-age=86400"}
Celery Infrastructure (apps/api/infrastructure/celery_app.py)
- Celery app configured with Redis broker
- Task routing configuration
- Beat schedule for periodic tasks
- Flower monitoring enabled
- Task result backend configured
Existing Image Processing:
- Show image uploads at apps/api/portal/views.py
- Image validation in serializers
- S3 upload via Django model FileField
- Pillow used for basic validation
Feature Flag System:
- Model: ProducerFeatureFlags (apps/api/producers/models.py)
- Existing flags: knowledge_base_enabled, ai_agents_enabled, roi_calculator_enabled
- Frontend hook: useFeatureFlags() (apps/producer/src/hooks/use-feature-flags.ts)
- Navigation integration in apps/producer/src/constants/data.ts
3.4 Integration Opportunities¶
Leverage Existing Components: - Reuse ImageUploader/FileUploader for image uploads - Follow show media form validation patterns - Use existing S3 storage configuration - Integrate with Celery task infrastructure - Add to existing feature flag system
API Patterns to Follow:
- RESTful endpoints with DRF ViewSets
- JWT authentication via IsAuthenticated permission
- Serializer validation patterns
- Pagination with PageNumberPagination
- Error responses with Response(status=400) format
Frontend Patterns to Follow:
- Next.js App Router file-based routing
- Feature-based component organization (features/media-assistant/)
- Shadcn UI component composition
- React Hook Form for form state
- Zod validation schemas
- API client in lib/api.ts for requests
4. Research & Best Practices¶
4.1 Image Processing Best Practices¶
Scaling Algorithms: - Lanczos resampling: Best quality for downscaling (Pillow.LANCZOS) - Bicubic interpolation: Good quality for upscaling (Pillow.BICUBIC) - Preserve aspect ratio: Always maintain original proportions - High-quality baseline: Use quality=95 for JPEG, optimize=True for PNG
Letterbox/Pillarbox Implementation:
1. Calculate target dimensions
2. Calculate scale factor: min(target_width/img_width, target_height/img_height)
3. Scale image: new_size = (img_width * scale, img_height * scale)
4. Create canvas: Image.new('RGB', target_size)
5. Calculate paste position: ((target_w - scaled_w) / 2, (target_h - scaled_h) / 2)
6. Paste scaled image onto canvas
Blur Background Technique:
1. Create background layer larger than target (e.g., 110% of target size)
2. Apply Gaussian blur with sigma=15-20
3. Crop to exact target size (centered)
4. Composite foreground image on top
Drop Shadow Effect:
1. Create shadow layer (black/dark gray)
2. Apply Gaussian blur for soft edges
3. Offset shadow by desired pixels
4. Reduce opacity (40-50%)
5. Composite: background -> shadow -> foreground
4.2 Color Extraction Techniques¶
K-Means Clustering: - Library: scikit-learn or colorthief - Extract 5-8 dominant colors - Remove near-duplicate colors (color distance threshold) - Sort by prominence/pixel count
Color Accessibility: - Ensure sufficient contrast ratios (WCAG AA: 4.5:1 for text) - Validate color combinations before applying to show pages - Provide accessible color scheme warnings
4.3 AI Image Generation Best Practices¶
Google Gemini 2.0 Flash (Default):
- API endpoint: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent
- Model: gemini-2.0-flash-exp
- Max prompt length: 8192 tokens
- Image generation via Imagen 3
- Sizes: Flexible aspect ratios
- Quality: High (1024x1024 equivalent)
- Speed: ~5-10 seconds per image
- Cost: $0.00025 per image (250x cheaper than DALL-E)
- API Key format: AIza...
OpenAI DALL-E 3 (Optional):
- API endpoint: https://api.openai.com/v1/images/generations
- Model: dall-e-3
- Max prompt length: 4000 characters
- Sizes: 1024x1024, 1792x1024, 1024x1792
- Quality: standard or hd
- Style: vivid or natural
- Cost: ~$0.04 per standard image, ~$0.08 per HD image
Prompt Engineering: - Be specific and detailed - Include style, mood, and composition keywords - Specify aspect ratio and orientation - Use negative prompts to avoid unwanted elements - Iterate based on results
Text Overlay Best Practices: - High contrast text (white on dark, dark on light) - Stroke/outline for readability over complex backgrounds - Limit text amount (max 3-4 lines) - Use professional, readable fonts - Consistent text positioning and alignment
4.4 API Key Security Best Practices¶
Encryption at Rest: - Use Django's cryptography library (Fernet encryption) - Separate encryption keys for different key types - Rotate encryption keys periodically - Store encryption keys in environment variables (not database)
Access Control: - Admin-only access to view/edit global API keys - Users can only view/edit their own API keys - Audit logging for all API key operations - IP whitelisting for admin API key management
Key Validation: - Test API keys before saving - Validate key format per provider - Check key permissions/scopes - Monitor for revoked/expired keys
4.5 Celery Task Architecture¶
Task Design Patterns: - Idempotency: Tasks should be safe to retry - Atomic operations: Each task does one thing well - Error handling: Comprehensive try/except with logging - Progress tracking: Update job status in database - Timeout limits: Set max execution time per task
Fallback Strategy:
def process_with_fallback(job_id):
try:
# Try async Celery task
task = process_image_async.delay(job_id)
return {"task_id": task.id, "mode": "async"}
except Exception as e:
# Celery unavailable, fall back to sync
logger.warning(f"Celery unavailable: {e}. Processing synchronously.")
result = process_image_sync(job_id)
return {"result": result, "mode": "sync"}
Task Monitoring: - Track task status: PENDING, STARTED, SUCCESS, FAILURE, RETRY - Store task results in database (not just Redis) - Implement task timeouts (30-60 seconds for processing) - Set up dead letter queues for failed tasks
5. Solution Design¶
5.1 Architecture Overview¶
┌─────────────────────────────────────────────────────────────────┐
│ Producer Portal Frontend │
│ (Next.js + React + TypeScript) │
└────────────────────┬────────────────────────────────────────────┘
│ HTTPS/JWT
│
┌────────────────────▼────────────────────────────────────────────┐
│ Django REST API │
│ /api/v1/media-assistant/ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ViewSets: AutoFormat, ColorPalette, AIGeneration │ │
│ │ Admin Views: AIModelSettings, APIKeyManagement │ │
│ └────┬─────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────▼──────────────────────┐ ┌─────────────────────────┐ │
│ │ Service Layer │ │ Task Dispatcher │ │
│ │ - ImageProcessor │◄───┤ - Try Celery │ │
│ │ - ColorExtractor │ │ - Fallback Sync │ │
│ │ - AIModelRouter │ └──────────┬──────────────┘ │
│ │ - APIKeyManager │ │ │
│ └───────────────────────────┘ │ │
└──────────────────────────────────────────────┼──────────────────┘
│ │
┌────────────┼──────────────────────────┼──────────────┐
│ │ │ │
┌───────▼──────┐ ┌──▼──────────┐ ┌─────────────▼─────────┐ ┌──▼────────┐
│ PostgreSQL │ │ Redis Broker│ │ Celery Workers │ │ AWS S3 │
│ - Jobs │ │ - Tasks │ │ - image_processor │ │ - Images │
│ - Palettes │ │ - Results │ │ - color_extractor │ │ - Assets │
│ - Assets │ └─────────────┘ │ - ai_generator │ └───────────┘
│ - API Keys │ └───────────┬──────────┘
│ - Settings │ │
└──────────────┘ ┌──────▼──────────────┐
│ AI Model Router │
│ (Select Provider) │
└──────┬──────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
┌─────▼──────┐ ┌──────▼──────┐ ┌───────▼──────┐
│ Gemini API │ │ OpenAI API │ │ Future APIs │
│ (Default) │ │ (Optional) │ │ (Extensible) │
└────────────┘ └─────────────┘ └──────────────┘
5.2 Database Schema¶
Models (Location: apps/api/media_assistant/models.py)
from django.db import models
from django.contrib.auth import get_user_model
from apps.producers.models import Producer
from cryptography.fernet import Fernet
from django.conf import settings
User = get_user_model()
class MediaAsset(models.Model):
"""Stores uploaded and processed images"""
ASSET_TYPE_CHOICES = [
('original', 'Original Upload'),
('poster', 'Show Poster (1200x1800)'),
('square', 'Square Asset (1200x1200)'),
('banner', 'Banner (2160x1089)'),
('ai_generated', 'AI Generated'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
producer = models.ForeignKey(Producer, on_delete=models.CASCADE, related_name='media_assets')
asset_type = models.CharField(max_length=20, choices=ASSET_TYPE_CHOICES)
file = models.ImageField(upload_to='media_assistant/%Y/%m/')
filename = models.CharField(max_length=255)
file_size = models.IntegerField() # bytes
width = models.IntegerField()
height = models.IntegerField()
format = models.CharField(max_length=10) # 'JPEG', 'PNG', 'WebP'
# Relationships
parent_asset = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL,
related_name='derived_assets')
processing_job = models.ForeignKey('ProcessingJob', null=True, blank=True,
on_delete=models.SET_NULL, related_name='output_assets')
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'media_assistant_assets'
ordering = ['-created_at']
indexes = [
models.Index(fields=['producer', '-created_at']),
models.Index(fields=['asset_type']),
]
class ProcessingJob(models.Model):
"""Tracks image processing jobs"""
JOB_TYPE_CHOICES = [
('auto_format', 'Auto Format Images'),
('color_extraction', 'Color Palette Extraction'),
('ai_generation', 'AI Image Generation'),
('ai_edit', 'AI Image Edit'),
]
STATUS_CHOICES = [
('pending', 'Pending'),
('processing', 'Processing'),
('completed', 'Completed'),
('failed', 'Failed'),
('cancelled', 'Cancelled'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
producer = models.ForeignKey(Producer, on_delete=models.CASCADE, related_name='processing_jobs')
job_type = models.CharField(max_length=20, choices=JOB_TYPE_CHOICES)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
# Input
input_asset = models.ForeignKey(MediaAsset, null=True, blank=True,
on_delete=models.SET_NULL, related_name='source_jobs')
input_data = models.JSONField(default=dict) # Job-specific parameters
# Processing
celery_task_id = models.CharField(max_length=255, null=True, blank=True)
processing_mode = models.CharField(max_length=10, choices=[('async', 'Async'), ('sync', 'Sync')])
started_at = models.DateTimeField(null=True, blank=True)
completed_at = models.DateTimeField(null=True, blank=True)
# AI-specific (which model/key was used)
ai_provider = models.CharField(max_length=50, null=True, blank=True) # 'gemini', 'openai'
used_user_api_key = models.BooleanField(default=False) # Track if user's own key was used
# Results
result_data = models.JSONField(null=True, blank=True) # Job results
error_message = models.TextField(null=True, blank=True)
retry_count = models.IntegerField(default=0)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'media_assistant_jobs'
ordering = ['-created_at']
indexes = [
models.Index(fields=['producer', '-created_at']),
models.Index(fields=['status', 'job_type']),
models.Index(fields=['celery_task_id']),
models.Index(fields=['ai_provider']),
]
class ColorPalette(models.Model):
"""Stores extracted color palettes"""
STATUS_CHOICES = [
('pending_approval', 'Pending Approval'),
('approved', 'Approved'),
('rejected', 'Rejected'),
('active', 'Active'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
producer = models.ForeignKey(Producer, on_delete=models.CASCADE, related_name='color_palettes')
source_asset = models.ForeignKey(MediaAsset, on_delete=models.CASCADE, related_name='color_palettes')
# Colors (stored as JSON array of hex codes)
colors = models.JSONField() # ['#FF5733', '#33FF57', ...]
primary_color = models.CharField(max_length=7) # Hex color
secondary_color = models.CharField(max_length=7)
accent_color = models.CharField(max_length=7)
background_color = models.CharField(max_length=7)
text_color = models.CharField(max_length=7)
# Approval workflow
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending_approval')
reviewed_by = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
reviewed_at = models.DateTimeField(null=True, blank=True)
rejection_reason = models.TextField(null=True, blank=True)
# Applied to show
applied_to_show = models.ForeignKey('portal.Show', null=True, blank=True,
on_delete=models.SET_NULL, related_name='color_palette')
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'media_assistant_palettes'
ordering = ['-created_at']
indexes = [
models.Index(fields=['producer', '-created_at']),
models.Index(fields=['status']),
]
class AIModelSettings(models.Model):
"""Global AI model configuration (Admin only)"""
PROVIDER_CHOICES = [
('gemini', 'Google Gemini'),
('openai', 'OpenAI DALL-E'),
]
MODEL_CHOICES = [
('gemini-2.0-flash-exp', 'Gemini 2.0 Flash'),
('gemini-2.0-pro-exp', 'Gemini 2.0 Pro'),
('dall-e-3', 'DALL-E 3'),
('dall-e-2', 'DALL-E 2'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
provider = models.CharField(max_length=50, choices=PROVIDER_CHOICES, unique=True)
model_name = models.CharField(max_length=100, choices=MODEL_CHOICES)
# Encrypted API key
encrypted_api_key = models.TextField() # Fernet encrypted
# Settings
is_enabled = models.BooleanField(default=True)
is_default = models.BooleanField(default=False) # Only one can be default
# Configuration
default_quality = models.CharField(max_length=20, default='standard')
max_requests_per_hour = models.IntegerField(default=100) # Global limit
# Cost tracking
cost_per_image = models.DecimalField(max_digits=10, decimal_places=6, default=0.0)
monthly_budget = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
current_month_spend = models.DecimalField(max_digits=10, decimal_places=2, default=0.0)
# Testing
last_tested_at = models.DateTimeField(null=True, blank=True)
test_status = models.CharField(max_length=20, null=True, blank=True) # 'success', 'failed'
test_error = models.TextField(null=True, blank=True)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
class Meta:
db_table = 'media_assistant_ai_settings'
verbose_name = 'AI Model Setting'
verbose_name_plural = 'AI Model Settings'
def save(self, *args, **kwargs):
# Ensure only one default provider
if self.is_default:
AIModelSettings.objects.filter(is_default=True).update(is_default=False)
super().save(*args, **kwargs)
def set_api_key(self, plain_key: str):
"""Encrypt and store API key"""
fernet = Fernet(settings.MEDIA_ASSISTANT_ENCRYPTION_KEY.encode())
self.encrypted_api_key = fernet.encrypt(plain_key.encode()).decode()
def get_api_key(self) -> str:
"""Decrypt and return API key"""
fernet = Fernet(settings.MEDIA_ASSISTANT_ENCRYPTION_KEY.encode())
return fernet.decrypt(self.encrypted_api_key.encode()).decode()
class UserAPIKey(models.Model):
"""User-provided API keys (optional, bypasses rate limits)"""
PROVIDER_CHOICES = [
('gemini', 'Google Gemini'),
('openai', 'OpenAI'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
producer = models.ForeignKey(Producer, on_delete=models.CASCADE, related_name='api_keys')
provider = models.CharField(max_length=50, choices=PROVIDER_CHOICES)
# Encrypted API key
encrypted_api_key = models.TextField() # Fernet encrypted (separate key from admin)
# Settings
is_active = models.BooleanField(default=True)
# Validation
last_validated_at = models.DateTimeField(null=True, blank=True)
validation_status = models.CharField(max_length=20, null=True, blank=True)
validation_error = models.TextField(null=True, blank=True)
# Usage tracking (for user's reference)
total_requests = models.IntegerField(default=0)
total_cost_estimate = models.DecimalField(max_digits=10, decimal_places=2, default=0.0)
last_used_at = models.DateTimeField(null=True, blank=True)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'media_assistant_user_api_keys'
unique_together = [['producer', 'provider']]
indexes = [
models.Index(fields=['producer', 'provider']),
]
def set_api_key(self, plain_key: str):
"""Encrypt and store user API key"""
fernet = Fernet(settings.MEDIA_ASSISTANT_USER_KEY_ENCRYPTION.encode())
self.encrypted_api_key = fernet.encrypt(plain_key.encode()).decode()
def get_api_key(self) -> str:
"""Decrypt and return user API key"""
fernet = Fernet(settings.MEDIA_ASSISTANT_USER_KEY_ENCRYPTION.encode())
return fernet.decrypt(self.encrypted_api_key.encode()).decode()
class AIImageTemplate(models.Model):
"""Predefined templates for AI image generation"""
TEMPLATE_TYPE_CHOICES = [
('poster', 'Show Poster'),
('square', 'Square Asset'),
('banner', 'Banner'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
name = models.CharField(max_length=100)
template_type = models.CharField(max_length=20, choices=TEMPLATE_TYPE_CHOICES)
description = models.TextField()
# Layout configuration
text_positions = models.JSONField() # Positions for show details
default_prompt = models.TextField()
style_preset = models.CharField(max_length=50)
# Metadata
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'media_assistant_templates'
ordering = ['name']
5.3 API Endpoints¶
Base URL: /api/v1/media-assistant/
Endpoints:
POST /auto-format/
- Upload image and auto-format to all sizes
- Request: multipart/form-data with image file
- Response: Job ID and status
GET /auto-format/{job_id}/
- Get auto-format job status and results
- Response: Job details with download URLs for all formats
POST /auto-format/{job_id}/download/
- Download ZIP of all formatted images
- Response: ZIP file
POST /color-palette/extract/
- Extract color palette from image
- Request: {"asset_id": "uuid"} or multipart image upload
- Response: Palette data with hex codes
POST /color-palette/
- Create custom color palette (requires approval)
- Request: Palette colors and assignments
- Response: Palette object with pending_approval status
GET /color-palette/
- List user's color palettes
- Query params: ?status=approved&show_id=123
PATCH /color-palette/{id}/
- Update custom palette
POST /color-palette/{id}/apply-to-show/
- Apply approved palette to show
- Request: {"show_id": 123}
POST /ai-generate/
- Generate AI image
- Request: {
"mode": "create" | "edit",
"image_type": "poster" | "square" | "banner",
"prompt": "text",
"negative_prompt": "text",
"show_id": 123, // optional
"show_details": {
"include_title": true,
"include_venue": true,
"include_dates": false,
"custom_text": "Limited Tickets!"
},
"style_preset": "modern",
"quality": "hd",
"num_variations": 2,
"use_own_api_key": false // NEW: use user's API key
}
- Response: Job ID
GET /ai-generate/{job_id}/
- Get AI generation job status
- Response: Job status with generated image URLs
POST /ai-generate/{job_id}/select/
- Select variation and optionally auto-format
- Request: {"variation_index": 0, "auto_format": true}
# User API Key Management
GET /api-keys/
- List user's API keys
- Response: Array of API keys (encrypted keys not returned)
POST /api-keys/
- Add user API key
- Request: {"provider": "gemini", "api_key": "..."}
- Response: API key object (validates before saving)
PATCH /api-keys/{provider}/
- Update user API key
- Request: {"api_key": "...", "is_active": true}
DELETE /api-keys/{provider}/
- Delete user API key
POST /api-keys/{provider}/validate/
- Validate user API key
- Response: {"valid": true/false, "error": "..."}
# Admin-only endpoints (staff required)
GET /admin/ai-models/
- List all AI model settings
POST /admin/ai-models/
- Add new AI model provider
- Request: {"provider": "gemini", "model_name": "...", "api_key": "..."}
PATCH /admin/ai-models/{provider}/
- Update AI model settings
POST /admin/ai-models/{provider}/test/
- Test API key validity
POST /admin/ai-models/{provider}/set-default/
- Set as default AI provider
GET /admin/usage-stats/
- Get usage statistics per provider
- Query params: ?start_date=2025-01-01&end_date=2025-01-31
# Asset management
GET /assets/
- List user's media assets
- Query params: ?asset_type=poster&limit=20&offset=0
GET /assets/{id}/
- Get asset details
DELETE /assets/{id}/
- Delete asset
GET /jobs/
- List user's processing jobs
- Query params: ?job_type=auto_format&status=completed
GET /jobs/{id}/
- Get job details
POST /jobs/{id}/retry/
- Retry failed job
DELETE /jobs/{id}/
- Cancel pending/processing job
5.4 Service Layer Architecture¶
Location: apps/api/media_assistant/services/
1. AI Model Router (ai_model_router.py)
class AIModelRouter:
"""Routes AI requests to appropriate provider"""
def get_provider(self, producer: Producer, use_own_key: bool = False) -> tuple:
"""
Get AI provider and API key for request
Returns:
(provider_name, api_key, model_name)
"""
if use_own_key:
# Check if user has own API key
user_key = UserAPIKey.objects.filter(
producer=producer,
is_active=True
).first()
if user_key:
return (
user_key.provider,
user_key.get_api_key(),
self._get_model_for_provider(user_key.provider)
)
# Use application default
default_settings = AIModelSettings.objects.filter(
is_default=True,
is_enabled=True
).first()
if not default_settings:
raise ValueError("No default AI provider configured")
return (
default_settings.provider,
default_settings.get_api_key(),
default_settings.model_name
)
def _get_model_for_provider(self, provider: str) -> str:
"""Get default model name for provider"""
models = {
'gemini': 'gemini-2.0-flash-exp',
'openai': 'dall-e-3',
}
return models.get(provider)
2. Image Processor Service (image_processor.py)
class ImageProcessorService:
"""Handles image auto-formatting with letterbox and blur background"""
TARGET_FORMATS = {
'poster': {'width': 1200, 'height': 1800},
'square': {'width': 1200, 'height': 1200},
'banner': {'width': 2160, 'height': 1089},
}
def process_image(self, input_image_path: str, output_format: str) -> Image:
"""
Main processing pipeline:
1. Load and validate image
2. Calculate scaling parameters
3. Generate blur background
4. Scale foreground image
5. Create drop shadow
6. Composite layers
7. Optimize and save
"""
def create_scaled_image(self, img: Image, target_size: tuple) -> Image:
"""Scale image preserving aspect ratio"""
def create_blur_background(self, img: Image, target_size: tuple) -> Image:
"""Create blurred background layer"""
def create_drop_shadow(self, img: Image) -> Image:
"""Generate drop shadow effect"""
def composite_layers(self, background: Image, shadow: Image,
foreground: Image, target_size: tuple) -> Image:
"""Composite all layers into final image"""
3. Color Extractor Service (color_extractor.py)
class ColorExtractorService:
"""Extracts color palettes from images"""
def extract_palette(self, image_path: str, num_colors: int = 6) -> List[str]:
"""Extract dominant colors using k-means clustering"""
def assign_color_roles(self, colors: List[str]) -> dict:
"""Assign colors to roles (primary, secondary, accent, etc.)"""
def validate_accessibility(self, palette: dict) -> dict:
"""Check WCAG contrast ratios and provide warnings"""
4. AI Image Generator Service (ai_generator.py)
class AIImageGeneratorService:
"""Handles multi-provider AI image generation"""
def generate_image(self, provider: str, api_key: str, model: str,
prompt: str, **kwargs) -> str:
"""
Route to appropriate provider
Providers:
- gemini: Google Gemini 2.0 Flash (default)
- openai: OpenAI DALL-E 3
"""
if provider == 'gemini':
return self._generate_gemini(api_key, model, prompt, **kwargs)
elif provider == 'openai':
return self._generate_openai(api_key, model, prompt, **kwargs)
else:
raise ValueError(f"Unsupported provider: {provider}")
def _generate_gemini(self, api_key: str, model: str, prompt: str,
size: str = "1024x1024") -> str:
"""Generate image with Google Gemini"""
import google.generativeai as genai
genai.configure(api_key=api_key)
model = genai.GenerativeModel(model)
response = model.generate_content(
[prompt],
generation_config={
"temperature": 0.9,
"top_p": 1,
"top_k": 32,
"max_output_tokens": 2048,
}
)
# Extract image from response
image_url = response.candidates[0].content.parts[0].inline_data.data
return image_url
def _generate_openai(self, api_key: str, model: str, prompt: str,
size: str = "1024x1024", quality: str = "standard") -> str:
"""Generate image with OpenAI DALL-E"""
import openai
client = openai.OpenAI(api_key=api_key)
response = client.images.generate(
model=model,
prompt=prompt,
size=size,
quality=quality,
n=1,
)
return response.data[0].url
def build_prompt(self, user_prompt: str, show_details: dict,
template: AIImageTemplate) -> str:
"""Construct optimized prompt from user input and show data"""
def overlay_text(self, image: Image, show_details: dict,
layout: dict) -> Image:
"""Add text overlays to generated image"""
def estimate_cost(self, provider: str, quality: str, num_variations: int) -> float:
"""Calculate estimated API cost per provider"""
costs = {
'gemini': {'standard': 0.00025, 'hd': 0.0005},
'openai': {'standard': 0.04, 'hd': 0.08},
}
cost_per_image = costs.get(provider, {}).get(quality, 0)
return cost_per_image * num_variations
5. Task Dispatcher (task_dispatcher.py)
class TaskDispatcher:
"""Manages task execution with Celery fallback"""
def dispatch(self, job_type: str, job_id: str, **kwargs) -> dict:
"""
Try to dispatch to Celery, fall back to sync if unavailable
Returns:
{
'mode': 'async' | 'sync',
'task_id': str (if async),
'result': dict (if sync)
}
"""
try:
task = self._dispatch_celery(job_type, job_id, **kwargs)
return {'mode': 'async', 'task_id': task.id}
except Exception as e:
logger.warning(f"Celery unavailable: {e}. Falling back to sync processing.")
result = self._dispatch_sync(job_type, job_id, **kwargs)
return {'mode': 'sync', 'result': result}
def _dispatch_celery(self, job_type: str, job_id: str, **kwargs):
"""Dispatch to Celery worker"""
def _dispatch_sync(self, job_type: str, job_id: str, **kwargs):
"""Execute synchronously in current process"""
5.5 Celery Tasks¶
Location: apps/api/media_assistant/tasks.py
from celery import shared_task
from .models import ProcessingJob
from .services import (
ImageProcessorService,
ColorExtractorService,
AIImageGeneratorService,
AIModelRouter
)
@shared_task(bind=True, max_retries=3, soft_time_limit=60)
def auto_format_image(self, job_id: str):
"""Celery task for auto-formatting images"""
# Implementation similar to previous version
@shared_task(bind=True, max_retries=3, soft_time_limit=30)
def extract_color_palette(self, job_id: str):
"""Celery task for color palette extraction"""
# Implementation similar to previous version
@shared_task(bind=True, max_retries=2, soft_time_limit=90)
def generate_ai_image(self, job_id: str):
"""Celery task for AI image generation with multi-provider support"""
try:
job = ProcessingJob.objects.get(id=job_id)
job.status = 'processing'
job.started_at = timezone.now()
job.save()
# Get AI provider and API key
router = AIModelRouter()
use_own_key = job.input_data.get('use_own_api_key', False)
provider, api_key, model = router.get_provider(job.producer, use_own_key)
# Track which provider/key was used
job.ai_provider = provider
job.used_user_api_key = use_own_key
job.save()
# Generate image
generator = AIImageGeneratorService()
prompt = job.input_data['prompt']
image_url = generator.generate_image(
provider=provider,
api_key=api_key,
model=model,
prompt=prompt,
size=job.input_data.get('size', '1024x1024'),
quality=job.input_data.get('quality', 'standard')
)
# Download and save image
# Create MediaAsset records
# Update job with results
job.status = 'completed'
job.completed_at = timezone.now()
job.result_data = {'image_url': image_url, 'provider': provider}
job.save()
return job.result_data
except Exception as exc:
job.status = 'failed'
job.error_message = str(exc)
job.save()
raise self.retry(exc=exc, countdown=2 ** self.request.retries)
5.6 Frontend Architecture¶
Route Structure:
apps/producer/src/app/dashboard/media-assistant/
├── page.tsx # Main landing page
├── layout.tsx # Media Assistant layout
├── settings/
│ └── page.tsx # NEW: API key settings
├── auto-format/
│ ├── page.tsx # Auto-format upload page
│ └── [jobId]/
│ └── page.tsx # Job status and results
├── color-palette/
│ ├── page.tsx # Palette extraction/management
│ └── [paletteId]/
│ └── page.tsx # Palette details and approval status
├── ai-generator/
│ ├── page.tsx # AI image creation interface
│ └── [jobId]/
│ └── page.tsx # AI job status and variations
└── library/
├── page.tsx # Media asset library
└── [assetId]/
└── page.tsx # Asset details
Feature Module Structure:
apps/producer/src/features/media-assistant/
├── components/
│ ├── settings/ # NEW
│ │ ├── api-key-form.tsx
│ │ ├── api-key-list.tsx
│ │ ├── api-key-test.tsx
│ │ └── usage-stats.tsx
│ ├── auto-format/
│ │ ├── upload-form.tsx
│ │ ├── format-preview.tsx
│ │ └── download-options.tsx
│ ├── color-palette/
│ │ ├── palette-extractor.tsx
│ │ ├── palette-picker.tsx
│ │ ├── color-swatch.tsx
│ │ └── approval-status.tsx
│ ├── ai-generator/
│ │ ├── mode-selector.tsx # Create vs Edit
│ │ ├── image-type-selector.tsx # Poster/Square/Banner
│ │ ├── provider-selector.tsx # NEW: Select Gemini/OpenAI
│ │ ├── api-key-toggle.tsx # NEW: Use own key toggle
│ │ ├── show-details-form.tsx # Show data toggles
│ │ ├── prompt-editor.tsx
│ │ ├── style-preset-picker.tsx
│ │ ├── cost-estimator.tsx # NEW: Show cost estimate
│ │ ├── variation-gallery.tsx
│ │ └── generation-preview.tsx
│ ├── shared/
│ │ ├── job-status-card.tsx
│ │ ├── processing-indicator.tsx
│ │ ├── asset-thumbnail.tsx
│ │ └── format-badge.tsx
│ └── library/
│ ├── asset-grid.tsx
│ ├── asset-filters.tsx
│ └── asset-card.tsx
├── hooks/
│ ├── use-auto-format.ts
│ ├── use-color-palette.ts
│ ├── use-ai-generator.ts
│ ├── use-api-keys.ts # NEW
│ ├── use-job-polling.ts
│ └── use-media-assets.ts
├── types/
│ ├── assets.ts
│ ├── jobs.ts
│ ├── palette.ts
│ └── api-keys.ts # NEW
└── api/
├── auto-format.ts
├── color-palette.ts
├── ai-generator.ts
├── api-keys.ts # NEW
└── assets.ts
Key New Components:
1. API Key Settings Form
// apps/producer/src/features/media-assistant/components/settings/api-key-form.tsx
export function APIKeyForm({ provider }: { provider: 'gemini' | 'openai' }) {
const { addKey, updateKey, testKey } = useAPIKeys();
const [isTestingKey, setIsTestingKey] = useState(false);
const handleTest = async (apiKey: string) => {
setIsTestingKey(true);
const result = await testKey(provider, apiKey);
setIsTestingKey(false);
return result;
};
return (
<Form>
<FormField
name="apiKey"
render={({ field }) => (
<FormItem>
<FormLabel>
{provider === 'gemini' ? 'Gemini' : 'OpenAI'} API Key
</FormLabel>
<FormControl>
<Input
type="password"
placeholder="sk-..."
{...field}
/>
</FormControl>
<FormDescription>
Your API key will be encrypted and stored securely.
Using your own key bypasses rate limits.
</FormDescription>
</FormItem>
)}
/>
<Button type="button" onClick={() => handleTest(form.getValues('apiKey'))}>
Test Key
</Button>
<Button type="submit">Save Key</Button>
</Form>
);
}
2. AI Provider Selector with Cost Display
// apps/producer/src/features/media-assistant/components/ai-generator/provider-selector.tsx
export function ProviderSelector() {
const { apiKeys } = useAPIKeys();
const [selectedProvider, setSelectedProvider] = useState<'gemini' | 'openai'>('gemini');
const [useOwnKey, setUseOwnKey] = useState(false);
const hasGeminiKey = apiKeys.some(k => k.provider === 'gemini' && k.is_active);
const hasOpenAIKey = apiKeys.some(k => k.provider === 'openai' && k.is_active);
return (
<div className="space-y-4">
<RadioGroup value={selectedProvider} onValueChange={setSelectedProvider}>
<div className="flex items-center space-x-2">
<RadioGroupItem value="gemini" id="gemini" />
<Label htmlFor="gemini">
Gemini 2.0 Flash (Default) - $0.00025/image
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="openai" id="openai" />
<Label htmlFor="openai">
OpenAI DALL-E 3 - $0.04/image
</Label>
</div>
</RadioGroup>
{(hasGeminiKey || hasOpenAIKey) && (
<div className="flex items-center space-x-2">
<Switch
checked={useOwnKey}
onCheckedChange={setUseOwnKey}
/>
<Label>Use my own API key (unlimited)</Label>
</div>
)}
{useOwnKey && !hasGeminiKey && selectedProvider === 'gemini' && (
<Alert>
<AlertDescription>
You don't have a Gemini API key saved.{' '}
<Link href="/dashboard/media-assistant/settings">Add one now</Link>
</AlertDescription>
</Alert>
)}
</div>
);
}
5.7 Feature Flag Integration¶
Backend:
# apps/api/producers/models.py
class ProducerFeatureFlags(models.Model):
# ... existing flags
media_assistant_enabled = models.BooleanField(default=False)
Frontend:
// apps/producer/src/constants/data.ts
export const addOnNavItems: NavItem[] = [
// ... existing items
{
title: 'Media Assistant',
url: '/dashboard/media-assistant',
icon: 'wand',
featureFlag: 'mediaAssistantEnabled'
},
];
6. Implementation Plan¶
6.1 Development Phases¶
Phase 1: Core Infrastructure & Auto-Format (Weeks 1-2)
Backend Tasks:
- [ ] Create media_assistant Django app
- [ ] Define models: MediaAsset, ProcessingJob, AIModelSettings, UserAPIKey
- [ ] Implement encryption utilities for API keys
- [ ] Implement ImageProcessorService:
- [ ] Scaling algorithm
- [ ] Letterbox/pillarbox logic
- [ ] Blur background generation
- [ ] Drop shadow effect
- [ ] Layer compositing
- [ ] Create Celery task: auto_format_image
- [ ] Implement TaskDispatcher with sync fallback
- [ ] Create API endpoints: /auto-format/
- [ ] Add feature flag: media_assistant_enabled
- [ ] Write unit tests for image processing
Frontend Tasks:
- [ ] Create route: /dashboard/media-assistant/auto-format
- [ ] Build AutoFormatUploadForm component
- [ ] Build FormatPreviewGrid component
- [ ] Build JobStatusCard with polling
- [ ] Build DownloadOptions component (individual + ZIP)
- [ ] Implement useAutoFormat hook
- [ ] Implement useJobPolling hook
- [ ] Add to navigation with feature flag
- [ ] Write component tests
Phase 2: AI Model Management & Settings (Weeks 3-4)
Backend Tasks:
- [ ] Implement AIModelRouter service
- [ ] Create admin interface for AIModelSettings
- [ ] Implement API key encryption/decryption
- [ ] Create API endpoints: /admin/ai-models/
- [ ] Implement Gemini API integration
- [ ] Implement OpenAI API integration
- [ ] Add API key validation endpoints
- [ ] Add usage tracking and cost calculation
- [ ] Write unit tests (mock AI API responses)
Frontend Tasks:
- [ ] Create route: /dashboard/media-assistant/settings
- [ ] Build APIKeyForm component
- [ ] Build APIKeyList component
- [ ] Build APIKeyTest component
- [ ] Build UsageStats component
- [ ] Implement useAPIKeys hook
- [ ] Add security warnings and help text
- [ ] Write component tests
Phase 3: AI Image Generation (Weeks 4-5)
Backend Tasks:
- [ ] Add model: AIImageTemplate
- [ ] Implement AIImageGeneratorService:
- [ ] Multi-provider routing
- [ ] Prompt construction
- [ ] Text overlay rendering
- [ ] Cost estimation
- [ ] Create Celery task: generate_ai_image
- [ ] Create API endpoints: /ai-generate/
- [ ] Implement rate limiting (respects user API keys)
- [ ] Create default templates
- [ ] Write unit tests
Frontend Tasks:
- [ ] Create route: /dashboard/media-assistant/ai-generator
- [ ] Build ModeSelector component (Create/Edit)
- [ ] Build ImageTypeSelector component
- [ ] Build ProviderSelector component
- [ ] Build APIKeyToggle component
- [ ] Build ShowDetailsForm component
- [ ] Build PromptEditor component
- [ ] Build StylePresetPicker component
- [ ] Build CostEstimator component
- [ ] Build VariationGallery component
- [ ] Implement useAIGenerator hook
- [ ] Integrate with auto-format flow
- [ ] Write component tests
Phase 4: Color Palette Extraction (Week 6)
Backend Tasks:
- [ ] Add model: ColorPalette
- [ ] Implement ColorExtractorService:
- [ ] K-means color extraction
- [ ] Color role assignment
- [ ] Accessibility validation
- [ ] Create Celery task: extract_color_palette
- [ ] Create API endpoints: /color-palette/
- [ ] Implement approval workflow in Django admin
- [ ] Add email notifications for approvals
- [ ] Write unit tests
Frontend Tasks:
- [ ] Create route: /dashboard/media-assistant/color-palette
- [ ] Build PaletteExtractor component
- [ ] Build PalettePicker component (custom colors)
- [ ] Build ColorSwatch component
- [ ] Build ApprovalStatus indicator
- [ ] Implement useColorPalette hook
- [ ] Integrate palette application to shows
- [ ] Write component tests
Phase 5: Media Library & Polish (Weeks 7-8)
Backend Tasks: - [ ] Optimize database queries (select_related, prefetch_related) - [ ] Add pagination to asset listings - [ ] Implement asset search/filtering - [ ] Add cleanup task for old assets (Celery beat) - [ ] Performance testing and optimization - [ ] Security audit (especially API key handling) - [ ] API documentation (OpenAPI/Swagger)
Frontend Tasks:
- [ ] Create route: /dashboard/media-assistant/library
- [ ] Build AssetGrid component
- [ ] Build AssetFilters component
- [ ] Build AssetCard component
- [ ] Implement asset deletion
- [ ] Add loading skeletons
- [ ] Error boundary implementation
- [ ] Accessibility audit
- [ ] Mobile responsiveness testing
- [ ] User documentation
6.2 File Change Summary¶
New Files (Backend):
apps/api/media_assistant/
├── __init__.py
├── admin.py # Django admin for models + AI settings
├── apps.py
├── models.py # All models including AIModelSettings, UserAPIKey
├── serializers.py # DRF serializers
├── views.py # API ViewSets + admin views
├── urls.py # URL routing
├── tasks.py # Celery tasks
├── permissions.py # Custom permissions
├── services/
│ ├── __init__.py
│ ├── image_processor.py # ImageProcessorService
│ ├── color_extractor.py # ColorExtractorService
│ ├── ai_generator.py # AIImageGeneratorService (multi-provider)
│ ├── ai_model_router.py # NEW: AIModelRouter
│ ├── api_key_manager.py # NEW: API key encryption/validation
│ └── task_dispatcher.py # TaskDispatcher
├── utils/
│ ├── __init__.py
│ └── encryption.py # NEW: Encryption utilities
├── tests/
│ ├── __init__.py
│ ├── test_models.py
│ ├── test_views.py
│ ├── test_tasks.py
│ ├── test_image_processor.py
│ ├── test_color_extractor.py
│ ├── test_ai_generator.py
│ ├── test_ai_model_router.py # NEW
│ └── test_api_key_manager.py # NEW
└── migrations/
└── 0001_initial.py
Modified Files (Backend):
apps/api/api/settings.py
- Add 'media_assistant' to INSTALLED_APPS
- Add MEDIA_ASSISTANT_ENCRYPTION_KEY (Fernet key)
- Add MEDIA_ASSISTANT_USER_KEY_ENCRYPTION (separate Fernet key)
apps/api/api/urls.py
- Include media_assistant URLs
apps/api/producers/models.py
- Add media_assistant_enabled field to ProducerFeatureFlags
apps/api/producers/migrations/
- New migration for feature flag
apps/api/requirements.txt
- Add scikit-learn==1.4.0
- Add colorthief==0.2.1
- Add google-generativeai==0.3.0
- Add openai==1.12.0 (optional)
- Add cryptography==42.0.0 (for Fernet encryption)
New Files (Frontend):
apps/producer/src/app/dashboard/media-assistant/
├── page.tsx
├── layout.tsx
├── settings/ # NEW
│ └── page.tsx
├── auto-format/
│ ├── page.tsx
│ └── [jobId]/
│ └── page.tsx
├── color-palette/
│ ├── page.tsx
│ └── [paletteId]/
│ └── page.tsx
├── ai-generator/
│ ├── page.tsx
│ └── [jobId]/
│ └── page.tsx
└── library/
├── page.tsx
└── [assetId]/
└── page.tsx
apps/producer/src/features/media-assistant/
├── components/
│ ├── settings/ # NEW
│ │ ├── api-key-form.tsx
│ │ ├── api-key-list.tsx
│ │ ├── api-key-test.tsx
│ │ └── usage-stats.tsx
│ ├── auto-format/
│ │ ├── upload-form.tsx
│ │ ├── format-preview.tsx
│ │ └── download-options.tsx
│ ├── color-palette/
│ │ ├── palette-extractor.tsx
│ │ ├── palette-picker.tsx
│ │ ├── color-swatch.tsx
│ │ └── approval-status.tsx
│ ├── ai-generator/
│ │ ├── mode-selector.tsx
│ │ ├── image-type-selector.tsx
│ │ ├── provider-selector.tsx # NEW
│ │ ├── api-key-toggle.tsx # NEW
│ │ ├── show-details-form.tsx
│ │ ├── prompt-editor.tsx
│ │ ├── style-preset-picker.tsx
│ │ ├── cost-estimator.tsx # NEW
│ │ ├── variation-gallery.tsx
│ │ └── generation-preview.tsx
│ ├── shared/
│ │ ├── job-status-card.tsx
│ │ ├── processing-indicator.tsx
│ │ ├── asset-thumbnail.tsx
│ │ └── format-badge.tsx
│ └── library/
│ ├── asset-grid.tsx
│ ├── asset-filters.tsx
│ └── asset-card.tsx
├── hooks/
│ ├── use-auto-format.ts
│ ├── use-color-palette.ts
│ ├── use-ai-generator.ts
│ ├── use-api-keys.ts # NEW
│ ├── use-job-polling.ts
│ └── use-media-assets.ts
├── types/
│ ├── assets.ts
│ ├── jobs.ts
│ ├── palette.ts
│ └── api-keys.ts # NEW
├── api/
│ ├── auto-format.ts
│ ├── color-palette.ts
│ ├── ai-generator.ts
│ ├── api-keys.ts # NEW
│ └── assets.ts
└── __tests__/
├── components/
└── hooks/
Modified Files (Frontend):
apps/producer/src/constants/data.ts
- Add Media Assistant to addOnNavItems
apps/producer/src/hooks/use-feature-flags.ts
- Type update for mediaAssistantEnabled flag
apps/producer/src/types/feature-flags.ts
- Add mediaAssistantEnabled: boolean
6.3 Task Breakdown by Role¶
Backend Developer (Django/Python):
| Task | Estimate | Priority |
|---|---|---|
| Create media_assistant app structure | 2h | P0 |
| Define database models (including API key models) | 6h | P0 |
| Implement encryption utilities | 3h | P0 |
| Implement ImageProcessorService | 12h | P0 |
| Implement ColorExtractorService | 8h | P1 |
| Implement AIModelRouter | 6h | P0 |
| Implement Gemini API integration | 8h | P0 |
| Implement OpenAI API integration | 6h | P1 |
| Implement AIImageGeneratorService | 10h | P0 |
| Create Celery tasks with retry logic | 8h | P0 |
| Implement TaskDispatcher with fallback | 4h | P0 |
| Create API ViewSets and serializers | 12h | P0 |
| Create admin interface for AI settings | 6h | P0 |
| Implement API key validation | 4h | P0 |
| Add feature flag to ProducerFeatureFlags | 1h | P0 |
| Implement approval workflow | 4h | P1 |
| Write unit tests (80% coverage) | 20h | P0 |
| API documentation | 4h | P2 |
| Performance optimization | 6h | P1 |
| Security audit (especially API keys) | 6h | P0 |
Frontend Developer (React/TypeScript):
| Task | Estimate | Priority |
|---|---|---|
| Set up route structure | 3h | P0 |
| Create feature module structure | 2h | P0 |
| Build API key settings components | 12h | P0 |
| Build auto-format components | 12h | P0 |
| Build color palette components | 10h | P1 |
| Build AI generator components | 16h | P0 |
| Build provider selector & cost display | 6h | P0 |
| Build media library components | 8h | P1 |
| Implement hooks (API integration) | 12h | P0 |
| Add job polling with SWR | 4h | P0 |
| Integrate with navigation/feature flags | 2h | P0 |
| Implement error handling | 4h | P0 |
| Add loading states and skeletons | 6h | P1 |
| Write component tests | 14h | P0 |
| Accessibility improvements | 6h | P1 |
| Mobile responsiveness | 8h | P1 |
DevOps/Infrastructure:
| Task | Estimate | Priority |
|---|---|---|
| Generate and configure encryption keys | 2h | P0 |
| Set up Celery worker scaling | 3h | P0 |
| Configure Redis for high availability | 4h | P1 |
| S3 bucket configuration for media_assistant | 2h | P0 |
| Set up monitoring (Flower, Sentry) | 3h | P1 |
| Database migration in production | 2h | P0 |
| Load testing infrastructure | 4h | P1 |
| Security review of API key storage | 3h | P0 |
QA/Testing:
| Task | Estimate | Priority |
|---|---|---|
| Create test plan | 4h | P0 |
| Manual testing of auto-format | 6h | P0 |
| Manual testing of AI settings | 6h | P0 |
| Manual testing of user API keys | 6h | P0 |
| Manual testing of color palette | 4h | P1 |
| Manual testing of AI generation (both providers) | 10h | P0 |
| Edge case testing (large files, errors) | 6h | P0 |
| Security testing (API key handling) | 6h | P0 |
| Cross-browser testing | 4h | P1 |
| Mobile device testing | 4h | P1 |
| Accessibility testing | 4h | P1 |
| Performance testing | 4h | P1 |
Total Estimates: - Backend: ~136 hours - Frontend: ~125 hours - DevOps: ~23 hours - QA: ~64 hours - Total: ~348 hours (~7-9 weeks with 2-3 developers)
7. Testing Strategy¶
7.1 Unit Tests¶
Backend Tests:
# apps/api/media_assistant/tests/test_ai_model_router.py
class TestAIModelRouter(TestCase):
def setUp(self):
self.router = AIModelRouter()
self.producer = ProducerFactory()
self.gemini_settings = AIModelSettings.objects.create(
provider='gemini',
model_name='gemini-2.0-flash-exp',
is_default=True,
is_enabled=True
)
self.gemini_settings.set_api_key('test-gemini-key')
self.gemini_settings.save()
def test_get_default_provider(self):
"""Should return default Gemini provider"""
provider, api_key, model = self.router.get_provider(self.producer)
self.assertEqual(provider, 'gemini')
self.assertEqual(api_key, 'test-gemini-key')
self.assertEqual(model, 'gemini-2.0-flash-exp')
def test_get_user_api_key(self):
"""Should use user's own API key when requested"""
user_key = UserAPIKey.objects.create(
producer=self.producer,
provider='openai',
is_active=True
)
user_key.set_api_key('user-openai-key')
user_key.save()
provider, api_key, model = self.router.get_provider(
self.producer,
use_own_key=True
)
self.assertEqual(provider, 'openai')
self.assertEqual(api_key, 'user-openai-key')
# apps/api/media_assistant/tests/test_api_key_manager.py
class TestAPIKeyEncryption(TestCase):
def test_encrypt_decrypt_admin_key(self):
"""Admin API key encryption should be reversible"""
settings = AIModelSettings.objects.create(
provider='gemini',
model_name='gemini-2.0-flash-exp'
)
original_key = 'AIzaSyDEMO123456789'
settings.set_api_key(original_key)
settings.save()
# Reload from database
settings.refresh_from_database()
decrypted_key = settings.get_api_key()
self.assertEqual(decrypted_key, original_key)
def test_user_key_separate_encryption(self):
"""User API keys should use separate encryption"""
producer = ProducerFactory()
user_key = UserAPIKey.objects.create(
producer=producer,
provider='gemini'
)
original_key = 'AIzaSyUSER123456789'
user_key.set_api_key(original_key)
user_key.save()
user_key.refresh_from_database()
decrypted_key = user_key.get_api_key()
self.assertEqual(decrypted_key, original_key)
Frontend Tests:
// apps/producer/src/features/media-assistant/__tests__/components/provider-selector.test.tsx
describe('ProviderSelector', () => {
it('should default to Gemini', () => {
render(<ProviderSelector />);
expect(screen.getByLabelText(/gemini/i)).toBeChecked();
});
it('should show cost estimates', () => {
render(<ProviderSelector />);
expect(screen.getByText(/\$0.00025\/image/i)).toBeInTheDocument();
expect(screen.getByText(/\$0.04\/image/i)).toBeInTheDocument();
});
it('should show own API key toggle when user has keys', () => {
const mockAPIKeys = [{ provider: 'gemini', is_active: true }];
render(<ProviderSelector />, {
wrapper: ({ children }) => (
<APIKeysProvider value={{ apiKeys: mockAPIKeys }}>
{children}
</APIKeysProvider>
),
});
expect(screen.getByText(/use my own api key/i)).toBeInTheDocument();
});
});
7.2 Integration Tests¶
API Integration Tests:
class TestAIGenerationWithUserKey(APITestCase):
def test_ai_generation_uses_user_key(self):
"""
Test that AI generation respects user's API key preference
"""
producer = self.create_authenticated_producer()
# Add user's Gemini API key
user_key = UserAPIKey.objects.create(
producer=producer,
provider='gemini',
is_active=True
)
user_key.set_api_key(settings.TEST_GEMINI_KEY)
user_key.save()
# Request AI generation with own key
response = self.client.post('/api/v1/media-assistant/ai-generate/', {
'prompt': 'Test image',
'use_own_api_key': True
})
self.assertEqual(response.status_code, 200)
job_id = response.data['job_id']
# Verify job used user's key
job = ProcessingJob.objects.get(id=job_id)
self.assertTrue(job.used_user_api_key)
7.3 Acceptance Criteria¶
Auto-Format Feature: - [x] All previous criteria (see original plan)
AI Model Management (Admin): - [ ] Admin can add new AI provider (Gemini, OpenAI) - [ ] Admin can store and update API keys securely - [ ] Admin can set default AI provider - [ ] Admin can enable/disable providers - [ ] Admin can test API key validity - [ ] Admin can view usage stats per provider - [ ] Admin can set monthly budget per provider - [ ] API keys are encrypted at rest - [ ] API keys never appear in API responses
User API Key Management: - [ ] User can add own API key for Gemini - [ ] User can add own API key for OpenAI - [ ] User can test API key before saving - [ ] User can toggle between app key and own key - [ ] User sees unlimited usage with own key - [ ] User sees cost estimate for own key - [ ] Security warnings displayed about key storage - [ ] User can delete saved API keys - [ ] User keys encrypted separately from admin keys
AI Generation with Multi-Provider: - [ ] Default provider is Gemini 2.0 Flash - [ ] User can select OpenAI if available - [ ] Cost estimate shown for selected provider - [ ] User can toggle to use own API key - [ ] Rate limits bypassed with user API key - [ ] Job tracking shows which provider was used - [ ] Job tracking shows if user's key was used - [ ] Both Gemini and OpenAI generate valid images - [ ] Fallback to sync processing if Celery fails
8. Risk Assessment & Mitigation¶
8.1 Technical Risks¶
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| API key encryption compromised | Low | Critical | Use industry-standard Fernet encryption. Separate encryption keys for admin vs user. Store encryption keys in environment variables. Regular security audits. Implement audit logging for all key access. |
| Gemini API downtime | Low | High | Fallback to OpenAI if configured. Cache common prompts/results. Provide clear error messages. Queue failed requests for retry. |
| User API key misuse | Medium | Medium | Implement validation before saving. Test keys before activation. Monitor for suspicious patterns. Provide clear terms of use. |
| Encryption key rotation | Low | High | Document key rotation procedure. Implement re-encryption script. Test rotation in staging. Plan maintenance window for rotation. |
| Celery broker failure | Medium | High | Implement synchronous fallback processing (detailed in plan). Monitor broker health. Set up Redis cluster for HA. |
| Large image processing timeouts | Medium | Medium | Set Celery soft_time_limit to 60s. Implement file size validation (max 10MB). Use efficient Pillow algorithms. |
| S3 storage costs exceed budget | Low | Medium | Implement asset cleanup after 90 days. Compress images with high quality settings. Monitor storage metrics. |
| Gemini API costs manageable | Low | Low | Cost: $0.00025/image (250x cheaper than DALL-E). Monitor usage. Set monthly budgets. Alert on anomalies. |
| User-provided API costs uncapped | Low | Medium | User bears cost with own keys. Provide cost estimates. Recommend usage limits. Track usage for transparency. |
8.2 Security Risks¶
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| API key exposure in logs | Medium | Critical | Sanitize all log output. Never log plain-text keys. Use key prefixes only (e.g., "AIza..."). Audit logging code. |
| API key exposure in responses | Medium | Critical | Never include encrypted keys in API responses. Return only metadata (provider, status, last_used). Audit serializers. |
| Unauthorized access to admin keys | Low | Critical | Staff-only permission on admin endpoints. IP whitelisting for admin panel. MFA for admin accounts. Audit trail for all changes. |
| Cross-user API key access | Low | Critical | Permission checks on all endpoints. Filter querysets by producer. Test authorization extensively. |
| Man-in-the-middle attacks | Low | High | Enforce HTTPS only. Use secure headers. Certificate pinning for API calls. |
8.3 Product/UX Risks¶
| Risk | Probability | Impact | Mitigation | |------|----------|----------| | Users confused about API key options | Medium | Medium | Clear help text and documentation. Video tutorials. In-app tooltips. Progressive disclosure (advanced option). | | Users concerned about key security | High | Medium | Transparent security documentation. Explain encryption. Provide key deletion option. Security badge/certification. | | Low adoption of own API keys | High | Low | Acceptable - feature is optional. Promote to power users. Show cost savings. Offer easy setup guide. | | Gemini quality concerns | Medium | Medium | A/B test Gemini vs OpenAI. Gather user feedback. Allow easy switching. Provide both options. Highlight Gemini's speed/cost benefits. |
9. Deployment & Operations¶
9.1 Deployment Plan¶
Phase 1: Staging Deployment 1. Generate Fernet encryption keys (2 separate keys) 2. Merge feature branch to staging 3. Run database migrations 4. Add encryption keys to environment 5. Deploy backend with Celery workers (2 workers initially) 6. Deploy frontend with feature flag OFF 7. Admin: Configure default Gemini API key in settings 8. Internal testing (1 week) 9. Enable feature flag for test producers 10. Test both admin keys and user keys
Phase 2: Production Rollout 1. Schedule deployment during low-traffic window 2. Database backup before migration 3. Generate production encryption keys (secure storage) 4. Run migrations on production database 5. Deploy backend code 6. Add encryption keys to production environment 7. Admin: Configure production Gemini API key 8. Scale Celery workers (start with 3, monitor) 9. Deploy frontend code 10. Enable feature flag for 5% of producers 11. Monitor for 72 hours 12. Gradual rollout: 10% → 25% → 50% → 100% over 3 weeks
Phase 3: Monitoring & Optimization 1. Monitor Celery task metrics (Flower dashboard) 2. Track API usage per provider (Gemini vs OpenAI) 3. Monitor API key usage (admin vs user keys) 4. Track API response times 5. Monitor S3 storage growth 6. Track API costs daily per provider 7. Monitor error rates 8. Gather user feedback 9. Optimize based on real-world patterns
9.2 Configuration¶
Environment Variables:
# Backend (.env)
MEDIA_ASSISTANT_ENCRYPTION_KEY=<Fernet key for admin API keys>
MEDIA_ASSISTANT_USER_KEY_ENCRYPTION=<Fernet key for user API keys>
MEDIA_ASSISTANT_AUTO_FORMAT_ENABLED=true
MEDIA_ASSISTANT_AI_GENERATION_ENABLED=true
MEDIA_ASSISTANT_MAX_FILE_SIZE_MB=10
MEDIA_ASSISTANT_RATE_LIMIT_AUTO_FORMAT=10 # per hour (with app keys)
MEDIA_ASSISTANT_RATE_LIMIT_AI_GENERATION=5 # per hour (with app keys)
Generating Encryption Keys:
# Generate Fernet keys (run once, store securely)
from cryptography.fernet import Fernet
admin_key = Fernet.generate_key()
user_key = Fernet.generate_key()
print(f"MEDIA_ASSISTANT_ENCRYPTION_KEY={admin_key.decode()}")
print(f"MEDIA_ASSISTANT_USER_KEY_ENCRYPTION={user_key.decode()}")
9.3 Monitoring & Alerting¶
Metrics to Track:
- API Key Metrics:
- Number of active admin API keys
- Number of active user API keys per provider
- API key validation success/failure rate
-
Failed API key access attempts (security)
-
AI Generation Metrics:
- Requests per provider (Gemini vs OpenAI)
- Success rate per provider
- Average generation time per provider
- Cost per provider (daily/monthly)
-
Percentage using user API keys vs app keys
-
Processing Metrics: (from previous plan)
- Auto-format job completion rate
- Average processing time
- Celery task queue length
- Redis memory usage
Alerts:
# Additional alerts for API key management
- name: High API Key Validation Failure Rate
condition: validation_failure_rate > 10% over 1 hour
severity: medium
- name: Gemini API Monthly Budget Exceeded
condition: monthly_gemini_spend > $500
severity: high
- name: Suspicious API Key Access Pattern
condition: failed_key_access_attempts > 10 per user per hour
severity: high
- name: Encryption Key Access Error
condition: encryption_error_count > 0
severity: critical
10. Success Measurement¶
10.1 Key Performance Indicators (KPIs)¶
Adoption Metrics (First 3 Months): - 60% of active producers use Media Assistant at least once - 40% of new shows created use auto-formatted images - 30% of shows use extracted color palettes - 25% of shows include AI-generated images - 15-20% of power users provide own API keys (cost reduction for platform)
Efficiency Metrics: - Average time to create show images: < 5 minutes (from 45 min) - 85% of auto-formatted images approved without edits - 90% of processing jobs complete successfully - Gemini vs OpenAI usage: 80% Gemini, 20% OpenAI (cost optimization) - Average AI generation time: < 15 seconds with Gemini
Cost Metrics: - Average cost per AI-generated image: < $0.001 (target with Gemini default) - Platform API cost reduction: 30% (due to user-provided keys) - Monthly AI generation budget adherence: 100%
10.2 User Feedback Collection¶
In-App Surveys: - API key setup experience survey - Provider preference survey (Gemini vs OpenAI quality) - Feature satisfaction after 14 days
Analytics Events:
// Track API key usage
analytics.track('media_assistant_api_key_added', {
provider: 'gemini',
user_type: 'power_user',
});
analytics.track('media_assistant_ai_generated', {
provider: 'gemini',
used_own_key: true,
generation_time_seconds: 12,
cost_estimate: 0.00025,
});
11. Appendices¶
Appendix A: Gemini API Integration Example¶
import google.generativeai as genai
from PIL import Image
import requests
from io import BytesIO
class GeminiImageGenerator:
def __init__(self, api_key: str):
genai.configure(api_key=api_key)
self.model = genai.GenerativeModel('gemini-2.0-flash-exp')
def generate_image(self, prompt: str, size: str = "1024x1024") -> str:
"""
Generate image with Gemini 2.0 Flash
Args:
prompt: Text description
size: Target size (e.g., "1024x1024")
Returns:
Base64 encoded image data or URL
"""
try:
# Enhance prompt with size specification
enhanced_prompt = f"{prompt}. Generate a {size} image."
response = self.model.generate_content(
[enhanced_prompt],
generation_config={
"temperature": 0.9,
"top_p": 1,
"top_k": 32,
"max_output_tokens": 2048,
}
)
# Extract image from response
# Note: Actual implementation depends on Gemini's image generation API
if response.candidates:
image_data = response.candidates[0].content.parts[0]
return image_data # May need further processing
except Exception as e:
logger.error(f"Gemini API error: {e}")
raise
def validate_api_key(self) -> tuple[bool, str]:
"""
Test API key validity
Returns:
(is_valid, error_message)
"""
try:
# Simple test request
response = self.model.generate_content("Test")
return (True, "")
except Exception as e:
return (False, str(e))
Appendix B: API Key Security Best Practices¶
Encryption Implementation:
# apps/api/media_assistant/utils/encryption.py
from cryptography.fernet import Fernet
from django.conf import settings
class APIKeyEncryption:
"""Secure API key encryption utilities"""
@staticmethod
def encrypt_admin_key(plain_key: str) -> str:
"""Encrypt admin API key"""
fernet = Fernet(settings.MEDIA_ASSISTANT_ENCRYPTION_KEY.encode())
return fernet.encrypt(plain_key.encode()).decode()
@staticmethod
def decrypt_admin_key(encrypted_key: str) -> str:
"""Decrypt admin API key"""
fernet = Fernet(settings.MEDIA_ASSISTANT_ENCRYPTION_KEY.encode())
return fernet.decrypt(encrypted_key.encode()).decode()
@staticmethod
def encrypt_user_key(plain_key: str) -> str:
"""Encrypt user API key (separate encryption key)"""
fernet = Fernet(settings.MEDIA_ASSISTANT_USER_KEY_ENCRYPTION.encode())
return fernet.encrypt(plain_key.encode()).decode()
@staticmethod
def decrypt_user_key(encrypted_key: str) -> str:
"""Decrypt user API key"""
fernet = Fernet(settings.MEDIA_ASSISTANT_USER_KEY_ENCRYPTION.encode())
return fernet.decrypt(encrypted_key.encode()).decode()
@staticmethod
def sanitize_key_for_logging(plain_key: str) -> str:
"""Return safe version of key for logging (prefix only)"""
if len(plain_key) > 8:
return f"{plain_key[:8]}..."
return "***"
Audit Logging:
# apps/api/media_assistant/services/api_key_manager.py
import logging
from django.contrib.admin.models import LogEntry, ADDITION, CHANGE, DELETION
from django.contrib.contenttypes.models import ContentType
audit_logger = logging.getLogger('media_assistant.audit')
class APIKeyAuditLog:
"""Audit logging for API key operations"""
@staticmethod
def log_key_added(user, provider, is_admin=False):
"""Log API key addition"""
audit_logger.info(
f"API key added: provider={provider}, "
f"is_admin={is_admin}, user={user.id}"
)
@staticmethod
def log_key_accessed(user, provider, success=True):
"""Log API key access attempt"""
status = "success" if success else "failed"
audit_logger.info(
f"API key accessed: provider={provider}, "
f"user={user.id}, status={status}"
)
@staticmethod
def log_key_deleted(user, provider):
"""Log API key deletion"""
audit_logger.warning(
f"API key deleted: provider={provider}, user={user.id}"
)
Appendix C: Cost Comparison¶
AI Provider Cost Analysis:
| Provider | Model | Cost per Image (Standard) | Cost per Image (HD) | Avg Generation Time |
|---|---|---|---|---|
| Gemini (Default) | 2.0 Flash | $0.00025 | $0.0005 | 5-10 seconds |
| OpenAI | DALL-E 3 | $0.040 | $0.080 | 15-30 seconds |
| Savings | - | 160x cheaper | 160x cheaper | 2-3x faster |
Monthly Cost Projections:
Assumptions: - 1,000 active producers - 30% use AI generation - Average 2 images per show - Average 4 shows per producer per month
Scenario 1: All Gemini (Default)
- Total images: 1000 * 0.30 * 2 * 4 = 2,400 images/month
- Cost: 2,400 * $0.00025 = $0.60/month
- **Total: < $1/month**
Scenario 2: All OpenAI
- Total images: 2,400 images/month
- Cost: 2,400 * $0.04 = $96/month
- **Total: $96/month**
Scenario 3: Mixed (80% Gemini, 20% OpenAI)
- Gemini: 1,920 * $0.00025 = $0.48
- OpenAI: 480 * $0.04 = $19.20
- **Total: ~$20/month**
Scenario 4: 20% users provide own keys
- Platform images: 1,920 (80% of 2,400)
- Cost with Gemini: 1,920 * $0.00025 = $0.48/month
- **Platform cost reduction: 20%**
Recommendation: Default to Gemini for dramatic cost savings. Allow OpenAI as premium option. Encourage power users to use own keys.
Document Revision History¶
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2025-11-03 | AI Assistant | Initial comprehensive planning document with Gemini default and multi-provider API key management |
End of Planning Document
Total Pages: ~50 | Word Count: ~18,000
This planning document provides complete specifications for implementing the Media Assistant feature with: - Google Gemini 2.0 Flash as the default AI provider (250x cheaper than OpenAI) - Multi-provider support (Gemini, OpenAI, extensible for future providers) - Admin API key management for platform-level configuration - User API key management allowing users to bypass limits with their own keys - Secure encryption of all API keys with separate encryption for admin vs user keys - Celery async processing with synchronous fallback - Complete implementation roadmap ready for development
The architecture is flexible, cost-effective, and puts power in users' hands while maintaining platform security and control.