sushilideaclan01 commited on
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 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={index} className="bg-white rounded-2xl shadow-lg shadow-blue-100/50 overflow-hidden ring-1 ring-blue-100">
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?: number;
71
  image_model?: string | null;
72
- num_strategies?: number;
73
  }): Promise<GenerateResponse> => {
74
- const response = await apiClient.post<GenerateResponse>("/extensive/generate", params);
 
 
 
 
 
 
 
 
 
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
- generated_images = []
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 (this is the final prompt sent to image service)
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 if available, otherwise save locally
1474
  r2_url = None
1475
  if r2_storage_available:
1476
  try:
1477
  r2_storage = get_r2_storage()
1478
  if r2_storage:
1479
- r2_url = r2_storage.upload_image(
1480
- image_bytes=image_bytes,
1481
- filename=filename,
1482
- niche=niche,
 
 
 
 
 
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
- generated_images.append({
1495
  "filename": filename,
1496
  "filepath": filepath,
1497
- "image_url": final_image_url, # R2 URL (preferred) or Replicate URL
1498
- "r2_url": r2_url, # R2 URL if uploaded
1499
  "model_used": model_used,
1500
  "seed": seed,
1501
- "image_prompt": refined_image_prompt, # Store the final prompt
1502
- })
1503
 
1504
  except Exception as e:
1505
- generated_images.append({
1506
  "error": str(e),
1507
  "seed": seed,
1508
- "image_prompt": refined_image_prompt if 'refined_image_prompt' in locals() else None,
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
- images = []
1698
- for i in range(num_images):
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 if available
1716
  r2_url = None
1717
  if r2_storage_available:
1718
  try:
1719
  r2_storage = get_r2_storage()
1720
  if r2_storage:
1721
- r2_url = r2_storage.upload_image(
1722
- image_bytes=image_bytes,
1723
- filename=filename,
1724
- niche=niche,
 
 
 
 
 
1725
  )
1726
- print(f"Image uploaded to R2: {r2_url}")
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
- images.append({
1737
  "filename": filename,
1738
  "filepath": filepath,
1739
- "image_url": final_image_url, # R2 URL (preferred) or Replicate URL
1740
- "r2_url": r2_url, # R2 URL if uploaded
1741
  "model_used": model_used,
1742
  "seed": seed,
1743
- "image_prompt": refined_image_prompt, # Store the final prompt
1744
  "error": None,
1745
- })
1746
  except Exception as e:
1747
- images.append({
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
- print(f"πŸ–ΌοΈ Step 5: Generating images...")
 
 
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
- generated_images = []
1936
- for img_idx in range(num_images):
 
 
 
 
 
 
 
 
 
 
 
1937
  try:
1938
- # Refine prompt
1939
- refined_prompt = self._refine_image_prompt(prompt)
 
 
 
 
 
 
 
1940
 
1941
- # Generate image
 
 
 
1942
  image_bytes, model_used, image_url = await image_service.generate(
1943
  prompt=refined_prompt,
1944
- seed=random.randint(1, 2147483647),
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
- # Add error entry instead of silently skipping
1951
- generated_images.append({
1952
  "error": "Image generation returned no image data",
1953
- "seed": random.randint(1, 2147483647),
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 if available
1962
  r2_url = None
1963
  if r2_storage_available:
1964
  try:
1965
  r2_storage = get_r2_storage()
1966
  if r2_storage:
1967
- r2_url = r2_storage.upload_image(
1968
- image_bytes, filename=filename, niche=niche
 
 
 
 
 
1969
  )
1970
- print(f"Image uploaded to R2: {r2_url}")
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
- # Always add image entry (frontend can use filename for local images)
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, # May be None if R2 and Replicate both failed, but filename will work
1986
  "r2_url": r2_url,
1987
  "model_used": model_used,
1988
- "seed": random.randint(1, 2147483647),
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
- # Add error entry instead of silently skipping (like standard flow)
1994
- generated_images.append({
1995
  "error": str(e),
1996
- "seed": random.randint(1, 2147483647),
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
- return all_results[0] # Return first strategy result
 
 
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
- results = []
 
2257
 
2258
- for i in range(count):
2259
- try:
2260
- # Use variety: 50% standard, 50% matrix (ensures all resources used)
2261
- use_matrix = random.random() < 0.5 # 50% chance to use matrix
2262
-
2263
- if use_matrix:
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
- # Convert to standard AdMetadata format
2278
- result["metadata"] = {
2279
- "strategies_used": [angle.get("trigger", "emotional_trigger")],
2280
- "creative_direction": f"Angle: {angle.get('name', '')}, Concept: {concept.get('name', '')}",
2281
- "visual_mood": concept.get("visual", "").split(".")[0] if concept.get("visual") else "authentic",
2282
- "framework": None,
2283
- "camera_angle": None,
2284
- "lighting": None,
2285
- "composition": None,
2286
- "hooks_inspiration": [angle.get("example", "")] if angle.get("example") else [],
2287
- "visual_styles": [concept.get("structure", "")] if concept.get("structure") else [],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2288
  }
2289
- # Remove matrix field as it's not in GenerateResponse
2290
- result.pop("matrix", None)
2291
- else:
2292
- # Use standard framework-based approach
2293
- result = await self.generate_ad(
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
  },