Kosala Nayanajith Deshapriya commited on
Commit
d4ea2d4
Β·
1 Parent(s): 4dc7a21
Files changed (2) hide show
  1. dashboard.py +37 -49
  2. youtube_auth.py +22 -15
dashboard.py CHANGED
@@ -6,9 +6,9 @@ Full OAuth login + integrated pipeline (no subprocess)
6
  import streamlit as st
7
  import json
8
  import os
9
- import tempfile
10
  import pandas as pd
11
  import plotly.graph_objects as go
 
12
 
13
  # ── Page Config ──
14
  st.set_page_config(
@@ -66,16 +66,19 @@ st.markdown("""
66
  </style>
67
  """, unsafe_allow_html=True)
68
 
 
 
 
 
69
 
70
  # ── Helpers ──
71
-
72
  def get_spot_quality(retention):
73
  if retention >= 50:
74
- return "🟒", "Great Spot", "placement-card", "badge-green"
75
  elif retention >= 30:
76
- return "🟑", "Decent Spot", "placement-card-warn", "badge-yellow"
77
  else:
78
- return "πŸ”΄", "Weak Spot", "placement-card-bad", "badge-red"
79
 
80
  def get_type_label(t):
81
  return {
@@ -107,40 +110,36 @@ def load_json(path):
107
  with open(path) as f:
108
  return json.load(f)
109
 
110
-
111
  # ══════════════════════════════════════════════
112
  # HEADER
113
  # ══════════════════════════════════════════════
114
  st.markdown('<div class="big-title">🎯 Ad Placement Recommender</div>', unsafe_allow_html=True)
115
  st.markdown('<div class="subtitle">Find the perfect moments in your video to place ads β€” so viewers stay happy and you earn more.</div>', unsafe_allow_html=True)
116
 
117
-
118
  # ══════════════════════════════════════════════
119
  # AUTH GATE
120
  # ══════════════════════════════════════════════
121
- from youtube_auth import get_credentials, show_login_button, logout
122
-
123
  creds = get_credentials()
124
 
125
  if creds is None:
126
  st.markdown("---")
127
- st.markdown("""
128
- <div class="login-card">
129
- <div style="font-size:3rem;">πŸ”</div>
130
- <div style="font-size:1.3rem; font-weight:700; color:#fff; margin:1rem 0;">
131
- Connect Your YouTube Account
132
- </div>
133
- <div style="color:#9ca3af; font-size:0.9rem; margin-bottom:1.5rem;">
134
- We need read-only access to your YouTube Analytics<br>
135
- to see where viewers drop off in your videos.<br><br>
136
- <b style="color:#6ee7b7;">We never post, modify or delete anything.</b>
137
- </div>
138
- </div>
139
- """, unsafe_allow_html=True)
140
-
141
- auth_url = show_login_button()
142
  col1, col2, col3 = st.columns([1, 2, 1])
143
  with col2:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  st.markdown(f"""
145
  <a href="{auth_url}" target="_self" style="text-decoration:none;">
146
  <div style="
@@ -155,7 +154,7 @@ if creds is None:
155
  """, unsafe_allow_html=True)
156
  st.stop()
157
 
158
- # ── Logged in β€” show logout in sidebar ──
159
  with st.sidebar:
160
  st.markdown("### πŸ‘€ Account")
161
  st.success("βœ… YouTube Connected")
@@ -171,7 +170,6 @@ with st.sidebar:
171
  4. πŸ“ See exactly where to place ads
172
  """)
173
 
174
-
175
  # ══════════════════════════════════════════════
176
  # INPUT FORM
177
  # ══════════════════════════════════════════════
@@ -201,13 +199,11 @@ if analyze_btn:
201
  if not video_id:
202
  st.error("❌ Could not extract video ID. Please check your YouTube URL.")
203
  else:
204
- # Save uploaded video to temp file
205
  os.makedirs("test_video", exist_ok=True)
206
  save_path = os.path.join("test_video", uploaded_file.name)
207
  with open(save_path, "wb") as f:
208
  f.write(uploaded_file.getbuffer())
209
 
210
- # Run pipeline with live progress
211
  from pipeline import run_full_pipeline
212
 
213
  progress_box = st.empty()
@@ -231,7 +227,6 @@ if analyze_btn:
231
 
232
  st.markdown("---")
233
 
234
-
235
  # ══════════════════════════════════════════════
236
  # LOAD RESULTS
237
  # ══════════════════════════════════════════════
@@ -249,18 +244,16 @@ total_placed = data.get("total_placements_recommended", 0)
249
  cands_list = candidates.get("ranked_placements", []) if candidates else []
250
  top_ret = max((c["retention_at_t"] for c in cands_list), default=0)
251
 
252
-
253
  # ══════════════════════════════════════════════
254
  # METRICS
255
  # ══════════════════════════════════════════════
256
  m1, m2, m3, m4 = st.columns(4)
257
- metrics = [
258
- ("🎬 Video Duration", duration, "#ffffff"),
259
- ("πŸ“ Best Ad Spots Found", total_placed, "#22c55e" if total_placed > 0 else "#ef4444"),
260
- ("πŸ”¬ Moments Analyzed", len(cands_list), "#ffffff"),
261
- ("πŸ‘€ Best Spot Retention", f"{top_ret:.0f}%", "#22c55e" if top_ret >= 50 else "#f59e0b"),
262
- ]
263
- for col, (label, value, color) in zip([m1, m2, m3, m4], metrics):
264
  with col:
265
  st.markdown(f"""
266
  <div class="metric-box">
@@ -270,15 +263,14 @@ for col, (label, value, color) in zip([m1, m2, m3, m4], metrics):
270
 
271
  st.markdown("<br>", unsafe_allow_html=True)
272
 
273
-
274
  # ══════════════════════════════════════════════
275
  # RETENTION CURVE
276
  # ══════════════════════════════════════════════
277
  st.markdown('<div class="section-header">πŸ“ˆ Your Viewer Retention Curve</div>', unsafe_allow_html=True)
278
- st.caption("This shows how many viewers are still watching at each moment. The markers show your recommended ad spots.")
279
 
280
  if retention:
281
- times = [p["time_seconds"] for p in retention]
282
  values = [p["retention_percent"] for p in retention]
283
 
284
  fig = go.Figure()
@@ -310,7 +302,6 @@ if retention:
310
 
311
  st.markdown("---")
312
 
313
-
314
  # ══════════════════════════════════════════════
315
  # RECOMMENDATIONS
316
  # ══════════════════════════════════════════════
@@ -369,7 +360,6 @@ else:
369
 
370
  st.markdown("---")
371
 
372
-
373
  # ══════════════════════════════════════════════
374
  # INSIGHTS FOR NEXT VIDEO
375
  # ══════════════════════════════════════════════
@@ -384,8 +374,8 @@ if cands_list:
384
 
385
  i1, i2, i3 = st.columns(3)
386
  for col, label, value, color, sub in [
387
- (i1, "πŸ† Peak Retention Spot", f"{best_ret:.0f}%", "#22c55e", "viewers at best moment"),
388
- (i2, "πŸ“‰ Biggest Drop-Off At", drop_time, "#ef4444", "most viewers left here"),
389
  (i3, "πŸ“Š Avg Retention at Breaks", f"{avg_ret:.0f}%", "#f59e0b", "across all detected moments"),
390
  ]:
391
  with col:
@@ -403,18 +393,17 @@ if cands_list:
403
  if best_ret >= 60:
404
  tips.append("βœ… **Strong early retention** β€” your intro hooks viewers well. Keep doing what you did in the first 2 minutes.")
405
  if worst_ret < 25:
406
- tips.append(f"⚠️ **Viewers drop off near {drop_time}** β€” tighten that section or add a re-engagement hook (a question, teaser, or visual change).")
407
  if avg_ret < 35:
408
- tips.append("πŸ“‰ **Overall retention is low** β€” try shorter videos or stronger pacing to keep viewers engaged longer.")
409
  if avg_ret >= 50:
410
- tips.append("πŸŽ‰ **Great overall retention** β€” your audience is engaged. You can confidently place ads and expect good visibility.")
411
  if total_placed == 0:
412
  tips.append("πŸ”΄ **No strong ad spots this time** β€” aim to keep 40%+ viewers watching past the 2-minute mark.")
413
 
414
  for tip in tips:
415
  st.markdown(f"- {tip}")
416
 
417
-
418
  # ══════════════════════════════════════════════
419
  # ADVANCED TABLE
420
  # ══════════════════════════════════════════════
@@ -428,7 +417,6 @@ with st.expander("πŸ”¬ View All Analyzed Moments (Advanced)"):
428
  df_show["ML Score"] = df_show["ML Score"].map(lambda x: f"{x:.4f}")
429
  st.dataframe(df_show, use_container_width=True, hide_index=True)
430
 
431
-
432
  # ── Footer ──
433
  st.markdown("<br>", unsafe_allow_html=True)
434
  st.markdown(
 
6
  import streamlit as st
7
  import json
8
  import os
 
9
  import pandas as pd
10
  import plotly.graph_objects as go
11
+ from youtube_auth import get_credentials, show_login_button, logout
12
 
13
  # ── Page Config ──
14
  st.set_page_config(
 
66
  </style>
67
  """, unsafe_allow_html=True)
68
 
69
+ # ── Handle post-login rerun FIRST (before anything else) ──
70
+ if "just_logged_in" in st.session_state:
71
+ del st.session_state["just_logged_in"]
72
+ st.rerun()
73
 
74
  # ── Helpers ──
 
75
  def get_spot_quality(retention):
76
  if retention >= 50:
77
+ return "🟒", "Great Spot", "placement-card", "badge-green"
78
  elif retention >= 30:
79
+ return "🟑", "Decent Spot", "placement-card-warn", "badge-yellow"
80
  else:
81
+ return "πŸ”΄", "Weak Spot", "placement-card-bad", "badge-red"
82
 
83
  def get_type_label(t):
84
  return {
 
110
  with open(path) as f:
111
  return json.load(f)
112
 
 
113
  # ══════════════════════════════════════════════
114
  # HEADER
115
  # ══════════════════════════════════════════════
116
  st.markdown('<div class="big-title">🎯 Ad Placement Recommender</div>', unsafe_allow_html=True)
117
  st.markdown('<div class="subtitle">Find the perfect moments in your video to place ads β€” so viewers stay happy and you earn more.</div>', unsafe_allow_html=True)
118
 
 
119
  # ══════════════════════════════════════════════
120
  # AUTH GATE
121
  # ══════════════════════════════════════════════
 
 
122
  creds = get_credentials()
123
 
124
  if creds is None:
125
  st.markdown("---")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  col1, col2, col3 = st.columns([1, 2, 1])
127
  with col2:
128
+ st.markdown("""
129
+ <div class="login-card">
130
+ <div style="font-size:3rem;">πŸ”</div>
131
+ <div style="font-size:1.3rem;font-weight:700;color:#fff;margin:1rem 0;">
132
+ Connect Your YouTube Account
133
+ </div>
134
+ <div style="color:#9ca3af;font-size:0.9rem;margin-bottom:1.5rem;">
135
+ We need read-only access to your YouTube Analytics<br>
136
+ to see where viewers drop off in your videos.<br><br>
137
+ <b style="color:#6ee7b7;">We never post, modify or delete anything.</b>
138
+ </div>
139
+ </div>
140
+ """, unsafe_allow_html=True)
141
+
142
+ auth_url = show_login_button()
143
  st.markdown(f"""
144
  <a href="{auth_url}" target="_self" style="text-decoration:none;">
145
  <div style="
 
154
  """, unsafe_allow_html=True)
155
  st.stop()
156
 
157
+ # ── Sidebar (logged in) ──
158
  with st.sidebar:
159
  st.markdown("### πŸ‘€ Account")
160
  st.success("βœ… YouTube Connected")
 
170
  4. πŸ“ See exactly where to place ads
171
  """)
172
 
 
173
  # ══════════════════════════════════════════════
174
  # INPUT FORM
175
  # ══════════════════════════════════════════════
 
199
  if not video_id:
200
  st.error("❌ Could not extract video ID. Please check your YouTube URL.")
201
  else:
 
202
  os.makedirs("test_video", exist_ok=True)
203
  save_path = os.path.join("test_video", uploaded_file.name)
204
  with open(save_path, "wb") as f:
205
  f.write(uploaded_file.getbuffer())
206
 
 
207
  from pipeline import run_full_pipeline
208
 
209
  progress_box = st.empty()
 
227
 
228
  st.markdown("---")
229
 
 
230
  # ══════════════════════════════════════════════
231
  # LOAD RESULTS
232
  # ══════════════════════════════════════════════
 
244
  cands_list = candidates.get("ranked_placements", []) if candidates else []
245
  top_ret = max((c["retention_at_t"] for c in cands_list), default=0)
246
 
 
247
  # ══════════════════════════════════════════════
248
  # METRICS
249
  # ══════════════════════════════════════════════
250
  m1, m2, m3, m4 = st.columns(4)
251
+ for col, label, value, color in [
252
+ (m1, "🎬 Video Duration", duration, "#ffffff"),
253
+ (m2, "πŸ“ Best Ad Spots Found", total_placed, "#22c55e" if total_placed > 0 else "#ef4444"),
254
+ (m3, "πŸ”¬ Moments Analyzed", len(cands_list), "#ffffff"),
255
+ (m4, "πŸ‘€ Best Spot Retention", f"{top_ret:.0f}%", "#22c55e" if top_ret >= 50 else "#f59e0b"),
256
+ ]:
 
257
  with col:
258
  st.markdown(f"""
259
  <div class="metric-box">
 
263
 
264
  st.markdown("<br>", unsafe_allow_html=True)
265
 
 
266
  # ══════════════════════════════════════════════
267
  # RETENTION CURVE
268
  # ══════════════════════════════════════════════
269
  st.markdown('<div class="section-header">πŸ“ˆ Your Viewer Retention Curve</div>', unsafe_allow_html=True)
270
+ st.caption("This shows how many viewers are still watching at each moment. Markers show recommended ad spots.")
271
 
272
  if retention:
273
+ times = [p["time_seconds"] for p in retention]
274
  values = [p["retention_percent"] for p in retention]
275
 
276
  fig = go.Figure()
 
302
 
303
  st.markdown("---")
304
 
 
305
  # ══════════════════════════════════════════════
306
  # RECOMMENDATIONS
307
  # ══════════════════════════════════════════════
 
360
 
361
  st.markdown("---")
362
 
 
363
  # ══════════════════════════════════════════════
364
  # INSIGHTS FOR NEXT VIDEO
365
  # ══════════════════════════════════════════════
 
374
 
375
  i1, i2, i3 = st.columns(3)
376
  for col, label, value, color, sub in [
377
+ (i1, "πŸ† Peak Retention Spot", f"{best_ret:.0f}%", "#22c55e", "viewers at best moment"),
378
+ (i2, "πŸ“‰ Biggest Drop-Off At", drop_time, "#ef4444", "most viewers left here"),
379
  (i3, "πŸ“Š Avg Retention at Breaks", f"{avg_ret:.0f}%", "#f59e0b", "across all detected moments"),
380
  ]:
381
  with col:
 
393
  if best_ret >= 60:
394
  tips.append("βœ… **Strong early retention** β€” your intro hooks viewers well. Keep doing what you did in the first 2 minutes.")
395
  if worst_ret < 25:
396
+ tips.append(f"⚠️ **Viewers drop off near {drop_time}** β€” tighten that section or add a re-engagement hook.")
397
  if avg_ret < 35:
398
+ tips.append("πŸ“‰ **Overall retention is low** β€” try shorter videos or stronger pacing.")
399
  if avg_ret >= 50:
400
+ tips.append("πŸŽ‰ **Great overall retention** β€” you can confidently place ads and expect good visibility.")
401
  if total_placed == 0:
402
  tips.append("πŸ”΄ **No strong ad spots this time** β€” aim to keep 40%+ viewers watching past the 2-minute mark.")
403
 
404
  for tip in tips:
405
  st.markdown(f"- {tip}")
406
 
 
407
  # ══════════════════════════════════════════════
408
  # ADVANCED TABLE
409
  # ══════════════════════════════════════════════
 
417
  df_show["ML Score"] = df_show["ML Score"].map(lambda x: f"{x:.4f}")
418
  st.dataframe(df_show, use_container_width=True, hide_index=True)
419
 
 
420
  # ── Footer ──
421
  st.markdown("<br>", unsafe_allow_html=True)
422
  st.markdown(
youtube_auth.py CHANGED
@@ -39,28 +39,36 @@ def build_flow():
39
  return flow
40
 
41
  def get_credentials():
 
42
  if "google_creds" in st.session_state:
43
- creds = Credentials.from_authorized_user_info(
44
- st.session_state["google_creds"], SCOPES
45
- )
46
- if creds.valid:
47
- return creds
48
- if creds.expired and creds.refresh_token:
49
- creds.refresh(Request())
50
- st.session_state["google_creds"] = json.loads(creds.to_json())
51
- return creds
 
 
 
52
 
53
- params = st.query_params
 
54
  if "code" in params:
55
  try:
56
  flow = build_flow()
57
  flow.fetch_token(code=params["code"])
58
  creds = flow.credentials
 
59
  st.session_state["google_creds"] = json.loads(creds.to_json())
 
60
  st.query_params.clear()
61
  return creds
62
  except Exception as e:
63
- st.error(f"Login failed: {e}")
 
64
  return None
65
 
66
  return None
@@ -74,7 +82,6 @@ def show_login_button():
74
  return auth_url
75
 
76
  def logout():
77
- if "google_creds" in st.session_state:
78
- del st.session_state["google_creds"]
79
- if "user_info" in st.session_state:
80
- del st.session_state["user_info"]
 
39
  return flow
40
 
41
  def get_credentials():
42
+ # Already authenticated this session
43
  if "google_creds" in st.session_state:
44
+ try:
45
+ creds = Credentials.from_authorized_user_info(
46
+ st.session_state["google_creds"], SCOPES
47
+ )
48
+ if creds.valid:
49
+ return creds
50
+ if creds.expired and creds.refresh_token:
51
+ creds.refresh(Request())
52
+ st.session_state["google_creds"] = json.loads(creds.to_json())
53
+ return creds
54
+ except Exception:
55
+ del st.session_state["google_creds"]
56
 
57
+ # Check for OAuth callback code in URL
58
+ params = dict(st.query_params)
59
  if "code" in params:
60
  try:
61
  flow = build_flow()
62
  flow.fetch_token(code=params["code"])
63
  creds = flow.credentials
64
+ # Save BEFORE clearing params
65
  st.session_state["google_creds"] = json.loads(creds.to_json())
66
+ st.session_state["just_logged_in"] = True
67
  st.query_params.clear()
68
  return creds
69
  except Exception as e:
70
+ st.error(f"Login failed: {e}. Please try again.")
71
+ st.query_params.clear()
72
  return None
73
 
74
  return None
 
82
  return auth_url
83
 
84
  def logout():
85
+ for key in ["google_creds", "just_logged_in", "user_info"]:
86
+ if key in st.session_state:
87
+ del st.session_state[key]