diff --git a/ai_engine.py b/ai_engine.py index 128059a..e883a8f 100644 --- a/ai_engine.py +++ b/ai_engine.py @@ -1,20 +1,18 @@ -"""AI engine — Gemini for copy, Nano Banana / Nano Banana Pro for imagery. +"""AI engine — Gemini for copy, Nano Banana / Pro for image-to-image product shots. -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) +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. -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 +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 +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 @@ -28,6 +26,13 @@ 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.""" @@ -50,14 +55,30 @@ def _call_gemini(prompt: str, temperature: float = 0.7) -> dict: 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: +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}".encode()).hexdigest()[:14] - # Check cache - for ext in ("png", "jpg", "jpeg"): + 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}") @@ -65,7 +86,10 @@ def _generate_image(prompt: str, model: str = IMG_PRO) -> tuple: try: response = client.models.generate_content( model=model, - contents=prompt, + contents=[ + types.Part.from_bytes(data=ref_bytes, mime_type=ref_mime), + prompt, + ], config=types.GenerateContentConfig( response_modalities=["IMAGE", "TEXT"], ), @@ -82,74 +106,268 @@ def _generate_image(prompt: str, model: str = IMG_PRO) -> tuple: 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 — One Product → 12 Assets +# DEMO A — Conversion-Optimised PDP + Asset Pack # ═══════════════════════════════════════════════════════════════ 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. + """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','')} -- Benefits: {json.dumps(product.get('benefits',[]))} -- Description: {product.get('description','')[:1500]} -- EFSA Health Claims: {json.dumps(product.get('health_claims',[]))} - 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 COMPLETE 12-asset marketing pack. Be specific to THIS product. +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: {{ - "hero_angles": [ - {{"headline":"…","target_desire":"…","best_for":"…"}}, - {{"headline":"…","target_desire":"…","best_for":"…"}}, - {{"headline":"…","target_desire":"…","best_for":"…"}} + "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": "…"}} ], - "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":"…"}} + "email_subjects": [ + {{"flow": "Welcome", "subject": "Under 50 chars", "preview": "Preview text"}}, + {{"flow": "Abandon Cart", "subject": "…", "preview": "…"}}, + {{"flow": "Restock", "subject": "…", "preview": "…"}} ] }} -RULES: EFSA claims must be accurate. Email subjects <50 chars. Meta title <60 chars, description <160 chars. UK English.""" +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 # ═══════════════════════════════════════════════════════════════ @@ -201,51 +419,58 @@ RULES: No false claims. EFSA/ASA compliant. Strategic, not aggressive.""" # ═══════════════════════════════════════════════════════════════ 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.", + "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 a PDP conversion specialist rewriting a product page for Just Vitamins. + 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 PDP. For EVERY change add a conversion annotation with estimated % impact. +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":"…", - "subtitle":"…", - "hero_copy":"Main persuasion paragraph", - "hero_annotation":"Why this works — conversion impact", + "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":"…","annotation":"Why — e.g. +22% add-to-cart"}}, + {{"text":"Feature → Benefit → Proof bullet","annotation":"Conversion principle + estimated lift"}}, + {{"text":"…","annotation":"…"}}, {{"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":"…" + "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 accurate. Realistic % lifts (5-40%). UK English.""" +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) @@ -254,17 +479,15 @@ RULES: EFSA claims accurate. Realistic % lifts (5-40%). UK English.""" # ═══════════════════════════════════════════════════════════════ 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. + 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','')} -- 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: {{ @@ -278,51 +501,5 @@ Rewrite everything. Output JSON: "faqs":[{{"q":"…","a":"…"}},{{"q":"…","a":"…"}},{{"q":"…","a":"…"}}] }} -Keep EFSA claims accurate. UK English.""" +EFSA claims only. 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 diff --git a/static/css/style.css b/static/css/style.css index a8667b4..ef6c5cf 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -144,6 +144,116 @@ h1{font-size:clamp(2.2rem,5.5vw,3.5rem);font-weight:800;line-height:1.08;letter- footer{padding:1.5rem 0;border-top:1px solid var(--border);display:flex;justify-content:space-between;font-family:'JetBrains Mono',monospace;font-size:.72rem;color:var(--muted)} footer a{color:var(--accent);text-decoration:none} +/* ═══ PDP LAYOUT ═══ */ +.pdp-page{background:var(--card);border:1px solid var(--border);border-radius:14px;overflow:hidden;animation:fadeUp .5s ease} + +/* Above the fold — gallery + info */ +.pdp-atf{display:grid;grid-template-columns:1fr 1fr;gap:0} +.pdp-gallery{background:var(--bg);padding:1.5rem;border-right:1px solid var(--border)} +.pdp-info{padding:1.5rem 2rem} +.pdp-gallery-main{position:relative;border-radius:10px;overflow:hidden;background:#0a0e14;margin-bottom:.75rem;aspect-ratio:1;display:flex;align-items:center;justify-content:center} +.pdp-gallery-main img{max-width:100%;max-height:100%;object-fit:contain} +.pdp-gallery-badge{position:absolute;top:.6rem;left:.6rem;font-family:'JetBrains Mono',monospace;font-size:.6rem;font-weight:700;padding:.2rem .5rem;border-radius:4px;background:rgba(6,10,15,.85);color:var(--accent);border:1px solid var(--border)} +.pdp-thumbs{display:flex;gap:.5rem;overflow-x:auto;padding:.25rem 0} +.pdp-thumb{width:72px;height:72px;border-radius:8px;overflow:hidden;cursor:pointer;border:2px solid transparent;position:relative;flex-shrink:0;background:var(--bg);transition:.2s} +.pdp-thumb.wide{width:110px} +.pdp-thumb.active{border-color:var(--accent)} +.pdp-thumb:hover{border-color:var(--border2)} +.pdp-thumb img{width:100%;height:100%;object-fit:cover} +.pdp-thumb-label{position:absolute;bottom:0;left:0;right:0;font-size:.5rem;font-weight:700;text-align:center;padding:.12rem;font-family:'JetBrains Mono',monospace;background:rgba(6,10,15,.85)} +.pdp-thumb-label.orig{color:var(--muted)} +.pdp-thumb-label.ai{color:var(--cyan)} + +/* Info panel */ +.pdp-breadcrumb{font-family:'JetBrains Mono',monospace;font-size:.68rem;color:var(--muted);margin-bottom:.5rem} +.pdp-title{font-size:1.35rem;font-weight:700;line-height:1.25;margin-bottom:.4rem} +.pdp-subhead{font-size:.9rem;color:var(--text2);margin-bottom:.75rem;line-height:1.5} +.pdp-value-props{display:flex;flex-wrap:wrap;gap:.35rem;margin-bottom:1rem} +.pdp-vp{font-size:.72rem;font-weight:600;color:var(--accent);background:var(--glow);padding:.2rem .5rem;border-radius:4px} + +/* Price block */ +.pdp-price-block{background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:1rem;margin-bottom:1rem} +.pdp-price-main{font-size:1.8rem;font-weight:800;color:var(--text)} +.pdp-price-qty{font-size:.78rem;color:var(--muted);margin-left:.3rem} +.pdp-price-daily{font-size:.82rem;color:var(--accent);font-weight:600;margin-top:.25rem} +.pdp-price-compare{font-size:.75rem;color:var(--muted);margin-top:.1rem} + +/* Trust bar */ +.pdp-trust-bar{display:flex;gap:.6rem;margin-bottom:1rem;flex-wrap:wrap} +.pdp-trust-item{display:flex;align-items:center;gap:.25rem;font-size:.72rem;font-weight:600;color:var(--text2);background:var(--bg);padding:.3rem .6rem;border-radius:6px;border:1px solid var(--border)} +.pdp-trust-icon{font-size:.85rem} + +/* CTAs */ +.pdp-cta-primary{width:100%;padding:.8rem;font-size:.95rem;font-weight:700;border:none;border-radius:9px;cursor:pointer;background:var(--accent);color:#060a0f;margin-bottom:.5rem;transition:.2s} +.pdp-cta-primary:hover{background:var(--accent2);transform:translateY(-1px)} +.pdp-cta-secondary{width:100%;padding:.6rem;font-size:.82rem;font-weight:600;border:1px solid var(--border2);border-radius:9px;cursor:pointer;background:transparent;color:var(--text2);margin-bottom:.75rem;transition:.2s} +.pdp-cta-secondary:hover{border-color:var(--accent);color:var(--accent)} +.pdp-urgency{font-family:'JetBrains Mono',monospace;font-size:.72rem;color:var(--gold);background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.15);padding:.4rem .7rem;border-radius:6px;margin-bottom:.75rem;text-align:center} +.pdp-usage{font-size:.78rem;color:var(--muted);line-height:1.6;padding-top:.5rem;border-top:1px solid var(--border)} + +/* Sections below fold */ +.pdp-section{padding:2rem 2.5rem;border-top:1px solid var(--border)} +.pdp-section h3{font-size:1.1rem;font-weight:700;margin-bottom:1rem} + +/* Benefit bullets */ +.pdp-bullets{display:grid;grid-template-columns:1fr 1fr;gap:.75rem} +.pdp-bullet{display:flex;gap:.6rem;padding:.75rem;background:var(--bg);border:1px solid var(--border);border-radius:9px} +.pdp-bullet-icon{font-size:1.3rem;flex-shrink:0;margin-top:.1rem} +.pdp-bullet strong{font-size:.84rem;color:var(--text)} +.pdp-bullet-detail{font-size:.78rem;color:var(--text2);display:block;margin-top:.15rem} +.pdp-bullet-proof{font-family:'JetBrains Mono',monospace;font-size:.65rem;color:var(--cyan);display:block;margin-top:.25rem} + +/* Stats bar */ +.pdp-stats-bar{display:flex;justify-content:center;gap:3rem;padding:1.5rem 2rem;background:var(--bg);border-top:1px solid var(--border)} +.pdp-stat{text-align:center} +.pdp-stat-num{font-family:'JetBrains Mono',monospace;font-size:1.4rem;font-weight:800;color:var(--accent);display:block} +.pdp-stat-label{font-size:.7rem;color:var(--muted)} + +/* Why section */ +.pdp-why p{font-size:.88rem;color:var(--text2);line-height:1.7;margin-bottom:.75rem} + +/* Review */ +.pdp-review-section{padding:2rem 2.5rem;border-top:1px solid var(--border);background:var(--bg)} +.pdp-review-card{max-width:600px;margin:0 auto;text-align:center} +.pdp-review-stars{font-size:1.3rem;color:var(--gold);margin-bottom:.5rem;letter-spacing:2px} +.pdp-review-text{font-size:.95rem;color:var(--text2);line-height:1.6;font-style:italic;margin-bottom:.5rem} +.pdp-review-author{font-family:'JetBrains Mono',monospace;font-size:.72rem;color:var(--muted)} + +/* FAQ */ +.pdp-faq{max-width:700px} +.pdp-faq-item{border:1px solid var(--border);border-radius:9px;margin-bottom:.5rem;overflow:hidden} +.pdp-faq-item summary{padding:.75rem 1rem;font-size:.88rem;font-weight:600;cursor:pointer;list-style:none;display:flex;align-items:center;justify-content:space-between} +.pdp-faq-item summary::after{content:'+';font-size:.9rem;color:var(--muted)} +.pdp-faq-item[open] summary::after{content:'−'} +.pdp-faq-item p{padding:0 1rem .75rem;font-size:.82rem;color:var(--text2);line-height:1.6} + +/* Divider */ +.pdp-divider{text-align:center;padding:1.5rem;border-top:1px solid var(--border);font-family:'JetBrains Mono',monospace;font-size:.75rem;color:var(--muted)} + +/* Asset sections */ +.pdp-asset-section{padding:1.5rem 2.5rem;border-top:1px solid var(--border)} +.pdp-asset-section h4{font-size:.9rem;font-weight:700;margin-bottom:.75rem} + +/* SEO preview */ +.pdp-seo{background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:1rem;max-width:600px} +.pdp-seo-title{font-size:1.05rem;color:var(--blue);font-weight:500;margin-bottom:.1rem} +.pdp-seo-url{font-size:.72rem;color:var(--accent);margin-bottom:.25rem} +.pdp-seo-desc{font-size:.82rem;color:var(--text2);line-height:1.5} + +/* Ad hooks */ +.pdp-hooks-grid{display:grid;grid-template-columns:1fr 1fr;gap:.5rem} +.pdp-hook{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:.75rem} +.pdp-hook-text{font-size:.84rem;font-weight:600;color:var(--text);line-height:1.4;margin-bottom:.3rem} +.pdp-hook-meta{font-family:'JetBrains Mono',monospace;font-size:.6rem;color:var(--muted)} + +/* Email */ +.pdp-emails{display:flex;flex-direction:column;gap:.5rem} +.pdp-email{display:flex;align-items:baseline;gap:.6rem;padding:.6rem .8rem;background:var(--bg);border:1px solid var(--border);border-radius:8px;font-size:.82rem} +.pdp-email-flow{font-family:'JetBrains Mono',monospace;font-size:.6rem;font-weight:700;color:var(--purple);text-transform:uppercase;background:rgba(139,92,246,.12);padding:.15rem .4rem;border-radius:3px;flex-shrink:0} +.pdp-email strong{color:var(--text)} +.pdp-email-preview{color:var(--muted);font-size:.75rem} + /* Responsive */ +@media(max-width:900px){.pdp-atf{grid-template-columns:1fr}.pdp-gallery{border-right:none;border-bottom:1px solid var(--border)}.pdp-bullets{grid-template-columns:1fr}.pdp-hooks-grid{grid-template-columns:1fr}.pdp-stats-bar{gap:1.5rem}} @media(max-width:800px){.input-row{flex-direction:column}.btn-gen{width:100%}.split{grid-template-columns:1fr}.split-left{border-right:none;border-bottom:1px solid var(--border)}.stats{gap:1.25rem}.step-bar{flex-direction:column;gap:.4rem}footer{flex-direction:column;gap:.3rem;text-align:center}} -@media(max-width:500px){.nav-links a{font-size:.68rem;padding:.25rem .4rem}.btn-row{flex-direction:column}.btn{width:100%;justify-content:center}} +@media(max-width:500px){.nav-links a{font-size:.68rem;padding:.25rem .4rem}.btn-row{flex-direction:column}.btn{width:100%;justify-content:center}.pdp-section{padding:1.25rem 1rem}.pdp-asset-section{padding:1.25rem 1rem}} diff --git a/static/js/app.js b/static/js/app.js index bb444be..57e590e 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1,4 +1,5 @@ -/* JustVitamin × QuikCue — Live AI Demos */ +/* JustVitamin × QuikCue — Live AI Demos + Demo A renders as a real conversion-optimised PDP page */ const $ = s => document.querySelector(s); const esc = s => { if (!s) return ''; const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }; @@ -9,7 +10,6 @@ function setStep(id, cls, text) { el.parentElement.className = `step ${cls}`; el.innerHTML = text; } - function setBtn(id, loading, text) { const btn = $(id); btn.disabled = loading; @@ -18,10 +18,12 @@ function setBtn(id, loading, text) { } // ═══════════════════════════════════════════════════════════════ -// DEMO A — One Product → 12 Assets + Images +// DEMO A — Product URL → Full PDP + Real Product Images // ═══════════════════════════════════════════════════════════════ let demoA_product = null; +let demoA_pack = null; +let demoA_imgs = null; async function runDemoA() { const url = $('#demoA-url').value.trim(); @@ -32,7 +34,7 @@ async function runDemoA() { ['#a-s1','#a-s2','#a-s3'].forEach(s => setStep(s, '', 'Waiting')); // Step 1: Scrape - setStep('#a-s1', 'active', ' Scraping...'); + setStep('#a-s1', 'active', 'Scraping product page...'); try { const r = await fetch('/api/scrape', { method: 'POST', headers: {'Content-Type':'application/json'}, @@ -41,16 +43,16 @@ async function runDemoA() { const product = await r.json(); if (product.error) throw new Error(product.error); demoA_product = product; - setStep('#a-s1', 'done', `✓ ${esc(product.title.substring(0,35))}...`); + setStep('#a-s1', 'done', `✓ ${esc(product.title.substring(0,40))}...`); } catch(e) { setStep('#a-s1', 'error', `✗ ${esc(e.message)}`); setBtn('#demoA-btn', false, '🔴 Generate the Whole Pack'); return; } - // Step 2: AI pack + Step 3: Images (parallel) - setStep('#a-s2', 'active', ' Gemini generating 12 assets...'); - setStep('#a-s3', 'active', ' Nano Banana generating images...'); + // Step 2 + 3: AI copy + images in parallel + setStep('#a-s2', 'active', 'Gemini generating PDP copy...'); + setStep('#a-s3', 'active', 'Generating product images from real photo...'); const packP = fetch('/api/generate-pack', { method: 'POST', headers: {'Content-Type':'application/json'}, @@ -62,164 +64,259 @@ async function runDemoA() { body: JSON.stringify(demoA_product) }).then(r => r.json()); - // Handle pack + let packOk = false, imgsOk = false; + try { - const pack = await packP; - if (pack.error) throw new Error(pack.error); - setStep('#a-s2', 'done', `✓ 12 assets generated (${pack._generation_time})`); - renderAssetPack(pack); - $('#demoA-output').classList.remove('hidden'); + demoA_pack = await packP; + if (demoA_pack.error) throw new Error(demoA_pack.error); + packOk = true; + setStep('#a-s2', 'done', `✓ PDP + assets generated (${demoA_pack._generation_time})`); } catch(e) { setStep('#a-s2', 'error', `✗ ${esc(e.message)}`); } - // Handle images try { - const imgs = await imgP; - if (imgs.error) throw new Error(imgs.error); - setStep('#a-s3', 'done', `✓ Images generated (${imgs._generation_time})`); - renderImages(imgs); + demoA_imgs = await imgP; + if (demoA_imgs.error && !demoA_imgs.original) throw new Error(demoA_imgs.error); + imgsOk = true; + setStep('#a-s3', 'done', `✓ Product images generated (${demoA_imgs._generation_time || ''})`); } catch(e) { setStep('#a-s3', 'error', `✗ ${esc(e.message)}`); } + if (packOk) { + renderPDP(demoA_product, demoA_pack, demoA_imgs); + $('#demoA-output').classList.remove('hidden'); + } + setBtn('#demoA-btn', false, '✓ Done — Run Again'); } -function renderAssetPack(pack) { - const meta = $('#demoA-meta'); - meta.innerHTML = ` - 🧠 Model: Gemini 2.5 Flash - ⏱ Time: ${esc(pack._generation_time)} - 📦 Product: ${esc(pack._product_title)} - `; - const grid = $('#demoA-assets'); - let cards = ''; - let n = 0; +// ── Render as a real PDP page ─────────────────────────────── - // Hero angles - (pack.hero_angles || []).forEach((a, i) => { - n++; - cards += assetCard('hero', `Hero Angle`, n, `"${esc(a.headline)}"
${esc(p)}
`).join(''); + + const statsBar = (pdp.stats_bar || []).map(s => + `${esc(f.a)}
+${esc(a.filename)}${esc(pdp.hero_subhead || product.subtitle)}
- grid.innerHTML = cards; +"${esc(review.text || '')}"
+ +${esc(hero.body)}
-${esc(hero.value_prop)}
-${esc(hero.body)}
+${esc(hero.value_prop)}
${esc(product.subtitle)}
-${esc(product.price)} ${esc(product.quantity)}
-${esc((product.description||'').substring(0,400))}...
- `; +${esc(product.subtitle)}
+${esc(product.price)} ${esc(product.quantity)}
+${esc(result.subtitle)}
- + ↑ SEO title +${esc(result.subtitle)}
${esc(result.usage_instruction)}
+${esc(result.usage_instruction)}
↑ ${esc(result.usage_annotation)} - -Paste a real Just Vitamins product URL. Gemini scrapes it, then generates a full marketing pack.
+Paste a real Just Vitamins product URL. We scrape the actual product images, then Gemini generates a complete conversion-optimised PDP with real product photography variations — not invented images.