image gen mess
This commit is contained in:
@@ -722,7 +722,8 @@ class AICore:
|
|||||||
n: int = 1,
|
n: int = 1,
|
||||||
api_key: Optional[str] = None,
|
api_key: Optional[str] = None,
|
||||||
negative_prompt: Optional[str] = None,
|
negative_prompt: Optional[str] = None,
|
||||||
function_name: str = 'generate_image'
|
function_name: str = 'generate_image',
|
||||||
|
style: Optional[str] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Generate image using AI with console logging.
|
Generate image using AI with console logging.
|
||||||
@@ -743,7 +744,7 @@ class AICore:
|
|||||||
print(f"[AI][{function_name}] Step 1: Preparing image generation request...")
|
print(f"[AI][{function_name}] Step 1: Preparing image generation request...")
|
||||||
|
|
||||||
if provider == 'openai':
|
if provider == 'openai':
|
||||||
return self._generate_image_openai(prompt, model, size, n, api_key, negative_prompt, function_name)
|
return self._generate_image_openai(prompt, model, size, n, api_key, negative_prompt, function_name, style)
|
||||||
elif provider == 'runware':
|
elif provider == 'runware':
|
||||||
return self._generate_image_runware(prompt, model, size, n, api_key, negative_prompt, function_name)
|
return self._generate_image_runware(prompt, model, size, n, api_key, negative_prompt, function_name)
|
||||||
elif provider == 'bria':
|
elif provider == 'bria':
|
||||||
@@ -767,9 +768,15 @@ class AICore:
|
|||||||
n: int,
|
n: int,
|
||||||
api_key: Optional[str],
|
api_key: Optional[str],
|
||||||
negative_prompt: Optional[str],
|
negative_prompt: Optional[str],
|
||||||
function_name: str
|
function_name: str,
|
||||||
|
style: Optional[str] = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Generate image using OpenAI DALL-E"""
|
"""Generate image using OpenAI DALL-E
|
||||||
|
|
||||||
|
Args:
|
||||||
|
style: For DALL-E 3 only. 'vivid' (hyper-real/dramatic) or 'natural' (more realistic).
|
||||||
|
Default is 'natural' for realistic photos.
|
||||||
|
"""
|
||||||
print(f"[AI][{function_name}] Provider: OpenAI")
|
print(f"[AI][{function_name}] Provider: OpenAI")
|
||||||
|
|
||||||
# Determine character limit based on model
|
# Determine character limit based on model
|
||||||
@@ -854,6 +861,15 @@ class AICore:
|
|||||||
'size': size
|
'size': size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# For DALL-E 3, add style parameter
|
||||||
|
# 'natural' = more realistic photos, 'vivid' = hyper-real/dramatic
|
||||||
|
if model == 'dall-e-3':
|
||||||
|
# Default to 'natural' for realistic images, but respect user preference
|
||||||
|
dalle_style = style if style in ['vivid', 'natural'] else 'natural'
|
||||||
|
data['style'] = dalle_style
|
||||||
|
data['quality'] = 'hd' # Always use HD quality for best results
|
||||||
|
print(f"[AI][{function_name}] DALL-E 3 style: {dalle_style}, quality: hd")
|
||||||
|
|
||||||
if negative_prompt:
|
if negative_prompt:
|
||||||
# Note: OpenAI DALL-E doesn't support negative_prompt in API, but we log it
|
# Note: OpenAI DALL-E doesn't support negative_prompt in API, but we log it
|
||||||
print(f"[AI][{function_name}] Note: Negative prompt provided but OpenAI DALL-E doesn't support it")
|
print(f"[AI][{function_name}] Note: Negative prompt provided but OpenAI DALL-E doesn't support it")
|
||||||
@@ -998,8 +1014,11 @@ class AICore:
|
|||||||
|
|
||||||
# Model-specific parameter configuration based on Runware documentation
|
# Model-specific parameter configuration based on Runware documentation
|
||||||
if runware_model.startswith('bria:'):
|
if runware_model.startswith('bria:'):
|
||||||
# Bria 3.2 (bria:10@1) - Commercial-ready, steps 4-10 (default 8)
|
# Bria 3.2 (bria:10@1) - Commercial-ready, steps 20-50 (API requires minimum 20)
|
||||||
inference_task['steps'] = 8
|
inference_task['steps'] = 20
|
||||||
|
# Enhanced negative prompt for Bria to prevent disfigured images
|
||||||
|
enhanced_negative = (negative_prompt or '') + ', disfigured, deformed, bad anatomy, wrong anatomy, extra limbs, missing limbs, floating limbs, mutated hands, extra fingers, missing fingers, fused fingers, poorly drawn hands, poorly drawn face, mutation, ugly, blurry, low quality, worst quality, jpeg artifacts, watermark, text, signature'
|
||||||
|
inference_task['negativePrompt'] = enhanced_negative
|
||||||
# Bria provider settings for enhanced quality
|
# Bria provider settings for enhanced quality
|
||||||
inference_task['providerSettings'] = {
|
inference_task['providerSettings'] = {
|
||||||
'bria': {
|
'bria': {
|
||||||
@@ -1009,12 +1028,15 @@ class AICore:
|
|||||||
'contentModeration': True
|
'contentModeration': True
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
print(f"[AI][{function_name}] Using Bria 3.2 config: steps=8, providerSettings enabled")
|
print(f"[AI][{function_name}] Using Bria 3.2 config: steps=20, enhanced negative prompt, providerSettings enabled")
|
||||||
elif runware_model.startswith('google:'):
|
elif runware_model.startswith('google:'):
|
||||||
# Nano Banana (google:4@2) - Premium quality, no explicit steps needed
|
# Nano Banana (google:4@2) - Premium quality
|
||||||
# Google models handle steps internally
|
# Google models use 'resolution' parameter INSTEAD of width/height
|
||||||
|
# Remove width/height and use resolution only
|
||||||
|
del inference_task['width']
|
||||||
|
del inference_task['height']
|
||||||
inference_task['resolution'] = '1k' # Use 1K tier for optimal speed/quality
|
inference_task['resolution'] = '1k' # Use 1K tier for optimal speed/quality
|
||||||
print(f"[AI][{function_name}] Using Nano Banana config: resolution=1k")
|
print(f"[AI][{function_name}] Using Nano Banana config: resolution=1k (no width/height)")
|
||||||
else:
|
else:
|
||||||
# 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
|
||||||
@@ -1036,7 +1058,29 @@ class AICore:
|
|||||||
print(f"[AI][{function_name}] Step 4: Received response in {request_duration:.2f}s (status={response.status_code})")
|
print(f"[AI][{function_name}] Step 4: Received response in {request_duration:.2f}s (status={response.status_code})")
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
error_msg = f"HTTP {response.status_code} error"
|
# Log the full error response for debugging
|
||||||
|
try:
|
||||||
|
error_body = response.json()
|
||||||
|
print(f"[AI][{function_name}][Error] Runware error response: {error_body}")
|
||||||
|
logger.error(f"[AI][{function_name}] Runware HTTP {response.status_code} error body: {error_body}")
|
||||||
|
|
||||||
|
# Extract specific error message from Runware response
|
||||||
|
error_detail = None
|
||||||
|
if isinstance(error_body, list):
|
||||||
|
for item in error_body:
|
||||||
|
if isinstance(item, dict) and 'errors' in item:
|
||||||
|
errors = item['errors']
|
||||||
|
if isinstance(errors, list) and len(errors) > 0:
|
||||||
|
err = errors[0]
|
||||||
|
error_detail = err.get('message') or err.get('error') or str(err)
|
||||||
|
break
|
||||||
|
elif isinstance(error_body, dict):
|
||||||
|
error_detail = error_body.get('message') or error_body.get('error') or str(error_body)
|
||||||
|
|
||||||
|
error_msg = f"HTTP {response.status_code}: {error_detail}" if error_detail else f"HTTP {response.status_code} error"
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"HTTP {response.status_code} error (could not parse response: {e})"
|
||||||
|
|
||||||
print(f"[AI][{function_name}][Error] {error_msg}")
|
print(f"[AI][{function_name}][Error] {error_msg}")
|
||||||
return {
|
return {
|
||||||
'url': None,
|
'url': None,
|
||||||
|
|||||||
@@ -218,17 +218,41 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
|||||||
image_type = config.get('image_type') or global_settings.image_style
|
image_type = config.get('image_type') or global_settings.image_style
|
||||||
image_format = config.get('image_format', 'webp')
|
image_format = config.get('image_format', 'webp')
|
||||||
|
|
||||||
|
# Style to prompt enhancement mapping
|
||||||
|
# These style descriptors are added to the image prompt for better results
|
||||||
|
STYLE_PROMPT_MAP = {
|
||||||
|
# Runware styles
|
||||||
|
'photorealistic': 'ultra realistic photography, natural lighting, real world look, photorealistic',
|
||||||
|
'illustration': 'digital illustration, clean lines, artistic style, modern illustration',
|
||||||
|
'3d_render': 'computer generated 3D render, modern polished 3D style, depth and dramatic lighting',
|
||||||
|
'minimal_flat': 'minimal flat design, simple shapes, flat colors, modern graphic design aesthetic',
|
||||||
|
'artistic': 'artistic painterly style, expressive brushstrokes, hand painted aesthetic',
|
||||||
|
'cartoon': 'cartoon stylized illustration, playful exaggerated forms, animated character style',
|
||||||
|
# DALL-E styles (mapped from OpenAI API style parameter)
|
||||||
|
'natural': 'natural realistic style',
|
||||||
|
'vivid': 'vivid dramatic hyper-realistic style',
|
||||||
|
# Legacy fallbacks
|
||||||
|
'realistic': 'ultra realistic photography, natural lighting, photorealistic',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the style description for prompt enhancement
|
||||||
|
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)
|
# Model-specific landscape sizes (square is always 1024x1024)
|
||||||
# Based on Runware documentation for optimal results per model
|
# For Runware models - based on Runware documentation for optimal results per model
|
||||||
|
# For OpenAI DALL-E 3 - uses 1792x1024 for landscape
|
||||||
MODEL_LANDSCAPE_SIZES = {
|
MODEL_LANDSCAPE_SIZES = {
|
||||||
'runware:97@1': '1280x768', # Hi Dream Full landscape
|
'runware:97@1': '1280x768', # Hi Dream Full landscape
|
||||||
'bria:10@1': '1344x768', # Bria 3.2 landscape (16:9)
|
'bria:10@1': '1344x768', # Bria 3.2 landscape (16:9)
|
||||||
'google:4@2': '1376x768', # Nano Banana 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'
|
DEFAULT_SQUARE_SIZE = '1024x1024'
|
||||||
|
|
||||||
# Get model-specific landscape size for featured images
|
# Get model-specific landscape size for featured images
|
||||||
model_landscape_size = MODEL_LANDSCAPE_SIZES.get(model, '1280x768')
|
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
|
||||||
@@ -398,7 +422,7 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
|||||||
# Calculate actual template length with placeholders filled
|
# Calculate actual template length with placeholders filled
|
||||||
# Format template with dummy values to measure actual length
|
# Format template with dummy values to measure actual length
|
||||||
template_with_dummies = image_prompt_template.format(
|
template_with_dummies = image_prompt_template.format(
|
||||||
image_type=image_type,
|
image_type=style_description, # Use actual style description length
|
||||||
post_title='X' * len(post_title), # Use same length as actual post_title
|
post_title='X' * len(post_title), # Use same length as actual post_title
|
||||||
image_prompt='' # Empty to measure template overhead
|
image_prompt='' # Empty to measure template overhead
|
||||||
)
|
)
|
||||||
@@ -425,7 +449,7 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
|||||||
image_prompt = image_prompt[:max_image_prompt_length - 3] + "..."
|
image_prompt = image_prompt[:max_image_prompt_length - 3] + "..."
|
||||||
|
|
||||||
formatted_prompt = image_prompt_template.format(
|
formatted_prompt = image_prompt_template.format(
|
||||||
image_type=image_type,
|
image_type=style_description, # Use full style description instead of raw value
|
||||||
post_title=post_title,
|
post_title=post_title,
|
||||||
image_prompt=image_prompt
|
image_prompt=image_prompt
|
||||||
)
|
)
|
||||||
@@ -510,6 +534,21 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
|||||||
else: # desktop or other (legacy)
|
else: # desktop or other (legacy)
|
||||||
image_size = in_article_square_size # Default to square
|
image_size = in_article_square_size # Default to square
|
||||||
|
|
||||||
|
# For DALL-E, convert image_type to style parameter
|
||||||
|
# image_type is from user settings (e.g., 'vivid', 'natural', 'realistic')
|
||||||
|
# DALL-E accepts 'vivid' or 'natural' - map accordingly
|
||||||
|
dalle_style = None
|
||||||
|
if provider == 'openai':
|
||||||
|
# Map image_type to DALL-E style
|
||||||
|
# 'natural' = more realistic photos (default)
|
||||||
|
# 'vivid' = hyper-real, dramatic images
|
||||||
|
if image_type in ['vivid']:
|
||||||
|
dalle_style = 'vivid'
|
||||||
|
else:
|
||||||
|
# Default to 'natural' for realistic photos
|
||||||
|
dalle_style = 'natural'
|
||||||
|
logger.info(f"[process_image_generation_queue] DALL-E style: {dalle_style} (from image_type: {image_type})")
|
||||||
|
|
||||||
result = ai_core.generate_image(
|
result = ai_core.generate_image(
|
||||||
prompt=formatted_prompt,
|
prompt=formatted_prompt,
|
||||||
provider=provider,
|
provider=provider,
|
||||||
@@ -517,7 +556,8 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
|||||||
size=image_size,
|
size=image_size,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
negative_prompt=negative_prompt,
|
negative_prompt=negative_prompt,
|
||||||
function_name='generate_images_from_prompts'
|
function_name='generate_images_from_prompts',
|
||||||
|
style=dalle_style
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update progress: Image generation complete (50%)
|
# Update progress: Image generation complete (50%)
|
||||||
|
|||||||
@@ -403,7 +403,7 @@ class GlobalIntegrationSettingsAdmin(Igny8ModelAdmin):
|
|||||||
"description": "Global Runware image generation configuration"
|
"description": "Global Runware image generation configuration"
|
||||||
}),
|
}),
|
||||||
("Universal Image Settings", {
|
("Universal Image Settings", {
|
||||||
"fields": ("image_quality", "image_style", "max_in_article_images", "desktop_image_size", "mobile_image_size"),
|
"fields": ("image_quality", "image_style", "max_in_article_images", "desktop_image_size"),
|
||||||
"description": "Image quality, style, and sizing settings that apply to ALL providers (DALL-E, Runware, etc.)"
|
"description": "Image quality, style, and sizing settings that apply to ALL providers (DALL-E, Runware, etc.)"
|
||||||
}),
|
}),
|
||||||
("Status", {
|
("Status", {
|
||||||
|
|||||||
@@ -198,14 +198,28 @@ class GlobalIntegrationSettings(models.Model):
|
|||||||
('hd', 'HD'),
|
('hd', 'HD'),
|
||||||
]
|
]
|
||||||
|
|
||||||
IMAGE_STYLE_CHOICES = [
|
# Image style choices with descriptions - used by both Runware and OpenAI
|
||||||
('vivid', 'Vivid'),
|
# Format: (value, label, description)
|
||||||
('natural', 'Natural'),
|
IMAGE_STYLE_OPTIONS = [
|
||||||
('realistic', 'Realistic'),
|
('photorealistic', 'Photorealistic', 'Ultra realistic photography style, natural lighting, real world look'),
|
||||||
('artistic', 'Artistic'),
|
('illustration', 'Illustration', 'Digital illustration, clean lines, artistic but not realistic'),
|
||||||
('cartoon', 'Cartoon'),
|
('3d_render', '3D Render', 'Computer generated 3D style, modern, polished, depth and lighting'),
|
||||||
|
('minimal_flat', 'Minimal / Flat Design', 'Simple shapes, flat colors, modern UI and graphic design look'),
|
||||||
|
('artistic', 'Artistic / Painterly', 'Expressive, painted or hand drawn aesthetic'),
|
||||||
|
('cartoon', 'Cartoon / Stylized', 'Playful, exaggerated forms, animated or mascot style'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Choices for Django model field (value, label only)
|
||||||
|
IMAGE_STYLE_CHOICES = [(opt[0], opt[1]) for opt in IMAGE_STYLE_OPTIONS]
|
||||||
|
|
||||||
|
# OpenAI DALL-E specific styles with descriptions (subset)
|
||||||
|
DALLE_STYLE_OPTIONS = [
|
||||||
|
('natural', 'Natural', 'More realistic, photographic style'),
|
||||||
|
('vivid', 'Vivid', 'Hyper-real, dramatic, artistic'),
|
||||||
|
]
|
||||||
|
|
||||||
|
DALLE_STYLE_CHOICES = [(opt[0], opt[1]) for opt in DALLE_STYLE_OPTIONS]
|
||||||
|
|
||||||
IMAGE_SERVICE_CHOICES = [
|
IMAGE_SERVICE_CHOICES = [
|
||||||
('openai', 'OpenAI DALL-E'),
|
('openai', 'OpenAI DALL-E'),
|
||||||
('runware', 'Runware'),
|
('runware', 'Runware'),
|
||||||
@@ -335,8 +349,8 @@ class GlobalIntegrationSettings(models.Model):
|
|||||||
help_text="Default image quality for all providers (accounts can override if plan allows)"
|
help_text="Default image quality for all providers (accounts can override if plan allows)"
|
||||||
)
|
)
|
||||||
image_style = models.CharField(
|
image_style = models.CharField(
|
||||||
max_length=20,
|
max_length=30,
|
||||||
default='realistic',
|
default='photorealistic',
|
||||||
choices=IMAGE_STYLE_CHOICES,
|
choices=IMAGE_STYLE_CHOICES,
|
||||||
help_text="Default image style for all providers (accounts can override if plan allows)"
|
help_text="Default image style for all providers (accounts can override if plan allows)"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -723,7 +723,7 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
|||||||
|
|
||||||
# Universal image settings (applies to all providers)
|
# Universal image settings (applies to all providers)
|
||||||
for key in ['image_type', 'image_quality', 'image_style', 'max_in_article_images', 'image_format',
|
for key in ['image_type', 'image_quality', 'image_style', 'max_in_article_images', 'image_format',
|
||||||
'desktop_enabled', 'mobile_enabled', 'featured_image_size', 'desktop_image_size']:
|
'desktop_enabled', 'featured_image_size', 'desktop_image_size']:
|
||||||
if key in config:
|
if key in config:
|
||||||
clean_config[key] = config[key]
|
clean_config[key] = config[key]
|
||||||
|
|
||||||
@@ -844,9 +844,17 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
|||||||
}
|
}
|
||||||
|
|
||||||
elif integration_type == 'image_generation':
|
elif integration_type == 'image_generation':
|
||||||
|
# Model-specific landscape sizes
|
||||||
|
MODEL_LANDSCAPE_SIZES = {
|
||||||
|
'runware:97@1': '1280x768',
|
||||||
|
'bria:10@1': '1344x768',
|
||||||
|
'google:4@2': '1376x768',
|
||||||
|
}
|
||||||
|
|
||||||
# Get default service and model based on global settings
|
# Get default service and model based on global settings
|
||||||
default_service = global_settings.default_image_service
|
default_service = global_settings.default_image_service
|
||||||
default_model = global_settings.dalle_model if default_service == 'openai' else global_settings.runware_model
|
default_model = global_settings.dalle_model if default_service == 'openai' else global_settings.runware_model
|
||||||
|
model_landscape_size = MODEL_LANDSCAPE_SIZES.get(default_model, '1280x768')
|
||||||
|
|
||||||
response_data = {
|
response_data = {
|
||||||
'id': 'image_generation',
|
'id': 'image_generation',
|
||||||
@@ -862,10 +870,8 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
|||||||
'max_in_article_images': global_settings.max_in_article_images,
|
'max_in_article_images': global_settings.max_in_article_images,
|
||||||
'image_format': 'webp',
|
'image_format': 'webp',
|
||||||
'desktop_enabled': True,
|
'desktop_enabled': True,
|
||||||
'mobile_enabled': True,
|
'featured_image_size': model_landscape_size, # Model-specific landscape
|
||||||
'featured_image_size': global_settings.dalle_size,
|
|
||||||
'desktop_image_size': global_settings.desktop_image_size,
|
'desktop_image_size': global_settings.desktop_image_size,
|
||||||
'mobile_image_size': global_settings.mobile_image_size,
|
|
||||||
'using_global': True,
|
'using_global': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -897,7 +903,7 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
|||||||
response_data['runwareModel'] = config['runwareModel']
|
response_data['runwareModel'] = config['runwareModel']
|
||||||
# Universal image settings
|
# Universal image settings
|
||||||
for key in ['image_type', 'image_quality', 'image_style', 'max_in_article_images', 'image_format',
|
for key in ['image_type', 'image_quality', 'image_style', 'max_in_article_images', 'image_format',
|
||||||
'desktop_enabled', 'mobile_enabled', 'featured_image_size', 'desktop_image_size']:
|
'desktop_enabled', 'featured_image_size', 'desktop_image_size']:
|
||||||
if key in config:
|
if key in config:
|
||||||
response_data[key] = config[key]
|
response_data[key] = config[key]
|
||||||
except IntegrationSettings.DoesNotExist:
|
except IntegrationSettings.DoesNotExist:
|
||||||
@@ -923,9 +929,18 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
|||||||
|
|
||||||
@action(detail=False, methods=['get'], url_path='image_generation', url_name='image_generation_settings')
|
@action(detail=False, methods=['get'], url_path='image_generation', url_name='image_generation_settings')
|
||||||
def get_image_generation_settings(self, request):
|
def get_image_generation_settings(self, request):
|
||||||
"""Get image generation settings for current account
|
"""Get image generation settings for current account.
|
||||||
Normal users fallback to system account (aws-admin) settings
|
|
||||||
|
Architecture:
|
||||||
|
1. If account has IntegrationSettings override -> use it (with GlobalIntegrationSettings as fallback for missing fields)
|
||||||
|
2. Otherwise -> use GlobalIntegrationSettings (platform-wide defaults)
|
||||||
|
|
||||||
|
Note: API keys are ALWAYS from GlobalIntegrationSettings (accounts cannot override API keys).
|
||||||
|
Account IntegrationSettings only store model/parameter overrides.
|
||||||
"""
|
"""
|
||||||
|
from .models import IntegrationSettings
|
||||||
|
from .global_settings_models import GlobalIntegrationSettings
|
||||||
|
|
||||||
account = getattr(request, 'account', None)
|
account = getattr(request, 'account', None)
|
||||||
|
|
||||||
if not account:
|
if not account:
|
||||||
@@ -933,89 +948,90 @@ class IntegrationSettingsViewSet(viewsets.ViewSet):
|
|||||||
user = getattr(request, 'user', None)
|
user = getattr(request, 'user', None)
|
||||||
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)
|
||||||
# Fallback to default account
|
|
||||||
if not account:
|
|
||||||
from igny8_core.auth.models import Account
|
|
||||||
try:
|
|
||||||
account = Account.objects.first()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not account:
|
# Get GlobalIntegrationSettings (platform defaults - always available)
|
||||||
return error_response(
|
global_settings = GlobalIntegrationSettings.get_instance()
|
||||||
error='Account not found',
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
# Model-specific landscape sizes (from GlobalIntegrationSettings)
|
||||||
request=request
|
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:
|
||||||
from .models import IntegrationSettings
|
# Check if account has specific overrides
|
||||||
from igny8_core.auth.models import Account
|
account_config = {}
|
||||||
|
if account:
|
||||||
# Try to get settings for user's account first
|
|
||||||
try:
|
|
||||||
integration = IntegrationSettings.objects.get(
|
|
||||||
account=account,
|
|
||||||
integration_type='image_generation',
|
|
||||||
is_active=True
|
|
||||||
)
|
|
||||||
logger.info(f"[get_image_generation_settings] Found settings for account {account.id}")
|
|
||||||
except IntegrationSettings.DoesNotExist:
|
|
||||||
# Fallback to system account (aws-admin) settings - normal users use centralized settings
|
|
||||||
logger.info(f"[get_image_generation_settings] No settings for account {account.id}, falling back to system account")
|
|
||||||
try:
|
try:
|
||||||
system_account = Account.objects.get(slug='aws-admin')
|
|
||||||
integration = IntegrationSettings.objects.get(
|
integration = IntegrationSettings.objects.get(
|
||||||
account=system_account,
|
account=account,
|
||||||
integration_type='image_generation',
|
integration_type='image_generation',
|
||||||
is_active=True
|
is_active=True
|
||||||
)
|
)
|
||||||
logger.info(f"[get_image_generation_settings] Using system account (aws-admin) settings")
|
account_config = integration.config or {}
|
||||||
except (Account.DoesNotExist, IntegrationSettings.DoesNotExist):
|
logger.info(f"[get_image_generation_settings] Found account {account.id} override: {list(account_config.keys())}")
|
||||||
logger.error("[get_image_generation_settings] No image generation settings found in aws-admin account")
|
except IntegrationSettings.DoesNotExist:
|
||||||
return error_response(
|
logger.info(f"[get_image_generation_settings] No override for account {account.id if account else 'None'}, using GlobalIntegrationSettings")
|
||||||
error='Image generation settings not configured in aws-admin account',
|
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
request=request
|
|
||||||
)
|
|
||||||
|
|
||||||
config = integration.config or {}
|
# Build response using account overrides with global fallbacks
|
||||||
|
provider = account_config.get('provider') or global_settings.default_image_service
|
||||||
|
|
||||||
# Debug: Log what's actually in the config
|
# Get model based on provider
|
||||||
logger.info(f"[get_image_generation_settings] Full config: {config}")
|
if provider == 'runware':
|
||||||
logger.info(f"[get_image_generation_settings] Config keys: {list(config.keys())}")
|
model = account_config.get('model') or account_config.get('imageModel') or global_settings.runware_model
|
||||||
logger.info(f"[get_image_generation_settings] model field: {config.get('model')}")
|
else:
|
||||||
logger.info(f"[get_image_generation_settings] imageModel field: {config.get('imageModel')}")
|
model = account_config.get('model') or account_config.get('imageModel') or global_settings.dalle_model
|
||||||
|
|
||||||
# Get model - try 'model' first, then 'imageModel' as fallback
|
# Get model-specific landscape size
|
||||||
model = config.get('model') or config.get('imageModel') or 'dall-e-3'
|
model_landscape_size = MODEL_LANDSCAPE_SIZES.get(model, '1280x768')
|
||||||
|
default_featured_size = model_landscape_size if provider == 'runware' else '1792x1024'
|
||||||
|
|
||||||
# Set defaults for image sizes if not present
|
# Get image style with provider-specific defaults
|
||||||
provider = config.get('provider', 'openai')
|
image_style = account_config.get('image_type') or global_settings.image_style
|
||||||
default_featured_size = '1280x832' if provider == 'runware' else '1024x1024'
|
|
||||||
|
# Style options from GlobalIntegrationSettings model - loaded dynamically
|
||||||
|
# Runware: Uses all styles with prompt enhancement
|
||||||
|
# OpenAI DALL-E: Only supports 'natural' or 'vivid'
|
||||||
|
if provider == 'openai':
|
||||||
|
# Get DALL-E styles from model definition
|
||||||
|
available_styles = [
|
||||||
|
{'value': opt[0], 'label': opt[1], 'description': opt[2]}
|
||||||
|
for opt in GlobalIntegrationSettings.DALLE_STYLE_OPTIONS
|
||||||
|
]
|
||||||
|
# Map stored style to DALL-E compatible
|
||||||
|
if image_style not in ['vivid', 'natural']:
|
||||||
|
image_style = 'natural' # Default to natural for photorealistic
|
||||||
|
else:
|
||||||
|
# Get Runware styles from model definition
|
||||||
|
available_styles = [
|
||||||
|
{'value': opt[0], 'label': opt[1], 'description': opt[2]}
|
||||||
|
for opt in GlobalIntegrationSettings.IMAGE_STYLE_OPTIONS
|
||||||
|
]
|
||||||
|
# Default to photorealistic for Runware if not set
|
||||||
|
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}")
|
||||||
|
|
||||||
return success_response(
|
return success_response(
|
||||||
data={
|
data={
|
||||||
'config': {
|
'config': {
|
||||||
'provider': config.get('provider', 'openai'),
|
'provider': provider,
|
||||||
'model': model,
|
'model': model,
|
||||||
'image_type': config.get('image_type', 'realistic'),
|
'image_type': image_style,
|
||||||
'max_in_article_images': config.get('max_in_article_images'),
|
'available_styles': available_styles, # Loaded from GlobalIntegrationSettings model
|
||||||
'image_format': config.get('image_format', 'webp'),
|
'max_in_article_images': account_config.get('max_in_article_images') or global_settings.max_in_article_images,
|
||||||
'desktop_enabled': config.get('desktop_enabled', True),
|
'image_format': account_config.get('image_format', 'webp'),
|
||||||
'mobile_enabled': config.get('mobile_enabled', True),
|
'desktop_enabled': account_config.get('desktop_enabled', True),
|
||||||
'featured_image_size': config.get('featured_image_size', default_featured_size),
|
'featured_image_size': account_config.get('featured_image_size') or default_featured_size,
|
||||||
'desktop_image_size': config.get('desktop_image_size', '1024x1024'),
|
'desktop_image_size': account_config.get('desktop_image_size') or global_settings.desktop_image_size,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
request=request
|
request=request
|
||||||
)
|
)
|
||||||
except IntegrationSettings.DoesNotExist:
|
|
||||||
return error_response(
|
|
||||||
error='Image generation settings not configured',
|
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
|
||||||
request=request
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[get_image_generation_settings] Error: {str(e)}", exc_info=True)
|
logger.error(f"[get_image_generation_settings] Error: {str(e)}", exc_info=True)
|
||||||
return error_response(
|
return error_response(
|
||||||
|
|||||||
@@ -339,5 +339,5 @@ This plan is ready for execution. The key technical findings are:
|
|||||||
| Model | AIR ID | Square | Landscape | Steps |
|
| Model | AIR ID | Square | Landscape | Steps |
|
||||||
|-------|--------|--------|-----------|-------|
|
|-------|--------|--------|-----------|-------|
|
||||||
| Hi Dream Full | `runware:97@1` | 1024×1024 | 1280×768 | 20 |
|
| Hi Dream Full | `runware:97@1` | 1024×1024 | 1280×768 | 20 |
|
||||||
| Bria 3.2 | `bria:10@1` | 1024×1024 | 1344×768 | 8 |
|
| Bria 3.2 | `bria:10@1` | 1024×1024 | 1344×768 | 20 (min 20, max 50) |
|
||||||
| Nano Banana | `google:4@2` | 1024×1024 | 1376×768 | Auto |
|
| Nano Banana | `google:4@2` | 1024×1024 | 1376×768 | Auto |
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ export const Modal: React.FC<ModalProps> = ({
|
|||||||
|
|
||||||
const contentClasses = isFullscreen
|
const contentClasses = isFullscreen
|
||||||
? "w-full h-full"
|
? "w-full h-full"
|
||||||
: "relative w-full max-w-lg mx-4 rounded-3xl bg-white dark:bg-gray-900 shadow-xl";
|
: className
|
||||||
|
? `relative mx-4 rounded-3xl bg-white dark:bg-gray-900 shadow-xl ${className}`
|
||||||
|
: "relative w-full max-w-lg mx-4 rounded-3xl bg-white dark:bg-gray-900 shadow-xl";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex items-center justify-center overflow-y-auto modal z-99999">
|
<div className="fixed inset-0 flex items-center justify-center overflow-y-auto modal z-99999">
|
||||||
@@ -63,7 +65,7 @@ export const Modal: React.FC<ModalProps> = ({
|
|||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
ref={modalRef}
|
ref={modalRef}
|
||||||
className={`${contentClasses} ${className}`}
|
className={contentClasses}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
|
|||||||
@@ -77,74 +77,94 @@ export default function SiteSettings() {
|
|||||||
const [contentGenerationSaving, setContentGenerationSaving] = useState(false);
|
const [contentGenerationSaving, setContentGenerationSaving] = useState(false);
|
||||||
|
|
||||||
// Image Settings state
|
// Image Settings state
|
||||||
const [imageQuality, setImageQuality] = useState<'standard' | 'premium' | 'best'>('premium');
|
const [imageQuality, setImageQuality] = useState<'basic' | 'quality' | 'premium'>('basic');
|
||||||
const [imageSettings, setImageSettings] = useState({
|
const [imageSettings, setImageSettings] = useState({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
service: 'openai' as 'openai' | 'runware',
|
service: 'runware' as 'runware',
|
||||||
provider: 'openai',
|
provider: 'runware',
|
||||||
model: 'dall-e-3',
|
model: 'runware:97@1',
|
||||||
image_type: 'realistic' as 'realistic' | 'artistic' | 'cartoon',
|
image_type: 'photorealistic' as string,
|
||||||
max_in_article_images: 2,
|
max_in_article_images: 2,
|
||||||
image_format: 'webp' as 'webp' | 'jpg' | 'png',
|
image_format: 'webp' as 'webp' | 'jpg' | 'png',
|
||||||
featured_image_size: '1024x1024',
|
|
||||||
});
|
});
|
||||||
const [imageSettingsLoading, setImageSettingsLoading] = useState(false);
|
const [imageSettingsLoading, setImageSettingsLoading] = useState(false);
|
||||||
const [imageSettingsSaving, setImageSettingsSaving] = useState(false);
|
const [imageSettingsSaving, setImageSettingsSaving] = useState(false);
|
||||||
|
|
||||||
// Image quality to config mapping
|
// Current image provider (from GlobalIntegrationSettings)
|
||||||
// Updated to use new Runware models via API
|
const [imageProvider, setImageProvider] = useState<'runware' | 'openai'>('runware');
|
||||||
const QUALITY_TO_CONFIG: Record<string, { service: 'openai' | 'runware'; model: string }> = {
|
|
||||||
standard: { service: 'openai', model: 'dall-e-2' },
|
// Available style options loaded from backend (dynamic based on provider)
|
||||||
premium: { service: 'openai', model: 'dall-e-3' },
|
const [availableStyles, setAvailableStyles] = useState<Array<{value: string; label: string; description?: string}>>([
|
||||||
best: { service: 'runware', model: 'runware:97@1' }, // Uses model-specific landscape size
|
{ value: 'photorealistic', label: 'Photorealistic', description: 'Ultra realistic photography style' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Image quality to config mapping - Runware models only with model-specific sizes
|
||||||
|
const QUALITY_TO_CONFIG: Record<string, { service: 'runware'; model: string }> = {
|
||||||
|
basic: { service: 'runware', model: 'runware:97@1' }, // 6 credits/image
|
||||||
|
quality: { service: 'runware', model: 'bria:10@1' }, // 10 credits/image
|
||||||
|
premium: { service: 'runware', model: 'google:4@2' }, // 15 credits/image
|
||||||
};
|
};
|
||||||
|
|
||||||
// Runware model choices with descriptions
|
// Runware model choices with credits per image (from backend AIModelConfig)
|
||||||
const RUNWARE_MODEL_CHOICES = [
|
const RUNWARE_MODEL_CHOICES = [
|
||||||
{ value: 'runware:97@1', label: 'Hi Dream Full - Basic', description: 'Fast & affordable' },
|
{ value: 'runware:97@1', label: 'Basic (6 credits/image)', credits: 6 },
|
||||||
{ value: 'bria:10@1', label: 'Bria 3.2 - Quality', description: 'Commercial-safe, licensed data' },
|
{ value: 'bria:10@1', label: 'Quality (10 credits/image)', credits: 10 },
|
||||||
{ value: 'google:4@2', label: 'Nano Banana - Premium', description: 'Best quality, text rendering' },
|
{ value: 'google:4@2', label: 'Premium (15 credits/image)', credits: 15 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const getQualityFromConfig = (service?: string, model?: string): 'standard' | 'premium' | 'best' => {
|
// OpenAI DALL-E model choices
|
||||||
if (service === 'runware') return 'best';
|
const DALLE_MODEL_CHOICES = [
|
||||||
if (model === 'dall-e-3') return 'premium';
|
{ value: 'dall-e-3', label: 'DALL-E 3 - HD Quality', credits: 40 },
|
||||||
return 'standard';
|
];
|
||||||
|
|
||||||
|
// Model-specific style options (image_type attribute compatible with each model)
|
||||||
|
const getModelStyleOptions = (model: string) => {
|
||||||
|
// Bria supports medium parameter (photography/art)
|
||||||
|
if (model === 'bria:10@1') {
|
||||||
|
return [
|
||||||
|
{ value: 'photography', label: 'Photography' },
|
||||||
|
{ value: 'art', label: 'Artistic' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// All models support these basic styles via prompt modification
|
||||||
|
return [
|
||||||
|
{ value: 'realistic', label: 'Realistic' },
|
||||||
|
{ value: 'artistic', label: 'Artistic' },
|
||||||
|
{ value: 'cartoon', label: 'Cartoon' },
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getImageSizes = (provider: string, model: string) => {
|
const getQualityFromConfig = (service?: string, model?: string): 'basic' | 'quality' | 'premium' => {
|
||||||
if (provider === 'runware') {
|
if (model === 'google:4@2') return 'premium';
|
||||||
// Model-specific sizes - featured uses landscape, in-article alternates
|
if (model === 'bria:10@1') return 'quality';
|
||||||
// Sizes shown are for featured image (landscape)
|
return 'basic'; // Default to basic (runware:97@1)
|
||||||
return [
|
};
|
||||||
{ value: '1280x768', label: '1280×768 (Landscape)' },
|
|
||||||
{ value: '1024x1024', label: '1024×1024 (Square)' },
|
// Model-specific landscape sizes (used for featured image)
|
||||||
];
|
const MODEL_LANDSCAPE_SIZES: Record<string, string> = {
|
||||||
} else if (provider === 'openai') {
|
'runware:97@1': '1280x768', // Hi Dream Full
|
||||||
if (model === 'dall-e-2') {
|
'bria:10@1': '1344x768', // Bria 3.2
|
||||||
return [
|
'google:4@2': '1376x768', // Nano Banana
|
||||||
{ value: '256x256', label: '256×256 pixels' },
|
'dall-e-3': '1792x1024', // DALL-E 3
|
||||||
{ value: '512x512', label: '512×512 pixels' },
|
'dall-e-2': '1024x1024', // DALL-E 2 (square only)
|
||||||
{ value: '1024x1024', label: '1024×1024 pixels' },
|
};
|
||||||
];
|
|
||||||
} else if (model === 'dall-e-3') {
|
const getLandscapeSizeForModel = (model: string): string => {
|
||||||
return [
|
return MODEL_LANDSCAPE_SIZES[model] || (imageProvider === 'openai' ? '1792x1024' : '1280x768');
|
||||||
{ value: '1024x1024', label: '1024×1024 pixels' },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [{ value: '1024x1024', label: '1024×1024 pixels' }];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCurrentImageConfig = useCallback(() => {
|
const getCurrentImageConfig = useCallback(() => {
|
||||||
|
// For OpenAI provider, return the DALL-E model directly
|
||||||
|
if (imageProvider === 'openai') {
|
||||||
|
return { service: 'openai', model: imageSettings.model || 'dall-e-3' };
|
||||||
|
}
|
||||||
|
// For Runware provider, use quality-based config
|
||||||
const config = QUALITY_TO_CONFIG[imageQuality];
|
const config = QUALITY_TO_CONFIG[imageQuality];
|
||||||
return { service: config.service, model: config.model };
|
return { service: config.service, model: config.model };
|
||||||
}, [imageQuality]);
|
}, [imageQuality, imageProvider, imageSettings.model]);
|
||||||
|
|
||||||
const availableImageSizes = getImageSizes(
|
// Get the current model's landscape size for display
|
||||||
getCurrentImageConfig().service,
|
const currentLandscapeSize = getLandscapeSizeForModel(imageSettings.model || getCurrentImageConfig().model);
|
||||||
getCurrentImageConfig().model
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sectors selection state
|
// Sectors selection state
|
||||||
const [industries, setIndustries] = useState<Industry[]>([]);
|
const [industries, setIndustries] = useState<Industry[]>([]);
|
||||||
@@ -228,31 +248,15 @@ export default function SiteSettings() {
|
|||||||
}
|
}
|
||||||
}, [activeTab, siteId]);
|
}, [activeTab, siteId]);
|
||||||
|
|
||||||
// Update image sizes when quality changes
|
// Update image config when quality changes (all Runware models)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const config = getCurrentImageConfig();
|
const config = getCurrentImageConfig();
|
||||||
const sizes = getImageSizes(config.service, config.model);
|
setImageSettings(prev => ({
|
||||||
const defaultSize = sizes.length > 0 ? sizes[0].value : '1024x1024';
|
...prev,
|
||||||
|
service: config.service,
|
||||||
const validSizes = sizes.map(s => s.value);
|
provider: config.service,
|
||||||
const needsFeaturedUpdate = !validSizes.includes(imageSettings.featured_image_size);
|
model: config.model,
|
||||||
|
}));
|
||||||
if (needsFeaturedUpdate) {
|
|
||||||
setImageSettings(prev => ({
|
|
||||||
...prev,
|
|
||||||
service: config.service,
|
|
||||||
provider: config.service,
|
|
||||||
model: config.model,
|
|
||||||
featured_image_size: needsFeaturedUpdate ? defaultSize : prev.featured_image_size,
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
setImageSettings(prev => ({
|
|
||||||
...prev,
|
|
||||||
service: config.service,
|
|
||||||
provider: config.service,
|
|
||||||
model: config.model,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}, [imageQuality, getCurrentImageConfig]);
|
}, [imageQuality, getCurrentImageConfig]);
|
||||||
|
|
||||||
// Load sites for selector
|
// Load sites for selector
|
||||||
@@ -429,21 +433,69 @@ export default function SiteSettings() {
|
|||||||
const loadImageSettings = async () => {
|
const loadImageSettings = async () => {
|
||||||
try {
|
try {
|
||||||
setImageSettingsLoading(true);
|
setImageSettingsLoading(true);
|
||||||
const imageData = await fetchAPI('/v1/system/settings/integrations/image_generation/');
|
const response = await fetchAPI('/v1/system/settings/integrations/image_generation/');
|
||||||
if (imageData) {
|
// API returns { data: { config: {...} } } structure - try multiple paths
|
||||||
const quality = getQualityFromConfig(imageData.service || imageData.provider, imageData.model);
|
const config = response?.data?.config || response?.config || response?.data || response || {};
|
||||||
|
|
||||||
|
console.log('[loadImageSettings] Raw response:', response);
|
||||||
|
console.log('[loadImageSettings] Extracted config:', config);
|
||||||
|
|
||||||
|
if (config) {
|
||||||
|
// Get provider from config (GlobalIntegrationSettings default_image_service)
|
||||||
|
const provider = config.provider || config.service || 'runware';
|
||||||
|
setImageProvider(provider as 'runware' | 'openai');
|
||||||
|
|
||||||
|
// Load available styles from backend (provider-specific)
|
||||||
|
// If not available from API, use default fallbacks
|
||||||
|
if (config.available_styles && Array.isArray(config.available_styles) && config.available_styles.length > 0) {
|
||||||
|
setAvailableStyles(config.available_styles);
|
||||||
|
console.log('[loadImageSettings] Loaded styles from API:', config.available_styles);
|
||||||
|
} else {
|
||||||
|
// Fallback styles if API doesn't return them (backward compatibility)
|
||||||
|
const fallbackStyles = provider === 'openai'
|
||||||
|
? [
|
||||||
|
{ value: 'natural', label: 'Natural', description: 'More realistic, photographic style' },
|
||||||
|
{ value: 'vivid', label: 'Vivid', description: 'Hyper-real, dramatic, artistic' },
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ value: 'photorealistic', label: 'Photorealistic', description: 'Ultra realistic photography style, natural lighting, real world look' },
|
||||||
|
{ value: 'illustration', label: 'Illustration', description: 'Digital illustration, clean lines, artistic but not realistic' },
|
||||||
|
{ value: '3d_render', label: '3D Render', description: 'Computer generated 3D style, modern, polished, depth and lighting' },
|
||||||
|
{ value: 'minimal_flat', label: 'Minimal / Flat Design', description: 'Simple shapes, flat colors, modern UI and graphic design look' },
|
||||||
|
{ value: 'artistic', label: 'Artistic / Painterly', description: 'Expressive, painted or hand drawn aesthetic' },
|
||||||
|
{ value: 'cartoon', label: 'Cartoon / Stylized', description: 'Playful, exaggerated forms, animated or mascot style' },
|
||||||
|
];
|
||||||
|
setAvailableStyles(fallbackStyles);
|
||||||
|
console.log('[loadImageSettings] Using fallback styles for provider:', provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get model based on provider
|
||||||
|
let loadedModel = config.model || 'runware:97@1';
|
||||||
|
if (provider === 'openai') {
|
||||||
|
loadedModel = config.model || 'dall-e-3';
|
||||||
|
} else {
|
||||||
|
loadedModel = config.model || config.runwareModel || 'runware:97@1';
|
||||||
|
}
|
||||||
|
|
||||||
|
const quality = getQualityFromConfig(provider, loadedModel);
|
||||||
setImageQuality(quality);
|
setImageQuality(quality);
|
||||||
|
|
||||||
|
// Get image_type from config - map old values to new ones
|
||||||
|
let imageType = config.image_type || (provider === 'openai' ? 'natural' : 'photorealistic');
|
||||||
|
// Map old style values to new ones
|
||||||
|
if (imageType === 'realistic') imageType = provider === 'openai' ? 'natural' : 'photorealistic';
|
||||||
|
|
||||||
setImageSettings({
|
setImageSettings({
|
||||||
enabled: imageData.enabled !== false,
|
enabled: config.enabled !== false,
|
||||||
service: imageData.service || imageData.provider || 'openai',
|
service: provider,
|
||||||
provider: imageData.provider || imageData.service || 'openai',
|
provider: provider,
|
||||||
model: imageData.model || 'dall-e-3',
|
model: loadedModel,
|
||||||
image_type: imageData.image_type || 'realistic',
|
image_type: imageType,
|
||||||
max_in_article_images: imageData.max_in_article_images || 2,
|
max_in_article_images: config.max_in_article_images || 4,
|
||||||
image_format: imageData.image_format || 'webp',
|
image_format: config.image_format || 'webp',
|
||||||
featured_image_size: imageData.featured_image_size || '1024x1024',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[loadImageSettings] Final settings - Provider:', provider, 'Model:', loadedModel, 'Quality:', quality, 'ImageType:', imageType);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error loading image settings:', error);
|
console.error('Error loading image settings:', error);
|
||||||
@@ -456,22 +508,28 @@ export default function SiteSettings() {
|
|||||||
try {
|
try {
|
||||||
setImageSettingsSaving(true);
|
setImageSettingsSaving(true);
|
||||||
const config = getCurrentImageConfig();
|
const config = getCurrentImageConfig();
|
||||||
|
const landscapeSize = getLandscapeSizeForModel(imageSettings.model);
|
||||||
const configToSave = {
|
const configToSave = {
|
||||||
enabled: imageSettings.enabled,
|
enabled: imageSettings.enabled,
|
||||||
service: config.service,
|
service: imageProvider,
|
||||||
provider: config.service,
|
provider: imageProvider,
|
||||||
model: config.model,
|
model: imageSettings.model,
|
||||||
runwareModel: config.service === 'runware' ? config.model : undefined,
|
runwareModel: imageProvider === 'runware' ? imageSettings.model : undefined,
|
||||||
image_type: imageSettings.image_type,
|
image_type: imageSettings.image_type,
|
||||||
max_in_article_images: imageSettings.max_in_article_images,
|
max_in_article_images: imageSettings.max_in_article_images,
|
||||||
image_format: imageSettings.image_format,
|
image_format: imageSettings.image_format,
|
||||||
featured_image_size: imageSettings.featured_image_size,
|
featured_image_size: landscapeSize, // Auto-determined by model
|
||||||
};
|
};
|
||||||
|
|
||||||
await fetchAPI('/v1/system/settings/integrations/image_generation/save/', {
|
console.log('[saveImageSettings] Saving config:', configToSave);
|
||||||
|
|
||||||
|
// URL pattern is /v1/system/settings/integrations/<pk>/save/
|
||||||
|
const result = await fetchAPI('/v1/system/settings/integrations/image_generation/save/', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(configToSave),
|
body: JSON.stringify(configToSave),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[saveImageSettings] Save result:', result);
|
||||||
toast.success('Image settings saved successfully');
|
toast.success('Image settings saved successfully');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error saving image settings:', error);
|
console.error('Error saving image settings:', error);
|
||||||
@@ -982,55 +1040,111 @@ export default function SiteSettings() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Row 1: Image Quality & Style */}
|
{/* Provider Info Banner - Show which provider is configured */}
|
||||||
|
<div className={`p-3 rounded-lg flex items-center gap-3 ${
|
||||||
|
imageProvider === 'openai'
|
||||||
|
? 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800'
|
||||||
|
: 'bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800'
|
||||||
|
}`}>
|
||||||
|
<div className={`p-2 rounded-lg ${
|
||||||
|
imageProvider === 'openai'
|
||||||
|
? 'bg-green-100 dark:bg-green-800'
|
||||||
|
: 'bg-purple-100 dark:bg-purple-800'
|
||||||
|
}`}>
|
||||||
|
<ImageIcon className={`w-4 h-4 ${
|
||||||
|
imageProvider === 'openai'
|
||||||
|
? 'text-green-600 dark:text-green-400'
|
||||||
|
: 'text-purple-600 dark:text-purple-400'
|
||||||
|
}`} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className={`text-sm font-medium ${
|
||||||
|
imageProvider === 'openai'
|
||||||
|
? 'text-green-700 dark:text-green-300'
|
||||||
|
: 'text-purple-700 dark:text-purple-300'
|
||||||
|
}`}>
|
||||||
|
{imageProvider === 'openai' ? 'OpenAI DALL-E' : 'Runware'} - Image Provider
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{imageProvider === 'openai'
|
||||||
|
? 'Using DALL-E 3 for high-quality AI images'
|
||||||
|
: 'Choose from multiple AI models with different quality tiers'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Row 1: Image Model/Quality & Style */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<Label className="mb-2">Image Quality</Label>
|
<Label className="mb-2">{imageProvider === 'openai' ? 'Image Model' : 'Image Quality'}</Label>
|
||||||
<SelectDropdown
|
{imageProvider === 'openai' ? (
|
||||||
options={[
|
<>
|
||||||
{ value: 'standard', label: 'Standard - Fast & economical (DALL·E 2)' },
|
<SelectDropdown
|
||||||
{ value: 'premium', label: 'Premium - High quality (DALL·E 3)' },
|
options={DALLE_MODEL_CHOICES.map(m => ({
|
||||||
{ value: 'best', label: 'Best - Highest quality (Runware)' },
|
value: m.value,
|
||||||
]}
|
label: m.label,
|
||||||
value={imageQuality}
|
}))}
|
||||||
onChange={(value) => setImageQuality(value as 'standard' | 'premium' | 'best')}
|
value={imageSettings.model}
|
||||||
className="w-full"
|
onChange={(value) => setImageSettings({ ...imageSettings, model: value })}
|
||||||
/>
|
className="w-full"
|
||||||
<p className="text-xs text-gray-500 mt-1">Higher quality produces better images</p>
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">DALL-E 3 generates high-quality images (40 credits/image)</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<SelectDropdown
|
||||||
|
options={RUNWARE_MODEL_CHOICES.map(m => ({
|
||||||
|
value: m.value === 'runware:97@1' ? 'basic' : m.value === 'bria:10@1' ? 'quality' : 'premium',
|
||||||
|
label: m.label,
|
||||||
|
}))}
|
||||||
|
value={imageQuality}
|
||||||
|
onChange={(value) => {
|
||||||
|
setImageQuality(value as 'basic' | 'quality' | 'premium');
|
||||||
|
const model = value === 'basic' ? 'runware:97@1' : value === 'quality' ? 'bria:10@1' : 'google:4@2';
|
||||||
|
setImageSettings({ ...imageSettings, model });
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">Higher credit cost = better image quality</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label className="mb-2">Image Style</Label>
|
<Label className="mb-2">Image Style</Label>
|
||||||
<SelectDropdown
|
<SelectDropdown
|
||||||
options={[
|
options={availableStyles.map(style => ({
|
||||||
{ value: 'realistic', label: 'Realistic' },
|
value: style.value,
|
||||||
{ value: 'artistic', label: 'Artistic' },
|
label: style.label,
|
||||||
{ value: 'cartoon', label: 'Cartoon' },
|
}))}
|
||||||
]}
|
|
||||||
value={imageSettings.image_type}
|
value={imageSettings.image_type}
|
||||||
onChange={(value) => setImageSettings({ ...imageSettings, image_type: value as any })}
|
onChange={(value) => setImageSettings({ ...imageSettings, image_type: value })}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">Choose the visual style that matches your brand</p>
|
{/* Show description of selected style */}
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
{availableStyles.find(s => s.value === imageSettings.image_type)?.description || 'Choose the visual style that matches your brand'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Row 2: Featured Image Size */}
|
{/* Row 2: Featured Image Info Card (compact, not full width) */}
|
||||||
<div>
|
<div className="max-w-md">
|
||||||
<Label className="mb-2">Featured Image</Label>
|
<Label className="mb-2">Featured Image</Label>
|
||||||
<div className="p-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-gradient-to-r from-purple-500 to-brand-500 text-white">
|
<div className="p-4 rounded-lg bg-gradient-to-r from-brand-500 to-purple-500 text-white">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="font-medium">Featured Image Size</div>
|
<div className="w-10 h-10 rounded-lg bg-white/20 flex items-center justify-center">
|
||||||
<div className="text-xs bg-white/20 px-2 py-1 rounded">Landscape (Model-specific)</div>
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">Always Landscape</div>
|
||||||
|
<div className="text-sm text-white/80">{currentLandscapeSize} pixels</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SelectDropdown
|
<p className="text-xs text-white/70 mt-3">
|
||||||
options={availableImageSizes}
|
Featured image size is automatically set based on your selected model
|
||||||
value={imageSettings.featured_image_size}
|
|
||||||
onChange={(value) => setImageSettings({ ...imageSettings, featured_image_size: value })}
|
|
||||||
className="w-full [&_.igny8-select-styled]:bg-white/10 [&_.igny8-select-styled]:border-white/20 [&_.igny8-select-styled]:text-white"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-white/70 mt-2">
|
|
||||||
In-article images alternate: Square (1024×1024) → Landscape → Square → Landscape
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1051,7 +1165,7 @@ export default function SiteSettings() {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||||
Images 1 & 3: Square | Images 2 & 4: Landscape
|
Images 1 & 3: Square (1024×1024) | Images 2 & 4: Landscape
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -326,12 +326,14 @@ export default function Images() {
|
|||||||
.sort((a, b) => (a.position || 0) - (b.position || 0));
|
.sort((a, b) => (a.position || 0) - (b.position || 0));
|
||||||
|
|
||||||
pendingInArticle.forEach((img, idx) => {
|
pendingInArticle.forEach((img, idx) => {
|
||||||
|
// Position is 0-indexed in backend, but labels should be 1-indexed for users
|
||||||
|
const displayPosition = (img.position ?? idx) + 1;
|
||||||
queue.push({
|
queue.push({
|
||||||
imageId: img.id || null,
|
imageId: img.id || null,
|
||||||
index: queueIndex++,
|
index: queueIndex++,
|
||||||
label: `In-Article Image ${img.position || idx + 1}`,
|
label: `In-Article Image ${displayPosition}`,
|
||||||
type: 'in_article',
|
type: 'in_article',
|
||||||
position: img.position || idx + 1,
|
position: img.position ?? idx,
|
||||||
contentTitle: contentImages.content_title || `Content #${contentId}`,
|
contentTitle: contentImages.content_title || `Content #${contentId}`,
|
||||||
prompt: img.prompt || undefined,
|
prompt: img.prompt || undefined,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
|
|||||||
Reference in New Issue
Block a user