Skip to content

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:

  1. ImageUploader (apps/producer/src/components/image-uploader.tsx)
  2. Single image upload with preview
  3. Dimension validation
  4. File size validation (default 5MB)
  5. Image preview with remove functionality
  6. Supports File arrays and string URLs

  7. FileUploader (apps/producer/src/components/file-uploader.tsx)

  8. React-dropzone integration
  9. Multiple file support
  10. Progress tracking
  11. Preview generation
  12. File validation
  13. 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:

  1. API Key Metrics:
  2. Number of active admin API keys
  3. Number of active user API keys per provider
  4. API key validation success/failure rate
  5. Failed API key access attempts (security)

  6. AI Generation Metrics:

  7. Requests per provider (Gemini vs OpenAI)
  8. Success rate per provider
  9. Average generation time per provider
  10. Cost per provider (daily/monthly)
  11. Percentage using user API keys vs app keys

  12. Processing Metrics: (from previous plan)

  13. Auto-format job completion rate
  14. Average processing time
  15. Celery task queue length
  16. 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.