sushilideaclan01 commited on
Commit
a162882
·
1 Parent(s): 68556d7

Implement motivator generation feature for ad creation

Browse files

- Added a new MotivatorGenerateRequest model and corresponding API endpoint to generate emotional motivators based on user-selected niche, angle, and concept.
- Integrated motivator generation into the ad creation workflow, allowing users to select motivators for enhanced ad targeting.
- Updated the frontend to support motivator generation, including state management for selecting and displaying generated motivators.
- Enhanced the generator service to incorporate motivators into ad copy and image prompts, ensuring a cohesive user experience.
- Refactored API types and endpoints to accommodate the new motivator generation functionality, maintaining type safety and consistency across the application.

frontend/app/generate/page.tsx CHANGED
@@ -12,11 +12,11 @@ import { ConceptSelector } from "@/components/matrix/ConceptSelector";
12
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
13
  import { Button } from "@/components/ui/Button";
14
  import { ProgressBar } from "@/components/ui/ProgressBar";
15
- import { generateAd, generateMatrixAd, generateBatch, generateExtensiveAd, refineCustomAngleOrConcept } from "@/lib/api/endpoints";
16
  import { useGenerationStore } from "@/store/generationStore";
17
  import { useMatrixStore } from "@/store/matrixStore";
18
  import { toast } from "react-hot-toast";
19
- import { Sparkles, Zap, Layers, Package, Workflow, Wand2, Check, Loader2 } from "lucide-react";
20
  import { IMAGE_MODELS } from "@/lib/constants/models";
21
  import type { Niche, GenerateResponse, AngleInfo, ConceptInfo } from "@/types/api";
22
 
@@ -35,6 +35,17 @@ export default function GeneratePage() {
35
  const [batchCount, setBatchCount] = useState(0);
36
  const [batchImagesPerAd, setBatchImagesPerAd] = useState(1);
37
 
 
 
 
 
 
 
 
 
 
 
 
38
  const {
39
  currentGeneration,
40
  progress,
@@ -163,6 +174,40 @@ export default function GeneratePage() {
163
  const effectiveAngle = useCustomAngle ? customAngleRefined : selectedAngle;
164
  const effectiveConcept = useCustomConcept ? customConceptRefined : selectedConcept;
165
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  const handleStandardGenerate = async (data: { niche: Niche; num_images: number; image_model?: string | null; target_audience?: string; offer?: string }) => {
167
  reset();
168
  setIsGenerating(true);
@@ -306,19 +351,24 @@ export default function GeneratePage() {
306
  try {
307
  // Generate batch using matrix method
308
  const results = await Promise.all(
309
- Array.from({ length: numImages }, (_, i) =>
310
- generateMatrixAd({
 
 
 
 
311
  niche,
312
  angle_key: useCustomAngle ? "custom" : effectiveAngle.key,
313
  concept_key: useCustomConcept ? "custom" : effectiveConcept.key,
314
  custom_angle: useCustomAngle ? JSON.stringify(effectiveAngle) : null,
315
  custom_concept: useCustomConcept ? JSON.stringify(effectiveConcept) : null,
316
- num_images: 1, // Each ad gets 1 image
317
  image_model: imageModel,
318
  target_audience: targetAudience || undefined,
319
  offer: offer || undefined,
320
- })
321
- )
 
322
  );
323
 
324
  setBatchResults(results as unknown as GenerateResponse[]);
@@ -358,16 +408,19 @@ export default function GeneratePage() {
358
  });
359
 
360
  try {
 
 
361
  const result = await generateMatrixAd({
362
  niche,
363
  angle_key: useCustomAngle ? "custom" : effectiveAngle.key,
364
  concept_key: useCustomConcept ? "custom" : effectiveConcept.key,
365
  custom_angle: useCustomAngle ? JSON.stringify(effectiveAngle) : null,
366
  custom_concept: useCustomConcept ? JSON.stringify(effectiveConcept) : null,
367
- num_images: 1, // Single image
368
  image_model: imageModel,
369
  target_audience: targetAudience || undefined,
370
  offer: offer || undefined,
 
371
  });
372
 
373
  setCurrentGeneration(result);
@@ -1014,6 +1067,79 @@ export default function GeneratePage() {
1014
  </Card>
1015
  )}
1016
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1017
  {/* Selection Summary */}
1018
  <Card variant="glass" className="border-2 border-blue-100 bg-gradient-to-r from-blue-50/50 to-cyan-50/50">
1019
  <CardContent className="pt-4 pb-4">
@@ -1041,6 +1167,23 @@ export default function GeneratePage() {
1041
  <span className="text-gray-400 italic">Not selected</span>
1042
  )}
1043
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1044
  </div>
1045
  </CardContent>
1046
  </Card>
 
12
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
13
  import { Button } from "@/components/ui/Button";
14
  import { ProgressBar } from "@/components/ui/ProgressBar";
15
+ import { generateAd, generateMatrixAd, generateBatch, generateExtensiveAd, refineCustomAngleOrConcept, generateMotivators } from "@/lib/api/endpoints";
16
  import { useGenerationStore } from "@/store/generationStore";
17
  import { useMatrixStore } from "@/store/matrixStore";
18
  import { toast } from "react-hot-toast";
19
+ import { Sparkles, Zap, Layers, Package, Workflow, Wand2, Check, Loader2, Brain } from "lucide-react";
20
  import { IMAGE_MODELS } from "@/lib/constants/models";
21
  import type { Niche, GenerateResponse, AngleInfo, ConceptInfo } from "@/types/api";
22
 
 
35
  const [batchCount, setBatchCount] = useState(0);
36
  const [batchImagesPerAd, setBatchImagesPerAd] = useState(1);
37
 
38
+ // Motivators (Matrix mode): generate from angle+concept, user selects one or more for ad generation
39
+ const [motivators, setMotivators] = useState<string[]>([]);
40
+ const [selectedMotivators, setSelectedMotivators] = useState<string[]>([]);
41
+ const [isGeneratingMotivators, setIsGeneratingMotivators] = useState(false);
42
+
43
+ const toggleMotivator = (m: string) => {
44
+ setSelectedMotivators((prev) =>
45
+ prev.includes(m) ? prev.filter((x) => x !== m) : [...prev, m]
46
+ );
47
+ };
48
+
49
  const {
50
  currentGeneration,
51
  progress,
 
174
  const effectiveAngle = useCustomAngle ? customAngleRefined : selectedAngle;
175
  const effectiveConcept = useCustomConcept ? customConceptRefined : selectedConcept;
176
 
177
+ const handleGenerateMotivators = async () => {
178
+ if (!effectiveAngle || !effectiveConcept) {
179
+ toast.error("Select an angle and a concept first");
180
+ return;
181
+ }
182
+ setIsGeneratingMotivators(true);
183
+ setMotivators([]);
184
+ setSelectedMotivators([]);
185
+ try {
186
+ const res = await generateMotivators({
187
+ niche,
188
+ angle: {
189
+ name: effectiveAngle.name,
190
+ trigger: effectiveAngle.trigger,
191
+ example: (effectiveAngle as AngleInfo).example ?? effectiveAngle.name,
192
+ },
193
+ concept: {
194
+ name: effectiveConcept.name,
195
+ structure: effectiveConcept.structure,
196
+ visual: effectiveConcept.visual,
197
+ },
198
+ target_audience: targetAudience || undefined,
199
+ offer: offer || undefined,
200
+ count: 6,
201
+ });
202
+ setMotivators(res.motivators);
203
+ toast.success(`Generated ${res.motivators.length} motivators`);
204
+ } catch (e: unknown) {
205
+ toast.error(e instanceof Error ? e.message : "Failed to generate motivators");
206
+ } finally {
207
+ setIsGeneratingMotivators(false);
208
+ }
209
+ };
210
+
211
  const handleStandardGenerate = async (data: { niche: Niche; num_images: number; image_model?: string | null; target_audience?: string; offer?: string }) => {
212
  reset();
213
  setIsGenerating(true);
 
351
  try {
352
  // Generate batch using matrix method
353
  const results = await Promise.all(
354
+ Array.from({ length: numImages }, (_, i) => {
355
+ const motivator =
356
+ selectedMotivators.length > 0
357
+ ? selectedMotivators[i % selectedMotivators.length]
358
+ : undefined;
359
+ return generateMatrixAd({
360
  niche,
361
  angle_key: useCustomAngle ? "custom" : effectiveAngle.key,
362
  concept_key: useCustomConcept ? "custom" : effectiveConcept.key,
363
  custom_angle: useCustomAngle ? JSON.stringify(effectiveAngle) : null,
364
  custom_concept: useCustomConcept ? JSON.stringify(effectiveConcept) : null,
365
+ num_images: 1,
366
  image_model: imageModel,
367
  target_audience: targetAudience || undefined,
368
  offer: offer || undefined,
369
+ core_motivator: motivator,
370
+ });
371
+ })
372
  );
373
 
374
  setBatchResults(results as unknown as GenerateResponse[]);
 
408
  });
409
 
410
  try {
411
+ const motivator =
412
+ selectedMotivators.length > 0 ? selectedMotivators[0] : undefined;
413
  const result = await generateMatrixAd({
414
  niche,
415
  angle_key: useCustomAngle ? "custom" : effectiveAngle.key,
416
  concept_key: useCustomConcept ? "custom" : effectiveConcept.key,
417
  custom_angle: useCustomAngle ? JSON.stringify(effectiveAngle) : null,
418
  custom_concept: useCustomConcept ? JSON.stringify(effectiveConcept) : null,
419
+ num_images: 1,
420
  image_model: imageModel,
421
  target_audience: targetAudience || undefined,
422
  offer: offer || undefined,
423
+ core_motivator: motivator,
424
  });
425
 
426
  setCurrentGeneration(result);
 
1067
  </Card>
1068
  )}
1069
 
1070
+ {/* Motivators: generate from angle+concept, select one for ad generation */}
1071
+ <Card variant="glass" className="border-2 border-purple-200 bg-gradient-to-br from-purple-50/50 to-pink-50/50">
1072
+ <CardHeader>
1073
+ <CardTitle className="flex items-center gap-2 text-base">
1074
+ <Brain className="h-5 w-5 text-purple-600" />
1075
+ Motivators
1076
+ </CardTitle>
1077
+ </CardHeader>
1078
+ <CardContent className="space-y-4">
1079
+ <p className="text-xs text-gray-600">
1080
+ Generate emotional motivators from your angle + concept. Pick one or more—each will be used for a separate image when generating multiple.
1081
+ </p>
1082
+ <Button
1083
+ variant="secondary"
1084
+ size="sm"
1085
+ className="w-full"
1086
+ onClick={handleGenerateMotivators}
1087
+ disabled={!effectiveAngle || !effectiveConcept || isGeneratingMotivators}
1088
+ >
1089
+ {isGeneratingMotivators ? (
1090
+ <>
1091
+ <Loader2 className="w-4 h-4 animate-spin mr-2" />
1092
+ Generating…
1093
+ </>
1094
+ ) : (
1095
+ <>
1096
+ <Brain className="w-4 h-4 mr-2" />
1097
+ Generate Motivators
1098
+ </>
1099
+ )}
1100
+ </Button>
1101
+ {motivators.length > 0 && (
1102
+ <div className="space-y-2">
1103
+ <p className="text-xs font-semibold text-gray-700">
1104
+ Pick one or more for ad generation:
1105
+ </p>
1106
+ <div className="space-y-1.5 max-h-48 overflow-y-auto">
1107
+ {motivators.map((m, i) => {
1108
+ const selected = selectedMotivators.includes(m);
1109
+ return (
1110
+ <button
1111
+ key={i}
1112
+ type="button"
1113
+ onClick={() => toggleMotivator(m)}
1114
+ className={`w-full text-left px-3 py-2 rounded-lg border-2 text-sm transition-all ${
1115
+ selected
1116
+ ? "border-purple-500 bg-purple-100 text-purple-900"
1117
+ : "border-gray-200 bg-white hover:border-purple-300 hover:bg-purple-50"
1118
+ }`}
1119
+ >
1120
+ <span className="flex items-center gap-2">
1121
+ {selected && (
1122
+ <Check className="w-4 h-4 text-purple-600 flex-shrink-0" />
1123
+ )}
1124
+ &ldquo;{m}&rdquo;
1125
+ </span>
1126
+ </button>
1127
+ );
1128
+ })}
1129
+ </div>
1130
+ {selectedMotivators.length > 0 && (
1131
+ <p className="text-xs text-purple-700 font-medium">
1132
+ Using {selectedMotivators.length} motivator
1133
+ {selectedMotivators.length !== 1 ? "s" : ""} for generation
1134
+ {numImages > 1 &&
1135
+ ` (cycled across ${numImages} image${numImages !== 1 ? "s" : ""})`}
1136
+ </p>
1137
+ )}
1138
+ </div>
1139
+ )}
1140
+ </CardContent>
1141
+ </Card>
1142
+
1143
  {/* Selection Summary */}
1144
  <Card variant="glass" className="border-2 border-blue-100 bg-gradient-to-r from-blue-50/50 to-cyan-50/50">
1145
  <CardContent className="pt-4 pb-4">
 
1167
  <span className="text-gray-400 italic">Not selected</span>
1168
  )}
1169
  </div>
1170
+ {selectedMotivators.length > 0 && (
1171
+ <div className="pt-1 border-t border-blue-200 mt-2 space-y-1">
1172
+ <span className="font-medium text-gray-600 block">
1173
+ Motivator{selectedMotivators.length !== 1 ? "s" : ""}:
1174
+ </span>
1175
+ <ul className="list-disc list-inside space-y-0.5 max-h-24 overflow-y-auto">
1176
+ {selectedMotivators.map((m, i) => (
1177
+ <li
1178
+ key={i}
1179
+ className="text-purple-600 font-medium text-xs"
1180
+ >
1181
+ &ldquo;{m}&rdquo;
1182
+ </li>
1183
+ ))}
1184
+ </ul>
1185
+ </div>
1186
+ )}
1187
  </div>
1188
  </CardContent>
1189
  </Card>
frontend/lib/api/endpoints.ts CHANGED
@@ -24,6 +24,8 @@ import type {
24
  FileUploadResponse,
25
  ModificationMode,
26
  CreativeAnalysisData,
 
 
27
  } from "../../types/api";
28
 
29
  // Health & Info
@@ -73,11 +75,22 @@ export const generateMatrixAd = async (params: {
73
  image_model?: string | null;
74
  target_audience?: string;
75
  offer?: string;
 
76
  }): Promise<MatrixGenerateResponse> => {
77
  const response = await apiClient.post<MatrixGenerateResponse>("/matrix/generate", params);
78
  return response.data;
79
  };
80
 
 
 
 
 
 
 
 
 
 
 
81
  // Extensive Endpoint
82
  export const generateExtensiveAd = async (params: {
83
  niche: Niche;
 
24
  FileUploadResponse,
25
  ModificationMode,
26
  CreativeAnalysisData,
27
+ MotivatorGenerateRequest,
28
+ MotivatorGenerateResponse,
29
  } from "../../types/api";
30
 
31
  // Health & Info
 
75
  image_model?: string | null;
76
  target_audience?: string;
77
  offer?: string;
78
+ core_motivator?: string;
79
  }): Promise<MatrixGenerateResponse> => {
80
  const response = await apiClient.post<MatrixGenerateResponse>("/matrix/generate", params);
81
  return response.data;
82
  };
83
 
84
+ export const generateMotivators = async (
85
+ params: MotivatorGenerateRequest
86
+ ): Promise<MotivatorGenerateResponse> => {
87
+ const response = await apiClient.post<MotivatorGenerateResponse>(
88
+ "/api/motivator/generate",
89
+ { ...params, count: params.count ?? 6 }
90
+ );
91
+ return response.data;
92
+ };
93
+
94
  // Extensive Endpoint
95
  export const generateExtensiveAd = async (params: {
96
  niche: Niche;
frontend/types/api.ts CHANGED
@@ -409,3 +409,16 @@ export interface ModelsListResponse {
409
  models: ImageModel[];
410
  default: string;
411
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  models: ImageModel[];
410
  default: string;
411
  }
412
+
413
+ export interface MotivatorGenerateRequest {
414
+ niche: Niche;
415
+ angle: { name: string; trigger: string; example?: string; [k: string]: unknown };
416
+ concept: { name: string; structure: string; visual: string; [k: string]: unknown };
417
+ target_audience?: string;
418
+ offer?: string;
419
+ count?: number;
420
+ }
421
+
422
+ export interface MotivatorGenerateResponse {
423
+ motivators: string[];
424
+ }
main.py CHANGED
@@ -28,6 +28,7 @@ from services.correction import correction_service
28
  from services.image import image_service
29
  from services.auth import auth_service
30
  from services.auth_dependency import get_current_user
 
31
  from config import settings
32
 
33
  # Configure logging for API
@@ -251,6 +252,10 @@ class MatrixGenerateRequest(BaseModel):
251
  default=None,
252
  description="Optional offer to run (e.g., 'Don't overpay your insurance')"
253
  )
 
 
 
 
254
 
255
 
256
  class RefineCustomRequest(BaseModel):
@@ -298,6 +303,26 @@ class RefineCustomResponse(BaseModel):
298
  error: Optional[str] = None
299
 
300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  class MatrixBatchRequest(BaseModel):
302
  """Request for batch matrix generation."""
303
  niche: Literal["home_insurance", "glp1"] = Field(
@@ -413,6 +438,7 @@ async def api_info():
413
  "GET /matrix/concept/{key}": "Get specific concept details",
414
  "GET /matrix/compatible/{angle_key}": "Get compatible concepts for angle",
415
  "POST /extensive/generate": "Generate ad using extensive (researcher → creative director → designer → copywriter)",
 
416
  "POST /api/correct": "Correct image for spelling mistakes and visual issues (requires image_id)",
417
  "POST /api/regenerate": "Regenerate image with optional model selection (requires image_id)",
418
  "GET /api/models": "List all available image generation models",
@@ -1433,7 +1459,8 @@ async def generate_with_matrix(
1433
  custom_concept=request.custom_concept,
1434
  num_images=request.num_images,
1435
  image_model=request.image_model,
1436
- username=username, # Pass current user
 
1437
  )
1438
  return result
1439
  except Exception as e:
@@ -1634,6 +1661,35 @@ async def refine_custom_angle_or_concept(request: RefineCustomRequest):
1634
  }
1635
 
1636
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1637
  # =============================================================================
1638
  # EXTENSIVE ENDPOINTS
1639
  # =============================================================================
 
28
  from services.image import image_service
29
  from services.auth import auth_service
30
  from services.auth_dependency import get_current_user
31
+ from services.motivator import generate_motivators as motivator_generate
32
  from config import settings
33
 
34
  # Configure logging for API
 
252
  default=None,
253
  description="Optional offer to run (e.g., 'Don't overpay your insurance')"
254
  )
255
+ core_motivator: Optional[str] = Field(
256
+ default=None,
257
+ description="Optional motivator selected by user to guide ad generation"
258
+ )
259
 
260
 
261
  class RefineCustomRequest(BaseModel):
 
303
  error: Optional[str] = None
304
 
305
 
306
+ # Motivator generation (angle + concept context)
307
+ class MotivatorGenerateRequest(BaseModel):
308
+ """Request to generate motivators from niche + angle + concept."""
309
+ niche: Literal["home_insurance", "glp1"] = Field(description="Target niche")
310
+ angle: Dict[str, Any] = Field(
311
+ description="Angle context: name, trigger, example (and optional key, category)"
312
+ )
313
+ concept: Dict[str, Any] = Field(
314
+ description="Concept context: name, structure, visual (and optional key, category)"
315
+ )
316
+ target_audience: Optional[str] = Field(default=None, description="Optional target audience")
317
+ offer: Optional[str] = Field(default=None, description="Optional offer")
318
+ count: int = Field(default=6, ge=3, le=10, description="Number of motivators to generate")
319
+
320
+
321
+ class MotivatorGenerateResponse(BaseModel):
322
+ """Response with generated motivators."""
323
+ motivators: List[str]
324
+
325
+
326
  class MatrixBatchRequest(BaseModel):
327
  """Request for batch matrix generation."""
328
  niche: Literal["home_insurance", "glp1"] = Field(
 
438
  "GET /matrix/concept/{key}": "Get specific concept details",
439
  "GET /matrix/compatible/{angle_key}": "Get compatible concepts for angle",
440
  "POST /extensive/generate": "Generate ad using extensive (researcher → creative director → designer → copywriter)",
441
+ "POST /api/motivator/generate": "Generate motivators from niche + angle + concept (Matrix mode)",
442
  "POST /api/correct": "Correct image for spelling mistakes and visual issues (requires image_id)",
443
  "POST /api/regenerate": "Regenerate image with optional model selection (requires image_id)",
444
  "GET /api/models": "List all available image generation models",
 
1459
  custom_concept=request.custom_concept,
1460
  num_images=request.num_images,
1461
  image_model=request.image_model,
1462
+ username=username,
1463
+ core_motivator=request.core_motivator,
1464
  )
1465
  return result
1466
  except Exception as e:
 
1661
  }
1662
 
1663
 
1664
+ # =============================================================================
1665
+ # MOTIVATOR ENDPOINTS
1666
+ # =============================================================================
1667
+
1668
+ @app.post("/api/motivator/generate", response_model=MotivatorGenerateResponse)
1669
+ async def motivator_generate_endpoint(
1670
+ request: MotivatorGenerateRequest,
1671
+ username: str = Depends(get_current_user),
1672
+ ):
1673
+ """
1674
+ Generate multiple motivators from niche + angle + concept context.
1675
+
1676
+ Used in Matrix mode: user selects angle and concept, then generates motivators
1677
+ to review and pick one for ad generation.
1678
+ """
1679
+ try:
1680
+ motivators = await motivator_generate(
1681
+ niche=request.niche,
1682
+ angle=request.angle,
1683
+ concept=request.concept,
1684
+ target_audience=request.target_audience,
1685
+ offer=request.offer,
1686
+ count=request.count,
1687
+ )
1688
+ return MotivatorGenerateResponse(motivators=motivators)
1689
+ except Exception as e:
1690
+ raise HTTPException(status_code=500, detail=str(e))
1691
+
1692
+
1693
  # =============================================================================
1694
  # EXTENSIVE ENDPOINTS
1695
  # =============================================================================
services/generator.py CHANGED
@@ -1691,7 +1691,8 @@ CRITICAL REQUIREMENTS:
1691
  custom_concept: Optional[str] = None,
1692
  num_images: int = 1,
1693
  image_model: Optional[str] = None,
1694
- username: Optional[str] = None, # Username of the user generating the ad
 
1695
  ) -> Dict[str, Any]:
1696
  """
1697
  Generate ad using angle × concept matrix approach.
@@ -1800,6 +1801,7 @@ CONCEPT: {concept.get('name', 'Custom Concept')}
1800
  angle=angle,
1801
  concept=concept,
1802
  niche_data=niche_data,
 
1803
  )
1804
 
1805
  # Generate ad copy
@@ -1839,6 +1841,7 @@ CONCEPT: {concept.get('name', 'Custom Concept')}
1839
  angle=angle,
1840
  concept=concept,
1841
  ad_copy=ad_copy,
 
1842
  )
1843
 
1844
  # Store the refined prompt for database saving (this is the final prompt sent to image service)
@@ -2471,6 +2474,7 @@ Return JSON:
2471
  angle: Dict[str, Any],
2472
  concept: Dict[str, Any],
2473
  niche_data: Dict[str, Any],
 
2474
  ) -> str:
2475
  """Build ad copy prompt using angle + concept framework."""
2476
 
@@ -2499,8 +2503,16 @@ DECISION: Include prices/numbers only if they enhance believability and fit the
2499
  Use oddly specific amounts (e.g., "$97.33" not "$100") when including prices."""
2500
  # Note: AI decides whether to use the numbers based on format and strategy
2501
 
2502
- return f"""You are an elite direct-response copywriter creating a Facebook ad.
 
 
 
 
2503
 
 
 
 
 
2504
  === ANGLE × CONCEPT FRAMEWORK ===
2505
 
2506
  ANGLE: {angle.get('name')}
@@ -2544,13 +2556,29 @@ Generate the ad now. Be bold, be specific, trigger {angle.get('trigger')}."""
2544
  angle: Dict[str, Any],
2545
  concept: Dict[str, Any],
2546
  ad_copy: Dict[str, Any],
 
2547
  ) -> str:
2548
  """Build image prompt using concept's visual guidance."""
2549
-
2550
  headline = ad_copy.get("headline", "")
 
 
 
2551
  image_brief = ad_copy.get("image_brief", "")
2552
 
2553
- # Text styling based on concept
 
 
 
 
 
 
 
 
 
 
 
 
 
2554
  text_styles = [
2555
  "naturally integrated into the scene",
2556
  "as part of a document or sign in the image",
@@ -2573,15 +2601,25 @@ NICHE: GLP-1 / Weight Loss
2573
  - People should be relatable, not fitness models
2574
  - Confidence and lifestyle improvement focus"""
2575
 
2576
- prompt = f"""Create a Facebook ad image with natural, authentic content.
2577
-
2578
- === CAMERA QUALITY ===
2579
- - The image should look like it was shot from a low quality camera
2580
- - Include characteristics of low quality camera: slight grain, reduced sharpness, lower resolution appearance, authentic camera imperfections
2581
- - Should have the authentic feel of a real photo taken with a basic or older camera device
2582
-
2583
- === HEADLINE TEXT (if included, should be part of natural scene) ===
2584
- "{headline}"
 
 
 
 
 
 
 
 
 
 
2585
 
2586
  TEXT REQUIREMENTS (natural integration, NOT overlay):
2587
  - Text should appear as part of the scene (on documents, signs, surfaces)
@@ -2589,7 +2627,20 @@ TEXT REQUIREMENTS (natural integration, NOT overlay):
2589
  - Must be READABLE
2590
  - Spell every word correctly
2591
  - CRITICAL: NO overlay boxes, banners, or decorative elements
2592
- - Text should look like it naturally belongs in the scene
 
 
 
 
 
 
 
 
 
 
 
 
 
2593
 
2594
  === VISUAL CONCEPT: {concept.get('name')} ===
2595
  Structure: {concept.get('structure')}
@@ -2614,9 +2665,7 @@ If this image includes people or faces, they MUST look like real, original peopl
2614
  - Faces that look like real photographs of real people, NOT AI-generated portraits
2615
  - Avoid any faces that look synthetic, fake, or obviously computer-generated
2616
 
2617
- === LAYOUT ===
2618
- - Text zone (bottom 25%): "{headline}"
2619
- - Visual zone (top 75%): Scene following {concept.get('name')} concept
2620
 
2621
  === AVOID ===
2622
  - Missing or misspelled text
@@ -2631,8 +2680,9 @@ If this image includes people or faces, they MUST look like real, original peopl
2631
  - Faces that look like they came from a face generator
2632
  - Overly perfect, flawless skin
2633
  - Cartoon-like or stylized faces
 
2634
 
2635
- Create a scroll-stopping ad image with "{headline}" prominently displayed."""
2636
 
2637
  # Refine and clean the prompt before sending (pass niche for demographic fixes)
2638
  refined_prompt = self._refine_image_prompt(prompt, niche=niche)
 
1691
  custom_concept: Optional[str] = None,
1692
  num_images: int = 1,
1693
  image_model: Optional[str] = None,
1694
+ username: Optional[str] = None,
1695
+ core_motivator: Optional[str] = None,
1696
  ) -> Dict[str, Any]:
1697
  """
1698
  Generate ad using angle × concept matrix approach.
 
1801
  angle=angle,
1802
  concept=concept,
1803
  niche_data=niche_data,
1804
+ core_motivator=core_motivator,
1805
  )
1806
 
1807
  # Generate ad copy
 
1841
  angle=angle,
1842
  concept=concept,
1843
  ad_copy=ad_copy,
1844
+ core_motivator=core_motivator,
1845
  )
1846
 
1847
  # Store the refined prompt for database saving (this is the final prompt sent to image service)
 
2474
  angle: Dict[str, Any],
2475
  concept: Dict[str, Any],
2476
  niche_data: Dict[str, Any],
2477
+ core_motivator: Optional[str] = None,
2478
  ) -> str:
2479
  """Build ad copy prompt using angle + concept framework."""
2480
 
 
2503
  Use oddly specific amounts (e.g., "$97.33" not "$100") when including prices."""
2504
  # Note: AI decides whether to use the numbers based on format and strategy
2505
 
2506
+ motivator_block = ""
2507
+ if core_motivator:
2508
+ motivator_block = f"""
2509
+ === CORE EMOTIONAL MOTIVATOR (USE THIS) ===
2510
+ The user chose this motivator: "{core_motivator}"
2511
 
2512
+ Use it to guide the hook, headline, and primary text. The ad must amplify this motivator.
2513
+ """
2514
+ return f"""You are an elite direct-response copywriter creating a Facebook ad.
2515
+ {motivator_block}
2516
  === ANGLE × CONCEPT FRAMEWORK ===
2517
 
2518
  ANGLE: {angle.get('name')}
 
2556
  angle: Dict[str, Any],
2557
  concept: Dict[str, Any],
2558
  ad_copy: Dict[str, Any],
2559
+ core_motivator: Optional[str] = None,
2560
  ) -> str:
2561
  """Build image prompt using concept's visual guidance."""
 
2562
  headline = ad_copy.get("headline", "")
2563
+ # When user selected a motivator, show it on the image instead of headline (like title)
2564
+ text_on_image = (core_motivator or "").strip() or (headline or "")
2565
+ use_motivator = bool((core_motivator or "").strip())
2566
  image_brief = ad_copy.get("image_brief", "")
2567
 
2568
+ # Natural blend options: motivator/text feels part of the scene, not forced
2569
+ natural_blend_options = [
2570
+ "as a handwritten note or sticky note in the scene",
2571
+ "as text on a phone screen, tablet, or laptop in frame",
2572
+ "as a message in a chat or social feed within the image",
2573
+ "on a sign, whiteboard, or poster that fits the environment",
2574
+ "as a caption or note lying naturally in the scene",
2575
+ "reflected in a mirror or window as part of the environment",
2576
+ "on a document, receipt, or paper in the scene",
2577
+ "as subtle text on a product or package in frame",
2578
+ ]
2579
+ text_blend = random.choice(natural_blend_options)
2580
+
2581
+ # Fallback simpler style when not using motivator
2582
  text_styles = [
2583
  "naturally integrated into the scene",
2584
  "as part of a document or sign in the image",
 
2601
  - People should be relatable, not fitness models
2602
  - Confidence and lifestyle improvement focus"""
2603
 
2604
+ if use_motivator:
2605
+ text_section = f'''=== MOTIVATOR PHRASE (blend naturally into the scene) ===
2606
+ Phrase: "{text_on_image}"
2607
+
2608
+ CRITICAL natural blend only:
2609
+ - Weave this phrase into the scene so it feels organic, not overlaid or forced.
2610
+ - It could appear {text_blend}.
2611
+ - The scene and concept come first; the phrase should feel like a natural part of that world.
2612
+ - No banners, boxes, or decorative overlays. No "ad-like" text placement.
2613
+ - Text must be READABLE and correctly spelled, but never the dominant focal point.
2614
+ - Avoid centering the phrase or making it look pasted on. It should belong in the environment.'''
2615
+ layout_section = f"""=== LAYOUT ===
2616
+ - Prioritize the scene from the brief and the {concept.get('name')} concept.
2617
+ - The phrase "{text_on_image}" may appear anywhere it fits naturally (e.g. on a device, note, sign, or surface in frame).
2618
+ - Do NOT force a dedicated "text zone." Let the composition feel organic."""
2619
+ closing = f'''Create a scroll-stopping, authentic image. The phrase "{text_on_image}" may appear naturally in the scene—never forced or overlay-style.'''
2620
+ else:
2621
+ text_section = f'''=== HEADLINE TEXT (if included, should be part of natural scene) ===
2622
+ "{text_on_image}"
2623
 
2624
  TEXT REQUIREMENTS (natural integration, NOT overlay):
2625
  - Text should appear as part of the scene (on documents, signs, surfaces)
 
2627
  - Must be READABLE
2628
  - Spell every word correctly
2629
  - CRITICAL: NO overlay boxes, banners, or decorative elements
2630
+ - Text should look like it naturally belongs in the scene'''
2631
+ layout_section = f"""=== LAYOUT ===
2632
+ - Text zone (bottom 25%): "{text_on_image}"
2633
+ - Visual zone (top 75%): Scene following {concept.get('name')} concept"""
2634
+ closing = f'''Create a scroll-stopping ad image with "{text_on_image}" prominently displayed.'''
2635
+
2636
+ prompt = f"""Create a Facebook ad image with natural, authentic content.
2637
+
2638
+ === CAMERA QUALITY ===
2639
+ - The image should look like it was shot from a low quality camera
2640
+ - Include characteristics of low quality camera: slight grain, reduced sharpness, lower resolution appearance, authentic camera imperfections
2641
+ - Should have the authentic feel of a real photo taken with a basic or older camera device
2642
+
2643
+ {text_section}
2644
 
2645
  === VISUAL CONCEPT: {concept.get('name')} ===
2646
  Structure: {concept.get('structure')}
 
2665
  - Faces that look like real photographs of real people, NOT AI-generated portraits
2666
  - Avoid any faces that look synthetic, fake, or obviously computer-generated
2667
 
2668
+ {layout_section}
 
 
2669
 
2670
  === AVOID ===
2671
  - Missing or misspelled text
 
2680
  - Faces that look like they came from a face generator
2681
  - Overly perfect, flawless skin
2682
  - Cartoon-like or stylized faces
2683
+ {chr(10) + "- Forced or overlay-style text; the motivator must feel naturally mixed into the scene" if use_motivator else ""}
2684
 
2685
+ {closing}"""
2686
 
2687
  # Refine and clean the prompt before sending (pass niche for demographic fixes)
2688
  refined_prompt = self._refine_image_prompt(prompt, niche=niche)
services/motivator.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Motivator service: generate multiple motivators from niche + angle + concept context.
3
+ Used in Matrix mode: user picks angle/concept, we generate motivators, user selects one for ad generation.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ from typing import Dict, Any, List, Optional
9
+
10
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
11
+
12
+ from services.llm import llm_service
13
+
14
+
15
+ async def generate_motivators(
16
+ niche: str,
17
+ angle: Dict[str, Any],
18
+ concept: Dict[str, Any],
19
+ target_audience: Optional[str] = None,
20
+ offer: Optional[str] = None,
21
+ count: int = 6,
22
+ ) -> List[str]:
23
+ """
24
+ Generate multiple emotional motivators using niche, angle, and concept as context.
25
+
26
+ Args:
27
+ niche: Target niche (e.g. home_insurance, glp1).
28
+ angle: Angle dict with name, trigger, example.
29
+ concept: Concept dict with name, structure, visual.
30
+ target_audience: Optional audience description.
31
+ offer: Optional offer description.
32
+ count: Number of motivators to generate (default 6).
33
+
34
+ Returns:
35
+ List of motivator strings (short, hook-ready, in customer's internal voice).
36
+ """
37
+ angle_name = angle.get("name", "Unknown")
38
+ angle_trigger = angle.get("trigger", "")
39
+ angle_example = angle.get("example", "")
40
+ concept_name = concept.get("name", "Unknown")
41
+ concept_structure = concept.get("structure", "")
42
+ concept_visual = concept.get("visual", "")
43
+
44
+ extra = []
45
+ if target_audience:
46
+ extra.append(f"Target audience: {target_audience}")
47
+ if offer:
48
+ extra.append(f"Offer: {offer}")
49
+ extra_block = "\n".join(extra) if extra else ""
50
+
51
+ system = """You are a marketing strategist who uncovers the deepest emotional motivators behind purchases.
52
+ Output only motivators: short, punchy statements in the customer's internal voice (first-person).
53
+ Each motivator should feel like a hidden truth—resentment, shame, betrayal, avoided identity, or unspoken truth.
54
+ No features, no demographics. Emotional truths only. Keep each to 7–12 words where possible."""
55
+
56
+ prompt = f"""Niche: {niche.replace("_", " ").title()}
57
+
58
+ ANGLE (psychological WHY):
59
+ - Name: {angle_name}
60
+ - Trigger: {angle_trigger}
61
+ - Example: "{angle_example}"
62
+
63
+ CONCEPT (visual HOW):
64
+ - Name: {concept_name}
65
+ - Structure: {concept_structure}
66
+ - Visual: {concept_visual}
67
+ {f"\n{extra_block}" if extra_block else ""}
68
+
69
+ Generate exactly {count} distinct emotional motivators that fit this angle and concept.
70
+ Each motivator = one short statement, customer's internal voice.
71
+ Output as a JSON array of strings only, e.g. ["Motivator 1", "Motivator 2", ...].
72
+ No numbering, no explanations."""
73
+
74
+ raw = await llm_service.generate(
75
+ prompt=prompt,
76
+ system_prompt=system,
77
+ temperature=0.75,
78
+ )
79
+
80
+ motivators = _parse_motivators(raw, count)
81
+ return motivators[:count]
82
+
83
+
84
+ def _parse_motivators(raw: str, max_count: int) -> List[str]:
85
+ """Parse LLM output into a list of motivator strings."""
86
+ import json
87
+ import re
88
+
89
+ s = raw.strip()
90
+ # Strip markdown code blocks
91
+ for prefix in ("```json", "```"):
92
+ if s.lower().startswith(prefix):
93
+ s = s[len(prefix) :].lstrip()
94
+ if s.endswith("```"):
95
+ s = s[: -3].rstrip()
96
+
97
+ try:
98
+ out = json.loads(s)
99
+ if isinstance(out, list):
100
+ return [str(x).strip().strip('"').strip("'") for x in out if x]
101
+ if isinstance(out, dict) and "motivators" in out:
102
+ arr = out["motivators"]
103
+ return [str(x).strip().strip('"').strip("'") for x in arr if x]
104
+ except json.JSONDecodeError:
105
+ pass
106
+
107
+ # Fallback: line-based extraction
108
+ result = []
109
+ for line in s.splitlines():
110
+ line = line.strip()
111
+ if not line:
112
+ continue
113
+ # Remove leading "1.", "- ", "* ", etc.
114
+ line = re.sub(r"^\s*[\d\-*]+\.[\s)]*", "", line).strip()
115
+ line = line.strip('"').strip("'").strip()
116
+ if len(line) > 10:
117
+ result.append(line)
118
+ return result[:max_count]