From 854b3efd45872fc95bc2d4cd1279b5acbb5791b5 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Sat, 10 Jan 2026 13:16:05 +0000 Subject: [PATCH] image s imsages images model fixes new model see dream --- backend/igny8_core/ai/ai_core.py | 15 +- backend/igny8_core/ai/tasks.py | 62 +-- ...alaimodelconfig_landscape_size_and_more.py | 38 ++ .../igny8_core/modules/system/ai_settings.py | 17 +- .../modules/system/integration_views.py | 100 +++-- ...isettings_default_quality_tier_and_more.py | 19 + .../modules/system/settings_views.py | 11 +- backend/igny8_core/utils/ai_processor.py | 13 +- docs/90-REFERENCE/IMAGE-GENERATION-GAPS.md | 373 ++++++++++++++++++ 9 files changed, 582 insertions(+), 66 deletions(-) create mode 100644 backend/igny8_core/modules/billing/migrations/0032_historicalaimodelconfig_landscape_size_and_more.py create mode 100644 backend/igny8_core/modules/system/migrations/0024_alter_systemaisettings_default_quality_tier_and_more.py create mode 100644 docs/90-REFERENCE/IMAGE-GENERATION-GAPS.md diff --git a/backend/igny8_core/ai/ai_core.py b/backend/igny8_core/ai/ai_core.py index 96ecacc3..57c33819 100644 --- a/backend/igny8_core/ai/ai_core.py +++ b/backend/igny8_core/ai/ai_core.py @@ -1028,9 +1028,18 @@ class AICore: print(f"[AI][{function_name}] Using Nano Banana config: resolution=1k (no width/height)") elif runware_model.startswith('bytedance:'): # Seedream 4.5 (bytedance:seedream@4.5) - High quality ByteDance model - # Uses basic format with just width/height - no steps, CFGScale, or special providerSettings needed - # Just use the base inference_task as-is - print(f"[AI][{function_name}] Using Seedream 4.5 config: basic format, width={width}, height={height}") + # Uses basic format - no steps, CFGScale, negativePrompt, or special providerSettings needed + # Remove negativePrompt as it's not supported + if 'negativePrompt' in inference_task: + del inference_task['negativePrompt'] + # Enforce minimum size for Seedream (min 3,686,400 pixels ~ 1920x1920) + current_pixels = width * height + if current_pixels < 3686400: + # Use default Seedream square size + inference_task['width'] = 2048 + inference_task['height'] = 2048 + width, height = 2048, 2048 + print(f"[AI][{function_name}] Using Seedream 4.5 config: basic format (no negativePrompt), width={inference_task['width']}, height={inference_task['height']}") elif runware_model.startswith('runware:'): # Hi Dream Full (runware:97@1) - General diffusion, steps 20, CFGScale 7 inference_task['steps'] = 20 diff --git a/backend/igny8_core/ai/tasks.py b/backend/igny8_core/ai/tasks.py index 2d76ad99..f50ba64d 100644 --- a/backend/igny8_core/ai/tasks.py +++ b/backend/igny8_core/ai/tasks.py @@ -187,22 +187,42 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None logger.info("[process_image_generation_queue] Step 1: Loading image generation settings") from igny8_core.modules.system.ai_settings import AISettings from igny8_core.ai.model_registry import ModelRegistry + from igny8_core.business.billing.models import AIModelConfig # Get effective settings image_type = AISettings.get_effective_image_style(account) image_format = 'webp' # Default format - # Get default image model from database - default_model = ModelRegistry.get_default_model('image') - if default_model: - model_config = ModelRegistry.get_model(default_model) - provider = model_config.provider if model_config else 'openai' - model = default_model + # Get user's selected quality tier (from account settings) + quality_tier = AISettings.get_effective_quality_tier(account) + logger.info(f"[process_image_generation_queue] User's quality tier: {quality_tier}") + + # Find image model based on quality tier (DYNAMIC from database) + model_config = None + if quality_tier: + model_config = AIModelConfig.objects.filter( + model_type='image', + quality_tier=quality_tier, + is_active=True + ).first() + + # Fallback to default image model if no tier match + if not model_config: + default_model = ModelRegistry.get_default_model('image') + if default_model: + model_config = ModelRegistry.get_model(default_model) + + # Set provider and model from database config + if model_config: + provider = model_config.provider or 'openai' + model = model_config.model_name else: + # Ultimate fallback (should never happen if database is configured) + logger.warning("[process_image_generation_queue] No image model found in database, using fallback") provider = 'openai' model = 'dall-e-3' - logger.info(f"[process_image_generation_queue] Using PROVIDER: {provider}, MODEL: {model} from settings") + logger.info(f"[process_image_generation_queue] Using PROVIDER: {provider}, MODEL: {model} from AIModelConfig") # Style to prompt enhancement mapping # These style descriptors are added to the image prompt for better results @@ -225,25 +245,23 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None style_description = STYLE_PROMPT_MAP.get(image_type, STYLE_PROMPT_MAP.get('photorealistic')) logger.info(f"[process_image_generation_queue] Style: {image_type} -> prompt enhancement: {style_description[:50]}...") - # Model-specific landscape sizes (square is always 1024x1024) - # For Runware models - based on Runware documentation for optimal results per model - # For OpenAI DALL-E 3 - uses 1792x1024 for landscape - MODEL_LANDSCAPE_SIZES = { - 'runware:97@1': '1280x768', # Hi Dream Full landscape - 'bria:10@1': '1344x768', # Bria 3.2 landscape (16:9) - 'google:4@2': '1376x768', # Nano Banana landscape (16:9) - 'dall-e-3': '1792x1024', # DALL-E 3 landscape - 'dall-e-2': '1024x1024', # DALL-E 2 only supports square - } - DEFAULT_SQUARE_SIZE = '1024x1024' - - # Get model-specific landscape size for featured images - model_landscape_size = MODEL_LANDSCAPE_SIZES.get(model, '1792x1024' if provider == 'openai' else '1280x768') + # Load image sizes from AIModelConfig (DYNAMIC from database) + # model_config was loaded above based on quality tier + if model_config: + # Get sizes from database (single source of truth) + model_landscape_size = model_config.landscape_size or '1792x1024' + model_square_size = model_config.square_size or '1024x1024' + logger.info(f"[process_image_generation_queue] Loaded sizes from AIModelConfig: landscape={model_landscape_size}, square={model_square_size}") + else: + # Fallback sizes if no model config (should never happen) + model_landscape_size = '1792x1024' + model_square_size = '1024x1024' + logger.warning(f"[process_image_generation_queue] No model config, using fallback sizes") # Featured image always uses model-specific landscape size featured_image_size = model_landscape_size # In-article images: alternating square/landscape based on position (handled in image loop) - in_article_square_size = DEFAULT_SQUARE_SIZE + in_article_square_size = model_square_size in_article_landscape_size = model_landscape_size logger.info(f"[process_image_generation_queue] Settings loaded:") diff --git a/backend/igny8_core/modules/billing/migrations/0032_historicalaimodelconfig_landscape_size_and_more.py b/backend/igny8_core/modules/billing/migrations/0032_historicalaimodelconfig_landscape_size_and_more.py new file mode 100644 index 00000000..97f1dc3a --- /dev/null +++ b/backend/igny8_core/modules/billing/migrations/0032_historicalaimodelconfig_landscape_size_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 5.2.10 on 2026-01-10 12:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('billing', '0031_add_seedream_model'), + ] + + operations = [ + migrations.AddField( + model_name='historicalaimodelconfig', + name='landscape_size', + field=models.CharField(blank=True, help_text="Landscape image size for this model (e.g., '1792x1024', '1280x768')", max_length=20, null=True), + ), + migrations.AddField( + model_name='historicalaimodelconfig', + name='square_size', + field=models.CharField(blank=True, default='1024x1024', help_text="Square image size for this model (e.g., '1024x1024')", max_length=20), + ), + migrations.AddField( + model_name='historicalaimodelconfig', + name='valid_sizes', + field=models.JSONField(blank=True, default=list, help_text="List of valid sizes for this model (e.g., ['1024x1024', '1792x1024'])"), + ), + migrations.AlterField( + model_name='aimodelconfig', + name='quality_tier', + field=models.CharField(blank=True, choices=[('basic', 'Basic'), ('quality', 'Quality'), ('quality_option2', 'Quality-Option2'), ('premium', 'Premium')], help_text='basic / quality / premium - for image models', max_length=20, null=True), + ), + migrations.AlterField( + model_name='historicalaimodelconfig', + name='quality_tier', + field=models.CharField(blank=True, choices=[('basic', 'Basic'), ('quality', 'Quality'), ('quality_option2', 'Quality-Option2'), ('premium', 'Premium')], help_text='basic / quality / premium - for image models', max_length=20, null=True), + ), + ] diff --git a/backend/igny8_core/modules/system/ai_settings.py b/backend/igny8_core/modules/system/ai_settings.py index 625a5b43..b25a2bcc 100644 --- a/backend/igny8_core/modules/system/ai_settings.py +++ b/backend/igny8_core/modules/system/ai_settings.py @@ -194,7 +194,7 @@ class SystemAISettings(models.Model): @classmethod def get_effective_quality_tier(cls, account=None) -> str: - """Get quality_tier, checking account override first (from ai_settings key)""" + """Get quality_tier, checking account override first, then default image model""" if account: # Check consolidated ai_settings first try: @@ -214,6 +214,21 @@ class SystemAISettings(models.Model): override = cls._get_account_override(account, 'ai.quality_tier') if override is not None: return str(override) + + # No account override - get tier from DEFAULT image model (is_default=True) + try: + from igny8_core.business.billing.models import AIModelConfig + default_model = AIModelConfig.objects.filter( + model_type='image', + is_default=True, + is_active=True + ).first() + if default_model and default_model.quality_tier: + return default_model.quality_tier + except Exception as e: + logger.debug(f"Could not get default image model tier: {e}") + + # Ultimate fallback return cls.get_instance().default_quality_tier @staticmethod diff --git a/backend/igny8_core/modules/system/integration_views.py b/backend/igny8_core/modules/system/integration_views.py index 145b488b..a323865e 100644 --- a/backend/igny8_core/modules/system/integration_views.py +++ b/backend/igny8_core/modules/system/integration_views.py @@ -793,23 +793,38 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): } elif integration_type == 'image_generation': - # Model-specific landscape sizes - MODEL_LANDSCAPE_SIZES = { - 'runware:97@1': '1280x768', - 'bria:10@1': '1344x768', - 'google:4@2': '1376x768', - } + from igny8_core.business.billing.models import AIModelConfig + from igny8_core.modules.system.ai_settings import AISettings - # Get default image model from AIModelConfig - default_model = ModelRegistry.get_default_model('image') - if default_model: - model_config = ModelRegistry.get_model(default_model) - default_service = model_config.provider if model_config else 'openai' + # Get user's quality tier (from account settings) + quality_tier = AISettings.get_effective_quality_tier(account) + + # Find image model based on quality tier (DYNAMIC from database) + model_config = None + if quality_tier: + model_config = AIModelConfig.objects.filter( + model_type='image', + quality_tier=quality_tier, + is_active=True + ).first() + + # Fallback to default image model + if not model_config: + default_model = ModelRegistry.get_default_model('image') + if default_model: + model_config = ModelRegistry.get_model(default_model) + + # Extract settings from model config + if model_config: + default_service = model_config.provider or 'openai' + default_model = model_config.model_name + model_landscape_size = model_config.landscape_size or '1792x1024' + model_square_size = model_config.square_size or '1024x1024' else: default_service = 'openai' default_model = 'dall-e-3' - - model_landscape_size = MODEL_LANDSCAPE_SIZES.get(default_model, '1280x768') + model_landscape_size = '1792x1024' + model_square_size = '1024x1024' response_data = { 'id': 'image_generation', @@ -826,8 +841,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): 'image_format': 'webp', 'desktop_enabled': True, 'featured_image_size': model_landscape_size, + 'in_article_landscape_size': model_landscape_size, + 'in_article_square_size': model_square_size, 'desktop_image_size': SystemAISettings.get_effective_image_size(account), 'using_global': True, + 'quality_tier': quality_tier, } else: # Other integration types - return empty @@ -856,9 +874,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): 1. SystemAISettings (singleton) provides system-wide defaults 2. AccountSettings (key-value) provides per-account overrides 3. API keys come from IntegrationProvider (accounts cannot override API keys) + 4. Model config (sizes, etc.) from AIModelConfig (DYNAMIC, single source of truth) """ - from igny8_core.modules.system.ai_settings import SystemAISettings + from igny8_core.modules.system.ai_settings import SystemAISettings, AISettings from igny8_core.ai.model_registry import ModelRegistry + from igny8_core.business.billing.models import AIModelConfig account = getattr(request, 'account', None) @@ -868,29 +888,36 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): if user and hasattr(user, 'is_authenticated') and user.is_authenticated: account = getattr(user, 'account', None) - # Model-specific landscape sizes - MODEL_LANDSCAPE_SIZES = { - 'runware:97@1': '1280x768', # Hi Dream Full landscape - 'bria:10@1': '1344x768', # Bria 3.2 landscape (16:9) - 'google:4@2': '1376x768', # Nano Banana landscape (16:9) - 'dall-e-3': '1792x1024', # DALL-E 3 landscape - 'dall-e-2': '1024x1024', # DALL-E 2 square only - } - try: - # Get default image model from AIModelConfig - default_model = ModelRegistry.get_default_model('image') - if default_model: - model_config = ModelRegistry.get_model(default_model) - provider = model_config.provider if model_config else 'openai' - model = default_model + # Get user's quality tier from account settings (DYNAMIC) + quality_tier = AISettings.get_effective_quality_tier(account) + + # Find image model based on quality tier (DYNAMIC from database) + model_config = None + if quality_tier: + model_config = AIModelConfig.objects.filter( + model_type='image', + quality_tier=quality_tier, + is_active=True + ).first() + + # Fallback to default image model + if not model_config: + default_model = ModelRegistry.get_default_model('image') + if default_model: + model_config = ModelRegistry.get_model(default_model) + + # Extract settings from model config (SINGLE SOURCE OF TRUTH) + if model_config: + provider = model_config.provider or 'openai' + model = model_config.model_name + model_landscape_size = model_config.landscape_size or '1792x1024' + model_square_size = model_config.square_size or '1024x1024' else: provider = 'openai' model = 'dall-e-3' - - # Get model-specific landscape size - model_landscape_size = MODEL_LANDSCAPE_SIZES.get(model, '1280x768') - default_featured_size = model_landscape_size if provider == 'runware' else '1792x1024' + model_landscape_size = '1792x1024' + model_square_size = '1024x1024' # Get image style from SystemAISettings with AccountSettings overrides image_style = SystemAISettings.get_effective_image_style(account) @@ -915,7 +942,7 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): if not image_style or image_style in ['natural', 'vivid']: image_style = 'photorealistic' - logger.info(f"[get_image_generation_settings] Returning: provider={provider}, model={model}, image_style={image_style}") + logger.info(f"[get_image_generation_settings] Returning: provider={provider}, model={model}, image_style={image_style}, quality_tier={quality_tier}") return success_response( data={ @@ -927,8 +954,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): 'max_in_article_images': SystemAISettings.get_effective_max_images(account), 'image_format': 'webp', 'desktop_enabled': True, - 'featured_image_size': default_featured_size, + 'featured_image_size': model_landscape_size, + 'in_article_landscape_size': model_landscape_size, + 'in_article_square_size': model_square_size, 'desktop_image_size': SystemAISettings.get_effective_image_size(account), + 'quality_tier': quality_tier, } }, request=request diff --git a/backend/igny8_core/modules/system/migrations/0024_alter_systemaisettings_default_quality_tier_and_more.py b/backend/igny8_core/modules/system/migrations/0024_alter_systemaisettings_default_quality_tier_and_more.py new file mode 100644 index 00000000..f76fc3a9 --- /dev/null +++ b/backend/igny8_core/modules/system/migrations/0024_alter_systemaisettings_default_quality_tier_and_more.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.10 on 2026-01-10 12:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('system', '0023_systemaisettings_max_allowed_images_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='systemaisettings', + name='default_quality_tier', + field=models.CharField(choices=[('basic', 'Basic'), ('quality', 'Quality'), ('quality_option2', 'Quality-Option2'), ('premium', 'Premium')], default='basic', help_text='Default quality tier for image generation', max_length=20), + ), + # Removed DeleteModel for AccountIntegrationOverride - table doesn't exist + ] diff --git a/backend/igny8_core/modules/system/settings_views.py b/backend/igny8_core/modules/system/settings_views.py index e9635d4a..b349024b 100644 --- a/backend/igny8_core/modules/system/settings_views.py +++ b/backend/igny8_core/modules/system/settings_views.py @@ -621,15 +621,20 @@ class ContentGenerationSettingsViewSet(viewsets.ViewSet): # Get system defaults system_defaults = SystemAISettings.get_instance() + # Get default image model (is_default=True) - SINGLE SOURCE OF TRUTH + default_image_model = AIModelConfig.get_default_image_model() + + # Determine default tier from default model (not hardcoded) + default_tier = default_image_model.quality_tier if default_image_model else 'basic' + # Apply account overrides or use system defaults temperature = account_settings.get('temperature', system_defaults.temperature) max_tokens = account_settings.get('max_tokens', system_defaults.max_tokens) image_style = account_settings.get('image_style', system_defaults.image_style) max_images = account_settings.get('max_images', system_defaults.max_images_per_article) - selected_tier = account_settings.get('quality_tier', system_defaults.default_quality_tier) - # Get default image model (or model for selected tier) - default_image_model = AIModelConfig.get_default_image_model() + # Get selected tier: account override > default model's tier + selected_tier = account_settings.get('quality_tier') or default_tier # Try to find model matching selected tier selected_model = AIModelConfig.objects.filter( diff --git a/backend/igny8_core/utils/ai_processor.py b/backend/igny8_core/utils/ai_processor.py index 5cd41eef..afc17b2c 100644 --- a/backend/igny8_core/utils/ai_processor.py +++ b/backend/igny8_core/utils/ai_processor.py @@ -869,8 +869,17 @@ Make sure each prompt is detailed enough for image generation, describing the vi del inference_task['height'] inference_task['resolution'] = '1k' elif runware_model.startswith('bytedance:'): - # Seedream models use basic format - no steps, CFGScale needed - pass + # Seedream models use basic format - no steps, CFGScale, negativePrompt needed + # Also require minimum 3,686,400 pixels (roughly 1920x1920) + if 'negativePrompt' in inference_task: + del inference_task['negativePrompt'] + # Enforce minimum size for Seedream (min ~1920x1920, use 2048x2048 for square) + current_pixels = width * height + if current_pixels < 3686400: + # Use default Seedream square size + inference_task['width'] = 2048 + inference_task['height'] = 2048 + logger.info(f"[AIProcessor.generate_image] Seedream requires min 3.6M pixels, adjusted to 2048x2048") elif runware_model.startswith('runware:'): # Hi Dream Full - needs steps and CFGScale inference_task['steps'] = 30 diff --git a/docs/90-REFERENCE/IMAGE-GENERATION-GAPS.md b/docs/90-REFERENCE/IMAGE-GENERATION-GAPS.md new file mode 100644 index 00000000..27be4b5a --- /dev/null +++ b/docs/90-REFERENCE/IMAGE-GENERATION-GAPS.md @@ -0,0 +1,373 @@ +# Image Generation System - Comprehensive Gap Analysis + +**Date:** January 2026 +**Status:** Audit Complete +**Reviewer:** System Audit + +## Executive Summary + +This document provides a comprehensive audit of the image generation system, analyzing the flow from model configuration to image delivery, both for manual and automation workflows. + +--- + +## 1. System Architecture Overview + +### Current Flow +``` +User Selects Quality Tier (basic/quality/quality_option2/premium) + ↓ +AIModelConfig (database) → provider, model_name, landscape_size, square_size + ↓ +process_image_generation_queue (Celery task) + ↓ +ai_core.generate_image() → provider-specific handler + ↓ +_generate_image_openai() / _generate_image_runware() + ↓ +Downloaded to /frontend/public/images/ai-images/ + ↓ +Image record updated (Images model) +``` + +### Image Count Determination +``` +User sets max_images (1-8) in Site Settings + ↓ +AISettings.get_effective_max_images(account) + ↓ +generate_image_prompts.py: 1 featured + max_images in_article + ↓ +Images created with positions: featured(0) + in_article(0,1,2,3...) +``` + +--- + +## 2. RESOLVED Issues (Previously Fixed) + +### ✅ 2.1 Quality Tier Fallback +- **Issue:** `get_effective_quality_tier()` was returning hardcoded 'basic' instead of default model's tier +- **Fixed in:** `ai_settings.py` lines 196-232 +- **Solution:** Now falls back to `AIModelConfig.is_default=True` model's `quality_tier` + +### ✅ 2.2 Image Sizes from Database +- **Issue:** `tasks.py` had hardcoded `MODEL_LANDSCAPE_SIZES` dict +- **Fixed in:** `tasks.py` lines 242-260 +- **Solution:** Now loads `landscape_size` and `square_size` from `AIModelConfig` + +### ✅ 2.3 Settings Endpoint Default Tier +- **Issue:** `settings_views.py` returned hardcoded 'basic' as default tier +- **Fixed in:** `settings_views.py` lines 616-640 +- **Solution:** Gets `default_tier` from `default_image_model.quality_tier` + +### ✅ 2.4 Integration Views Dynamic Sizes +- **Issue:** Two endpoints in `integration_views.py` had hardcoded size lookup +- **Fixed:** Both endpoints now load from `AIModelConfig` + +--- + +## 3. REMAINING Gaps (Action Required) + +### 🔴 GAP-1: Hardcoded Size Constants in global_settings_models.py + +**Location:** `backend/igny8_core/modules/system/global_settings_models.py` lines 180-188 + +**Code:** +```python +# Model-specific landscape sizes (square is always 1024x1024 for all models) +MODEL_LANDSCAPE_SIZES = { + 'runware:97@1': '1280x768', # Hi Dream Full landscape + 'bria:10@1': '1344x768', # Bria 3.2 landscape (16:9) + 'google:4@2': '1376x768', # Nano Banana landscape (16:9) +} + +# Default square size (universal across all models) +DEFAULT_SQUARE_SIZE = '1024x1024' +``` + +**Impact:** These constants are UNUSED but could cause confusion. They're legacy from before the AIModelConfig migration. + +**Recommendation:** +- [ ] Remove unused `MODEL_LANDSCAPE_SIZES` dict +- [ ] Remove unused `DEFAULT_SQUARE_SIZE` constant +- [ ] Add deprecation comment if keeping for reference + +--- + +### 🔴 GAP-2: Hardcoded Size Fallbacks in Tasks + +**Location:** `backend/igny8_core/ai/tasks.py` lines 254-260 + +**Code:** +```python +else: + # Fallback sizes if no model config (should never happen) + model_landscape_size = '1792x1024' + model_square_size = '1024x1024' + logger.warning(f"[process_image_generation_queue] No model config, using fallback sizes") +``` + +**Impact:** LOW - Fallback only triggers if database is misconfigured. But the hardcoded sizes may not match actual model requirements. + +**Recommendation:** +- [ ] Consider failing gracefully with clear error instead of using fallback +- [ ] Or: Load fallback from a system default in database + +--- + +### 🔴 GAP-3: Frontend VALID_SIZES_BY_MODEL Hardcoded + +**Location:** `frontend/src/components/common/ImageGenerationCard.tsx` lines 52-55 + +**Code:** +```tsx +const VALID_SIZES_BY_MODEL: Record = { + 'dall-e-3': ['1024x1024', '1024x1792', '1792x1024'], + 'dall-e-2': ['256x256', '512x512', '1024x1024'], +}; +``` + +**Impact:** MEDIUM - Test image generation card only shows OpenAI sizes, not Runware/Bytedance sizes. + +**Recommendation:** +- [ ] Fetch valid_sizes from AIModelConfig via API +- [ ] Or: Pass sizes from backend settings endpoint + +--- + +### 🔴 GAP-4: Backend VALID_SIZES_BY_MODEL Hardcoded + +**Location:** `backend/igny8_core/ai/constants.py` lines 40-43 + +**Code:** +```python +VALID_SIZES_BY_MODEL = { + 'dall-e-3': ['1024x1024', '1024x1792', '1792x1024'], + 'dall-e-2': ['256x256', '512x512', '1024x1024'], +} +``` + +**Impact:** MEDIUM - Used for OpenAI validation only. Runware models bypass this validation. + +**Status:** PARTIAL - Only affects OpenAI validation. Runware has its own validation via provider-specific code. + +**Recommendation:** +- [ ] Move validation to AIModelConfig.valid_sizes field +- [ ] Validate against model's valid_sizes from database + +--- + +### 🔴 GAP-5: Missing Runware Model Size Validation + +**Location:** `backend/igny8_core/ai/ai_core.py` lines 943-1050 + +**Code:** The `_generate_image_runware()` method does NOT validate sizes against `valid_sizes` from database. + +**Impact:** LOW - Runware API will reject invalid sizes anyway, but error message won't be clear. + +**Recommendation:** +- [ ] Add validation: check `size` against `AIModelConfig.valid_sizes` before API call +- [ ] Return clear error: "Size X is not valid for model Y" + +--- + +### 🔴 GAP-6: Seedream Minimum Pixel Validation Hardcoded + +**Location:** `backend/igny8_core/ai/ai_core.py` lines 1018-1027 + +**Code:** +```python +elif runware_model.startswith('bytedance:'): + # Enforce minimum size for Seedream (min 3,686,400 pixels ~ 1920x1920) + current_pixels = width * height + if current_pixels < 3686400: + # Use default Seedream square size + inference_task['width'] = 2048 + inference_task['height'] = 2048 +``` + +**Impact:** LOW - This hardcoded check works, but should come from database. + +**Recommendation:** +- [ ] Add `min_pixels` field to AIModelConfig +- [ ] Check model.min_pixels instead of hardcoded 3686400 + +--- + +### 🔴 GAP-7: Provider-Specific Steps/CFGScale Hardcoded + +**Location:** `backend/igny8_core/ai/ai_core.py` lines 995-1040 + +**Code:** +```python +if runware_model.startswith('bria:'): + inference_task['steps'] = 20 + # ... +elif runware_model.startswith('runware:'): + inference_task['steps'] = 20 + inference_task['CFGScale'] = 7 +``` + +**Impact:** LOW - Works correctly but adding new models requires code changes. + +**Recommendation:** +- [ ] Add `generation_params` JSON field to AIModelConfig +- [ ] Store steps, CFGScale, etc. in database per model + +--- + +### 🔴 GAP-8: Image Count Not Per-Content Configurable + +**Location:** System-wide setting only + +**Current Behavior:** +- `max_images` is a global setting (site-wide) +- All articles get the same number of in_article images + +**Impact:** MEDIUM - Users cannot set different image counts per article/keyword. + +**Recommendation:** +- [ ] Add `images_count` field to Content model (nullable, inherits from site default) +- [ ] Or: Add to Keywords model for keyword-level override + +--- + +### 🟡 GAP-9: Legacy generate_images.py Function (Partially Dead Code) + +**Location:** `backend/igny8_core/ai/functions/generate_images.py` + +**Issue:** `generate_images_core()` function exists but appears to be legacy. Main flow uses `process_image_generation_queue()` in tasks.py. + +**Impact:** LOW - Code duplication, potential maintenance burden. + +**Recommendation:** +- [ ] Audit if `generate_images.py` is actually used anywhere +- [ ] If not: Add deprecation warning or remove +- [ ] If used: Ensure it uses same dynamic config loading + +--- + +### 🟡 GAP-10: No Validation of quality_tier Values + +**Location:** Multiple locations + +**Issue:** When user selects a quality_tier, there's no validation that: +1. The tier exists in AIModelConfig +2. The tier has an active model +3. The user's plan allows that tier + +**Impact:** MEDIUM - Could lead to runtime errors if tier doesn't exist. + +**Recommendation:** +- [ ] Add validation in settings save endpoint +- [ ] Return error if selected tier has no active model + +--- + +## 4. Image Count Flow (Working Correctly) + +### How image count works: + +1. **User configures:** + - `max_images` (1-8) in Site Settings → saved to AccountSettings + +2. **Prompt generation:** + ```python + # generate_image_prompts.py + max_images = AISettings.get_effective_max_images(account) # e.g., 4 + # Creates: 1 featured + 4 in_article = 5 image prompts + ``` + +3. **Image types:** + - `featured` - Always 1, position=0, landscape size + - `in_article` - Up to max_images, positions 0,1,2,3..., alternating square/landscape + +4. **Size determination:** + ```python + # tasks.py + if image.image_type == 'featured': + image_size = featured_image_size # landscape + elif image.image_type == 'in_article': + position = image.position or 0 + if position % 2 == 0: # 0, 2 + image_size = square_size + else: # 1, 3 + image_size = landscape_size + ``` + +**STATUS:** ✅ Working correctly. No gaps identified in image count logic. + +--- + +## 5. Automation Flow (Working Correctly) + +### Stage 6: Image Generation + +**Location:** `automation_service.py` lines 1236-1400 + +**Flow:** +1. Query `Images.objects.filter(site=site, status='pending')` +2. For each image: `process_image_generation_queue.delay(image_ids=[image.id], ...)` +3. Monitor task completion +4. Update run progress + +**STATUS:** ✅ Uses same task as manual generation. Consistent behavior. + +--- + +## 6. Model Provider Support Matrix + +| Provider | Models | Status | Gaps | +|----------|--------|--------|------| +| OpenAI | dall-e-3, dall-e-2 | ✅ Working | Valid sizes hardcoded | +| Runware | runware:97@1 | ✅ Working | No size validation | +| Runware | bria:10@1 | ✅ Working | Steps hardcoded | +| Runware | google:4@2 | ✅ Working | Resolution param hardcoded | +| Runware | bytedance:seedream@4.5 | ✅ Working | Min pixels hardcoded | + +--- + +## 7. Priority Action Items + +### High Priority +1. **GAP-4/5:** Implement database-driven size validation for all providers +2. **GAP-10:** Add quality_tier validation on save + +### Medium Priority +3. **GAP-6/7:** Move provider-specific params to AIModelConfig.generation_params +4. **GAP-8:** Consider per-content image count override + +### Low Priority +5. **GAP-1:** Clean up unused constants +6. **GAP-9:** Audit and deprecate legacy code +7. **GAP-3:** Fetch valid sizes from API in frontend + +--- + +## 8. Recommendations Summary + +### Short-term (Before Launch) +- Ensure all hardcoded fallbacks are clearly logged +- Test each model tier end-to-end + +### Medium-term (Post-Launch) +- Migrate all hardcoded params to AIModelConfig fields +- Add model validation on quality_tier save + +### Long-term (Future Enhancement) +- Per-content image count override +- Per-keyword image style override +- Image regeneration without deleting existing + +--- + +## Appendix: Key Files Reference + +| File | Purpose | +|------|---------| +| `ai/tasks.py` | Main image generation Celery task | +| `ai/ai_core.py` | Provider-specific generation methods | +| `ai/functions/generate_image_prompts.py` | Extract prompts from content | +| `modules/system/ai_settings.py` | System defaults + account overrides | +| `modules/system/settings_views.py` | Frontend settings API | +| `business/billing/models.py` | AIModelConfig model | +| `business/automation/services/automation_service.py` | Automation Stage 6 |