Commit Β·
533051b
1
Parent(s): 8158a5c
Enhance image generation and logging features across frontend and backend
Browse files- Added verification and logging for generated images in GeneratePage and AdPreview components to improve user feedback and debugging.
- Updated API endpoint to ensure required parameters are always sent for image generation.
- Enhanced image generation logic in the generator service to support parallel processing and improved error handling.
- frontend/app/generate/page.tsx +7 -0
- frontend/components/generation/AdPreview.tsx +53 -2
- frontend/lib/api/endpoints.ts +12 -3
- services/generator.py +259 -137
- services/third_flow.py +12 -1
frontend/app/generate/page.tsx
CHANGED
|
@@ -279,6 +279,13 @@ export default function GeneratePage() {
|
|
| 279 |
|
| 280 |
clearInterval(progressInterval);
|
| 281 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
setProgress({
|
| 283 |
step: "saving",
|
| 284 |
progress: 90,
|
|
|
|
| 279 |
|
| 280 |
clearInterval(progressInterval);
|
| 281 |
|
| 282 |
+
// Verify all images are included
|
| 283 |
+
if (result.images && result.images.length > 0) {
|
| 284 |
+
console.log(`β Extensive generation completed with ${result.images.length} image(s)`);
|
| 285 |
+
console.log(`π
Generated at: ${result.created_at || 'N/A'}`);
|
| 286 |
+
console.log(`π Note: Generate page shows images from the first strategy only. All strategies are saved separately in the database and appear on the Dashboard.`);
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
setProgress({
|
| 290 |
step: "saving",
|
| 291 |
progress: 90,
|
frontend/components/generation/AdPreview.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import React, { useState } from "react";
|
|
| 4 |
import { Button } from "@/components/ui/Button";
|
| 5 |
import { Download, Copy } from "lucide-react";
|
| 6 |
import { downloadImage, copyToClipboard } from "@/lib/utils/export";
|
| 7 |
-
import { getImageUrl, getImageUrlFallback } from "@/lib/utils/formatters";
|
| 8 |
import { toast } from "react-hot-toast";
|
| 9 |
import type { GenerateResponse, MatrixGenerateResponse } from "@/types/api";
|
| 10 |
|
|
@@ -15,6 +15,22 @@ interface AdPreviewProps {
|
|
| 15 |
export const AdPreview: React.FC<AdPreviewProps> = ({ ad }) => {
|
| 16 |
const [imageErrors, setImageErrors] = useState<Record<number, boolean>>({});
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
const handleDownloadImage = async (imageUrl: string | null | undefined, filename: string | null | undefined) => {
|
| 19 |
if (!imageUrl && !filename) {
|
| 20 |
toast.error("No image URL available");
|
|
@@ -55,6 +71,18 @@ export const AdPreview: React.FC<AdPreviewProps> = ({ ad }) => {
|
|
| 55 |
{/* Images */}
|
| 56 |
{ad.images && ad.images.length > 0 && (
|
| 57 |
<div className="space-y-4">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
{ad.images.length === 1 ? (
|
| 59 |
// Single image - full width with better styling
|
| 60 |
<div className="bg-white rounded-2xl shadow-lg shadow-blue-100/50 overflow-hidden ring-1 ring-blue-100">
|
|
@@ -71,6 +99,11 @@ export const AdPreview: React.FC<AdPreviewProps> = ({ ad }) => {
|
|
| 71 |
className="w-full h-auto"
|
| 72 |
onError={() => handleImageError(0, image)}
|
| 73 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
<div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity">
|
| 75 |
<div className="relative group/btn">
|
| 76 |
<Button
|
|
@@ -111,8 +144,11 @@ export const AdPreview: React.FC<AdPreviewProps> = ({ ad }) => {
|
|
| 111 |
const { primary, fallback } = getImageUrlFallback(image.image_url, image.filename);
|
| 112 |
const imageUrl = imageErrors[index] ? fallback : (primary || fallback);
|
| 113 |
|
|
|
|
|
|
|
|
|
|
| 114 |
return (
|
| 115 |
-
<div key={
|
| 116 |
{imageUrl ? (
|
| 117 |
<div className="relative group">
|
| 118 |
<img
|
|
@@ -121,6 +157,10 @@ export const AdPreview: React.FC<AdPreviewProps> = ({ ad }) => {
|
|
| 121 |
className="w-full h-auto"
|
| 122 |
onError={() => handleImageError(index, image)}
|
| 123 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
| 125 |
<div className="relative group/btn">
|
| 126 |
<Button
|
|
@@ -152,6 +192,17 @@ export const AdPreview: React.FC<AdPreviewProps> = ({ ad }) => {
|
|
| 152 |
<p className="text-xs text-red-800">Error: {image.error}</p>
|
| 153 |
</div>
|
| 154 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
</div>
|
| 156 |
);
|
| 157 |
})}
|
|
|
|
| 4 |
import { Button } from "@/components/ui/Button";
|
| 5 |
import { Download, Copy } from "lucide-react";
|
| 6 |
import { downloadImage, copyToClipboard } from "@/lib/utils/export";
|
| 7 |
+
import { getImageUrl, getImageUrlFallback, formatRelativeDate } from "@/lib/utils/formatters";
|
| 8 |
import { toast } from "react-hot-toast";
|
| 9 |
import type { GenerateResponse, MatrixGenerateResponse } from "@/types/api";
|
| 10 |
|
|
|
|
| 15 |
export const AdPreview: React.FC<AdPreviewProps> = ({ ad }) => {
|
| 16 |
const [imageErrors, setImageErrors] = useState<Record<number, boolean>>({});
|
| 17 |
|
| 18 |
+
// Debug: Log image details to verify uniqueness
|
| 19 |
+
React.useEffect(() => {
|
| 20 |
+
if (ad.images && ad.images.length > 1) {
|
| 21 |
+
console.log(`AdPreview: Displaying ${ad.images.length} images`);
|
| 22 |
+
console.log(`π
Generation timestamp: ${ad.created_at || 'N/A'}`);
|
| 23 |
+
ad.images.forEach((img, idx) => {
|
| 24 |
+
console.log(` Image ${idx + 1}:`, {
|
| 25 |
+
filename: img.filename,
|
| 26 |
+
image_url: img.image_url?.substring(0, 50) + '...',
|
| 27 |
+
seed: img.seed,
|
| 28 |
+
});
|
| 29 |
+
});
|
| 30 |
+
}
|
| 31 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 32 |
+
}, [ad.id, ad.images?.length, ad.created_at]); // Use stable dependencies: id, length, and timestamp
|
| 33 |
+
|
| 34 |
const handleDownloadImage = async (imageUrl: string | null | undefined, filename: string | null | undefined) => {
|
| 35 |
if (!imageUrl && !filename) {
|
| 36 |
toast.error("No image URL available");
|
|
|
|
| 71 |
{/* Images */}
|
| 72 |
{ad.images && ad.images.length > 0 && (
|
| 73 |
<div className="space-y-4">
|
| 74 |
+
{ad.images.length > 1 && (
|
| 75 |
+
<div className="flex items-center justify-between">
|
| 76 |
+
<h3 className="text-sm font-semibold text-gray-700">
|
| 77 |
+
Images ({ad.images.length})
|
| 78 |
+
</h3>
|
| 79 |
+
{ad.created_at && (
|
| 80 |
+
<span className="text-xs text-gray-500 font-medium">
|
| 81 |
+
Generated {formatRelativeDate(ad.created_at)}
|
| 82 |
+
</span>
|
| 83 |
+
)}
|
| 84 |
+
</div>
|
| 85 |
+
)}
|
| 86 |
{ad.images.length === 1 ? (
|
| 87 |
// Single image - full width with better styling
|
| 88 |
<div className="bg-white rounded-2xl shadow-lg shadow-blue-100/50 overflow-hidden ring-1 ring-blue-100">
|
|
|
|
| 99 |
className="w-full h-auto"
|
| 100 |
onError={() => handleImageError(0, image)}
|
| 101 |
/>
|
| 102 |
+
{ad.created_at && (
|
| 103 |
+
<div className="absolute top-4 left-4 bg-black/60 backdrop-blur-sm text-white text-xs font-medium px-2 py-1 rounded-md">
|
| 104 |
+
{formatRelativeDate(ad.created_at)}
|
| 105 |
+
</div>
|
| 106 |
+
)}
|
| 107 |
<div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity">
|
| 108 |
<div className="relative group/btn">
|
| 109 |
<Button
|
|
|
|
| 144 |
const { primary, fallback } = getImageUrlFallback(image.image_url, image.filename);
|
| 145 |
const imageUrl = imageErrors[index] ? fallback : (primary || fallback);
|
| 146 |
|
| 147 |
+
// Use unique key based on filename or URL to prevent duplicate rendering
|
| 148 |
+
const uniqueKey = image.filename || image.image_url || `image-${index}`;
|
| 149 |
+
|
| 150 |
return (
|
| 151 |
+
<div key={uniqueKey} className="bg-white rounded-2xl shadow-lg shadow-blue-100/50 overflow-hidden ring-1 ring-blue-100">
|
| 152 |
{imageUrl ? (
|
| 153 |
<div className="relative group">
|
| 154 |
<img
|
|
|
|
| 157 |
className="w-full h-auto"
|
| 158 |
onError={() => handleImageError(index, image)}
|
| 159 |
/>
|
| 160 |
+
{/* Image number badge */}
|
| 161 |
+
<div className="absolute top-2 left-2 bg-black/60 backdrop-blur-sm text-white text-xs font-bold px-2 py-1 rounded-md">
|
| 162 |
+
Image {index + 1}
|
| 163 |
+
</div>
|
| 164 |
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
| 165 |
<div className="relative group/btn">
|
| 166 |
<Button
|
|
|
|
| 192 |
<p className="text-xs text-red-800">Error: {image.error}</p>
|
| 193 |
</div>
|
| 194 |
)}
|
| 195 |
+
{/* Image metadata footer */}
|
| 196 |
+
<div className="p-2 bg-gray-50 border-t border-gray-100 flex items-center justify-between text-xs">
|
| 197 |
+
<span className="text-gray-500">
|
| 198 |
+
{image.seed && `Seed: ${image.seed}`}
|
| 199 |
+
</span>
|
| 200 |
+
{image.filename && (
|
| 201 |
+
<span className="text-gray-400 font-mono text-[10px] truncate max-w-[150px]">
|
| 202 |
+
{image.filename.split('_').pop()?.split('.')[0]}
|
| 203 |
+
</span>
|
| 204 |
+
)}
|
| 205 |
+
</div>
|
| 206 |
</div>
|
| 207 |
);
|
| 208 |
})}
|
frontend/lib/api/endpoints.ts
CHANGED
|
@@ -67,11 +67,20 @@ export const generateExtensiveAd = async (params: {
|
|
| 67 |
niche: Niche;
|
| 68 |
target_audience: string;
|
| 69 |
offer: string;
|
| 70 |
-
num_images
|
| 71 |
image_model?: string | null;
|
| 72 |
-
num_strategies
|
| 73 |
}): Promise<GenerateResponse> => {
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
return response.data;
|
| 76 |
};
|
| 77 |
|
|
|
|
| 67 |
niche: Niche;
|
| 68 |
target_audience: string;
|
| 69 |
offer: string;
|
| 70 |
+
num_images: number;
|
| 71 |
image_model?: string | null;
|
| 72 |
+
num_strategies: number;
|
| 73 |
}): Promise<GenerateResponse> => {
|
| 74 |
+
// Ensure required parameters are always sent
|
| 75 |
+
const requestParams = {
|
| 76 |
+
niche: params.niche,
|
| 77 |
+
target_audience: params.target_audience,
|
| 78 |
+
offer: params.offer,
|
| 79 |
+
num_images: params.num_images || 1,
|
| 80 |
+
num_strategies: params.num_strategies || 1,
|
| 81 |
+
...(params.image_model && { image_model: params.image_model }),
|
| 82 |
+
};
|
| 83 |
+
const response = await apiClient.post<GenerateResponse>("/extensive/generate", requestParams);
|
| 84 |
return response.data;
|
| 85 |
};
|
| 86 |
|
services/generator.py
CHANGED
|
@@ -14,8 +14,10 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
| 14 |
import random
|
| 15 |
import uuid
|
| 16 |
import json
|
|
|
|
| 17 |
from datetime import datetime
|
| 18 |
from typing import Dict, Any, List, Optional
|
|
|
|
| 19 |
|
| 20 |
from config import settings
|
| 21 |
from services.llm import llm_service
|
|
@@ -1105,6 +1107,17 @@ NO text overlays, decorative elements, borders, banners, or overlays.
|
|
| 1105 |
=== VISUAL SCENE ===
|
| 1106 |
{image_brief}
|
| 1107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1108 |
IMPORTANT: Do NOT display numbers, prices, dollar amounts, or savings figures in the image unless they naturally appear as part of the scene (like on a document someone is holding, or a sign in the background). Focus on the visual scene and people, not numerical information. Numbers should be in the ad copy, not the image.
|
| 1109 |
|
| 1110 |
=== VISUAL SPECIFICATIONS ===
|
|
@@ -1131,6 +1144,21 @@ PEOPLE (if present):
|
|
| 1131 |
- Natural expressions (not posed smiles)
|
| 1132 |
- Relatable, trustworthy appearance
|
| 1133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1134 |
SETTINGS (if present):
|
| 1135 |
- Real locations (homes, yards, offices)
|
| 1136 |
- Lived-in, not staged
|
|
@@ -1152,6 +1180,16 @@ DOCUMENTS (if present):
|
|
| 1152 |
- NO missing or garbled text
|
| 1153 |
- NO brand watermarks
|
| 1154 |
- NO distorted anatomy
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1155 |
- NO decorative borders, frames, or boxes around the image
|
| 1156 |
- NO banners, badges, or logos in corners (like "BREAKING", "TRUSTED", etc.)
|
| 1157 |
- NO overlay boxes or rectangular overlays with text
|
|
@@ -1413,10 +1451,9 @@ CRITICAL REQUIREMENTS:
|
|
| 1413 |
temperature=0.95, # High for variety
|
| 1414 |
)
|
| 1415 |
|
| 1416 |
-
# Generate image(s) with professional prompt
|
| 1417 |
-
|
| 1418 |
-
|
| 1419 |
-
for i in range(num_images):
|
| 1420 |
# Build image prompt with all parameters
|
| 1421 |
image_prompt = self._build_image_prompt(
|
| 1422 |
niche=niche,
|
|
@@ -1430,13 +1467,14 @@ CRITICAL REQUIREMENTS:
|
|
| 1430 |
niche_visual_guidance_data=niche_visual_guidance_data,
|
| 1431 |
)
|
| 1432 |
|
| 1433 |
-
# Store the refined prompt for database saving
|
| 1434 |
refined_image_prompt = image_prompt
|
| 1435 |
|
| 1436 |
# Generate with random seed
|
| 1437 |
seed = random.randint(1, 2147483647)
|
| 1438 |
|
| 1439 |
try:
|
|
|
|
| 1440 |
image_bytes, model_used, image_url = await image_service.generate(
|
| 1441 |
prompt=image_prompt,
|
| 1442 |
width=settings.image_width,
|
|
@@ -1448,40 +1486,25 @@ CRITICAL REQUIREMENTS:
|
|
| 1448 |
# Generate filename
|
| 1449 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 1450 |
unique_id = str(uuid.uuid4())[:8]
|
| 1451 |
-
filename = f"{niche}_{timestamp}_{unique_id}.png"
|
| 1452 |
-
|
| 1453 |
-
# Upload to R2 if available, otherwise save locally
|
| 1454 |
-
r2_url = None
|
| 1455 |
-
if r2_storage_available:
|
| 1456 |
-
try:
|
| 1457 |
-
r2_storage = get_r2_storage()
|
| 1458 |
-
if r2_storage:
|
| 1459 |
-
r2_url = r2_storage.upload_image(
|
| 1460 |
-
image_bytes=image_bytes,
|
| 1461 |
-
filename=filename,
|
| 1462 |
-
niche=niche,
|
| 1463 |
-
)
|
| 1464 |
-
print(f"Image uploaded to R2: {r2_url}")
|
| 1465 |
-
except Exception as e:
|
| 1466 |
-
print(f"Warning: Failed to upload to R2: {e}. Saving locally as backup.")
|
| 1467 |
-
|
| 1468 |
-
# Generate filename
|
| 1469 |
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 1470 |
-
unique_id = str(uuid.uuid4())[:8]
|
| 1471 |
-
filename = f"{niche}_{timestamp}_{unique_id}.png"
|
| 1472 |
|
| 1473 |
-
# Upload to R2
|
| 1474 |
r2_url = None
|
| 1475 |
if r2_storage_available:
|
| 1476 |
try:
|
| 1477 |
r2_storage = get_r2_storage()
|
| 1478 |
if r2_storage:
|
| 1479 |
-
|
| 1480 |
-
|
| 1481 |
-
|
| 1482 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1483 |
)
|
| 1484 |
-
print(f"Image uploaded to R2: {r2_url}")
|
| 1485 |
except Exception as e:
|
| 1486 |
print(f"Warning: Failed to upload to R2: {e}. Saving locally as backup.")
|
| 1487 |
|
|
@@ -1491,22 +1514,31 @@ CRITICAL REQUIREMENTS:
|
|
| 1491 |
# Use R2 URL if available, otherwise use Replicate URL, fallback to local
|
| 1492 |
final_image_url = r2_url or image_url
|
| 1493 |
|
| 1494 |
-
|
| 1495 |
"filename": filename,
|
| 1496 |
"filepath": filepath,
|
| 1497 |
-
"image_url": final_image_url,
|
| 1498 |
-
"r2_url": r2_url,
|
| 1499 |
"model_used": model_used,
|
| 1500 |
"seed": seed,
|
| 1501 |
-
"image_prompt": refined_image_prompt,
|
| 1502 |
-
}
|
| 1503 |
|
| 1504 |
except Exception as e:
|
| 1505 |
-
|
| 1506 |
"error": str(e),
|
| 1507 |
"seed": seed,
|
| 1508 |
-
"image_prompt": refined_image_prompt
|
| 1509 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1510 |
|
| 1511 |
# Generate unique ID
|
| 1512 |
ad_id = str(uuid.uuid4())
|
|
@@ -1693,12 +1725,13 @@ CONCEPT: {concept['name']}
|
|
| 1693 |
# Store the refined prompt for database saving (this is the final prompt sent to image service)
|
| 1694 |
refined_image_prompt = image_prompt
|
| 1695 |
|
| 1696 |
-
# Generate images
|
| 1697 |
-
|
| 1698 |
-
|
| 1699 |
seed = random.randint(1, 2**31 - 1)
|
| 1700 |
|
| 1701 |
try:
|
|
|
|
| 1702 |
image_bytes, model_used, image_url = await image_service.generate(
|
| 1703 |
prompt=image_prompt,
|
| 1704 |
model_key=image_model or settings.image_model,
|
|
@@ -1710,20 +1743,25 @@ CONCEPT: {concept['name']}
|
|
| 1710 |
# Generate filename
|
| 1711 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 1712 |
unique_id = uuid.uuid4().hex[:8]
|
| 1713 |
-
filename = f"{niche}_{timestamp}_{unique_id}.png"
|
| 1714 |
|
| 1715 |
-
# Upload to R2
|
| 1716 |
r2_url = None
|
| 1717 |
if r2_storage_available:
|
| 1718 |
try:
|
| 1719 |
r2_storage = get_r2_storage()
|
| 1720 |
if r2_storage:
|
| 1721 |
-
|
| 1722 |
-
|
| 1723 |
-
|
| 1724 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1725 |
)
|
| 1726 |
-
print(f"
|
| 1727 |
except Exception as e:
|
| 1728 |
print(f"Warning: Failed to upload to R2: {e}. Saving locally as backup.")
|
| 1729 |
|
|
@@ -1733,18 +1771,18 @@ CONCEPT: {concept['name']}
|
|
| 1733 |
# Use R2 URL if available, otherwise use Replicate URL
|
| 1734 |
final_image_url = r2_url or image_url
|
| 1735 |
|
| 1736 |
-
|
| 1737 |
"filename": filename,
|
| 1738 |
"filepath": filepath,
|
| 1739 |
-
"image_url": final_image_url,
|
| 1740 |
-
"r2_url": r2_url,
|
| 1741 |
"model_used": model_used,
|
| 1742 |
"seed": seed,
|
| 1743 |
-
"image_prompt": refined_image_prompt,
|
| 1744 |
"error": None,
|
| 1745 |
-
}
|
| 1746 |
except Exception as e:
|
| 1747 |
-
|
| 1748 |
"filename": None,
|
| 1749 |
"filepath": None,
|
| 1750 |
"image_url": None,
|
|
@@ -1752,7 +1790,16 @@ CONCEPT: {concept['name']}
|
|
| 1752 |
"seed": seed,
|
| 1753 |
"image_prompt": refined_image_prompt if 'refined_image_prompt' in locals() else None,
|
| 1754 |
"error": str(e),
|
| 1755 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1756 |
|
| 1757 |
# Generate unique ID
|
| 1758 |
ad_id = str(uuid.uuid4())
|
|
@@ -1898,7 +1945,8 @@ CONCEPT: {concept['name']}
|
|
| 1898 |
ads_knowledge = ads_future.result()
|
| 1899 |
|
| 1900 |
# Step 3: Creative Director
|
| 1901 |
-
print("π¨ Step 3: Creating creative strategies...")
|
|
|
|
| 1902 |
creative_strategies = third_flow_service.creative_director(
|
| 1903 |
researcher_output=researcher_output,
|
| 1904 |
book_knowledge=book_knowledge,
|
|
@@ -1912,6 +1960,10 @@ CONCEPT: {concept['name']}
|
|
| 1912 |
if not creative_strategies:
|
| 1913 |
raise ValueError("Creative director returned no strategies")
|
| 1914 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1915 |
# Step 4: Process strategies in parallel (designer + copywriter)
|
| 1916 |
print(f"β‘ Step 4: Processing {len(creative_strategies)} strategies in parallel...")
|
| 1917 |
from concurrent.futures import ThreadPoolExecutor as TPE
|
|
@@ -1923,51 +1975,81 @@ CONCEPT: {concept['name']}
|
|
| 1923 |
))
|
| 1924 |
|
| 1925 |
# Step 5: Generate images for each strategy
|
| 1926 |
-
|
|
|
|
|
|
|
| 1927 |
all_results = []
|
| 1928 |
|
| 1929 |
-
for idx, (prompt, title, body, description) in enumerate(strategy_results):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1930 |
if not prompt:
|
| 1931 |
print(f"Warning: Strategy {idx + 1} has no prompt, skipping...")
|
| 1932 |
continue
|
| 1933 |
|
| 1934 |
-
# Generate images for this strategy
|
| 1935 |
-
|
| 1936 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1937 |
try:
|
| 1938 |
-
# Refine prompt
|
| 1939 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1940 |
|
| 1941 |
-
# Generate image
|
|
|
|
|
|
|
|
|
|
| 1942 |
image_bytes, model_used, image_url = await image_service.generate(
|
| 1943 |
prompt=refined_prompt,
|
| 1944 |
-
seed=
|
| 1945 |
model_key=image_model,
|
| 1946 |
)
|
| 1947 |
|
| 1948 |
if not image_bytes:
|
| 1949 |
print(f"Warning: Failed to generate image {img_idx + 1} for strategy {idx + 1}")
|
| 1950 |
-
|
| 1951 |
-
generated_images.append({
|
| 1952 |
"error": "Image generation returned no image data",
|
| 1953 |
-
"seed":
|
| 1954 |
"image_prompt": refined_prompt,
|
| 1955 |
-
}
|
| 1956 |
-
continue
|
| 1957 |
|
| 1958 |
-
# Generate filename
|
| 1959 |
-
filename = f"{niche}_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}.png"
|
| 1960 |
|
| 1961 |
-
# Upload to R2
|
| 1962 |
r2_url = None
|
| 1963 |
if r2_storage_available:
|
| 1964 |
try:
|
| 1965 |
r2_storage = get_r2_storage()
|
| 1966 |
if r2_storage:
|
| 1967 |
-
|
| 1968 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1969 |
)
|
| 1970 |
-
print(f"
|
| 1971 |
except Exception as r2_e:
|
| 1972 |
print(f"Warning: Failed to upload image to R2: {r2_e}")
|
| 1973 |
|
|
@@ -1977,25 +2059,34 @@ CONCEPT: {concept['name']}
|
|
| 1977 |
# Use R2 URL if available, otherwise use Replicate URL
|
| 1978 |
final_image_url = r2_url or image_url
|
| 1979 |
|
| 1980 |
-
|
| 1981 |
-
# If we have image_bytes, we saved it locally, so filename is available
|
| 1982 |
-
generated_images.append({
|
| 1983 |
"filename": filename,
|
| 1984 |
"filepath": filepath,
|
| 1985 |
-
"image_url": final_image_url,
|
| 1986 |
"r2_url": r2_url,
|
| 1987 |
"model_used": model_used,
|
| 1988 |
-
"seed":
|
| 1989 |
"image_prompt": refined_prompt,
|
| 1990 |
-
}
|
|
|
|
|
|
|
| 1991 |
except Exception as e:
|
| 1992 |
print(f"Error generating image {img_idx + 1} for strategy {idx + 1}: {e}")
|
| 1993 |
-
|
| 1994 |
-
|
| 1995 |
"error": str(e),
|
| 1996 |
-
"seed":
|
| 1997 |
"image_prompt": refined_prompt if 'refined_prompt' in locals() else None,
|
| 1998 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1999 |
|
| 2000 |
if not generated_images:
|
| 2001 |
print(f"Warning: No images generated for strategy {idx + 1}, skipping...")
|
|
@@ -2062,7 +2153,7 @@ CONCEPT: {concept['name']}
|
|
| 2062 |
"cta": cta,
|
| 2063 |
"psychological_angle": strategy.phsychologyTrigger or "",
|
| 2064 |
"why_it_works": f"Angle: {strategy.angle}, Concept: {strategy.concept}",
|
| 2065 |
-
"images": generated_images,
|
| 2066 |
"metadata": {
|
| 2067 |
"strategies_used": [strategy.phsychologyTrigger] if strategy.phsychologyTrigger else ["extensive"],
|
| 2068 |
"creative_direction": f"Extensive: {strategy.angle} Γ {strategy.concept}",
|
|
@@ -2075,10 +2166,13 @@ CONCEPT: {concept['name']}
|
|
| 2075 |
"visual_styles": [strategy.concept] if strategy.concept else [],
|
| 2076 |
},
|
| 2077 |
})
|
|
|
|
| 2078 |
|
| 2079 |
# Return first result (or all if needed)
|
| 2080 |
if all_results:
|
| 2081 |
-
|
|
|
|
|
|
|
| 2082 |
else:
|
| 2083 |
raise ValueError("No ads generated from extensive")
|
| 2084 |
|
|
@@ -2216,6 +2310,17 @@ This image should trigger: {angle.get('trigger')}
|
|
| 2216 |
|
| 2217 |
{niche_guidance}
|
| 2218 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2219 |
=== LAYOUT ===
|
| 2220 |
- Text zone (bottom 25%): "{headline}"
|
| 2221 |
- Visual zone (top 75%): Scene following {concept.get('name')} concept
|
|
@@ -2226,6 +2331,14 @@ This image should trigger: {angle.get('trigger')}
|
|
| 2226 |
- Text that is too small to read
|
| 2227 |
- Generic stock photo look
|
| 2228 |
- Watermarks, logos
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2229 |
|
| 2230 |
Create a scroll-stopping ad image with "{headline}" prominently displayed."""
|
| 2231 |
|
|
@@ -2242,8 +2355,9 @@ Create a scroll-stopping ad image with "{headline}" prominently displayed."""
|
|
| 2242 |
username: Optional[str] = None, # Username of the user generating the ads
|
| 2243 |
) -> List[Dict[str, Any]]:
|
| 2244 |
"""
|
| 2245 |
-
Generate multiple ad creatives.
|
| 2246 |
Uses variety: 50% standard generation, 50% matrix generation.
|
|
|
|
| 2247 |
|
| 2248 |
Args:
|
| 2249 |
niche: Target niche
|
|
@@ -2253,55 +2367,63 @@ Create a scroll-stopping ad image with "{headline}" prominently displayed."""
|
|
| 2253 |
Returns:
|
| 2254 |
List of ad results (all normalized to GenerateResponse format)
|
| 2255 |
"""
|
| 2256 |
-
|
|
|
|
| 2257 |
|
| 2258 |
-
|
| 2259 |
-
|
| 2260 |
-
|
| 2261 |
-
|
| 2262 |
-
|
| 2263 |
-
|
| 2264 |
-
# Use angle Γ concept matrix approach
|
| 2265 |
-
result = await self.generate_ad_with_matrix(
|
| 2266 |
-
niche=niche,
|
| 2267 |
-
num_images=images_per_ad,
|
| 2268 |
-
image_model=image_model,
|
| 2269 |
-
username=username, # Pass username
|
| 2270 |
-
)
|
| 2271 |
-
# Normalize matrix result to standard format for batch response
|
| 2272 |
-
# Extract matrix info and convert metadata
|
| 2273 |
-
matrix_info = result.get("matrix", {})
|
| 2274 |
-
angle = matrix_info.get("angle", {})
|
| 2275 |
-
concept = matrix_info.get("concept", {})
|
| 2276 |
|
| 2277 |
-
|
| 2278 |
-
|
| 2279 |
-
|
| 2280 |
-
|
| 2281 |
-
|
| 2282 |
-
|
| 2283 |
-
|
| 2284 |
-
|
| 2285 |
-
|
| 2286 |
-
|
| 2287 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2288 |
}
|
| 2289 |
-
|
| 2290 |
-
|
| 2291 |
-
|
| 2292 |
-
|
| 2293 |
-
|
| 2294 |
-
niche=niche,
|
| 2295 |
-
num_images=images_per_ad,
|
| 2296 |
-
image_model=image_model,
|
| 2297 |
-
username=username, # Pass username
|
| 2298 |
-
)
|
| 2299 |
-
results.append(result)
|
| 2300 |
-
except Exception as e:
|
| 2301 |
-
results.append({
|
| 2302 |
-
"error": str(e),
|
| 2303 |
-
"index": i,
|
| 2304 |
-
})
|
| 2305 |
|
| 2306 |
return results
|
| 2307 |
|
|
|
|
| 14 |
import random
|
| 15 |
import uuid
|
| 16 |
import json
|
| 17 |
+
import asyncio
|
| 18 |
from datetime import datetime
|
| 19 |
from typing import Dict, Any, List, Optional
|
| 20 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 21 |
|
| 22 |
from config import settings
|
| 23 |
from services.llm import llm_service
|
|
|
|
| 1107 |
=== VISUAL SCENE ===
|
| 1108 |
{image_brief}
|
| 1109 |
|
| 1110 |
+
=== CRITICAL: PEOPLE AND FACES ===
|
| 1111 |
+
If this image includes people or faces, they MUST look like real, original people with:
|
| 1112 |
+
- Photorealistic faces with natural skin texture, visible pores, and realistic skin imperfections
|
| 1113 |
+
- Natural facial asymmetry (no perfectly symmetrical faces)
|
| 1114 |
+
- Unique, individual facial features (not generic or model-like)
|
| 1115 |
+
- Natural expressions with authentic micro-expressions
|
| 1116 |
+
- Realistic skin tones with natural variations and undertones
|
| 1117 |
+
- Natural hair texture with individual strands
|
| 1118 |
+
- Faces that look like real photographs of real people, NOT AI-generated portraits
|
| 1119 |
+
- Avoid any faces that look synthetic, fake, or obviously computer-generated
|
| 1120 |
+
|
| 1121 |
IMPORTANT: Do NOT display numbers, prices, dollar amounts, or savings figures in the image unless they naturally appear as part of the scene (like on a document someone is holding, or a sign in the background). Focus on the visual scene and people, not numerical information. Numbers should be in the ad copy, not the image.
|
| 1122 |
|
| 1123 |
=== VISUAL SPECIFICATIONS ===
|
|
|
|
| 1144 |
- Natural expressions (not posed smiles)
|
| 1145 |
- Relatable, trustworthy appearance
|
| 1146 |
|
| 1147 |
+
FACE REQUIREMENTS (CRITICAL - for natural, original-looking faces):
|
| 1148 |
+
- Photorealistic faces with natural skin texture and pores
|
| 1149 |
+
- Subtle facial asymmetry (real faces are never perfectly symmetrical)
|
| 1150 |
+
- Natural skin imperfections: fine lines, freckles, moles, natural variations in skin tone
|
| 1151 |
+
- Realistic facial features: unique nose shapes, eye shapes, lip shapes (not generic or perfect)
|
| 1152 |
+
- Natural hair texture with individual strands visible, not overly smooth or perfect
|
| 1153 |
+
- Authentic expressions with natural micro-expressions and subtle wrinkles around eyes/mouth
|
| 1154 |
+
- Realistic lighting on faces showing natural shadows and highlights
|
| 1155 |
+
- Natural skin tones with realistic color variations and undertones
|
| 1156 |
+
- Avoid overly smooth, plastic-looking skin
|
| 1157 |
+
- Avoid perfectly symmetrical faces
|
| 1158 |
+
- Avoid generic or "model-like" features
|
| 1159 |
+
- Include natural facial hair, age spots, or other authentic characteristics when appropriate
|
| 1160 |
+
- Faces should look like real photographs of real people, not AI-generated portraits
|
| 1161 |
+
|
| 1162 |
SETTINGS (if present):
|
| 1163 |
- Real locations (homes, yards, offices)
|
| 1164 |
- Lived-in, not staged
|
|
|
|
| 1180 |
- NO missing or garbled text
|
| 1181 |
- NO brand watermarks
|
| 1182 |
- NO distorted anatomy
|
| 1183 |
+
- NO AI-generated faces, synthetic faces, or fake-looking faces
|
| 1184 |
+
- NO overly smooth or plastic-looking skin
|
| 1185 |
+
- NO perfectly symmetrical faces
|
| 1186 |
+
- NO generic or model-like facial features
|
| 1187 |
+
- NO uncanny valley faces
|
| 1188 |
+
- NO faces that look like they came from a face generator
|
| 1189 |
+
- NO overly perfect, flawless skin
|
| 1190 |
+
- NO cartoon-like or stylized faces
|
| 1191 |
+
- NO faces with unnatural smoothness or lack of texture
|
| 1192 |
+
- NO faces that look like stock photos or professional headshots
|
| 1193 |
- NO decorative borders, frames, or boxes around the image
|
| 1194 |
- NO banners, badges, or logos in corners (like "BREAKING", "TRUSTED", etc.)
|
| 1195 |
- NO overlay boxes or rectangular overlays with text
|
|
|
|
| 1451 |
temperature=0.95, # High for variety
|
| 1452 |
)
|
| 1453 |
|
| 1454 |
+
# Generate image(s) with professional prompt - PARALLELIZED
|
| 1455 |
+
async def generate_single_image(image_index: int):
|
| 1456 |
+
"""Helper function to generate a single image with all processing."""
|
|
|
|
| 1457 |
# Build image prompt with all parameters
|
| 1458 |
image_prompt = self._build_image_prompt(
|
| 1459 |
niche=niche,
|
|
|
|
| 1467 |
niche_visual_guidance_data=niche_visual_guidance_data,
|
| 1468 |
)
|
| 1469 |
|
| 1470 |
+
# Store the refined prompt for database saving
|
| 1471 |
refined_image_prompt = image_prompt
|
| 1472 |
|
| 1473 |
# Generate with random seed
|
| 1474 |
seed = random.randint(1, 2147483647)
|
| 1475 |
|
| 1476 |
try:
|
| 1477 |
+
# Generate image (async)
|
| 1478 |
image_bytes, model_used, image_url = await image_service.generate(
|
| 1479 |
prompt=image_prompt,
|
| 1480 |
width=settings.image_width,
|
|
|
|
| 1486 |
# Generate filename
|
| 1487 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 1488 |
unique_id = str(uuid.uuid4())[:8]
|
| 1489 |
+
filename = f"{niche}_{timestamp}_{unique_id}_{image_index}.png"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1490 |
|
| 1491 |
+
# Upload to R2 in parallel thread (sync operation)
|
| 1492 |
r2_url = None
|
| 1493 |
if r2_storage_available:
|
| 1494 |
try:
|
| 1495 |
r2_storage = get_r2_storage()
|
| 1496 |
if r2_storage:
|
| 1497 |
+
# Run R2 upload in thread pool (sync operation)
|
| 1498 |
+
loop = asyncio.get_event_loop()
|
| 1499 |
+
r2_url = await loop.run_in_executor(
|
| 1500 |
+
None,
|
| 1501 |
+
lambda: r2_storage.upload_image(
|
| 1502 |
+
image_bytes=image_bytes,
|
| 1503 |
+
filename=filename,
|
| 1504 |
+
niche=niche,
|
| 1505 |
+
)
|
| 1506 |
)
|
| 1507 |
+
print(f"Image {image_index + 1} uploaded to R2: {r2_url}")
|
| 1508 |
except Exception as e:
|
| 1509 |
print(f"Warning: Failed to upload to R2: {e}. Saving locally as backup.")
|
| 1510 |
|
|
|
|
| 1514 |
# Use R2 URL if available, otherwise use Replicate URL, fallback to local
|
| 1515 |
final_image_url = r2_url or image_url
|
| 1516 |
|
| 1517 |
+
return {
|
| 1518 |
"filename": filename,
|
| 1519 |
"filepath": filepath,
|
| 1520 |
+
"image_url": final_image_url,
|
| 1521 |
+
"r2_url": r2_url,
|
| 1522 |
"model_used": model_used,
|
| 1523 |
"seed": seed,
|
| 1524 |
+
"image_prompt": refined_image_prompt,
|
| 1525 |
+
}
|
| 1526 |
|
| 1527 |
except Exception as e:
|
| 1528 |
+
return {
|
| 1529 |
"error": str(e),
|
| 1530 |
"seed": seed,
|
| 1531 |
+
"image_prompt": refined_image_prompt,
|
| 1532 |
+
}
|
| 1533 |
+
|
| 1534 |
+
# Generate all images in parallel using asyncio.gather
|
| 1535 |
+
if num_images > 1:
|
| 1536 |
+
print(f"π Generating {num_images} images in parallel...")
|
| 1537 |
+
image_tasks = [generate_single_image(i) for i in range(num_images)]
|
| 1538 |
+
generated_images = await asyncio.gather(*image_tasks)
|
| 1539 |
+
else:
|
| 1540 |
+
# Single image - no need for parallelization
|
| 1541 |
+
generated_images = [await generate_single_image(0)]
|
| 1542 |
|
| 1543 |
# Generate unique ID
|
| 1544 |
ad_id = str(uuid.uuid4())
|
|
|
|
| 1725 |
# Store the refined prompt for database saving (this is the final prompt sent to image service)
|
| 1726 |
refined_image_prompt = image_prompt
|
| 1727 |
|
| 1728 |
+
# Generate images - PARALLELIZED
|
| 1729 |
+
async def generate_single_matrix_image(image_index: int):
|
| 1730 |
+
"""Helper function to generate a single matrix image with all processing."""
|
| 1731 |
seed = random.randint(1, 2**31 - 1)
|
| 1732 |
|
| 1733 |
try:
|
| 1734 |
+
# Generate image (async)
|
| 1735 |
image_bytes, model_used, image_url = await image_service.generate(
|
| 1736 |
prompt=image_prompt,
|
| 1737 |
model_key=image_model or settings.image_model,
|
|
|
|
| 1743 |
# Generate filename
|
| 1744 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 1745 |
unique_id = uuid.uuid4().hex[:8]
|
| 1746 |
+
filename = f"{niche}_{timestamp}_{unique_id}_{image_index}.png"
|
| 1747 |
|
| 1748 |
+
# Upload to R2 in parallel thread (sync operation)
|
| 1749 |
r2_url = None
|
| 1750 |
if r2_storage_available:
|
| 1751 |
try:
|
| 1752 |
r2_storage = get_r2_storage()
|
| 1753 |
if r2_storage:
|
| 1754 |
+
# Run R2 upload in thread pool (sync operation)
|
| 1755 |
+
loop = asyncio.get_event_loop()
|
| 1756 |
+
r2_url = await loop.run_in_executor(
|
| 1757 |
+
None,
|
| 1758 |
+
lambda: r2_storage.upload_image(
|
| 1759 |
+
image_bytes=image_bytes,
|
| 1760 |
+
filename=filename,
|
| 1761 |
+
niche=niche,
|
| 1762 |
+
)
|
| 1763 |
)
|
| 1764 |
+
print(f"Matrix image {image_index + 1} uploaded to R2: {r2_url}")
|
| 1765 |
except Exception as e:
|
| 1766 |
print(f"Warning: Failed to upload to R2: {e}. Saving locally as backup.")
|
| 1767 |
|
|
|
|
| 1771 |
# Use R2 URL if available, otherwise use Replicate URL
|
| 1772 |
final_image_url = r2_url or image_url
|
| 1773 |
|
| 1774 |
+
return {
|
| 1775 |
"filename": filename,
|
| 1776 |
"filepath": filepath,
|
| 1777 |
+
"image_url": final_image_url,
|
| 1778 |
+
"r2_url": r2_url,
|
| 1779 |
"model_used": model_used,
|
| 1780 |
"seed": seed,
|
| 1781 |
+
"image_prompt": refined_image_prompt,
|
| 1782 |
"error": None,
|
| 1783 |
+
}
|
| 1784 |
except Exception as e:
|
| 1785 |
+
return {
|
| 1786 |
"filename": None,
|
| 1787 |
"filepath": None,
|
| 1788 |
"image_url": None,
|
|
|
|
| 1790 |
"seed": seed,
|
| 1791 |
"image_prompt": refined_image_prompt if 'refined_image_prompt' in locals() else None,
|
| 1792 |
"error": str(e),
|
| 1793 |
+
}
|
| 1794 |
+
|
| 1795 |
+
# Generate all images in parallel using asyncio.gather
|
| 1796 |
+
if num_images > 1:
|
| 1797 |
+
print(f"π Generating {num_images} matrix images in parallel...")
|
| 1798 |
+
image_tasks = [generate_single_matrix_image(i) for i in range(num_images)]
|
| 1799 |
+
images = await asyncio.gather(*image_tasks)
|
| 1800 |
+
else:
|
| 1801 |
+
# Single image - no need for parallelization
|
| 1802 |
+
images = [await generate_single_matrix_image(0)]
|
| 1803 |
|
| 1804 |
# Generate unique ID
|
| 1805 |
ad_id = str(uuid.uuid4())
|
|
|
|
| 1945 |
ads_knowledge = ads_future.result()
|
| 1946 |
|
| 1947 |
# Step 3: Creative Director
|
| 1948 |
+
print(f"π¨ Step 3: Creating {num_strategies} creative strategy/strategies...")
|
| 1949 |
+
print(f"π Parameters: num_strategies={num_strategies}, num_images={num_images}")
|
| 1950 |
creative_strategies = third_flow_service.creative_director(
|
| 1951 |
researcher_output=researcher_output,
|
| 1952 |
book_knowledge=book_knowledge,
|
|
|
|
| 1960 |
if not creative_strategies:
|
| 1961 |
raise ValueError("Creative director returned no strategies")
|
| 1962 |
|
| 1963 |
+
# Limit to requested number of strategies (in case LLM returns more)
|
| 1964 |
+
creative_strategies = creative_strategies[:num_strategies]
|
| 1965 |
+
print(f"π Using {len(creative_strategies)} strategy/strategies (requested: {num_strategies})")
|
| 1966 |
+
|
| 1967 |
# Step 4: Process strategies in parallel (designer + copywriter)
|
| 1968 |
print(f"β‘ Step 4: Processing {len(creative_strategies)} strategies in parallel...")
|
| 1969 |
from concurrent.futures import ThreadPoolExecutor as TPE
|
|
|
|
| 1975 |
))
|
| 1976 |
|
| 1977 |
# Step 5: Generate images for each strategy
|
| 1978 |
+
# Ensure we only process the requested number of strategies
|
| 1979 |
+
strategies_to_process = min(len(strategy_results), num_strategies)
|
| 1980 |
+
print(f"πΌοΈ Step 5: Generating {num_images} image(s) per strategy for {strategies_to_process} strategy/strategies...")
|
| 1981 |
all_results = []
|
| 1982 |
|
| 1983 |
+
for idx, (prompt, title, body, description) in enumerate(strategy_results[:strategies_to_process]):
|
| 1984 |
+
if idx >= num_strategies:
|
| 1985 |
+
print(f"β οΈ Stopping at strategy {idx + 1} (requested {num_strategies} strategies)")
|
| 1986 |
+
break
|
| 1987 |
+
|
| 1988 |
if not prompt:
|
| 1989 |
print(f"Warning: Strategy {idx + 1} has no prompt, skipping...")
|
| 1990 |
continue
|
| 1991 |
|
| 1992 |
+
# Generate images for this strategy (respecting num_images parameter) - PARALLELIZED
|
| 1993 |
+
print(f" Generating {num_images} image(s) for strategy {idx + 1}/{len(creative_strategies)}...")
|
| 1994 |
+
|
| 1995 |
+
# Variation modifiers to ensure different images
|
| 1996 |
+
variation_modifiers = [
|
| 1997 |
+
"different camera angle, unique composition",
|
| 1998 |
+
"alternative perspective, varied lighting",
|
| 1999 |
+
"distinct framing, different visual style",
|
| 2000 |
+
"unique viewpoint, varied composition",
|
| 2001 |
+
"alternative angle, different mood",
|
| 2002 |
+
]
|
| 2003 |
+
|
| 2004 |
+
async def generate_single_extensive_image(img_idx: int):
|
| 2005 |
+
"""Helper function to generate a single extensive image with all processing."""
|
| 2006 |
try:
|
| 2007 |
+
# Refine prompt and add variation for each image
|
| 2008 |
+
base_refined_prompt = self._refine_image_prompt(prompt)
|
| 2009 |
+
|
| 2010 |
+
# Add variation modifier if generating multiple images
|
| 2011 |
+
if num_images > 1:
|
| 2012 |
+
variation = variation_modifiers[img_idx % len(variation_modifiers)]
|
| 2013 |
+
refined_prompt = f"{base_refined_prompt}, {variation}"
|
| 2014 |
+
else:
|
| 2015 |
+
refined_prompt = base_refined_prompt
|
| 2016 |
|
| 2017 |
+
# Generate unique seed for each image
|
| 2018 |
+
actual_seed = random.randint(1, 2147483647)
|
| 2019 |
+
|
| 2020 |
+
# Generate image (async)
|
| 2021 |
image_bytes, model_used, image_url = await image_service.generate(
|
| 2022 |
prompt=refined_prompt,
|
| 2023 |
+
seed=actual_seed,
|
| 2024 |
model_key=image_model,
|
| 2025 |
)
|
| 2026 |
|
| 2027 |
if not image_bytes:
|
| 2028 |
print(f"Warning: Failed to generate image {img_idx + 1} for strategy {idx + 1}")
|
| 2029 |
+
return {
|
|
|
|
| 2030 |
"error": "Image generation returned no image data",
|
| 2031 |
+
"seed": actual_seed,
|
| 2032 |
"image_prompt": refined_prompt,
|
| 2033 |
+
}
|
|
|
|
| 2034 |
|
| 2035 |
+
# Generate filename with unique identifier
|
| 2036 |
+
filename = f"{niche}_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}_{img_idx}.png"
|
| 2037 |
|
| 2038 |
+
# Upload to R2 in parallel thread (sync operation)
|
| 2039 |
r2_url = None
|
| 2040 |
if r2_storage_available:
|
| 2041 |
try:
|
| 2042 |
r2_storage = get_r2_storage()
|
| 2043 |
if r2_storage:
|
| 2044 |
+
# Run R2 upload in thread pool (sync operation)
|
| 2045 |
+
loop = asyncio.get_event_loop()
|
| 2046 |
+
r2_url = await loop.run_in_executor(
|
| 2047 |
+
None,
|
| 2048 |
+
lambda: r2_storage.upload_image(
|
| 2049 |
+
image_bytes, filename=filename, niche=niche
|
| 2050 |
+
)
|
| 2051 |
)
|
| 2052 |
+
print(f"Extensive image {img_idx + 1} uploaded to R2: {r2_url}")
|
| 2053 |
except Exception as r2_e:
|
| 2054 |
print(f"Warning: Failed to upload image to R2: {r2_e}")
|
| 2055 |
|
|
|
|
| 2059 |
# Use R2 URL if available, otherwise use Replicate URL
|
| 2060 |
final_image_url = r2_url or image_url
|
| 2061 |
|
| 2062 |
+
result = {
|
|
|
|
|
|
|
| 2063 |
"filename": filename,
|
| 2064 |
"filepath": filepath,
|
| 2065 |
+
"image_url": final_image_url,
|
| 2066 |
"r2_url": r2_url,
|
| 2067 |
"model_used": model_used,
|
| 2068 |
+
"seed": actual_seed,
|
| 2069 |
"image_prompt": refined_prompt,
|
| 2070 |
+
}
|
| 2071 |
+
print(f" β Image {img_idx + 1}/{num_images} generated with seed {actual_seed}")
|
| 2072 |
+
return result
|
| 2073 |
except Exception as e:
|
| 2074 |
print(f"Error generating image {img_idx + 1} for strategy {idx + 1}: {e}")
|
| 2075 |
+
error_seed = random.randint(1, 2147483647)
|
| 2076 |
+
return {
|
| 2077 |
"error": str(e),
|
| 2078 |
+
"seed": error_seed,
|
| 2079 |
"image_prompt": refined_prompt if 'refined_prompt' in locals() else None,
|
| 2080 |
+
}
|
| 2081 |
+
|
| 2082 |
+
# Generate all images in parallel using asyncio.gather
|
| 2083 |
+
if num_images > 1:
|
| 2084 |
+
print(f" π Generating {num_images} images in parallel for strategy {idx + 1}...")
|
| 2085 |
+
image_tasks = [generate_single_extensive_image(img_idx) for img_idx in range(num_images)]
|
| 2086 |
+
generated_images = await asyncio.gather(*image_tasks)
|
| 2087 |
+
else:
|
| 2088 |
+
# Single image - no need for parallelization
|
| 2089 |
+
generated_images = [await generate_single_extensive_image(0)]
|
| 2090 |
|
| 2091 |
if not generated_images:
|
| 2092 |
print(f"Warning: No images generated for strategy {idx + 1}, skipping...")
|
|
|
|
| 2153 |
"cta": cta,
|
| 2154 |
"psychological_angle": strategy.phsychologyTrigger or "",
|
| 2155 |
"why_it_works": f"Angle: {strategy.angle}, Concept: {strategy.concept}",
|
| 2156 |
+
"images": generated_images, # Include ALL images for this strategy
|
| 2157 |
"metadata": {
|
| 2158 |
"strategies_used": [strategy.phsychologyTrigger] if strategy.phsychologyTrigger else ["extensive"],
|
| 2159 |
"creative_direction": f"Extensive: {strategy.angle} Γ {strategy.concept}",
|
|
|
|
| 2166 |
"visual_styles": [strategy.concept] if strategy.concept else [],
|
| 2167 |
},
|
| 2168 |
})
|
| 2169 |
+
print(f"β Strategy {idx + 1} completed with {len(generated_images)} image(s)")
|
| 2170 |
|
| 2171 |
# Return first result (or all if needed)
|
| 2172 |
if all_results:
|
| 2173 |
+
result = all_results[0]
|
| 2174 |
+
print(f"π€ Returning result with {len(result.get('images', []))} image(s)")
|
| 2175 |
+
return result # Return first strategy result with all its images
|
| 2176 |
else:
|
| 2177 |
raise ValueError("No ads generated from extensive")
|
| 2178 |
|
|
|
|
| 2310 |
|
| 2311 |
{niche_guidance}
|
| 2312 |
|
| 2313 |
+
=== CRITICAL: PEOPLE AND FACES ===
|
| 2314 |
+
If this image includes people or faces, they MUST look like real, original people with:
|
| 2315 |
+
- Photorealistic faces with natural skin texture, visible pores, and realistic skin imperfections
|
| 2316 |
+
- Natural facial asymmetry (no perfectly symmetrical faces)
|
| 2317 |
+
- Unique, individual facial features (not generic or model-like)
|
| 2318 |
+
- Natural expressions with authentic micro-expressions
|
| 2319 |
+
- Realistic skin tones with natural variations and undertones
|
| 2320 |
+
- Natural hair texture with individual strands
|
| 2321 |
+
- Faces that look like real photographs of real people, NOT AI-generated portraits
|
| 2322 |
+
- Avoid any faces that look synthetic, fake, or obviously computer-generated
|
| 2323 |
+
|
| 2324 |
=== LAYOUT ===
|
| 2325 |
- Text zone (bottom 25%): "{headline}"
|
| 2326 |
- Visual zone (top 75%): Scene following {concept.get('name')} concept
|
|
|
|
| 2331 |
- Text that is too small to read
|
| 2332 |
- Generic stock photo look
|
| 2333 |
- Watermarks, logos
|
| 2334 |
+
- AI-generated faces, synthetic faces, or fake-looking faces
|
| 2335 |
+
- Overly smooth or plastic-looking skin
|
| 2336 |
+
- Perfectly symmetrical faces
|
| 2337 |
+
- Generic or model-like facial features
|
| 2338 |
+
- Uncanny valley faces
|
| 2339 |
+
- Faces that look like they came from a face generator
|
| 2340 |
+
- Overly perfect, flawless skin
|
| 2341 |
+
- Cartoon-like or stylized faces
|
| 2342 |
|
| 2343 |
Create a scroll-stopping ad image with "{headline}" prominently displayed."""
|
| 2344 |
|
|
|
|
| 2355 |
username: Optional[str] = None, # Username of the user generating the ads
|
| 2356 |
) -> List[Dict[str, Any]]:
|
| 2357 |
"""
|
| 2358 |
+
Generate multiple ad creatives - PARALLELIZED.
|
| 2359 |
Uses variety: 50% standard generation, 50% matrix generation.
|
| 2360 |
+
Uses semaphore to limit concurrent operations and prevent resource exhaustion.
|
| 2361 |
|
| 2362 |
Args:
|
| 2363 |
niche: Target niche
|
|
|
|
| 2367 |
Returns:
|
| 2368 |
List of ad results (all normalized to GenerateResponse format)
|
| 2369 |
"""
|
| 2370 |
+
# Use semaphore to limit concurrent ad generation (max 3 at a time to avoid overwhelming APIs)
|
| 2371 |
+
semaphore = asyncio.Semaphore(3)
|
| 2372 |
|
| 2373 |
+
async def generate_single_ad(ad_index: int):
|
| 2374 |
+
"""Helper function to generate a single ad with semaphore control."""
|
| 2375 |
+
async with semaphore:
|
| 2376 |
+
try:
|
| 2377 |
+
# Use variety: 50% standard, 50% matrix (ensures all resources used)
|
| 2378 |
+
use_matrix = random.random() < 0.5 # 50% chance to use matrix
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2379 |
|
| 2380 |
+
if use_matrix:
|
| 2381 |
+
# Use angle Γ concept matrix approach
|
| 2382 |
+
result = await self.generate_ad_with_matrix(
|
| 2383 |
+
niche=niche,
|
| 2384 |
+
num_images=images_per_ad,
|
| 2385 |
+
image_model=image_model,
|
| 2386 |
+
username=username, # Pass username
|
| 2387 |
+
)
|
| 2388 |
+
# Normalize matrix result to standard format for batch response
|
| 2389 |
+
# Extract matrix info and convert metadata
|
| 2390 |
+
matrix_info = result.get("matrix", {})
|
| 2391 |
+
angle = matrix_info.get("angle", {})
|
| 2392 |
+
concept = matrix_info.get("concept", {})
|
| 2393 |
+
|
| 2394 |
+
# Convert to standard AdMetadata format
|
| 2395 |
+
result["metadata"] = {
|
| 2396 |
+
"strategies_used": [angle.get("trigger", "emotional_trigger")],
|
| 2397 |
+
"creative_direction": f"Angle: {angle.get('name', '')}, Concept: {concept.get('name', '')}",
|
| 2398 |
+
"visual_mood": concept.get("visual", "").split(".")[0] if concept.get("visual") else "authentic",
|
| 2399 |
+
"framework": None,
|
| 2400 |
+
"camera_angle": None,
|
| 2401 |
+
"lighting": None,
|
| 2402 |
+
"composition": None,
|
| 2403 |
+
"hooks_inspiration": [angle.get("example", "")] if angle.get("example") else [],
|
| 2404 |
+
"visual_styles": [concept.get("structure", "")] if concept.get("structure") else [],
|
| 2405 |
+
}
|
| 2406 |
+
# Remove matrix field as it's not in GenerateResponse
|
| 2407 |
+
result.pop("matrix", None)
|
| 2408 |
+
else:
|
| 2409 |
+
# Use standard framework-based approach
|
| 2410 |
+
result = await self.generate_ad(
|
| 2411 |
+
niche=niche,
|
| 2412 |
+
num_images=images_per_ad,
|
| 2413 |
+
image_model=image_model,
|
| 2414 |
+
username=username, # Pass username
|
| 2415 |
+
)
|
| 2416 |
+
return result
|
| 2417 |
+
except Exception as e:
|
| 2418 |
+
return {
|
| 2419 |
+
"error": str(e),
|
| 2420 |
+
"index": ad_index,
|
| 2421 |
}
|
| 2422 |
+
|
| 2423 |
+
# Generate all ads in parallel using asyncio.gather with semaphore control
|
| 2424 |
+
print(f"π Generating {count} ads in parallel (max 3 concurrent)...")
|
| 2425 |
+
ad_tasks = [generate_single_ad(i) for i in range(count)]
|
| 2426 |
+
results = await asyncio.gather(*ad_tasks)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2427 |
|
| 2428 |
return results
|
| 2429 |
|
services/third_flow.py
CHANGED
|
@@ -476,7 +476,18 @@ class ThirdFlowService:
|
|
| 476 |
Affiliate marketing is a performance-based model where you promote someone else's product or service and earn a commission for each qualified action (click, lead, or sale).
|
| 477 |
In affiliate marketing 'Low-production, realistic images often outperform studio creatives' runs most.
|
| 478 |
|
| 479 |
-
For nano banana image model here's structure for the prompt: [The Hook] + [The Subject] + [The Context/Setting] + [The Technical Polish]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
}
|
| 481 |
]
|
| 482 |
},
|
|
|
|
| 476 |
Affiliate marketing is a performance-based model where you promote someone else's product or service and earn a commission for each qualified action (click, lead, or sale).
|
| 477 |
In affiliate marketing 'Low-production, realistic images often outperform studio creatives' runs most.
|
| 478 |
|
| 479 |
+
For nano banana image model here's structure for the prompt: [The Hook] + [The Subject] + [The Context/Setting] + [The Technical Polish]
|
| 480 |
+
|
| 481 |
+
CRITICAL: If the image includes people or faces, ensure they look like real, original people with:
|
| 482 |
+
- Photorealistic faces with natural skin texture, visible pores, and realistic skin imperfections
|
| 483 |
+
- Natural facial asymmetry (no perfectly symmetrical faces)
|
| 484 |
+
- Unique, individual facial features (not generic or model-like)
|
| 485 |
+
- Natural expressions with authentic micro-expressions
|
| 486 |
+
- Realistic skin tones with natural variations
|
| 487 |
+
- Faces that look like real photographs of real people, NOT AI-generated portraits
|
| 488 |
+
- Avoid any faces that look synthetic, fake, or obviously computer-generated
|
| 489 |
+
- Avoid overly smooth or plastic-looking skin
|
| 490 |
+
- Avoid perfectly symmetrical faces or generic model-like features"""
|
| 491 |
}
|
| 492 |
]
|
| 493 |
},
|