IMage genartion service and models revamp - #Migration Runs
This commit is contained in:
@@ -982,24 +982,51 @@ class AICore:
|
||||
# Runware uses array payload with authentication task first, then imageInference
|
||||
# Reference: image-generation.php lines 79-97
|
||||
import uuid
|
||||
|
||||
# Build base inference task
|
||||
inference_task = {
|
||||
'taskType': 'imageInference',
|
||||
'taskUUID': str(uuid.uuid4()),
|
||||
'positivePrompt': prompt,
|
||||
'negativePrompt': negative_prompt or '',
|
||||
'model': runware_model,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'numberResults': 1,
|
||||
'outputFormat': 'webp'
|
||||
}
|
||||
|
||||
# Model-specific parameter configuration based on Runware documentation
|
||||
if runware_model.startswith('bria:'):
|
||||
# Bria 3.2 (bria:10@1) - Commercial-ready, steps 4-10 (default 8)
|
||||
inference_task['steps'] = 8
|
||||
# Bria provider settings for enhanced quality
|
||||
inference_task['providerSettings'] = {
|
||||
'bria': {
|
||||
'promptEnhancement': True,
|
||||
'enhanceImage': True,
|
||||
'medium': 'photography',
|
||||
'contentModeration': True
|
||||
}
|
||||
}
|
||||
print(f"[AI][{function_name}] Using Bria 3.2 config: steps=8, providerSettings enabled")
|
||||
elif runware_model.startswith('google:'):
|
||||
# Nano Banana (google:4@2) - Premium quality, no explicit steps needed
|
||||
# Google models handle steps internally
|
||||
inference_task['resolution'] = '1k' # Use 1K tier for optimal speed/quality
|
||||
print(f"[AI][{function_name}] Using Nano Banana config: resolution=1k")
|
||||
else:
|
||||
# Hi Dream Full (runware:97@1) - General diffusion, steps 20, CFGScale 7
|
||||
inference_task['steps'] = 20
|
||||
inference_task['CFGScale'] = 7
|
||||
print(f"[AI][{function_name}] Using Hi Dream Full config: steps=20, CFGScale=7")
|
||||
|
||||
payload = [
|
||||
{
|
||||
'taskType': 'authentication',
|
||||
'apiKey': api_key
|
||||
},
|
||||
{
|
||||
'taskType': 'imageInference',
|
||||
'taskUUID': str(uuid.uuid4()),
|
||||
'positivePrompt': prompt,
|
||||
'negativePrompt': negative_prompt or '',
|
||||
'model': runware_model,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'steps': 30,
|
||||
'CFGScale': 7.5,
|
||||
'numberResults': 1,
|
||||
'outputFormat': 'webp'
|
||||
}
|
||||
inference_task
|
||||
]
|
||||
|
||||
request_start = time.time()
|
||||
|
||||
@@ -101,8 +101,6 @@ class GenerateImagesFunction(BaseAIFunction):
|
||||
'model': model,
|
||||
'image_type': image_settings.get('image_type') or global_settings.image_style,
|
||||
'max_in_article_images': int(image_settings.get('max_in_article_images') or global_settings.max_in_article_images),
|
||||
'desktop_enabled': image_settings.get('desktop_enabled', True),
|
||||
'mobile_enabled': image_settings.get('mobile_enabled', True),
|
||||
}
|
||||
|
||||
def build_prompt(self, data: Dict, account=None) -> Dict:
|
||||
|
||||
@@ -217,20 +217,32 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
||||
logger.info(f"[process_image_generation_queue] Using PROVIDER: {provider}, MODEL: {model} from settings")
|
||||
image_type = config.get('image_type') or global_settings.image_style
|
||||
image_format = config.get('image_format', 'webp')
|
||||
desktop_enabled = config.get('desktop_enabled', True)
|
||||
mobile_enabled = config.get('mobile_enabled', True)
|
||||
# Get image sizes from config, with fallback defaults
|
||||
featured_image_size = config.get('featured_image_size') or ('1280x832' if provider == 'runware' else '1024x1024')
|
||||
desktop_image_size = config.get('desktop_image_size') or global_settings.desktop_image_size
|
||||
in_article_image_size = config.get('in_article_image_size') or '512x512' # Default to 512x512
|
||||
|
||||
# Model-specific landscape sizes (square is always 1024x1024)
|
||||
# Based on Runware documentation for optimal results per model
|
||||
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 = '1024x1024'
|
||||
|
||||
# Get model-specific landscape size for featured images
|
||||
model_landscape_size = MODEL_LANDSCAPE_SIZES.get(model, '1280x768')
|
||||
|
||||
# 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_landscape_size = model_landscape_size
|
||||
|
||||
logger.info(f"[process_image_generation_queue] Settings loaded:")
|
||||
logger.info(f" - Provider: {provider}")
|
||||
logger.info(f" - Model: {model}")
|
||||
logger.info(f" - Image type: {image_type}")
|
||||
logger.info(f" - Image format: {image_format}")
|
||||
logger.info(f" - Desktop enabled: {desktop_enabled}")
|
||||
logger.info(f" - Mobile enabled: {mobile_enabled}")
|
||||
logger.info(f" - Featured image size: {featured_image_size}")
|
||||
logger.info(f" - In-article square: {in_article_square_size}, landscape: {in_article_landscape_size}")
|
||||
|
||||
# Get provider API key
|
||||
# API keys are ALWAYS from GlobalIntegrationSettings (accounts cannot override API keys)
|
||||
@@ -478,15 +490,25 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
||||
}
|
||||
)
|
||||
|
||||
# Use appropriate size based on image type
|
||||
# Use appropriate size based on image type and position
|
||||
# Featured: Always landscape (model-specific)
|
||||
# In-article: Alternating square/landscape based on position
|
||||
# Position 0: Square (1024x1024)
|
||||
# Position 1: Landscape (model-specific)
|
||||
# Position 2: Square (1024x1024)
|
||||
# Position 3: Landscape (model-specific)
|
||||
if image.image_type == 'featured':
|
||||
image_size = featured_image_size # Read from config
|
||||
elif image.image_type == 'desktop':
|
||||
image_size = desktop_image_size
|
||||
elif image.image_type == 'mobile':
|
||||
image_size = '512x512' # Fixed mobile size
|
||||
else: # in_article or other
|
||||
image_size = in_article_image_size # Read from config, default 512x512
|
||||
image_size = featured_image_size # Model-specific landscape
|
||||
elif image.image_type == 'in_article':
|
||||
# Alternate based on position: even=square, odd=landscape
|
||||
position = image.position or 0
|
||||
if position % 2 == 0: # Position 0, 2: Square
|
||||
image_size = in_article_square_size
|
||||
else: # Position 1, 3: Landscape
|
||||
image_size = in_article_landscape_size
|
||||
logger.info(f"[process_image_generation_queue] In-article image position {position}: using {'square' if position % 2 == 0 else 'landscape'} size {image_size}")
|
||||
else: # desktop or other (legacy)
|
||||
image_size = in_article_square_size # Default to square
|
||||
|
||||
result = ai_core.generate_image(
|
||||
prompt=formatted_prompt,
|
||||
|
||||
@@ -568,10 +568,33 @@ class Images(SoftDeletableModel, SiteSectorBaseModel):
|
||||
models.Index(fields=['content', 'position']),
|
||||
models.Index(fields=['task', 'position']),
|
||||
]
|
||||
# Ensure unique position per content+image_type combination
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=['content', 'image_type', 'position'],
|
||||
name='unique_content_image_type_position',
|
||||
condition=models.Q(is_deleted=False)
|
||||
),
|
||||
]
|
||||
|
||||
objects = SoftDeleteManager()
|
||||
all_objects = models.Manager()
|
||||
|
||||
@property
|
||||
def aspect_ratio(self):
|
||||
"""
|
||||
Determine aspect ratio based on position for layout rendering.
|
||||
Position 0, 2: square (1:1)
|
||||
Position 1, 3: landscape (16:9 or similar)
|
||||
Featured: always landscape
|
||||
"""
|
||||
if self.image_type == 'featured':
|
||||
return 'landscape'
|
||||
elif self.image_type == 'in_article':
|
||||
# Even positions are square, odd positions are landscape
|
||||
return 'square' if (self.position or 0) % 2 == 0 else 'landscape'
|
||||
return 'square' # Default
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Track image usage when creating new images"""
|
||||
is_new = self.pk is None
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
Migration: Update Runware/Image model configurations for new model structure
|
||||
|
||||
This migration:
|
||||
1. Updates runware:97@1 to "Hi Dream Full - Basic"
|
||||
2. Adds Bria 3.2 model as bria:10@1 (correct AIR ID)
|
||||
3. Adds Nano Banana (Google) as google:4@2 (Premium tier)
|
||||
4. Removes old civitai model reference
|
||||
5. Adds one_liner_description field values
|
||||
"""
|
||||
from decimal import Decimal
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def update_image_models(apps, schema_editor):
|
||||
"""Update image models in AIModelConfig"""
|
||||
AIModelConfig = apps.get_model('billing', 'AIModelConfig')
|
||||
|
||||
# Update existing runware:97@1 model
|
||||
AIModelConfig.objects.update_or_create(
|
||||
model_name='runware:97@1',
|
||||
defaults={
|
||||
'display_name': 'Hi Dream Full - Basic',
|
||||
'model_type': 'image',
|
||||
'provider': 'runware',
|
||||
'cost_per_image': Decimal('0.006'), # Basic tier, cheaper
|
||||
'valid_sizes': ['1024x1024', '1280x768', '768x1280'],
|
||||
'supports_json_mode': False,
|
||||
'supports_vision': False,
|
||||
'supports_function_calling': False,
|
||||
'is_active': True,
|
||||
'is_default': True,
|
||||
'sort_order': 10,
|
||||
'description': 'Fast & affordable image generation. Steps: 20, CFG: 7. Good for quick iterations.',
|
||||
}
|
||||
)
|
||||
|
||||
# Add Bria 3.2 model with correct AIR ID
|
||||
AIModelConfig.objects.update_or_create(
|
||||
model_name='bria:10@1',
|
||||
defaults={
|
||||
'display_name': 'Bria 3.2 - Quality',
|
||||
'model_type': 'image',
|
||||
'provider': 'runware', # Via Runware API
|
||||
'cost_per_image': Decimal('0.010'), # Quality tier
|
||||
'valid_sizes': ['1024x1024', '1344x768', '768x1344', '1216x832', '832x1216'],
|
||||
'supports_json_mode': False,
|
||||
'supports_vision': False,
|
||||
'supports_function_calling': False,
|
||||
'is_active': True,
|
||||
'is_default': False,
|
||||
'sort_order': 11,
|
||||
'description': 'Commercial-safe AI. Steps: 8, prompt enhancement enabled. Licensed training data.',
|
||||
}
|
||||
)
|
||||
|
||||
# Add Nano Banana (Google) Premium model
|
||||
AIModelConfig.objects.update_or_create(
|
||||
model_name='google:4@2',
|
||||
defaults={
|
||||
'display_name': 'Nano Banana - Premium',
|
||||
'model_type': 'image',
|
||||
'provider': 'runware', # Via Runware API
|
||||
'cost_per_image': Decimal('0.015'), # Premium tier
|
||||
'valid_sizes': ['1024x1024', '1376x768', '768x1376', '1264x848', '848x1264'],
|
||||
'supports_json_mode': False,
|
||||
'supports_vision': False,
|
||||
'supports_function_calling': False,
|
||||
'is_active': True,
|
||||
'is_default': False,
|
||||
'sort_order': 12,
|
||||
'description': 'Google Gemini 3 Pro. Best quality, text rendering, advanced reasoning. Premium pricing.',
|
||||
}
|
||||
)
|
||||
|
||||
# Deactivate old civitai model (replaced by correct bria:10@1)
|
||||
AIModelConfig.objects.filter(
|
||||
model_name='civitai:618692@691639'
|
||||
).update(is_active=False)
|
||||
|
||||
# Deactivate other old models
|
||||
AIModelConfig.objects.filter(
|
||||
model_name__in=['runware:100@1', 'runware:101@1']
|
||||
).update(is_active=False)
|
||||
|
||||
|
||||
def reverse_migration(apps, schema_editor):
|
||||
"""Reverse the migration"""
|
||||
AIModelConfig = apps.get_model('billing', 'AIModelConfig')
|
||||
|
||||
# Restore old display names
|
||||
AIModelConfig.objects.filter(model_name='runware:97@1').update(
|
||||
display_name='Hi Dream Full - Standard',
|
||||
)
|
||||
|
||||
# Remove new models
|
||||
AIModelConfig.objects.filter(model_name__in=['bria:10@1', 'google:4@2']).delete()
|
||||
|
||||
# Re-activate old models
|
||||
AIModelConfig.objects.filter(
|
||||
model_name__in=['runware:100@1', 'runware:101@1', 'civitai:618692@691639']
|
||||
).update(is_active=True)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('billing', '0023_update_runware_models'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_image_models, reverse_migration),
|
||||
]
|
||||
@@ -63,8 +63,9 @@ def get_image_model_choices(provider=None):
|
||||
]
|
||||
elif provider == 'runware':
|
||||
return [
|
||||
('runware:97@1', 'Hi Dream Full - Standard'),
|
||||
('civitai:618692@691639', 'Bria 3.2 - Premium'),
|
||||
('runware:97@1', 'Hi Dream Full - Basic'),
|
||||
('bria:10@1', 'Bria 3.2 - Quality'),
|
||||
('google:4@2', 'Nano Banana - Premium'),
|
||||
]
|
||||
return []
|
||||
|
||||
@@ -171,10 +172,21 @@ class GlobalIntegrationSettings(models.Model):
|
||||
]
|
||||
|
||||
RUNWARE_MODEL_CHOICES = [
|
||||
('runware:97@1', 'Hi Dream Full - Standard'),
|
||||
('civitai:618692@691639', 'Bria 3.2 - Premium'),
|
||||
('runware:97@1', 'Hi Dream Full - Basic'),
|
||||
('bria:10@1', 'Bria 3.2 - Quality'),
|
||||
('google:4@2', 'Nano Banana - Premium'),
|
||||
]
|
||||
|
||||
# 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'
|
||||
|
||||
BRIA_MODEL_CHOICES = [
|
||||
('bria-2.3', 'Bria 2.3 - High Quality ($0.015/image)'),
|
||||
('bria-2.3-fast', 'Bria 2.3 Fast - Quick Generation ($0.010/image)'),
|
||||
@@ -335,13 +347,9 @@ class GlobalIntegrationSettings(models.Model):
|
||||
desktop_image_size = models.CharField(
|
||||
max_length=20,
|
||||
default='1024x1024',
|
||||
help_text="Default desktop image size (accounts can override if plan allows)"
|
||||
)
|
||||
mobile_image_size = models.CharField(
|
||||
max_length=20,
|
||||
default='512x512',
|
||||
help_text="Default mobile image size (accounts can override if plan allows)"
|
||||
help_text="Default image size for in-article images (accounts can override if plan allows)"
|
||||
)
|
||||
# Note: mobile_image_size removed - no longer needed
|
||||
|
||||
# Metadata
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Generated migration for updating Runware image models
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('system', '0013_add_anthropic_integration'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Update runware_model field with new model choices
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='runware_model',
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
('runware:97@1', 'Hi Dream Full - Basic'),
|
||||
('bria:10@1', 'Bria 3.2 - Quality'),
|
||||
('google:4@2', 'Nano Banana - Premium'),
|
||||
],
|
||||
default='runware:97@1',
|
||||
help_text='Default Runware model (accounts can override if plan allows)',
|
||||
max_length=100,
|
||||
),
|
||||
),
|
||||
# Update desktop_image_size help text (mobile removed)
|
||||
migrations.AlterField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='desktop_image_size',
|
||||
field=models.CharField(
|
||||
default='1024x1024',
|
||||
help_text='Default image size for in-article images (accounts can override if plan allows)',
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
# Remove mobile_image_size field if it exists (safe removal)
|
||||
migrations.RemoveField(
|
||||
model_name='globalintegrationsettings',
|
||||
name='mobile_image_size',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated migration for Images model unique constraint
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('writer', '0015_add_publishing_scheduler_fields'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Add unique constraint for content + image_type + position
|
||||
# This ensures no duplicate positions for the same image type within a content
|
||||
migrations.AddConstraint(
|
||||
model_name='images',
|
||||
constraint=models.UniqueConstraint(
|
||||
condition=models.Q(('is_deleted', False)),
|
||||
fields=('content', 'image_type', 'position'),
|
||||
name='unique_content_image_type_position',
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -76,6 +76,7 @@ class ImagesSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for Images model"""
|
||||
task_title = serializers.SerializerMethodField()
|
||||
content_title = serializers.SerializerMethodField()
|
||||
aspect_ratio = serializers.ReadOnlyField() # Expose aspect_ratio property
|
||||
|
||||
class Meta:
|
||||
model = Images
|
||||
@@ -92,11 +93,12 @@ class ImagesSerializer(serializers.ModelSerializer):
|
||||
'caption',
|
||||
'status',
|
||||
'position',
|
||||
'aspect_ratio',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'account_id',
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id']
|
||||
read_only_fields = ['id', 'created_at', 'updated_at', 'account_id', 'aspect_ratio']
|
||||
|
||||
def get_task_title(self, obj):
|
||||
"""Get task title"""
|
||||
|
||||
Reference in New Issue
Block a user