Migrations
This commit is contained in:
@@ -485,21 +485,32 @@ class AICore:
|
|||||||
"""Generate image using OpenAI DALL-E"""
|
"""Generate image using OpenAI DALL-E"""
|
||||||
print(f"[AI][{function_name}] Provider: OpenAI")
|
print(f"[AI][{function_name}] Provider: OpenAI")
|
||||||
|
|
||||||
# CRITICAL: Truncate prompt to OpenAI's 1000 character limit BEFORE any processing
|
# Determine character limit based on model
|
||||||
if len(prompt) > 1000:
|
# DALL-E 2: 1000 chars, DALL-E 3: 4000 chars
|
||||||
print(f"[AI][{function_name}][Warning] Prompt too long ({len(prompt)} chars), truncating to 1000")
|
model = model or 'dall-e-3'
|
||||||
|
if model == 'dall-e-2':
|
||||||
|
max_length = 1000
|
||||||
|
elif model == 'dall-e-3':
|
||||||
|
max_length = 4000
|
||||||
|
else:
|
||||||
|
# Default to 1000 for safety
|
||||||
|
max_length = 1000
|
||||||
|
|
||||||
|
# CRITICAL: Truncate prompt to model-specific limit BEFORE any processing
|
||||||
|
if len(prompt) > max_length:
|
||||||
|
print(f"[AI][{function_name}][Warning] Prompt too long ({len(prompt)} chars), truncating to {max_length} for {model}")
|
||||||
# Try word-aware truncation, but fallback to hard truncate if no space found
|
# Try word-aware truncation, but fallback to hard truncate if no space found
|
||||||
truncated = prompt[:997]
|
truncated = prompt[:max_length - 3]
|
||||||
last_space = truncated.rfind(' ')
|
last_space = truncated.rfind(' ')
|
||||||
if last_space > 900: # Only use word-aware if we have a reasonable space
|
if last_space > max_length * 0.9: # Only use word-aware if we have a reasonable space
|
||||||
prompt = truncated[:last_space] + "..."
|
prompt = truncated[:last_space] + "..."
|
||||||
else:
|
else:
|
||||||
prompt = prompt[:1000] # Hard truncate if no good space found
|
prompt = prompt[:max_length] # Hard truncate if no good space found
|
||||||
print(f"[AI][{function_name}] Truncated prompt length: {len(prompt)}")
|
print(f"[AI][{function_name}] Truncated prompt length: {len(prompt)}")
|
||||||
# Final safety check
|
# Final safety check
|
||||||
if len(prompt) > 1000:
|
if len(prompt) > max_length:
|
||||||
prompt = prompt[:1000]
|
prompt = prompt[:max_length]
|
||||||
print(f"[AI][{function_name}][Error] Had to hard truncate to exactly 1000 chars")
|
print(f"[AI][{function_name}][Error] Had to hard truncate to exactly {max_length} chars")
|
||||||
|
|
||||||
api_key = api_key or self._openai_api_key
|
api_key = api_key or self._openai_api_key
|
||||||
if not api_key:
|
if not api_key:
|
||||||
@@ -675,19 +686,30 @@ class AICore:
|
|||||||
|
|
||||||
url = 'https://api.runware.ai/v1'
|
url = 'https://api.runware.ai/v1'
|
||||||
print(f"[AI][{function_name}] Step 3: Sending request to Runware API...")
|
print(f"[AI][{function_name}] Step 3: Sending request to Runware API...")
|
||||||
|
print(f"[AI][{function_name}] Runware API key check: has_key={bool(api_key)}, key_length={len(api_key) if api_key else 0}")
|
||||||
|
|
||||||
# Runware uses array payload
|
# Runware uses array payload with authentication task first, then imageInference
|
||||||
payload = [{
|
# Reference: image-generation.php lines 79-97
|
||||||
'taskType': 'imageInference',
|
import uuid
|
||||||
'model': runware_model,
|
payload = [
|
||||||
'prompt': prompt,
|
{
|
||||||
'width': width,
|
'taskType': 'authentication',
|
||||||
'height': height,
|
'apiKey': api_key
|
||||||
'apiKey': api_key
|
},
|
||||||
}]
|
{
|
||||||
|
'taskType': 'imageInference',
|
||||||
if negative_prompt:
|
'taskUUID': str(uuid.uuid4()),
|
||||||
payload[0]['negativePrompt'] = negative_prompt
|
'positivePrompt': prompt,
|
||||||
|
'negativePrompt': negative_prompt or '',
|
||||||
|
'model': runware_model,
|
||||||
|
'width': width,
|
||||||
|
'height': height,
|
||||||
|
'steps': 30,
|
||||||
|
'CFGScale': 7.5,
|
||||||
|
'numberResults': 1,
|
||||||
|
'outputFormat': 'webp'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
request_start = time.time()
|
request_start = time.time()
|
||||||
try:
|
try:
|
||||||
@@ -706,10 +728,28 @@ class AICore:
|
|||||||
}
|
}
|
||||||
|
|
||||||
body = response.json()
|
body = response.json()
|
||||||
# Runware returns array with image data
|
print(f"[AI][{function_name}] Runware response type: {type(body)}, length: {len(body) if isinstance(body, list) else 'N/A'}")
|
||||||
if isinstance(body, list) and len(body) > 0:
|
|
||||||
image_data = body[0]
|
# Runware returns array: [auth_result, image_result]
|
||||||
image_url = image_data.get('imageURL') or image_data.get('url')
|
# image_result has 'data' array with image objects containing 'imageURL'
|
||||||
|
image_url = None
|
||||||
|
if isinstance(body, list):
|
||||||
|
# Find the imageInference result (usually second element)
|
||||||
|
for item in body:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
# Check for 'data' key (image result)
|
||||||
|
if 'data' in item and isinstance(item['data'], list) and len(item['data']) > 0:
|
||||||
|
image_data = item['data'][0]
|
||||||
|
image_url = image_data.get('imageURL') or image_data.get('image_url')
|
||||||
|
if image_url:
|
||||||
|
break
|
||||||
|
# Check for direct 'imageURL' (fallback)
|
||||||
|
elif 'imageURL' in item:
|
||||||
|
image_url = item.get('imageURL')
|
||||||
|
if image_url:
|
||||||
|
break
|
||||||
|
|
||||||
|
if image_url:
|
||||||
|
|
||||||
cost = 0.036 * n # Runware pricing
|
cost = 0.036 * n # Runware pricing
|
||||||
print(f"[AI][{function_name}] Step 5: Image generated successfully")
|
print(f"[AI][{function_name}] Step 5: Image generated successfully")
|
||||||
|
|||||||
@@ -317,32 +317,44 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Format template with image prompt from database
|
# Format template with image prompt from database
|
||||||
# For DALL-E 2: Use image prompt directly (no template)
|
# For DALL-E 2: Use image prompt directly (no template), 1000 char limit
|
||||||
# For DALL-E 3 and others: Use template with placeholders
|
# For DALL-E 3: Use template with placeholders, 4000 char limit
|
||||||
# CRITICAL: OpenAI has strict 1000 character limit for prompts
|
# CRITICAL: DALL-E 2 has 1000 char limit, DALL-E 3 has 4000 char limit
|
||||||
image_prompt = image.prompt or ""
|
image_prompt = image.prompt or ""
|
||||||
|
|
||||||
|
# Determine character limit based on model
|
||||||
|
if model == 'dall-e-2':
|
||||||
|
max_prompt_length = 1000
|
||||||
|
elif model == 'dall-e-3':
|
||||||
|
max_prompt_length = 4000
|
||||||
|
else:
|
||||||
|
# Default to 1000 for safety
|
||||||
|
max_prompt_length = 1000
|
||||||
|
logger.warning(f"Unknown model '{model}', using 1000 char limit")
|
||||||
|
|
||||||
|
logger.info(f"[process_image_generation_queue] Model: {model}, Max prompt length: {max_prompt_length} chars")
|
||||||
|
|
||||||
if model == 'dall-e-2':
|
if model == 'dall-e-2':
|
||||||
# DALL-E 2: Use image prompt directly, no template
|
# DALL-E 2: Use image prompt directly, no template
|
||||||
logger.info(f"[process_image_generation_queue] Using DALL-E 2 - skipping template, using image prompt directly")
|
logger.info(f"[process_image_generation_queue] Using DALL-E 2 - skipping template, using image prompt directly")
|
||||||
formatted_prompt = image_prompt
|
formatted_prompt = image_prompt
|
||||||
|
|
||||||
# Truncate to 1000 chars if needed
|
# Truncate to 1000 chars if needed
|
||||||
if len(formatted_prompt) > 1000:
|
if len(formatted_prompt) > max_prompt_length:
|
||||||
logger.warning(f"DALL-E 2 prompt too long ({len(formatted_prompt)} chars), truncating to 1000")
|
logger.warning(f"DALL-E 2 prompt too long ({len(formatted_prompt)} chars), truncating to {max_prompt_length}")
|
||||||
truncated = formatted_prompt[:997]
|
truncated = formatted_prompt[:max_prompt_length - 3]
|
||||||
last_space = truncated.rfind(' ')
|
last_space = truncated.rfind(' ')
|
||||||
if last_space > 900:
|
if last_space > max_prompt_length * 0.9:
|
||||||
formatted_prompt = truncated[:last_space] + "..."
|
formatted_prompt = truncated[:last_space] + "..."
|
||||||
else:
|
else:
|
||||||
formatted_prompt = formatted_prompt[:1000]
|
formatted_prompt = formatted_prompt[:max_prompt_length]
|
||||||
else:
|
else:
|
||||||
# DALL-E 3 and others: Use template
|
# DALL-E 3 and others: Use template
|
||||||
try:
|
try:
|
||||||
# Truncate post_title aggressively (max 80 chars to leave more room for image_prompt)
|
# Truncate post_title (max 200 chars for DALL-E 3 to leave room for image_prompt)
|
||||||
post_title = content.title or content.meta_title or f"Content #{content.id}"
|
post_title = content.title or content.meta_title or f"Content #{content.id}"
|
||||||
if len(post_title) > 80:
|
if len(post_title) > 200:
|
||||||
post_title = post_title[:77] + "..."
|
post_title = post_title[:197] + "..."
|
||||||
|
|
||||||
# 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
|
||||||
@@ -353,11 +365,11 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
|||||||
)
|
)
|
||||||
template_overhead = len(template_with_dummies)
|
template_overhead = len(template_with_dummies)
|
||||||
|
|
||||||
# Calculate max image_prompt length: 1000 - template_overhead - safety margin (20)
|
# Calculate max image_prompt length: max_prompt_length - template_overhead - safety margin (50)
|
||||||
max_image_prompt_length = 1000 - template_overhead - 20
|
max_image_prompt_length = max_prompt_length - template_overhead - 50
|
||||||
if max_image_prompt_length < 50:
|
if max_image_prompt_length < 100:
|
||||||
# If template is too long, use minimum 50 chars for image_prompt
|
# If template is too long, use minimum 100 chars for image_prompt
|
||||||
max_image_prompt_length = 50
|
max_image_prompt_length = 100
|
||||||
logger.warning(f"Template is very long ({template_overhead} chars), limiting image_prompt to {max_image_prompt_length}")
|
logger.warning(f"Template is very long ({template_overhead} chars), limiting image_prompt to {max_image_prompt_length}")
|
||||||
|
|
||||||
logger.info(f"[process_image_generation_queue] Template overhead: {template_overhead} chars, max image_prompt: {max_image_prompt_length} chars")
|
logger.info(f"[process_image_generation_queue] Template overhead: {template_overhead} chars, max image_prompt: {max_image_prompt_length} chars")
|
||||||
@@ -379,47 +391,48 @@ def process_image_generation_queue(self, image_ids: list, account_id: int = None
|
|||||||
image_prompt=image_prompt
|
image_prompt=image_prompt
|
||||||
)
|
)
|
||||||
|
|
||||||
# CRITICAL: Final safety check - ALWAYS truncate to 1000 chars max
|
# CRITICAL: Final safety check - truncate to model-specific limit
|
||||||
if len(formatted_prompt) > 1000:
|
if len(formatted_prompt) > max_prompt_length:
|
||||||
logger.warning(f"Formatted prompt too long ({len(formatted_prompt)} chars), truncating to 1000")
|
logger.warning(f"Formatted prompt too long ({len(formatted_prompt)} chars), truncating to {max_prompt_length} for {model}")
|
||||||
# Try word-aware truncation
|
# Try word-aware truncation
|
||||||
truncated = formatted_prompt[:997]
|
truncated = formatted_prompt[:max_prompt_length - 3]
|
||||||
last_space = truncated.rfind(' ')
|
last_space = truncated.rfind(' ')
|
||||||
if last_space > 900: # Only use word-aware if we have a reasonable space
|
if last_space > max_prompt_length * 0.9: # Only use word-aware if we have a reasonable space
|
||||||
formatted_prompt = truncated[:last_space] + "..."
|
formatted_prompt = truncated[:last_space] + "..."
|
||||||
else:
|
else:
|
||||||
formatted_prompt = formatted_prompt[:1000] # Hard truncate
|
formatted_prompt = formatted_prompt[:max_prompt_length] # Hard truncate
|
||||||
|
|
||||||
# Double-check after truncation - MUST be <= 1000
|
# Double-check after truncation - MUST be <= max_prompt_length
|
||||||
if len(formatted_prompt) > 1000:
|
if len(formatted_prompt) > max_prompt_length:
|
||||||
logger.error(f"Prompt still too long after truncation ({len(formatted_prompt)} chars), forcing hard truncate")
|
logger.error(f"Prompt still too long after truncation ({len(formatted_prompt)} chars), forcing hard truncate to {max_prompt_length}")
|
||||||
formatted_prompt = formatted_prompt[:1000]
|
formatted_prompt = formatted_prompt[:max_prompt_length]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Fallback if template formatting fails
|
# Fallback if template formatting fails
|
||||||
logger.warning(f"Prompt template formatting failed: {e}, using image prompt directly")
|
logger.warning(f"Prompt template formatting failed: {e}, using image prompt directly")
|
||||||
formatted_prompt = image_prompt
|
formatted_prompt = image_prompt
|
||||||
# CRITICAL: Truncate to 1000 chars even in fallback
|
# CRITICAL: Truncate to model-specific limit even in fallback
|
||||||
if len(formatted_prompt) > 1000:
|
if len(formatted_prompt) > max_prompt_length:
|
||||||
logger.warning(f"Fallback prompt too long ({len(formatted_prompt)} chars), truncating to 1000")
|
logger.warning(f"Fallback prompt too long ({len(formatted_prompt)} chars), truncating to {max_prompt_length} for {model}")
|
||||||
# Try word-aware truncation
|
# Try word-aware truncation
|
||||||
truncated = formatted_prompt[:997]
|
truncated = formatted_prompt[:max_prompt_length - 3]
|
||||||
last_space = truncated.rfind(' ')
|
last_space = truncated.rfind(' ')
|
||||||
if last_space > 900:
|
if last_space > max_prompt_length * 0.9:
|
||||||
formatted_prompt = truncated[:last_space] + "..."
|
formatted_prompt = truncated[:last_space] + "..."
|
||||||
else:
|
else:
|
||||||
formatted_prompt = formatted_prompt[:1000] # Hard truncate
|
formatted_prompt = formatted_prompt[:max_prompt_length] # Hard truncate
|
||||||
# Final hard truncate if still too long - MUST be <= 1000
|
# Final hard truncate if still too long - MUST be <= max_prompt_length
|
||||||
if len(formatted_prompt) > 1000:
|
if len(formatted_prompt) > max_prompt_length:
|
||||||
formatted_prompt = formatted_prompt[:1000]
|
logger.error(f"Fallback prompt still too long ({len(formatted_prompt)} chars), forcing hard truncate to {max_prompt_length}")
|
||||||
|
formatted_prompt = formatted_prompt[:max_prompt_length]
|
||||||
|
|
||||||
# Generate image (using same approach as test image generation)
|
# Generate image (using same approach as test image generation)
|
||||||
logger.info(f"[process_image_generation_queue] Generating image {index}/{total_images} (ID: {image_id})")
|
logger.info(f"[process_image_generation_queue] Generating image {index}/{total_images} (ID: {image_id})")
|
||||||
logger.info(f"[process_image_generation_queue] Provider: {provider}, Model: {model}")
|
logger.info(f"[process_image_generation_queue] Provider: {provider}, Model: {model}")
|
||||||
logger.info(f"[process_image_generation_queue] Prompt length: {len(formatted_prompt)} (MUST be <= 1000)")
|
logger.info(f"[process_image_generation_queue] Prompt length: {len(formatted_prompt)} (MUST be <= {max_prompt_length} for {model})")
|
||||||
if len(formatted_prompt) > 1000:
|
if len(formatted_prompt) > max_prompt_length:
|
||||||
logger.error(f"[process_image_generation_queue] ERROR: Prompt is {len(formatted_prompt)} chars, truncating NOW!")
|
logger.error(f"[process_image_generation_queue] ERROR: Prompt is {len(formatted_prompt)} chars, truncating NOW to {max_prompt_length}!")
|
||||||
formatted_prompt = formatted_prompt[:1000]
|
formatted_prompt = formatted_prompt[:max_prompt_length]
|
||||||
logger.info(f"[process_image_generation_queue] Final prompt length: {len(formatted_prompt)}")
|
logger.info(f"[process_image_generation_queue] Final prompt length: {len(formatted_prompt)}")
|
||||||
logger.info(f"[process_image_generation_queue] Image type: {image_type}")
|
logger.info(f"[process_image_generation_queue] Image type: {image_type}")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated migration to change image_url from URLField to CharField
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('writer', '0007_add_content_to_images'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='images',
|
||||||
|
name='image_url',
|
||||||
|
field=models.CharField(blank=True, help_text='URL of the generated/stored image', max_length=500, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
@@ -843,6 +843,7 @@ Make sure each prompt is detailed enough for image generation, describing the vi
|
|||||||
# Runware uses array payload with authentication and imageInference tasks
|
# Runware uses array payload with authentication and imageInference tasks
|
||||||
# Reference: image-generation.php lines 79-97
|
# Reference: image-generation.php lines 79-97
|
||||||
import uuid
|
import uuid
|
||||||
|
logger.info(f"[AIProcessor.generate_image] Runware API key check: has_key={bool(api_key)}, key_length={len(api_key) if api_key else 0}, key_preview={api_key[:10] + '...' + api_key[-4:] if api_key and len(api_key) > 14 else 'N/A'}")
|
||||||
payload = [
|
payload = [
|
||||||
{
|
{
|
||||||
'taskType': 'authentication',
|
'taskType': 'authentication',
|
||||||
|
|||||||
Reference in New Issue
Block a user