"""AI engine — Gemini for copy, Nano Banana / Pro for image-to-image product shots. Image generation uses the REAL scraped product image as a reference. Gemini receives the actual photo and generates ecommerce-grade variations that maintain product consistency. Models: Text: gemini-2.5-flash Image: gemini-2.5-flash-image — Nano Banana (fast edits) Image: gemini-3-pro-image-preview — Nano Banana Pro (premium product photography) """ import os, json, hashlib, re, io, time from pathlib import Path import requests as http_requests 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 HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" } # ── Helpers ────────────────────────────────────────────────── 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 _download_image(url: str) -> tuple: """Download image, return (bytes, mime_type) or (None, None).""" try: r = http_requests.get(url, headers=HEADERS, timeout=15) r.raise_for_status() ct = r.headers.get("content-type", "image/jpeg") mime = ct.split(";")[0].strip() if "image" not in mime: mime = "image/jpeg" return (r.content, mime) except Exception as e: print(f"[download] Failed {url}: {e}") return (None, None) def _generate_image_from_ref(prompt: str, ref_bytes: bytes, ref_mime: str, model: str = IMG_PRO) -> tuple: """Generate image using a real product photo as reference. Returns (filename, mime_type) or ('', '').""" if not client or not ref_bytes: return ("", "") cache_key = hashlib.md5(f"{model}:{prompt}:{hashlib.md5(ref_bytes).hexdigest()[:8]}".encode()).hexdigest()[:16] for ext in ("png", "jpg", "jpeg", "webp"): 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=[ types.Part.from_bytes(data=ref_bytes, mime_type=ref_mime), 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 ("", "") def _generate_image_text_only(prompt: str, model: str = IMG_PRO) -> tuple: """Fallback: text-only image generation when no reference available.""" if not client: return ("", "") cache_key = hashlib.md5(f"{model}:{prompt}".encode()).hexdigest()[:16] for ext in ("png", "jpg", "jpeg", "webp"): 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-fallback] {model} error: {e}") return ("", "") # ═══════════════════════════════════════════════════════════════ # DEMO A — Conversion-Optimised PDP + Asset Pack # ═══════════════════════════════════════════════════════════════ def generate_asset_pack(product: dict) -> dict: """Generate a full conversion-optimised PDP structure. The output maps directly to a real product detail page layout: hero section, image gallery context, benefit bullets, trust bar, persuasion copy, FAQ, meta SEO, ad hooks, email subjects. """ prompt = f"""You are a senior ecommerce conversion strategist who has optimised PDPs for brands doing £50M+/yr. You're writing for Just Vitamins (justvitamins.co.uk) — a trusted UK supplement brand: - 4.8★ Trustpilot (230,000+ customers) - 20 years trading - Eco bio-pouch packaging - UK-made, GMP certified 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','')} - Category: {product.get('category','')} - Benefits: {json.dumps(product.get('benefits',[]))} - Description: {product.get('description','')[:2000]} - EFSA Health Claims: {json.dumps(product.get('health_claims',[]))} Generate a conversion-optimised PDP structure following these ecommerce best practices: 1. ABOVE THE FOLD: Hero headline + value prop must stop the scroll 2. GALLERY CONTEXT: Captions for each product image (main, lifestyle, scale, ingredients) 3. BENEFIT BULLETS: Feature → Benefit → Proof format. Max 6, icon-ready. 4. TRUST BAR: 4 trust signals (Trustpilot, years, GMP, eco) 5. PERSUASION SECTION: "Why this formula" — science-led, 2-3 short paragraphs 6. SOCIAL PROOF: Real-format review quote + stats 7. PRICE FRAMING: Reframe as daily cost, compare to coffee/etc 8. URGENCY: Ethical scarcity or time-based nudge 9. FAQ: 4 questions that handle real objections (dosage, interactions, results timeline, quality) 10. CTA: Primary + secondary button text 11. META SEO: Title <60 chars, description <155 chars, primary keyword 12. AD HOOKS: 5 scroll-stopping hooks for Meta/TikTok ads 13. EMAIL: 3 subject lines with preview text for welcome/abandon/restock flows Return JSON: {{ "pdp": {{ "hero_headline": "Conversion-optimised H1", "hero_subhead": "One sentence that makes them stay", "value_props": ["Short prop 1", "Short prop 2", "Short prop 3"], "gallery_captions": {{ "main": "What the buyer sees first — describe ideal main shot context", "lifestyle": "Describe the lifestyle scene this product belongs in", "scale": "Describe a scale/size reference shot", "ingredients": "Describe the close-up ingredients/label shot" }}, "benefit_bullets": [ {{"icon": "emoji", "headline": "Short benefit", "detail": "Why it matters to the buyer", "proof": "Clinical or data point"}}, {{"icon": "emoji", "headline": "…", "detail": "…", "proof": "…"}}, {{"icon": "emoji", "headline": "…", "detail": "…", "proof": "…"}}, {{"icon": "emoji", "headline": "…", "detail": "…", "proof": "…"}}, {{"icon": "emoji", "headline": "…", "detail": "…", "proof": "…"}} ], "trust_signals": [ {{"icon": "⭐", "text": "4.8★ Trustpilot"}}, {{"icon": "🏆", "text": "20 Years Trusted"}}, {{"icon": "🇬🇧", "text": "UK Made · GMP"}}, {{"icon": "🌿", "text": "Eco Bio-Pouch"}} ], "why_section_title": "Why This Formula", "why_paragraphs": ["Science paragraph 1", "Absorption/bioavailability paragraph 2", "Who benefits paragraph 3"], "review_quote": {{"text": "Realistic-sounding 5★ review", "author": "Verified Buyer name", "stars": 5}}, "stats_bar": [ {{"number": "230,000+", "label": "Happy Customers"}}, {{"number": "4.8★", "label": "Trustpilot Rating"}}, {{"number": "20+", "label": "Years Trusted"}} ], "price_display": {{ "main_price": "{product.get('price','')}", "price_per_day": "Daily cost calculation", "comparison": "That's less than a daily coffee", "savings_note": "Subscribe & save note if applicable" }}, "urgency_note": "Ethical urgency message", "cta_primary": "Primary button text", "cta_secondary": "Secondary action text", "faq": [ {{"q": "Dosage question", "a": "Clear answer"}}, {{"q": "Results timeline question", "a": "Honest answer"}}, {{"q": "Quality/safety question", "a": "Trust-building answer"}}, {{"q": "Interaction question", "a": "Responsible answer"}} ], "usage_instructions": "Simple, friction-free how-to-take instructions" }}, "meta_seo": {{ "title": "SEO title under 60 chars", "description": "Meta description under 155 chars with primary keyword", "primary_keyword": "target keyword" }}, "ad_hooks": [ {{"hook": "Scroll-stopping first line", "angle": "What desire it targets", "platform": "Meta/TikTok/Google"}}, {{"hook": "…", "angle": "…", "platform": "…"}}, {{"hook": "…", "angle": "…", "platform": "…"}}, {{"hook": "…", "angle": "…", "platform": "…"}}, {{"hook": "…", "angle": "…", "platform": "…"}} ], "email_subjects": [ {{"flow": "Welcome", "subject": "Under 50 chars", "preview": "Preview text"}}, {{"flow": "Abandon Cart", "subject": "…", "preview": "…"}}, {{"flow": "Restock", "subject": "…", "preview": "…"}} ] }} RULES: - EFSA health claims only — no therapeutic claims - UK English - Be specific to THIS product — no generic filler - Every bullet must pass the "so what?" test - Price framing must use real numbers from the data""" return _call_gemini(prompt, 0.75) # ═══════════════════════════════════════════════════════════════ # IMAGE GENERATION — Reference-based product photography # ═══════════════════════════════════════════════════════════════ def generate_product_images(product: dict) -> dict: """Generate ecommerce product images using the REAL scraped product photo. Downloads the actual product image from justvitamins.co.uk, sends it to Gemini as visual reference, and generates conversion-optimised variations that maintain product consistency. """ title = product.get("title", "vitamin supplement") images = product.get("images", []) results = {"original_images": images} # Download the primary product image as reference ref_bytes, ref_mime = None, None for img_url in images: ref_bytes, ref_mime = _download_image(img_url) if ref_bytes: # Save the original too orig_hash = hashlib.md5(ref_bytes).hexdigest()[:12] ext = "jpg" if "jpeg" in ref_mime else "png" orig_name = f"orig_{orig_hash}.{ext}" orig_path = GEN_DIR / orig_name if not orig_path.exists(): orig_path.write_bytes(ref_bytes) results["original"] = {"filename": orig_name, "mime": ref_mime, "source": img_url} break if not ref_bytes: results["error"] = "Could not download product image from source" return results # ── 1. HERO: Clean white-background product shot ───────── hero_prompt = ( "You are a professional ecommerce product photographer. " "Take this exact product and create a clean, premium product photograph. " "KEEP THE EXACT SAME PRODUCT — same packaging, same label, same colours. " "Place it on a pure white background with soft studio lighting and subtle shadow. " "The product should be centred, well-lit, and sharp. " "This is the main product image for an online store — it must look professional " "and match Amazon/Shopify product photography standards. " "Do NOT change the product, do NOT add text, do NOT alter the packaging." ) fname, mime = _generate_image_from_ref(hero_prompt, ref_bytes, ref_mime, IMG_PRO) results["hero"] = {"filename": fname, "mime": mime, "model": "Nano Banana Pro", "caption": "Main product shot — clean white background, studio lighting"} # ── 2. LIFESTYLE: Product in real-life setting ─────────── lifestyle_prompt = ( "You are a lifestyle product photographer for a premium health brand. " "Take this exact supplement product and photograph it in a beautiful morning " "kitchen scene. Place the EXACT SAME product on a light marble countertop " "with morning sunlight, a glass of water, and fresh green leaves nearby. " "Warm, healthy, inviting mood. Shallow depth of field with product in sharp focus. " "KEEP the product exactly as it is — same packaging, same label. " "Do NOT change, redesign, or alter the product in any way. " "Professional lifestyle product photography for an ecommerce website." ) fname, mime = _generate_image_from_ref(lifestyle_prompt, ref_bytes, ref_mime, IMG_PRO) results["lifestyle"] = {"filename": fname, "mime": mime, "model": "Nano Banana Pro", "caption": "Lifestyle shot — morning kitchen scene, natural light"} # ── 3. SCALE: Product with hand/everyday object ────────── scale_prompt = ( "You are a product photographer creating a scale reference image. " "Show this exact supplement product being held in a person's hand, " "or placed next to a coffee mug for size reference. " "The product must be the EXACT SAME — same packaging, same label, same design. " "Clean, bright lighting. Natural skin tones. " "This helps online shoppers understand the actual size of the product. " "Do NOT modify, redesign, or change the product appearance." ) fname, mime = _generate_image_from_ref(scale_prompt, ref_bytes, ref_mime, IMG_FAST) results["scale"] = {"filename": fname, "mime": mime, "model": "Nano Banana", "caption": "Scale reference — real-world size context"} # ── 4. INGREDIENTS: Close-up detail shot ───────────────── ingredients_prompt = ( "You are a detail product photographer. " "Create a close-up macro shot of this supplement product, focusing on the " "label, ingredients panel, or the capsules/tablets spilling out of the packaging. " "KEEP the exact same product — same packaging, same branding. " "Sharp focus on details, soft bokeh background. " "Premium, trustworthy feel — suitable for a health brand website. " "Do NOT change the product design or branding." ) fname, mime = _generate_image_from_ref(ingredients_prompt, ref_bytes, ref_mime, IMG_FAST) results["ingredients"] = {"filename": fname, "mime": mime, "model": "Nano Banana", "caption": "Detail shot — ingredients & quality close-up"} # ── 5. BANNER: Wide hero banner for category/landing page ─ banner_prompt = ( "You are creating a wide ecommerce hero banner image. " "Place this exact supplement product on the right side of a wide composition. " "The left side should have clean space for text overlay (no text in the image). " "Use a soft gradient background in natural greens and creams. " "Include subtle natural elements — leaves, light rays, bokeh. " "KEEP the product exactly as it is — same packaging, same label. " "Wide aspect ratio, suitable for a website hero banner. " "Do NOT add any text, logos, or modify the product." ) fname, mime = _generate_image_from_ref(banner_prompt, ref_bytes, ref_mime, IMG_PRO) results["banner"] = {"filename": fname, "mime": mime, "model": "Nano Banana Pro", "caption": "Hero banner — wide format for landing pages"} return results # ═══════════════════════════════════════════════════════════════ # 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. Think Huel or Athletic Greens.", "premium": "Premium aspirational voice. Sophisticated language, formulation science focus, target affluent health-conscious buyers. Think Lyma or Seed.", "dr": "Direct-response style. Pattern interrupts, urgency, specific numbers, stacked value, scarcity. Think agora-style health copy.", "medical": "Clinical, medically-safe tone. Proper nomenclature, structure/function claims only, evidence citations. Think Thorne or Pure Encapsulations.", } def pdp_surgeon(product: dict, style: str = "balanced") -> dict: instruction = STYLE_INSTRUCTIONS.get(style, STYLE_INSTRUCTIONS["balanced"]) prompt = f"""You are an elite PDP conversion specialist. You've increased CVR by 30-80% for DTC supplement brands. PRODUCT: - 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 Claims: {json.dumps(product.get('health_claims',[]))} STYLE: {style.upper()} — {instruction} Rewrite the entire PDP in this style. For EVERY element, add a conversion annotation explaining the psychology and estimated lift. Output JSON: {{ "style":"{style}", "title":"Conversion-optimised title", "subtitle":"Desire-triggering subtitle", "hero_copy":"2-3 sentence persuasion paragraph — the most important copy on the page", "hero_annotation":"Why this works — which conversion principle, estimated % lift", "bullets":[ {{"text":"Feature → Benefit → Proof bullet","annotation":"Conversion principle + estimated lift"}}, {{"text":"…","annotation":"…"}}, {{"text":"…","annotation":"…"}}, {{"text":"…","annotation":"…"}}, {{"text":"…","annotation":"…"}} ], "social_proof":"Specific social proof line using 4.8★ Trustpilot, 230K customers, years trading", "social_proof_annotation":"Which social proof principle this uses + estimated lift", "price_reframe":"Reframe the price as a no-brainer — use daily cost, comparison anchoring", "price_annotation":"Price psychology principle + estimated lift", "cta_text":"CTA button text — action-oriented, benefit-driven", "cta_annotation":"Why this CTA works", "usage_instruction":"How to take — written to build routine and reduce friction", "usage_annotation":"How this reduces returns and increases LTV" }} RULES: - EFSA claims only — no disease claims, no cure claims - Realistic lift estimates (5-40% range) - UK English - Every annotation must cite a specific conversion principle (Cialdini, Kahneman, Fogg, etc.)""" 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 — 4.8★ Trustpilot, 230K+ customers, 20 years. PRODUCT: - Title: {product['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',[]))} 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":"…"}}] }} EFSA claims only. UK English.""" return _call_gemini(prompt, 0.7)