v2: Live Flask app — real Gemini AI demos, Nano Banana image gen, real £19.4M data dashboard
- Flask + gunicorn backend replacing static nginx - 3 live AI demos powered by Gemini 2.5 Flash - Nano Banana + Nano Banana Pro for product image generation - Real JV ecommerce dashboard (728K orders, 230K customers, 4MB data) - AI Infrastructure Proposal + Offer pages - Live product scraper for justvitamins.co.uk + competitor pages - API: /api/scrape, /api/generate-pack, /api/competitor-xray, /api/pdp-surgeon, /api/generate-images
This commit is contained in:
328
ai_engine.py
Normal file
328
ai_engine.py
Normal file
@@ -0,0 +1,328 @@
|
||||
"""AI engine — Gemini for copy, Nano Banana / Nano Banana Pro for imagery.
|
||||
|
||||
Models used:
|
||||
Text: gemini-2.5-flash — all copy generation
|
||||
Image: gemini-2.5-flash-image — Nano Banana (fast lifestyle shots)
|
||||
Image: gemini-3-pro-image-preview — Nano Banana Pro (premium hero/product shots)
|
||||
|
||||
Powers:
|
||||
Demo A: generate_asset_pack() — 1 product → 12 marketing assets
|
||||
Demo B: competitor_xray() — competitor URL → analysis + JV upgrade
|
||||
Demo C: pdp_surgeon() — existing copy → style variants
|
||||
PDP: optimise_pdp_copy() — full PDP rewrite
|
||||
Images: generate_all_images() — Nano Banana product imagery
|
||||
"""
|
||||
|
||||
import os, json, hashlib, re
|
||||
from pathlib import Path
|
||||
from google import genai
|
||||
from google.genai import types
|
||||
|
||||
GEMINI_KEY = os.environ.get("GEMINI_API_KEY", "")
|
||||
client = genai.Client(api_key=GEMINI_KEY) if GEMINI_KEY else None
|
||||
|
||||
GEN_DIR = Path(__file__).parent / "generated"
|
||||
GEN_DIR.mkdir(exist_ok=True)
|
||||
|
||||
TEXT_MODEL = "gemini-2.5-flash"
|
||||
IMG_FAST = "gemini-2.5-flash-image" # Nano Banana
|
||||
IMG_PRO = "gemini-3-pro-image-preview" # Nano Banana Pro
|
||||
|
||||
|
||||
def _call_gemini(prompt: str, temperature: float = 0.7) -> dict:
|
||||
"""Call Gemini text model, return parsed JSON."""
|
||||
if not client:
|
||||
return {"error": "GEMINI_API_KEY not configured"}
|
||||
response = client.models.generate_content(
|
||||
model=TEXT_MODEL,
|
||||
contents=prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
temperature=temperature,
|
||||
response_mime_type="application/json",
|
||||
),
|
||||
)
|
||||
try:
|
||||
return json.loads(response.text)
|
||||
except json.JSONDecodeError:
|
||||
match = re.search(r'\{.*\}', response.text, re.DOTALL)
|
||||
if match:
|
||||
return json.loads(match.group())
|
||||
return {"error": "Failed to parse AI response", "raw": response.text[:500]}
|
||||
|
||||
|
||||
def _generate_image(prompt: str, model: str = IMG_PRO) -> tuple:
|
||||
"""Generate image via Nano Banana. Returns (filename, mime_type) or ('','')."""
|
||||
if not client:
|
||||
return ("", "")
|
||||
|
||||
cache_key = hashlib.md5(f"{model}:{prompt}".encode()).hexdigest()[:14]
|
||||
# Check cache
|
||||
for ext in ("png", "jpg", "jpeg"):
|
||||
cached = GEN_DIR / f"{cache_key}.{ext}"
|
||||
if cached.exists():
|
||||
return (cached.name, f"image/{ext}")
|
||||
|
||||
try:
|
||||
response = client.models.generate_content(
|
||||
model=model,
|
||||
contents=prompt,
|
||||
config=types.GenerateContentConfig(
|
||||
response_modalities=["IMAGE", "TEXT"],
|
||||
),
|
||||
)
|
||||
for part in response.candidates[0].content.parts:
|
||||
if part.inline_data and part.inline_data.data:
|
||||
mime = part.inline_data.mime_type or "image/png"
|
||||
ext = "jpg" if "jpeg" in mime or "jpg" in mime else "png"
|
||||
filename = f"{cache_key}.{ext}"
|
||||
(GEN_DIR / filename).write_bytes(part.inline_data.data)
|
||||
return (filename, mime)
|
||||
except Exception as e:
|
||||
print(f"[img-gen] {model} error: {e}")
|
||||
return ("", "")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# DEMO A — One Product → 12 Assets
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def generate_asset_pack(product: dict) -> dict:
|
||||
prompt = f"""You are a world-class ecommerce copywriter and marketing strategist for Just Vitamins (justvitamins.co.uk), a trusted UK vitamin brand — 4.8★ Trustpilot, 230,000+ customers, 20 years trading.
|
||||
|
||||
PRODUCT DATA:
|
||||
- Title: {product.get('title','')}
|
||||
- Subtitle: {product.get('subtitle','')}
|
||||
- Price: {product.get('price','')} for {product.get('quantity','')}
|
||||
- Per unit: {product.get('per_unit_cost','')}
|
||||
- Benefits: {json.dumps(product.get('benefits',[]))}
|
||||
- Description: {product.get('description','')[:1500]}
|
||||
- EFSA Health Claims: {json.dumps(product.get('health_claims',[]))}
|
||||
- Category: {product.get('category','')}
|
||||
|
||||
Generate a COMPLETE 12-asset marketing pack. Be specific to THIS product.
|
||||
|
||||
Return JSON:
|
||||
{{
|
||||
"hero_angles": [
|
||||
{{"headline":"…","target_desire":"…","best_for":"…"}},
|
||||
{{"headline":"…","target_desire":"…","best_for":"…"}},
|
||||
{{"headline":"…","target_desire":"…","best_for":"…"}}
|
||||
],
|
||||
"pdp_copy": {{
|
||||
"headline":"…",
|
||||
"bullets":["…","…","…","…","…"],
|
||||
"faq":[{{"q":"…","a":"…"}},{{"q":"…","a":"…"}},{{"q":"…","a":"…"}}]
|
||||
}},
|
||||
"ad_hooks":["…","…","…","…","…"],
|
||||
"email_subjects":[
|
||||
{{"subject":"…","preview":"…"}},
|
||||
{{"subject":"…","preview":"…"}},
|
||||
{{"subject":"…","preview":"…"}}
|
||||
],
|
||||
"tiktok_script":{{
|
||||
"title":"…",
|
||||
"hook_0_3s":"…",
|
||||
"body_3_12s":"…",
|
||||
"cta_12_15s":"…",
|
||||
"why_it_works":"…"
|
||||
}},
|
||||
"blog_outline":{{
|
||||
"title":"…",
|
||||
"sections":["…","…","…","…","…"],
|
||||
"seo_keyword":"…",
|
||||
"monthly_searches":"…"
|
||||
}},
|
||||
"meta_seo":{{
|
||||
"title":"…","description":"…","title_chars":0,"desc_chars":0
|
||||
}},
|
||||
"alt_text":[
|
||||
{{"image_type":"Hero product shot","alt":"…","filename":"…"}},
|
||||
{{"image_type":"Lifestyle image","alt":"…","filename":"…"}}
|
||||
],
|
||||
"ab_variants":[
|
||||
{{"label":"Rational","copy":"…"}},
|
||||
{{"label":"Emotional","copy":"…"}},
|
||||
{{"label":"Social Proof","copy":"…"}}
|
||||
]
|
||||
}}
|
||||
|
||||
RULES: EFSA claims must be accurate. Email subjects <50 chars. Meta title <60 chars, description <160 chars. UK English."""
|
||||
return _call_gemini(prompt, 0.75)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# DEMO B — Competitor X-Ray
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def competitor_xray(competitor_data: dict) -> dict:
|
||||
prompt = f"""You are a competitive intelligence analyst for Just Vitamins (justvitamins.co.uk) — trusted UK supplement brand, 20 yrs, 4.8★ Trustpilot, 230K+ customers, eco bio-pouch packaging.
|
||||
|
||||
COMPETITOR PAGE:
|
||||
- URL: {competitor_data.get('url','')}
|
||||
- Title: {competitor_data.get('title','')}
|
||||
- Brand: {competitor_data.get('brand','')}
|
||||
- Price: {competitor_data.get('price','')}
|
||||
- Meta: {competitor_data.get('meta_description','')}
|
||||
- Description: {competitor_data.get('description','')[:2000]}
|
||||
- Bullets: {json.dumps(competitor_data.get('bullets',[])[:10])}
|
||||
- Page extract: {competitor_data.get('raw_text','')[:2000]}
|
||||
|
||||
Perform a deep competitive analysis. Output JSON:
|
||||
{{
|
||||
"competitor_name":"…",
|
||||
"what_theyre_selling":"One sentence — what they're REALLY selling (emotional promise, not product)",
|
||||
"top_5_tactics":[
|
||||
{{"tactic":"…","explanation":"…"}},
|
||||
{{"tactic":"…","explanation":"…"}},
|
||||
{{"tactic":"…","explanation":"…"}},
|
||||
{{"tactic":"…","explanation":"…"}},
|
||||
{{"tactic":"…","explanation":"…"}}
|
||||
],
|
||||
"weakest_claim":"Their most vulnerable claim / biggest gap",
|
||||
"jv_hero_section":{{
|
||||
"headline":"Killer headline positioning JV as better",
|
||||
"body":"2-3 sentences of copy that beats them without naming them",
|
||||
"value_prop":"Single most powerful reason to choose JV"
|
||||
}},
|
||||
"differentiators":[
|
||||
{{"point":"…","proof_idea":"Specific content or test idea to prove it"}},
|
||||
{{"point":"…","proof_idea":"…"}},
|
||||
{{"point":"…","proof_idea":"…"}}
|
||||
],
|
||||
"do_not_say":["Compliance note 1","…","…","…"]
|
||||
}}
|
||||
|
||||
RULES: No false claims. EFSA/ASA compliant. Strategic, not aggressive."""
|
||||
return _call_gemini(prompt, 0.7)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# DEMO C — PDP Surgeon
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
STYLE_INSTRUCTIONS = {
|
||||
"balanced": "Balanced, trustworthy DTC supplement voice. Mix emotional hooks with rational proof.",
|
||||
"premium": "Premium aspirational voice. Sophisticated language, formulation science, target affluent buyers.",
|
||||
"dr": "Direct-response style. Pattern interrupts, urgency, specific numbers, scarcity, stacked bonuses.",
|
||||
"medical": "Clinical, medically-safe tone. Proper nomenclature, structure/function claims only, FDA disclaimer.",
|
||||
}
|
||||
|
||||
def pdp_surgeon(product: dict, style: str = "balanced") -> dict:
|
||||
instruction = STYLE_INSTRUCTIONS.get(style, STYLE_INSTRUCTIONS["balanced"])
|
||||
prompt = f"""You are a PDP conversion specialist rewriting a product page for Just Vitamins.
|
||||
|
||||
PRODUCT:
|
||||
- Title: {product.get('title','')}
|
||||
- Subtitle: {product.get('subtitle','')}
|
||||
- Price: {product.get('price','')} for {product.get('quantity','')}
|
||||
- Benefits: {json.dumps(product.get('benefits',[]))}
|
||||
- Description: {product.get('description','')[:1500]}
|
||||
- EFSA Claims: {json.dumps(product.get('health_claims',[]))}
|
||||
|
||||
STYLE: {style.upper()} — {instruction}
|
||||
|
||||
Rewrite the PDP. For EVERY change add a conversion annotation with estimated % impact.
|
||||
|
||||
Output JSON:
|
||||
{{
|
||||
"style":"{style}",
|
||||
"title":"…",
|
||||
"subtitle":"…",
|
||||
"hero_copy":"Main persuasion paragraph",
|
||||
"hero_annotation":"Why this works — conversion impact",
|
||||
"bullets":[
|
||||
{{"text":"…","annotation":"Why — e.g. +22% add-to-cart"}},
|
||||
{{"text":"…","annotation":"…"}},
|
||||
{{"text":"…","annotation":"…"}},
|
||||
{{"text":"…","annotation":"…"}}
|
||||
],
|
||||
"social_proof":"Line using 4.8★ Trustpilot, 230K customers",
|
||||
"social_proof_annotation":"…",
|
||||
"price_reframe":"Reframe price as no-brainer",
|
||||
"price_annotation":"…",
|
||||
"cta_text":"CTA button text",
|
||||
"usage_instruction":"How to take — written to remove friction",
|
||||
"usage_annotation":"…"
|
||||
}}
|
||||
|
||||
RULES: EFSA claims accurate. Realistic % lifts (5-40%). UK English."""
|
||||
return _call_gemini(prompt, 0.8)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# FULL PDP OPTIMISATION
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def optimise_pdp_copy(product: dict) -> dict:
|
||||
prompt = f"""You are an expert ecommerce copywriter for Just Vitamins (justvitamins.co.uk) — 4.8★ Trustpilot, 230K+ customers, 20 years.
|
||||
|
||||
PRODUCT:
|
||||
- Title: {product['title']}
|
||||
- Subtitle: {product.get('subtitle','')}
|
||||
- Price: {product.get('price','')} for {product.get('quantity','')}
|
||||
- Per unit: {product.get('per_unit_cost','')}
|
||||
- Benefits: {json.dumps(product.get('benefits',[]))}
|
||||
- Description: {product.get('description','')[:1500]}
|
||||
- EFSA Claims: {json.dumps(product.get('health_claims',[]))}
|
||||
- Category: {product.get('category','')}
|
||||
|
||||
Rewrite everything. Output JSON:
|
||||
{{
|
||||
"seo_title":"…",
|
||||
"subtitle":"…",
|
||||
"benefit_bullets":["…","…","…","…","…"],
|
||||
"why_section":"Para 1\\n\\nPara 2\\n\\nPara 3",
|
||||
"who_for":["…","…","…"],
|
||||
"social_proof":"…",
|
||||
"meta_description":"…",
|
||||
"faqs":[{{"q":"…","a":"…"}},{{"q":"…","a":"…"}},{{"q":"…","a":"…"}}]
|
||||
}}
|
||||
|
||||
Keep EFSA claims accurate. UK English."""
|
||||
return _call_gemini(prompt, 0.7)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# IMAGE GENERATION — Nano Banana / Pro
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def generate_product_images(product: dict) -> dict:
|
||||
"""Generate product images using Nano Banana (fast) + Nano Banana Pro (hero)."""
|
||||
title = product.get("title", "vitamin supplement")
|
||||
category = product.get("category", "vitamins")
|
||||
results = {}
|
||||
|
||||
# Hero — use Nano Banana Pro for best quality
|
||||
hero_prompt = (
|
||||
f"Professional product hero photograph for an ecommerce page. "
|
||||
f"A premium eco-friendly kraft bio-pouch supplement packaging for '{title}' "
|
||||
f"by Just Vitamins UK. Clean cream/white background, soft natural shadows, "
|
||||
f"minimalist staging with a small ceramic dish of capsules/tablets beside it "
|
||||
f"and a sprig of green herb. Premium, trustworthy, modern. "
|
||||
f"Commercial product photography, sharp focus, studio lighting."
|
||||
)
|
||||
fname, mime = _generate_image(hero_prompt, IMG_PRO)
|
||||
results["hero"] = {"filename": fname, "mime": mime, "model": "Nano Banana Pro"}
|
||||
|
||||
# Lifestyle — Nano Banana (fast)
|
||||
lifestyle_prompt = (
|
||||
f"Lifestyle product photograph: '{title}' by Just Vitamins UK in an eco "
|
||||
f"bio-pouch sitting on a marble kitchen counter with morning sunlight "
|
||||
f"streaming through a window. Fresh fruit, a glass of water, and green "
|
||||
f"leaves nearby. Warm, healthy, inviting mood. Shallow depth of field. "
|
||||
f"Photorealistic, 4K quality."
|
||||
)
|
||||
fname, mime = _generate_image(lifestyle_prompt, IMG_FAST)
|
||||
results["lifestyle"] = {"filename": fname, "mime": mime, "model": "Nano Banana"}
|
||||
|
||||
# Benefits — Nano Banana Pro
|
||||
benefits_prompt = (
|
||||
f"Clean infographic-style illustration showing health benefits of "
|
||||
f"{category} supplements: strong bones, immune support, energy, vitality. "
|
||||
f"Modern flat design, warm gold/green/cream palette. "
|
||||
f"Professional, suitable for a premium UK health brand website. No text."
|
||||
)
|
||||
fname, mime = _generate_image(benefits_prompt, IMG_PRO)
|
||||
results["benefits"] = {"filename": fname, "mime": mime, "model": "Nano Banana Pro"}
|
||||
|
||||
return results
|
||||
Reference in New Issue
Block a user