""" Dashboard — Creator-Friendly Ad Placement Recommender (Premium UI) """ import os os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" import streamlit as st import json import math import tempfile import pandas as pd import plotly.graph_objects as go import base64 from PIL import Image from youtube_auth import get_credentials, show_login_button, logout # ── Page Config ── st.set_page_config( page_title="Ad Placement Recommender", page_icon="▷", layout="wide" ) # ═══════════════════════════════════════════════════════════════ # DESIGN SYSTEM — CSS # ═══════════════════════════════════════════════════════════════ st.markdown(""" """, unsafe_allow_html=True) # ═══════════════════════════════════════════════════════════════ # HELPERS # ═══════════════════════════════════════════════════════════════ def get_spot_quality(retention): if retention >= 50: return "✦", "Great Spot", "placement-card", "badge-green" elif retention >= 30: return "◆", "Decent Spot", "placement-card-warn", "badge-yellow" else: return "▼", "Weak Spot", "placement-card-bad", "badge-red" def get_type_label(t): return { "scene_change": "🎬 Scene Break", "silence": "🔇 Quiet Moment", "transcript_boundary": "🗣️ Topic Change" }.get(t, t) def get_type_tip(t): return { "scene_change": "The video visually transitions here — viewers naturally expect a brief pause. Perfect for a sponsorship read.", "silence": "There's a quiet gap in audio here — inserting an ad won't feel jarring or cut off speech.", "transcript_boundary": "The speaker shifts to a new topic — a natural mental break for the viewer." }.get(t, "Natural break detected in the video.") def extract_video_id(url): import re for pattern in [r"youtu\.be/([^?&]+)", r"youtube\.com/watch\?v=([^&]+)"]: m = re.search(pattern, url) if m: return m.group(1) if re.match(r'^[A-Za-z0-9_-]{11}$', url.strip()): return url.strip() return None def load_json(path): if not os.path.exists(path): return None with open(path) as f: return json.load(f) def build_retention_ring(percent, color, size=76): radius = 30 circumference = 2 * math.pi * radius offset = circumference - (percent / 100) * circumference return f""" {percent:.0f}% """ def get_favicon_base64(): try: if os.path.exists("favicon.png"): with open("favicon.png", "rb") as f: return base64.b64encode(f.read()).decode() except Exception: pass return None # ═══════════════════════════════════════════════════════════════ # HERO HEADER # ═══════════════════════════════════════════════════════════════ favicon_b64 = get_favicon_base64() icon_html = f'' if favicon_b64 else "[ ▶︎ ]" st.markdown(f"""
{icon_html} Ad Placement Recommender
Find the perfect moments in your video to place ads — so viewers stay happy and you earn more.
""", unsafe_allow_html=True) # ═══════════════════════════════════════════════════════════════ # AUTH GATE # ═══════════════════════════════════════════════════════════════ creds = get_credentials() if creds is None: st.markdown('
', unsafe_allow_html=True) col1, col2, col3 = st.columns([1, 2, 1]) with col2: auth_url = show_login_button() st.markdown(f"""
ᯓ➤
Connect Your YouTube Account
We need read-only access to your YouTube Analytics to see where viewers drop off in your videos.
🔒 Read-Only Access 🛡️ 256-bit Encrypted 🚫 No Data Stored
▶   Sign in with YouTube
✓   We never post, modify, or delete anything
""", unsafe_allow_html=True) st.stop() # ═══════════════════════════════════════════════════════════════ # SIDEBAR # ═══════════════════════════════════════════════════════════════ with st.sidebar: st.markdown("### 👤 Account") st.markdown(""" """, unsafe_allow_html=True) if st.button("🚪 Logout"): logout() st.rerun() st.markdown('
', unsafe_allow_html=True) st.markdown("### ℹ️ How It Works") steps = [ ("1", "ᯓ➤ Paste YouTube URL (optional for unpublished)"), ("2", "🎬 Upload your video as .mp4"), ("3", "🚀 Click Analyze"), ("4", "📍 See exactly where to place ads"), ] for num, text in steps: st.markdown(f""" """, unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) st.markdown( '
' 'Ad Placement Recommender
Built for YouTube Creators
', unsafe_allow_html=True ) # ═══════════════════════════════════════════════════════════════ # INPUT FORM # ═══════════════════════════════════════════════════════════════ st.markdown('
📥 Analyze Your Video
', unsafe_allow_html=True) col1, col2 = st.columns([1, 1]) with col1: yt_input = st.text_input( "[ ▶︎ ] YouTube Video URL (optional for unpublished videos)", placeholder="https://youtu.be/4Rq-LY16WxM", help="Leave blank if video is not yet published on YouTube" ) with col2: uploaded_file = st.file_uploader( "🎬 Upload Your Video File (.mp4)", type=["mp4"], help="Upload your video file — required for analysis" ) analyze_btn = st.button("🚀 Analyze My Video & Find Best Ad Spots") if analyze_btn: # ── Validate: video file is always required ── if not uploaded_file: st.warning("⚠️ Please upload your video file (.mp4) to continue.") else: # ── Determine pipeline mode ── use_offline = not yt_input.strip() video_id = extract_video_id(yt_input.strip()) if yt_input.strip() else None if yt_input.strip() and not video_id: st.error("❌ Could not extract video ID. Please check your YouTube URL.") else: # Save uploaded file to temp path os.makedirs("test_video", exist_ok=True) save_path = os.path.join("test_video", uploaded_file.name) with open(save_path, "wb") as f: f.write(uploaded_file.getbuffer()) # Loading UI loading_container = st.empty() with loading_container.container(): if use_offline: st.markdown("""
[ ▶︎ ]
Predicting Ad Spots...
Analyzing audio energy to simulate viewer retention
[ ▶︎ ] Pre-Publish Prediction Mode
""", unsafe_allow_html=True) else: st.markdown("""
[ ▶︎ ]
Analyzing Video Content...
Processing frames and fetching YouTube retention data
""", unsafe_allow_html=True) progress_bar = st.progress(0) steps_done = [0] def on_progress(msg): steps_done[0] += 1 progress_bar.progress(min(steps_done[0] / 4, 1.0)) try: if use_offline: # ── Unpublished video — audio-based prediction ── from pipeline import run_full_pipeline_offline run_full_pipeline_offline(save_path, progress_callback=on_progress) st.session_state["prediction_mode"] = True else: # ── Published video — full YouTube Analytics pipeline ── from pipeline import run_full_pipeline run_full_pipeline(save_path, video_id, creds, progress_callback=on_progress) st.session_state["prediction_mode"] = False progress_bar.progress(1.0) loading_container.empty() st.session_state["analysis_done"] = True st.rerun() except Exception as e: loading_container.empty() st.error(f"❌ Pipeline error: {str(e)}") if not use_offline: st.info("💡 Make sure your video is public or unlisted and belongs to your connected YouTube channel.") st.markdown('
', unsafe_allow_html=True) # ═══════════════════════════════════════════════════════════════ # LOAD RESULTS # ═══════════════════════════════════════════════════════════════ if not st.session_state.get("analysis_done", False): data = None else: data = load_json("final_recommendations.json") candidates = load_json("ranked_candidates.json") if st.session_state.get("analysis_done") else None retention = load_json("retention_curve.json") if st.session_state.get("analysis_done") else None if not data: st.markdown("""
📹
Ready to analyze your video
Upload your video above — paste a YouTube URL for published videos, or leave it blank to predict spots for an unpublished video.
""", unsafe_allow_html=True) st.stop() recs = data.get("recommendations", []) duration = data.get("video_duration_formatted", "N/A") total_placed = data.get("total_placements_recommended", 0) cands_list = candidates.get("ranked_placements", []) if candidates else [] top_ret = max((c["retention_at_t"] for c in cands_list), default=0) is_predict = st.session_state.get("prediction_mode", False) # ═══════════════════════════════════════════════════════════════ # PREDICTION MODE BANNER # ═══════════════════════════════════════════════════════════════ if is_predict: st.markdown("""
[ ▶︎ ] Pre-Publish Prediction Mode
No YouTube data available yet — ad spots are predicted using audio energy analysis. Accuracy improves once you publish and re-analyze with real viewer retention data.
""", unsafe_allow_html=True) # ═══════════════════════════════════════════════════════════════ # METRICS # ═══════════════════════════════════════════════════════════════ metrics_data = [ ("🎬", "Video Duration", duration, "#e2e8f0", "rgba(99,102,241,0.15)"), ("📍", "Ad Spots Found", str(total_placed), "#10b981" if total_placed > 0 else "#ef4444", "rgba(16,185,129,0.15)" if total_placed > 0 else "rgba(239,68,68,0.15)"), ("🔬", "Moments Analyzed", str(len(cands_list)), "#e2e8f0", "rgba(99,102,241,0.15)"), ("👀", "Best Retention", f"{top_ret:.0f}%", "#10b981" if top_ret >= 50 else "#f59e0b", "rgba(16,185,129,0.15)" if top_ret >= 50 else "rgba(245,158,11,0.15)"), ] m1, m2, m3, m4 = st.columns(4) for i, (col, (icon, label, value, color, icon_bg)) in enumerate(zip([m1, m2, m3, m4], metrics_data)): with col: st.markdown(f"""
{icon}
{label}
{value}
""", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) # ═══════════════════════════════════════════════════════════════ # RETENTION CURVE # ═══════════════════════════════════════════════════════════════ st.markdown('
📈 Viewer Retention Curve
', unsafe_allow_html=True) if is_predict: st.caption("Simulated retention curve based on audio energy analysis. Markers show predicted ad spots.") else: st.caption("How many viewers are still watching at each moment. Markers show recommended ad spots.") if retention: times = [p["time_seconds"] for p in retention] values = [p["retention_percent"] for p in retention] fig = go.Figure() fig.add_trace(go.Scatter( x=times, y=values, mode="lines", name="Viewer Retention", line=dict(color="#6366f1", width=3, shape="spline", smoothing=1.2), fill="tozeroy", fillgradient=dict( type="vertical", colorscale=[[0.0, "rgba(99,102,241,0.0)"], [1.0, "rgba(99,102,241,0.2)"]] ), hovertemplate="%{x:.0f}s
Retention: %{y:.1f}%" )) spot_colors = ["#10b981", "#f59e0b", "#ef4444"] for i, r in enumerate(recs): color = spot_colors[i % len(spot_colors)] ts = r["timestamp_seconds"] ret_val = r.get("retention_at_t", 50) fig.add_trace(go.Scatter( x=[ts], y=[ret_val], mode="markers+text", marker=dict(symbol="diamond", size=14, color=color, line=dict(width=2, color="white")), text=[f"Ad {r['placement_number']}"], textposition="top center", textfont=dict(color=color, size=12, family="Inter"), hovertemplate=f"Ad Spot {r['placement_number']}
{r['timestamp_formatted']}
Retention: {ret_val:.1f}%", showlegend=False )) fig.add_vline(x=ts, line_dash="dot", line_color=color, line_width=1.5, opacity=0.5) fig.update_layout( plot_bgcolor="rgba(0,0,0,0)", paper_bgcolor="rgba(0,0,0,0)", font=dict(color="#94a3b8", family="Inter"), height=400, xaxis=dict(title="Time (seconds)", gridcolor="rgba(255,255,255,0.04)", zeroline=False, tickfont=dict(size=11)), yaxis=dict(title="Viewers Still Watching (%)", gridcolor="rgba(255,255,255,0.04)", range=[0, 105], zeroline=False, tickfont=dict(size=11)), margin=dict(l=10, r=10, t=20, b=40), legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1, bgcolor="rgba(0,0,0,0)"), hoverlabel=dict(bgcolor="#161c2d", bordercolor="#6366f1", font_size=13, font_family="Inter") ) st.plotly_chart(fig, width='stretch') st.markdown('
', unsafe_allow_html=True) # ═══════════════════════════════════════════════════════════════ # RECOMMENDATIONS # ═══════════════════════════════════════════════════════════════ st.markdown( f'
{"[ ▶︎ ] Predicted" if is_predict else "🎯"} Where to Place Your Ads
', unsafe_allow_html=True ) if not recs: st.markdown("""
😕
No strong ad spots found for this video
This usually means viewer retention dropped too quickly.
Try a video where viewers watch past the halfway point.
""", unsafe_allow_html=True) else: for idx, r in enumerate(recs): emoji, quality, card_class, badge_class = get_spot_quality(r["retention_at_t"]) type_label = get_type_label(r["type"]) type_tip = get_type_tip(r["type"]) ret = r["retention_at_t"] conf = r["confidence"] conf_color = "#10b981" if conf == "HIGH" else "#f59e0b" if conf == "MEDIUM" else "#94a3b8" ring_color = "#10b981" if ret >= 50 else "#f59e0b" if ret >= 30 else "#ef4444" ring_svg = build_retention_ring(ret, ring_color) is_predict = st.session_state.get("prediction_mode", False) predict_note = "(predicted)" if is_predict else "" html = ( f'
' f'
' f'
' f'📍 Ad Spot {r["placement_number"]}' f'  ' f'{r["timestamp_formatted"]}' f' {predict_note}' f'
' f'{emoji} {quality}' f'
' f'
' f'
' f'
' f'
VIEWERS WATCHING
' f'
{ret:.0f}%
' f'
' f'
' f'
BREAK TYPE
' f'
{type_label}
' f'
' f'
' f'
CONFIDENCE
' f'
{conf}
' f'
' f'
' f'
{ring_svg}
' f'
' f'
' f'Why this spot? {type_tip}

' f'What to do: Place your sponsorship or enable mid-roll at {r["timestamp_formatted"]}. ' f'At this moment, {ret:.0f}% of your audience is still watching.' f'
' f'
' ) st.markdown(html, unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) # ═══════════════════════════════════════════════════════════════ # INSIGHTS FOR NEXT VIDEO # ═══════════════════════════════════════════════════════════════ st.markdown('
📊 Insights for Your Next Video
', unsafe_allow_html=True) if cands_list: df = pd.DataFrame(cands_list) best_ret = df["retention_at_t"].max() worst_ret = df["retention_at_t"].min() avg_ret = df["retention_at_t"].mean() drop_time = df.loc[df["retention_at_t"].idxmin(), "timestamp_formatted"] insight_metrics = [ ("🏆", "Peak Retention", f"{best_ret:.0f}%", "#10b981", "rgba(16,185,129,0.15)", "viewers at best moment"), ("📉", "Biggest Drop-Off", drop_time, "#ef4444", "rgba(239,68,68,0.15)", "most viewers left here"), ("📊", "Avg Retention", f"{avg_ret:.0f}%", "#f59e0b", "rgba(245,158,11,0.15)", "across all break points"), ] i1, i2, i3 = st.columns(3) for i, (col, (icon, label, value, color, icon_bg, sub)) in enumerate(zip([i1, i2, i3], insight_metrics)): with col: st.markdown(f"""
{icon}
{label}
{value}
{sub}
""", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) st.markdown('
💡 What This Means for Your Next Video
', unsafe_allow_html=True) tips = [] if best_ret >= 60: tips.append(("✅", "Strong early retention — your intro hooks viewers well. Keep doing what you did in the first 2 minutes.")) if worst_ret < 25: tips.append(("⚠️", f"Viewers drop off near {drop_time} — tighten that section or add a re-engagement hook.")) if avg_ret < 35: tips.append(("📉", "Overall retention is low — try shorter videos or stronger pacing.")) if avg_ret >= 50: tips.append(("🎉", "Great overall retention — you can confidently place ads and expect good visibility.")) if total_placed == 0: tips.append(("🔴", "No strong ad spots this time — aim to keep 40%+ viewers watching past the 2-minute mark.")) if is_predict: tips.append(("[ ▶︎ ]", "This was a predicted analysis. Once published, re-analyze with your YouTube URL for real retention data and more accurate spots.")) for icon, text in tips: st.markdown(f"""
{icon}
{text}
""", unsafe_allow_html=True) # ═══════════════════════════════════════════════════════════════ # ADVANCED TABLE # ═══════════════════════════════════════════════════════════════ st.markdown('
', unsafe_allow_html=True) with st.expander("🔬 View All Analyzed Moments (Advanced)"): if cands_list: df_show = pd.DataFrame(cands_list)[["timestamp_formatted", "type", "retention_at_t", "placement_score"]] df_show.columns = ["Timestamp", "Break Type", "Viewers Watching (%)", "ML Score"] df_show["Break Type"] = df_show["Break Type"].map(get_type_label) df_show["Viewers Watching (%)"] = df_show["Viewers Watching (%)"].map(lambda x: f"{x:.1f}%") df_show["ML Score"] = df_show["ML Score"].map(lambda x: f"{x:.4f}") st.dataframe(df_show, width='stretch', hide_index=True) # ═══════════════════════════════════════════════════════════════ # PAST ANALYSES (CHANNEL HISTORY) # ═══════════════════════════════════════════════════════════════ history_data = load_json("channel_history.json") if history_data: st.markdown('
', unsafe_allow_html=True) with st.expander("📚 Past Analyses (Channel History)"): st.markdown(""" ### Why It's Important Right now, your app analyzes each video in isolation — it has no memory. Every analysis starts fresh with zero knowledge of your channel's patterns. History storage gives the system memory and intelligence over time. **Without History (Now)** * Video 1 analyzed → recommendations → forgotten ❌ * Video 2 analyzed → recommendations → forgotten ❌ * *Each video treated like the first video ever* **With History (After)** * Video 1 analyzed → saved to history ✅ * Video 2 analyzed → compared to Video 1 → better prediction ✅ * Video 10 analyzed → compared to 9 past videos → highly accurate ✅ * *System gets SMARTER with every video you analyze*

""", unsafe_allow_html=True) # Reverse the list so newest is on top history_data = history_data[::-1] df_history = pd.DataFrame(history_data) df_history = df_history[["timestamp", "video_path", "duration", "placements", "is_prediction"]] df_history.columns = ["Date", "Video", "Duration", "Spots Found", "Predicted?"] df_history["Predicted?"] = df_history["Predicted?"].apply(lambda x: "[ ▶︎ ] Yes" if x else "✅ No") st.dataframe(df_history, width='stretch', hide_index=True) # ═══════════════════════════════════════════════════════════════ # FOOTER # ═══════════════════════════════════════════════════════════════ st.markdown("""
""", unsafe_allow_html=True)