fixes for ai and iamge related models bacekedn

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-10 05:11:24 +00:00
parent 0c693dc1cc
commit 975eab46cf
9 changed files with 156 additions and 176 deletions

View File

@@ -1,78 +0,0 @@
# Generated migration for adding image size fields to AIModelConfig
from django.db import migrations, models
def populate_image_sizes(apps, schema_editor):
"""Populate image sizes based on model name"""
AIModelConfig = apps.get_model('billing', 'AIModelConfig')
# Model-specific sizes
model_sizes = {
'runware:97@1': {
'landscape_size': '1280x768',
'square_size': '1024x1024',
'valid_sizes': ['1024x1024', '1280x768', '768x1280'],
},
'bria:10@1': {
'landscape_size': '1344x768',
'square_size': '1024x1024',
'valid_sizes': ['1024x1024', '1344x768', '768x1344'],
},
'google:4@2': {
'landscape_size': '1376x768',
'square_size': '1024x1024',
'valid_sizes': ['1024x1024', '1376x768', '768x1376'],
},
'dall-e-3': {
'landscape_size': '1792x1024',
'square_size': '1024x1024',
'valid_sizes': ['1024x1024', '1792x1024', '1024x1792'],
},
'dall-e-2': {
'landscape_size': '1024x1024',
'square_size': '1024x1024',
'valid_sizes': ['256x256', '512x512', '1024x1024'],
},
}
for model_name, sizes in model_sizes.items():
AIModelConfig.objects.filter(
model_name=model_name,
model_type='image'
).update(**sizes)
def reverse_migration(apps, schema_editor):
"""Clear image size fields"""
AIModelConfig = apps.get_model('billing', 'AIModelConfig')
AIModelConfig.objects.filter(model_type='image').update(
landscape_size=None,
square_size='1024x1024',
valid_sizes=[],
)
class Migration(migrations.Migration):
dependencies = [
('billing', '0026_populate_aimodel_credits'),
]
operations = [
migrations.AddField(
model_name='aimodelconfig',
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='aimodelconfig',
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='aimodelconfig',
name='valid_sizes',
field=models.JSONField(blank=True, default=list, help_text="List of valid sizes for this model (e.g., ['1024x1024', '1792x1024'])"),
),
migrations.RunPython(populate_image_sizes, reverse_migration),
]

View File

@@ -0,0 +1,15 @@
# Generated manually - Add image size fields to AIModelConfig
from django.db import migrations, models
class Migration(migrations.Migration):
"""Add landscape_size, square_size, and valid_sizes fields to AIModelConfig."""
dependencies = [
('billing', '0029_add_webhook_event_and_manual_reference_constraint'),
]
operations = [
# Fields already added via direct SQL, just mark as noop
# This ensures the model matches the database schema
]

View File

@@ -47,6 +47,12 @@ class SystemAISettings(models.Model):
('hd', 'HD'), ('hd', 'HD'),
] ]
QUALITY_TIER_CHOICES = [
('basic', 'Basic'),
('quality', 'Quality'),
('premium', 'Premium'),
]
IMAGE_SIZE_CHOICES = [ IMAGE_SIZE_CHOICES = [
('1024x1024', '1024x1024 (Square)'), ('1024x1024', '1024x1024 (Square)'),
('1792x1024', '1792x1024 (Landscape)'), ('1792x1024', '1792x1024 (Landscape)'),
@@ -70,6 +76,12 @@ class SystemAISettings(models.Model):
choices=IMAGE_STYLE_CHOICES, choices=IMAGE_STYLE_CHOICES,
help_text="Default image style" help_text="Default image style"
) )
default_quality_tier = models.CharField(
max_length=20,
default='basic',
choices=QUALITY_TIER_CHOICES,
help_text="Default quality tier for image generation"
)
image_quality = models.CharField( image_quality = models.CharField(
max_length=20, max_length=20,
default='standard', default='standard',
@@ -78,7 +90,11 @@ class SystemAISettings(models.Model):
) )
max_images_per_article = models.IntegerField( max_images_per_article = models.IntegerField(
default=4, default=4,
help_text="Max in-article images (1-8)" help_text="Default number of in-article images"
)
max_allowed_images = models.IntegerField(
default=8,
help_text="Maximum allowed in-article images (dropdown limit)"
) )
image_size = models.CharField( image_size = models.CharField(
max_length=20, max_length=20,

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.10 on 2026-01-10 04:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0021_add_smtp_email_settings'),
]
operations = [
migrations.AddField(
model_name='systemaisettings',
name='default_quality_tier',
field=models.CharField(choices=[('basic', 'Basic'), ('quality', 'Quality'), ('premium', 'Premium')], default='basic', help_text='Default quality tier for image generation', max_length=20),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.10 on 2026-01-10 04:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('system', '0022_systemaisettings_default_quality_tier_and_more'),
]
operations = [
migrations.AddField(
model_name='systemaisettings',
name='max_allowed_images',
field=models.IntegerField(default=8, help_text='Maximum allowed in-article images (dropdown limit)'),
),
migrations.AlterField(
model_name='systemaisettings',
name='max_images_per_article',
field=models.IntegerField(default=4, help_text='Default number of in-article images'),
),
]

View File

@@ -528,12 +528,17 @@ class ContentGenerationSettingsViewSet(viewsets.ViewSet):
This endpoint returns: This endpoint returns:
- content_generation: temperature, max_tokens - content_generation: temperature, max_tokens
- image_generation: quality_tiers, selected_tier, styles, selected_style, max_images - image_generation: quality_tiers, selected_tier, styles, selected_style, max_images
Settings are stored in a single AccountSettings record with key='ai_settings'
""" """
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
authentication_classes = [JWTAuthentication] authentication_classes = [JWTAuthentication]
throttle_scope = 'system' throttle_scope = 'system'
throttle_classes = [DebugScopedRateThrottle] throttle_classes = [DebugScopedRateThrottle]
# Single key for all AI settings per account
AI_SETTINGS_KEY = 'ai_settings'
def _get_account(self, request): def _get_account(self, request):
"""Get account from request""" """Get account from request"""
account = getattr(request, 'account', None) account = getattr(request, 'account', None)
@@ -543,6 +548,20 @@ class ContentGenerationSettingsViewSet(viewsets.ViewSet):
account = getattr(user, 'account', None) account = getattr(user, 'account', None)
return account return account
def _get_account_ai_settings(self, account):
"""Get consolidated AI settings for account, returns dict with all settings"""
if not account:
return {}
setting = AccountSettings.objects.filter(
account=account,
key=self.AI_SETTINGS_KEY
).first()
if setting and setting.value:
return setting.value
return {}
def list(self, request): def list(self, request):
""" """
GET /api/v1/accounts/settings/ai/ GET /api/v1/accounts/settings/ai/
@@ -566,6 +585,9 @@ class ContentGenerationSettingsViewSet(viewsets.ViewSet):
try: try:
from igny8_core.business.billing.models import AIModelConfig from igny8_core.business.billing.models import AIModelConfig
# Get consolidated account settings
account_settings = self._get_account_ai_settings(account)
# Get quality tiers from AIModelConfig (image models) # Get quality tiers from AIModelConfig (image models)
quality_tiers = [] quality_tiers = []
for model in AIModelConfig.objects.filter(model_type='image', is_active=True).order_by('credits_per_image'): for model in AIModelConfig.objects.filter(model_type='image', is_active=True).order_by('credits_per_image'):
@@ -594,21 +616,15 @@ class ContentGenerationSettingsViewSet(viewsets.ViewSet):
for opt in SystemAISettings.IMAGE_STYLE_CHOICES for opt in SystemAISettings.IMAGE_STYLE_CHOICES
] ]
# Get effective settings (SystemAISettings with AccountSettings overrides) # Get system defaults
temperature = SystemAISettings.get_effective_temperature(account) system_defaults = SystemAISettings.get_instance()
max_tokens = SystemAISettings.get_effective_max_tokens(account)
image_style = SystemAISettings.get_effective_image_style(account)
max_images = SystemAISettings.get_effective_max_images(account)
# Get selected quality tier from AccountSettings # Apply account overrides or use system defaults
selected_tier = 'quality' # Default temperature = account_settings.get('temperature', system_defaults.temperature)
if account: max_tokens = account_settings.get('max_tokens', system_defaults.max_tokens)
tier_setting = AccountSettings.objects.filter( image_style = account_settings.get('image_style', system_defaults.image_style)
account=account, max_images = account_settings.get('max_images', system_defaults.max_images_per_article)
key='ai.image_quality_tier' selected_tier = account_settings.get('quality_tier', system_defaults.default_quality_tier)
).first()
if tier_setting and tier_setting.value: # Model uses 'value' field
selected_tier = tier_setting.value.get('value', 'quality')
# Get default image model (or model for selected tier) # Get default image model (or model for selected tier)
default_image_model = AIModelConfig.get_default_image_model() default_image_model = AIModelConfig.get_default_image_model()
@@ -641,7 +657,7 @@ class ContentGenerationSettingsViewSet(viewsets.ViewSet):
'styles': styles, 'styles': styles,
'selected_style': image_style, 'selected_style': image_style,
'max_images': max_images, 'max_images': max_images,
'max_allowed': 8, 'max_allowed': system_defaults.max_allowed_images,
# Image sizes based on selected model # Image sizes based on selected model
'featured_image_size': featured_image_size, 'featured_image_size': featured_image_size,
'landscape_image_size': landscape_image_size, 'landscape_image_size': landscape_image_size,
@@ -665,20 +681,14 @@ class ContentGenerationSettingsViewSet(viewsets.ViewSet):
""" """
PUT/POST /api/v1/accounts/settings/ai/ PUT/POST /api/v1/accounts/settings/ai/
Save account-specific overrides to AccountSettings. Save account-specific overrides to a single AccountSettings record.
All AI settings are stored in one record with key='ai_settings'.
Accepts nested structure: Accepts nested structure:
{ {
"content_generation": { "temperature": 0.8, "max_tokens": 4096 }, "content_generation": { "temperature": 0.8, "max_tokens": 4096 },
"image_generation": { "quality_tier": "premium", "image_style": "illustration", "max_images_per_article": 6 } "image_generation": { "quality_tier": "premium", "image_style": "illustration", "max_images_per_article": 6 }
} }
Or flat structure:
{
"temperature": 0.8,
"max_tokens": 4096,
"image_quality_tier": "premium",
"image_style": "illustration",
"max_images": 6
}
""" """
account = self._get_account(request) account = self._get_account(request)
@@ -691,44 +701,38 @@ class ContentGenerationSettingsViewSet(viewsets.ViewSet):
try: try:
data = request.data data = request.data
saved_keys = []
# Get existing settings or start fresh
existing_settings = self._get_account_ai_settings(account)
# Handle nested structure from frontend # Handle nested structure from frontend
content_gen = data.get('content_generation', {}) content_gen = data.get('content_generation', {})
image_gen = data.get('image_generation', {}) image_gen = data.get('image_generation', {})
# Flatten nested structure or use flat keys # Update with new values (only if provided)
flat_data = { if content_gen.get('temperature') is not None:
'temperature': content_gen.get('temperature') if content_gen else data.get('temperature'), existing_settings['temperature'] = content_gen['temperature']
'max_tokens': content_gen.get('max_tokens') if content_gen else data.get('max_tokens'), if content_gen.get('max_tokens') is not None:
'image_quality_tier': image_gen.get('quality_tier') if image_gen else data.get('image_quality_tier'), existing_settings['max_tokens'] = content_gen['max_tokens']
'image_style': image_gen.get('image_style') if image_gen else data.get('image_style'),
'max_images': image_gen.get('max_images_per_article') if image_gen else data.get('max_images'),
}
# Map request fields to AccountSettings keys if image_gen.get('quality_tier') is not None:
key_mappings = { existing_settings['quality_tier'] = image_gen['quality_tier']
'temperature': 'ai.temperature', if image_gen.get('image_style') is not None:
'max_tokens': 'ai.max_tokens', existing_settings['image_style'] = image_gen['image_style']
'image_quality_tier': 'ai.image_quality_tier', if image_gen.get('max_images_per_article') is not None:
'image_style': 'ai.image_style', existing_settings['max_images'] = image_gen['max_images_per_article']
'max_images': 'ai.max_images',
}
for field, account_key in key_mappings.items(): # Save as single consolidated record
value = flat_data.get(field) setting, created = AccountSettings.objects.update_or_create(
if value is not None: account=account,
AccountSettings.objects.update_or_create( key=self.AI_SETTINGS_KEY,
account=account, defaults={'value': existing_settings}
key=account_key, )
defaults={'value': {'value': value}} # Model uses 'value' field
)
saved_keys.append(account_key)
logger.info(f"[ContentGenerationSettings] Saved {saved_keys} for account {account.id}") logger.info(f"[ContentGenerationSettings] Saved ai_settings for account {account.id}: {existing_settings}")
return success_response( return success_response(
data={'saved_keys': saved_keys}, data={'settings': existing_settings},
message='AI settings saved successfully', message='AI settings saved successfully',
request=request request=request
) )

View File

@@ -776,25 +776,16 @@ UNFOLD = {
{"title": "Downloads", "icon": "download", "link": lambda request: "/admin/plugins/plugindownload/"}, {"title": "Downloads", "icon": "download", "link": lambda request: "/admin/plugins/plugindownload/"},
], ],
}, },
# Automation
{
"title": "Automation",
"icon": "smart_toy",
"collapsible": True,
"items": [
{"title": "Configs", "icon": "settings_suggest", "link": lambda request: "/admin/automation/automationconfig/"},
{"title": "Runs", "icon": "play_circle", "link": lambda request: "/admin/automation/automationrun/"},
],
},
# AI Configuration # AI Configuration
{ {
"title": "AI Configuration", "title": "AI Configuration",
"icon": "psychology", "icon": "psychology",
"collapsible": True, "collapsible": True,
"items": [ "items": [
{"title": "System AI Settings", "icon": "tune", "link": lambda request: "/admin/system/systemaisettings/"},
{"title": "AI Models", "icon": "model_training", "link": lambda request: "/admin/billing/aimodelconfig/"}, {"title": "AI Models", "icon": "model_training", "link": lambda request: "/admin/billing/aimodelconfig/"},
{"title": "Credit Costs", "icon": "calculate", "link": lambda request: "/admin/billing/creditcostconfig/"}, {"title": "Credit Costs by Function", "icon": "calculate", "link": lambda request: "/admin/billing/creditcostconfig/"},
{"title": "Billing Config", "icon": "tune", "link": lambda request: "/admin/billing/billingconfiguration/"}, {"title": "Account-Specific AI Settings", "icon": "account_circle", "link": lambda request: "/admin/system/aisettings/"},
{"title": "AI Task Logs", "icon": "history", "link": lambda request: "/admin/ai/aitasklog/"}, {"title": "AI Task Logs", "icon": "history", "link": lambda request: "/admin/ai/aitasklog/"},
], ],
}, },
@@ -817,11 +808,25 @@ UNFOLD = {
"collapsible": True, "collapsible": True,
"items": [ "items": [
{"title": "Integration Providers", "icon": "key", "link": lambda request: "/admin/system/integrationprovider/"}, {"title": "Integration Providers", "icon": "key", "link": lambda request: "/admin/system/integrationprovider/"},
{"title": "System AI Settings", "icon": "psychology", "link": lambda request: "/admin/system/systemaisettings/"}, {"title": "Global AI Prompts", "icon": "chat", "link": lambda request: "/admin/system/globalaiprompt/"},
{"title": "Automation Configs", "icon": "settings_suggest", "link": lambda request: "/admin/automation/automationconfig/"},
{"title": "Automation Runs", "icon": "play_circle", "link": lambda request: "/admin/automation/automationrun/"},
{"title": "Module Settings", "icon": "view_module", "link": lambda request: "/admin/system/globalmodulesettings/"}, {"title": "Module Settings", "icon": "view_module", "link": lambda request: "/admin/system/globalmodulesettings/"},
{"title": "AI Prompts", "icon": "smart_toy", "link": lambda request: "/admin/system/globalaiprompt/"},
{"title": "Author Profiles", "icon": "person_outline", "link": lambda request: "/admin/system/globalauthorprofile/"}, {"title": "Author Profiles", "icon": "person_outline", "link": lambda request: "/admin/system/globalauthorprofile/"},
{"title": "Strategies", "icon": "strategy", "link": lambda request: "/admin/system/globalstrategy/"}, {"title": "Strategies", "icon": "strategy", "link": lambda request: "/admin/system/globalstrategy/"},
{"title": "Billing Configuration", "icon": "payments", "link": lambda request: "/admin/billing/billingconfiguration/"},
],
},
# System Configuration
{
"title": "System Configuration",
"icon": "tune",
"collapsible": True,
"items": [
{"title": "System Settings", "icon": "settings", "link": lambda request: "/admin/system/systemsettings/"},
{"title": "Account Settings", "icon": "account_circle", "link": lambda request: "/admin/system/accountsettings/"},
{"title": "User Settings", "icon": "person_search", "link": lambda request: "/admin/system/usersettings/"},
{"title": "Module Settings", "icon": "view_module", "link": lambda request: "/admin/system/modulesettings/"},
], ],
}, },
# Resources # Resources

View File

@@ -87,23 +87,8 @@ export default function SiteList() {
}, []); }, []);
const loadUserPreferences = async () => { const loadUserPreferences = async () => {
try { // User preferences are now loaded from site/account data, not from a separate endpoint
const { fetchAccountSetting } = await import('../../services/api'); // This function is kept for backward compatibility but does nothing
const setting = await fetchAccountSetting('user_preferences');
const preferences = setting.config as { selectedIndustry?: string; selectedSectors?: string[] } | undefined;
if (preferences) {
setUserPreferences(preferences);
}
} catch (error: any) {
// 404 means preferences don't exist yet - that's fine
// 500 and other errors should be handled silently - user can still use the page
if (error?.status === 404) {
// Preferences don't exist yet - this is expected for new users
return;
}
// Silently handle other errors (500, network errors, etc.) - don't spam console
// User can still use the page without preferences
}
}; };
useEffect(() => { useEffect(() => {

View File

@@ -433,16 +433,8 @@ export default function SiteSettings() {
}; };
const loadUserPreferences = async () => { const loadUserPreferences = async () => {
try { // User preferences are now loaded from site/account data, not from a separate endpoint
const { fetchAccountSetting } = await import('../../services/api'); // This function is kept for backward compatibility but does nothing
const setting = await fetchAccountSetting('user_preferences');
const preferences = setting.config as { selectedIndustry?: string; selectedSectors?: string[] } | undefined;
if (preferences) {
setUserPreferences(preferences);
}
} catch (error: any) {
// Silently handle errors
}
}; };
const loadSiteSectors = async () => { const loadSiteSectors = async () => {
@@ -947,7 +939,7 @@ export default function SiteSettings() {
<SelectDropdown <SelectDropdown
options={qualityTiers.length > 0 options={qualityTiers.length > 0
? qualityTiers.map(tier => ({ ? qualityTiers.map(tier => ({
value: tier.value, value: tier.tier || tier.value,
label: `${tier.label} (${tier.credits} credits)` label: `${tier.label} (${tier.credits} credits)`
})) }))
: [ : [
@@ -956,7 +948,7 @@ export default function SiteSettings() {
{ value: 'premium', label: 'Premium (15 credits)' }, { value: 'premium', label: 'Premium (15 credits)' },
] ]
} }
value={selectedTier} value={selectedTier || 'quality'}
onChange={(value) => setSelectedTier(value)} onChange={(value) => setSelectedTier(value)}
className="w-full" className="w-full"
/> />
@@ -987,11 +979,11 @@ export default function SiteSettings() {
<div> <div>
<Label className="mb-2">Images per Article</Label> <Label className="mb-2">Images per Article</Label>
<SelectDropdown <SelectDropdown
options={Array.from({ length: 4 }, (_, i) => ({ options={Array.from({ length: maxAllowed || 8 }, (_, i) => ({
value: String(i + 1), value: String(i + 1),
label: `${i + 1} image${i > 0 ? 's' : ''}`, label: `${i + 1} image${i > 0 ? 's' : ''}`,
}))} }))}
value={String(maxImages)} value={String(maxImages || 4)}
onChange={(value) => setMaxImages(parseInt(value))} onChange={(value) => setMaxImages(parseInt(value))}
className="w-full" className="w-full"
/> />