diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..d186725 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "Creative Analyzer (app.py)", + "runtimeExecutable": "/Users/anilpandit/tribe_env/bin/streamlit", + "runtimeArgs": ["run", "app.py", "--server.port=8501"], + "port": 8501 + }, + { + "name": "Demo App (demo_app.py)", + "runtimeExecutable": "/Users/anilpandit/tribe_env/bin/streamlit", + "runtimeArgs": ["run", "demo_app.py", "--server.port=8502"], + "port": 8502 + } + ] +} diff --git a/app.py b/app.py new file mode 100644 index 0000000..b41d413 --- /dev/null +++ b/app.py @@ -0,0 +1,6682 @@ +""" +app.py — Creative Intelligence Analyzer +---------------------------------------- +Supports single-creative analysis AND multi-creative comparison (2–5 images). + +Pipeline (per creative): + upload → save_upload() → analyze_creative() → map_to_cognitive_signals() + → compute_cpci() → show_results() / show_comparison() +""" + +import os +import json +import time +import hashlib +import colorsys + +import streamlit as st + +from creative_vision import analyze_creative +from cognitive_signals import map_to_cognitive_signals +from narrative_engine import generate_narrative +from science_tab import show_science_tab, show_glossary_tab + + +# ── Page config ─────────────────────────────────────────────────────────────── + +st.set_page_config( + page_title="Creative Intelligence Analyzer", + page_icon="🧠", + layout="wide", +) + +# ── Global CSS ──────────────────────────────────────────────────────────────── + +st.markdown(""" + +""", unsafe_allow_html=True) + + +# ── Tooltip helper ──────────────────────────────────────────────────────────── + +def _tooltip(label: str, title: str, bullets: list) -> str: + """Return inline HTML: label + ⓘ that reveals a tooltip card on hover.""" + pts = "".join(f"{b}" for b in bullets) + return ( + f"{label}" + f"" + f"{title}{pts}" + f"" + ) + +# Pre-built tooltip HTML for the 4 tracked metrics +_TT_ATTN = _tooltip("Attention", "Attention Score", [ + "→ Measures visual stopping power", + "→ Based on contrast, faces & clutter", + "→ Predicts scroll-stop probability", +]) +_TT_MEM = _tooltip("Memory", "Memory Score", [ + "→ Measures brand recall potential", + "→ Based on text density & visual simplicity", + "→ Drives recognition at point of purchase", +]) +_TT_VAL = _tooltip("Emotion", "Emotional Valence", [ + "→ Measures positive vs. negative tone", + "→ Derived from face expression & color warmth", + "→ Shapes brand affinity & purchase intent", +]) +_TT_CPCI = _tooltip("CPCi", "Cost Per Cognitive Impression", [ + "→ Composite cognitive impact score (0–100)", + "→ Weighted blend of Attention + Memory + Emotion", + "→ Predicts ad effectiveness before media spend", +]) + +# ── Core pipeline ───────────────────────────────────────────────────────────── + +UPLOAD_DIR = os.path.expanduser("~/tribev2/uploads") + +def save_upload(uploaded_file) -> str: + """Save uploaded file to a permanent MD5-hashed path. Returns absolute path.""" + os.makedirs(UPLOAD_DIR, exist_ok=True) + file_bytes = uploaded_file.read() + content_hash = hashlib.md5(file_bytes).hexdigest()[:10] + stem = os.path.splitext(uploaded_file.name)[0].replace(" ", "_") + ext = os.path.splitext(uploaded_file.name)[-1].lower() + path = os.path.join(UPLOAD_DIR, f"{stem}_{content_hash}{ext}") + if not os.path.exists(path): + with open(path, "wb") as f: + f.write(file_bytes) + return path + + +# ── Use-case config ─────────────────────────────────────────────────────────── + +USE_CASES = { + "FMCG Branding": { + "icon": "🛒", + "description": "Long-term brand salience · emotional recall · shelf recognition", + "accent": "#CBD5E1", + "weights": { + "attention": 0.20, + "memory": 0.50, + "emotion": 0.30, + }, + # No cognitive-load penalty — FMCG audiences are patient shoppers + "load_penalty": False, + "rationale": ( + "FMCG success depends on memory encoding (recognition at point of purchase) " + "and emotional warmth (brand affinity). Attention matters less — shoppers scan " + "shelves slowly. Emotion drives trial and repeat purchase." + ), + }, + "Performance Marketing": { + "icon": "🎯", + "description": "Direct response · click-through · immediate conversion", + "accent": "#3B82F6", + "weights": { + "attention": 0.50, + "memory": 0.30, + "emotion": 0.20, + }, + "load_penalty": False, + "rationale": ( + "Performance creatives must stop the scroll first — attention is the primary " + "lever for click-through rate. Memory matters for retargeting sequences. " + "Emotion plays a smaller role; the CTA does the conversion work." + ), + }, + "Retail Media": { + "icon": "🏪", + "description": "In-store / on-platform · price-sensitive · high-clutter context", + "accent": "#22C55E", + "weights": { + "attention": 0.40, + "memory": 0.30, + "emotion": 0.30, + }, + # Retail environments are visually noisy — high cognitive load is extra damaging + "load_penalty": True, + "rationale": ( + "Retail media sits in a high-clutter environment (product carousels, price tags, " + "competing banners). A cognitive load penalty is applied because confused shoppers " + "don't buy. Attention and emotion are weighted equally — both drive the impulse " + "purchase decision." + ), + }, +} + +LOAD_PENALTY_MAP = {"Low": 0, "Medium": -5, "High": -12} + + +def compute_cpci(signals: dict, weights: dict = None, apply_load_penalty: bool = False) -> float: + """ + CPCi = w_attn × Attention + w_mem × Memory + w_emotion × Valence_norm + [− load_penalty if apply_load_penalty] + + Args: + signals: Output of map_to_cognitive_signals(). + weights: Dict with keys "attention", "memory", "emotion". + Defaults to balanced Performance Marketing weights. + apply_load_penalty: If True, subtracts 0/5/12 pts for Low/Medium/High load. + + Valence is normalised from [-1, +1] → [0, 100] before weighting so all + three inputs live on the same 0–100 scale. + """ + if weights is None: + weights = {"attention": 0.40, "memory": 0.40, "emotion": 0.20} + + # ── Guard: reject None signals immediately ──────────────────────────────── + attn = signals.get("attention_score") + mem = signals.get("memory_score") + val = signals.get("emotional_valence") + cl = signals.get("cognitive_load", "Medium") + + if attn is None or mem is None or val is None: + raise ValueError( + f"compute_cpci: signal is None — " + f"attention={attn}, memory={mem}, valence={val}. " + "Check map_to_cognitive_signals() output." + ) + + # ── Debug print (remove once stable) ───────────────────────────────────── + print(f"[CPCi] attention={attn}, memory={mem}, valence={val}, weights={weights}") + + valence_norm = (val + 1) / 2 * 100 # [-1,+1] → [0,100] + + raw = ( + weights["attention"] * attn + + weights["memory"] * mem + + weights["emotion"] * valence_norm + ) + + if apply_load_penalty: + raw += LOAD_PENALTY_MAP.get(cl, 0) + + result = round(max(0, min(100, raw)), 1) + print(f"[CPCi] raw={raw:.2f} → cpci={result}") + return result + + +def run_pipeline( + uploaded_file, + weights: dict = None, + apply_load_penalty: bool = False, + use_case: str = "Performance Marketing", +) -> dict: + """ + Full analysis pipeline for one creative. + Generates cognitive signals, CPCi, and the narrative intelligence report. + """ + file_path = save_upload(uploaded_file) + features = analyze_creative(file_path) + signals_raw = map_to_cognitive_signals(features) + reasoning = signals_raw.pop("_reasoning") + cpci = compute_cpci(signals_raw, weights, apply_load_penalty) + narrative = generate_narrative( + features = features, + signals = signals_raw, + cpci = cpci, + use_case = use_case, + reasoning = reasoning, + ) + return { + "name": uploaded_file.name, + "file_path": file_path, + "visual_features": features, + "signals": signals_raw, + "reasoning": reasoning, + "cpci": cpci, + "narrative": narrative, + } + + +# ── UI helpers ──────────────────────────────────────────────────────────────── + +def badge(label: str, style: str) -> str: + return f"{label}" + +def score_badge(v: float, low: int = 40, high: int = 70) -> str: + if v >= high: return "good" + if v >= low: return "warn" + return "bad" + +def cpci_color(v: float) -> str: + if v >= 70: return "#22C55E" + if v >= 40: return "#F59E0B" + return "#EF4444" + +def color_swatches(hex_colors: list) -> str: + return "".join( + f"" + for c in hex_colors + ) + +def short_name(name: str, max_len: int = 20) -> str: + """Truncate long filenames for table display.""" + return name if len(name) <= max_len else name[:max_len - 3] + "..." + + +# ── Section card renderer ──────────────────────────────────────────────────── + +def _section_card( + icon: str, + title: str, + accent: str, + body: str, + score: object = None, + score_label: str = "", + pointers: list = None, # list of (label, value, value_color) tuples +) -> None: + """ + Full narrative section card with: + - Coloured left-border header (icon + title + optional score pill) + - Paragraph body text + - Pointer strip: data chips at the bottom for quick-scan reference + """ + # Score pill (top-right of header) + pill_html = "" + if score is not None: + pill_html = ( + f"
" + f"
{score}
" + f"
{score_label}
" + f"
" + ) + + # Pointer chips + ptr_html = "" + if pointers: + chips = "" + for lbl, val, vcol in pointers: + chips += ( + f"" + f" " + f"{lbl} {val}" + f"" + ) + ptr_html = f"
{chips}
" + + # Render \n\n-separated paragraphs as

blocks for readability + paras = [p.strip() for p in body.split("\n\n") if p.strip()] + body_html = "".join(f"

{p}

" for p in paras) if paras else f"

{body}

" + + st.markdown(f""" +
+
+
+ {icon} + {title} +
+ {pill_html} +
+
{body_html}
+ {ptr_html} +
""", unsafe_allow_html=True) + + +# ── Cognitive Diagnosis renderer ───────────────────────────────────────────── + +def _cognitive_diagnosis( + attn, mem, val, cl, cl_score, vf, + a_color, a_label, m_color, m_label, + v_color, v_label, cl_color, +): + """ + Renders the merged 🧬 Cognitive Diagnosis section. + Four compact mini-blocks side by side: Attention · Memory · Emotion · Load. + Each has: score, 2–3 bullets, 1 implication line. + """ + + def _bullets(*items): + return "".join( + f"
" + f"
" + f"{text}
" + for text, color in items + ) + + def _implication(text, color): + return ( + f"
" + f"{text}
" + ) + + # ── Attention bullets ───────────────────────────────────────────────────── + face_txt = ( + f"{vf['face_count']} face(s) — will trigger an orienting response and accelerate processing" + if vf["face_count"] > 0 + else "No face — will not trigger the fastest biological attention mechanism available" + ) + contrast_txt = ( + f"Contrast {vf['contrast_score']:.0f}/100 — " + + ("will pass the visual salience gate in a busy feed" if vf["contrast_score"] >= 60 + else "will not pass the visual salience test — disappears in a competitive feed") + ) + obj_txt = ( + f"{vf['object_count']} object(s) — " + + ("clean composition — focus will land on the primary subject" if vf["object_count"] <= 4 + else "visual attention will fragment across elements — no single thing will dominate") + ) + if attn > 60: + attn_impl = _implication("→ Will interrupt scrolling and trigger processing in cold audiences", a_color) + elif attn >= 30: + attn_impl = _implication("→ Will not reliably stop cold audiences — loses the first cognitive gate before the message is seen", a_color) + else: + attn_impl = _implication("→ Will be skipped at near-zero processing depth — the brain never engages with the content", a_color) + + # ── Memory bullets ──────────────────────────────────────────────────────── + text_pct = vf["text_density"] * 100 + if text_pct < 5: + text_mem_txt = f"Text coverage {text_pct:.0f}% — no verbal anchor; the visual alone will not survive working memory" + elif text_pct <= 25: + text_mem_txt = f"Text coverage {text_pct:.0f}% — dual-coding range; both visual and verbal channels will encode simultaneously" + else: + text_mem_txt = f"Text coverage {text_pct:.0f}% — verbal overload; neither channel will encode cleanly, recall will suffer" + + mem_obj_txt = ( + f"{vf['object_count']} element(s) — " + + ("sparse enough to form a dominant memory trace" if vf["object_count"] <= 4 + else "too many competing elements — nothing will be remembered as the primary object") + ) + if mem > 70: + mem_impl = _implication("→ Will be recognised at point of purchase — brand memory survives the gap between exposure and decision", m_color) + elif mem >= 40: + mem_impl = _implication("→ Will not encode on a single exposure — requires 6–8 impressions to build reliable recall, increasing effective CPM", m_color) + else: + mem_impl = _implication("→ Will leave no trace — viewers will not recall the brand or message within minutes of scrolling past", m_color) + + # ── Emotion bullets ─────────────────────────────────────────────────────── + val_norm = (val + 1) / 2 * 100 + face_emo = ( + f"{vf['face_count']} face(s) — will drive affiliative warmth and accelerate positive affect" + if vf["face_count"] > 0 + else "No face — will forfeit the strongest emotion driver available in still imagery" + ) + palette_hex = vf["dominant_colors"][0] if vf["dominant_colors"] else "#888888" + r_hex = int(palette_hex[1:3], 16) if len(palette_hex) == 7 else 128 + b_hex = int(palette_hex[5:7], 16) if len(palette_hex) == 7 else 128 + v_hex = (int(palette_hex[1:3], 16) + int(palette_hex[3:5], 16) + b_hex) // 3 + if r_hex > b_hex + 30: + pal_txt = f"Warm palette ({palette_hex}) — will activate approach affect and lift emotional valence" + elif b_hex > r_hex + 20: + pal_txt = f"Cool palette ({palette_hex}) — will signal credibility but will not drive arousal or urgency" + elif v_hex < 80: + pal_txt = f"Dark palette ({palette_hex}) — will project premium cues but will suppress warmth and affinity" + else: + pal_txt = f"Neutral palette ({palette_hex}) — will generate no emotional signal — a missed valence opportunity" + + if val > 0.1: + val_impl = _implication("→ Will build positive brand associations with repeated exposure — emotion compounds into long-term affinity", v_color) + elif val > -0.1: + val_impl = _implication("→ Will generate no emotional memory — neutral affect means the brand will not benefit from the exposure beyond the impression", v_color) + else: + val_impl = _implication("→ Will silently erode brand equity — negative affect embeds subconsciously and accumulates across each impression served", v_color) + + # ── Cognitive Load bullets ──────────────────────────────────────────────── + vis_complexity = ( + f"{vf['object_count']} visual elements — " + + ("within comfortable processing capacity — brain will not fragment focus" if vf["object_count"] <= 5 + else "exceeds working memory capacity — brain will abandon full processing") + ) + text_load = ( + f"Text at {text_pct:.0f}% coverage — " + + ("low verbal demand — will not compete with visual processing" if text_pct <= 15 + else "high verbal demand — audience will skim or skip rather than read") + ) + if cl == "Low": + cl_impl = _implication("→ Will process in under 2 seconds — fits feed, display, and OOH without cognitive friction", cl_color) + elif cl == "Medium": + cl_impl = _implication("→ Will underperform in fast-scroll formats — requires dwell time the audience will not give", cl_color) + else: + cl_impl = _implication("→ Will saturate working memory before the message lands — high load kills attention, memory, and emotion simultaneously", cl_color) + + # ── Build HTML ──────────────────────────────────────────────────────────── + def _block(icon, label, score_val, score_lbl, color, bullets_html, impl_html, sig_color=None): + lbl_color = sig_color if sig_color else color + return ( + f"
" + f"
" + f"{icon}" + f"
" + f"
{score_val}
" + f"
{score_lbl}
" + f"
" + f"
{label}
" + f"{bullets_html}" + f"{impl_html}" + f"
" + ) + + attn_block = _block( + "🎯", _tooltip("Attention", "Attention Score", [ + "→ Measures visual stopping power", + "→ Based on contrast, faces & clutter", + "→ Predicts scroll-stop probability", + ]), attn, a_label, a_color, + _bullets( + (face_txt, a_color), + (contrast_txt, "#334455"), + (obj_txt, "#334455"), + ), + attn_impl, + sig_color="#3B82F6", + ) + mem_block = _block( + "🧩", _tooltip("Memory", "Memory Score", [ + "→ Measures brand recall potential", + "→ Based on text density & visual simplicity", + "→ Drives recognition at point of purchase", + ]), mem, m_label, m_color, + _bullets( + (text_mem_txt, m_color), + (mem_obj_txt, "#334455"), + ), + mem_impl, + sig_color="#8B5CF6", + ) + val_block = _block( + "💭", _tooltip("Emotion", "Emotional Valence", [ + "→ Measures positive vs. negative tone", + "→ Derived from face expression & color warmth", + "→ Shapes brand affinity & purchase intent", + ]), f"{val:+.2f}", v_label, v_color, + _bullets( + (face_emo, v_color), + (pal_txt, "#334455"), + (f"Valence {val_norm:.0f}/100 on the positive–negative scale", "#334455"), + ), + val_impl, + sig_color="#EC4899", + ) + cl_block = _block( + "⚙️", "Cog. Load", cl, f"score {cl_score:.0f}/100", cl_color, + _bullets( + (vis_complexity, cl_color), + (text_load, "#334455"), + ), + cl_impl, + sig_color="#F59E0B", + ) + + st.markdown(f""" +
+
+
🧬 Cognitive Diagnosis
+
What each signal predicts about real-world performance
+
+
+ {attn_block} + {mem_block} + {val_block} + {cl_block} +
+
""", unsafe_allow_html=True) + + +# ── Recommendations renderer ───────────────────────────────────────────────── + +import re as _re + +_REC_META = { + # keyword → (icon, color) + "priority": ("🎯", "#3B82F6"), + "attention": ("🎯", "#3B82F6"), + "memory": ("🧩", "#8B5CF6"), + "emotion": ("💭", "#EC4899"), + "load": ("⚙️", "#F59E0B"), + "scaling": ("🧪", "#ce93d8"), + "a/b": ("🧪", "#ce93d8"), + "before": ("🧪", "#ce93d8"), +} + +def _rec_meta(label: str): + """Return (icon, color) for a recommendation label.""" + lo = label.lower() + for kw, meta in _REC_META.items(): + if kw in lo: + return meta + return ("→", "#667788") + +def _render_recommendations(body: str, pointers: list) -> None: + """ + Premium Recommendations panel. + Parses **Label:** body paragraphs into numbered, color-coded action cards. + Falls back to plain paragraph rendering if no bold labels found. + """ + paragraphs = [p.strip() for p in body.split("\n\n") if p.strip()] + + # Parse each paragraph into (label, body_text) + parsed = [] + for para in paragraphs: + m = _re.match(r'^\*\*(.+?):\*\*\s*(.*)', para, _re.DOTALL) + if m: + parsed.append((m.group(1).strip(), m.group(2).strip())) + else: + parsed.append(("", para)) + + # Build item HTML + items_html = "" + for idx, (label, text) in enumerate(parsed, 1): + icon, color = _rec_meta(label) if label else ("→", "#667788") + badge_html = ( + f"
{icon}
" + ) + lbl_html = "" + if label: + lbl_html = ( + f"
" + f"" + f"{label}" + f"
" + ) + items_html += ( + f"
" + f"
{badge_html}
" + f"
{lbl_html}" + f"
{text}
" + f"
" + f"
" + ) + + # Build pointer chips + chips_html = "" + for lbl, val, vcol in (pointers or []): + chips_html += ( + f"" + f" " + f"{lbl} {val}" + f"" + ) + + count = len(parsed) + st.markdown(f""" +
+
+ 🛠 + Optimization Recommendations + {count} ACTION{'S' if count != 1 else ''} +
+
{items_html}
+ +
""", unsafe_allow_html=True) + + +# ── Quick Read generator ────────────────────────────────────────────────────── + +def _verdict_color(cpci: float, val: float, cl: str) -> str: + if cpci >= 70 and cl != "High": return "#22C55E" + if cpci >= 55 and val >= -0.05: return "#F59E0B" + if cpci >= 40: return "#EF4444" + return "#EF4444" + + +def _final_verdict( + cpci: float, attn: int, mem: int, val: float, cl: str, use_case: str, +) -> None: + """Renders the Final Verdict strip in single-creative view.""" + verdict = _final_verdict_text(cpci, attn, mem, val, cl, use_case) + vcolor = _verdict_color(cpci, val, cl) + st.markdown( + f"
" + f"
Final Verdict
" + f"
" + f"{verdict}
" + f"
", + unsafe_allow_html=True, + ) + + +def _quick_read( + cpci: float, + attn: int, + mem: int, + val: float, + cl: str, + vf: dict, + use_case: str, +) -> None: + """ + Renders the 🧠 What This Means (Quick Read) banner. + Three plain-English lines: performance prediction, core issue, immediate fix. + No technical language. Max one sentence each. + """ + + # ── Use-case context plain name ──────────────────────────────────────────── + ctx = { + "FMCG Branding": "brand awareness campaigns", + "Performance Marketing": "paid social and search ads", + "Retail Media": "retail and in-store placements", + }.get(use_case, "this campaign type") + + # ── Line 1 — Performance Prediction ─────────────────────────────────────── + if cpci >= 70: + prediction = f"This creative will earn its media spend in {ctx} — the cognitive gates are clear." + elif cpci >= 55: + prediction = f"This creative will generate impressions in {ctx} but will not convert efficiently — one weak signal is costing reach." + elif cpci >= 40: + prediction = f"This creative will struggle in cold audiences for {ctx} — it may survive retargeting but will not scale profitably." + else: + prediction = f"This creative will underperform regardless of budget — the cognitive barriers are too significant to overcome with spend." + + # ── Line 2 — Core Issue ─────────────────────────────────────────────────── + attn_gap = max(0, 60 - attn) + mem_gap = max(0, 70 - mem) + val_gap = max(0, 0.1 - val) * 100 + load_gap = 25 if cl == "High" else (10 if cl == "Medium" else 0) + + worst = max(attn_gap, mem_gap, val_gap, load_gap) + + if worst == 0: + issue = "All signals are above threshold — no single dimension is holding this back." + elif worst == load_gap and cl == "High": + issue = "Visual overload will cause viewers to disengage before the message registers — the brain cannot parse this quickly enough in a feed." + elif worst == attn_gap: + if vf.get("face_count", 0) == 0: + issue = "This creative will fail to trigger an orienting response — without a face or salient focal point, the brain will not interrupt scrolling to process it." + elif vf.get("contrast_score", 50) < 45: + issue = "This creative will disappear in a busy feed — insufficient contrast means it fails the first visual gate before content is even assessed." + else: + issue = "This creative will be processed at low depth — no dominant focal point means the brain distributes attention thinly and nothing is prioritised." + elif worst == mem_gap: + if vf.get("text_density", 0) > 0.25: + issue = "This creative will be remembered only if repeatedly exposed — text overload prevents dual-coding, so neither the visual nor verbal message encodes cleanly." + elif vf.get("text_density", 0) < 0.05: + issue = "This creative will be seen but not recalled — without a verbal anchor, the visual alone will not survive beyond a few seconds in working memory." + else: + issue = "This creative will not build brand recall efficiently — too many competing elements mean nothing dominates the memory trace." + elif worst == val_gap: + if vf.get("face_count", 0) == 0: + issue = "This creative will generate neutral-to-negative affect on each exposure — without a human face, the palette alone cannot drive positive brand association." + else: + issue = "This creative will subtly undermine brand affinity over time — the colour palette is triggering mild avoidance without the viewer being aware of it." + elif worst == load_gap: + issue = "This creative demands more cognitive effort than a scrolling audience will commit — the message will not land in fast-feed placements." + else: + issue = "Attention, memory, and emotion are pulling in different directions — the creative is sending mixed signals that dilute overall cognitive impact." + + # ── Line 3 — Immediate Fix ──────────────────────────────────────────────── + if vf.get("face_count", 0) == 0 and attn_gap > 15: + fix = "Introduce a human face as the dominant visual element — it is the single fastest mechanism for triggering an orienting response in feed." + elif vf.get("contrast_score", 50) < 45 and attn_gap > 10: + fix = "Increase foreground-to-background contrast significantly — the creative needs to pass the visual salience test before any other signal matters." + elif vf.get("object_count", 0) > 7 and (worst == load_gap or worst == mem_gap): + fix = "Eliminate all secondary visual elements and commit to a single hero — cognitive load is suppressing every other signal." + elif vf.get("text_density", 0) > 0.28: + fix = "Reduce copy to a single declarative line under six words — audiences process far less text than advertisers write." + elif vf.get("text_density", 0) < 0.04 and mem_gap > 15: + fix = "Add a 4–6 word brand or message line — the verbal channel is completely unused, which is costing you recall without any benefit." + elif val < -0.05 and vf.get("face_count", 0) == 0: + fix = "Add a person with a natural, warm expression — emotional valence cannot be fixed with colour alone at this deficit." + elif val < -0.05: + fix = "Replace the dominant cool or dark tones with warmer equivalents — the palette is the primary driver of the negative valence reading." + elif attn_gap > 10: + fix = "Scale up the primary subject and increase its contrast against the background — give the eye an unmissable landing point." + elif mem_gap > 15: + fix = "Reduce the composition to one dominant image and one short message — simplicity is the only reliable route to single-exposure recall." + else: + fix = "Test a version with a human face replacing the current hero element — it will lift both attention and emotional valence simultaneously." + + # ── Render ───────────────────────────────────────────────────────────────── + pcolor = "#22C55E" if cpci >= 70 else ("#F59E0B" if cpci >= 40 else "#EF4444") + + st.markdown(f""" +
+
🧠  What This Means
+
+ 01 + Performance + {prediction} +
+
+ 02 + Core Issue + {issue} +
+
+ 03 + Fix First + {fix} +
+
""", unsafe_allow_html=True) + + +# ── Why This Matters hero block ─────────────────────────────────────────────── + +def _why_this_matters( + cpci: float, + attn: int, + mem: int, + val: float, + cl: str, + use_case: str, +) -> None: + """ + Full-width emotional impact statement placed directly after the CPCi number. + Goal: make the user FEEL the score, not just read it. + """ + + # ── Tier-adaptive copy ──────────────────────────────────────────────────── + if cpci < 30: + accent = "#EF4444" + bg = "rgba(239,68,68,0.07)" + border_col = "#7F1D1D" + icon = "🚨" + bold_line = ( + "This creative is likely wasting 50–70% of your media budget " + "due to critically low cognitive engagement." + ) + support = ( + f"At CPCi {cpci}, the brain won't reliably process this message. " + "Impressions served are impressions lost — " + "no targeting strategy can compensate for a creative the brain ignores." + ) + + elif cpci < 40: + accent = "#EF4444" + bg = "rgba(239,68,68,0.06)" + border_col = "#7F1D1D" + icon = "🚨" + bold_line = ( + "This creative is likely wasting 40–55% of your media budget " + "due to low cognitive engagement." + ) + support = ( + f"At CPCi {cpci}, most impressions will not cognitively register. " + "The brand message won't be encoded — and won't be recalled at point of purchase." + ) + + elif cpci < 55: + accent = "#F59E0B" + bg = "rgba(245,158,11,0.06)" + border_col = "#78350F" + icon = "⚠️" + bold_line = "This creative is leaving 25–40% of its potential media efficiency on the table." + support = ( + f"At CPCi {cpci}, you have signal — but the brain is only partially engaged. " + "You'll need higher frequency to achieve the same recall as a top-quartile creative " + "at half the impressions." + ) + + elif cpci < 70: + accent = "#F59E0B" + bg = "rgba(245,158,11,0.05)" + border_col = "#78350F" + icon = "⚠️" + bold_line = ( + "This creative is performing adequately — but there is a gap before it earns " + "top-quartile media efficiency." + ) + support = ( + f"At CPCi {cpci}, you're in the average band. A single targeted fix — " + "attention, memory, or load — could close the gap and meaningfully reduce " + "your effective cost per recalled impression." + ) + + else: + accent = "#22C55E" + bg = "rgba(34,197,94,0.06)" + border_col = "#14532D" + icon = "✅" + bold_line = ( + "This creative earns every impression — the brain is processing, " + "encoding, and responding at above-average efficiency." + ) + support = ( + f"At CPCi {cpci}, this is top-quartile cognitive performance. " + "Your media spend is working at maximum brain-level impact. " + "Scale with confidence." + ) + + # ── Media cost multiplier (uses mem, the function parameter) ───────────── + effective_memory = max(mem, 10) + multiplier = round(70 / effective_memory, 1) + + if mem < 70: + mult_color = ( + "#EF4444" if multiplier > 1.8 else + "#F59E0B" if multiplier >= 1.2 else + "#22C55E" + ) + media_cost_html = ( + f"
" + f"🔥 This creative will cost you {multiplier}× more media " + f"to achieve the same recall." + f"
" + ) + waste_html = ( + "
" + "Equivalent to wasting ~35–50% of your media budget on ineffective impressions." + "
" + ) if multiplier > 1.5 else "
" + else: + media_cost_html = ( + "
" + "This creative is operating at efficient memory levels — no excess media cost." + "
" + ) + waste_html = "" + + # ── Render ──────────────────────────────────────────────────────────────── + # Line 1 — eyebrow + st.markdown( + f"
" + + # Eyebrow: "🚨 WHAT THIS MEANS FOR YOUR MEDIA SPEND" + f"
" + f"{icon}  WHAT THIS MEANS FOR YOUR MEDIA SPEND" + f"
" + + # Line 2 — bold emotional sentence + f"
" + f"{bold_line}" + f"
" + + # Line 3 — media cost multiplier + + media_cost_html + + # Line 4 — waste callout (conditional on multiplier > 1.5) + + waste_html + + # Line 5 — supporting explanation + + f"
" + f"{support}" + f"
" + + f"
", + unsafe_allow_html=True, + ) + + +# ── Business Impact ─────────────────────────────────────────────────────────── + +def _business_impact( + cpci: float, + attn: int, + mem: int, + val: float, + cl: str, + use_case: str, +) -> None: + """ + CMO-facing Business Impact panel. + Translates CPCi into commercial language: waste risk, media efficiency, + and a single deployment recommendation. + No technical signal language — decisions only. + """ + + # ── Tier classification ─────────────────────────────────────────────────── + if cpci >= 70: + risk_label = "High Efficiency" + risk_color = "#22C55E" + risk_bg = "#22C55E14" + risk_border = "#22C55E44" + headline = "This creative is ready to scale — cognitive signals clear." + sub = ( + "Attention, memory, and emotion are working in alignment. " + "Media spend behind this creative is likely to generate strong cognitive impact at scale." + ) + deploy_rec = "Deploy with confidence. Scale budget progressively." + deploy_color = "#22C55E" + elif cpci >= 40: + risk_label = "Moderate Performance" + risk_color = "#F59E0B" + risk_bg = "#F59E0B14" + risk_border = "#F59E0B44" + headline = "This creative will generate results — but not at full media efficiency." + sub = ( + "Some impressions will land, many will not. " + "One weak cognitive signal is suppressing returns. " + "A targeted fix could unlock significantly better performance before spend increases." + ) + deploy_rec = "Deploy selectively. Fix the weakest signal before scaling." + deploy_color = "#F59E0B" + else: + risk_label = "High Waste Risk" + risk_color = "#EF4444" + risk_bg = "#EF444414" + risk_border = "#EF444444" + headline = "This creative is likely to underperform in paid media." + sub = ( + "High risk of wasted impressions. " + "The cognitive barriers are significant enough that no targeting, bidding, " + "or placement strategy will compensate for them at scale." + ) + deploy_rec = "Do not deploy. Rebuild creative before any media spend." + deploy_color = "#EF4444" + + # ── Media efficiency impact bullets ────────────────────────────────────── + if cpci >= 70: + efficiency_bullets = [ + ("Impression quality", "High — creatives at this level typically drive 2–3× better recall than average", "#22C55E"), + ("Conversion signal", "Strong — attention and memory are both above threshold for cold-audience response", "#22C55E"), + ("Brand equity", "Positive accumulation — each impression builds durable brand memory", "#22C55E"), + ("Recommended budget", f"Open to full {use_case} spend — cognitive signal supports scale", "#22C55E"), + ] + elif cpci >= 55: + efficiency_bullets = [ + ("Impression quality", "Moderate — mixed signals mean a significant share of impressions will not engage", "#F59E0B"), + ("Conversion signal", "Weak on cold audiences — restrict to warm audiences and lookalikes initially", "#F59E0B"), + ("Brand equity", "Partial — some recall will build but not at efficient frequency", "#F59E0B"), + ("Recommended budget", "Limit cold-audience spend until the weakest signal is fixed", "#F59E0B"), + ] + elif cpci >= 40: + efficiency_bullets = [ + ("Impression quality", "Low — majority of impressions will generate no measurable cognitive response", "#EF4444"), + ("Conversion signal", "Retargeting only — not viable for cold-audience acquisition", "#F59E0B"), + ("Brand equity", "Minimal to neutral — recall is unlikely to build at current signal levels", "#F59E0B"), + ("Recommended budget", "Cap spend — retargeting with strict frequency cap (2–3× per user) only", "#F59E0B"), + ] + else: + efficiency_bullets = [ + ("Impression quality", "Very low — impressions are likely to generate no cognitive engagement", "#EF4444"), + ("Conversion signal", "None — cognitive barriers prevent any reliable conversion mechanism", "#EF4444"), + ("Brand equity", "Negative risk — repeated exposure to an ineffective creative can erode brand recall","#EF4444"), + ("Recommended budget", "Zero — do not commit media spend to this creative in its current state", "#EF4444"), + ] + + # ── Visible summary (always shown — 2 lines max) ───────────────────────── + st.markdown( + f"
" + + f"
" + f"💼  Business Impact
" + + # Badge + headline on one row + f"
" + f"
" + f"{risk_label}" + f"
" + f"
" + f"{headline}
" + f"
" + + # Deployment decision — single line, always visible + f"
" + f"→  {deploy_rec}" + f"
" + + f"
", + unsafe_allow_html=True, + ) + + # ── Detail expander ─────────────────────────────────────────────────────── + with st.expander("View media efficiency breakdown"): + rows_html = "" + for label, value, color in efficiency_bullets: + rows_html += ( + f"
" + f"
{label}
" + f"
{value}
" + f"
" + ) + st.markdown( + f"
" + f"{sub}
" + f"
" + f"Expected Media Efficiency Impact
" + f"{rows_html}", + unsafe_allow_html=True, + ) + + st.markdown("
", unsafe_allow_html=True) + + +# ── Creative Optimization Scenario ─────────────────────────────────────────── + +def _compute_scenarios( + cpci: float, attn: int, mem: int, val: float, cl: str, vf: dict, use_case: str +) -> list: + """ + Compute up to 4 independent CPCi lift scenarios. + Each uses the real CPCi formula with the actual use-case weights. + Returns a list of dicts sorted by lift descending. + """ + w = USE_CASES[use_case]["weights"] + val_norm = (val + 1) / 2 * 100 # [-1,+1] → [0,100] + load_pen = LOAD_PENALTY_MAP.get(cl, 0) + + def _new_cpci(new_attn=attn, new_mem=mem, new_val_norm=val_norm, new_load_pen=load_pen): + raw = (w["attention"]*new_attn + w["memory"]*new_mem + + w["emotion"]*new_val_norm + new_load_pen) + return round(max(0.0, min(100.0, raw)), 1) + + face_count = vf.get("face_count", 0) + contrast = vf.get("contrast_score", 50) + obj_count = vf.get("object_count", 4) + text_pct = vf.get("text_density", 0.1) * 100 + + scenarios = [] + + # ── Scenario A: attention improvement ──────────────────────────────────── + attn_gap = max(0, 65 - attn) + if attn_gap >= 8: + lift_pts = min(attn_gap, 22) + new_a = min(attn + lift_pts, 100) + projected = _new_cpci(new_attn=new_a) + delta = round(projected - cpci, 1) + if delta >= 2: + if face_count == 0: + action = "Add a human face as the primary visual subject" + rationale = "Face presence triggers the brain's fastest attention mechanism — an orienting response in under 13ms." + elif contrast < 45: + action = "Increase contrast ratio to at least 4.5:1 against the background" + rationale = "Pre-attentive salience is driven by contrast. Below threshold, the creative disappears in a competitive feed." + else: + action = "Simplify to one dominant focal point — remove competing visual elements" + rationale = "Fragmented compositions split visual attention. A single dominant subject maximises stopping power." + scenarios.append({ + "signal": "Attention", + "sig_color": "#3B82F6", + "action": action, + "rationale": rationale, + "method": "improving attention signal", + "from_signal": attn, "to_signal": new_a, + "from_cpci": cpci, "to_cpci": projected, + "lift": delta, + }) + + # ── Scenario B: memory improvement ──────────────────────────────────────── + mem_gap = max(0, 70 - mem) + if mem_gap >= 8: + lift_pts = min(mem_gap, 22) + new_m = min(mem + lift_pts, 100) + projected = _new_cpci(new_mem=new_m) + delta = round(projected - cpci, 1) + if delta >= 2: + if text_pct < 5: + action = "Add a short brand tagline or product callout overlay" + rationale = "Dual-coding (visual + verbal) simultaneously encodes two memory traces — significantly lifting recall." + elif obj_count > 5: + action = "Reduce to one dominant product or hero image" + rationale = "Memory encodes the most salient object. Competing elements prevent a single strong trace from forming." + else: + action = "Strengthen logo placement and brand mnemonic visibility" + rationale = "Recognition at point of purchase requires a clear brand anchor encoded during ad exposure." + scenarios.append({ + "signal": "Memory", + "sig_color": "#8B5CF6", + "action": action, + "rationale": rationale, + "method": "improving memory encoding", + "from_signal": mem, "to_signal": new_m, + "from_cpci": cpci, "to_cpci": projected, + "lift": delta, + }) + + # ── Scenario C: load reduction (High → Medium) ──────────────────────────── + if cl == "High": + new_pen = LOAD_PENALTY_MAP.get("Medium", -5) + projected = _new_cpci(new_load_pen=new_pen) + delta = round(projected - cpci, 1) + if delta >= 2: + scenarios.append({ + "signal": "Cognitive Load", + "sig_color": "#F59E0B", + "action": "Reduce on-screen text and visual elements by 40%", + "rationale": "High cognitive load causes viewers to abandon processing before the message registers — no targeting fix compensates for this.", + "method": "reducing cognitive load from High to Medium", + "from_signal": "High", "to_signal": "Medium", + "from_cpci": cpci, "to_cpci": projected, + "lift": delta, + }) + + # ── Scenario D: valence improvement ─────────────────────────────────────── + val_gap = max(0.0, 0.15 - val) + if val_gap >= 0.12: + new_val = min(val + 0.28, 1.0) + new_vn = (new_val + 1) / 2 * 100 + projected = _new_cpci(new_val_norm=new_vn) + delta = round(projected - cpci, 1) + if delta >= 2: + if face_count == 0: + action = "Introduce a human face with a positive expression" + rationale = "Facial expressions are the most direct driver of emotional valence — warmth and affinity are triggered involuntarily." + else: + action = "Shift colour palette toward warmer tones (amber, coral, warm white)" + rationale = "Cool and neutral palettes suppress approach affect. Warm palettes activate positive emotional responses without conscious effort." + scenarios.append({ + "signal": "Emotion", + "sig_color": "#EC4899", + "action": action, + "rationale": rationale, + "method": "improving emotional valence", + "from_signal": f"{val:+.2f}", "to_signal": f"{new_val:+.2f}", + "from_cpci": cpci, "to_cpci": projected, + "lift": delta, + }) + + scenarios.sort(key=lambda s: s["lift"], reverse=True) + return scenarios + + +def _optimization_scenario( + cpci: float, attn: int, mem: int, val: float, cl: str, vf: dict, use_case: str +) -> None: + """ + CMO-facing optimization scenario card. + Shows before/after CPCi, potential lift, and specific actionable fixes. + """ + scenarios = _compute_scenarios(cpci, attn, mem, val, cl, vf, use_case) + + if not scenarios: + # All signals already at or above target — no meaningful lift available + st.markdown( + "
" + "
🎯  Creative Optimization Scenario
" + "
" + "All cognitive signals are above threshold — no single optimization is likely to " + "produce meaningful additional lift. This creative is ready to scale.
" + "
", + unsafe_allow_html=True, + ) + return + + best = scenarios[0] + from_c = int(round(best["from_cpci"])) + to_c = int(round(best["to_cpci"])) + lift = best["lift"] + sig_color = best["sig_color"] + + # Tier label helper + def _tier(c): + if c >= 70: return ("High Efficiency", "#22C55E") + if c >= 40: return ("Moderate Performance","#F59E0B") + return ("High Waste Risk", "#EF4444") + + from_tier_label, from_tier_color = _tier(from_c) + to_tier_label, to_tier_color = _tier(to_c) + tier_change = from_tier_label != to_tier_label + + # ── Before/after progress bar ───────────────────────────────────────────── + bar_from = f"{from_c}%" + bar_to = f"{to_c}%" + + # Secondary scenario bullets + secondary_html = "" + for s in scenarios[1:3]: # show up to 2 more + secondary_html += ( + f"
" + f"
" + f"
" + f"{s['signal']}" + f" · " + f"+{s['lift']:.0f} pts lift · " + f"{s['action']}" + f"
" + f"
" + ) + + tier_transition_html = "" + if tier_change: + tier_transition_html = ( + f"
" + f"{from_tier_label}" + f"" + f"{to_tier_label}" + f"
" + ) + + # ── Visible summary — before/after + primary action (always shown) ─────── + st.markdown( + f"
" + + f"
" + f"🎯  Optimization Scenario
" + + f"{tier_transition_html}" + + # Before / After numbers — compact + f"
" + f"
" + f"
Now
" + f"
{from_c}
" + f"
" + f"
" + f"
" + f"
" + f"+{lift:.0f} pts" + f"
" + f"
" + f"
" + f"
Projected
" + f"
{to_c}
" + f"
" + f"
" + + # Primary action — one line + f"
" + f"{best['signal']} · " + f"{best['action']}" + f"
" + + f"
", + unsafe_allow_html=True, + ) + + # ── Detail expander — rationale + secondary scenarios ───────────────────── + with st.expander("Why this works + additional opportunities"): + # Progress bar + st.markdown( + f"
" + f"
" + f"
" + f"
" + f"
" + f"Improving {best['method']} could increase CPCi from " + f"{from_c} → " + f"{to_c}.
" + f"
" + f"{best['rationale']}
" + + ( + f"
" + f"Additional Opportunities
" + f"{secondary_html}" + if secondary_html else "" + ), + unsafe_allow_html=True, + ) + + st.markdown("
", unsafe_allow_html=True) + + +# ── Creative Brief ──────────────────────────────────────────────────────────── + +def _creative_brief( + cpci: float, + attn: int, + mem: int, + val: float, + cl: str, + vf: dict, + use_case: str, +) -> None: + """ + Compact, executive-style brief. One panel. Every line is a decision. + Format mirrors a media/creative brief — diagnosis, issue, fix, use / don't use. + """ + face = vf.get("face_count", 0) > 0 + contrast = vf.get("contrast_score", 50) + objects = vf.get("object_count", 4) + text_pct = vf.get("text_density", 0.1) * 100 + load_high = cl == "High" + load_low = cl == "Low" + attn_str = attn >= 65 + attn_weak = attn < 45 + mem_str = mem >= 65 + mem_weak = mem < 45 + val_pos = val > 0.10 + val_neg = val < -0.05 + + cc = "#22C55E" if cpci >= 70 else ("#F59E0B" if cpci >= 40 else "#EF4444") + + # ── Scale label ─────────────────────────────────────────────────────────── + if cpci >= 70: scale_label = "Ready to scale" + elif cpci >= 55: scale_label = "Optimise before scaling" + elif cpci >= 40: scale_label = "Not ready for scale" + else: scale_label = "Do not deploy" + + # ── Diagnosis (2 lines max) ─────────────────────────────────────────────── + if cpci >= 70: + if attn_str and mem_str: + diag = [ + "This creative clears every cognitive gate — attention, memory, and emotion are all above threshold.", + "It will work in feed environments against cold audiences and compound brand recall over time.", + ] + elif attn_str: + diag = [ + "This creative has the attention signal to stop cold audiences, but memory encoding limits its long-term brand value.", + "It will drive clicks and initial engagement but will not build durable recall without frequency.", + ] + else: + diag = [ + "This creative builds strong brand memory and emotional affinity, but its attention signal is below the cold-scroll threshold.", + "It will perform reliably with warm audiences and frequency-based brand campaigns.", + ] + elif cpci >= 55: + if attn_weak: + diag = [ + "This creative will not reliably interrupt cold-audience feeds — it lacks the attention signal to win the first cognitive gate.", + "It can contribute to recall if repeatedly exposed to warm audiences, but acquisition spend is premature.", + ] + elif load_high: + diag = [ + "This creative carries too much visual complexity for fast-scroll environments — the brain abandons processing before the message lands.", + "Stripping cognitive load is the single highest-leverage fix before any spend increase.", + ] + else: + diag = [ + "This creative has real potential but one signal is suppressing the composite score.", + "A targeted fix — not a rebuild — is likely all that stands between this and scale-readiness.", + ] + elif cpci >= 40: + if not face and attn_weak: + diag = [ + "This creative will not perform in feed environments — it lacks an attention trigger and fails to produce an orienting response.", + "It may work in retargeting environments where familiarity reduces the processing effort required.", + ] + elif load_high: + diag = [ + "This creative asks too much of a scrolling audience — visual overload causes disengagement before the message registers.", + "No amount of targeting precision will compensate for cognitive friction at this level.", + ] + elif val_neg: + diag = [ + "This creative generates mildly negative affect on each impression — a hidden brand tax at scale.", + "The emotional tone must be corrected before deployment; otherwise, spend compounds the damage.", + ] + else: + diag = [ + "This creative has insufficient cognitive signal strength to justify cold-audience spend.", + "Narrow the audience to warm segments while the creative is improved.", + ] + else: + diag = [ + "This creative will not perform regardless of budget, targeting, or placement.", + "The cognitive barriers are fundamental — a partial fix will not be sufficient.", + ] + + # ── Primary issue ───────────────────────────────────────────────────────── + attn_gap = max(0, 60 - attn) + mem_gap = max(0, 70 - mem) + val_gap = max(0, 0.1 - val) * 100 + load_gap = 25 if cl == "High" else (10 if cl == "Medium" else 0) + worst = max(attn_gap, mem_gap, val_gap, load_gap) + + if worst == 0: + primary_issue = "No dominant weakness — all signals are above threshold." + elif worst == load_gap and load_high: + primary_issue = "Visual overload — too many elements competing for attention simultaneously." + elif worst == attn_gap and not face: + primary_issue = "No dominant focal point — nothing triggers an orienting response." + elif worst == attn_gap and contrast < 45: + primary_issue = "Insufficient contrast — creative is invisible in a competitive feed." + elif worst == attn_gap: + primary_issue = "Diffuse composition — attention fragments with no single dominant subject." + elif worst == mem_gap and text_pct > 25: + primary_issue = "Text overload — verbal channel is saturated, blocking dual-coding." + elif worst == mem_gap and text_pct < 5: + primary_issue = "No verbal anchor — visual alone will not survive working memory." + elif worst == mem_gap: + primary_issue = "Too many competing elements — no single memory trace will dominate." + elif worst == val_gap and not face: + primary_issue = "No human presence — palette alone cannot generate positive affect." + else: + primary_issue = "Negative emotional signal — colour palette is triggering subconscious avoidance." + + # ── Fix ─────────────────────────────────────────────────────────────────── + if not face and attn_gap > 15: + fix = "Introduce a face or strong visual anchor — it is the highest-leverage single change available." + elif contrast < 45 and attn_gap > 10: + fix = "Increase foreground contrast significantly — pass the visual salience threshold first." + elif objects > 7 and (worst == load_gap or worst == mem_gap): + fix = "Commit to one hero element — remove everything that is not the primary message." + elif text_pct > 28: + fix = "Cut copy to a single line under six words — audiences read far less than advertisers write." + elif text_pct < 4 and mem_gap > 15: + fix = "Add a 4–6 word brand line — the verbal channel is unused at zero cost to attention." + elif val_neg and not face: + fix = "Add a person with a warm, natural expression — valence cannot be fixed with colour at this deficit." + elif val_neg: + fix = "Replace dominant cool or dark tones with warmer equivalents — the palette is the valence driver." + elif attn_gap > 10: + fix = "Scale up the primary subject and push contrast — give the eye an unmissable landing point." + else: + fix = "Simplify to one dominant image and one short message — single-exposure recall demands it." + + # ── Recommended use ─────────────────────────────────────────────────────── + recommended, avoid = [], [] + + if cpci >= 70: + recommended = ["Cold acquisition", "Full-funnel spend", "High-reach brand campaigns"] + avoid = [] + elif cpci >= 55: + if attn_weak: + recommended = ["Warm retargeting", "Lookalike audiences (L1–L3)", "Frequency-capped brand campaigns"] + avoid = ["Cold acquisition", "Prospecting campaigns"] + else: + recommended = ["Warm retargeting", "Mid-funnel conversion", "Frequency builds"] + avoid = ["Top-of-funnel cold spend at scale"] + elif cpci >= 40: + recommended = ["Retargeting (strict frequency cap: 2–3x)", "High-intent warm audiences only"] + avoid = ["Cold acquisition", "Reach campaigns", "OOH or display"] + else: + recommended = [] + avoid = ["Any paid deployment", "Cold audiences", "Retargeting", "Brand campaigns"] + + # ── Build HTML ──────────────────────────────────────────────────────────── + def arrow_list(items, color): + return "".join( + f"
" + f"" + f"{item}" + f"
" + for item in items + ) + + diag_html = "".join( + f"
{d}
" + for d in diag + ) + + rec_html = arrow_list(recommended, "#22C55E") if recommended else ( + f"
No deployment recommended — rebuild first.
" + ) + avoid_html = arrow_list(avoid, "#EF4444") if avoid else "" + + st.markdown( + f"
" + + # Header row + f"
" + f"
📄  Creative Brief
" + f"
CPCi {cpci} — {scale_label}
" + f"
" + + # Diagnosis + f"{diag_html}" + + # Issue + Fix row + f"
" + + f"
" + f"
" + f"Primary Issue
" + f"
{primary_issue}
" + f"
" + + f"
" + f"
" + f"Fix
" + f"
{fix}
" + f"
" + + f"
" + + # Use / Don't use + f"
" + + f"
" + f"
" + f"Recommended Use
" + f"{rec_html}" + f"
" + + + ( + f"
" + f"
" + f"Do Not Use For
" + f"{avoid_html}" + f"
" + if avoid_html else "" + ) + + + f"
" + f"
", + unsafe_allow_html=True, + ) + + +# ── Creative Classification ─────────────────────────────────────────────────── + +# Four archetypes — each with a label, icon, colour, one-line reason, and +# a brief description of what this creative type is designed to do. +_CREATIVE_TYPES = { + "Acquisition": { + "icon": "⚡", + "color": "#3B82F6", + "tag": "Acquisition Creative", + "role": "Designed to interrupt cold audiences and drive immediate action.", + "needs": "High attention · Low cognitive load · Neutral-to-positive valence", + }, + "Recall / Branding": { + "icon": "🧠", + "color": "#a78bfa", + "tag": "Recall / Branding Creative", + "role": "Designed to build long-term brand memory and emotional affinity.", + "needs": "High memory encoding · Positive valence · Manageable load", + }, + "Retargeting": { + "icon": "🔁", + "color": "#F59E0B", + "tag": "Retargeting Creative", + "role": "Designed to close warm audiences who already have brand awareness.", + "needs": "Moderate memory · Positive valence · Works with lower attention", + }, + "Retail Conversion": { + "icon": "🛒", + "color": "#22C55E", + "tag": "Retail Conversion Creative", + "role": "Designed to drive recognition and action at point of purchase.", + "needs": "Strong memory encoding · Low cognitive load · Clear product focus", + }, +} + + +def _classify_creative( + cpci: float, + attn: int, + mem: int, + val: float, + cl: str, + vf: dict, + use_case: str, +) -> tuple: + """ + Returns (archetype_key, reason_string) based on signal balance. + + Decision logic: + Acquisition → attn ≥ 60 AND cl ≠ High AND val ≥ 0 + (stops cold scroll; processable; not brand-negative) + Retail Conversion → mem ≥ 55 AND cl = Low AND attn ≥ 40 + (recognition at point of purchase; fast-process format) + Recall / Branding → mem ≥ 60 AND val > 0.05 + (memory + emotion = brand building over time) + Retargeting → default when cold-audience signals are insufficient + """ + load_high = cl == "High" + load_low = cl == "Low" + + # ── Priority 1: Acquisition ──────────────────────────────────────────────── + if attn >= 60 and not load_high and val >= 0: + if attn >= 70: + reason = ( + f"Exceptional attention ({attn}/100) with manageable cognitive load — " + "will interrupt cold-audience feeds and drive immediate engagement. " + "The brain's orienting response is reliably triggered at this signal level." + ) + else: + reason = ( + f"Strong attention ({attn}/100) clears the cold-scroll threshold, " + f"cognitive load is {cl.lower()}, and emotional tone is not brand-damaging. " + "This creative is built to acquire — not to retain." + ) + return "Acquisition", reason + + # ── Priority 2: Retail Conversion ───────────────────────────────────────── + if mem >= 55 and load_low and attn >= 40: + reason = ( + f"Memory encoding is strong ({mem}/100) and cognitive load is low — " + "ideal for the point-of-purchase context where the viewer must recognise " + "the brand and make a decision in under two seconds. " + "Attention is sufficient for warm and in-aisle placements." + ) + return "Retail Conversion", reason + + # ── Priority 3: Recall / Branding ───────────────────────────────────────── + if mem >= 60 and val > 0.05: + reason = ( + f"Memory encoding ({mem}/100) and emotional valence ({val:+.2f}) are both " + "strong — the two signals required for long-term brand recall. " + f"Attention is {attn}/100, which is {'sufficient' if attn >= 45 else 'below threshold'} " + "for cold audiences, but this creative will build equity with frequency. " + "Best deployed in brand awareness campaigns, not performance." + ) + return "Recall / Branding", reason + + # ── Priority 4: Retargeting (default) ───────────────────────────────────── + if attn < 55: + attn_note = f"attention is below the cold-scroll threshold ({attn}/100)" + elif load_high: + attn_note = f"cognitive load is High — unsuitable for fast-scroll cold audiences" + else: + attn_note = f"signal profile is mixed across dimensions" + + reason = ( + f"The {attn_note}, which means this creative will not perform efficiently against " + "cold audiences. However, with warm audiences where brand awareness is pre-established, " + "the attention bar is lower — this creative can still contribute to conversion " + "in a retargeting context. Do not scale to prospecting." + ) + return "Retargeting", reason + + +def _render_classification(cpci, attn, mem, val, cl, vf, use_case) -> None: + """Renders the creative classification strip between the signal bar and Quick Read.""" + archetype, reason = _classify_creative(cpci, attn, mem, val, cl, vf, use_case) + meta = _CREATIVE_TYPES[archetype] + c = meta["color"] + + # Check if secondary archetype also applies (for multi-purpose creatives) + secondary = None + if archetype == "Acquisition" and mem >= 60 and val > 0.05: + secondary = "Recall / Branding" + elif archetype == "Retail Conversion" and attn >= 60: + secondary = "Acquisition" + elif archetype == "Recall / Branding" and attn >= 60: + secondary = "Acquisition" + + sec_html = "" + if secondary: + sm = _CREATIVE_TYPES[secondary] + sm_c = sm["color"] + sm_icon = sm["icon"] + sec_html = ( + f"" + f"also works as " + f"" + f"{sm_icon} {secondary}" + ) + + st.markdown( + f"
" + + f"
" + f"Best Suited For
" + + f"
" + f"" + f"{meta['icon']} {meta['tag']}" + f"{sec_html}" + f"
" + + f"
" + f"{reason}
" + + f"
" + f"Signal basis: " + f"{meta['needs']}
" + + f"
", + unsafe_allow_html=True, + ) + + +# ── Media Implications ──────────────────────────────────────────────────────── + +def _media_implications( + cpci: float, + attn: int, + mem: int, + val: float, + cl: str, + vf: dict, + use_case: str, +) -> None: + """ + Translates cognitive signals into placement-level media planning decisions: + - Where this creative will work + - Where it will fail + - Recommended media strategy + """ + + face = vf.get("face_count", 0) > 0 + contrast = vf.get("contrast_score", 50) + objects = vf.get("object_count", 4) + text_pct = vf.get("text_density", 0.1) * 100 + load_high = cl == "High" + load_low = cl == "Low" + attn_str = attn >= 65 + attn_weak = attn < 45 + mem_str = mem >= 65 + mem_weak = mem < 45 + val_pos = val > 0.10 + val_neg = val < -0.05 + + # ── Placement definitions ────────────────────────────────────────────────── + ALL_PLACEMENTS = { + "Social Feed": {"requires": "attn_str or contrast >= 60", "format": "fast-scroll"}, + "YouTube Pre-roll": {"requires": "attn_str and val_pos", "format": "interruptive"}, + "Display / Programmatic": {"requires": "not load_high", "format": "passive"}, + "Retail Media": {"requires": "mem_str and not load_high", "format": "purchase-adjacent"}, + "OOH / DOOH": {"requires": "load_low and not load_high and contrast >= 60", "format": "sub-2s"}, + "Retargeting": {"requires": "not attn_str", "format": "warm-audience"}, + "CTV / Connected TV": {"requires": "val_pos and mem_str", "format": "lean-back"}, + } + + # Evaluate each placement + works, fails = [], [] + + # Social Feed + if attn_str or contrast >= 60: + works.append(("Social Feed", "Attention signal is strong enough to interrupt the scroll")) + else: + fails.append(("Social Feed", "Will not survive the scroll — attention signal too weak to compete in a competitive feed")) + + # YouTube Pre-roll + if attn_str and val_pos: + works.append(("YouTube Pre-roll", "Strong attention + positive valence — will hold viewers through the skip window")) + elif attn_str and not val_pos: + fails.append(("YouTube Pre-roll", "Will get noticed but emotional tone will not build the affinity needed to drive post-view action")) + else: + fails.append(("YouTube Pre-roll", "Insufficient attention to survive the skip — viewers will disengage before the message lands")) + + # Display / Programmatic + if not load_high: + works.append(("Display / Programmatic", "Low cognitive load means the message processes passively — effective for frequency builds")) + else: + fails.append(("Display / Programmatic", "High cognitive load in a passive format — the viewer will not invest the effort required to decode this")) + + # Retail Media + if mem_str and not load_high: + works.append(("Retail Media", "Strong memory encoding at point of purchase proximity — will drive recognition when intent is highest")) + else: + fails.append(("Retail Media", "Weak memory signal means the brand will not be recognised at the shelf or checkout — the moment the placement is designed for")) + + # OOH / DOOH + if load_low and contrast >= 60: + works.append(("OOH / DOOH", "Low load + high contrast — will process within the 1.5s average dwell time for outdoor formats")) + else: + fails.append(("OOH / DOOH", "Will not process in under 2 seconds — too complex or too low-contrast for outdoor dwell times")) + + # CTV + if val_pos and mem_str: + works.append(("CTV / Connected TV", "Positive valence + strong memory encoding — lean-back audiences will absorb and retain the brand message")) + else: + fails.append(("CTV / Connected TV", "Neutral or negative valence in a lean-back format — the emotional opportunity of CTV will be wasted")) + + # ── Strategy ─────────────────────────────────────────────────────────────── + if cpci >= 70: + if attn_str and mem_str: + strategy = "top_funnel" + strat_label = "Full-funnel — prioritise reach" + strat_detail = ( + "Deploy at scale across cold audiences. This creative clears the cognitive bar " + "for both acquisition and recall — a rare combination. Allocate the majority of " + "budget to prospecting. Retargeting will compound the memory already built." + ) + elif attn_str: + strategy = "top_funnel" + strat_label = "Top-of-funnel acquisition" + strat_detail = ( + "Strong in cold audiences — deploy for reach and awareness. " + "Pair with a simpler retargeting creative to close the memory gap, " + "or add a short text overlay to anchor the brand name." + ) + else: + strategy = "frequency" + strat_label = "Frequency play — build recall" + strat_detail = ( + "CPCi is strong but memory needs frequency to compound. Cap at 4–5 " + "impressions per user per week. Recall will build reliably — avoid " + "over-exposing or the returns diminish." + ) + elif cpci >= 55: + strategy = "warm_audience" + strat_label = "Warm audiences — not cold" + strat_detail = ( + "This creative is not ready for cold-audience acquisition — the cognitive " + "signal is too mixed to convert efficiently at scale. Restrict spend to " + "retargeting and lookalike audiences where intent is pre-established. " + "Fix the weakest signal before opening to prospecting." + ) + elif cpci >= 40: + strategy = "retargeting" + strat_label = "Retargeting only — strict frequency cap" + strat_detail = ( + "Limit to retargeting with a hard frequency cap of 2–3 exposures. " + "The cognitive signal is insufficient for cold acquisition — spend here " + "will generate impressions but not conversions. Treat as a stopgap " + "while the creative is rebuilt." + ) + else: + strategy = "hold" + strat_label = "Hold spend — rebuild before deployment" + strat_detail = ( + "Do not deploy at any scale. Budget spent on this creative will generate " + "impressions at near-zero cognitive impact — or worse, accumulate negative " + "brand associations. The creative requires a rebuild before media spend is justified." + ) + + strat_colors = { + "top_funnel": "#22C55E", + "full_funnel": "#22C55E", + "frequency": "#3B82F6", + "warm_audience": "#F59E0B", + "retargeting": "#F59E0B", + "hold": "#EF4444", + } + sc = strat_colors.get(strategy, "#CBD5E1") + + # ── Render ───────────────────────────────────────────────────────────────── + def _place_row(icon, name, reason, good: bool): + dot = "#22C55E" if good else "#EF4444" + label = "Works" if good else "Fails" + return ( + f"
" + f"
" + f"
" + f"
" + f"{name}" + f"{label}" + f"
" + f"
{reason}
" + f"
" + f"
" + ) + + works_html = "".join(_place_row("✓", n, r, True) for n, r in works) + fails_html = "".join(_place_row("✗", n, r, False) for n, r in fails) + + st.markdown( + "
" + "" + "📡 Media Implications
", + unsafe_allow_html=True, + ) + + pl_col, strat_col = st.columns([3, 2], gap="large") + + with pl_col: + st.markdown( + "
" + "Placement Fit
", + unsafe_allow_html=True, + ) + st.markdown(works_html + fails_html, unsafe_allow_html=True) + + with strat_col: + st.markdown( + f"
" + f"
" + f"Recommended Strategy
" + f"
{strat_label}
" + f"
" + f"{strat_detail}" + f"
" + f"
", + unsafe_allow_html=True, + ) + + +# ── Export helpers ──────────────────────────────────────────────────────────── + +def _quick_read_data(cpci, attn, mem, val, cl, vf, use_case): + """Return (prediction, issue, fix) as plain strings — no HTML.""" + ctx = { + "FMCG Branding": "brand awareness campaigns", + "Performance Marketing": "paid social and search ads", + "Retail Media": "retail and in-store placements", + }.get(use_case, "this campaign type") + + if cpci >= 70: + prediction = f"This creative will earn its media spend in {ctx} — the cognitive gates are clear." + elif cpci >= 55: + prediction = f"This creative will generate impressions in {ctx} but will not convert efficiently — one weak signal is costing reach." + elif cpci >= 40: + prediction = f"This creative will struggle in cold audiences for {ctx} — it may survive retargeting but will not scale profitably." + else: + prediction = f"This creative will underperform regardless of budget — the cognitive barriers are too significant to overcome with spend." + + attn_gap = max(0, 60 - attn) + mem_gap = max(0, 70 - mem) + val_gap = max(0, 0.1 - val) * 100 + load_gap = 25 if cl == "High" else (10 if cl == "Medium" else 0) + worst = max(attn_gap, mem_gap, val_gap, load_gap) + + if worst == 0: + issue = "All signals are above threshold — no single dimension is holding this back." + elif worst == load_gap and cl == "High": + issue = "Visual overload will cause viewers to disengage before the message registers — the brain cannot parse this quickly enough in a feed." + elif worst == attn_gap: + issue = ("This creative will fail to trigger an orienting response — without a face or salient focal point, the brain will not interrupt scrolling to process it." + if vf.get("face_count", 0) == 0 else + "This creative will disappear in a busy feed — insufficient contrast means it fails the first visual gate before content is even assessed." + if vf.get("contrast_score", 50) < 45 else + "This creative will be processed at low depth — no dominant focal point means the brain distributes attention thinly and nothing is prioritised.") + elif worst == mem_gap: + issue = ("This creative will be remembered only if repeatedly exposed — text overload prevents dual-coding, so neither the visual nor verbal message encodes cleanly." + if vf.get("text_density", 0) > 0.25 else + "This creative will be seen but not recalled — without a verbal anchor, the visual alone will not survive beyond a few seconds in working memory." + if vf.get("text_density", 0) < 0.05 else + "This creative will not build brand recall efficiently — too many competing elements mean nothing dominates the memory trace.") + elif worst == val_gap: + issue = ("This creative will generate neutral-to-negative affect on each exposure — without a human face, the palette alone cannot drive positive brand association." + if vf.get("face_count", 0) == 0 else + "This creative will subtly undermine brand affinity over time — the colour palette is triggering mild avoidance without the viewer being aware of it.") + else: + issue = "This creative demands more cognitive effort than a scrolling audience will commit — the message will not land in fast-feed placements." + + if vf.get("face_count", 0) == 0 and attn_gap > 15: + fix = "Introduce a human face as the dominant visual element — it is the single fastest mechanism for triggering an orienting response in feed." + elif vf.get("contrast_score", 50) < 45 and attn_gap > 10: + fix = "Increase foreground-to-background contrast significantly — the creative needs to pass the visual salience test before any other signal matters." + elif vf.get("object_count", 0) > 7 and (worst == load_gap or worst == mem_gap): + fix = "Eliminate all secondary visual elements and commit to a single hero — cognitive load is suppressing every other signal." + elif vf.get("text_density", 0) > 0.28: + fix = "Reduce copy to a single declarative line under six words — audiences process far less text than advertisers write." + elif vf.get("text_density", 0) < 0.04 and mem_gap > 15: + fix = "Add a 4–6 word brand or message line — the verbal channel is completely unused, which is costing you recall without any benefit." + elif val < -0.05 and vf.get("face_count", 0) == 0: + fix = "Add a person with a natural, warm expression — emotional valence cannot be fixed with colour alone at this deficit." + elif val < -0.05: + fix = "Replace the dominant cool or dark tones with warmer equivalents — the palette is the primary driver of the negative valence reading." + elif attn_gap > 10: + fix = "Scale up the primary subject and increase its contrast against the background — give the eye an unmissable landing point." + elif mem_gap > 15: + fix = "Reduce the composition to one dominant image and one short message — simplicity is the only reliable route to single-exposure recall." + else: + fix = "Test a version with a human face replacing the current hero element — it will lift both attention and emotional valence simultaneously." + + return prediction, issue, fix + + +def _build_report_data(r: dict, use_case: str, client_name: str = "") -> dict: + """Assemble all client-report content into a plain dict.""" + s = r["signals"] + vf = r["visual_features"] + narr = r.get("narrative", {}) + cpci = r["cpci"] + attn = s["attention_score"] + mem = s["memory_score"] + val = s["emotional_valence"] + cl = s["cognitive_load"] + _rsn = r.get("reasoning") or {} + cl_score = ( + _rsn["load"]["composite"] + if isinstance(_rsn, dict) and "load" in _rsn + else s.get("cognitive_load_score", 50) + ) + + if cpci >= 70: perf_label = "Strong Performer" + elif cpci >= 40: perf_label = "Needs Optimisation" + else: perf_label = "Not Ready to Scale" + + if attn > 60: a_label = "High Attention" + elif attn >= 30: a_label = "Moderate" + else: a_label = "Scroll-Past Risk" + + if mem > 70: m_label = "Strong Recall" + elif mem >= 40: m_label = "Moderate" + else: m_label = "Low Retention" + + if val > 0.1: v_label = "Positive" + elif val > -0.1: v_label = "Neutral" + else: v_label = "Negative" + + verdict = _final_verdict_text(cpci, attn, mem, val, cl, use_case) + prediction, issue, fix = _quick_read_data(cpci, attn, mem, val, cl, vf, use_case) + recommendation = _client_recommendation(narr, cpci, attn, mem, val, cl, vf) + + # Creative Brief data + if cpci >= 70: scale_label = "Ready to scale" + elif cpci >= 55: scale_label = "Optimise before scaling" + elif cpci >= 40: scale_label = "Not ready for scale" + else: scale_label = "Do not deploy" + + uc = USE_CASES.get(use_case, {}) + w = uc.get("weights", {"attention": 0.4, "memory": 0.3, "emotion": 0.3}) + + # Compute top-3 optimization scenarios for PDF recommendations + scenarios = _compute_scenarios(cpci, attn, mem, val, cl, vf, use_case) + + return { + "name": r["name"], + "use_case": use_case, + "client_name": client_name, + "cpci": cpci, + "perf_label": perf_label, + "scale_label": scale_label, + "verdict": verdict, + "performance": prediction, + "core_issue": issue, + "fix_first": fix, + "recommendation": recommendation, + # signals + "attn": attn, "attn_label": a_label, + "mem": mem, "mem_label": m_label, + "val": val, "val_label": v_label, + "cl": cl, "cl_score": cl_score, + # visual features + "face_count": vf.get("face_count", 0), + "contrast_score": vf.get("contrast_score", 0), + "object_count": vf.get("object_count", 0), + "text_density": vf.get("text_density", 0), + "dominant_colors": vf.get("dominant_colors", []), + # narrative + "strategic_implication": narr.get("strategic_implication", ""), + # weights + "w_attn": int(w.get("attention", 0.4) * 100), + "w_mem": int(w.get("memory", 0.3) * 100), + "w_emo": int(w.get("emotion", 0.3) * 100), + # optimization scenarios (top 3, sorted by lift desc) + "scenarios": scenarios[:3], + } + + +def _generate_pdf_bytes(data: dict) -> bytes: + """Build a full multi-section client-ready PDF. Returns raw bytes.""" + import io + from reportlab.lib.pagesizes import A4 + from reportlab.lib import colors + from reportlab.lib.units import mm + from reportlab.platypus import ( + SimpleDocTemplate, Paragraph, Spacer, HRFlowable, Table, TableStyle, PageBreak, + KeepTogether, + ) + from reportlab.lib.styles import ParagraphStyle + from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT + from datetime import date + + buf = io.BytesIO() + W, H = A4 + margin = 22 * mm + inner = W - 2 * margin + + doc = SimpleDocTemplate( + buf, + pagesize=A4, + leftMargin=margin, rightMargin=margin, + topMargin=20 * mm, bottomMargin=20 * mm, + ) + + # ── Color palette ───────────────────────────────────────────────────────── + BLUE = colors.HexColor("#3B82F6") + WHITE = colors.HexColor("#FFFFFF") + MUTED = colors.HexColor("#CBD5E1") + LABEL = colors.HexColor("#94A3B8") + BORDER = colors.HexColor("#1F2937") + CARD = colors.HexColor("#141B24") + GREEN = colors.HexColor("#22C55E") + AMBER = colors.HexColor("#F59E0B") + RED = colors.HexColor("#EF4444") + BG = colors.HexColor("#0B0F14") + + cpci = data["cpci"] + sc = GREEN if cpci >= 70 else (AMBER if cpci >= 40 else RED) + # Plain hex strings for use inside tags in Paragraph markup. + # HexColor.hexval() returns "0x..." — the leading "0" makes the slice wrong, + # so we define the strings directly from the thresholds instead. + sc_hex = "#22C55E" if cpci >= 70 else ("#F59E0B" if cpci >= 40 else "#EF4444") + + # signal colors + def sig_c(v, hi=60, lo=30): + return GREEN if v >= hi else (AMBER if v >= lo else RED) + def sig_hex(v, hi=60, lo=30): + return "#22C55E" if v >= hi else ("#F59E0B" if v >= lo else "#EF4444") + a_c = sig_c(data["attn"]) + m_c = sig_c(data["mem"], 70, 40) + v_c = GREEN if data["val"] > 0.1 else (AMBER if data["val"] > -0.1 else RED) + cl_c = GREEN if data["cl"] == "Low" else (AMBER if data["cl"] == "Medium" else RED) + a_hex = sig_hex(data["attn"]) + m_hex = sig_hex(data["mem"], 70, 40) + v_hex = "#22C55E" if data["val"] > 0.1 else ("#F59E0B" if data["val"] > -0.1 else "#EF4444") + cl_hex = "#22C55E" if data["cl"] == "Low" else ("#F59E0B" if data["cl"] == "Medium" else "#EF4444") + + # ── Style factory ───────────────────────────────────────────────────────── + def S(name, **kw): + return ParagraphStyle(name, **kw) + + sEye = S("eye", fontSize=7, leading=10, textColor=LABEL, + fontName="Helvetica", spaceAfter=1, letterSpacing=0.8) + sTitle = S("title", fontSize=24, leading=30, textColor=WHITE, + fontName="Helvetica-Bold", spaceAfter=4) + sSub = S("sub", fontSize=10, leading=14, textColor=MUTED, + fontName="Helvetica", spaceAfter=0) + sLbl = S("lbl", fontSize=7, leading=10, textColor=LABEL, + fontName="Helvetica-Bold", spaceBefore=14, spaceAfter=5, + letterSpacing=1.2) + sScore = S("score", fontSize=72, leading=76, textColor=sc, + fontName="Helvetica-Bold", spaceAfter=0) + sPerf = S("perf", fontSize=12, leading=16, textColor=sc, + fontName="Helvetica-Bold") + sVerd = S("verd", fontSize=17, leading=24, textColor=sc, + fontName="Helvetica-Bold", spaceAfter=0) + sH2 = S("h2", fontSize=11, leading=15, textColor=WHITE, + fontName="Helvetica-Bold", spaceBefore=12, spaceAfter=4) + sBody = S("body", fontSize=11, leading=17, textColor=WHITE, + fontName="Helvetica", spaceAfter=0) + sMuted = S("muted", fontSize=10, leading=15, textColor=MUTED, + fontName="Helvetica", spaceAfter=0) + sTag = S("tag", fontSize=8, leading=12, textColor=LABEL, + fontName="Helvetica-Bold", letterSpacing=0.5) + sFoot = S("foot", fontSize=7, leading=10, textColor=LABEL, + fontName="Helvetica", alignment=TA_CENTER) + sNote = S("note", fontSize=9, leading=13, textColor=LABEL, + fontName="Helvetica-Oblique", spaceAfter=0) + sSigV = S("sigv", fontSize=22, leading=26, textColor=WHITE, + fontName="Helvetica-Bold", spaceAfter=0) + sSigL = S("sigl", fontSize=8, leading=11, textColor=LABEL, + fontName="Helvetica", spaceAfter=0) + sSigN = S("sign", fontSize=8, leading=11, textColor=LABEL, + fontName="Helvetica-Bold", letterSpacing=0.8, spaceAfter=0) + + def rule(c=BORDER, t=0.4): + return HRFlowable(width="100%", thickness=t, color=c, spaceAfter=6, spaceBefore=6) + + def thick_rule(c=BLUE): + return HRFlowable(width="100%", thickness=1.5, color=c, spaceAfter=6, spaceBefore=0) + + def section_label(text): + return Paragraph(text.upper(), sLbl) + + def card_table(rows_data, col_widths, style_extras=None): + t = Table(rows_data, colWidths=col_widths) + base = [ + ("BACKGROUND", (0, 0), (-1, -1), CARD), + ("BOX", (0, 0), (-1, -1), 0.5, BORDER), + ("ROUNDEDCORNERS", [4]), + ("LEFTPADDING", (0, 0), (-1, -1), 10), + ("RIGHTPADDING", (0, 0), (-1, -1), 10), + ("TOPPADDING", (0, 0), (-1, -1), 10), + ("BOTTOMPADDING",(0, 0), (-1, -1), 10), + ("VALIGN", (0, 0), (-1, -1), "TOP"), + ] + if style_extras: + base += style_extras + t.setStyle(TableStyle(base)) + return t + + today = date.today().strftime("%B %d, %Y") + story = [] + + # Shared footer text + FOOTER_TEXT = "ADVantage Insights · Cognitive Signal Engine™ · © Anil Pandit · Confidential" + + # ══════════════════════════════════════════════════════════════════════════ + # COVER PAGE + # ══════════════════════════════════════════════════════════════════════════ + + # Large product name + sCoverProduct = S("coverprod", fontSize=11, leading=15, textColor=BLUE, + fontName="Helvetica-Bold", letterSpacing=2.0, spaceAfter=4) + sCoverTagline = S("covertagline", fontSize=13, leading=18, textColor=MUTED, + fontName="Helvetica", spaceAfter=0) + sCoverHero = S("coverhero", fontSize=36, leading=44, textColor=WHITE, + fontName="Helvetica-Bold", spaceAfter=6) + sCoverMeta = S("covermeta", fontSize=10, leading=15, textColor=LABEL, + fontName="Helvetica", spaceAfter=0) + sCoverClient = S("coverclient", fontSize=22, leading=28, textColor=WHITE, + fontName="Helvetica-Bold", spaceAfter=0) + sCoverSub = S("coversub", fontSize=10, leading=15, textColor=MUTED, + fontName="Helvetica", spaceAfter=0) + + # ── Top brand strip ─────────────────────────────────────────────────────── + story.append(Spacer(1, 28)) + story.append(Paragraph("COGNITIVE SIGNAL ENGINE™", sCoverProduct)) + story.append(Paragraph("Creative Intelligence Analyzer", sCoverSub)) + story.append(Spacer(1, 32)) + story.append(thick_rule(BLUE)) + story.append(Spacer(1, 40)) + + # ── Hero score & product name ───────────────────────────────────────────── + story.append(Paragraph( + f"{cpci}", + S("covercpci", fontSize=96, leading=100, textColor=sc, + fontName="Helvetica-Bold", spaceAfter=4), + )) + story.append(Paragraph("CPCi — Cost Per Cognitive Impression", sCoverSub)) + + # ── 1-line CPCi interpretation ──────────────────────────────────────────── + if cpci >= 70: + _interp_hex = "#22C55E" + _interp_text = "High efficiency — ready for scale" + elif cpci >= 40: + _interp_hex = "#F59E0B" + _interp_text = "Moderate efficiency — optimise before scaling" + else: + _interp_hex = "#EF4444" + _interp_text = "High inefficiency — not ready for deployment" + + story.append(Spacer(1, 8)) + story.append(Paragraph( + f"{_interp_text}", + S("coverinterp", fontSize=11, leading=15, fontName="Helvetica", + textColor=colors.HexColor(_interp_hex), alignment=TA_CENTER, spaceAfter=0), + )) + story.append(Spacer(1, 24)) + + # ── Creative title ──────────────────────────────────────────────────────── + story.append(Paragraph(data["name"], sCoverHero)) + story.append(Spacer(1, 12)) + story.append(Paragraph(f"Use Case: {data['use_case']}", sCoverMeta)) + story.append(Spacer(1, 4)) + + if data.get("client_name"): + story.append(Spacer(1, 14)) + story.append(Paragraph( + "CLIENT", + S("coverclientlabel", fontSize=9, leading=12, textColor=MUTED, + fontName="Helvetica-Bold", letterSpacing=1.8, spaceAfter=3), + )) + story.append(Paragraph(data["client_name"], sCoverClient)) + story.append(Spacer(1, 14)) + + story.append(Paragraph(f"Date: {today}", sCoverMeta)) + story.append(Spacer(1, 40)) + story.append(thick_rule(BLUE)) + story.append(Spacer(1, 16)) + + # ── Prepared by line ────────────────────────────────────────────────────── + story.append(Paragraph( + "Prepared by Cognitive Signal Engine™ · ADVantage Insights · Anil Pandit", + S("coverprepby", fontSize=9, leading=13, textColor=LABEL, + fontName="Helvetica", alignment=TA_CENTER), + )) + + story.append(Spacer(1, 8)) + story.append(rule()) + story.append(Paragraph(FOOTER_TEXT, sFoot)) + + # ══════════════════════════════════════════════════════════════════════════ + # EXECUTIVE SUMMARY PAGE + # ══════════════════════════════════════════════════════════════════════════ + story.append(PageBreak()) + + story.append(Paragraph("COGNITIVE SIGNAL ANALYSIS REPORT", sEye)) + story.append(Paragraph("EXECUTIVE SUMMARY", S("execeyebrow", fontSize=8, leading=11, + textColor=BLUE, fontName="Helvetica-Bold", spaceAfter=2, letterSpacing=1.2))) + story.append(thick_rule(BLUE)) + story.append(Spacer(1, 10)) + + # ── CPCi Score callout ──────────────────────────────────────────────────── + story.append(section_label("CPCi — Cost Per Cognitive Impression")) + exec_score_row = Table( + [[Paragraph(str(cpci), sScore), + Spacer(6, 1), + Paragraph( + f"{data['perf_label']}

" + f"Scale decision: {data['scale_label']}

" + f"Use-case weights:
" + f"Attention {data['w_attn']}% · Memory {data['w_mem']}% · Emotion {data['w_emo']}%", + sMuted, + )]], + colWidths=[55*mm, 6*mm, None], + ) + exec_score_row.setStyle(TableStyle([ + ("VALIGN", (0,0),(-1,-1),"MIDDLE"), + ("LEFTPADDING", (0,0),(-1,-1),0), + ("RIGHTPADDING", (0,0),(-1,-1),0), + ("TOPPADDING", (0,0),(-1,-1),0), + ("BOTTOMPADDING",(0,0),(-1,-1),0), + ])) + story.append(exec_score_row) + story.append(Spacer(1, 12)) + story.append(rule()) + + # ── Verdict ─────────────────────────────────────────────────────────────── + story.append(section_label("Verdict")) + story.append(Paragraph(data["verdict"], sVerd)) + story.append(Spacer(1, 12)) + story.append(rule()) + + # ══════════════════════════════════════════════════════════════════════════ + # EXECUTIVE DECISION BLOCK + # ══════════════════════════════════════════════════════════════════════════ + _ed_mem = data["mem"] + _ed_eff_mem = max(_ed_mem, 10) + _ed_multiplier = round(70 / _ed_eff_mem, 1) + + if _ed_mem < 55: + _ed_risk = "HIGH" + elif _ed_mem < 70: + _ed_risk = "MODERATE" + else: + _ed_risk = "LOW" + + if _ed_multiplier > 1.8 or _ed_risk == "HIGH": + _ed_decision = "DO NOT SCALE IN COLD MEDIA" + _ed_tone_hex = "#EF4444" + _ed_bg_hex = "#1F0A0A" + _ed_border_hex = "#7F1D1D" + _ed_rec = "Deploy in retargeting / warm audiences only" + elif _ed_multiplier > 1.3 or _ed_risk == "MODERATE": + _ed_decision = "OPTIMISE BEFORE SCALING" + _ed_tone_hex = "#F59E0B" + _ed_bg_hex = "#1A1200" + _ed_border_hex = "#78350F" + _ed_rec = "Test in controlled budget before scaling" + else: + _ed_decision = "READY TO SCALE" + _ed_tone_hex = "#22C55E" + _ed_bg_hex = "#071A0F" + _ed_border_hex = "#14532D" + _ed_rec = "Safe for broader deployment" + + _ed_tone_c = colors.HexColor(_ed_tone_hex) + _ed_bg_c = colors.HexColor(_ed_bg_hex) + _ed_border_c = colors.HexColor(_ed_border_hex) + + # Outer box table (1×1 with background + border) + _ed_content = [ + # TITLE row + Paragraph( + "EXECUTIVE DECISION", + S("edtitle", fontSize=8, fontName="Helvetica-Bold", leading=11, + textColor=colors.HexColor(_ed_tone_hex), letterSpacing=2.0, + spaceAfter=6), + ), + # DECISION row — large, bold, colour-coded + Paragraph( + f"{_ed_decision}", + S("eddecision", fontSize=22, fontName="Helvetica-Bold", leading=28, + textColor=_ed_tone_c, spaceAfter=8), + ), + # Supporting line + Paragraph( + f"This creative will cost approximately {_ed_multiplier}× more media " + f"and has {_ed_risk} cold audience deployment risk.", + S("edsupport", fontSize=10, fontName="Helvetica", leading=15, + textColor=colors.HexColor("#CBD5E1"), spaceAfter=6), + ), + # Recommendation line + Paragraph( + f"▶ {_ed_rec}", + S("edrec", fontSize=10, fontName="Helvetica-Bold", leading=14, + textColor=_ed_tone_c, spaceAfter=0), + ), + ] + + _ed_inner = Table( + [[_ed_content[0]], [_ed_content[1]], [_ed_content[2]], [_ed_content[3]]], + colWidths=[None], + ) + _ed_inner.setStyle(TableStyle([ + ("BACKGROUND", (0,0),(-1,-1), _ed_bg_c), + ("BOX", (0,0),(-1,-1), 1.5, _ed_border_c), + ("LEFTPADDING", (0,0),(-1,-1), 16), + ("RIGHTPADDING", (0,0),(-1,-1), 16), + ("TOPPADDING", (0,0),(0,0), 14), # more top padding on title row + ("TOPPADDING", (1,0),(-1,-1), 2), + ("BOTTOMPADDING",(0,0),(-2,-1), 4), + ("BOTTOMPADDING",(-1,0),(-1,-1), 14), # more bottom padding on last row + ("VALIGN", (0,0),(-1,-1), "TOP"), + ("LINEBELOW", (0,0),(0,0), 0.5, colors.HexColor(_ed_border_hex)), # line under title + ])) + + story.append(Spacer(1, 4)) + story.append(_ed_inner) + story.append(Spacer(1, 14)) + story.append(rule()) + + # ── Business Impact summary ─────────────────────────────────────────────── + story.append(section_label("Business Impact")) + + if cpci >= 70: + risk_label = "High Efficiency" + risk_hex = "#22C55E" + impact_bullets = [ + "Impression quality is strong — CPCi above 70 indicates a top-quartile creative.", + "Conversion signal is optimised — high attention and memory scores drive click-through intent.", + "Brand equity: positive valence builds cumulative warmth across exposures.", + "Deployment decision: deploy at full planned budget without optimisation.", + ] + elif cpci >= 40: + risk_label = "Moderate Performance" + risk_hex = "#F59E0B" + impact_bullets = [ + "Impression quality is moderate — some cognitive signal gap remains before peak efficiency.", + "Conversion signal is partial — memory or attention weakness will require higher frequency.", + "Brand equity: neutral-to-positive valence; brand sentiment stable but not building.", + "Deployment decision: optimise the primary signal weakness before full budget deployment.", + ] + else: + risk_label = "High Waste Risk" + risk_hex = "#EF4444" + impact_bullets = [ + "Impression quality is poor — CPCi below 40 means most impressions will not cognitively register.", + "Conversion signal is absent — media spend is unlikely to generate measurable purchase intent.", + "Brand equity: low or negative valence risks eroding brand affinity over repeated exposure.", + "Deployment decision: do not deploy. Revise the creative before any media investment.", + ] + + risk_c = colors.HexColor(risk_hex) + story.append(Paragraph( + f"{risk_label}", + S("risklabel", fontSize=14, leading=18, textColor=risk_c, + fontName="Helvetica-Bold", spaceAfter=8), + )) + story.append(Spacer(1, 4)) + + bi_rows = [] + for i, bullet in enumerate(impact_bullets, 1): + bi_rows.append([ + Paragraph(f"0{i}", S("binum", fontSize=9, fontName="Helvetica-Bold", + leading=13, textColor=LABEL)), + Paragraph(bullet, S("bibody", fontSize=10, fontName="Helvetica", + leading=15, textColor=WHITE)), + ]) + + bi_t = Table(bi_rows, colWidths=[14*mm, None]) + bi_t.setStyle(TableStyle([ + ("BACKGROUND", (0,0),(-1,-1), CARD), + ("BOX", (0,0),(-1,-1), 0.5, BORDER), + ("INNERGRID", (0,0),(-1,-1), 0.3, BORDER), + ("LEFTPADDING", (0,0),(-1,-1), 10), + ("RIGHTPADDING", (0,0),(-1,-1), 10), + ("TOPPADDING", (0,0),(-1,-1), 9), + ("BOTTOMPADDING",(0,0),(-1,-1), 9), + ("VALIGN", (0,0),(-1,-1), "TOP"), + ("LINEAFTER", (0,0),(0,-1), 0.5, BORDER), + ])) + story.append(bi_t) + story.append(Spacer(1, 12)) + story.append(rule()) + + # ── Media Efficiency Impact ─────────────────────────────────────────────── + story.append(section_label("Media Efficiency Impact")) + + _pdf_mem = data["mem"] + _pdf_eff_mem = max(_pdf_mem, 10) + _pdf_multiplier = round(70 / _pdf_eff_mem, 1) + _pdf_mult_hex = ( + "#EF4444" if _pdf_multiplier > 1.8 else + "#F59E0B" if _pdf_multiplier >= 1.2 else + "#22C55E" + ) + _pdf_mult_color = colors.HexColor(_pdf_mult_hex) + + sMedEff = S("medeff", fontSize=13, leading=18, fontName="Helvetica-Bold", + textColor=WHITE, spaceAfter=6) + sMedSub = S("medsub", fontSize=11, leading=16, fontName="Helvetica", + textColor=colors.HexColor("#F87171"), spaceAfter=0) + + if _pdf_mem < 70: + story.append(Paragraph( + f"" + f"This creative will cost approximately {_pdf_multiplier}× more media " + f"to achieve the same recall as a top-quartile creative." + f"", + sMedEff, + )) + if _pdf_multiplier > 1.5: + story.append(Spacer(1, 4)) + story.append(Paragraph( + "Equivalent to wasting ~35–50% of your media budget on ineffective impressions.", + sMedSub, + )) + else: + story.append(Paragraph( + "" + "This creative is operating at efficient memory levels — no excess media cost." + "", + sMedEff, + )) + + story.append(Spacer(1, 12)) + story.append(rule()) + + # ── What This Means (executive summary version) ─────────────────────────── + story.append(section_label("Quick Read")) + wtm_rows_exec = [ + ("PERFORMANCE", data["performance"]), + ("CORE ISSUE", data["core_issue"]), + ("FIX FIRST", data["fix_first"]), + ] + for tag, body in wtm_rows_exec: + row = Table( + [[Paragraph(tag, sTag), Paragraph(body, sBody)]], + colWidths=[36*mm, None], + ) + row.setStyle(TableStyle([ + ("VALIGN", (0,0),(-1,-1),"TOP"), + ("LEFTPADDING", (0,0),(-1,-1),0), + ("RIGHTPADDING", (0,0),(-1,-1),0), + ("TOPPADDING", (0,0),(-1,-1),7), + ("BOTTOMPADDING",(0,0),(-1,-1),7), + ("LINEBELOW", (0,0),(-1,-1),0.3,BORDER), + ])) + story.append(row) + + story.append(Spacer(1, 8)) + story.append(rule()) + story.append(Paragraph(FOOTER_TEXT, sFoot)) + + # ══════════════════════════════════════════════════════════════════════════ + # PAGE — CPCi · Verdict · What This Means (detailed) + # ══════════════════════════════════════════════════════════════════════════ + story.append(PageBreak()) + + # Header + story.append(Paragraph("COGNITIVE SIGNAL ANALYSIS REPORT", sEye)) + story.append(Paragraph("Cognitive Signal Engine™ · Creative Intelligence Analyzer · CPCi", sEye)) + story.append(Spacer(1, 6)) + story.append(thick_rule(BLUE)) + story.append(Spacer(1, 6)) + + story.append(Paragraph(data["name"], sTitle)) + story.append(Paragraph(f"{data['use_case']} · {today}", sSub)) + story.append(Spacer(1, 16)) + story.append(rule()) + + # ── CPCi Score — technical breakdown ───────────────────────────────────── + # (business summary already on Page 2; this page shows the formula detail) + story.append(section_label("CPCi — Score & Formula Detail")) + + score_row = Table( + [[Paragraph(str(cpci), sScore), Spacer(6, 1), Paragraph( + f"{data['perf_label']}

" + f"Scale decision: {data['scale_label']}

" + f"Formula weights for {data['use_case']}:
" + f"Attention {data['w_attn']}% · Memory {data['w_mem']}% " + f"· Emotion {data['w_emo']}%

" + f"CPCi = (Attention × {data['w_attn']}%) + (Memory × {data['w_mem']}%) " + f"+ (Emotion × {data['w_emo']}%) − Load Penalty", + sMuted)]], + colWidths=[55*mm, 6*mm, None], + ) + score_row.setStyle(TableStyle([ + ("VALIGN", (0,0),(-1,-1),"MIDDLE"), + ("LEFTPADDING", (0,0),(-1,-1),0), + ("RIGHTPADDING", (0,0),(-1,-1),0), + ("TOPPADDING", (0,0),(-1,-1),0), + ("BOTTOMPADDING",(0,0),(-1,-1),0), + ])) + story.append(score_row) + story.append(Spacer(1, 10)) + story.append(rule()) + + # ── Verdict ─────────────────────────────────────────────────────────────── + story.append(section_label("Deployment Verdict")) + story.append(Paragraph(data["verdict"], sVerd)) + story.append(Spacer(1, 14)) + story.append(rule()) + + story.append(Spacer(1, 6)) + + # Footer page + story.append(rule()) + story.append(Paragraph(FOOTER_TEXT, sFoot)) + + # ══════════════════════════════════════════════════════════════════════════ + # PAGE 2 — Signal Breakdown + # ══════════════════════════════════════════════════════════════════════════ + story.append(PageBreak()) + + story.append(Paragraph("COGNITIVE SIGNAL ANALYSIS REPORT", sEye)) + story.append(thick_rule(BLUE)) + story.append(Spacer(1, 6)) + + story.append(section_label("Cognitive Signal Breakdown")) + story.append(Paragraph( + "The Cognitive Signal Engine measures four independent brain-level signals. " + "Each is scored separately, then weighted by use-case to produce the CPCi. " + "The table below shows the detected value, threshold, and what it means for media performance.", + sMuted, + )) + story.append(Spacer(1, 10)) + + # ── Visual signal bars ──────────────────────────────────────────────────── + def _make_bar(score_pct: float, hex_color: str, total_mm: float = 95) -> Table: + """Horizontal progress bar as a two-cell nested Table.""" + filled_w = max(score_pct / 100, 0.02) * total_mm * mm + empty_w = max(total_mm * mm - filled_w, 0) + bar = Table([[Spacer(1, 1), Spacer(1, 1)]], + colWidths=[filled_w, empty_w], rowHeights=[7]) + bar.setStyle(TableStyle([ + ("BACKGROUND", (0,0),(0,0), colors.HexColor(hex_color)), + ("BACKGROUND", (1,0),(1,0), colors.HexColor("#1F2937")), + ("LEFTPADDING", (0,0),(-1,-1), 0), + ("RIGHTPADDING", (0,0),(-1,-1), 0), + ("TOPPADDING", (0,0),(-1,-1), 0), + ("BOTTOMPADDING",(0,0),(-1,-1), 0), + ])) + return bar + + _val_pct = round((data["val"] + 1) / 2 * 100) # -1..+1 → 0..100 + + _bar_rows = [ + ("Attention", data["attn"], a_hex, f"{data['attn']}/100"), + ("Memory", data["mem"], m_hex, f"{data['mem']}/100"), + ("Emotional Valence", _val_pct, v_hex, f"{data['val']:+.2f}"), + ("Cognitive Load", data["cl_score"], cl_hex, + f"{data['cl']} ({data['cl_score']:.0f}/100)"), + ] + + _sBarLabel = S("blabel", fontSize=9, fontName="Helvetica-Bold", + leading=13, textColor=WHITE) + _sBarScore = S("bscore", fontSize=10, fontName="Helvetica-Bold", + leading=14, textColor=WHITE) + + for _bl, _bs, _bh, _bd in _bar_rows: + _br = Table( + [[Paragraph(f"{_bl}", _sBarLabel), + _make_bar(_bs, _bh), + Paragraph(f"{_bd}", _sBarScore)]], + colWidths=[38*mm, 95*mm, None], + ) + _br.setStyle(TableStyle([ + ("VALIGN", (0,0),(-1,-1), "MIDDLE"), + ("LEFTPADDING", (0,0),(-1,-1), 0), + ("RIGHTPADDING", (0,0),(-1,-1), 0), + ("TOPPADDING", (0,0),(-1,-1), 6), + ("BOTTOMPADDING",(0,0),(-1,-1), 6), + ("LINEBELOW", (0,0),(-1,-1), 0.3, BORDER), + ])) + story.append(_br) + + story.append(Spacer(1, 14)) + story.append(rule()) + story.append(section_label("Signal Detail")) + story.append(Spacer(1, 6)) + + # Signal explanation rows + _RL_COLOR_MAP = {GREEN: "#22C55E", AMBER: "#F59E0B", RED: "#EF4444", + WHITE: "#FFFFFF", MUTED: "#CBD5E1", LABEL: "#94A3B8"} + + def sig_row(label, score_str, color, threshold_str, what_it_measures, implication): + chex = _RL_COLOR_MAP.get(color, "#FFFFFF") + return [ + Paragraph(f"{label}", S("sl", fontSize=9, fontName="Helvetica-Bold", + leading=13, textColor=WHITE)), + Paragraph(f"{score_str}", + S("sv", fontSize=14, fontName="Helvetica-Bold", + leading=18, textColor=color)), + Paragraph(threshold_str, S("st", fontSize=8, fontName="Helvetica", + leading=12, textColor=MUTED)), + Paragraph(what_it_measures, S("sm", fontSize=9, fontName="Helvetica", + leading=13, textColor=MUTED)), + Paragraph(implication, S("si", fontSize=9, fontName="Helvetica", + leading=13, textColor=WHITE)), + ] + + # Header row + hdr_style = S("hdr", fontSize=7, fontName="Helvetica-Bold", leading=10, + textColor=LABEL, letterSpacing=0.8) + sig_table_data = [ + [Paragraph("SIGNAL", hdr_style), Paragraph("SCORE", hdr_style), + Paragraph("THRESHOLD", hdr_style), Paragraph("WHAT IT MEASURES", hdr_style), + Paragraph("IMPLICATION", hdr_style)], + ] + + # Attention row + attn = data["attn"] + sig_table_data.append([ + Paragraph("Attention", S("_", fontSize=9, fontName="Helvetica-Bold", leading=13, textColor=WHITE)), + Paragraph(f"{attn}/100", S("_", fontSize=14, fontName="Helvetica-Bold", leading=18, textColor=a_c)), + Paragraph("Good ≥ 60\nWeak < 30", S("_", fontSize=8, fontName="Helvetica", leading=12, textColor=MUTED)), + Paragraph("Visual stopping power in a competitive feed. Based on face presence, contrast, and object clutter.", S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=MUTED)), + Paragraph( + ("Will interrupt scrolling and trigger processing in cold audiences." if attn > 60 + else "Will not reliably stop cold audiences — loses the first cognitive gate." if attn >= 30 + else "Will scroll past — no mechanism to trigger an orienting response."), + S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=WHITE) + ), + ]) + + # Memory row + mem = data["mem"] + sig_table_data.append([ + Paragraph("Memory", S("_", fontSize=9, fontName="Helvetica-Bold", leading=13, textColor=WHITE)), + Paragraph(f"{mem}/100", S("_", fontSize=14, fontName="Helvetica-Bold", leading=18, textColor=m_c)), + Paragraph("Good ≥ 70\nWeak < 40", S("_", fontSize=8, fontName="Helvetica", leading=12, textColor=MUTED)), + Paragraph("Brand recall potential after a single exposure. Based on text density, visual simplicity, and dual-coding principles.", S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=MUTED)), + Paragraph( + ("Strong memory encoding — brand will be recognised at point of purchase." if mem > 70 + else "Moderate recall — will require frequency to build durable memory." if mem >= 40 + else "Low retention — brand will not survive a single-exposure feed environment."), + S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=WHITE) + ), + ]) + + # Emotional Valence row + val = data["val"] + sig_table_data.append([ + Paragraph("Emotional Valence", S("_", fontSize=9, fontName="Helvetica-Bold", leading=13, textColor=WHITE)), + Paragraph(f"{val:+.2f}", S("_", fontSize=14, fontName="Helvetica-Bold", leading=18, textColor=v_c)), + Paragraph("Positive > +0.10\nNegative < -0.10", S("_", fontSize=8, fontName="Helvetica", leading=12, textColor=MUTED)), + Paragraph("Positive vs. negative emotional tone. Derived from colour psychology, face expression cues, and palette warmth. Range: -1.0 to +1.0.", S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=MUTED)), + Paragraph( + ("Positive affect — will build brand warmth and purchase intent over repeated exposures." if val > 0.1 + else "Neutral affect — will not build or erode brand sentiment." if val > -0.1 + else "Negative affect — may subtly undermine brand affinity over time."), + S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=WHITE) + ), + ]) + + # Cognitive Load row + cl = data["cl"] + cl_score = data["cl_score"] + sig_table_data.append([ + Paragraph("Cognitive Load", S("_", fontSize=9, fontName="Helvetica-Bold", leading=13, textColor=WHITE)), + Paragraph(f"{cl}
{cl_score:.0f}/100", S("_", fontSize=14, fontName="Helvetica-Bold", leading=18, textColor=cl_c)), + Paragraph("Low = best\nHigh = penalty", S("_", fontSize=8, fontName="Helvetica", leading=12, textColor=MUTED)), + Paragraph("Processing effort required to decode the creative. Based on object count, text density, and visual complexity. High load reduces all other signals.", S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=MUTED)), + Paragraph( + ("Low cognitive demand — brain can process the message in under 1.5 seconds." if cl == "Low" + else "Moderate effort required — will work in lean-back formats but may struggle in fast feeds." if cl == "Medium" + else "High cognitive demand — viewers will abandon processing before the message registers."), + S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=WHITE) + ), + ]) + + col_ws = [28*mm, 22*mm, 26*mm, 48*mm, None] + sig_t = Table(sig_table_data, colWidths=col_ws, repeatRows=1) + sig_t.setStyle(TableStyle([ + ("BACKGROUND", (0,0),(-1,0), colors.HexColor("#111827")), + ("BACKGROUND", (0,1),(-1,-1), CARD), + ("BOX", (0,0),(-1,-1), 0.5, BORDER), + ("INNERGRID", (0,0),(-1,-1), 0.3, BORDER), + ("LEFTPADDING", (0,0),(-1,-1), 8), + ("RIGHTPADDING", (0,0),(-1,-1), 8), + ("TOPPADDING", (0,0),(-1,-1), 8), + ("BOTTOMPADDING",(0,0),(-1,-1), 8), + ("VALIGN", (0,0),(-1,-1), "TOP"), + ])) + story.append(sig_t) + story.append(Spacer(1, 14)) + story.append(rule()) + + # ── Visual Feature Detection ─────────────────────────────────────────────── + story.append(section_label("Visual Feature Detection")) + story.append(Paragraph( + "Raw computer-vision measurements from the creative. These values directly feed the signal scores above.", + sMuted, + )) + story.append(Spacer(1, 8)) + + def feat_explanation(key, value): + exps = { + "face": ("Faces trigger the fusiform face area — the brain's fastest biological attention mechanism. " + f"{'1 face present — orienting response will activate.' if value > 0 else 'No face detected — missing the highest-ROI attention driver.'}"), + "contrast": (f"Contrast {value:.0f}/100. " + + ("Passes the visual salience gate — will stand out in a competitive feed." if value >= 60 + else "Below the 60/100 salience threshold — will blend into feed backgrounds.")), + "objects": (f"{value} objects detected. " + + ("Clean composition — full attentional focus on primary element." if value <= 3 + else "Moderate clutter — small attention penalty." if value <= 6 + else f"High clutter — attention fragments across {value} elements, suppressing all signals.")), + "text": (f"Text covers {value*100:.0f}% of the creative area. " + + ("No verbal anchor — memory recall will rely on visual alone." if value < 0.04 + else "Optimal text balance — verbal and visual channels both active." if value <= 0.20 + else "Text-heavy — cognitive load increases and visual processing is suppressed.")), + } + return exps.get(key, "") + + vf_data = [ + [Paragraph("FEATURE", hdr_style), Paragraph("DETECTED VALUE", hdr_style), + Paragraph("THRESHOLD", hdr_style), Paragraph("INTERPRETATION", hdr_style)], + [Paragraph("Human Faces", S("_", fontSize=9, fontName="Helvetica-Bold", leading=13, textColor=WHITE)), + Paragraph(f"{data['face_count']} face(s)", S("_", fontSize=11, fontName="Helvetica-Bold", leading=15, textColor=GREEN if data['face_count'] > 0 else RED)), + Paragraph("≥ 1 = strong\n0 = penalty", S("_", fontSize=8, fontName="Helvetica", leading=12, textColor=MUTED)), + Paragraph(feat_explanation("face", data["face_count"]), S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=MUTED))], + [Paragraph("Contrast Score", S("_", fontSize=9, fontName="Helvetica-Bold", leading=13, textColor=WHITE)), + Paragraph(f"{data['contrast_score']:.0f} / 100", S("_", fontSize=11, fontName="Helvetica-Bold", leading=15, textColor=GREEN if data['contrast_score'] >= 60 else RED)), + Paragraph("Good ≥ 60\nWeak < 45", S("_", fontSize=8, fontName="Helvetica", leading=12, textColor=MUTED)), + Paragraph(feat_explanation("contrast", data["contrast_score"]), S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=MUTED))], + [Paragraph("Object Count", S("_", fontSize=9, fontName="Helvetica-Bold", leading=13, textColor=WHITE)), + Paragraph(f"{data['object_count']} objects", S("_", fontSize=11, fontName="Helvetica-Bold", leading=15, textColor=GREEN if data['object_count'] <= 4 else (AMBER if data['object_count'] <= 7 else RED))), + Paragraph("≤ 4 = clean\n> 7 = high load", S("_", fontSize=8, fontName="Helvetica", leading=12, textColor=MUTED)), + Paragraph(feat_explanation("objects", data["object_count"]), S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=MUTED))], + [Paragraph("Text Density", S("_", fontSize=9, fontName="Helvetica-Bold", leading=13, textColor=WHITE)), + Paragraph(f"{data['text_density']*100:.0f}% of area", S("_", fontSize=11, fontName="Helvetica-Bold", leading=15, textColor=AMBER if data['text_density'] > 0.25 else (GREEN if data['text_density'] >= 0.04 else RED))), + Paragraph("4–20% = optimal\n> 25% = overloaded", S("_", fontSize=8, fontName="Helvetica", leading=12, textColor=MUTED)), + Paragraph(feat_explanation("text", data["text_density"]), S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=MUTED))], + ] + if data["dominant_colors"]: + colors_str = " · ".join(data["dominant_colors"][:5]) + vf_data.append([ + Paragraph("Dominant Colours", S("_", fontSize=9, fontName="Helvetica-Bold", leading=13, textColor=WHITE)), + Paragraph(colors_str, S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=MUTED)), + Paragraph("Warm = positive\nCool/dark = risk", S("_", fontSize=8, fontName="Helvetica", leading=12, textColor=MUTED)), + Paragraph("Palette drives emotional valence. Warm tones (reds, oranges, yellows) build positive affect. Cool or dark palettes risk neutral-to-negative brand association.", S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=MUTED)), + ]) + + vf_col_ws = [32*mm, 28*mm, 26*mm, None] + vf_t = Table(vf_data, colWidths=vf_col_ws, repeatRows=1) + vf_t.setStyle(TableStyle([ + ("BACKGROUND", (0,0),(-1,0), colors.HexColor("#111827")), + ("BACKGROUND", (0,1),(-1,-1), CARD), + ("BOX", (0,0),(-1,-1), 0.5, BORDER), + ("INNERGRID", (0,0),(-1,-1), 0.3, BORDER), + ("LEFTPADDING", (0,0),(-1,-1), 8), + ("RIGHTPADDING", (0,0),(-1,-1), 8), + ("TOPPADDING", (0,0),(-1,-1), 8), + ("BOTTOMPADDING",(0,0),(-1,-1), 8), + ("VALIGN", (0,0),(-1,-1), "TOP"), + ])) + story.append(vf_t) + + story.append(Spacer(1, 8)) + story.append(rule()) + story.append(Paragraph(FOOTER_TEXT, sFoot)) + + # ══════════════════════════════════════════════════════════════════════════ + # PAGE 3 — Strategic Analysis & Recommendations + # ══════════════════════════════════════════════════════════════════════════ + story.append(PageBreak()) + + story.append(Paragraph("COGNITIVE SIGNAL ANALYSIS REPORT", sEye)) + story.append(thick_rule(BLUE)) + story.append(Spacer(1, 6)) + + # ── Creative Brief ───────────────────────────────────────────────────────── + story.append(section_label("Creative Brief — Diagnosis & Scale Decision")) + story.append(Paragraph( + "A media-ready brief for creative and planning teams. Every line references a detected value.", + sMuted, + )) + story.append(Spacer(1, 8)) + + brief_rows = [ + ("CPCi Score", f"{cpci} / 100"), + ("Performance", data["perf_label"]), + ("Scale Decision", data["scale_label"]), + ("Use Case", data["use_case"]), + ("Attention", f"{data['attn']} / 100 — {data['attn_label']}"), + ("Memory", f"{data['mem']} / 100 — {data['mem_label']}"), + ("Emotional Valence",f"{data['val']:+.2f} — {data['val_label']}"), + ("Cognitive Load", f"{data['cl']} ({data['cl_score']:.0f} / 100)"), + ("Faces Detected", str(data["face_count"])), + ("Contrast Score", f"{data['contrast_score']:.0f} / 100"), + ("Object Count", str(data["object_count"])), + ("Text Density", f"{data['text_density']*100:.0f}%"), + ] + + b_rows = [] + for k, v in brief_rows: + b_rows.append([ + Paragraph(k, S("bk", fontSize=9, fontName="Helvetica-Bold", leading=13, textColor=LABEL)), + Paragraph(v, S("bv", fontSize=9, fontName="Helvetica", leading=13, textColor=WHITE)), + ]) + + brief_t = Table(b_rows, colWidths=[55*mm, None]) + brief_t.setStyle(TableStyle([ + ("BACKGROUND", (0,0),(-1,-1), CARD), + ("BOX", (0,0),(-1,-1), 0.5, BORDER), + ("INNERGRID", (0,0),(-1,-1), 0.3, BORDER), + ("LEFTPADDING", (0,0),(-1,-1), 10), + ("RIGHTPADDING", (0,0),(-1,-1), 10), + ("TOPPADDING", (0,0),(-1,-1), 7), + ("BOTTOMPADDING",(0,0),(-1,-1), 7), + ("VALIGN", (0,0),(-1,-1), "MIDDLE"), + ])) + story.append(brief_t) + story.append(Spacer(1, 14)) + story.append(rule()) + + # ── Strategic Implication ───────────────────────────────────────────────── + if data.get("strategic_implication"): + story.append(section_label("Strategic Implication")) + story.append(Paragraph(data["strategic_implication"], sBody)) + + # Cold audience deployment risk — dynamic, memory-driven + _si_mem = data["mem"] + if _si_mem < 55: + _risk_label = "HIGH" + _risk_hex = "#EF4444" + elif _si_mem < 70: + _risk_label = "MODERATE" + _risk_hex = "#F59E0B" + else: + _risk_label = "LOW" + _risk_hex = "#22C55E" + + story.append(Spacer(1, 10)) + story.append(Paragraph( + f"COLD AUDIENCE DEPLOYMENT RISK: {_risk_label}", + S("deployrisk", fontSize=11, fontName="Helvetica-Bold", leading=15, + textColor=colors.HexColor(_risk_hex), spaceAfter=0), + )) + story.append(Spacer(1, 12)) + story.append(rule()) + + # ── Recommendation ──────────────────────────────────────────────────────── + story.append(section_label("Recommendation — Priority Actions")) + story.append(Paragraph( + "Ranked by expected CPCi impact. Each action references a detected value " + "and targets the weakest cognitive signal first.", + sMuted, + )) + story.append(Spacer(1, 8)) + story.append(Paragraph(data["recommendation"], sBody)) + story.append(Spacer(1, 14)) + story.append(rule()) + + # ── Key Recommendations (Top 3 optimization scenarios) ──────────────────── + story.append(section_label("Key Recommendations — Top 3 Optimization Actions")) + story.append(Paragraph( + "Each recommendation is derived from the CPCi formula. Projected lifts use " + "the real use-case weights — not estimates.", + sMuted, + )) + story.append(Spacer(1, 10)) + + scenarios = data.get("scenarios", []) + if scenarios: + rec_rows = [] + for idx, sc_item in enumerate(scenarios[:3], 1): + lift_hex = "#22C55E" if sc_item["lift"] >= 15 else ("#F59E0B" if sc_item["lift"] >= 8 else "#60A5FA") + from_cpci = sc_item["from_cpci"] + to_cpci = sc_item["to_cpci"] + rec_rows.append([ + Paragraph( + f"0{idx}", + S("recnum", fontSize=16, fontName="Helvetica-Bold", + leading=20, textColor=colors.HexColor(sc_item["sig_color"])), + ), + Paragraph( + f"{sc_item['signal']} · " + f"+{sc_item['lift']} pts
" + f"{sc_item['action']}
" + f"{sc_item['rationale']}", + S("recbody", fontSize=9, fontName="Helvetica", leading=14, textColor=WHITE), + ), + Paragraph( + f"{from_cpci}

" + f"{to_cpci}", + S("reclift", fontSize=12, fontName="Helvetica-Bold", + leading=16, textColor=WHITE, alignment=TA_CENTER), + ), + ]) + + rec_t = Table(rec_rows, colWidths=[12*mm, None, 20*mm]) + rec_t.setStyle(TableStyle([ + ("BACKGROUND", (0,0),(-1,-1), CARD), + ("BOX", (0,0),(-1,-1), 0.5, BORDER), + ("INNERGRID", (0,0),(-1,-1), 0.3, BORDER), + ("LEFTPADDING", (0,0),(-1,-1), 10), + ("RIGHTPADDING", (0,0),(-1,-1), 10), + ("TOPPADDING", (0,0),(-1,-1), 10), + ("BOTTOMPADDING",(0,0),(-1,-1), 10), + ("VALIGN", (0,0),(-1,-1), "TOP"), + ("ALIGN", (2,0),(-1,-1), "CENTER"), + ])) + story.append(rec_t) + else: + story.append(Paragraph( + "All cognitive signals are above threshold — no single optimization is likely " + "to produce meaningful additional lift. This creative is ready to scale.", + S("recnone", fontSize=11, fontName="Helvetica", leading=16, textColor=GREEN), + )) + + story.append(Spacer(1, 14)) + story.append(rule()) + + # ── CPCi Formula ───────────────────────────────────────────────────────── + story.append(section_label("How CPCi Is Calculated")) + story.append(Paragraph( + "CPCi is a weighted composite score. Weights are calibrated per use-case to reflect " + "the cognitive priorities of each media environment.", + sMuted, + )) + story.append(Spacer(1, 6)) + + formula_rows = [ + [Paragraph("COMPONENT", hdr_style), Paragraph("WEIGHT", hdr_style), + Paragraph("THIS CREATIVE", hdr_style), Paragraph("CONTRIBUTION", hdr_style)], + [Paragraph("Attention Score", S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=WHITE)), + Paragraph(f"{data['w_attn']}%", S("_", fontSize=9, fontName="Helvetica-Bold", leading=13, textColor=MUTED)), + Paragraph(str(data["attn"]), S("_", fontSize=9, fontName="Helvetica-Bold", leading=13, textColor=a_c)), + Paragraph(f"{data['attn'] * data['w_attn'] / 100:.1f} pts", S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=WHITE))], + [Paragraph("Memory Encoding", S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=WHITE)), + Paragraph(f"{data['w_mem']}%", S("_", fontSize=9, fontName="Helvetica-Bold", leading=13, textColor=MUTED)), + Paragraph(str(data["mem"]), S("_", fontSize=9, fontName="Helvetica-Bold", leading=13, textColor=m_c)), + Paragraph(f"{data['mem'] * data['w_mem'] / 100:.1f} pts", S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=WHITE))], + [Paragraph("Emotional Valence", S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=WHITE)), + Paragraph(f"{data['w_emo']}%", S("_", fontSize=9, fontName="Helvetica-Bold", leading=13, textColor=MUTED)), + Paragraph(f"{data['val']:+.2f}", S("_", fontSize=9, fontName="Helvetica-Bold", leading=13, textColor=v_c)), + Paragraph(f"{((data['val']+1)/2*100) * data['w_emo'] / 100:.1f} pts", S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=WHITE))], + [Paragraph("Cognitive Load Penalty", S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=WHITE)), + Paragraph("Applied if High", S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=MUTED)), + Paragraph(data["cl"], S("_", fontSize=9, fontName="Helvetica-Bold", leading=13, textColor=cl_c)), + Paragraph("−10 pts" if data["cl"] == "High" else "0 pts", S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=RED if data["cl"] == "High" else MUTED))], + [Paragraph("CPCi TOTAL", S("_", fontSize=9, fontName="Helvetica-Bold", leading=13, textColor=WHITE)), + Paragraph("", S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=WHITE)), + Paragraph("", S("_", fontSize=9, fontName="Helvetica", leading=13, textColor=WHITE)), + Paragraph(f"{cpci}", S("_", fontSize=14, fontName="Helvetica-Bold", leading=18, textColor=sc))], + ] + + form_t = Table(formula_rows, colWidths=[55*mm, 25*mm, 35*mm, None], repeatRows=1) + form_t.setStyle(TableStyle([ + ("BACKGROUND", (0,0),(-1,0), colors.HexColor("#111827")), + ("BACKGROUND", (0,1),(-1,-2), CARD), + ("BACKGROUND", (0,-1),(-1,-1),colors.HexColor("#111827")), + ("BOX", (0,0),(-1,-1), 0.5, BORDER), + ("INNERGRID", (0,0),(-1,-1), 0.3, BORDER), + ("LEFTPADDING", (0,0),(-1,-1), 8), + ("RIGHTPADDING", (0,0),(-1,-1), 8), + ("TOPPADDING", (0,0),(-1,-1), 8), + ("BOTTOMPADDING",(0,0),(-1,-1), 8), + ("VALIGN", (0,0),(-1,-1), "MIDDLE"), + ])) + story.append(form_t) + story.append(Spacer(1, 16)) + + # ── Methodology note ────────────────────────────────────────────────────── + story.append(rule()) + story.append(Paragraph( + "Methodology: Cognitive Signal Engine™ integrates computer vision (OpenCV), OCR (Tesseract), " + "Cognitive Load Theory (Sweller, 1988), Dual-Coding Theory (Paivio, 1971), and colour psychology. " + "Developed by Anil Pandit. All scores are deterministic — identical inputs produce identical outputs.", + sNote, + )) + story.append(Spacer(1, 8)) + story.append(rule()) + story.append(Paragraph(FOOTER_TEXT, sFoot)) + + # ── Render with dark background ─────────────────────────────────────────── + def on_page(canvas, doc): + canvas.saveState() + canvas.setFillColor(BG) + canvas.rect(0, 0, W, H, fill=1, stroke=0) + canvas.restoreState() + + doc.build(story, onFirstPage=on_page, onLaterPages=on_page) + return buf.getvalue() + + +def _generate_linkedin_text(data: dict) -> str: + """ + LinkedIn-ready shareable summary. 3-line insight + CPCi + recommendation. + Designed to be punchy, viral-friendly, and attribution-stamped. + """ + cpci = data["cpci"] + perf = data["perf_label"] + attn = data["attn"] + mem = data["mem"] + cl = data["cl"] + val = data["val"] + scens = data.get("scenarios", []) + + # ── Tier-specific hook ──────────────────────────────────────────────────── + if cpci >= 70: + hook = ( + f"🧠 We ran a brain-signal test on an ad creative before spending a penny on media.\n\n" + f"CPCi score: {cpci}/100 — {perf}.\n\n" + f"The neuroscience said: this one is ready to scale." + ) + cta = ( + "Most media waste happens before the first impression is served.\n" + "CPCi catches it at the creative stage — not on the post-campaign report." + ) + elif cpci >= 40: + best_lift = scens[0]["lift"] if scens else None + lift_line = ( + f"One fix could lift CPCi by +{best_lift} points.\n" + f"That's the difference between average and top-quartile performance." + ) if best_lift else ( + "There's a gap. The signals show exactly where." + ) + hook = ( + f"🧠 We brain-scored an ad creative before committing media budget.\n\n" + f"CPCi: {cpci}/100 — {perf}. There's signal. But there's a gap.\n\n" + f"{lift_line}" + ) + cta = ( + "Pre-bid creative scoring means you optimise before you spend.\n" + "Not after." + ) + else: + hook = ( + f"🧠 We almost spent media budget on a creative the brain wouldn't process.\n\n" + f"CPCi: {cpci}/100 — {perf}.\n\n" + f"At that score, most impressions won't cognitively register. " + f"The message won't be encoded. The brand won't be recalled." + ) + cta = ( + "We caught it before a single impression was served.\n" + "That's the whole point of pre-bid creative intelligence." + ) + + # ── 3-line signal insights ──────────────────────────────────────────────── + a_insight = ( + "Attention is strong — this creative will stop the scroll." + if attn > 60 else + "Attention is moderate — won't reliably interrupt cold-audience feeds." + if attn >= 30 else + "Attention is weak — the brain won't pause for this creative." + ) + m_insight = ( + "Memory encoding is high — one exposure is enough to build brand recall." + if mem > 70 else + "Memory encoding is moderate — needs frequency to build durable recall." + if mem >= 40 else + "Memory encoding is low — the brand won't survive a single-exposure feed." + ) + load_insight = ( + "Cognitive load is low — the message reaches the brain cleanly in <1.5s." + if cl == "Low" else + "Cognitive load is medium — will work in lean-back formats, struggles in feed." + if cl == "Medium" else + "Cognitive load is high — viewers abandon processing before the message lands." + ) + + # ── Primary recommendation ──────────────────────────────────────────────── + if scens: + top = scens[0] + rec_line = ( + f"Priority fix: {top['action']}\n" + f"Projected impact: CPCi {top['from_cpci']} → {top['to_cpci']} " + f"(+{top['lift']} pts)" + ) + else: + rec_line = data.get("fix_first", "") + + lines = [ + hook, + "", + "Here's what the 3 cognitive signals revealed:", + f"→ {a_insight}", + f"→ {m_insight}", + f"→ {load_insight}", + "", + rec_line, + "", + cta, + "", + "─" * 40, + "#CPCi #CreativeIntelligence #MediaEfficiency #Neuroscience " + "#AdTech #BrainScience #CognitiveScience #BrandStrategy #MediaPlanning", + "", + "Powered by Cognitive Signal Engine™ — ADVantage Insights", + ] + return "\n".join(lines) + + +def _generate_summary_text(data: dict) -> str: + """Plain-text version of the client report — for clipboard copy.""" + lines = [ + f"COGNITIVE SIGNAL ANALYSIS REPORT", + f"{'─' * 44}", + f"Creative: {data['name']}", + f"Use Case: {data['use_case']}", + f"", + f"CPCi SCORE {data['cpci']} / 100 — {data['perf_label']}", + f"", + f"VERDICT", + f"{data['verdict']}", + f"", + f"WHAT THIS MEANS", + f"01 Performance {data['performance']}", + f"02 Core Issue {data['core_issue']}", + f"03 Fix First {data['fix_first']}", + f"", + f"RECOMMENDATION", + f"{data['recommendation']}", + f"", + f"{'─' * 44}", + f"Cognitive Signal Engine™ · Creative Intelligence Analyzer · © Anil Pandit", + ] + return "\n".join(lines) + + +# ── Demo Mode — pre-built synthetic results ─────────────────────────────────── +# +# Three archetypes that showcase the full scoring range and "Why this wins" +# logic without requiring any file uploads. Used for presentations and demos. +# +_DEMO_RESULTS: list = [ + { + "name": "Creative_A_Hero_Product.jpg", + "file_path": "", + "cpci": 78, + "signals": { + "attention_score": 72, + "memory_score": 68, + "emotional_valence": 0.22, + "cognitive_load": "Low", + "cognitive_load_score": 28, + }, + "visual_features": { + "face_count": 1, + "contrast_score": 74.0, + "object_count": 3, + "text_density": 0.12, + "dominant_colors": ["#E8F4FD", "#2563EB", "#F9FAFB"], + "is_video": False, + "duration": 0, + "fps": 0, + "frame_count": 0, + }, + "reasoning": "", + "narrative": { + "strategic_implication": ( + "Strong hero-product layout — single face, minimal copy, " + "high contrast. Will stop the scroll in cold audiences and " + "encode the brand quickly." + ), + "recommendations": ( + "Test a version with an even tighter crop on the face to push " + "attention scores above 80. Consider adding one short headline " + "to anchor memory." + ), + }, + }, + { + "name": "Creative_B_Lifestyle_Scene.jpg", + "file_path": "", + "cpci": 61, + "signals": { + "attention_score": 58, + "memory_score": 64, + "emotional_valence": 0.14, + "cognitive_load": "Medium", + "cognitive_load_score": 52, + }, + "visual_features": { + "face_count": 2, + "contrast_score": 55.0, + "object_count": 6, + "text_density": 0.18, + "dominant_colors": ["#FEF3C7", "#D97706", "#FFFFFF"], + "is_video": False, + "duration": 0, + "fps": 0, + "frame_count": 0, + }, + "reasoning": "", + "narrative": { + "strategic_implication": ( + "Warm lifestyle scene builds emotional affinity and recall " + "but the composition is slightly busy — the eye doesn't land " + "cleanly on a single focal point." + ), + "recommendations": ( + "Crop tighter to remove background clutter and increase " + "subject contrast. Reducing objects from 6 to 3–4 should " + "lift CPCi by 8–12 points." + ), + }, + }, + { + "name": "Creative_C_Text_Heavy.jpg", + "file_path": "", + "cpci": 34, + "signals": { + "attention_score": 31, + "memory_score": 40, + "emotional_valence": -0.08, + "cognitive_load": "High", + "cognitive_load_score": 81, + }, + "visual_features": { + "face_count": 0, + "contrast_score": 38.0, + "object_count": 11, + "text_density": 0.41, + "dominant_colors": ["#6B7280", "#374151", "#9CA3AF"], + "is_video": False, + "duration": 0, + "fps": 0, + "frame_count": 0, + }, + "reasoning": "", + "narrative": { + "strategic_implication": ( + "Text-heavy layout with no face and low contrast. " + "The visual channel is overloaded — viewers will scroll past " + "before the message registers." + ), + "recommendations": ( + "Rebuild with a single dominant image (preferably a face), " + "reduce copy to one headline under 6 words, and increase " + "background contrast to at least 60/100." + ), + }, + }, +] + + +# ── Demo creative metadata (persona names + descriptions) ───────────────────── +# Keyed by the "name" field in _DEMO_RESULTS +_DEMO_META = { + "Creative_A_Hero_Product.jpg": { + "persona": "Hero Product Ad", + "archetype": "High Performer", + "summary": "Single face, clean layout, high contrast — clears every cognitive gate.", + "facts": ["Face present", "Low cognitive load", "Strong contrast (74/100)"], + "tier_label":"High Efficiency", + "tier_color":"#22C55E", + }, + "Creative_B_Lifestyle_Scene.jpg": { + "persona": "Lifestyle Scene", + "archetype": "Moderate Performer", + "summary": "Warm and emotional, but the busy composition dilutes attention signal.", + "facts": ["2 faces", "Medium load", "6 visual elements"], + "tier_label":"Moderate Performance", + "tier_color":"#F59E0B", + }, + "Creative_C_Text_Heavy.jpg": { + "persona": "Text-Heavy Ad", + "archetype": "Underperformer", + "summary": "No face, high load, low contrast — fails the first cognitive gate.", + "facts": ["No face", "High cognitive load", "Low contrast (38/100)"], + "tier_label":"High Waste Risk", + "tier_color":"#EF4444", + }, +} + +_DEMO_STEPS = [ + ("1", "Select Creative", "Choose a synthetic ad to analyze"), + ("2", "Read the Analysis", "Understand CPCi and what drives it"), + ("3", "See the Opportunity","Optimization scenario + full comparison"), +] + +_GUIDED_BANNERS = { + "step1": ( + "👋 Welcome to the demo", + "Select one of three synthetic ad creatives below. Each represents a real-world " + "scenario: a high performer, a moderate performer, and an underperformer. " + "The analysis will show you exactly why each scores the way it does.", + ), + "before_cpci": ( + "📊 CPCi — Cost Per Cognitive Impression", + "This is the composite score. A score above 70 means the creative is likely to " + "perform in cold audiences. Below 40 signals high waste risk. The number you see " + "is derived from brain-level signals — not opinion.", + ), + "before_signals": ( + "🧠 Four cognitive signals", + "These are the inputs that produce CPCi. Each measures a different brain function: " + "Attention (stopping power), Memory (recall probability), Emotion (brand affinity), " + "and Cognitive Load (processing friction). A weakness in any one suppresses the score.", + ), + "before_business": ( + "💼 Business Impact", + "This section translates cognitive scores into commercial language. Designed for " + "budget holders — it tells you whether to deploy, fix, or kill the creative " + "before any media spend is committed.", + ), + "before_optimization": ( + "🎯 The growth story", + "This is the key output for your creative team. It simulates what one targeted fix " + "is worth in CPCi points — and maps that to a specific action, not a vague suggestion.", + ), + "step3": ( + "📊 Three creatives, one decision", + "This is what the tool looks like in a real A/B review. It tells you which creative " + "to put media behind — and exactly why the winner wins.", + ), +} + + +def _guided_banner(key: str) -> None: + """Renders a guided-mode contextual banner. Only shows when guided_walkthrough is ON.""" + if not st.session_state.get("guided_walkthrough", True): + return + title, body = _GUIDED_BANNERS.get(key, ("", "")) + if not title: + return + st.markdown( + f"
" + f"
{title}
" + f"
{body}
" + f"
", + unsafe_allow_html=True, + ) + + +def _render_step_indicator(current: int) -> None: + """Horizontal 3-step progress bar.""" + parts = [] + for num, label, sub in _DEMO_STEPS: + step_n = int(num) + if step_n < current: + dot_bg, dot_color, txt_color = "#22C55E", "#FFFFFF", "#22C55E" + icon = "✓" + elif step_n == current: + dot_bg, dot_color, txt_color = "#3B82F6", "#FFFFFF", "#FFFFFF" + icon = num + else: + dot_bg, dot_color, txt_color = "#1F2937", "#94A3B8", "#94A3B8" + icon = num + + connector = ( + f"
" + if step_n < 3 else "" + ) + + parts.append( + f"
" + f"
{icon}
" + f"
{label}
" + f"
{sub}
" + f"
" + + connector + ) + + st.markdown( + f"
" + + "".join(parts) + + f"
", + unsafe_allow_html=True, + ) + + +def _render_demo_mode(client_mode: bool) -> None: + """ + Full demo experience with 3 preloaded creatives and optional guided walkthrough. + Replaces the uploader when demo_mode is active. + """ + # ── Init session state ──────────────────────────────────────────────────── + if "demo_step" not in st.session_state: st.session_state["demo_step"] = 1 + if "demo_creative" not in st.session_state: st.session_state["demo_creative"] = 0 + if "guided_walkthrough" not in st.session_state: + st.session_state["guided_walkthrough"] = True + + demo_sorted = sorted(_DEMO_RESULTS, key=lambda x: x["cpci"], reverse=True) + step = st.session_state["demo_step"] + + # ── Demo header ─────────────────────────────────────────────────────────── + hdr_left, hdr_right = st.columns([3, 1]) + with hdr_left: + st.markdown( + "
" + "🎬" + "" + "Demo Mode" + "" + "" + "3 synthetic creatives · Performance Marketing" + "
", + unsafe_allow_html=True, + ) + with hdr_right: + st.toggle( + "Guided Walkthrough", + value=st.session_state.get("guided_walkthrough", True), + key="guided_walkthrough", + help="Turn on for step-by-step explanations of each metric and section.", + ) + + # ── Step indicator ──────────────────────────────────────────────────────── + _render_step_indicator(step) + + # ══════════════════════════════════════════════════════════════════════════ + # STEP 1 — Select a creative + # ══════════════════════════════════════════════════════════════════════════ + if step == 1: + _guided_banner("step1") + + cols = st.columns(3, gap="medium") + for col, r in zip(cols, demo_sorted): + meta = _DEMO_META.get(r["name"], {}) + cpci_v = r["cpci"] + tc = meta.get("tier_color", "#94A3B8") + tl = meta.get("tier_label", "") + persona = meta.get("persona", r["name"]) + archetype= meta.get("archetype", "") + summary = meta.get("summary", "") + facts = meta.get("facts", []) + + facts_html = "".join( + f"
" + f"" + f"{f}" + f"
" + for f in facts + ) + + with col: + st.markdown( + f"
" + + f"
{archetype}
" + + f"
{persona}
" + + f"
" + f"{cpci_v}" + f"/100 CPCi" + f"
" + + f"
{summary}
" + + f"
{facts_html}
" + + f"
" + f"{tl}" + f"
" + + f"
", + unsafe_allow_html=True, + ) + # Spacer then button below the card + st.markdown("
", unsafe_allow_html=True) + idx = demo_sorted.index(r) + if st.button( + f"Analyze this creative →", + key=f"demo_pick_{idx}", + use_container_width=True, + ): + st.session_state["demo_creative"] = idx + st.session_state["demo_step"] = 2 + st.rerun() + + st.markdown("
", unsafe_allow_html=True) + if st.button("Skip to full comparison →", use_container_width=False): + st.session_state["demo_step"] = 3 + st.rerun() + + # ══════════════════════════════════════════════════════════════════════════ + # STEP 2 — Full analysis of selected creative + # ══════════════════════════════════════════════════════════════════════════ + elif step == 2: + r = demo_sorted[st.session_state["demo_creative"]] + meta = _DEMO_META.get(r["name"], {}) + tc = meta.get("tier_color", "#94A3B8") + + # Creative identifier strip + st.markdown( + f"
" + f"" + f"{meta.get('archetype','Creative')}" + f"·" + f"" + f"{meta.get('persona', r['name'])}" + f"" + f"CPCi {r['cpci']} / 100" + f"
", + unsafe_allow_html=True, + ) + + # Guided banners — fire before key sections + _guided_banner("before_cpci") + + # Run full analysis + show_results(r, elapsed=None, use_case="Performance Marketing", + client_mode=client_mode) + + # Navigation row + st.markdown("
", unsafe_allow_html=True) + nav_back, nav_fwd = st.columns([1, 2]) + with nav_back: + if st.button("← Back to creative selection", use_container_width=True): + st.session_state["demo_step"] = 1 + st.rerun() + with nav_fwd: + if st.button( + "Next: Compare all 3 creatives →", + type="primary", + use_container_width=True, + ): + st.session_state["demo_step"] = 3 + st.rerun() + + # ══════════════════════════════════════════════════════════════════════════ + # STEP 3 — Comparison + exit CTA + # ══════════════════════════════════════════════════════════════════════════ + elif step == 3: + _guided_banner("step3") + + show_comparison(demo_sorted, "Performance Marketing", client_mode) + + # Exit CTA + st.markdown("
", unsafe_allow_html=True) + st.markdown( + "
" + "
Ready to analyze your own creative?
" + "
" + "Upload any JPG, PNG, or MP4 — results in under 30 seconds.
" + "
", + unsafe_allow_html=True, + ) + cta_back, cta_exit = st.columns([1, 2]) + with cta_back: + if st.button("← Back to analysis", use_container_width=True): + st.session_state["demo_step"] = 2 + st.rerun() + with cta_exit: + if st.button( + "Upload my own creative →", + type="primary", + use_container_width=True, + ): + # Can't set "demo_mode" directly here — it's bound to a widget key. + # Signal the header guard (runs before the toggle) to clear it on next rerun. + st.session_state["_exit_demo"] = True + st.rerun() + + +def _render_trust_indicators(cpci: float, attn: int, mem: int, conf_level: str, conf_color: str) -> None: + """ + Three trust signals rendered below every result: + 1. How this works (collapsed expander) + 2. Limitations (1-line inline note) + 3. Confidence indicator (dot + label) + """ + st.markdown( + "
", + unsafe_allow_html=True, + ) + + left_col, right_col = st.columns([3, 1], gap="large") + + # ── Left: How this works (collapsed) + Limitations ──────────────────────── + with left_col: + with st.expander("ℹ️ How this works", expanded=False): + st.markdown( + "
" + + "What CPCi measures
" + "CPCi (Cost Per Cognitive Impression) is a composite score produced by the " + "Cognitive Signal Engine™ — an original multi-layer analytical system that models how " + "the human brain processes advertising creative. It draws on principles from " + "neuroscience, behavioural science, and advertising theory to produce three weighted " + "signals: Attention, " + "Memory encoding, and " + "Emotional valence.

" + + "Signal sources
" + "Each signal is derived from measurable visual properties — face presence, " + "contrast ratios, object density, text load, colour palette, and spatial " + "composition — across independent analytical layers. The system does not rely " + "on a single AI model. Theoretical grounding comes from Sweller's Cognitive " + "Load Theory, Paivio's Dual-Coding Theory, and empirical attention research.

" + + "Use case weighting
" + "The formula shifts weights based on your campaign objective — " + "Performance Marketing prioritises attention, Brand campaigns prioritise " + "memory encoding. This changes the final CPCi score without changing " + "the underlying signal readings.

" + + "Origin
" + "Cognitive Signal Engine™ is a proprietary framework developed by " + "Anil Pandit, " + "integrating neuroscience, behavioral science, and advertising theory " + "into a decision system for creative effectiveness. " + "It was built independently using computer vision, signal processing, " + "and cognitive science theory. TRIBE v2 was the originating research context — " + "the Cognitive Signal Engine is the productised analytical layer built on top of it." + + "
", + unsafe_allow_html=True, + ) + + st.markdown( + "
" + "⚠ Limitations  " + "This is a predictive cognitive model based on visual signals, not real user " + "behaviour. Scores are directional — use them to prioritise testing, not to " + "replace it." + "
", + unsafe_allow_html=True, + ) + st.markdown( + "
" + "Methodology note" + " — This system is inspired by advances like Meta's TRIBE v2, but extends " + "them into a practical decision framework for advertising using cognitive signal modeling." + "
", + unsafe_allow_html=True, + ) + + # ── Right: Confidence indicator ─────────────────────────────────────────── + with right_col: + conf_bg = {"High": "#22C55E18", "Medium": "#F59E0B18", "Low": "#EF444418"}.get(conf_level, "#1F2937") + conf_desc = { + "High": "Both attention and memory signals are strong. Score is reliable.", + "Medium": "Signals are mixed. Score is directional — validate with a test.", + "Low": "Signals diverge significantly. Treat this score as indicative only.", + }.get(conf_level, "") + st.markdown( + f"
" + f"
" + f"Model Confidence
" + f"
" + f"" + f"{conf_level}" + f"
" + f"
{conf_desc}
" + f"
", + unsafe_allow_html=True, + ) + + +def _render_cta_block(r: dict, use_case: str) -> None: + """Bottom-of-page CTA — makes the app feel like a product, not a demo.""" + st.markdown( + "
" + "
" + "Cognitive Signal Engine™
" + "
" + "Ready to test your creatives before media spend?
" + "
" + "Stop guessing. Know which creatives earn attention, build memory, " + "and drive response — before you commit budget.
" + "
", + unsafe_allow_html=True, + ) + + # Button row — centred with spacer columns + _, btn_l, btn_r, _ = st.columns([1.5, 1, 1, 1.5]) + + with btn_l: + if st.button( + "🧠 Analyze Your Creative", + use_container_width=True, + type="primary", + key=f"cta_analyze_{r.get('name','default')}", + ): + # Scroll to top / re-trigger upload flow + st.session_state["_cta_new_analysis"] = True + st.rerun() + + with btn_r: + # Reuse the PDF bytes — carry client_name from the export bar input + _cta_client_name = st.session_state.get( + f"pdf_client_name_{r.get('name','default')}", "" + ) + data = _build_report_data(r, use_case, client_name=_cta_client_name) + try: + pdf_bytes = _generate_pdf_bytes(data) + st.download_button( + "📄 Download Report", + pdf_bytes, + file_name=f"CSE_Report_{r.get('name','creative')}.pdf", + mime="application/pdf", + use_container_width=True, + key=f"cta_dl_{r.get('name','default')}", + ) + except Exception: + st.button( + "📄 Download Report", + use_container_width=True, + disabled=True, + key=f"cta_dl_disabled_{r.get('name','default')}", + ) + + st.markdown( + "

" + "ADVantage Insights · Cognitive Signal Engine™ · © Anil Pandit

", + unsafe_allow_html=True, + ) + + +def _render_export_bar(r: dict, use_case: str) -> None: + """Renders the Export Report button row below any result view.""" + st.markdown( + "
" + "Export Report" + "
", + unsafe_allow_html=True, + ) + + client_name = st.text_input( + "Client name (appears on PDF cover page)", + placeholder="e.g. Unilever, Nike, P&G", + key=f"pdf_client_name_{r.get('name','default')}", + label_visibility="visible", + ) + + data = _build_report_data(r, use_case, client_name=client_name) + + pdf_col, txt_col, _ = st.columns([1, 1, 3]) + + with pdf_col: + pdf_bytes = _generate_pdf_bytes(data) + fname = data["name"].rsplit(".", 1)[0] + "_cpci_report.pdf" + st.download_button( + "⬇️ Download PDF", + data = pdf_bytes, + file_name = fname, + mime = "application/pdf", + use_container_width=True, + ) + + with txt_col: + summary = _generate_summary_text(data) + st.download_button( + "📋 Download Summary", + data = summary, + file_name = data["name"].rsplit(".", 1)[0] + "_summary.txt", + mime = "text/plain", + use_container_width=True, + ) + + # ── LinkedIn Share Card (collapsed) ────────────────────────────────────── + with st.expander("🔗 Share on LinkedIn"): + linkedin_text = _generate_linkedin_text(data) + cpci_li = data["cpci"] + li_badge = ("🟢 Strong Performer" if cpci_li >= 70 + else "🟡 Average Performer" if cpci_li >= 40 + else "🔴 Not Ready to Scale") + st.markdown( + f"
" + f"CPCi {cpci_li}/100 · {li_badge} — copy and paste to LinkedIn:
", + unsafe_allow_html=True, + ) + st.code(linkedin_text, language=None) + + +# ── Single-creative renderer ────────────────────────────────────────────────── + +def show_results(r: dict, elapsed: float = None, use_case: str = "Performance Marketing", client_mode: bool = False) -> None: + """Full analysis report for a single creative.""" + if client_mode: + _show_results_client(r, use_case) + return + s = r["signals"] + vf = r["visual_features"] + cpci = round(r["cpci"], 1) + narr = r.get("narrative", {}) + attn = s["attention_score"] + mem = s["memory_score"] + val = s["emotional_valence"] + cl = s["cognitive_load"] + _rsn = r.get("reasoning") or {} + cl_score = ( + _rsn["load"]["composite"] + if isinstance(_rsn, dict) and "load" in _rsn + else s.get("cognitive_load_score", 50) + ) + uc = USE_CASES[use_case] + w = uc["weights"] + + # ── Derived color + label ───────────────────────────────────────────────── + if cpci >= 70: cc, clabel, cstyle = "#22C55E", "Strong Performer", "good" + elif cpci >= 40: cc, clabel, cstyle = "#F59E0B", "Average Performer", "warn" + else: cc, clabel, cstyle = "#EF4444", "Needs Improvement", "bad" + + if attn > 60: a_color, a_label = "#22C55E", "High Attention" + elif attn >= 30: a_color, a_label = "#F59E0B", "Moderate" + else: a_color, a_label = "#EF4444", "Scroll-Past Risk" + + if mem > 70: m_color, m_label = "#22C55E", "Strong Recall" + elif mem >= 40: m_color, m_label = "#F59E0B", "Moderate" + else: m_color, m_label = "#EF4444", "Low Retention" + + if val > 0.1: v_color, v_label = "#22C55E", "Positive" + elif val > -0.1: v_color, v_label = "#F59E0B", "Neutral" + else: v_color, v_label = "#EF4444", "Negative" + + if cl == "Low": cl_color = "#22C55E" + elif cl == "Medium": cl_color = "#F59E0B" + else: cl_color = "#EF4444" + + # ── Confidence level ────────────────────────────────────────────────────── + if attn > 50 and mem > 50: + conf_level, conf_color = "High", "#22C55E" + elif attn < 30 and mem < 30: + conf_level, conf_color = "Low", "#EF4444" + elif abs(attn - mem) > 35 or (attn < 30 or mem < 30): + conf_level, conf_color = "Low", "#EF4444" + else: + conf_level, conf_color = "Medium", "#F59E0B" + + source = narr.get("_source", "rules") + source_icon = "✨ AI · Claude" if source == "claude" else "⚙️ Rule Engine" + + # ── Timer bar (full width) ──────────────────────────────────────────────── + if elapsed: + st.markdown( + f"
" + f"
⚡ Analysis completed in {elapsed:.2f}s
" + f"
{source_icon}
" + f"
", + unsafe_allow_html=True, + ) + + # ── Video metadata badge (shown only for video creatives) ──────────────── + if vf.get("is_video"): + _dur = vf.get("duration", 0) + _fps = vf.get("fps", 0) + _fc = vf.get("frame_count", 0) + _dur_str = f"{int(_dur // 60)}m {int(_dur % 60)}s" if _dur >= 60 else f"{_dur:.1f}s" + st.markdown( + f"
" + f"🎬 Video Creative" + f"Duration: {_dur_str}" + f"FPS: {_fps:.0f}" + f"Frames sampled: 6" + f"CPCi scored on frame samples" + f"
", + unsafe_allow_html=True, + ) + + # ── Creative image — anchors analysis to the actual creative ───────────── + _render_creative_hero(r.get("file_path", ""), r.get("name", ""), is_video=r.get("visual_features", {}).get("is_video", False)) + + # ══════════════════════════════════════════════════════════════════════════ + # TIER 1 — CPCi + Verdict + What This Means (full-width hero) + # ══════════════════════════════════════════════════════════════════════════ + + # ── CPCi hero — full-width standalone, centered ────────────────────────── + # CSS @property syntax:'' rejects floats → always cast to int + _cpci_int = int(round(cpci)) + _uid = f"cp{abs(hash(str(_cpci_int) + use_case)) % 99991}" + st.markdown( + f"" + f"
" + f"
" + f"🧠  CPCi — Cost Per Cognitive Impression  {_TT_CPCI}
" + f"
" + f"
" + f"Predicts cognitive impact before media spend
" + f"
" + f"out of 100  ·  {uc['icon']} {use_case}
" + f"
", + unsafe_allow_html=True, + ) + + # ── Plain-text fallback render (always visible regardless of CSS animation) ── + st.markdown( + f"", + unsafe_allow_html=True, + ) + + # ── Why This Matters — emotional impact hero ────────────────────────────── + _why_this_matters(cpci, attn, mem, val, cl, use_case) + + # ── Fallback: warn if CPCi is genuinely zero (pipeline failure) ─────────── + if cpci == 0 and (attn > 0 or mem > 0): + st.warning( + "⚠️ CPCi shows 0 but signals are non-zero — check signal pipeline. " + f"(Attention={attn}, Memory={mem}, Valence={val:.2f})" + ) + + # ── Signal strip — 4 numbers ────────────────────────────────────────────── + st.markdown( + f"
" + f"
" + f"
{_TT_ATTN}
" + f"
{attn}
" + f"
{a_label}
" + f"
" + f"
" + f"
{_TT_MEM}
" + f"
{mem}
" + f"
{m_label}
" + f"
" + f"
" + f"
{_TT_VAL}
" + f"
{val:+.2f}
" + f"
{v_label}
" + f"
" + f"
" + f"
Cognitive Load
" + f"
{cl}
" + f"
{cl_score:.0f} / 100
" + f"
" + f"
", + unsafe_allow_html=True, + ) + + # ── Final Verdict — standalone section ──────────────────────────────────── + verdict_txt = _final_verdict_text(cpci, attn, mem, val, cl, use_case) + st.markdown( + f"
" + f"
" + f"⚡  Final Verdict
" + f"
{verdict_txt}
" + f"
" + f"
{badge(clabel, cstyle)}
" + f"
" + f" " + f"Confidence: {conf_level}" + f"
" + f"
" + f"
", + unsafe_allow_html=True, + ) + + st.markdown("
", + unsafe_allow_html=True) + + # ── Business Impact — CMO-facing commercial translation ─────────────────── + _business_impact(cpci, attn, mem, val, cl, use_case) + + # ── Creative Optimization Scenario — what one fix is worth ──────────────── + _optimization_scenario(cpci, attn, mem, val, cl, vf, use_case) + + # ── Collapsed details (Brief + Classification) ─────────────────────────── + with st.expander("📋 Creative Brief & Classification"): + _creative_brief(cpci, attn, mem, val, cl, vf, use_case) + _render_classification(cpci, attn, mem, val, cl, vf, use_case) + + st.markdown("
", + unsafe_allow_html=True) + + # ── What This Means — full width, immediately below ─────────────────────── + _quick_read(cpci, attn, mem, val, cl, vf, use_case) + + st.markdown("
", + unsafe_allow_html=True) + + # ── Media Implications — placement fit + strategy ───────────────────────── + _media_implications(cpci, attn, mem, val, cl, vf, use_case) + + # ══════════════════════════════════════════════════════════════════════════ + # TIER 2 — Secondary detail (visually quiet, below a clear separator) + # ══════════════════════════════════════════════════════════════════════════ + st.markdown( + "
" + "" + "Detailed Analysis
", + unsafe_allow_html=True, + ) + + detail_left, detail_right = st.columns([1, 1], gap="large") + + with detail_left: + _section_card( + icon = "📈", + title = "Strategic Implication", + accent= "#3B82F6", + body = narr.get("strategic_implication", ""), + pointers=[ + ("Use case", use_case, "#3B82F6"), + ("CPCi", f"{cpci}/100", cc), + ("Attention", f"{attn}/100", a_color), + ("Memory", f"{mem}/100", m_color), + ], + ) + + with detail_right: + _render_recommendations( + body = narr.get("recommendations", ""), + pointers = [ + ("Priority fix", + "Add face" if vf["face_count"] == 0 + else "Boost contrast" if vf["contrast_score"] < 60 + else "Reduce objects" if vf["object_count"] > 6 + else "Add tagline" if vf["text_density"] < 0.05 + else "Trim copy", + "#F59E0B"), + ("Attention gap", f"{max(0, 60-attn)} pts", a_color), + ("Memory gap", f"{max(0, 70-mem)} pts", m_color), + ("Load", cl, cl_color), + ], + ) + + # Cognitive Diagnosis — full width, most technical, last + _cognitive_diagnosis( + attn=attn, mem=mem, val=val, cl=cl, cl_score=cl_score, + vf=vf, + a_color=a_color, a_label=a_label, + m_color=m_color, m_label=m_label, + v_color=v_color, v_label=v_label, + cl_color=cl_color, + ) + + # Trust indicators — how this works, limitations, confidence + st.markdown("
", unsafe_allow_html=True) + _render_trust_indicators(cpci, attn, mem, conf_level, conf_color) + + # Export report bar + st.markdown("
", unsafe_allow_html=True) + _render_export_bar(r, use_case) + + # ── CTA block ──────────────────────────────────────────────────────────── + _render_cta_block(r, use_case) + + +# ── Multi-creative comparison ───────────────────────────────────────────────── + +import base64 as _b64 + +def _client_insight(narr: dict, cpci: float, attn: int, mem: int, val: float, cl: str, use_case: str) -> str: + """ + One plain-English sentence a client can instantly understand. + Pulled from narrative if available, otherwise generated from signals. + No jargon. No scores. Just what it means for the campaign. + """ + # Try narrative first + si = narr.get("strategic_implication", "") + if si and len(si) > 20: + # Return first sentence only + return si.split(".")[0].strip() + "." + + # Fallback: signal-driven + if cpci >= 70: + if attn >= 65: + return "This creative will stop the scroll and encode the brand — both are rare in the same creative, and this is ready to scale." + return "This creative will drive cognitive engagement efficiently — all signals are above the threshold required for reliable performance." + elif cpci >= 55: + if attn < 45: + return "This creative will build recall among viewers who see it, but will not reliably stop cold audiences — acquisition efficiency is at risk." + if mem < 50: + return "This creative will attract attention but will not be remembered after a single exposure — reach without recall is wasted media." + return "This creative is one signal fix away from scale — the foundation is strong but one dimension is suppressing the composite score." + elif cpci >= 40: + if cl == "High": + return "This creative will lose viewers before the message lands — visual overload is causing the brain to abandon processing before engagement occurs." + return "This creative will not convert efficiently at scale — the cognitive signal profile is too weak to justify full budget deployment." + else: + return "This creative will not perform regardless of budget or targeting — the cognitive barriers require a rebuild, not a boost in spend." + + +def _client_recommendation(narr: dict, cpci: float, attn: int, mem: int, val: float, cl: str, vf: dict) -> str: + """One actionable sentence a client can take to their creative team.""" + rec = narr.get("recommendations", "") + if rec and len(rec) > 20: + return rec.split(".")[0].strip() + "." + + # Fallback + if vf.get("face_count", 0) == 0 and attn < 50: + return "Add a human face as the primary visual — it is the fastest single change to trigger an orienting response and lift emotional valence." + if cl == "High": + return "Remove at least half the visual elements before scaling — working memory saturation is actively blocking every other signal." + if mem < 45: + return "Rebuild around one dominant image and one short line of copy — single-exposure recall requires radical simplicity." + if val < -0.05: + return "Replace the dominant cool tones with warmer equivalents — the palette is generating subconscious avoidance that will compound across impressions." + if attn < 45: + return "Increase the primary subject's size and contrast — the creative needs to clear the visual salience threshold before targeting or spend can help." + return "Test a version with a human face as the hero — it is the highest-probability change for lifting attention, emotion, and recall simultaneously." + + +def _show_results_client(r: dict, use_case: str) -> None: + """Client Mode — single creative. Clean, no technical metrics.""" + s = r["signals"] + narr = r.get("narrative", {}) + cpci = r["cpci"] + attn = s["attention_score"] + mem = s["memory_score"] + val = s["emotional_valence"] + cl = s["cognitive_load"] + vf = r["visual_features"] + + cc = "#22C55E" if cpci >= 70 else ("#F59E0B" if cpci >= 40 else "#EF4444") + verdict = _final_verdict_text(cpci, attn, mem, val, cl, use_case) + insight = _client_insight(narr, cpci, attn, mem, val, cl, use_case) + rec = _client_recommendation(narr, cpci, attn, mem, val, cl, vf) + + # Confidence (same logic as expert mode) + if attn > 50 and mem > 50: + conf_level, conf_color = "High", "#22C55E" + elif attn < 30 and mem < 30: + conf_level, conf_color = "Low", "#EF4444" + elif abs(attn - mem) > 35 or (attn < 30 or mem < 30): + conf_level, conf_color = "Low", "#EF4444" + else: + conf_level, conf_color = "Medium", "#F59E0B" + + # Label + if cpci >= 70: perf_label = "Strong Performer" + elif cpci >= 40: perf_label = "Needs Optimisation" + else: perf_label = "Not Ready to Scale" + + # ── Creative image hero ─────────────────────────────────────────────────── + _render_creative_hero(r.get("file_path", ""), r.get("name", ""), is_video=r.get("visual_features", {}).get("is_video", False)) + + left, right = st.columns([2, 3], gap="large") + + with left: + st.markdown( + f"
" + f"
Creative Score
" + f"
{cpci}
" + f"
" + f"out of 100  ·  {use_case}
" + f"
" + f"
Performance Outlook
" + f"
" + f"{perf_label}
" + f"
", + unsafe_allow_html=True, + ) + + with right: + st.markdown( + f"
" + f"
Verdict
" + f"
{verdict}
" + f"
" + f"
Key Insight
" + f"
{insight}
" + f"
" + f"
Recommendation
" + f"
{rec}
" + f"
", + unsafe_allow_html=True, + ) + + st.markdown("
", + unsafe_allow_html=True) + + # Why This Matters — emotional impact hero before business detail + _why_this_matters(cpci, attn, mem, val, cl, use_case) + + # Business Impact — CMO-facing translation + _business_impact(cpci, attn, mem, val, cl, use_case) + + # Optimization Scenario — turns the diagnosis into a growth story + _optimization_scenario(cpci, attn, mem, val, cl, vf, use_case) + + # ── Collapsed details (Brief + Classification) ─────────────────────────── + with st.expander("📋 Creative Brief & Classification"): + _creative_brief(cpci, attn, mem, val, cl, vf, use_case) + _render_classification(cpci, attn, mem, val, cl, vf, use_case) + + st.markdown("
", + unsafe_allow_html=True) + + # Media Implications + _media_implications(cpci, attn, mem, val, cl, vf, use_case) + + st.markdown("
", + unsafe_allow_html=True) + + # Trust indicators — how this works, limitations, confidence + st.markdown("
", unsafe_allow_html=True) + _render_trust_indicators(cpci, attn, mem, conf_level, conf_color) + + # Export report bar + st.markdown("
", unsafe_allow_html=True) + _render_export_bar(r, use_case) + + # ── CTA block ──────────────────────────────────────────────────────────── + _render_cta_block(r, use_case) + + +def _show_comparison_client(sorted_results: list, use_case: str) -> None: + """Client Mode — comparison. One card per creative, verdict + insight + recommendation only.""" + n = len(sorted_results) + + st.markdown( + "
Cognitive Signal Engine™
" + "
Creative Intelligence Analyzer
" + f"
Client Report · {use_case} · Comparing {n} Creatives
", + unsafe_allow_html=True, + ) + + cols = st.columns(n, gap="medium") + for i, (col, r) in enumerate(zip(cols, sorted_results)): + s = r["signals"] + narr = r.get("narrative", {}) + cpci = r["cpci"] + attn = s["attention_score"] + mem = s["memory_score"] + val = s["emotional_valence"] + cl = s["cognitive_load"] + vf = r["visual_features"] + is_win = (i == 0) + cc = "#22C55E" if cpci >= 70 else ("#F59E0B" if cpci >= 40 else "#EF4444") + card_cls = "ab-card-winner" if is_win else "ab-card" + + verdict = _final_verdict_text(cpci, attn, mem, val, cl, use_case) + insight = _client_insight(narr, cpci, attn, mem, val, cl, use_case) + rec = _client_recommendation(narr, cpci, attn, mem, val, cl, vf) + + img_src = _img_b64(r.get("file_path", "")) + img_html = ( + f"" + if img_src else "" + ) + + rank_html = ( + "
🏆 Recommended
" + if is_win else + f"
Option {i+1}
" + ) + + col.markdown( + f"
" + f"{rank_html}" + f"{img_html}" + f"
{short_name(r['name'], 24)}
" + f"
{cpci}
" + f"
/ 100
" + f"
Verdict
" + f"
{verdict}
" + f"
Key Insight
" + f"
{insight}
" + f"
Recommendation
" + f"
" + f"{rec}
" + f"
", + unsafe_allow_html=True, + ) + + # Why the winner wins (client-friendly language) + if n > 1: + winner = sorted_results[0] + runner_up = sorted_results[1] + why = _why_wins(winner, runner_up, use_case) + st.markdown( + f"
" + f"
Our Recommendation
" + f"
🏆 {winner['name']}
" + f"
{why}
" + f"
", + unsafe_allow_html=True, + ) + + st.markdown( + "

Cognitive Signal Engine™

", + unsafe_allow_html=True, + ) + + +_IMG_EXTS = {"jpg", "jpeg", "png", "gif", "webp"} +_VIDEO_EXTS_B64 = {"mp4", "mov", "avi", "webm", "m4v", "mkv"} + +def _img_b64(file_path: str) -> str: + """Return a base64 data URI for an image file (for inline HTML display). + For video files, uses the saved thumbnail (generated during analysis). + Returns '' if unavailable so callers show a 'No preview' fallback. + """ + if not file_path: + return "" + try: + ext = file_path.rsplit(".", 1)[-1].lower() + # For video files, use the thumbnail PNG saved alongside it + if ext in _VIDEO_EXTS_B64: + thumb = file_path + "_thumb.png" + if os.path.exists(thumb): + with open(thumb, "rb") as f: + data = _b64.b64encode(f.read()).decode() + return f"data:image/png;base64,{data}" + return "" # no thumbnail → show "No preview" placeholder + # Image files — only encode known image types to avoid binary garbage + if ext not in _IMG_EXTS: + return "" + with open(file_path, "rb") as f: + data = _b64.b64encode(f.read()).decode() + mime = {"jpg": "jpeg", "jpeg": "jpeg", "png": "png", + "gif": "gif", "webp": "webp"}.get(ext, "jpeg") + return f"data:image/{mime};base64,{data}" + except Exception: + return "" + + +_HERO_VIDEO_EXTS = {".mp4", ".mov", ".avi", ".webm", ".m4v"} + +def _render_creative_hero(file_path: str, name: str = "", is_video: bool = False) -> None: + """Render the uploaded creative prominently above analysis output.""" + label_html = ( + f"
" + f"{'🎬 Analyzed Video Creative' if is_video else 'Analyzed Creative'}
" + ) + name_html = ( + f"
{name}
" + if name else "" + ) + + # Check by extension if not explicitly flagged + if not is_video: + is_video = os.path.splitext(file_path)[1].lower() in _HERO_VIDEO_EXTS + + if is_video and os.path.exists(file_path): + st.markdown( + f"
{label_html}
", + unsafe_allow_html=True, + ) + # Centre the video player with constrained width + _vid_l, _vid_c, _vid_r = st.columns([1, 4, 1]) + with _vid_c: + with open(file_path, "rb") as vf: + st.video(vf.read()) + if name: + st.caption(name) + return + + img_src = _img_b64(file_path) + if not img_src: + return + st.markdown( + f"
" + f"{label_html}" + f"
" + f"" + f"{name_html}" + f"
" + f"
", + unsafe_allow_html=True, + ) + + +def _why_wins(winner: dict, runner_up: dict, use_case: str) -> str: + """ + Return a tight 2–3 sentence decisive summary of why the winner beats the runner-up. + No hedging. No 'it seems'. Just facts + numbers. + """ + ws = winner["signals"]; rs = runner_up["signals"] + wvf = winner["visual_features"]; rvf = runner_up["visual_features"] + w_cpci = winner["cpci"]; r_cpci = runner_up["cpci"] + gap = round(w_cpci - r_cpci, 1) + + parts = [] + + # --- CPCi gap opener + if gap >= 20: + parts.append( + f"{winner['name']} dominates by {gap} CPCi points — " + f"a margin that translates directly to higher conversion probability in market." + ) + elif gap >= 10: + parts.append( + f"{winner['name']} outperforms by {gap} CPCi points — " + f"a clear, meaningful edge across the cognitive signal stack." + ) + else: + parts.append( + f"{winner['name']} edges ahead by {gap} CPCi points — " + f"a narrow but consistent advantage across multiple signals." + ) + + # --- Strongest signal advantage + attn_gap = ws["attention_score"] - rs["attention_score"] + mem_gap = ws["memory_score"] - rs["memory_score"] + val_gap = round(ws["emotional_valence"] - rs["emotional_valence"], 2) + load_adv = {"Low": 0, "Medium": 1, "High": 2} + load_gap = load_adv.get(rs["cognitive_load"], 1) - load_adv.get(ws["cognitive_load"], 1) + + advantages = sorted( + [("attn", attn_gap), ("mem", mem_gap), ("val", val_gap * 40), ("load", load_gap * 15)], + key=lambda x: x[1], reverse=True, + ) + top_sig, top_gap = advantages[0] + + if top_sig == "attn" and attn_gap > 3: + reason = ( + f"The primary edge is attention (+{attn_gap} pts) — " + ) + if wvf.get("face_count", 0) > rvf.get("face_count", 0): + reason += "the winner's human face triggers involuntary fixation before the viewer consciously decides to look." + elif wvf.get("contrast_score", 50) > rvf.get("contrast_score", 50) + 8: + reason += "higher contrast pops the winner out of feed clutter where the loser blends in." + else: + reason += "the winner's cleaner composition gives the eye a clear landing point — the loser fragments attention across too many elements." + elif top_sig == "mem" and mem_gap > 3: + reason = ( + f"The decisive factor is memory encoding (+{mem_gap} pts) — " + ) + if wvf.get("object_count", 5) < rvf.get("object_count", 5): + reason += f"the winner's simpler layout ({wvf.get('object_count',0)} objects vs {rvf.get('object_count',0)}) encodes cleanly into long-term memory; the loser's clutter gets discarded." + elif 0.05 <= wvf.get("text_density", 0) <= 0.25 and rvf.get("text_density", 0) > 0.25: + reason += "optimal text density gives the brain a verbal anchor; the loser overloads the verbal channel and nothing sticks." + else: + reason += "a simpler visual hierarchy lets the brain commit the message — the loser asks viewers to do too much cognitive work." + elif top_sig == "load" and load_gap > 0: + reason = ( + f"Lower cognitive load is the key differentiator — " + f"the winner processes in milliseconds ({ws['cognitive_load']}), " + f"the loser demands effort ({rs['cognitive_load']}) that scrolling audiences won't give." + ) + elif top_sig == "val" and val_gap > 0.05: + reason = ( + f"Emotional valence (+{val_gap:+.2f}) tips the balance — " + ) + if wvf.get("face_count", 0) > rvf.get("face_count", 0): + reason += "the winner's human face generates affiliative warmth the loser's visuals simply cannot replicate." + else: + reason += "the winner's color palette encodes positive affect subconsciously; the loser's palette creates mild aversion most viewers never consciously notice." + else: + reason = ( + "The winner holds a consistent but slim edge across all signals — " + "no single metric dominates, but small advantages in contrast, simplicity, " + "and color warmth compound into a measurable CPCi lead." + ) + + parts.append(reason) + + # --- Use-case verdict + is_perf = use_case == "Performance Marketing" + is_brand = use_case == "FMCG Branding" + if is_perf: + parts.append( + f"For Performance Marketing, attention is the first conversion gate — " + f"{winner['name']} clears it; {runner_up['name']} risks the scroll-past before a click can happen." + ) + elif is_brand: + parts.append( + f"For Brand Building, memory encoding determines whether this spend compounds over time — " + f"{winner['name']} encodes; {runner_up['name']} fades." + ) + else: + parts.append( + f"Scale {winner['name']} first. Test {runner_up['name']} only after fixing its weakest signal." + ) + + return " ".join(parts) + + +def show_comparison(sorted_results: list, use_case: str = "Performance Marketing", client_mode: bool = False) -> None: + """ + A/B testing decision screen: + 1. Side-by-side creative cards — winner glows green + 2. "Why this wins" decisive summary + 3. Signal bar breakdown + 4. Per-creative score explanations + download + """ + if client_mode: + _show_comparison_client(sorted_results, use_case) + return + winner = sorted_results[0] + n = len(sorted_results) + + # ── Header ──────────────────────────────────────────────────────────────── + st.markdown( + "
Cognitive Signal Engine™
" + "
Creative Intelligence Analyzer
" + f"
Comparing {n} Creatives — {use_case}
", + unsafe_allow_html=True, + ) + + # ── Creative cards ──────────────────────────────────────────────────────── + cols = st.columns(n, gap="small") + for i, (col, r) in enumerate(zip(cols, sorted_results)): + s = r["signals"] + cpci = r["cpci"] + is_win = (i == 0) + cc = "#22C55E" if cpci >= 70 else ("#F59E0B" if cpci >= 40 else "#EF4444") + card_cls = "ab-card-winner" if is_win else "ab-card" + + # Rank label / winner badge + rank_html = ( + "
🏆 Winner
" + if is_win else + f"
#{i+1}
" + ) + + # Signal colors + a_c = "#22C55E" if s["attention_score"] >= 65 else ("#F59E0B" if s["attention_score"] >= 35 else "#EF4444") + m_c = "#22C55E" if s["memory_score"] >= 65 else ("#F59E0B" if s["memory_score"] >= 40 else "#EF4444") + v = s["emotional_valence"] + v_c = "#22C55E" if v > 0.1 else ("#F59E0B" if v > -0.1 else "#EF4444") + cl = s["cognitive_load"] + l_c = "#22C55E" if cl == "Low" else ("#F59E0B" if cl == "Medium" else "#EF4444") + + # Verdict + vdict_color = cc + vdict_txt = _final_verdict_text(cpci, s["attention_score"], s["memory_score"], v, cl, use_case) + + # Image thumbnail + img_src = _img_b64(r.get("file_path", "")) + img_html = ( + f"" + if img_src else + f"
No preview
" + ) + + col.markdown( + f"
" + f"{rank_html}" + f"{img_html}" + f"
{short_name(r['name'], 22)}
" + f"
{cpci}
" + f"
CPCi Score / 100
" + f"
" + f"
Attention
" + f"
{s['attention_score']}
" + f"
Memory
" + f"
{s['memory_score']}
" + f"
Valence
" + f"
{v:+.2f}
" + f"
Load
" + f"
{cl}
" + f"
" + f"
" + f"{vdict_txt}
" + f"
", + unsafe_allow_html=True, + ) + + # ── CPCi score bars ──────────────────────────────────────────────────────── + max_cpci = sorted_results[0]["cpci"] if sorted_results[0]["cpci"] > 0 else 1 + bars_html = "" + for r in sorted_results: + pct = int(r["cpci"] / max_cpci * 100) + bc = "#22C55E" if r["cpci"] >= 70 else ("#F59E0B" if r["cpci"] >= 40 else "#EF4444") + nm = short_name(r["name"], 18) + bars_html += ( + f"
" + f"
{nm}
" + f"
" + f"
" + f"
{r['cpci']}
" + f"
" + ) + + st.markdown( + f"
" + f"
CPCi Score Comparison
" + f"{bars_html}" + f"
", + unsafe_allow_html=True, + ) + + # ── Why this wins ───────────────────────────────────────────────────────── + runner_up = sorted_results[1] if n > 1 else sorted_results[0] + why_body = _why_wins(winner, runner_up, use_case) + + st.markdown( + f"
" + f"
Why this wins
" + f"
🏆 {winner['name']}
" + f"
{why_body}
" + f"
", + unsafe_allow_html=True, + ) + + # ── Per-creative detail ──────────────────────────────────────────────────── + st.markdown("
", unsafe_allow_html=True) + show_score_explanations(sorted_results) + + # ── Trust indicators (use winner's signals for confidence) ──────────────── + w_attn = winner["signals"]["attention_score"] + w_mem = winner["signals"]["memory_score"] + w_cpci = winner["cpci"] + if w_attn > 50 and w_mem > 50: + w_conf_level, w_conf_color = "High", "#22C55E" + elif w_attn < 30 and w_mem < 30: + w_conf_level, w_conf_color = "Low", "#EF4444" + elif abs(w_attn - w_mem) > 35 or (w_attn < 30 or w_mem < 30): + w_conf_level, w_conf_color = "Low", "#EF4444" + else: + w_conf_level, w_conf_color = "Medium", "#F59E0B" + _render_trust_indicators(w_cpci, w_attn, w_mem, w_conf_level, w_conf_color) + + # ── Download ────────────────────────────────────────────────────────────── + st.markdown("
", unsafe_allow_html=True) + export_all = [{k: v for k, v in r.items() if k not in ("reasoning", "file_path")} for r in sorted_results] + st.download_button( + "⬇️ Export Comparison Report", + json.dumps(export_all, indent=2), + "creative_comparison_report.json", + "application/json", + use_container_width=True, + ) + st.markdown( + "

" + "Powered by OpenCV · Tesseract OCR · Cognitive Signal Engine™ · " + "Color Psychology · CLT (Sweller) · Dual-Coding (Paivio)

", + unsafe_allow_html=True, + ) + + +def _final_verdict_text(cpci, attn, mem, val, cl, use_case) -> str: + """Return the verdict string only (used by both show_results and comparison cards).""" + attn_weak = attn < 45; mem_weak = mem < 50 + load_high = cl == "High"; val_neg = val < -0.05 + attn_str = attn >= 65; mem_str = mem >= 65 + is_perf = use_case == "Performance Marketing" + is_brand = use_case == "FMCG Branding" + if cpci >= 70: + if attn_str and mem_str: return "Ready to scale — strong on every signal." + if attn_str and mem_weak: return "Strong for attention, weak for recall — add retargeting." + if mem_str and attn_weak: return "Powerful recall, weak hook — fix opening frame." + if load_high: return "High scores, high clutter — simplify before scaling." + return "Ready to scale — above threshold on all signals." + elif cpci >= 55: + if attn_weak and not mem_weak: return "Strong recall, weak acquisition — not cold-audience ready." + if mem_weak and not attn_weak: return "Stops the scroll but won't be remembered." + if load_high: return "High potential — strip cognitive load first." + if val_neg and is_brand: return "Brand risk — emotional tone must improve before spend." + return "High potential — optimize attention before scaling." + elif cpci >= 40: + if attn_weak and mem_weak: return "NOT ready for scale." + if load_high and attn_weak: return "Too cluttered to convert — rebuild." + if val_neg: return "Negative signal — will hurt brand at scale." + if is_perf and mem_weak: return "Will not convert — memory encoding too weak." + return "Borderline — fix one signal before scaling." + else: + if val_neg and load_high: return "Do not run — will damage brand perception." + if attn < 25: return "NOT ready for scale — will be ignored." + if mem < 30: return "Forgettable at any spend level — rebuild." + return "NOT ready for scale." + + +MEDALS = ["🥇", "🥈", "🥉", "④", "⑤"] + +def show_comparison_table(results: list) -> None: + """ + Section 1: Comparison table — one row per creative, all key metrics side by side. + Sorted in upload order (not ranked — ranking is Section 2). + """ + st.markdown("
", unsafe_allow_html=True) + st.markdown("
📊 Side-by-Side Comparison
", unsafe_allow_html=True) + + # Header row + cols = st.columns([2.5, 1, 1, 1, 1.2, 1.2]) + headers = ["Creative", "Attention", "Memory", "Valence", "Cog. Load", "CPCi (Cognitive Impact)"] + for col, h in zip(cols, headers): + col.markdown(f"
{h}
", unsafe_allow_html=True) + + # Data rows + for i, r in enumerate(results): + s = r["signals"] + bg = "#141B24" if i % 2 == 0 else "#141B24" + cols = st.columns([2.5, 1, 1, 1, 1.2, 1.2]) + + # Creative name + color palette + swatch_html = color_swatches(r["visual_features"]["dominant_colors"]) + cols[0].markdown( + f"
" + f"{short_name(r['name'])}
" + f"{swatch_html}
", + unsafe_allow_html=True, + ) + + # Attention + ac = "#22C55E" if s["attention_score"] >= 70 else ("#F59E0B" if s["attention_score"] >= 40 else "#EF4444") + cols[1].markdown( + f"
" + f"{s['attention_score']}
", unsafe_allow_html=True, + ) + + # Memory + mc = "#22C55E" if s["memory_score"] >= 70 else ("#F59E0B" if s["memory_score"] >= 40 else "#EF4444") + cols[2].markdown( + f"
" + f"{s['memory_score']}
", unsafe_allow_html=True, + ) + + # Valence + val = s["emotional_valence"] + vc = "#22C55E" if val > 0.1 else ("#F59E0B" if val > -0.1 else "#EF4444") + cols[3].markdown( + f"
" + f"{val:+.2f}
", unsafe_allow_html=True, + ) + + # Cognitive load + cl = s["cognitive_load"] + cl_c = "#22C55E" if cl == "Low" else ("#F59E0B" if cl == "Medium" else "#EF4444") + cols[4].markdown( + f"
" + f"{cl}
", unsafe_allow_html=True, + ) + + # CPCi + cc = cpci_color(r["cpci"]) + cols[5].markdown( + f"
" + f"{r['cpci']}
", unsafe_allow_html=True, + ) + + +def show_ranking(sorted_results: list) -> None: + """ + Section 2: Ranked list sorted by CPCi descending. + Winner gets gold highlight + medal. Progress bars show relative CPCi. + """ + st.markdown("
", unsafe_allow_html=True) + st.markdown("
🏆 Performance Ranking
", unsafe_allow_html=True) + + max_cpci = sorted_results[0]["cpci"] if sorted_results else 100 + + for i, r in enumerate(sorted_results): + medal = MEDALS[i] if i < len(MEDALS) else f"#{i+1}" + card_class = "rank-card rank-winner" if i == 0 else ("rank-card rank-second" if i == 1 else "rank-card") + cc = cpci_color(r["cpci"]) + s = r["signals"] + + st.markdown(f""" +
+
+
+ {medal} + {r['name']} + {"Winner" if i == 0 else ""} +
+
{r['cpci']}/100
+
+
+ 🎯 Attention: {s['attention_score']} + 🧠 Memory: {s['memory_score']} + ❤️ Valence: {s['emotional_valence']:+.2f} + ⚙️ Load: {s['cognitive_load']} +
+
""", unsafe_allow_html=True) + + # Progress bar showing CPCi relative to the winner + bar_pct = r["cpci"] / max_cpci if max_cpci > 0 else 0 + st.progress(bar_pct) + + +def show_winner_explanation(sorted_results: list) -> None: + """ + Section 3: Explain why the top creative beats each competitor, metric by metric. + Identifies specific weaknesses in each losing creative. + """ + st.markdown("
", unsafe_allow_html=True) + st.markdown("
🧬 Why the Winner Wins
", unsafe_allow_html=True) + + winner = sorted_results[0] + losers = sorted_results[1:] + ws = winner["signals"] + wvf = winner["visual_features"] + w_attn = ws["attention_score"] + w_mem = ws["memory_score"] + w_val = ws["emotional_valence"] + w_cl = ws["cognitive_load"] + w_cpci = winner["cpci"] + + load_rank = {"Low": 0, "Medium": 1, "High": 2} + + # Winner summary box + st.markdown(f""" +
+
+ 🥇 Winner +
+
{winner['name']}
+
+ CPCi {w_cpci} — + Attention {w_attn} · + Memory {w_mem} · + Valence {w_val:+.2f} · + Load {w_cl} +
+
+ This creative wins because it delivers the highest cognitive impact (CPCi) — + meaning it is most likely to convert attention into memory and action. +
+
""", unsafe_allow_html=True) + + # Per-competitor breakdown + for loser in losers: + ls = loser["signals"] + lvf = loser["visual_features"] + l_attn = ls["attention_score"] + l_mem = ls["memory_score"] + l_val = ls["emotional_valence"] + l_cl = ls["cognitive_load"] + l_cpci = loser["cpci"] + cpci_gap = round(w_cpci - l_cpci, 1) + + # Build advantage list + advantages = [] + weaknesses = [] + + if w_attn > l_attn + 5: + diff = w_attn - l_attn + advantages.append( + f"+{diff} pts Attention ({w_attn} vs {l_attn}) — " + + ("Winner has faces creating involuntary fixation. " if wvf["face_count"] > lvf["face_count"] else "") + + (f"Winner contrast {wvf['contrast_score']:.0f} vs {lvf['contrast_score']:.0f} — " + f"higher contrast stops the scroll. " if wvf["contrast_score"] > lvf["contrast_score"] + 10 else "") + + (f"Loser has {lvf['object_count']} objects vs {wvf['object_count']} — clutter fragments attention." if lvf["object_count"] > wvf["object_count"] + 2 else "") + ) + elif l_attn > w_attn + 5: + weaknesses.append(f"Loser has +{l_attn - w_attn} pts higher attention ({l_attn} vs {w_attn}) but weaker on other metrics.") + + if w_mem > l_mem + 5: + diff = w_mem - l_mem + advantages.append( + f"+{diff} pts Memory Encoding ({w_mem} vs {l_mem}) — " + + (f"Winner has {wvf['object_count']} objects vs {lvf['object_count']} — simpler composition encodes more cleanly. " if wvf["object_count"] < lvf["object_count"] else "") + + ("Winner text density is in optimal 5–25% range. " if 0.05 <= wvf["text_density"] <= 0.25 and not (0.05 <= lvf["text_density"] <= 0.25) else "") + + (f"Loser text density {lvf['text_density']*100:.0f}% overloads the verbal channel." if lvf["text_density"] > 0.25 else "") + ) + + if w_val > l_val + 0.1: + diff = round(w_val - l_val, 2) + advantages.append( + f"+{diff} Emotional Valence ({w_val:+.2f} vs {l_val:+.2f}) — " + + ("Winner has warmer color palette driving positive affect. " if w_val > 0 else "") + + ("Loser colors are dark/desaturated, encoding mild aversion. " if l_val < -0.05 else "") + + (f"Winner has {wvf['face_count']} face(s) adding affiliative warmth. " if wvf["face_count"] > lvf["face_count"] else "") + ) + + if load_rank[w_cl] < load_rank[l_cl]: + advantages.append( + f"Lower Cognitive Load ({w_cl} vs {l_cl}) — " + f"winner processes faster in scroll environments. " + f"Loser has {lvf['object_count']} objects + {lvf['text_density']*100:.0f}% text " + f"saturating working memory capacity." + ) + + # If winner leads on everything, add a clean sweep note + if not advantages: + advantages.append( + f"Narrow but consistent margin — winner outperforms by {cpci_gap} CPCi points " + f"across a combination of small improvements in contrast, simplicity, and color warmth." + ) + + adv_html = "".join(f"
  • {a}
  • " for a in advantages) + weak_html = ( + "
    Note: " + " ".join(weaknesses) + "" + if weaknesses else "" + ) + + st.markdown(f""" +
    + Winner vs {loser['name']} + + Loser CPCi: {l_cpci}  |  Gap: -{cpci_gap} +
    +
      + {adv_html} +
    + {weak_html} +
    """, unsafe_allow_html=True) + + # Actionable summary + st.markdown("
    ", unsafe_allow_html=True) + export_all = [ + {k: v for k, v in r.items() if k != "reasoning"} + for r in sorted_results + ] + st.download_button( + "⬇️ Export Comparison Report", + json.dumps(export_all, indent=2), + "creative_comparison_report.json", + "application/json", + use_container_width=True, + ) + st.markdown( + "

    " + "Powered by OpenCV · Tesseract OCR · Cognitive Signal Engine™ · " + "Color Psychology · Cognitive Load Theory (Sweller) · Dual-Coding Theory (Paivio)" + "

    ", + unsafe_allow_html=True, + ) + + +# ── Per-creative score explanation ─────────────────────────────────────────── + +def _hex_to_hsv(hex_color: str): + """Parse a hex color string to HSV tuple. Returns (h_degrees, s, v).""" + hex_color = hex_color.lstrip("#") + r, g, b = (int(hex_color[i:i+2], 16) / 255.0 for i in (0, 2, 4)) + h, s, v = colorsys.rgb_to_hsv(r, g, b) + return h * 360, s, v + + +def generate_score_explanation(r: dict) -> dict: + """ + Generate rule-based bullet point explanations for each metric score. + + Every bullet references a REAL detected value — no generic copy. + Returns a dict with keys: "attention", "memory", "valence", "load", + each mapping to a list of (icon, text) tuples. + + Icons: ✅ positive driver ⚠️ negative driver ➡️ neutral/moderate + """ + s = r["signals"] + vf = r["visual_features"] + obj_count = vf["object_count"] + face_count = vf["face_count"] + contrast = vf["contrast_score"] + text_dens = vf["text_density"] + colors = vf["dominant_colors"] + cl = s["cognitive_load"] + reasoning = r.get("reasoning") or {} + cl_score = ( + reasoning["load"]["composite"] + if isinstance(reasoning, dict) and "load" in reasoning + else s.get("cognitive_load_score", 50) + ) + + bullets = {"attention": [], "memory": [], "valence": [], "load": []} + + # ── ATTENTION ───────────────────────────────────────────────────────────── + + # Object clutter + if obj_count > 7: + penalty = min(30, (obj_count - 3) * 2) + bullets["attention"].append( + ("⚠️", f"{obj_count} objects detected — high visual clutter fragments attention " + f"(clutter penalty: -{penalty} pts; threshold is >7 objects)") + ) + elif obj_count <= 3: + bullets["attention"].append( + ("✅", f"{obj_count} object(s) — clean composition, full attentional focus on primary element") + ) + else: + bullets["attention"].append( + ("➡️", f"{obj_count} objects — moderate clutter, small attention penalty applied") + ) + + # Face presence + if face_count > 0: + boost = 22 if face_count == 1 else (36 if face_count == 2 else 44) + bullets["attention"].append( + ("✅", f"{face_count} human face(s) detected — faces trigger involuntary fixation " + f"via the fusiform face area (+{boost} pts attention boost)") + ) + else: + bullets["attention"].append( + ("⚠️", "No faces detected — missing the strongest natural attention trigger in ad creative. " + "Adding one face can add +22 pts to attention score") + ) + + # Contrast + if contrast < 40: + bullets["attention"].append( + ("⚠️", f"Contrast score {contrast:.0f}/100 — low contrast means the creative blends " + f"into the feed; pre-attentive pop is weak") + ) + elif contrast >= 70: + bullets["attention"].append( + ("✅", f"Contrast score {contrast:.0f}/100 — high contrast creates a pre-attentive " + f"visual pop that stops scrolling before conscious processing") + ) + else: + bullets["attention"].append( + ("➡️", f"Contrast score {contrast:.0f}/100 — adequate but not scroll-stopping; " + f"increasing to 70+ can meaningfully improve attention") + ) + + # ── MEMORY ──────────────────────────────────────────────────────────────── + + # Simplicity + if obj_count <= 4: + bullets["memory"].append( + ("✅", f"{obj_count} object(s) — simple composition gives the brain a clear primary " + f"element to bind into long-term memory (picture superiority effect)") + ) + elif obj_count > 7: + bullets["memory"].append( + ("⚠️", f"{obj_count} objects — too many competing elements fragment the hippocampal " + f"binding event; each extra object above 2 costs ~6 memory points") + ) + else: + bullets["memory"].append( + ("➡️", f"{obj_count} objects — acceptable complexity, but simplifying to ≤4 " + f"would improve memory encoding") + ) + + # Text density + td_pct = text_dens * 100 + if 0.05 <= text_dens <= 0.25: + bullets["memory"].append( + ("✅", f"Text coverage {td_pct:.1f}% — optimal zone (5–25%). " + f"Enough text to provide a verbal anchor for brand recall " + f"without overloading the visual channel (dual-coding theory)") + ) + elif text_dens < 0.05: + bullets["memory"].append( + ("⚠️", f"Text coverage {td_pct:.1f}% — too little text. " + f"Without a verbal anchor (tagline, brand name), " + f"memory relies entirely on the visual trace which fades faster") + ) + elif text_dens > 0.30: + bullets["memory"].append( + ("⚠️", f"Text coverage {td_pct:.1f}% — excess copy overwhelms the visual channel. " + f"Above 25% density, memory encoding declines as the brain " + f"shifts from image processing to slow serial text reading") + ) + else: + bullets["memory"].append( + ("➡️", f"Text coverage {td_pct:.1f}% — approaching overload zone (>25%). " + f"Trim copy to stay in the optimal range") + ) + + # ── EMOTIONAL VALENCE ───────────────────────────────────────────────────── + + # Face warmth + if face_count > 0: + face_boost = min(0.30, face_count * 0.25) + bullets["valence"].append( + ("✅", f"{face_count} face(s) — trigger affiliative warmth and emotional mirroring. " + f"Valence boost from faces: +{face_boost:.2f} " + f"(capped at +0.30 to prevent compounding)") + ) + else: + bullets["valence"].append( + ("⚠️", "No faces detected — faces are the strongest single valence driver. " + "Adding a person with visible emotion can shift valence by +0.20 to +0.30") + ) + + # Color temperature analysis + warm_found = [] + dark_found = [] + for hex_c in colors: + try: + h_deg, sat, val_hsv = _hex_to_hsv(hex_c) + if val_hsv < 0.20: + dark_found.append(hex_c) + elif sat > 0.30 and (h_deg < 70 or h_deg >= 345): + warm_found.append(hex_c) + except Exception: + pass + + if warm_found: + bullets["valence"].append( + ("✅", f"Warm tones in palette: {', '.join(warm_found[:2])} — " + f"red/orange/yellow hues sit in the high-arousal, positive-valence " + f"quadrant of Russell's circumplex model") + ) + else: + bullets["valence"].append( + ("➡️", "No dominant warm tones — palette is cool or neutral. " + "Cool colors (blue, grey) are emotionally safe but do not actively " + "drive positive valence or purchase intent") + ) + + if dark_found: + bullets["valence"].append( + ("⚠️", f"Dark colors present: {', '.join(dark_found[:2])} — " + f"HSV value < 20% encodes mild negative affect regardless of hue. " + f"Dark palettes reduce perceived warmth and energy") + ) + + # ── COGNITIVE LOAD ──────────────────────────────────────────────────────── + + high_obj = obj_count > 7 + high_text = text_dens > 0.30 + low_obj = obj_count <= 4 + low_text = text_dens < 0.10 + + if high_obj and high_text: + bullets["load"].append( + ("⚠️", f"{obj_count} objects AND {td_pct:.0f}% text — both the visual and " + f"linguistic processing channels are simultaneously overloaded. " + f"Working memory (Miller's 7±2 limit) will saturate; viewer disengages") + ) + elif high_obj: + bullets["load"].append( + ("⚠️", f"{obj_count} objects — visual channel overloaded (>7 exceeds Miller's 7±2 rule). " + f"Reduce to ≤7 objects to bring visual complexity into acceptable range") + ) + elif high_text: + bullets["load"].append( + ("⚠️", f"{td_pct:.0f}% text — linguistic channel overloaded (>30% density). " + f"Compress copy to one primary message; every extra word costs processing capacity") + ) + elif low_obj and low_text: + bullets["load"].append( + ("✅", f"{obj_count} objects + {td_pct:.0f}% text — both channels are well within " + f"processing capacity. Effortless comprehension; ideal for mobile scroll environments") + ) + else: + bullets["load"].append( + ("➡️", f"{obj_count} objects + {td_pct:.0f}% text — moderate load, " + f"acceptable for most placements but may reduce performance in " + f"passive scroll contexts (Reels, TikTok)") + ) + + icon = "✅" if cl == "Low" else ("➡️" if cl == "Medium" else "⚠️") + bullets["load"].append( + (icon, f"Composite load score: {cl_score:.0f}/100 → classified as {cl} load") + ) + + return bullets + + +def show_score_explanations(results: list) -> None: + """ + Render 'Why this score?' bullet points for each creative as collapsible expanders. + Each expander shows 4 metric sections with real-value bullet points. + """ + st.markdown("
    ", unsafe_allow_html=True) + st.markdown( + "
    📝 Why Each Creative Scored This Way
    ", + unsafe_allow_html=True, + ) + st.markdown( + "

    " + "Click any creative to expand its full score explanation — every bullet " + "references an actual detected value.

    ", + unsafe_allow_html=True, + ) + + METRIC_CONFIG = [ + ("attention", "🎯 Attention Score", "#3B82F6"), + ("memory", "🧠 Memory Encoding", "#22C55E"), + ("valence", "❤️ Emotional Valence", "#CBD5E1"), + ("load", "⚙️ Cognitive Load", "#F59E0B"), + ] + + ICON_COLOR = {"✅": "#22C55E", "⚠️": "#EF4444", "➡️": "#F59E0B"} + + for r in results: + s = r["signals"] + cpci_val = r["cpci"] + label = ( + "Strong Performer" if cpci_val >= 70 + else ("Average Performer" if cpci_val >= 45 else "Needs Improvement") + ) + cc = cpci_color(cpci_val) + + expander_title = ( + f"{r['name']} — CPCi {cpci_val} | " + f"Attn {s['attention_score']} | Mem {s['memory_score']} | " + f"Val {s['emotional_valence']:+.2f} | Load {s['cognitive_load']}" + ) + + with st.expander(expander_title, expanded=False): + explanation = generate_score_explanation(r) + + # Two-column grid: left = Attention + Memory, right = Valence + Load + left_col, right_col = st.columns(2) + + for col, keys in [(left_col, ["attention", "memory"]), + (right_col, ["valence", "load"])]: + with col: + for key, title, accent in METRIC_CONFIG: + if key not in keys: + continue + score_val = ( + s["attention_score"] if key == "attention" + else s["memory_score"] if key == "memory" + else s["emotional_valence"] if key == "valence" + else s["cognitive_load"] + ) + score_str = ( + f"{score_val}/100" if key in ("attention", "memory") + else f"{score_val:+.3f}" if key == "valence" + else str(score_val) + ) + + st.markdown( + f"
    " + f"{title}  {score_str}
    ", + unsafe_allow_html=True, + ) + + for icon, text in explanation[key]: + icon_color = ICON_COLOR.get(icon, "#FFFFFF") + st.markdown( + f"
    " + f"{icon}" + f"{text}
    ", + unsafe_allow_html=True, + ) + + +# ── Main UI ─────────────────────────────────────────────────────────────────── + +# ── Landing screen ──────────────────────────────────────────────────────────── +if "app_entered" not in st.session_state: + st.session_state["app_entered"] = False + +if not st.session_state["app_entered"]: + st.markdown(""" + + +
    + +
    + Cognitive Signal
    Engine™ +
    + +
    Creative Intelligence Analyzer
    + +
    + Measure how creatives perform in the brain
    — before media spend. +
    + +
    + +
    +
    +
    +
    Pre-bid creative scoring
    +
    +
    +
    +
    Reduce wasted media spend
    +
    +
    +
    +
    Faster creative decisions
    +
    +
    +
    + """, unsafe_allow_html=True) + + # ── Scientific Foundation panel (separate call — avoids Streamlit style-block issue) ── + st.markdown( + "
    " + "
    " + + # Header bar + "
    " + "
    " + "
    Scientific Foundation — Built on Meta TRIBE v2 · Extended by CPCi
    " + "
    " + + # Two columns via flex + "
    " + + # LEFT — Meta TRIBE v2 + "
    " + "
    " + "
    META FAIR
    " + "
    TRIBE v2 · 2025
    " + "
    " + "
    " + "Tri-modal Brain Imaging Encoding model. Predicts fMRI brain activity from video, audio & text" + " — trained on 720 subjects, " + "1,117 hours of brain scans. " + "Ranked #1 at Algonauts 2025 (263 teams)." + "
    " + "
    " + " Neuroscience weights: which brain regions respond to contrast, faces, text
    " + " Signal thresholds: V1–V7 visual cortex, FaceBody area, Language regions, Limbic
    " + " Brain data: face attention fires in <13ms · contrast drives pre-attentive salience" + "
    " + "
    " + "d'Ascoli, Rapin, Benchetrit, King et al. · Meta FAIR 2025 · " + "CC BY-NC 4.0" + "
    " + "
    " + + # Divider + "
    " + + # RIGHT — CPCi + "
    " + "
    " + "
    COGNITIVE SIGNAL ENGINE™
    " + "
    CPCi · Anil Pandit
    " + "
    " + "
    " + "Cost Per Cognitive Impression. Translates TRIBE v2 brain-encoding patterns into a " + "single 0–100 ad effectiveness score — " + "with use-case weights, load penalties, and a full narrative strategy engine." + "
    " + "
    " + " CPCi formula: weighted combination of Attention + Memory + Valence signals
    " + " Use-case weights: FMCG / Performance / Retail tuned for media context
    " + " Narrative engine: converts scores into actionable creative strategy" + "
    " + "
    " + "Anil Pandit · Cognitive Signal Engine™ · AI and Data Leader · " + "Proprietary framework" + "
    " + "
    " + + "
    " # end flex row + "
    " # end border card + "
    ", # end max-width wrapper + unsafe_allow_html=True, + ) + + _lnd_gap, _lnd_cta, _lnd_demo, _lnd_gap2 = st.columns([3, 2, 2, 3], gap="small") + + with _lnd_cta: + if st.button("Start Analysis", type="primary", use_container_width=True): + st.session_state["app_entered"] = True + st.rerun() + + with _lnd_demo: + if st.button("View Demo", use_container_width=True): + st.session_state["app_entered"] = True + st.session_state["demo_mode"] = True + st.rerun() + + st.markdown( + "
    " + "
    " + "Cognitive Signal Engine™
    " + "
    Creative Intelligence Analyzer
    " + "
    " + "© Anil Pandit  ·  ADVantage Insights" + "
    " + "
    ", + unsafe_allow_html=True, + ) + st.stop() + +_BRAIN_SVG = ( + "" + "" + "" + "" + "" + "" +) +# ── App header ─────────────────────────────────────────────────────────────── +_hdr_left, _hdr_right = st.columns([3, 2], gap="large") + +with _hdr_left: + st.markdown( + "
    " + "
    " + "" + "" + "" + "" + "
    " + "
    " + "
    " + "Cognitive Signal Engine™
    " + "
    Creative Intelligence Analyzer
    " + "
    " + "
    ", + unsafe_allow_html=True, + ) + +with _hdr_right: + # Guard: if demo mode exit was requested programmatically (from inside _render_demo_mode), + # apply it HERE — before any widget with key="demo_mode" is instantiated, so Streamlit + # doesn't raise StreamlitAPIException about modifying widget-bound state. + if st.session_state.pop("_exit_demo", False): + st.session_state["demo_mode"] = False + st.session_state["demo_step"] = 1 + st.session_state["demo_creative"] = 0 + + st.markdown("
    ", unsafe_allow_html=True) + _tog_a, _tog_b = st.columns(2, gap="small") + with _tog_a: + _expert_on = st.toggle( + "Expert Mode", + value=st.session_state.get("expert_mode", False), + key="expert_mode", + ) + with _tog_b: + _demo_on = st.toggle( + "Demo Mode", + value=st.session_state.get("demo_mode", False), + key="demo_mode", + ) + st.markdown("
    ", unsafe_allow_html=True) + +st.markdown( + "
    ", + unsafe_allow_html=True, +) + +# ── Top-level tabs ──────────────────────────────────────────────────────────── +tab_analyzer, tab_science, tab_glossary = st.tabs([ + "🔬 Analyzer", + "🧠 Science & Methodology", + "📖 Glossary", +]) + +with tab_science: + show_science_tab() + +with tab_glossary: + show_glossary_tab() + +with tab_analyzer: + # ── Framework ───────────────────────────────────────────────────────────── + with st.expander("📐 Framework", expanded=False): + fw_left, fw_right = st.columns([3, 2], gap="large") + + with fw_left: + st.markdown( + "
    " + "
    " + "Cognitive Signal Engine (CSE)
    " + + "
    " + "This system is built on the Cognitive Signal Engine — " + "a proprietary framework that models how advertising stimuli are processed " + "across four dimensions:" + "
    " + + "
    " + + "
    " + "01" + "
    Attention" + " · Does it get noticed?
    " + "
    " + + "
    " + "02" + "
    Memory" + " · Is it encoded?
    " + "
    " + + "
    " + "03" + "
    Emotion" + " · Does it create affinity?
    " + "
    " + + "
    " + "04" + "
    Cognitive Load" + " · Is it easy to process?
    " + "
    " + + "
    " + + "
    " + "These signals are combined into " + "CPCi — a unified measure of " + "cognitive effectiveness before media spend." + "
    " + "
    ", + unsafe_allow_html=True, + ) + + with fw_right: + st.markdown( + "
    " + + "
    " + "Signal Architecture
    " + + "
    " + + "
    " + "Layer" + "Source" + "
    " + + "
    " + "Attention" + "Contrast · Face · Clutter" + "
    " + + "
    " + "Memory" + "Composition · Text density" + "
    " + + "
    " + "Emotion" + "Colour · Face warmth" + "
    " + + "
    " + "Cognitive Load" + "Objects · Entropy · Text %" + "
    " + + "
    " + "
    Output
    " + "
    CPCi Score
    " + "
    " + "Weighted composite · 0–100 · Use-case calibrated
    " + "
    " + + "
    " + "
    ", + unsafe_allow_html=True, + ) + + st.markdown("
    ", unsafe_allow_html=True) + + # ── Why CPCi? ───────────────────────────────────────────────────────────── + with st.expander("⚡ Why CPCi?", expanded=False): + st.markdown( + "
    " + + # — Full form + one-liner —————————————————————————————————————————— + "
    " + "CPCi" + " · " + "Cost Per Cognitive Impression" + "
    " + "A single, pre-spend measure of how effectively an ad creative engages " + "the human brain — across attention, memory, emotion, and processing ease." + "
    " + "
    " + + # — The contrast argument —————————————————————————————————————————— + "
    " + + "
    " + "
    " + "Traditional metrics
    " + "
    " + "CTR, CVR, ROAS — measured after exposure.
    " + "They tell you what happened. They do not tell you why.

    " + "By the time you have the data, budget has already been spent on creative " + "that failed at the first cognitive gate." + "
    " + "
    " + + "
    " + "
    " + "CPCi
    " + "
    " + "Measured before exposure — at the visual signal layer.
    " + "Models what happens in the first 1–3 seconds: does the brain notice, " + "encode, and respond positively?

    " + "Evaluate creative effectiveness before media spend. " + "Kill weak creatives before they consume budget." + "
    " + "
    " + + "
    " + + # — Benchmark rationale ———————————————————————————————————————————— + "
    " + "
    " + "How the benchmarks were set
    " + + "
    " + + "
    " + "
    70+
    " + "
    " + "Scale-ready
    " + "
    " + "All three signals (attention, memory, emotion) clear their individual " + "thresholds. Cognitive Load Theory suggests low-load creative at these " + "levels is processed fluently — the necessary precondition for action." + "
    " + "
    " + + "
    " + "
    40–69
    " + "
    " + "Optimise first
    " + "
    " + "Mixed signal profile — at least one dimension is below the encoding " + "floor. Paivio's Dual-Coding research indicates partial encoding leads " + "to poor recall and weak brand linkage over time." + "
    " + "
    " + + "
    " + "
    <40
    " + "
    " + "Do not scale
    " + "
    " + "Insufficient cognitive engagement. Consistent with attention research " + "showing creatives below contrast and salience minimums are processed " + "at near-zero depth — spend is wasted before the message lands." + "
    " + "
    " + + "
    " + "
    " + + "
    ", + unsafe_allow_html=True, + ) + + st.markdown("
    ", unsafe_allow_html=True) + + # ── What is CPCi? ───────────────────────────────────────────────────────── + with st.expander("💡 What is CPCi? — Click to learn how scoring works", expanded=False): + col_a, col_b, col_c, col_d = st.columns(4) + col_a.markdown(""" +
    +
    What is CPCi?
    +
    + CPCi is produced by the Cognitive Signal Engine — + a system that models how advertising creative is processed by the human brain, + before any click or conversion occurs.

    + Higher CPCi = stronger cognitive processing = better media efficiency. +
    +
    """, unsafe_allow_html=True) + col_b.markdown(""" +
    +
    🎯
    +
    Attention
    +
    + Does it stop the scroll?

    + Driven by visual contrast, face presence, and how clean or cluttered the image is. +
    +
    """, unsafe_allow_html=True) + col_c.markdown(""" +
    +
    🧠
    +
    Memory
    +
    + Will the brand be remembered?

    + Simple compositions and balanced text leave a stronger memory trace. +
    +
    """, unsafe_allow_html=True) + col_d.markdown(""" +
    +
    ❤️
    +
    Emotional Valence
    +
    + Does it create a positive response?

    + Warm colours and human faces drive positive valence. +
    +
    """, unsafe_allow_html=True) + st.markdown(""" +
    + How weights work: + CPCi formula is adjusted per use case — brand campaign prioritises memory, + performance campaign prioritises attention. See the 🧠 Science tab for the full formula. +
    """, unsafe_allow_html=True) + + # ── Resolve mode flags from header toggles ─────────────────────────────── + client_mode = not st.session_state.get("expert_mode", False) + demo_mode = st.session_state.get("demo_mode", False) + + st.markdown("
    ", unsafe_allow_html=True) + + # ── Use-case selector ───────────────────────────────────────────────────── + uc_col, info_col = st.columns([1, 2]) + with uc_col: + selected_uc = st.selectbox( + "🎯 Select Use Case", + options=list(USE_CASES.keys()), + index=1, + help="Changes CPCi weights to match your campaign objective", + ) + uc_cfg = USE_CASES[selected_uc] + uc_weights = uc_cfg["weights"] + with info_col: + w = uc_weights + penalty_note = " · Load Penalty Active" if uc_cfg["load_penalty"] else "" + st.markdown( + f"
    " + f"
    " + f"{uc_cfg['icon']} {selected_uc}" + f"{uc_cfg['description']}
    " + f"
    " + f"Weights: " + f"Attention {int(w['attention']*100)}% · " + f"Memory {int(w['memory']*100)}% · " + f"Emotion {int(w['emotion']*100)}%" + f"{penalty_note}
    " + f"
    " + f"{uc_cfg['rationale']}
    ", + unsafe_allow_html=True, + ) + + st.markdown("
    ", unsafe_allow_html=True) + + # ── Demo Mode — full guided walkthrough experience ──────────────────────── + if demo_mode: + _render_demo_mode(client_mode) + st.stop() + + # ── CTA "Analyze Your Creative" — clear cached results and scroll up ──── + if st.session_state.pop("_cta_new_analysis", False): + for _k in ("cached_results", "analyzed_files", "all_results", "elapsed"): + st.session_state.pop(_k, None) + st.toast("Upload a new creative below ↓", icon="🧠") + + # ── File uploader ───────────────────────────────────────────────────────── + uploaded_files = st.file_uploader( + "📁 Upload ad creatives — Images (JPG, PNG) or Videos (MP4, MOV, AVI, WEBM) · 1 file for analysis, 2–5 for comparison", + type=["jpg", "jpeg", "png", "mp4", "mov", "avi", "webm", "m4v"], + accept_multiple_files=True, + ) + + if uploaded_files: + n = len(uploaded_files) + if n > 5: + st.error("⚠️ Maximum 5 creatives at once. Please remove some files and try again.") + st.stop() + + st.markdown( + f"
    " + f"{'1 creative — full analysis mode' if n == 1 else f'{n} creatives — comparison mode'}" + f"  ·  {uc_cfg['icon']} {selected_uc}" + f"
    ", + unsafe_allow_html=True, + ) + + _VIDEO_EXTS_UI = {".mp4", ".mov", ".avi", ".webm", ".m4v"} + thumb_cols = st.columns(min(n, 5)) + for col, uf in zip(thumb_cols, uploaded_files): + ext = os.path.splitext(uf.name)[1].lower() + if ext in _VIDEO_EXTS_UI: + col.video(uf) + col.caption(f"🎬 {short_name(uf.name, 22)}") + else: + col.image(uf, caption=short_name(uf.name, 22), use_container_width=True) + + st.markdown("
    ", unsafe_allow_html=True) + _has_video = any( + os.path.splitext(uf.name)[1].lower() in _VIDEO_EXTS_UI + for uf in uploaded_files + ) + _est_low = n * 3 if _has_video else n + _est_high = n * 8 if _has_video else n * 2 + analyze_btn = st.button( + f"🔬 {'Analyze Creative' if n == 1 else f'Compare {n} Creatives'} [{selected_uc}]", + type="primary", + use_container_width=True, + ) + st.markdown( + f"

    " + f"~{_est_low}–{_est_high}s · No GPU · Attn {int(uc_weights['attention']*100)}% · " + f"Mem {int(uc_weights['memory']*100)}% · " + f"Emo {int(uc_weights['emotion']*100)}%" + f"{' ·  🎬 Video: 6-frame sampling' if _has_video else ''}

    ", + unsafe_allow_html=True, + ) + + # ── Restore cached results ──────────────────────────────────────────── + cached_names = st.session_state.get("analyzed_files", []) + cached_uc = st.session_state.get("analyzed_uc", "") + current_names = [uf.name for uf in uploaded_files] + cache_valid = ( + "all_results" in st.session_state + and cached_names == current_names + and cached_uc == selected_uc + ) + if cache_valid: + cached = st.session_state["all_results"] + cached_sorted = sorted(cached, key=lambda x: x["cpci"], reverse=True) + if n == 1: + show_results(cached[0], st.session_state.get("elapsed"), selected_uc, client_mode) + else: + show_comparison(cached_sorted, selected_uc, client_mode) + + # ── Run analysis ────────────────────────────────────────────────────── + if analyze_btn: + all_results = [] + t0 = time.time() + progress_bar = st.progress(0, text="Starting analysis…") + for i, uf in enumerate(uploaded_files): + progress_bar.progress( + i / n, + text=f"Analyzing {uf.name} ({i+1}/{n})…", + ) + with st.spinner(f"🔬 Processing {short_name(uf.name)}…"): + result = run_pipeline( + uf, + weights=uc_weights, + apply_load_penalty=uc_cfg["load_penalty"], + use_case=selected_uc, + ) + all_results.append(result) + + progress_bar.progress(1.0, text="✅ All creatives analyzed.") + elapsed = time.time() - t0 + st.session_state["all_results"] = all_results + st.session_state["analyzed_files"] = [uf.name for uf in uploaded_files] + st.session_state["analyzed_uc"] = selected_uc + st.session_state["elapsed"] = elapsed + st.markdown( + f"
    ⚡ {n} creative{'s' if n > 1 else ''} analyzed " + f"in {elapsed:.2f}s  ·  {uc_cfg['icon']} {selected_uc}
    ", + unsafe_allow_html=True, + ) + if n == 1: + show_results(all_results[0], elapsed, selected_uc, client_mode) + else: + sorted_results = sorted(all_results, key=lambda x: x["cpci"], reverse=True) + show_comparison(sorted_results, selected_uc, client_mode) + + +# ── Global footer (visible on all tabs) ─────────────────────────────────────── +_FOOTER_SVG = ( + "" + "" + "" + "" + "" +) +st.markdown( + f"
    " + f"
    " + + # Left — logo + name + f"
    " + f"
    {_FOOTER_SVG}
    " + f"
    " + f"
    Cognitive Signal Engine™
    " + f"
    Creative Intelligence Analyzer
    " + f"
    " + f"
    " + + # Centre — single clean line + f"
    " + f"© Anil Pandit  ·  ADVantage Insights  ·  CPCi — Cost Per Cognitive Impression" + f"
    " + + # Right — tagline only + f"
    " + f"Neuroscience → Cognitive signals → Media decisions." + f"
    " + + f"
    " + f"
    ", + unsafe_allow_html=True, +) diff --git a/science_tab.py b/science_tab.py new file mode 100644 index 0000000..905f524 --- /dev/null +++ b/science_tab.py @@ -0,0 +1,984 @@ +""" +science_tab.py +-------------- +Renders two tabs: + - 🧠 Science & Methodology + - 📖 Glossary + +Content is drawn from: + - TRIBE v2 paper (Meta FAIR, d'Ascoli et al. 2025) + - Cognitive science literature cited in the pipeline + - CPCi methodology developed for this tool + +All content is pointer-style (no big paragraphs). +""" + +import streamlit as st + + +# ── Shared CSS ──────────────────────────────────────────────────────────────── + +SCI_CSS = """ + +""" + + +def _ptr(arrow_color: str, bold: str, rest: str = "") -> str: + """Single pointer row HTML.""" + return ( + f"
    " + f"" + f"{bold}{' ' + rest if rest else ''}" + f"
    " + ) + + +def _block(title: str, accent: str, content: str) -> str: + return ( + f"
    " + f"
    {title}
    " + f"{content}" + f"
    " + ) + + +# ══════════════════════════════════════════════════════════════════════════════ +# TAB 1 — Science & Methodology +# ══════════════════════════════════════════════════════════════════════════════ + +def show_science_tab() -> None: + st.markdown(SCI_CSS, unsafe_allow_html=True) + + st.markdown( + "
    The Research Foundation
    " + "
    " + "TRIBE v2 × Creative Intelligence
    " + "
    " + "How a Meta brain-encoding model became the science behind your ad scores
    ", + unsafe_allow_html=True, + ) + + # ── Key stats bar ───────────────────────────────────────────────────────── + s1, s2, s3, s4, s5 = st.columns(5) + for col, big, label, color in [ + (s1, "720", "fMRI Subjects", "#40c4ff"), + (s2, "1,117h", "Brain Scan Hours", "#00e676"), + (s3, "20,484", "Cortical Vertices", "#ffb300"), + (s4, "#1", "Algonauts 2025 Rank", "#ffd700"), + (s5, "263", "Competing Teams", "#ff80ab"), + ]: + col.markdown( + f"
    " + f"
    {big}
    " + f"
    {label}
    " + f"
    ", + unsafe_allow_html=True, + ) + + st.markdown("
    ", + unsafe_allow_html=True) + + # ── Section A: What is TRIBE v2 ─────────────────────────────────────────── + st.markdown( + "
    A · What Is TRIBE v2
    ", + unsafe_allow_html=True, + ) + + col_l, col_r = st.columns([3, 2]) + + with col_l: + st.markdown(_block("The Model", "#40c4ff", + _ptr("#40c4ff", "Full name:", "Tri-modal Brain Imaging Encoding model v2") + + _ptr("#40c4ff", "Origin:", "Meta FAIR (Fundamental AI Research), 2025") + + _ptr("#40c4ff", "Authors:", "d'Ascoli, Rapin, Benchetrit, King et al.") + + _ptr("#40c4ff", "Paper:", "\"A foundation model of vision, audition, and language for in-silico neuroscience\"") + + _ptr("#40c4ff", "Type:", "Tri-modal transformer encoder — processes video + audio + text simultaneously") + ), unsafe_allow_html=True) + + st.markdown(_block("What It Does", "#00e676", + _ptr("#00e676", "Core task:", "Predict human brain activity (fMRI) from any audio, visual, or language stimulus") + + _ptr("#00e676", "Input modalities:", "Video (V-JEPA embeddings) · Audio (W2V-BERT embeddings) · Text (LLaMA embeddings)") + + _ptr("#00e676", "Output:", "BOLD signal across 20,484 cortical + 8,802 subcortical brain locations") + + _ptr("#00e676", "Key innovation:", "One model — all stimuli types, all brain regions, zero-shot generalisation to new subjects") + ), unsafe_allow_html=True) + + with col_r: + st.markdown(_block("Training Scale", "#ffb300", + _ptr("#ffb300", "Subjects:", "720 healthy volunteers") + + _ptr("#ffb300", "Sessions:", "5,094 scan sessions") + + _ptr("#ffb300", "fMRI hours:", "1,117 hours of brain recordings") + + _ptr("#ffb300", "Video:", "121 hours of stimulus video") + + _ptr("#ffb300", "Audio:", "142 hours of stimulus audio") + + _ptr("#ffb300", "Text sentences:", "71,000 transcribed sentences") + + _ptr("#ffb300", "Training GPU:", "128 × V100 GPUs for feature extraction") + ), unsafe_allow_html=True) + + st.markdown( + "
    " + "\"By aligning the representations of AI systems to those of the human brain, " + "we demonstrate that a single architecture can integrate a vast range " + "of fMRI responses… moving from the fragmented mapping of isolated cognitive tasks " + "toward a unified, predictive foundation model.\"" + "

    — d'Ascoli et al., Meta FAIR 2025" + "
    ", + unsafe_allow_html=True, + ) + + # ── Section B: How Brain Encoding Connects to Ads ───────────────────────── + st.markdown("
    ", + unsafe_allow_html=True) + st.markdown( + "
    B · From Brain Science to Ad Intelligence
    ", + unsafe_allow_html=True, + ) + + # Pipeline steps + st.markdown( + "
    How your ad image becomes a CPCi score:
    ", + unsafe_allow_html=True, + ) + st.markdown(""" +
    +
    📁 UploadJPG / PNG
    +
    +
    👁 VisionOpenCV scan
    +
    +
    🧬 SignalsCognitive map
    +
    +
    🧠 TRIBEBrain basis
    +
    +
    📊 CPCiWeighted score
    +
    +
    📝 ReportStrategy
    +
    """, unsafe_allow_html=True) + + b1, b2, b3 = st.columns(3) + + with b1: + st.markdown(_block("Step 1 — Visual Feature Extraction", "#40c4ff", + _ptr("#40c4ff", "Tool:", "OpenCV + Tesseract OCR") + + _ptr("#40c4ff", "Object count:", "Canny edge detection + contour analysis") + + _ptr("#40c4ff", "Face detection:", "Haar Cascade classifier (< 200ms)") + + _ptr("#40c4ff", "Text density:", "Tesseract PSM 11 sparse-text mode") + + _ptr("#40c4ff", "Contrast:", "Std deviation of grayscale pixel values") + + _ptr("#40c4ff", "Colours:", "K-means clustering (k=3) on 100×100 downsample") + + _ptr("#40c4ff", "Speed:", "< 2 seconds total · No GPU required") + ), unsafe_allow_html=True) + + with b2: + st.markdown(_block("Step 2 — Cognitive Signal Mapping", "#ff80ab", + _ptr("#ff80ab", "Attention:", "Contrast × 0.5 + Face boost + Clutter penalty") + + _ptr("#ff80ab", "Memory:", "Simplicity score + Dual-coding text factor") + + _ptr("#ff80ab", "Valence:", "HSV colour psychology + Face warmth component") + + _ptr("#ff80ab", "Cog. Load:", "Object count + text density composite (Low / Medium / High)") + + _ptr("#ff80ab", "TRIBE basis:", "Signal thresholds calibrated against TRIBE v2 visual cortex encoding patterns") + + _ptr("#ff80ab", "Transparency:", "All thresholds named as constants — no black box") + ), unsafe_allow_html=True) + + with b3: + st.markdown(_block("Step 3 — CPCi Computation", "#ffb300", + _ptr("#ffb300", "Formula:", "w_attn × Attention + w_mem × Memory + w_emo × Valence_norm") + + _ptr("#ffb300", "Valence norm:", "(Valence + 1) / 2 × 100 → maps −1/+1 to 0/100 scale") + + _ptr("#ffb300", "Load penalty:", "−0 / −5 / −12 for Low / Medium / High (Retail Media only)") + + _ptr("#ffb300", "Use-case weights:", "FMCG: 20/50/30 · Performance: 50/30/20 · Retail: 40/30/30") + + _ptr("#ffb300", "Score range:", "0 → 100 · < 40 weak · 40–70 average · > 70 strong") + + _ptr("#ffb300", "Clamp:", "max(0, min(100, raw)) — never negative or over 100") + ), unsafe_allow_html=True) + + # ── Section B2: From Brain Science to Business Decisions ────────────────── + st.markdown("
    ", + unsafe_allow_html=True) + st.markdown( + "
    B2 · Architecture
    " + "
    " + "From Brain Science to Business Decisions
    " + "
    " + "Two layers — one system. TRIBE v2 predicts brain activation. " + "CPCi converts those signals into a single business-ready score.
    ", + unsafe_allow_html=True, + ) + + # ── Two-layer architecture diagram ─────────────────────────────────────── + lay_l, lay_mid, lay_r = st.columns([5, 1, 5]) + + with lay_l: + st.markdown( + "
    " + # Badge + "
    " + "
    LAYER 1
    " + "
    TRIBE v2
    " + "
    " + # Role + "
    " + "Brain activation prediction model trained on 720 subjects and 1,117 hours of fMRI scans. " + "Given any visual, audio, or language stimulus, TRIBE v2 predicts which brain regions fire " + "— and at what intensity." + "
    " + # Pointer list + + _ptr("#3B82F6", "Input:", "Ad creative (image, video, or text)") + + _ptr("#3B82F6", "Process:", "Tri-modal transformer — V-JEPA · W2V-BERT · LLaMA") + + _ptr("#3B82F6", "Output:", "Predicted BOLD activation across 20,484 cortical vertices") + + _ptr("#3B82F6", "Key regions:", "V1–V7 visual cortex · FaceBody area · Language areas · Limbic") + + _ptr("#3B82F6", "Performance:", "#1 Algonauts 2025 benchmark — 263 competing teams") + + + "
    " + "What it tells us: " + "Which features of a creative cause the human brain to activate — " + "contrast, faces, text, emotional tone — and by how much." + "
    " + "
    ", + unsafe_allow_html=True, + ) + + with lay_mid: + st.markdown( + "
    " + "
    " + "
    converts to
    " + "
    ", + unsafe_allow_html=True, + ) + + with lay_r: + st.markdown( + "
    " + # Badge + "
    " + "
    LAYER 2
    " + "
    CPCi
    " + "
    Cost Per Cognitive Impression
    " + "
    " + # Role + "
    " + "CPCi takes TRIBE v2's brain-region activation patterns and converts them into four " + "measurable signals — each one a direct media performance lever." + "
    " + # Signal pills with contribution arrows + "
    " + + # Attention + "
    " + "
    " + "Attention
    " + "
    " + "Visual cortex activation → stopping power in feed
    " + "
    " + + # Memory + "
    " + "
    " + "Memory
    " + "
    " + "Language area activity → brand recall after exposure
    " + "
    " + + # Emotion + "
    " + "
    " + "Emotion
    " + "
    " + "Limbic/subcortical response → valence and purchase intent
    " + "
    " + + # Load + "
    " + "
    " + "Load
    " + "
    " + "Working memory saturation → processing penalty
    " + "
    " + + # Business score callout + "
    " + "
    " + "
    " + "Business
    Score
    " + "
    " + "CPCi 0–100
    " + "Weighted composite · Use-case calibrated
    " + "Single number a CMO can act on" + "
    " + "
    " + "
    " + + "
    " + "
    ", + unsafe_allow_html=True, + ) + + # ── Tagline callout ─────────────────────────────────────────────────────── + st.markdown( + "
    " + "
    " + "This system translates neuroscience into actionable media decisions." + "
    " + "
    " + "TRIBE v2 answers: which brain regions respond to this creative?  ·  " + "CPCi answers: should you spend media budget on it?" + "
    " + "
    ", + unsafe_allow_html=True, + ) + + # ── Section C: The Brain Regions That Matter for Ads ────────────────────── + st.markdown("
    ", + unsafe_allow_html=True) + st.markdown( + "
    C · Brain Regions That Drive Ad Response
    ", + unsafe_allow_html=True, + ) + + c1, c2, c3, c4 = st.columns(4) + for col, region, role, signals, color in [ + (c1, "V1 – V7\nVisual Cortex", + "Primary visual processing — contrast, edges, motion", + "→ Contrast score\n→ Object detection\n→ Attention score", "#40c4ff"), + (c2, "FaceBody Area\n(Lateral Cortex)", + "Face and body detection — activates in < 13ms, involuntary", + "→ Face count\n→ Attention boost\n→ Valence warmth", "#ff80ab"), + (c3, "Language Areas\n(Left Hemisphere)", + "Text and verbal processing — competes with visual memory trace", + "→ Text density\n→ Memory encoding\n→ Cognitive load", "#ffb300"), + (c4, "Limbic / Emotional\n(Subcortical)", + "Colour, arousal, reward — determines whether content is engaging", + "→ Emotional valence\n→ Colour psychology\n→ Purchase intent", "#00e676"), + ]: + col.markdown( + f"
    " + f"
    {region}
    " + f"
    {role}
    " + + "".join( + f"
    " + f"{s.replace('→ ', '')}
    " + for s in signals.split("\n") + ) + + f"
    ", + unsafe_allow_html=True, + ) + + # Paper note + st.markdown( + "
    " + "TRIBE v2 predicts responses across all of these regions simultaneously. " + "The cognitive signal thresholds in this tool are calibrated against TRIBE's " + "encoding score patterns — the same patterns that showed V1–V7 responding " + "to contrast and edges, the FaceBody area responding to faces, and language areas " + "competing with visual processing for working memory resources." + "
    ", + unsafe_allow_html=True, + ) + + # ── Section D: The Cognitive Science Frameworks ─────────────────────────── + st.markdown("
    ", + unsafe_allow_html=True) + st.markdown( + "
    D · Cognitive Science Frameworks Used
    ", + unsafe_allow_html=True, + ) + + d1, d2 = st.columns(2) + with d1: + st.markdown(_block("Dual-Coding Theory — Paivio (1971)", "#c084fc", + _ptr("#c084fc", "Core claim:", "The brain has two separate memory channels — visual and verbal") + + _ptr("#c084fc", "How it works:", "Images stored in visual memory · words/text in verbal memory") + + _ptr("#c084fc", "Key finding:", "Content encoded in BOTH channels is 2× more likely to be recalled") + + _ptr("#c084fc", "In this tool:", "Text density 5–25% = dual-coding sweet spot · < 5% = verbal channel unused · > 25% = verbal drowns visual") + + _ptr("#c084fc", "Ad implication:", "A single 5–8 word tagline + strong visual is more memorable than text-only or image-only") + ), unsafe_allow_html=True) + + st.markdown(_block("Russell Circumplex Model of Affect (1980)", "#ff80ab", + _ptr("#ff80ab", "Core claim:", "All emotions can be mapped on two axes: Valence (positive/negative) + Arousal (excited/calm)") + + _ptr("#ff80ab", "Why it matters:", "Emotional valence is the single strongest predictor of long-term brand equity") + + _ptr("#ff80ab", "In this tool:", "Valence scored −1.0 (aversive) → +1.0 (rewarding)") + + _ptr("#ff80ab", "Colour mapping:", "Warm hues (red/orange/yellow) → positive arousal · Cool/dark → lower valence") + + _ptr("#ff80ab", "Face contribution:", "Each face detected adds up to +0.25 valence (affiliative warmth)") + ), unsafe_allow_html=True) + + with d2: + st.markdown(_block("Cognitive Load Theory — Sweller (1988)", "#ffb300", + _ptr("#ffb300", "Core claim:", "Working memory has a fixed capacity (~7±2 items)") + + _ptr("#ffb300", "Three load types:", "Intrinsic (task complexity) · Extraneous (visual noise) · Germane (learning)") + + _ptr("#ffb300", "Ad relevance:", "Extraneous load = clutter = objects + excess text competing for attention") + + _ptr("#ffb300", "In this tool:", "< 4 objects = Low load · 4–8 = Medium · > 8 = High") + + _ptr("#ffb300", "Cost of High load:", "Saturated working memory → brand message not encoded → wasted impressions") + + _ptr("#ffb300", "Retail Media:", "High load penalised −12 CPCi points (clutter compounds with environment)") + ), unsafe_allow_html=True) + + st.markdown(_block("Miller's Law (1956) + Pre-attentive Processing", "#40c4ff", + _ptr("#40c4ff", "Miller's Law:", "Short-term memory holds 7 ± 2 'chunks' of information simultaneously") + + _ptr("#40c4ff", "Why it caps objects:", "More than 7 objects → chunking fails → viewer samples randomly") + + _ptr("#40c4ff", "Pre-attentive:", "The brain processes certain features (contrast, colour, motion) in < 200ms — before conscious attention") + + _ptr("#40c4ff", "Face reflex:", "Face detection activates in < 13ms — the fastest involuntary attention trigger available") + + _ptr("#40c4ff", "Implication:", "Ads that pass the pre-attentive filter don't need to 'ask' for attention — they capture it automatically") + ), unsafe_allow_html=True) + + # ── Section E: CPCi Score Interpretation ───────────────────────────────── + st.markdown("
    ", + unsafe_allow_html=True) + st.markdown( + "
    E · CPCi Score Zones
    ", + unsafe_allow_html=True, + ) + + e1, e2, e3 = st.columns(3) + for col, zone, score, color, points in [ + (e1, "NEEDS IMPROVEMENT", "0 – 39", "#ff5252", [ + "Likely scrolled past in cold-audience feeds", + "Weak attention + memory = low platform Quality Score", + "High CPM inflation — algorithm penalises low engagement", + "Do not scale. Fix at least 1 dimension before spending", + "Action: redesign the creative, not just the copy", + ]), + (e2, "AVERAGE PERFORMER", "40 – 69", "#ffb300", [ + "Will deliver in warm/retargeted audiences", + "Moderate recall — needs 6–8 impressions to build salience", + "Safe for mid-funnel, not efficient for cold prospecting", + "Gap to top-quartile is closeable with 1–2 targeted changes", + "Action: run A/B test before committing full budget", + ]), + (e3, "STRONG PERFORMER", "70 – 100", "#00e676", [ + "Above-average thumb-stop rate in social feed formats", + "Strong memory trace — brand recall lifts in 3–5 impressions", + "Suitable for broad reach and top-of-funnel investment", + "Correlates with 3–5pt brand lift in campaign measurement", + "Action: scale budget 20–30%, cap frequency at 7", + ]), + ]: + col.markdown( + f"
    " + f"
    {zone}
    " + f"
    {score}
    " + + "".join( + f"
    " + f"{p}
    " + for p in points + ) + + f"
    ", + unsafe_allow_html=True, + ) + + st.markdown( + "
    " + "CPCi = (w_attention × Attention) + (w_memory × Memory) + (w_emotion × ((Valence + 1) / 2 × 100))" + "
    [ − Load Penalty if use case = Retail Media ]" + "
    ", + unsafe_allow_html=True, + ) + + # ── Section F: Provenance & Attribution ─────────────────────────────────── + st.markdown("
    ", + unsafe_allow_html=True) + st.markdown( + "
    F · Provenance & Attribution
    " + "
    " + "Exactly which numbers come from Meta TRIBE v2, and which were designed by Anil Pandit for CPCi
    ", + unsafe_allow_html=True, + ) + + # Two-column overview cards + fa, fb = st.columns(2) + + with fa: + st.markdown( + "
    " + "
    " + "
    META FAIR
    " + "
    TRIBE v2 Contribution
    " + "
    " + "
    " + "TRIBE v2 (d'Ascoli et al., Meta FAIR 2025) is a multimodal brain encoding model trained on " + "720 subjects · 1,117h fMRI · 121h video stimuli. " + "It maps AI representations onto cortical brain activity, achieving rank #1 at Algonauts 2025 " + "(263 competing teams) with a mean encoding score of 0.2146.

    " + "The TRIBE v2 brain data established which visual features activate which brain regions — " + "and at what magnitudes. This is the neuroscience foundation for the signal thresholds, " + "clutter penalties, face boosts, and text density curves in CPCi." + "
    " + "
    " + + _ptr("#3B82F6", "V1–V7 visual cortex", "validates contrast as the primary pre-attentive driver") + + _ptr("#3B82F6", "FaceBody area (lateral cortex)", "fires in <13ms — justifies face attention boost of +22 pts") + + _ptr("#3B82F6", "Language areas (left hemisphere)", "competes with visual memory — basis for text density sweet spot") + + _ptr("#3B82F6", "Limbic / subcortical", "colour and face affect — basis for emotional valence scoring") + + "
    " + "
    " + "Published: d'Ascoli S, Rapin J, Benchetrit Y, King J-R et al. · Meta FAIR 2025 · " + "Paper · " + "Weights (HuggingFace) · " + "License: CC BY-NC 4.0" + "
    " + "
    ", + unsafe_allow_html=True, + ) + + with fb: + st.markdown( + "
    " + "
    " + "
    COGNITIVE SIGNAL ENGINE™
    " + "
    Anil Pandit / CPCi Contribution
    " + "
    " + "
    " + "Anil Pandit designed the CPCi framework to translate TRIBE v2's neuroscience insights " + "into a fast, practical, pre-bid ad scoring system. " + "This includes the computer vision pipeline (OpenCV + OCR), all scoring constants, " + "the weighted CPCi formula, use-case tuning, the load penalty system, " + "and the narrative intelligence engine." + "
    " + "
    " + + _ptr("#EF4444", "Creative vision pipeline", "OpenCV + Tesseract OCR — feature extraction in <2s") + + _ptr("#EF4444", "CPCi formula + weights", "w_attn × Attn + w_mem × Mem + w_emo × Val_norm") + + _ptr("#EF4444", "Use-case weight system", "FMCG 20/50/30 · Performance 50/30/20 · Retail 40/30/30") + + _ptr("#EF4444", "Cognitive load penalty", "−0 / −5 / −12 pts for Low / Medium / High (Retail Media)") + + _ptr("#EF4444", "Narrative engine", "Rule + Claude AI narrative · strategy recommendations") + + "
    " + "
    " + "Cognitive Signal Engine™ — Proprietary framework · © Anil Pandit · All rights reserved" + "
    " + "
    ", + unsafe_allow_html=True, + ) + + # ── Detailed provenance table ───────────────────────────────────────────── + st.markdown( + "
    Constant-Level Provenance — Every Number, Every Source
    ", + unsafe_allow_html=True, + ) + + prov_rows = [ + # (Constant / Signal, Value, Source, Neuroscience Basis) + ("Contrast → Attention weight", "0.50 (50 pts max)", + "TRIBE v2 + CPCi", + "V1–V7 visual cortex encodes luminance contrast edges as the primary pre-attentive signal; TRIBE v2 encoding scores confirm contrast as the strongest low-level visual driver"), + ("Face attention boost — 1 face", "+22 pts", + "TRIBE v2 (FaceBody area)", + "FaceBody lateral cortex activates in <13ms; TRIBE v2 predicts the highest encoding scores in this region for face-containing stimuli vs. all other image types"), + ("Face attention boost — 2 faces", "+14 pts (diminishing)", + "TRIBE v2 + CPCi", + "TRIBE v2 face encoding shows diminishing incremental activation when multiple faces compete; Anil Pandit calibrated the step-down to 14 pts based on this pattern"), + ("Face attention boost — 3+ faces","+8 pts", + "CPCi (calibrated on TRIBE v2)", + "Beyond 2 faces, faces create attention fragmentation; Anil Pandit set the cap at +8 based on FaceBody saturation pattern observed in TRIBE v2 multi-face stimuli"), + ("Clutter penalty — mild (4–7 obj)","−4 pts per object", + "Miller's Law + CPCi", + "Miller (1956): working memory holds 7±2 items. Anil Pandit calibrated the per-object penalty to exhaust the attention budget at ~7 objects, consistent with TRIBE v2 visual cortex saturation"), + ("Clutter penalty — severe (8+ obj)","−2 pts per object (cap −30)", + "CPCi", + "Diminishing penalty beyond severe threshold — attention is already fragmented; cap prevents negative scores. Anil Pandit's design decision"), + ("Text density sweet spot", "5%–25% of image area", + "Dual-Coding (Paivio 1986) + TRIBE v2", + "TRIBE v2 language area encoding peaks when verbal content is present but not dominant; Paivio's dual-coding theory (1971) defines the verbal/visual balance — Anil Pandit mapped both to the 5–25% text density range"), + ("Text overload threshold", ">50% → memory collapses", + "Sweller CLT + CPCi", + "Cognitive Load Theory (Sweller 1988): dual-channel saturation causes disengagement. TRIBE v2 language vs. visual cortex competition patterns support the 50% collapse point"), + ("Emotional valence — face boost", "+0.25 per face (cap +0.30)", + "TRIBE v2 (Limbic) + Russell (1980)", + "TRIBE v2 subcortical encoding shows positive affect response to faces via limbic activation; Russell circumplex maps face presence to the high-valence arousal quadrant"), + ("Colour valence — warm hues", "Red/Orange: +0.30 to +0.40", + "Russell Circumplex + CPCi", + "Russell's two-dimensional affect model places warm hues in the high-valence zone. Anil Pandit mapped HSV hue ranges to valence scores, scaled by saturation"), + ("Colour valence — dark/achromatic","≤ −0.25 (dark) / 0.0 (gray)", + "TRIBE v2 (Limbic) + CPCi", + "TRIBE v2 subcortical encoding shows reduced positive affect for low-luminance stimuli; dark images correlate with negative valence in limbic regions"), + ("CPCi formula weights", "Varies by use case", + "CPCi", + "Marketing science judgment: Performance ads need attention most (50%), FMCG needs memory (50%), Retail needs balance. Anil Pandit's proprietary use-case tuning"), + ("Cognitive load penalty", "−0/−5/−12 (Low/Med/High)", + "CPCi", + "Retail Media context amplifies load effects (cluttered screen environment). Anil Pandit's penalty system; not derived from TRIBE v2 directly"), + ("Score zone thresholds", "<40 weak · 40–70 avg · >70 strong", + "CPCi", + "CPCi score bands designed to align with typical creative performance quartiles in paid media. Anil Pandit's proprietary calibration"), + ] + + # Table header + st.markdown( + "
    " + "
    Constant / Signal
    " + "
    Value
    " + "
    Source
    " + "
    Neuroscience / Design Basis
    " + "
    ", + unsafe_allow_html=True, + ) + + rows_html = "
    " + for i, (constant, value, source, basis) in enumerate(prov_rows): + bg = "#060e1a" if i % 2 == 0 else "#070f1c" + # Source badge colour + if "TRIBE v2" in source and "CPCi" in source: + src_color = "#a78bfa" + elif "TRIBE v2" in source: + src_color = "#3B82F6" + else: + src_color = "#EF4444" + rows_html += ( + f"
    " + f"
    {constant}
    " + f"
    {value}
    " + f"
    " + f"{source}" + f"
    " + f"
    {basis}
    " + f"
    " + ) + rows_html += "
    " + st.markdown(rows_html, unsafe_allow_html=True) + + st.markdown( + "
    " + "Note on methodology: " + "TRIBE v2 provides the neuroscience foundation — it tells us which brain regions respond to " + "contrast, faces, and text, and at what relative magnitudes. " + "Anil Pandit's CPCi translates those brain-level insights into a fast, deterministic scoring system " + "using OpenCV + OCR features — no GPU required, no fMRI needed. " + "The signal thresholds and formula constants were calibrated against TRIBE v2's published encoding " + "scores and brain parcel activations, then validated against marketing effectiveness benchmarks. " + "CPCi does not run TRIBE v2 inference directly — it uses TRIBE v2's brain data as its calibration reference." + "
    ", + unsafe_allow_html=True, + ) + + +# ══════════════════════════════════════════════════════════════════════════════ +# TAB 2 — Glossary +# ══════════════════════════════════════════════════════════════════════════════ + +GLOSSARY = { + "Neuroscience & TRIBE": [ + ("TRIBE v2", + "Tri-modal Brain Imaging Encoding model v2. A foundation model from Meta FAIR that predicts fMRI brain activity in response to video, audio, and text. Used as the neuroscience basis for CPCi signal calibration.", + "#40c4ff", + ["Meta FAIR 2025", "720 subjects", "1,117h fMRI", "#1 Algonauts 2025"]), + + ("fMRI", + "Functional Magnetic Resonance Imaging. Measures changes in blood oxygenation across the brain. When a brain region is more active, it consumes more oxygen — fMRI detects this as a BOLD signal change.", + "#40c4ff", + ["Blood oxygen measurement", "~2s temporal resolution", "Whole-brain coverage"]), + + ("BOLD Signal", + "Blood-Oxygen-Level-Dependent signal. The raw measurement from fMRI. Higher BOLD = higher neuronal activity. TRIBE v2 predicts BOLD across 20,484 cortical vertices and 8,802 subcortical voxels.", + "#40c4ff", + ["Neuronal activity proxy", "TRIBE predicts this", "20,484 cortical points"]), + + ("Encoding Model", + "A model that predicts brain activity from sensory inputs. TRIBE v2 is an encoding model — it takes video/audio/text and outputs the expected BOLD signal. Encoding score = Pearson correlation between predicted and actual brain response.", + "#40c4ff", + ["Input → brain output", "Pearson correlation score", "Used in Algonauts"]), + + ("Encoding Score", + "The accuracy metric for brain encoding models. Measured as Pearson correlation (r) between the model's predicted BOLD signal and the participant's actual fMRI signal. TRIBE v2 achieved rank #1 with mean score 0.2146 ± 0.0312.", + "#40c4ff", + ["Pearson correlation", "TRIBE: 0.2146 mean", "Higher = better prediction"]), + + ("Cortical Vertices / Parcellation", + "The brain's surface is divided into 20,484 measurement points (vertices). The HCP atlas groups these into 360 functional parcels (brain regions) including visual cortex V1–V7, face-body areas, language regions, and subcortical zones.", + "#40c4ff", + ["360 HCP parcels", "V1–V7 visual regions", "FaceBody, Language areas"]), + + ("Foundation Model", + "A large pre-trained AI model that can be applied to many downstream tasks without retraining from scratch. TRIBE v2 is a foundation model for neuroscience — one model predicts brain responses across all stimulus types and experimental conditions.", + "#40c4ff", + ["Pre-trained at scale", "Zero-shot generalisation", "One model, many tasks"]), + + ("ICA (Independent Component Analysis)", + "A statistical technique to separate a multi-dimensional signal into independent components. Used in the TRIBE v2 paper to reveal that the model learned neuroscientifically meaningful brain patterns without being explicitly trained to do so.", + "#40c4ff", + ["Unsupervised decomposition", "Reveals latent structure", "Validates TRIBE learning"]), + ], + + "Cognitive Science": [ + ("Attention Score", + "A 0–100 measure of a creative's ability to capture and hold visual attention. Driven by visual contrast (50% weight), face presence (up to +36 pts), and clutter penalty (based on object count). Calibrated against V1–V7 visual cortex encoding.", + "#00e676", + ["> 60 = High Capture", "30–60 = Moderate", "< 30 = Scroll-Past Risk"]), + + ("Memory Score", + "A 0–100 measure of how strongly a creative will be encoded into long-term memory. Based on composition simplicity (fewer objects = better) and dual-coding balance (text density 5–25% optimal). Maps to hippocampal encoding strength.", + "#00e676", + ["> 70 = Strong Signal", "40–70 = Moderate", "< 40 = Low Retention"]), + + ("Emotional Valence", + "The positive/negative emotional charge a creative produces in the viewer. Measured −1.0 (strongly aversive) to +1.0 (strongly rewarding). Driven by face presence, dominant colour HSV properties, and composition warmth.", + "#00e676", + ["+1.0 = Rewarding", "0.0 = Neutral", "−1.0 = Aversive"]), + + ("Cognitive Load", + "The total mental effort required to process a creative. Calculated as a composite of visual complexity (object count) and verbal load (text density). Low = effortless processing. High = working memory overload → brand message not retained.", + "#00e676", + ["Low = < 35 composite", "Medium = 35–60", "High = > 60 · −12 CPCi"]), + + ("Pre-attentive Processing", + "Brain processing that happens automatically before conscious attention, typically within 200ms. Features processed pre-attentively: luminance contrast, colour, motion, face presence, orientation. Ads that pass this filter don't 'ask' for attention.", + "#00e676", + ["< 200ms response", "Before conscious choice", "Contrast + face = strongest"]), + + ("Dual-Coding Theory", + "Allan Paivio's (1971) theory that the brain stores verbal information (text) and visual information (images) in separate memory systems. Content encoded in both systems is 2× more memorable. Source of the 5–25% text density guideline.", + "#00e676", + ["Paivio 1971", "Two memory channels", "5–25% text = sweet spot"]), + + ("Cognitive Load Theory", + "John Sweller's (1988) theory that working memory has a fixed capacity. Extraneous load (visual clutter, excess copy) consumes that capacity, leaving less room for germane load (brand message encoding). Basis for the object-count thresholds.", + "#00e676", + ["Sweller 1988", "7 ± 2 items limit", "Clutter = capacity waste"]), + + ("Russell Circumplex", + "James Russell's (1980) two-dimensional model of affect: Valence (pleasant/unpleasant) × Arousal (activated/deactivated). Emotional valence in this tool maps to the horizontal valence axis. Used to explain why colour and faces shift emotional response.", + "#00e676", + ["Russell 1980", "Valence + Arousal axes", "Predicts purchase intent"]), + + ("Miller's Law", + "George Miller's (1956) finding that short-term memory holds 7 ± 2 chunks simultaneously. In ad creative, each distinct visual object is a 'chunk'. Beyond 7 objects, viewers sample randomly — the brand message may not be what gets sampled.", + "#00e676", + ["Miller 1956", "7 ± 2 chunks", "Object threshold basis"]), + ], + + "Visual Detection": [ + ("Object Detection (Contour Analysis)", + "TRIBE pipeline step that counts distinct visual objects using Canny edge detection, dilation, and contour analysis. Threshold: a contour must cover ≥ 0.1% of total image area to count. Outputs object_count.", + "#ffb300", + ["OpenCV Canny", "0.1% area threshold", "Proxy for visual clutter"]), + + ("Face Detection (Haar Cascade)", + "OpenCV's Haar Cascade classifier detects frontal human faces. Settings: scaleFactor=1.1, minNeighbors=5, minSize=30×30px. Runs in ~50–200ms. Face presence is the single highest-return attention variable in ad creative.", + "#ffb300", + ["OpenCV Haar", "50–200ms", "+22 pts attention per face"]), + + ("Text Density (OCR)", + "Tesseract OCR in PSM 11 sparse-text mode detects all text blocks regardless of layout. Text density = sum of text bounding box areas ÷ total image area. Confidence threshold ≥ 40% to filter noise.", + "#ffb300", + ["Tesseract PSM 11", "Confidence ≥ 40%", "0.0–1.0 ratio"]), + + ("Contrast Score", + "Computed as the standard deviation of grayscale pixel values, normalised to 0–100 (max std = 127.5 for an 8-bit image). Low std = flat/washed out. High std = strong light-dark variation = pre-attentive pop-out.", + "#ffb300", + ["Std dev / 127.5 × 100", "0 = flat", "100 = max contrast"]), + + ("Dominant Colours (K-means)", + "K-means clustering (k=3) on a 100×100 downscaled image identifies the 3 most dominant colours by pixel count. Sorted by frequency. Converted to RGB hex strings. Used for valence calculation via HSV colour psychology.", + "#ffb300", + ["K-means k=3", "100×100 downsample", "Sorted by frequency"]), + + ("HSV Colour Space", + "Hue-Saturation-Value colour representation. Used for emotion classification instead of RGB because HSV separates colour identity (hue) from brightness (value) more naturally. Warm hues (0–60°) = positive valence. Dark (V < 0.20) = negative.", + "#ffb300", + ["Hue 0–360°", "Saturation 0–1", "Value 0–1 (brightness)"]), + ], + + "Media & Advertising": [ + ("CPCi (Creative Performance Composite Index)", + "A 0–100 weighted score combining Attention, Memory, and Emotional Valence, calibrated to the specific cognitive demands of a given use case. Higher CPCi = stronger cognitive impact = better predicted media efficiency.", + "#ff80ab", + ["0–100 scale", "Use-case weighted", "Predicts media ROI"]), + + ("Thumb-Stop Rate", + "The percentage of users who stop scrolling when they encounter an ad. Directly correlated with Attention Score. Low attention ads produce low thumb-stop rates, which the platform algorithm interprets as low quality, increasing effective CPM.", + "#ff80ab", + ["Attention proxy", "Algorithm quality signal", "Determines CPM cost"]), + + ("Brand Salience", + "How quickly and easily a brand comes to mind in buying situations. Built through consistent memory encoding over multiple exposures. High Memory Score creatives build salience faster and with fewer exposures required.", + "#ff80ab", + ["Byron Sharp concept", "Built via Memory Score", "Drives shelf recognition"]), + + ("Effective CPM", + "The true cost per 1,000 impressions accounting for placement quality. Low-attention creatives inflate effective CPM because the platform algorithm bids them into lower-quality inventory with lower engagement.", + "#ff80ab", + ["True cost metric", "Rises with low attention", "CPCi predicts direction"]), + + ("Brand Lift", + "An increase in brand awareness, preference, or purchase intent measured by surveys after campaign exposure. CPCi scores above 70 historically correlate with 3–5 percentage point brand lift per campaign.", + "#ff80ab", + ["Survey-measured", "CPCi > 70 = 3–5pt lift", "Gold standard KPI"]), + + ("Frequency Capping", + "Setting a maximum number of times a user sees the same ad. High Memory Score creatives need lower frequency (3–5 impressions) to achieve the same recall as low-memory creatives (6–9 impressions needed). Impacts media budget efficiency.", + "#ff80ab", + ["3–5 for high memory", "6–9 for low memory", "Controls wear-out"]), + + ("Cognitive Load Penalty", + "A CPCi deduction applied in Retail Media contexts only: −0 for Low load, −5 for Medium, −12 for High. Applied because retail environments already impose high cognitive load on shoppers — an ad that adds to it will not convert.", + "#ff80ab", + ["Retail Media only", "−0 / −5 / −12", "Reflects real-world clutter"]), + + ("Quality Score", + "The ad quality metric used by platforms (Google, Meta) that determines ad auction ranking and CPM. Creatives with high engagement (driven by attention and positive emotion) receive better Quality Scores, lowering cost per click.", + "#ff80ab", + ["Platform auction metric", "Attention → higher score", "Lowers CPC directly"]), + ], +} + + +def show_glossary_tab() -> None: + st.markdown(SCI_CSS, unsafe_allow_html=True) + + st.markdown( + "
    " + "📖 Glossary of Terms
    " + "
    " + "Every technical term used in the reports, scores, and methodology — explained for marketers.
    ", + unsafe_allow_html=True, + ) + + for category, terms in GLOSSARY.items(): + with st.expander(f"{'🔬' if 'Neuro' in category else '🧩' if 'Cognitive' in category else '👁' if 'Visual' in category else '📢'} {category} — {len(terms)} terms", expanded=False): + cols = st.columns(2) + for idx, (term, definition, color, tags) in enumerate(terms): + tag_html = " ".join(f"{t}" for t in tags) + cols[idx % 2].markdown( + f"
    " + f"
    {term}
    " + f"
    {definition}
    " + f"
    {tag_html}
    " + f"
    ", + unsafe_allow_html=True, + ) + + # Quick-reference cheat sheet + st.markdown("
    ", + unsafe_allow_html=True) + st.markdown( + "
    ⚡ Quick-Reference Cheat Sheet
    ", + unsafe_allow_html=True, + ) + + qs_rows = [ + ("You see...", "It means...", "Action"), + ("Attention < 30", "Ad will be ignored before consciously seen", "Add face or boost contrast first"), + ("Memory < 40", "Brand won't be remembered after exposure", "Add tagline or simplify layout"), + ("Valence < −0.1", "Subtle aversion — silently kills brand love", "Warm up palette or add a face"), + ("Cognitive Load = High", "Working memory saturated — message lost", "Remove objects, cut copy"), + ("CPCi < 40", "Do not scale — fix creative first", "A/B test before spending"), + ("CPCi 40–70", "Potential there, but limited", "Identify weakest signal and fix it"), + ("CPCi > 70", "Ready to scale — strong cognitive impact", "Increase budget 20–30%"), + ("Text density > 25%", "Verbal channel drowning visual", "Cut copy to one key message"), + ("Text density < 5%", "No verbal anchor — recall will fade", "Add 5–10 word tagline"), + ("Faces = 0", "Missing fastest attention + emotion driver", "Add human face to primary zone"), + ("Contrast < 45", "Won't pop in feed — washed out pre-attention","Increase contrast in post"), + ] + + header, *rows = qs_rows + header_html = ( + "
    " + + "".join( + f"
    {h}
    " + for h in header + ) + + "
    " + ) + st.markdown(header_html, unsafe_allow_html=True) + + rows_html = "
    " + for i, (signal, meaning, action) in enumerate(rows): + bg = "#060e1a" if i % 2 == 0 else "#080f1c" + rows_html += ( + f"
    " + f"
    {signal}
    " + f"
    {meaning}
    " + f"
    {action}
    " + f"
    " + ) + rows_html += "
    " + st.markdown(rows_html, unsafe_allow_html=True)