image s imsages images model fixes new model see dream

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-10 13:16:05 +00:00
parent 6fb0411f56
commit 854b3efd45
9 changed files with 582 additions and 66 deletions

View File

@@ -1028,9 +1028,18 @@ class AICore:
print(f"[AI][{function_name}] Using Nano Banana config: resolution=1k (no width/height)") print(f"[AI][{function_name}] Using Nano Banana config: resolution=1k (no width/height)")
elif runware_model.startswith('bytedance:'): elif runware_model.startswith('bytedance:'):
# Seedream 4.5 (bytedance:seedream@4.5) - High quality ByteDance model # 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 # Uses basic format - no steps, CFGScale, negativePrompt, or special providerSettings needed
# Just use the base inference_task as-is # Remove negativePrompt as it's not supported
print(f"[AI][{function_name}] Using Seedream 4.5 config: basic format, width={width}, height={height}") 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:'): elif runware_model.startswith('runware:'):
# Hi Dream Full (runware:97@1) - General diffusion, steps 20, CFGScale 7 # Hi Dream Full (runware:97@1) - General diffusion, steps 20, CFGScale 7
inference_task['steps'] = 20 inference_task['steps'] = 20

View File

@@ -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") logger.info("[process_image_generation_queue] Step 1: Loading image generation settings")
from igny8_core.modules.system.ai_settings import AISettings from igny8_core.modules.system.ai_settings import AISettings
from igny8_core.ai.model_registry import ModelRegistry from igny8_core.ai.model_registry import ModelRegistry
from igny8_core.business.billing.models import AIModelConfig
# Get effective settings # Get effective settings
image_type = AISettings.get_effective_image_style(account) image_type = AISettings.get_effective_image_style(account)
image_format = 'webp' # Default format image_format = 'webp' # Default format
# Get default image model from database # Get user's selected quality tier (from account settings)
default_model = ModelRegistry.get_default_model('image') quality_tier = AISettings.get_effective_quality_tier(account)
if default_model: logger.info(f"[process_image_generation_queue] User's quality tier: {quality_tier}")
model_config = ModelRegistry.get_model(default_model)
provider = model_config.provider if model_config else 'openai' # Find image model based on quality tier (DYNAMIC from database)
model = default_model 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: 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' provider = 'openai'
model = 'dall-e-3' 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 # Style to prompt enhancement mapping
# These style descriptors are added to the image prompt for better results # 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')) 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]}...") logger.info(f"[process_image_generation_queue] Style: {image_type} -> prompt enhancement: {style_description[:50]}...")
# Model-specific landscape sizes (square is always 1024x1024) # Load image sizes from AIModelConfig (DYNAMIC from database)
# For Runware models - based on Runware documentation for optimal results per model # model_config was loaded above based on quality tier
# For OpenAI DALL-E 3 - uses 1792x1024 for landscape if model_config:
MODEL_LANDSCAPE_SIZES = { # Get sizes from database (single source of truth)
'runware:97@1': '1280x768', # Hi Dream Full landscape model_landscape_size = model_config.landscape_size or '1792x1024'
'bria:10@1': '1344x768', # Bria 3.2 landscape (16:9) model_square_size = model_config.square_size or '1024x1024'
'google:4@2': '1376x768', # Nano Banana landscape (16:9) logger.info(f"[process_image_generation_queue] Loaded sizes from AIModelConfig: landscape={model_landscape_size}, square={model_square_size}")
'dall-e-3': '1792x1024', # DALL-E 3 landscape else:
'dall-e-2': '1024x1024', # DALL-E 2 only supports square # Fallback sizes if no model config (should never happen)
} model_landscape_size = '1792x1024'
DEFAULT_SQUARE_SIZE = '1024x1024' model_square_size = '1024x1024'
logger.warning(f"[process_image_generation_queue] No model config, using fallback sizes")
# Get model-specific landscape size for featured images
model_landscape_size = MODEL_LANDSCAPE_SIZES.get(model, '1792x1024' if provider == 'openai' else '1280x768')
# Featured image always uses model-specific landscape size # Featured image always uses model-specific landscape size
featured_image_size = model_landscape_size featured_image_size = model_landscape_size
# In-article images: alternating square/landscape based on position (handled in image loop) # 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 in_article_landscape_size = model_landscape_size
logger.info(f"[process_image_generation_queue] Settings loaded:") logger.info(f"[process_image_generation_queue] Settings loaded:")

View File

@@ -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),
),
]

View File

@@ -194,7 +194,7 @@ class SystemAISettings(models.Model):
@classmethod @classmethod
def get_effective_quality_tier(cls, account=None) -> str: 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: if account:
# Check consolidated ai_settings first # Check consolidated ai_settings first
try: try:
@@ -214,6 +214,21 @@ class SystemAISettings(models.Model):
override = cls._get_account_override(account, 'ai.quality_tier') override = cls._get_account_override(account, 'ai.quality_tier')
if override is not None: if override is not None:
return str(override) 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 return cls.get_instance().default_quality_tier
@staticmethod @staticmethod

View File

@@ -793,23 +793,38 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
} }
elif integration_type == 'image_generation': elif integration_type == 'image_generation':
# Model-specific landscape sizes from igny8_core.business.billing.models import AIModelConfig
MODEL_LANDSCAPE_SIZES = { from igny8_core.modules.system.ai_settings import AISettings
'runware:97@1': '1280x768',
'bria:10@1': '1344x768',
'google:4@2': '1376x768',
}
# Get default image model from AIModelConfig # Get user's quality tier (from account settings)
default_model = ModelRegistry.get_default_model('image') quality_tier = AISettings.get_effective_quality_tier(account)
if default_model:
model_config = ModelRegistry.get_model(default_model) # Find image model based on quality tier (DYNAMIC from database)
default_service = model_config.provider if model_config else 'openai' 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: else:
default_service = 'openai' default_service = 'openai'
default_model = 'dall-e-3' default_model = 'dall-e-3'
model_landscape_size = '1792x1024'
model_landscape_size = MODEL_LANDSCAPE_SIZES.get(default_model, '1280x768') model_square_size = '1024x1024'
response_data = { response_data = {
'id': 'image_generation', 'id': 'image_generation',
@@ -826,8 +841,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
'image_format': 'webp', 'image_format': 'webp',
'desktop_enabled': True, 'desktop_enabled': True,
'featured_image_size': model_landscape_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), 'desktop_image_size': SystemAISettings.get_effective_image_size(account),
'using_global': True, 'using_global': True,
'quality_tier': quality_tier,
} }
else: else:
# Other integration types - return empty # Other integration types - return empty
@@ -856,9 +874,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
1. SystemAISettings (singleton) provides system-wide defaults 1. SystemAISettings (singleton) provides system-wide defaults
2. AccountSettings (key-value) provides per-account overrides 2. AccountSettings (key-value) provides per-account overrides
3. API keys come from IntegrationProvider (accounts cannot override API keys) 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.ai.model_registry import ModelRegistry
from igny8_core.business.billing.models import AIModelConfig
account = getattr(request, 'account', None) account = getattr(request, 'account', None)
@@ -868,29 +888,36 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
if user and hasattr(user, 'is_authenticated') and user.is_authenticated: if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
account = getattr(user, 'account', None) 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: try:
# Get default image model from AIModelConfig # Get user's quality tier from account settings (DYNAMIC)
default_model = ModelRegistry.get_default_model('image') quality_tier = AISettings.get_effective_quality_tier(account)
if default_model:
model_config = ModelRegistry.get_model(default_model) # Find image model based on quality tier (DYNAMIC from database)
provider = model_config.provider if model_config else 'openai' model_config = None
model = default_model 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: else:
provider = 'openai' provider = 'openai'
model = 'dall-e-3' model = 'dall-e-3'
model_landscape_size = '1792x1024'
# Get model-specific landscape size model_square_size = '1024x1024'
model_landscape_size = MODEL_LANDSCAPE_SIZES.get(model, '1280x768')
default_featured_size = model_landscape_size if provider == 'runware' else '1792x1024'
# Get image style from SystemAISettings with AccountSettings overrides # Get image style from SystemAISettings with AccountSettings overrides
image_style = SystemAISettings.get_effective_image_style(account) 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']: if not image_style or image_style in ['natural', 'vivid']:
image_style = 'photorealistic' 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( return success_response(
data={ data={
@@ -927,8 +954,11 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
'max_in_article_images': SystemAISettings.get_effective_max_images(account), 'max_in_article_images': SystemAISettings.get_effective_max_images(account),
'image_format': 'webp', 'image_format': 'webp',
'desktop_enabled': True, '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), 'desktop_image_size': SystemAISettings.get_effective_image_size(account),
'quality_tier': quality_tier,
} }
}, },
request=request request=request

View File

@@ -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
]

View File

@@ -621,15 +621,20 @@ class ContentGenerationSettingsViewSet(viewsets.ViewSet):
# Get system defaults # Get system defaults
system_defaults = SystemAISettings.get_instance() 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 # Apply account overrides or use system defaults
temperature = account_settings.get('temperature', system_defaults.temperature) temperature = account_settings.get('temperature', system_defaults.temperature)
max_tokens = account_settings.get('max_tokens', system_defaults.max_tokens) max_tokens = account_settings.get('max_tokens', system_defaults.max_tokens)
image_style = account_settings.get('image_style', system_defaults.image_style) image_style = account_settings.get('image_style', system_defaults.image_style)
max_images = account_settings.get('max_images', system_defaults.max_images_per_article) 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) # Get selected tier: account override > default model's tier
default_image_model = AIModelConfig.get_default_image_model() selected_tier = account_settings.get('quality_tier') or default_tier
# Try to find model matching selected tier # Try to find model matching selected tier
selected_model = AIModelConfig.objects.filter( selected_model = AIModelConfig.objects.filter(

View File

@@ -869,8 +869,17 @@ Make sure each prompt is detailed enough for image generation, describing the vi
del inference_task['height'] del inference_task['height']
inference_task['resolution'] = '1k' inference_task['resolution'] = '1k'
elif runware_model.startswith('bytedance:'): elif runware_model.startswith('bytedance:'):
# Seedream models use basic format - no steps, CFGScale needed # Seedream models use basic format - no steps, CFGScale, negativePrompt needed
pass # 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:'): elif runware_model.startswith('runware:'):
# Hi Dream Full - needs steps and CFGScale # Hi Dream Full - needs steps and CFGScale
inference_task['steps'] = 30 inference_task['steps'] = 30

View File

@@ -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<string, string[]> = {
'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 |