Hammad712 commited on
Commit
7b6df86
Β·
1 Parent(s): 6f77b72

Added Tone and updated the banner prompt

Browse files
app/ads/budget_routes.py DELETED
@@ -1,29 +0,0 @@
1
- # app/routes/budget_routes.py
2
- import logging
3
- from fastapi import APIRouter, HTTPException
4
- from typing import List
5
-
6
- from app.ads.schemas import BudgetRequest, BudgetPlan
7
- from app.ads.budget_service import generate_budget_plans
8
-
9
- router = APIRouter(prefix="/Ads", tags=["Ads"])
10
- logger = logging.getLogger(__name__)
11
- logger.addHandler(logging.NullHandler())
12
-
13
-
14
- @router.post("/price", response_model=List[BudgetPlan])
15
- def create_budget_options(payload: BudgetRequest):
16
- """
17
- Generate two budget options (daily & lifetime) for ad campaigns based on business inputs.
18
- Returns a list of two objects:
19
- [
20
- {"type":"daily","budget":"25$/day","duration":"7 days"},
21
- {"type":"lifetime","budget":"15$/day","duration":"62 days"}
22
- ]
23
- """
24
- try:
25
- plans = generate_budget_plans(payload)
26
- return plans
27
- except Exception as e:
28
- logger.exception("Failed to generate budget plans: %s", e)
29
- raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/ads/descriptions_service.py CHANGED
@@ -12,7 +12,6 @@ from app.ads.schemas import DescriptionsRequest, Persona
12
  logger = logging.getLogger(__name__)
13
  logger.addHandler(logging.NullHandler())
14
 
15
- # Ensure genai configured (harmless if already configured elsewhere)
16
  API_KEY = os.getenv("GEMINI_API_KEY")
17
  if API_KEY:
18
  try:
@@ -21,7 +20,6 @@ if API_KEY:
21
  except Exception:
22
  logger.exception("Failed to configure google.generativeai in descriptions service")
23
 
24
-
25
  def _extract_json_array(raw: str) -> str:
26
  start = raw.find('[')
27
  end = raw.rfind(']')
@@ -29,11 +27,7 @@ def _extract_json_array(raw: str) -> str:
29
  return raw[start:end + 1]
30
  return raw
31
 
32
-
33
  def _build_descriptions_prompt(req: DescriptionsRequest) -> str:
34
- """
35
- Build prompt asking Gemini to return ONLY a JSON array of strings (ad descriptions).
36
- """
37
  try:
38
  personas_json = json.dumps([p.dict() for p in req.selected_personas], indent=2)
39
  except Exception:
@@ -41,17 +35,20 @@ def _build_descriptions_prompt(req: DescriptionsRequest) -> str:
41
 
42
  main_goal_value = req.main_goal.value
43
  main_goal_desc = getattr(req.main_goal, "description", "")
 
 
44
 
45
  prompt = f"""
46
  You are an expert ad copywriter specialized in short, high-converting ad descriptions for digital ads.
47
 
 
 
48
  Task:
49
  Produce exactly {req.num_descriptions} ad descriptions (each 1-2 short sentences) tailored to the business and the selected persona(s). RETURN ONLY a JSON array of strings (e.g. ["Desc 1", "Desc 2", ...]) and nothing else.
50
 
51
  Requirements:
52
  - Each description should be concise (max ~140 characters preferred), benefit-focused, and aligned with the business value and main goal.
53
- - Vary tone across descriptions (e.g., urgent, aspirational, trust-building, practical).
54
- - Include the main value or offer where appropriate (e.g., "MVP development", "AI integration", "fast time-to-market", "trusted SaaS partner").
55
  - If the selected personas have different priorities, generate descriptions that address those priorities.
56
  - Goal: "{main_goal_value}" β€” {main_goal_desc}
57
 
@@ -71,10 +68,9 @@ Selected persona(s) (use these to shape descriptions):
71
 
72
  Now generate exactly {req.num_descriptions} unique ad descriptions as a JSON array of strings. No explanation, no extra text.
73
  """
74
- logger.debug("Built descriptions prompt (len=%d) for business '%s'", len(prompt), req.business_name)
75
  return prompt.strip()
76
 
77
-
78
  def generate_descriptions(req: DescriptionsRequest) -> List[str]:
79
  prompt = _build_descriptions_prompt(req)
80
 
@@ -97,7 +93,6 @@ def generate_descriptions(req: DescriptionsRequest) -> List[str]:
97
  logger.exception("Gemini generate_content failed for descriptions")
98
  raise RuntimeError(f"Gemini request failed: {e}")
99
 
100
- # extract raw text
101
  raw = None
102
  try:
103
  if response and hasattr(response, "text") and response.text:
 
12
  logger = logging.getLogger(__name__)
13
  logger.addHandler(logging.NullHandler())
14
 
 
15
  API_KEY = os.getenv("GEMINI_API_KEY")
16
  if API_KEY:
17
  try:
 
20
  except Exception:
21
  logger.exception("Failed to configure google.generativeai in descriptions service")
22
 
 
23
  def _extract_json_array(raw: str) -> str:
24
  start = raw.find('[')
25
  end = raw.rfind(']')
 
27
  return raw[start:end + 1]
28
  return raw
29
 
 
30
  def _build_descriptions_prompt(req: DescriptionsRequest) -> str:
 
 
 
31
  try:
32
  personas_json = json.dumps([p.dict() for p in req.selected_personas], indent=2)
33
  except Exception:
 
35
 
36
  main_goal_value = req.main_goal.value
37
  main_goal_desc = getattr(req.main_goal, "description", "")
38
+ tone_enum = getattr(req, "tone", None)
39
+ tone_value = tone_enum.value if tone_enum is not None else "Professional"
40
 
41
  prompt = f"""
42
  You are an expert ad copywriter specialized in short, high-converting ad descriptions for digital ads.
43
 
44
+ Tone: Use a "{tone_value}" tone for all descriptions. If tone is "Professional", write concise, formal copy. If "Casual / Friendly", be approachable and conversational. If "Bold / Persuasive", use urgency and strong value statements. If "Inspiring / Visionary", use motivational and future-focused language.
45
+
46
  Task:
47
  Produce exactly {req.num_descriptions} ad descriptions (each 1-2 short sentences) tailored to the business and the selected persona(s). RETURN ONLY a JSON array of strings (e.g. ["Desc 1", "Desc 2", ...]) and nothing else.
48
 
49
  Requirements:
50
  - Each description should be concise (max ~140 characters preferred), benefit-focused, and aligned with the business value and main goal.
51
+ - Include the main value or offer where appropriate.
 
52
  - If the selected personas have different priorities, generate descriptions that address those priorities.
53
  - Goal: "{main_goal_value}" β€” {main_goal_desc}
54
 
 
68
 
69
  Now generate exactly {req.num_descriptions} unique ad descriptions as a JSON array of strings. No explanation, no extra text.
70
  """
71
+ logger.debug("Built descriptions prompt (len=%d) for business '%s' (tone=%s)", len(prompt), req.business_name, tone_value)
72
  return prompt.strip()
73
 
 
74
  def generate_descriptions(req: DescriptionsRequest) -> List[str]:
75
  prompt = _build_descriptions_prompt(req)
76
 
 
93
  logger.exception("Gemini generate_content failed for descriptions")
94
  raise RuntimeError(f"Gemini request failed: {e}")
95
 
 
96
  raw = None
97
  try:
98
  if response and hasattr(response, "text") and response.text:
app/ads/headings_service.py CHANGED
@@ -12,7 +12,6 @@ from app.ads.schemas import HeadingsRequest, Persona
12
  logger = logging.getLogger(__name__)
13
  logger.addHandler(logging.NullHandler())
14
 
15
- # Ensure Gemini SDK is configured (harmless if already configured elsewhere)
16
  API_KEY = os.getenv("GEMINI_API_KEY")
17
  if not API_KEY:
18
  logger.error("GEMINI_API_KEY not set; headings generation will fail if called without configuration.")
@@ -23,39 +22,32 @@ else:
23
  except Exception as e:
24
  logger.exception("Failed to configure google.generativeai in headings service: %s", e)
25
 
26
-
27
  def _extract_json_array(raw: str) -> str:
28
- """
29
- Return the first JSON array substring from raw (from '[' to ']') to be robust
30
- against extra commentary in model output.
31
- """
32
  start = raw.find('[')
33
  end = raw.rfind(']')
34
  if start != -1 and end != -1 and end > start:
35
  return raw[start:end + 1]
36
  return raw
37
 
38
-
39
  def _build_headings_prompt(req: HeadingsRequest) -> str:
40
- """
41
- Build a clear prompt asking Gemini to return ONLY a JSON array of strings (headings).
42
- """
43
- # Convert selected_personas to compact JSON for context
44
  personas_json = json.dumps([p.dict() for p in req.selected_personas], indent=2)
45
 
46
  main_goal_value = req.main_goal.value
47
  main_goal_desc = getattr(req.main_goal, "description", "")
 
 
48
 
49
  prompt = f"""
50
  You are an expert copywriter specialized in short, high-converting ad headlines for digital ads.
51
 
 
 
52
  Task:
53
  Produce exactly {req.num_headings} short, punchy ad headings (strings) for a paid ad campaign that target the selected personas and align with the business goal. RETURN ONLY a JSON array of strings (e.g. ["Heading 1", "Heading 2", ...]) and nothing else.
54
 
55
  Requirements:
56
  - Each heading should be concise (max ~60 characters), benefit-focused, and tailored to the provided personas and business goal.
57
  - Use active language and mention the key value when appropriate (e.g., "scale", "AI", "launch", "MVP", "secure funding", "reduce time-to-market").
58
- - Vary the tone across the 4 headings (e.g., urgent, aspirational, trust-building, and practical).
59
  - Avoid punctuation-only headlines, and do not include numbering in text.
60
  - If the main goal is "{main_goal_value}", use that intention as a primary framing. Goal description: {main_goal_desc}
61
 
@@ -75,14 +67,10 @@ Selected persona(s) (use these to shape headings):
75
 
76
  Now generate exactly {req.num_headings} unique ad headings as a JSON array of strings. No explanation, no extra text.
77
  """
78
- logger.debug("Built headings prompt (len=%d) for business '%s'", len(prompt), req.business_name)
79
  return prompt.strip()
80
 
81
-
82
  def generate_headings(req: HeadingsRequest) -> List[str]:
83
- """
84
- Call Gemini to generate ad headings and return list[str].
85
- """
86
  prompt = _build_headings_prompt(req)
87
 
88
  model_name = "gemini-2.5-pro"
@@ -104,7 +92,6 @@ def generate_headings(req: HeadingsRequest) -> List[str]:
104
  logger.exception("Gemini generate_content failed for headings")
105
  raise RuntimeError(f"Gemini request failed: {e}")
106
 
107
- # Extract raw text from response
108
  raw = None
109
  try:
110
  if response and hasattr(response, "text") and response.text:
@@ -129,7 +116,6 @@ def generate_headings(req: HeadingsRequest) -> List[str]:
129
  logger.error("Empty response from Gemini when generating headings")
130
  raise RuntimeError("Empty response from Gemini")
131
 
132
- # Robust JSON extraction & parsing
133
  snippet = _extract_json_array(raw)
134
  try:
135
  parsed = json.loads(snippet)
@@ -141,7 +127,6 @@ def generate_headings(req: HeadingsRequest) -> List[str]:
141
  logger.error("Parsed headings JSON is not a list of strings. Parsed type: %s", type(parsed))
142
  raise RuntimeError("Gemini did not return a JSON array of strings as expected.")
143
 
144
- # Normalize: ensure exactly num_headings items
145
  headings = parsed
146
  if len(headings) < req.num_headings:
147
  logger.warning("Gemini returned %d headings; expected %d. Returning what we have.",
@@ -150,7 +135,6 @@ def generate_headings(req: HeadingsRequest) -> List[str]:
150
  headings = headings[: req.num_headings]
151
  logger.debug("Trimmed headings to requested num_headings=%d", req.num_headings)
152
 
153
- # Final basic cleanup (strip whitespace)
154
  headings = [h.strip() for h in headings]
155
 
156
  logger.info("Generated %d headings for business '%s'", len(headings), req.business_name)
 
12
  logger = logging.getLogger(__name__)
13
  logger.addHandler(logging.NullHandler())
14
 
 
15
  API_KEY = os.getenv("GEMINI_API_KEY")
16
  if not API_KEY:
17
  logger.error("GEMINI_API_KEY not set; headings generation will fail if called without configuration.")
 
22
  except Exception as e:
23
  logger.exception("Failed to configure google.generativeai in headings service: %s", e)
24
 
 
25
  def _extract_json_array(raw: str) -> str:
 
 
 
 
26
  start = raw.find('[')
27
  end = raw.rfind(']')
28
  if start != -1 and end != -1 and end > start:
29
  return raw[start:end + 1]
30
  return raw
31
 
 
32
  def _build_headings_prompt(req: HeadingsRequest) -> str:
 
 
 
 
33
  personas_json = json.dumps([p.dict() for p in req.selected_personas], indent=2)
34
 
35
  main_goal_value = req.main_goal.value
36
  main_goal_desc = getattr(req.main_goal, "description", "")
37
+ tone_enum = getattr(req, "tone", None)
38
+ tone_value = tone_enum.value if tone_enum is not None else "Professional"
39
 
40
  prompt = f"""
41
  You are an expert copywriter specialized in short, high-converting ad headlines for digital ads.
42
 
43
+ Tone: Use a "{tone_value}" tone for all headings. If tone is "Professional", keep headlines formal and trustworthy. If "Casual / Friendly", keep them approachable. If "Bold / Persuasive", use urgency and commanding language. If "Inspiring / Visionary", use aspirational language.
44
+
45
  Task:
46
  Produce exactly {req.num_headings} short, punchy ad headings (strings) for a paid ad campaign that target the selected personas and align with the business goal. RETURN ONLY a JSON array of strings (e.g. ["Heading 1", "Heading 2", ...]) and nothing else.
47
 
48
  Requirements:
49
  - Each heading should be concise (max ~60 characters), benefit-focused, and tailored to the provided personas and business goal.
50
  - Use active language and mention the key value when appropriate (e.g., "scale", "AI", "launch", "MVP", "secure funding", "reduce time-to-market").
 
51
  - Avoid punctuation-only headlines, and do not include numbering in text.
52
  - If the main goal is "{main_goal_value}", use that intention as a primary framing. Goal description: {main_goal_desc}
53
 
 
67
 
68
  Now generate exactly {req.num_headings} unique ad headings as a JSON array of strings. No explanation, no extra text.
69
  """
70
+ logger.debug("Built headings prompt (len=%d) for business '%s' (tone=%s)", len(prompt), req.business_name, tone_value)
71
  return prompt.strip()
72
 
 
73
  def generate_headings(req: HeadingsRequest) -> List[str]:
 
 
 
74
  prompt = _build_headings_prompt(req)
75
 
76
  model_name = "gemini-2.5-pro"
 
92
  logger.exception("Gemini generate_content failed for headings")
93
  raise RuntimeError(f"Gemini request failed: {e}")
94
 
 
95
  raw = None
96
  try:
97
  if response and hasattr(response, "text") and response.text:
 
116
  logger.error("Empty response from Gemini when generating headings")
117
  raise RuntimeError("Empty response from Gemini")
118
 
 
119
  snippet = _extract_json_array(raw)
120
  try:
121
  parsed = json.loads(snippet)
 
127
  logger.error("Parsed headings JSON is not a list of strings. Parsed type: %s", type(parsed))
128
  raise RuntimeError("Gemini did not return a JSON array of strings as expected.")
129
 
 
130
  headings = parsed
131
  if len(headings) < req.num_headings:
132
  logger.warning("Gemini returned %d headings; expected %d. Returning what we have.",
 
135
  headings = headings[: req.num_headings]
136
  logger.debug("Trimmed headings to requested num_headings=%d", req.num_headings)
137
 
 
138
  headings = [h.strip() for h in headings]
139
 
140
  logger.info("Generated %d headings for business '%s'", len(headings), req.business_name)
app/ads/image_service.py CHANGED
@@ -1,10 +1,11 @@
1
  import io
2
  import logging
3
- from typing import Tuple
4
  from google import genai
5
  from google.genai import types
6
 
7
- from app.ads.schemas import ImageRequest, Persona
 
8
 
9
  logger = logging.getLogger(__name__)
10
 
@@ -15,30 +16,109 @@ def _persona_to_text(persona: Persona) -> str:
15
 
16
  def generate_image(req: ImageRequest) -> Tuple[bytes, str]:
17
  """
18
- Generate an Ad image using Gemini 2.0 flash experimental image model.
19
- Falls back to returning text prompts if Gemini returns only text.
20
  """
 
 
21
  # Safely convert Persona objects into strings
22
  if req.selected_personas and len(req.selected_personas) > 0:
23
  personas_text = "; ".join(_persona_to_text(p) for p in req.selected_personas)
24
  else:
25
  personas_text = "general target audience"
26
 
27
- prompt = (
28
- f"Create a professional advertisement image for the business '{req.business_name}'. "
29
- f"Category: {req.business_category}. "
30
- f"Description: {req.business_description}. "
31
- f"Promotion type: {req.promotion_type}. "
32
- f"Offer details: {req.offer_description}. "
33
- f"Value proposition: {req.value}. "
34
- f"Main goal: {req.main_goal.value}. "
35
- f"Serving clients info: {req.serving_clients_info}. "
36
- f"Location: {req.serving_clients_location}. "
37
- f"Target persona(s): {personas_text}. "
38
- "The image should be modern, vibrant, and suitable for a social media advertisement."
39
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  logger.info("Requesting Gemini image generation for business '%s'", req.business_name)
 
42
 
43
  try:
44
  client = genai.Client()
@@ -61,10 +141,13 @@ def generate_image(req: ImageRequest) -> Tuple[bytes, str]:
61
  for part in response.candidates[0].content.parts:
62
  if getattr(part, "text", None):
63
  logger.warning("Gemini returned text instead of image: %s", part.text[:300])
64
- raise RuntimeError("Gemini returned text only, no image data found.")
65
 
66
  raise RuntimeError("No image data found in Gemini response.")
67
 
 
 
 
68
  except Exception as e:
69
  logger.exception("Gemini image generation failed: %s", e)
70
- raise RuntimeError(f"Gemini image generation failed: {e}")
 
1
  import io
2
  import logging
3
+ from typing import Tuple, List, Optional
4
  from google import genai
5
  from google.genai import types
6
 
7
+ # Assuming this is your schema file
8
+ from app.ads.schemas import ImageRequest, Persona, ToneEnum, GoalEnum
9
 
10
  logger = logging.getLogger(__name__)
11
 
 
16
 
17
  def generate_image(req: ImageRequest) -> Tuple[bytes, str]:
18
  """
19
+ Generate an Ad image using Gemini with a highly-structured prompt.
 
20
  """
21
+ # --- 1. Prepare dynamic prompt components ---
22
+
23
  # Safely convert Persona objects into strings
24
  if req.selected_personas and len(req.selected_personas) > 0:
25
  personas_text = "; ".join(_persona_to_text(p) for p in req.selected_personas)
26
  else:
27
  personas_text = "general target audience"
28
 
29
+ # Define aspect ratio
30
+ aspect_ratio_str = f"{req.width}:{req.height}"
31
+ if aspect_ratio_str == "1200:628":
32
+ aspect_ratio_desc = "1.91:1 (Landscape Link Ad)"
33
+ elif aspect_ratio_str == "1080:1080":
34
+ aspect_ratio_desc = "1:1 (Square Feed Ad)"
35
+ elif aspect_ratio_str == "1080:1920":
36
+ aspect_ratio_desc = "9:16 (Vertical Story Ad)"
37
+ else:
38
+ aspect_ratio_desc = f"{aspect_ratio_str} (Custom)"
39
+
40
+ # Create strings for optional fields
41
+ brand_color_str = ""
42
+ if req.brand_colors:
43
+ brand_color_str = f"* **Color Palette:** Strictly use this palette as the primary and accent colors: {', '.join(req.brand_colors)}."
44
+ else:
45
+ brand_color_str = f"* **Color Palette:** Use a professional color palette (e.g., blues, greys, with one strong accent color) that matches the '{req.tone.value}' tone."
46
+
47
+ visual_pref_text = ""
48
+ if req.visual_preference:
49
+ visual_pref_text = f"The main visual *must* be: **'{req.visual_preference}'**. This should be the element that catches the eye first."
50
+ else:
51
+ visual_pref_text = "Create a single, strong, and uncluttered focal point that represents the offer (e.g., an abstract graphic of growth, a clean icon, or a person matching the persona)."
52
+
53
+ cta_text_str = ""
54
+ if req.cta_text:
55
+ cta_text_str = f"The banner *must* also include the *exact* phrase: **'{req.cta_text}'**. This can be in a button-like shape or as clear, bold text."
56
+
57
+
58
+ # --- 2. The New, Detailed, and Structured Prompt ---
59
+
60
+ prompt = f'''
61
+ **ROLE:** You are an expert Art Director at a world-class advertising agency.
62
+ **YOUR GOAL:** To generate a *single*, professional, and high-conversion ad banner for a Meta (Facebook/Instagram) campaign.
63
+ **YOUR TASK:** Generate one (1) image file based *only* on the strict hierarchy, components, and constraints below.
64
+
65
+ ---
66
+
67
+ ### 1. HIERARCHY OF IMPORTANCE (Strict Priority)
68
+
69
+ This is the order of what the user MUST see. Your design must follow this.
70
+
71
+ * **P1: FOCAL POINT (The Visual):**
72
+ * This is the **most important** element. It must stop the user from scrolling.
73
+ * It must be a single, clean, high-quality, professional image or graphic.
74
+ * **Visual Content:** {visual_pref_text}
75
+ * It must be the primary focus of the entire banner.
76
+
77
+ * **P2: HEADLINE (The Offer):**
78
+ * This is the **second most important** element. It is the primary text.
79
+ * It must be bold, clear, and easy to read in 1 second.
80
+ * **Text Content:** It must be the *exact* phrase: **'{req.offer_description}'**
81
+
82
+ * **P3: CALL-TO-ACTION (The Instruction):**
83
+ * This is the **third most important** element. It tells the user what to do next.
84
+ * It must be visually distinct (e.g., a button shape or contrasting bold text).
85
+ * **Text Content:** {cta_text_str}
86
+
87
+ * **P4: BRANDING (The Signature):**
88
+ * This is the **least important** element. It is for brand recognition only.
89
+ * It must be small, clean, and placed in a corner (e.g., bottom-left or top-right).
90
+ * **Text Content:** **'{req.business_name}'** (Render this as clean text, not a complex logo).
91
+
92
+ ---
93
+
94
+ ### 2. DESIGN & LAYOUT MANDATES
95
+
96
+ * **ASPECT RATIO:** **{aspect_ratio_desc} ({req.width}x{req.height}px).** This is a non-negotiable technical requirement.
97
+ * **STYLE & TONE:** **{req.style}, {req.tone.value}.**
98
+ * **TARGET AUDIENCE CONTEXT:** The entire design (font choice, imagery, colors) must feel professional and trustworthy to this user: **{personas_text}**.
99
+ * **COLOR PALETTE:** {brand_color_str}
100
+
101
+ ---
102
+
103
+ ### 3. WHAT TO AVOID (CRITICAL FAILURES)
104
+
105
+ * **DO NOT** add *any text* not explicitly listed in P2, P3, or P4.
106
+ * **DO NOT** clutter the image. White space is essential. The design must be **minimalist** and **clean**.
107
+ * **DO NOT** use generic, cheesy, or low-quality stock photos.
108
+ * **DO NOT** use hard-to-read, cursive, or overly-stylized fonts. Readability is the top priority for text.
109
+ * **DO NOT** make the branding (P4) large or the main focus. It must be subtle.
110
+
111
+ ---
112
+
113
+ ### FINAL OUTPUT
114
+
115
+ Generate *only* the final, polished ad banner image. Do not respond with text, questions, or comments.
116
+ '''
117
+
118
+ # --- 3. Execute the API Call ---
119
 
120
  logger.info("Requesting Gemini image generation for business '%s'", req.business_name)
121
+ logger.debug("Full prompt:\n%s", prompt) # Good for debugging
122
 
123
  try:
124
  client = genai.Client()
 
141
  for part in response.candidates[0].content.parts:
142
  if getattr(part, "text", None):
143
  logger.warning("Gemini returned text instead of image: %s", part.text[:300])
144
+ raise RuntimeError(f"Gemini returned text only: {part.text[:300]}...")
145
 
146
  raise RuntimeError("No image data found in Gemini response.")
147
 
148
+ except AttributeError as e:
149
+ logger.exception(f"Failed to parse Gemini response. It's possible the API response structure is unexpected. {e}")
150
+ raise RuntimeError(f"Failed to parse Gemini response: {e}")
151
  except Exception as e:
152
  logger.exception("Gemini image generation failed: %s", e)
153
+ raise RuntimeError(f"Gemini image generation failed: {e}")
app/ads/persona_routes.py CHANGED
@@ -3,12 +3,22 @@ from fastapi import APIRouter, HTTPException
3
  from typing import List
4
  from fastapi.responses import StreamingResponse
5
 
6
- from app.ads.schemas import BusinessInput, Persona, RegenerateRequest, HeadingsRequest, DescriptionsRequest, ImageRequest
 
 
 
 
 
 
 
 
 
7
  import io
8
  import app.ads.image_service as image_service
9
  import app.ads.headings_service as headings_service
10
  import app.ads.descriptions_service as descriptions_service
11
- from app.ads.persona_service import generate_personas , regenerate_personas
 
12
 
13
  router = APIRouter(prefix="/Ads", tags=["Ads"])
14
 
@@ -29,7 +39,6 @@ def regenerate_personas_endpoint(payload: RegenerateRequest):
29
  try:
30
  personas = regenerate_personas(payload, payload.previous_personas)
31
  except Exception as e:
32
- # return the error message to the client for quick debugging
33
  raise HTTPException(status_code=500, detail=str(e))
34
  return personas
35
 
@@ -41,7 +50,6 @@ def create_headings(payload: HeadingsRequest):
41
  try:
42
  return headings_service.generate_headings(payload)
43
  except Exception as e:
44
- # Log error server-side; return HTTP 500 with message for debugging
45
  raise HTTPException(status_code=500, detail=str(e))
46
 
47
  @router.post("/Descriptions", response_model=List[str])
@@ -60,4 +68,15 @@ def create_image(payload: ImageRequest):
60
  img_bytes, mime = image_service.generate_image(payload)
61
  return StreamingResponse(io.BytesIO(img_bytes), media_type=mime)
62
  except Exception as e:
63
- raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
3
  from typing import List
4
  from fastapi.responses import StreamingResponse
5
 
6
+ from app.ads.schemas import (
7
+ BusinessInput,
8
+ Persona,
9
+ RegenerateRequest,
10
+ HeadingsRequest,
11
+ DescriptionsRequest,
12
+ ImageRequest,
13
+ BudgetRequest,
14
+ BudgetPlan,
15
+ )
16
  import io
17
  import app.ads.image_service as image_service
18
  import app.ads.headings_service as headings_service
19
  import app.ads.descriptions_service as descriptions_service
20
+ from app.ads.persona_service import generate_personas, regenerate_personas
21
+ from app.ads.budget_service import generate_budget_plans
22
 
23
  router = APIRouter(prefix="/Ads", tags=["Ads"])
24
 
 
39
  try:
40
  personas = regenerate_personas(payload, payload.previous_personas)
41
  except Exception as e:
 
42
  raise HTTPException(status_code=500, detail=str(e))
43
  return personas
44
 
 
50
  try:
51
  return headings_service.generate_headings(payload)
52
  except Exception as e:
 
53
  raise HTTPException(status_code=500, detail=str(e))
54
 
55
  @router.post("/Descriptions", response_model=List[str])
 
68
  img_bytes, mime = image_service.generate_image(payload)
69
  return StreamingResponse(io.BytesIO(img_bytes), media_type=mime)
70
  except Exception as e:
71
+ raise HTTPException(status_code=500, detail=str(e))
72
+
73
+ @router.post("/price", response_model=List[BudgetPlan])
74
+ def create_budget_options(payload: BudgetRequest):
75
+ """
76
+ Generate two budget options (daily & lifetime) for ad campaigns based on business inputs.
77
+ """
78
+ try:
79
+ plans = generate_budget_plans(payload)
80
+ return plans
81
+ except Exception as e:
82
+ raise HTTPException(status_code=500, detail=str(e))
app/ads/persona_service.py CHANGED
@@ -9,20 +9,15 @@ from pydantic import ValidationError
9
 
10
  from app.ads.schemas import Persona, BusinessInput
11
 
12
- # module logger
13
  logger = logging.getLogger(__name__)
14
- # Avoid configuring global logging here; app-level config should set handlers/levels.
15
- # But ensure we have at least a NullHandler to avoid "No handler" warnings in some apps.
16
  logger.addHandler(logging.NullHandler())
17
 
18
-
19
  # initialize client (reads GEMINI_API_KEY from environment)
20
  API_KEY = os.getenv("GEMINI_API_KEY")
21
  if not API_KEY:
22
  logger.error("GEMINI_API_KEY environment variable not set")
23
  raise RuntimeError("Please set the GEMINI_API_KEY environment variable")
24
 
25
- # Configure the genai SDK (reference pattern)
26
  try:
27
  genai.configure(api_key=API_KEY)
28
  logger.info("Configured google.generativeai with provided API key")
@@ -30,9 +25,7 @@ except Exception as e:
30
  logger.exception("Failed to configure google.generativeai: %s", e)
31
  raise
32
 
33
-
34
  def _build_prompt(b: BusinessInput) -> str:
35
-
36
  examples_json = [
37
  {
38
  "name": "Startup Founders",
@@ -74,13 +67,16 @@ def _build_prompt(b: BusinessInput) -> str:
74
  }
75
  ]
76
 
77
- # Use both the enum value and the associated description
78
  main_goal_value = b.main_goal.value
79
  main_goal_desc = getattr(b.main_goal, "description", "")
 
 
80
 
81
  prompt = f'''
82
  You are a senior marketing strategist specialized in creating *ideal-customer / target-audience personas* for businesses. Produce exactly {b.num_personas} distinct IDEAL-CUSTOMER personas tailored to the business described below.
83
 
 
 
84
  **Output format (required):** Return ONLY a JSON array of objects. Each object must contain these properties in this exact order:
85
  1. name (string)
86
  2. headline (string; 3-6 words)
@@ -95,11 +91,10 @@ You are a senior marketing strategist specialized in creating *ideal-customer /
95
  - primary buying trigger or motivation,
96
  - preferred channels to reach them (e.g., Instagram, LinkedIn, email, local events),
97
  - why they would choose this business / how the offer solves their need.
 
98
 
99
  **Do NOT include any extra top-level keys, comments, or explanation text. JSON array only.**
100
 
101
- Below are three example personas showing the exact style and level of detail I want your output to match. Use them as format examples β€” but produce personas specific to the business inputs that follow.
102
-
103
  EXAMPLE PERSONAS (format example):
104
  {json.dumps(examples_json, indent=2)}
105
 
@@ -117,15 +112,10 @@ Business inputs:
117
  Generate the {b.num_personas} personas now as a JSON array that exactly matches the schema and style shown above.
118
  '''
119
  built = prompt.strip()
120
- logger.debug("Built persona prompt (goal=%s): %s", main_goal_value, built[:400] + ("…" if len(built) > 400 else ""))
121
  return built
122
 
123
-
124
  def _extract_json_array(raw: str) -> str:
125
- """
126
- Find and return the first JSON array substring in raw text (from '[' to ']').
127
- If not found, return raw as-is (parsing will attempt).
128
- """
129
  start = raw.find('[')
130
  end = raw.rfind(']')
131
  if start != -1 and end != -1 and end > start:
@@ -135,12 +125,7 @@ def _extract_json_array(raw: str) -> str:
135
  logger.debug("No JSON array brackets found in raw response; returning full raw text for parsing")
136
  return raw
137
 
138
-
139
  def generate_personas(b: BusinessInput) -> List[Persona]:
140
- """
141
- Generate personas using Gemini. Returns a list of Persona Pydantic models.
142
- Logs important steps and errors for easier debugging.
143
- """
144
  prompt = _build_prompt(b)
145
 
146
  try:
@@ -155,11 +140,9 @@ def generate_personas(b: BusinessInput) -> List[Persona]:
155
  logger.info("Gemini generate_content completed in %.2fs", duration)
156
 
157
  except Exception as e:
158
- # surface Gemini initialization / network errors with stack trace
159
  logger.exception("Gemini request failed for business '%s': %s", b.business_name, e)
160
  raise RuntimeError(f"Gemini request failed: {e}")
161
 
162
- # Inspect response for text or safety block
163
  raw = None
164
  try:
165
  if response and hasattr(response, "text") and response.text:
@@ -184,17 +167,13 @@ def generate_personas(b: BusinessInput) -> List[Persona]:
184
  logger.error("Empty response received from Gemini for business '%s'", b.business_name)
185
  raise RuntimeError("Empty response received from Gemini")
186
 
187
- # Extract JSON array substring (robust for extra commentary)
188
  json_snippet = _extract_json_array(raw)
189
  logger.info("Attempting to parse JSON snippet for business '%s' (snippet length=%d)", b.business_name, len(json_snippet))
190
 
191
- # Attempt to parse JSON and validate against Persona schema
192
  try:
193
  parsed = json.loads(json_snippet)
194
 
195
- # If model returned a dict wrapper, attempt to find the list inside
196
  if isinstance(parsed, dict):
197
- # common wrapper keys to check
198
  for key in ("items", "personas", "data", "results"):
199
  if key in parsed and isinstance(parsed[key], list):
200
  parsed = parsed[key]
@@ -212,7 +191,6 @@ def generate_personas(b: BusinessInput) -> List[Persona]:
212
  personas.append(persona)
213
  logger.debug("Validated persona %d: %s", idx, persona.name)
214
  except ValidationError as ve:
215
- # include which item failed for better debugging
216
  logger.error("Persona validation failed for item %s: %s\nRaw item: %s", idx, ve, obj)
217
  raise
218
 
@@ -220,35 +198,29 @@ def generate_personas(b: BusinessInput) -> List[Persona]:
220
  return personas
221
 
222
  except (json.JSONDecodeError, ValidationError, ValueError) as e:
223
- # Provide helpful debug output including raw Gemini text
224
  logger.exception("Failed to parse/validate Gemini JSON output for business '%s'", b.business_name)
225
  raise RuntimeError(
226
  f"Failed to parse Gemini response as JSON Personas: {e}\n\nRaw Gemini response:\n{raw}"
227
  )
228
 
229
  def regenerate_personas(b: BusinessInput, previous_personas: List[Persona]) -> List[Persona]:
230
- """
231
- Generate a new set of personas given the business input AND a list of
232
- previously generated personas. The model is instructed to avoid duplicating
233
- the previous personas and produce distinct/improved target-audience personas.
234
- """
235
- # Build base prompt from existing function
236
  base_prompt = _build_prompt(b)
237
 
238
- # Convert previous_personas to simple dicts (Pydantic models -> plain dicts)
239
  try:
240
  prev_list = [p.dict() if hasattr(p, "dict") else p for p in previous_personas]
241
  except Exception:
242
- # Defensive: if previous_personas are plain dicts already
243
  prev_list = previous_personas
244
 
245
  prev_json = json.dumps(prev_list, indent=2)
 
 
246
 
247
- # Append clear instructions about previous personas and uniqueness requirement
248
  extra_instructions = f"""
249
  Previous personas provided (do NOT repeat these exactly):
250
  {prev_json}
251
 
 
 
252
  Instructions:
253
  - Produce exactly {b.num_personas} personas tailored to the same business inputs above.
254
  - **Do not duplicate** persona names or core audience segments included in the previous list.
@@ -261,13 +233,11 @@ Instructions:
261
  """
262
  prompt = base_prompt + "\n\n" + extra_instructions
263
 
264
- logger.info("Regenerating personas for business '%s' with %d previous personas",
265
- b.business_name, len(prev_list))
266
 
267
- # call model (same pattern as generate_personas)
268
  try:
269
  model_name = "gemini-2.5-pro"
270
- logger.info("Initializing Gemini model for regeneration: %s", model_name)
271
  model = genai.GenerativeModel(model_name)
272
 
273
  start_ts = time.perf_counter()
@@ -279,7 +249,6 @@ Instructions:
279
  logger.exception("Gemini regenerate request failed for business '%s': %s", b.business_name, e)
280
  raise RuntimeError(f"Gemini regenerate request failed: {e}")
281
 
282
- # Extract raw text (same robust logic)
283
  raw = None
284
  try:
285
  if response and hasattr(response, "text") and response.text:
@@ -304,7 +273,6 @@ Instructions:
304
  logger.error("Empty regenerate response from Gemini for business '%s'", b.business_name)
305
  raise RuntimeError("Empty response received from Gemini")
306
 
307
- # Extract JSON and validate (reuse helper)
308
  json_snippet = _extract_json_array(raw)
309
  logger.info("Attempting to parse regenerated JSON snippet for business '%s' (len=%d)",
310
  b.business_name, len(json_snippet))
@@ -312,7 +280,6 @@ Instructions:
312
  try:
313
  parsed = json.loads(json_snippet)
314
 
315
- # If wrapper -> find list
316
  if isinstance(parsed, dict):
317
  for key in ("items", "personas", "data", "results"):
318
  if key in parsed and isinstance(parsed[key], list):
 
9
 
10
  from app.ads.schemas import Persona, BusinessInput
11
 
 
12
  logger = logging.getLogger(__name__)
 
 
13
  logger.addHandler(logging.NullHandler())
14
 
 
15
  # initialize client (reads GEMINI_API_KEY from environment)
16
  API_KEY = os.getenv("GEMINI_API_KEY")
17
  if not API_KEY:
18
  logger.error("GEMINI_API_KEY environment variable not set")
19
  raise RuntimeError("Please set the GEMINI_API_KEY environment variable")
20
 
 
21
  try:
22
  genai.configure(api_key=API_KEY)
23
  logger.info("Configured google.generativeai with provided API key")
 
25
  logger.exception("Failed to configure google.generativeai: %s", e)
26
  raise
27
 
 
28
  def _build_prompt(b: BusinessInput) -> str:
 
29
  examples_json = [
30
  {
31
  "name": "Startup Founders",
 
67
  }
68
  ]
69
 
 
70
  main_goal_value = b.main_goal.value
71
  main_goal_desc = getattr(b.main_goal, "description", "")
72
+ tone_enum = getattr(b, "tone", None)
73
+ tone_value = tone_enum.value if tone_enum is not None else "Professional"
74
 
75
  prompt = f'''
76
  You are a senior marketing strategist specialized in creating *ideal-customer / target-audience personas* for businesses. Produce exactly {b.num_personas} distinct IDEAL-CUSTOMER personas tailored to the business described below.
77
 
78
+ **Tone:** Use a "{tone_value}" tone for all persona text (including descriptions and headlines). Apply the tone consistently. If tone is "Professional", use a formal, trustworthy voice. If "Casual / Friendly", be approachable and conversational. If "Bold / Persuasive", use urgency and strong calls-to-action. If "Inspiring / Visionary", use motivating, future-focused language.
79
+
80
  **Output format (required):** Return ONLY a JSON array of objects. Each object must contain these properties in this exact order:
81
  1. name (string)
82
  2. headline (string; 3-6 words)
 
91
  - primary buying trigger or motivation,
92
  - preferred channels to reach them (e.g., Instagram, LinkedIn, email, local events),
93
  - why they would choose this business / how the offer solves their need.
94
+ - Use the requested tone when phrasing motivations and channels.
95
 
96
  **Do NOT include any extra top-level keys, comments, or explanation text. JSON array only.**
97
 
 
 
98
  EXAMPLE PERSONAS (format example):
99
  {json.dumps(examples_json, indent=2)}
100
 
 
112
  Generate the {b.num_personas} personas now as a JSON array that exactly matches the schema and style shown above.
113
  '''
114
  built = prompt.strip()
115
+ logger.debug("Built persona prompt (goal=%s; tone=%s): %s", main_goal_value, tone_value, built[:400] + ("…" if len(built) > 400 else ""))
116
  return built
117
 
 
118
  def _extract_json_array(raw: str) -> str:
 
 
 
 
119
  start = raw.find('[')
120
  end = raw.rfind(']')
121
  if start != -1 and end != -1 and end > start:
 
125
  logger.debug("No JSON array brackets found in raw response; returning full raw text for parsing")
126
  return raw
127
 
 
128
  def generate_personas(b: BusinessInput) -> List[Persona]:
 
 
 
 
129
  prompt = _build_prompt(b)
130
 
131
  try:
 
140
  logger.info("Gemini generate_content completed in %.2fs", duration)
141
 
142
  except Exception as e:
 
143
  logger.exception("Gemini request failed for business '%s': %s", b.business_name, e)
144
  raise RuntimeError(f"Gemini request failed: {e}")
145
 
 
146
  raw = None
147
  try:
148
  if response and hasattr(response, "text") and response.text:
 
167
  logger.error("Empty response received from Gemini for business '%s'", b.business_name)
168
  raise RuntimeError("Empty response received from Gemini")
169
 
 
170
  json_snippet = _extract_json_array(raw)
171
  logger.info("Attempting to parse JSON snippet for business '%s' (snippet length=%d)", b.business_name, len(json_snippet))
172
 
 
173
  try:
174
  parsed = json.loads(json_snippet)
175
 
 
176
  if isinstance(parsed, dict):
 
177
  for key in ("items", "personas", "data", "results"):
178
  if key in parsed and isinstance(parsed[key], list):
179
  parsed = parsed[key]
 
191
  personas.append(persona)
192
  logger.debug("Validated persona %d: %s", idx, persona.name)
193
  except ValidationError as ve:
 
194
  logger.error("Persona validation failed for item %s: %s\nRaw item: %s", idx, ve, obj)
195
  raise
196
 
 
198
  return personas
199
 
200
  except (json.JSONDecodeError, ValidationError, ValueError) as e:
 
201
  logger.exception("Failed to parse/validate Gemini JSON output for business '%s'", b.business_name)
202
  raise RuntimeError(
203
  f"Failed to parse Gemini response as JSON Personas: {e}\n\nRaw Gemini response:\n{raw}"
204
  )
205
 
206
  def regenerate_personas(b: BusinessInput, previous_personas: List[Persona]) -> List[Persona]:
 
 
 
 
 
 
207
  base_prompt = _build_prompt(b)
208
 
 
209
  try:
210
  prev_list = [p.dict() if hasattr(p, "dict") else p for p in previous_personas]
211
  except Exception:
 
212
  prev_list = previous_personas
213
 
214
  prev_json = json.dumps(prev_list, indent=2)
215
+ tone_enum = getattr(b, "tone", None)
216
+ tone_value = tone_enum.value if tone_enum is not None else "Professional"
217
 
 
218
  extra_instructions = f"""
219
  Previous personas provided (do NOT repeat these exactly):
220
  {prev_json}
221
 
222
+ Tone: Use a "{tone_value}" tone consistently in names and descriptions. If tone is "Professional" use a formal voice; if "Casual / Friendly" be conversational; if "Bold / Persuasive" be urgency-driven; if "Inspiring / Visionary" be motivational.
223
+
224
  Instructions:
225
  - Produce exactly {b.num_personas} personas tailored to the same business inputs above.
226
  - **Do not duplicate** persona names or core audience segments included in the previous list.
 
233
  """
234
  prompt = base_prompt + "\n\n" + extra_instructions
235
 
236
+ logger.info("Regenerating personas for business '%s' with %d previous personas (tone=%s)",
237
+ b.business_name, len(prev_list), tone_value)
238
 
 
239
  try:
240
  model_name = "gemini-2.5-pro"
 
241
  model = genai.GenerativeModel(model_name)
242
 
243
  start_ts = time.perf_counter()
 
249
  logger.exception("Gemini regenerate request failed for business '%s': %s", b.business_name, e)
250
  raise RuntimeError(f"Gemini regenerate request failed: {e}")
251
 
 
252
  raw = None
253
  try:
254
  if response and hasattr(response, "text") and response.text:
 
273
  logger.error("Empty regenerate response from Gemini for business '%s'", b.business_name)
274
  raise RuntimeError("Empty response received from Gemini")
275
 
 
276
  json_snippet = _extract_json_array(raw)
277
  logger.info("Attempting to parse regenerated JSON snippet for business '%s' (len=%d)",
278
  b.business_name, len(json_snippet))
 
280
  try:
281
  parsed = json.loads(json_snippet)
282
 
 
283
  if isinstance(parsed, dict):
284
  for key in ("items", "personas", "data", "results"):
285
  if key in parsed and isinstance(parsed[key], list):
app/ads/schemas.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from enum import Enum
2
  from pydantic import BaseModel, Field
3
  from typing import List, Optional
@@ -27,6 +28,12 @@ class GoalEnum(str, Enum):
27
  obj.description = description
28
  return obj
29
 
 
 
 
 
 
 
30
  class Persona(BaseModel):
31
  uuid: str = Field(default_factory=lambda: str(uuid4()), description="Unique identifier for this persona")
32
  flag: bool = Field(False, description="Boolean flag for client use (defaults to False)")
@@ -48,7 +55,7 @@ class BusinessInput(BaseModel):
48
  serving_clients_info: str
49
  serving_clients_location: str
50
  num_personas: int = 3
51
-
52
 
53
  class RegenerateRequest(BusinessInput):
54
  """
@@ -58,7 +65,6 @@ class RegenerateRequest(BusinessInput):
58
  """
59
  previous_personas: List[Persona]
60
 
61
-
62
  class HeadingsRequest(BaseModel):
63
  business_name: str = Field(..., example="GrowthAspired")
64
  business_category: str = Field(..., example="Software House")
@@ -69,11 +75,9 @@ class HeadingsRequest(BaseModel):
69
  main_goal: GoalEnum = Field(..., description="Primary marketing goal (enum)")
70
  serving_clients_info: str
71
  serving_clients_location: str
72
- # list of previously generated or selected persona objects (use Persona model)
73
  selected_personas: List[Persona] = Field(..., description="List of selected persona objects to target")
74
- # optional: prefer number of headings (defaults to 4)
75
  num_headings: Optional[int] = Field(4, description="How many headings to generate")
76
-
77
 
78
  class DescriptionsRequest(BaseModel):
79
  business_name: str = Field(..., example="GrowthAspired")
@@ -87,7 +91,7 @@ class DescriptionsRequest(BaseModel):
87
  serving_clients_location: str
88
  selected_personas: List[Persona] = Field(..., description="List of selected persona objects to target")
89
  num_descriptions: Optional[int] = Field(4, description="How many ad descriptions to generate (default 4)")
90
-
91
 
92
  class ImageRequest(BaseModel):
93
  business_name: str = Field(..., example="GrowthAspired")
@@ -100,24 +104,37 @@ class ImageRequest(BaseModel):
100
  serving_clients_info: str
101
  serving_clients_location: str
102
  selected_personas: List[Persona] = Field(..., description="List of selected persona objects to target")
103
- # optional style/size params
104
  style: Optional[str] = Field("modern", description="Desired art style or mood (e.g. modern, minimal, illustrative)")
105
  width: Optional[int] = Field(1200, description="Image width in px")
106
  height: Optional[int] = Field(628, description="Image height in px")
 
107
 
108
-
109
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  class BudgetType(str, Enum):
111
  daily = "daily"
112
  lifetime = "lifetime"
113
 
114
-
115
  class BudgetPlan(BaseModel):
116
  type: BudgetType
117
  budget: str
118
  duration: str
119
 
120
-
121
  class BudgetRequest(BaseModel):
122
  business_name: str = Field(..., example="GrowthAspired")
123
  business_category: str = Field(..., example="Software House")
@@ -129,5 +146,5 @@ class BudgetRequest(BaseModel):
129
  serving_clients_info: str
130
  serving_clients_location: str
131
  selected_personas: List[Persona] = Field(..., description="List of selected persona objects to target")
132
- # optional override if you want more/less options in future
133
  num_options: Optional[int] = Field(2, description="Number of budget option groups to return (default 2)")
 
 
1
+ # app/ads/schemas.py
2
  from enum import Enum
3
  from pydantic import BaseModel, Field
4
  from typing import List, Optional
 
28
  obj.description = description
29
  return obj
30
 
31
+ class ToneEnum(str, Enum):
32
+ PROFESSIONAL = "Professional"
33
+ CASUAL_FRIENDLY = "Casual / Friendly"
34
+ BOLD_PERSUASIVE = "Bold / Persuasive"
35
+ INSPIRING_VISIONARY = "Inspiring / Visionary"
36
+
37
  class Persona(BaseModel):
38
  uuid: str = Field(default_factory=lambda: str(uuid4()), description="Unique identifier for this persona")
39
  flag: bool = Field(False, description="Boolean flag for client use (defaults to False)")
 
55
  serving_clients_info: str
56
  serving_clients_location: str
57
  num_personas: int = 3
58
+ tone: Optional[ToneEnum] = Field(ToneEnum.PROFESSIONAL, description="Tone to use when generating content. Defaults to 'Professional'.")
59
 
60
  class RegenerateRequest(BusinessInput):
61
  """
 
65
  """
66
  previous_personas: List[Persona]
67
 
 
68
  class HeadingsRequest(BaseModel):
69
  business_name: str = Field(..., example="GrowthAspired")
70
  business_category: str = Field(..., example="Software House")
 
75
  main_goal: GoalEnum = Field(..., description="Primary marketing goal (enum)")
76
  serving_clients_info: str
77
  serving_clients_location: str
 
78
  selected_personas: List[Persona] = Field(..., description="List of selected persona objects to target")
 
79
  num_headings: Optional[int] = Field(4, description="How many headings to generate")
80
+ tone: Optional[ToneEnum] = Field(ToneEnum.PROFESSIONAL, description="Tone to use for headings. Defaults to 'Professional'.")
81
 
82
  class DescriptionsRequest(BaseModel):
83
  business_name: str = Field(..., example="GrowthAspired")
 
91
  serving_clients_location: str
92
  selected_personas: List[Persona] = Field(..., description="List of selected persona objects to target")
93
  num_descriptions: Optional[int] = Field(4, description="How many ad descriptions to generate (default 4)")
94
+ tone: Optional[ToneEnum] = Field(ToneEnum.PROFESSIONAL, description="Tone to use for descriptions. Defaults to 'Professional'.")
95
 
96
  class ImageRequest(BaseModel):
97
  business_name: str = Field(..., example="GrowthAspired")
 
104
  serving_clients_info: str
105
  serving_clients_location: str
106
  selected_personas: List[Persona] = Field(..., description="List of selected persona objects to target")
 
107
  style: Optional[str] = Field("modern", description="Desired art style or mood (e.g. modern, minimal, illustrative)")
108
  width: Optional[int] = Field(1200, description="Image width in px")
109
  height: Optional[int] = Field(628, description="Image height in px")
110
+ tone: Optional[ToneEnum] = Field(ToneEnum.PROFESSIONAL, description="Tone for image copy or captions. Optional.")
111
 
112
+ # --- NEW FIELDS TO ADD ---
113
+ cta_text: Optional[str] = Field(
114
+ None,
115
+ description="The exact call-to-action text for the banner (e.g., 'Learn More', 'Get Free Call').",
116
+ example="Get Your Free Strategy Call"
117
+ )
118
+ brand_colors: Optional[List[str]] = Field(
119
+ None,
120
+ description="A list of primary brand color hex codes.",
121
+ example=["#0D47A1", "#FFC107"]
122
+ )
123
+ visual_preference: Optional[str] = Field(
124
+ None,
125
+ description="Specific request for the main visual (e.g., 'Show a graph of growth', 'Show a professional consultant').",
126
+ example="Show a graph representing website traffic growth"
127
+ )
128
+
129
  class BudgetType(str, Enum):
130
  daily = "daily"
131
  lifetime = "lifetime"
132
 
 
133
  class BudgetPlan(BaseModel):
134
  type: BudgetType
135
  budget: str
136
  duration: str
137
 
 
138
  class BudgetRequest(BaseModel):
139
  business_name: str = Field(..., example="GrowthAspired")
140
  business_category: str = Field(..., example="Software House")
 
146
  serving_clients_info: str
147
  serving_clients_location: str
148
  selected_personas: List[Persona] = Field(..., description="List of selected persona objects to target")
 
149
  num_options: Optional[int] = Field(2, description="Number of budget option groups to return (default 2)")
150
+ tone: Optional[ToneEnum] = Field(ToneEnum.PROFESSIONAL, description="Tone for budget explanations (optional).")
app/main.py CHANGED
@@ -22,7 +22,6 @@ from app.keywords.routes import router as keywords_router
22
  from app.uiux import routes as uiux_routes
23
  from app.mobile_usability import routes as mobile_usability
24
  from app.ads.persona_routes import router as persona_router
25
- from app.ads.budget_routes import router as budget_router
26
 
27
  # ─────────────────────────────────────────────
28
  # Suppress warnings
@@ -85,7 +84,6 @@ app.include_router(keywords_router)
85
  app.include_router(uiux_routes.router)
86
  app.include_router(mobile_usability.router)
87
  app.include_router(persona_router)
88
- app.include_router(budget_router)
89
 
90
 
91
  # CORS
 
22
  from app.uiux import routes as uiux_routes
23
  from app.mobile_usability import routes as mobile_usability
24
  from app.ads.persona_routes import router as persona_router
 
25
 
26
  # ─────────────────────────────────────────────
27
  # Suppress warnings
 
84
  app.include_router(uiux_routes.router)
85
  app.include_router(mobile_usability.router)
86
  app.include_router(persona_router)
 
87
 
88
 
89
  # CORS